mirror of
https://github.com/enso-org/enso.git
synced 2024-11-24 00:27:16 +03:00
Cognito auth 5/7 - set username (#5866)
5th PR for IDE/Cloud authorization with cognito. This PR introduces user username templates + flows + backend wrappers for setting username. Forgot Password flows are to be added in next PRs to keep the changes reviewable.
This commit is contained in:
parent
8125d89676
commit
8c18d7c106
@ -0,0 +1,71 @@
|
|||||||
|
/** @file Container responsible for rendering and interactions in setting username flow, after
|
||||||
|
* registration. */
|
||||||
|
|
||||||
|
import * as auth from "../providers/auth";
|
||||||
|
import * as hooks from "../../hooks";
|
||||||
|
import * as icons from "../../components/svg";
|
||||||
|
import * as utils from "../../utils";
|
||||||
|
|
||||||
|
// ===================
|
||||||
|
// === SetUsername ===
|
||||||
|
// ===================
|
||||||
|
|
||||||
|
function SetUsername() {
|
||||||
|
const { setUsername } = auth.useAuth();
|
||||||
|
const { accessToken, email } = auth.usePartialUserSession();
|
||||||
|
|
||||||
|
const [username, bindUsername] = hooks.useInput("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-300">
|
||||||
|
<div className="flex flex-col bg-white shadow-md px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md w-full max-w-md">
|
||||||
|
<div className="font-medium self-center text-xl sm:text-2xl uppercase text-gray-800">
|
||||||
|
Set your username
|
||||||
|
</div>
|
||||||
|
<div className="mt-10">
|
||||||
|
<form
|
||||||
|
onSubmit={utils.handleEvent(() =>
|
||||||
|
setUsername(accessToken, username, email)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col mb-6">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="inline-flex items-center justify-center absolute left-0 top-0 h-full w-10 text-gray-400">
|
||||||
|
<icons.Svg data={icons.PATHS.at} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
{...bindUsername}
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
className={
|
||||||
|
"text-sm sm:text-base placeholder-gray-500 pl-10 pr-4 rounded-lg border border-gray-400 " +
|
||||||
|
"w-full py-2 focus:outline-none focus:border-blue-400"
|
||||||
|
}
|
||||||
|
placeholder="Username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={
|
||||||
|
"flex items-center justify-center focus:outline-none text-white text-sm sm:text-base bg-blue-600 " +
|
||||||
|
"hover:bg-blue-700 rounded py-2 w-full transition duration-150 ease-in"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="mr-2 uppercase">Set username</span>
|
||||||
|
<span>
|
||||||
|
<icons.Svg data={icons.PATHS.rightArrow} />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SetUsername;
|
@ -21,6 +21,7 @@ import * as sessionProvider from "./session";
|
|||||||
const MESSAGES = {
|
const MESSAGES = {
|
||||||
signUpSuccess: "We have sent you an email with further instructions!",
|
signUpSuccess: "We have sent you an email with further instructions!",
|
||||||
confirmSignUpSuccess: "Your account has been confirmed! Please log in.",
|
confirmSignUpSuccess: "Your account has been confirmed! Please log in.",
|
||||||
|
setUsernameSuccess: "Your username has been set!",
|
||||||
signInWithPasswordSuccess: "Successfully logged in!",
|
signInWithPasswordSuccess: "Successfully logged in!",
|
||||||
pleaseWait: "Please wait...",
|
pleaseWait: "Please wait...",
|
||||||
} as const;
|
} as const;
|
||||||
@ -76,6 +77,11 @@ export interface PartialUserSession {
|
|||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
signUp: (email: string, password: string) => Promise<void>;
|
signUp: (email: string, password: string) => Promise<void>;
|
||||||
confirmSignUp: (email: string, code: string) => Promise<void>;
|
confirmSignUp: (email: string, code: string) => Promise<void>;
|
||||||
|
setUsername: (
|
||||||
|
accessToken: string,
|
||||||
|
username: string,
|
||||||
|
email: string
|
||||||
|
) => Promise<void>;
|
||||||
signInWithGoogle: () => Promise<void>;
|
signInWithGoogle: () => Promise<void>;
|
||||||
signInWithGitHub: () => Promise<void>;
|
signInWithGitHub: () => Promise<void>;
|
||||||
signInWithPassword: (email: string, password: string) => Promise<void>;
|
signInWithPassword: (email: string, password: string) => Promise<void>;
|
||||||
@ -232,9 +238,30 @@ export function AuthProvider(props: AuthProviderProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setUsername = async (
|
||||||
|
accessToken: string,
|
||||||
|
username: string,
|
||||||
|
email: string
|
||||||
|
) => {
|
||||||
|
const body: backendService.SetUsernameRequestBody = {
|
||||||
|
userName: username,
|
||||||
|
userEmail: email,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** TODO [NP]: https://github.com/enso-org/cloud-v2/issues/343
|
||||||
|
* The API client is reinitialised on every request. That is an inefficient way of usage.
|
||||||
|
* Fix it by using React context and implementing it as a singleton. */
|
||||||
|
const backend = backendService.createBackend(accessToken, logger);
|
||||||
|
|
||||||
|
await backend.setUsername(body);
|
||||||
|
navigate(app.DASHBOARD_PATH);
|
||||||
|
toast.success(MESSAGES.setUsernameSuccess);
|
||||||
|
};
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
signUp: withLoadingToast(signUp),
|
signUp: withLoadingToast(signUp),
|
||||||
confirmSignUp: withLoadingToast(confirmSignUp),
|
confirmSignUp: withLoadingToast(confirmSignUp),
|
||||||
|
setUsername,
|
||||||
signInWithGoogle: cognito.signInWithGoogle.bind(cognito),
|
signInWithGoogle: cognito.signInWithGoogle.bind(cognito),
|
||||||
signInWithGitHub: cognito.signInWithGitHub.bind(cognito),
|
signInWithGitHub: cognito.signInWithGitHub.bind(cognito),
|
||||||
signInWithPassword: withLoadingToast(signInWithPassword),
|
signInWithPassword: withLoadingToast(signInWithPassword),
|
||||||
@ -298,13 +325,23 @@ export function ProtectedLayout() {
|
|||||||
export function GuestLayout() {
|
export function GuestLayout() {
|
||||||
const { session } = useAuth();
|
const { session } = useAuth();
|
||||||
|
|
||||||
if (session?.variant === "full") {
|
if (session?.variant === "partial") {
|
||||||
|
return <router.Navigate to={app.SET_USERNAME_PATH} />;
|
||||||
|
} else if (session?.variant === "full") {
|
||||||
return <router.Navigate to={app.DASHBOARD_PATH} />;
|
return <router.Navigate to={app.DASHBOARD_PATH} />;
|
||||||
} else {
|
} else {
|
||||||
return <router.Outlet />;
|
return <router.Outlet />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================
|
||||||
|
// === usePartialUserSession ===
|
||||||
|
// =============================
|
||||||
|
|
||||||
|
export function usePartialUserSession() {
|
||||||
|
return router.useOutletContext<PartialUserSession>();
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================
|
// ==========================
|
||||||
// === useFullUserSession ===
|
// === useFullUserSession ===
|
||||||
// ==========================
|
// ==========================
|
||||||
|
@ -47,6 +47,7 @@ import ConfirmRegistration from "../authentication/components/confirmRegistratio
|
|||||||
import Dashboard from "../dashboard/components/dashboard";
|
import Dashboard from "../dashboard/components/dashboard";
|
||||||
import Login from "../authentication/components/login";
|
import Login from "../authentication/components/login";
|
||||||
import Registration from "../authentication/components/registration";
|
import Registration from "../authentication/components/registration";
|
||||||
|
import SetUsername from "../authentication/components/setUsername";
|
||||||
|
|
||||||
// =================
|
// =================
|
||||||
// === Constants ===
|
// === Constants ===
|
||||||
@ -60,6 +61,8 @@ export const LOGIN_PATH = "/login";
|
|||||||
export const REGISTRATION_PATH = "/registration";
|
export const REGISTRATION_PATH = "/registration";
|
||||||
/** Path to the confirm registration page. */
|
/** Path to the confirm registration page. */
|
||||||
export const CONFIRM_REGISTRATION_PATH = "/confirmation";
|
export const CONFIRM_REGISTRATION_PATH = "/confirmation";
|
||||||
|
/** Path to the set username page. */
|
||||||
|
export const SET_USERNAME_PATH = "/set-username";
|
||||||
|
|
||||||
// ===========
|
// ===========
|
||||||
// === App ===
|
// === App ===
|
||||||
@ -144,6 +147,10 @@ function AppRouter(props: AppProps) {
|
|||||||
{/* Protected pages are visible to authenticated users. */}
|
{/* Protected pages are visible to authenticated users. */}
|
||||||
<router.Route element={<authProvider.ProtectedLayout />}>
|
<router.Route element={<authProvider.ProtectedLayout />}>
|
||||||
<router.Route path={DASHBOARD_PATH} element={<Dashboard />} />
|
<router.Route path={DASHBOARD_PATH} element={<Dashboard />} />
|
||||||
|
<router.Route
|
||||||
|
path={SET_USERNAME_PATH}
|
||||||
|
element={<SetUsername />}
|
||||||
|
/>
|
||||||
</router.Route>
|
</router.Route>
|
||||||
{/* Other pages are visible to unauthenticated and authenticated users. */}
|
{/* Other pages are visible to unauthenticated and authenticated users. */}
|
||||||
<router.Route
|
<router.Route
|
||||||
|
@ -10,6 +10,8 @@ import * as loggerProvider from "../providers/logger";
|
|||||||
// === Constants ===
|
// === Constants ===
|
||||||
// =================
|
// =================
|
||||||
|
|
||||||
|
/** Relative HTTP path to the "set username" endpoint of the Cloud backend API. */
|
||||||
|
const SET_USER_NAME_PATH = "users";
|
||||||
/** Relative HTTP path to the "get user" endpoint of the Cloud backend API. */
|
/** Relative HTTP path to the "get user" endpoint of the Cloud backend API. */
|
||||||
const GET_USER_PATH = "users/me";
|
const GET_USER_PATH = "users/me";
|
||||||
|
|
||||||
@ -24,6 +26,12 @@ export interface Organization {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** HTTP request body for the "set username" endpoint. */
|
||||||
|
export interface SetUsernameRequestBody {
|
||||||
|
userName: string;
|
||||||
|
userEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ===============
|
// ===============
|
||||||
// === Backend ===
|
// === Backend ===
|
||||||
// ===============
|
// ===============
|
||||||
@ -65,6 +73,13 @@ export class Backend {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the username of the current user, on the Cloud backend API. */
|
||||||
|
setUsername(body: SetUsernameRequestBody): Promise<Organization> {
|
||||||
|
return this.post<Organization>(SET_USER_NAME_PATH, body).then((response) =>
|
||||||
|
response.json()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns organization info for the current user, from the Cloud backend API.
|
/** Returns organization info for the current user, from the Cloud backend API.
|
||||||
*
|
*
|
||||||
* @returns `null` if status code 401 or 404 was received. */
|
* @returns `null` if status code 401 or 404 was received. */
|
||||||
|
Loading…
Reference in New Issue
Block a user