Skip to content

Commit

Permalink
feat: change password form (#46)
Browse files Browse the repository at this point in the history
Co-authored-by: Guillaume Belanger <[email protected]>
  • Loading branch information
kayra1 and gruyaume committed Jul 29, 2024
1 parent cf69453 commit 79891c5
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 1 deletion.
1 change: 1 addition & 0 deletions ui/src/app/auth/authContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const AuthProvider = ({ children }: Readonly<{ children: React.ReactNode
const token = cookies.user_token;
if (token) {
let userObject = jwtDecode(cookies.user_token) as User
userObject.authToken = cookies.user_token
setUser(userObject);
} else {
setUser(null)
Expand Down
100 changes: 100 additions & 0 deletions ui/src/app/change_password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"use client"

import { changePassword } from "../queries"
import { useMutation } from "react-query"
import { useState, ChangeEvent } from "react"
import { useRouter } from "next/navigation"
import { useAuth } from "../auth/authContext"
import { passwordIsValid } from "../utils"

export default function ChangePasswordPage() {
const router = useRouter()
const auth = useAuth()
if (!auth.user) {
router.push('/login')
}

const mutation = useMutation(changePassword, {
onSuccess: (e) => {
setErrorText("")
router.push('/certificate_requests')
},
onError: (e: Error) => {
setErrorText(e.message)
}
})

const [password1, setPassword1] = useState<string>("")
const [password2, setPassword2] = useState<string>("")
const [showPassword1, setShowPassword1] = useState<boolean>(false)
const passwordsMatch = password1 === password2
const [errorText, setErrorText] = useState<string>("")
const handlePassword1Change = (event: ChangeEvent<HTMLInputElement>) => { setPassword1(event.target.value) }
const handlePassword2Change = (event: ChangeEvent<HTMLInputElement>) => { setPassword2(event.target.value) }
return (
<div style={{
display: "flex",
alignContent: "center",
justifyContent: "center",
flexWrap: "wrap",
height: "100vh",
}}>
<div className="p-panel" style={{
width: "45rem",
minWidth: "min-content",
minHeight: "min-content",
}}>
<div className="p-panel__content">
<div className="u-fixed-width">
<form className={"p-form-validation " + ( (!passwordIsValid(password1) && password1 != "") || (!passwordsMatch && password2 != "") ? "is-error" : "")}>
<fieldset>
<h4 className="p-panel__title">Change Password</h4>
<label className="p-form__label">New 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>
<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 New Password</label>
<input className="p-form-validation__input" type="password" id="InputPassword" name="password2" placeholder="******" autoComplete="current-password" onChange={handlePassword2Change} />
{!passwordIsValid(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(); mutation.mutate({ authToken: (auth.user ? auth.user.authToken : ""), username: (auth.user ? auth.user.username : ""), password: password1 }) }}>Submit</button>
)}
</fieldset>
</form>
</div>
</div>
</div >
</div >
)
}
4 changes: 3 additions & 1 deletion ui/src/app/login.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useState } from "react";
import { useAuth } from "./auth/authContext";
import { useCookies } from "react-cookie";
import { useRouter } from "next/navigation";

export function AccountTab() {
const router = useRouter()
const [cookies, setCookie, removeCookie] = useCookies(['user_token']);
const [menuOpen, setMenuOpen] = useState<boolean>(false)
const authDetails = useAuth()
Expand All @@ -19,7 +21,7 @@ export function AccountTab() {
<i className="p-icon--menu"></i>
<span className="p-contextual-menu__dropdown" id="menu-3" aria-hidden={!menuOpen} style={{ bottom: "40px" }}>
<span className="p-contextual-menu__group">
<button className="p-contextual-menu__link">Change Password</button>
<button className="p-contextual-menu__link" onMouseDown={() => router.push("/change_password")}>Change Password</button>
<button className="p-contextual-menu__link" onMouseDown={() => removeCookie("user_token")}>Log Out</button>
</span>
</span>
Expand Down
5 changes: 5 additions & 0 deletions ui/src/app/nav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ vi.mock('next/navigation', () => ({
usePathname: () => {
return "/certificate_requests"
},
useRouter: () => {
return {

}
}
}));

describe('Navigation', () => {
Expand Down
16 changes: 16 additions & 0 deletions ui/src/app/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,27 @@ export async function revokeCertificate(params: RequiredParams) {
export async function login(userForm: { username: string, password: string }) {
const response = await fetch("/login", {
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 changePassword(changePasswordForm: { authToken: string, username: string, password: string }) {
const response = await fetch("/api/v1/accounts/me/change_password", {
method: "POST",
headers: {
'Authorization': 'Bearer ' + changePasswordForm.authToken
},
body: JSON.stringify({ "username": changePasswordForm.username, "password": changePasswordForm.password })
})
const responseText = await response.text()
if (!response.ok) {
throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${responseText}`)
}
return responseText
}
1 change: 1 addition & 0 deletions ui/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export type User = {
id: number
permissions: number
username: string
authToken: string
}
30 changes: 30 additions & 0 deletions ui/src/app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,34 @@ export const HTTPStatus = (code: number): string => {
throw new Error("code not recognized: " + code)
}
return map[code]
}

export const passwordIsValid = (pw: string) => {
if (pw.length < 8) return false

const result = {
hasCapital: false,
hasLowercase: false,
hasSymbol: false,
hasNumber: false
};

if (/[A-Z]/.test(pw)) {
result.hasCapital = true;
}
if (/[a-z]/.test(pw)) {
result.hasLowercase = true;
}
if (/[0-9]/.test(pw)) {
result.hasNumber = true;
}
if (/[^A-Za-z0-9]/.test(pw)) {
result.hasSymbol = true;
}
console.log(result)

if (result.hasCapital && result.hasLowercase && (result.hasSymbol || result.hasNumber)) {
return true
}
return false
}

0 comments on commit 79891c5

Please sign in to comment.