Skip to content

Commit

Permalink
feat: onboarding flow for the frontend (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
kayra1 committed Aug 2, 2024
1 parent 8c3383f commit f77697b
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 10 deletions.
10 changes: 7 additions & 3 deletions ui/src/app/auth/authContext.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
"use client"

import { createContext, useContext, useState, useEffect } from 'react';
import { createContext, useContext, useState, useEffect, Dispatch, SetStateAction } from 'react';
import { User } from '../types';
import { useCookies } from 'react-cookie';
import { jwtDecode } from 'jwt-decode';
import { useRouter } from 'next/navigation';

type AuthContextType = {
user: User | null
firstUserCreated: boolean
setFirstUserCreated: Dispatch<SetStateAction<boolean>>
}

const AuthContext = createContext<AuthContextType>({ user: null });
const AuthContext = createContext<AuthContextType>({ user: null, firstUserCreated: false, setFirstUserCreated: () => { } });

export const AuthProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
const [cookies, setCookie, removeCookie] = useCookies(['user_token']);
const [user, setUser] = useState<User | null>(null);
const [firstUserCreated, setFirstUserCreated] = useState<boolean>(false)
const router = useRouter();

useEffect(() => {
Expand All @@ -23,14 +26,15 @@ export const AuthProvider = ({ children }: Readonly<{ children: React.ReactNode
let userObject = jwtDecode(cookies.user_token) as User
userObject.authToken = cookies.user_token
setUser(userObject);
setFirstUserCreated(true)
} else {
setUser(null)
router.push('/login');
}
}, [cookies.user_token, router]);

return (
<AuthContext.Provider value={{ user }}>
<AuthContext.Provider value={{ user, firstUserCreated, setFirstUserCreated }}>
{children}
</AuthContext.Provider>
);
Expand Down
138 changes: 138 additions & 0 deletions ui/src/app/initialize/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"use client"

import { useState, ChangeEvent } from "react"
import { getStatus, login, postFirstUser } from "../queries"
import { useMutation, useQuery } from "react-query"
import { useRouter } from "next/navigation"
import { passwordIsValid } from "../utils"
import { useAuth } from "../auth/authContext"
import { Logo } from "../nav"
import { useCookies } from "react-cookie"
import { statusResponse } from "../types"


export default function Initialize() {
const router = useRouter()
const auth = useAuth()
const [cookies, setCookie, removeCookie] = useCookies(['user_token']);
const statusQuery = useQuery<statusResponse, Error>({
queryFn: () => getStatus()
})
if (statusQuery.data && statusQuery.data.initialized) {
auth.setFirstUserCreated(true)
router.push("/login")
}
const loginMutation = useMutation(login, {
onSuccess: (e) => {
setErrorText("")
setCookie('user_token', e, {
sameSite: true,
secure: true,
expires: new Date(new Date().getTime() + 60 * 60 * 1000),
})
router.push('/certificate_requests')
},
onError: (e: Error) => {
setErrorText(e.message)
}
})
const postUserMutation = useMutation(postFirstUser, {
onSuccess: () => {
setErrorText("")
auth.setFirstUserCreated(true)
loginMutation.mutate({ username: username, password: password1 })
},
onError: (e: Error) => {
setErrorText(e.message)
}
})
const [username, setUsername] = useState<string>("")
const [password1, setPassword1] = useState<string>("")
const [password2, setPassword2] = useState<string>("")
const passwordsMatch = password1 === password2
const [errorText, setErrorText] = useState<string>("")
const [showPassword1, setShowPassword1] = useState<boolean>(false)
const handleUsernameChange = (event: ChangeEvent<HTMLInputElement>) => { setUsername(event.target.value) }
const handlePassword1Change = (event: ChangeEvent<HTMLInputElement>) => { setPassword1(event.target.value) }
const handlePassword2Change = (event: ChangeEvent<HTMLInputElement>) => { setPassword2(event.target.value) }
return (
<>
<div style={{ backgroundColor: "#262626", height: "6.5vh" }}>
<div style={{ marginLeft: "30px" }}>
<Logo />
</div>
</div>
<div style={{
display: "flex",
alignContent: "center",
justifyContent: "center",
flexWrap: "wrap",
height: "93.5vh",
}}>
<div className="p-panel" style={{
width: "45rem",
minWidth: "min-content",
minHeight: "min-content",
}}>
<fieldset>
<div className="p-panel__header">
<h2>Initialize GoCert</h2>
</div>
<div className="p-panel__content">
<form className={"p-form-validation " + ((!passwordIsValid(password1) && password1 != "") || (!passwordsMatch && password2 != "") ? "is-error" : "")}>
<div className="p-form__group row">
<h4>Create the initial admin user</h4>
<label className="p-form__label">Username</label>
<input type="text" id="InputUsername" name="InputUsername" onChange={handleUsernameChange} />
<div>
<label className="p-form__label">Password</label>
<button className="p-button--base u-no-margin--bottom has-icon" style={{ float: "right" }} aria-live="polite" aria-controls="password" onClick={(e) => { e.preventDefault(); setShowPassword1(!showPassword1) }}>
{showPassword1 ? (
<>
<span className="p-form-password-toggle__label">
Hide
</span>
<i className="p-icon--hide"></i>
</>
) : (
<>
<span className="p-form-password-toggle__label">
Show
</span>
<i className="p-icon--show"></i>
</>
)}
</button>
</div>
<input className="p-form-validation__input" type={showPassword1 ? "text" : "password"} id="password1" name="password" placeholder="******" autoComplete="current-password" required={true} onChange={handlePassword1Change} />
<p className="p-form-help-text">
Password must have 8 or more characters, must include at least one capital letter, one lowercase letter, and either a number or a symbol.
</p>
<label htmlFor="p-form__label">Confirm Password</label>
<input className="p-form-validation__input" type="password" id="InputPassword" name="password2" placeholder="******" autoComplete="current-password" onChange={handlePassword2Change} />
{!passwordIsValid(password1) && password1 != "" && <p className="p-form-validation__message">Password is not valid</p>}
{passwordIsValid(password1) && !passwordsMatch && password2 != "" && <p className="p-form-validation__message">Passwords do not match</p>}
{errorText &&
<div className="p-notification--negative">
<div className="p-notification__content">
<h5 className="p-notification__title">Error</h5>
<p className="p-notification__message">{errorText.split("error: ")}</p>
</div>
</div>
}
{!passwordsMatch || !passwordIsValid(password1) ? (
<>
<button className="p-button--positive" type="submit" name="submit" disabled={true}>Submit</button>
</>
) : (
<button className="p-button--positive" type="submit" name="submit" onClick={(event) => { event.preventDefault(); postUserMutation.mutate({ username: username, password: password1 }) }}>Submit</button>
)}
</div>
</form>
</div>
</fieldset>
</div >
</div>
</>
)
}
14 changes: 12 additions & 2 deletions ui/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
"use client"

import { login } from "../queries"
import { useMutation } from "react-query"
import { getStatus, login } from "../queries"
import { useMutation, useQuery } from "react-query"
import { useState, ChangeEvent } from "react"
import { useCookies } from "react-cookie"
import { useRouter } from "next/navigation"
import { useAuth } from "../auth/authContext"
import { statusResponse } from "../types"


export default function LoginPage() {
const router = useRouter()
const auth = useAuth()
const [cookies, setCookie, removeCookie] = useCookies(['user_token']);
const statusQuery = useQuery<statusResponse, Error>({
queryFn: () => getStatus()
})
if (!auth.firstUserCreated && (statusQuery.data && !statusQuery.data.initialized)) {
router.push("/initialize")
}
const mutation = useMutation(login, {
onSuccess: (e) => {
setErrorText("")
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export default function Navigation({
children: React.ReactNode;
}>) {
const activePath = usePathname()
const noNavRoutes = ['/login'];
const noNavRoutes = ['/login', '/initialize'];

const shouldRenderNavigation = !noNavRoutes.includes(activePath);
const [sidebarVisible, setSidebarVisible] = useState<boolean>(true)
Expand Down
26 changes: 24 additions & 2 deletions ui/src/app/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export type RequiredCSRParams = {
cert?: string
}

export async function getStatus() {
const response = await fetch("/status")
if (!response.ok) {
throw new Error(`${response.status}: ${HTTPStatus(response.status)}`)
}
return response.json()
}

export async function getCertificateRequests(params: { authToken: string }): Promise<CSREntry[]> {
const response = await fetch("/api/v1/certificate_requests", {
headers: { "Authorization": "Bearer " + params.authToken }
Expand Down Expand Up @@ -159,13 +167,27 @@ export async function deleteUser(params: { authToken: string, id: string }) {
return response.json()
}

export async function postFirstUser(userForm: { username: string, password: string }) {
const response = await fetch("/api/v1/accounts", {
method: "POST",
body: JSON.stringify({ "username": userForm.username, "password": userForm.password })
})
const responseText = await response.text()
if (!response.ok) {
throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${responseText}`)
}
return responseText
}

export async function postUser(userForm: { authToken: string, username: string, password: string }) {
const response = await fetch("/api/v1/accounts", {
method: "POST",
body: JSON.stringify({
"username": userForm.username, "password": userForm.password
}),
headers: {
'Authorization': "Bearer " + userForm.authToken
},
body: JSON.stringify({ "username": userForm.username, "password": userForm.password })
}
})
const responseText = await response.text()
if (!response.ok) {
Expand Down
4 changes: 4 additions & 0 deletions ui/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ export type User = {
export type UserEntry = {
id: number
username: string
}

export type statusResponse = {
initialized: boolean
}
3 changes: 1 addition & 2 deletions ui/src/app/users/asideForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ function AddNewUserForm() {
</>
) : (
<button className="p-button--positive" type="submit" name="submit" onClick={(event) => { event.preventDefault(); mutation.mutate({ authToken: (auth.user ? auth.user.authToken : ""), username: username, password: password1 }) }}>Submit</button>
)
}
)}
</div>
</form>
)
Expand Down

0 comments on commit f77697b

Please sign in to comment.