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:
Nikita Pekin 2023-03-20 07:57:27 -04:00 committed by GitHub
parent 8125d89676
commit 8c18d7c106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 131 additions and 1 deletions

View File

@ -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;

View File

@ -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 ===
// ========================== // ==========================

View File

@ -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

View File

@ -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. */