Skip to content

Commit

Permalink
allow admin to change everyones passwords
Browse files Browse the repository at this point in the history
  • Loading branch information
kayra1 committed Aug 1, 2024
1 parent 2f856ad commit 582b3b9
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 71 deletions.
4 changes: 3 additions & 1 deletion ui/src/app/aside.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SetStateAction, Dispatch, createContext, useContext, ComponentType } from "react"
import { useAuth } from "./auth/authContext"

type AsideContextType = {
isOpen: boolean,
Expand All @@ -17,9 +18,10 @@ export const AsideContext = createContext<AsideContextType>({
})

export function Aside({ FormComponent }: { FormComponent: React.ComponentType<any> }) {
const auth = useAuth()
const asideContext = useContext(AsideContext)
return (
<aside className={"l-aside" + (asideContext.isOpen ? "" : " is-collapsed")} id="aside-panel" aria-label="aside-panel" >
<aside className={"l-aside" + (auth.user && asideContext.isOpen ? "" : " is-collapsed")} id="aside-panel" aria-label="aside-panel" >
<FormComponent />
</aside >
)
Expand Down
1 change: 1 addition & 0 deletions ui/src/app/certificate_requests/asideForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default function CertificateRequestsAsidePanel(): JSX.Element {
const queryClient = useQueryClient()
const mutation = useMutation(postCSR, {
onSuccess: () => {
asideContext.setIsOpen(false)
queryClient.invalidateQueries('csrs')
},
})
Expand Down
9 changes: 5 additions & 4 deletions ui/src/app/change_password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use client"

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

export default function ChangePasswordPage() {
const router = useRouter()
Expand All @@ -14,7 +15,7 @@ export default function ChangePasswordPage() {
router.push('/login')
}

const mutation = useMutation(changePassword, {
const mutation = useMutation(changeSelfPassword, {
onSuccess: (e) => {
setErrorText("")
router.push('/certificate_requests')
Expand Down Expand Up @@ -88,7 +89,7 @@ export default function ChangePasswordPage() {
<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>
<button className="p-button--positive" type="submit" name="submit" onClick={(event) => { event.preventDefault(); mutation.mutate({ authToken: (auth.user ? auth.user.authToken : ""), password: password1 }) }}>Submit</button>
)}
</fieldset>
</form>
Expand Down
8 changes: 4 additions & 4 deletions ui/src/app/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Aside, AsideContext } from "./aside";
import { AccountTab } from "./login"
import { usePathname } from "next/navigation";
import { useAuth } from "./auth/authContext";
import CertificateRequestsAsidePanel from "./certificate_requests/asideForm";
import UsersPageAsidePanel from "./users/asideForm";
import UploadCSRAsidePanel from "./certificate_requests/asideForm";
import UploadUserAsidePanel from "./users/asideForm";

export function SideBar({ activePath, sidebarVisible, setSidebarVisible }: { activePath: string, sidebarVisible: boolean, setSidebarVisible: Dispatch<SetStateAction<boolean>> }) {
const auth = useAuth()
Expand Down Expand Up @@ -106,9 +106,9 @@ export default function Navigation({
const [sidebarVisible, setSidebarVisible] = useState<boolean>(true)
const [asideOpen, setAsideOpen] = useState<boolean>(false)
const [asideData, setAsideData] = useState<any>(null)
let asideForm = CertificateRequestsAsidePanel
let asideForm = UploadCSRAsidePanel
if (activePath == "/users") {
asideForm = UsersPageAsidePanel
asideForm = UploadUserAsidePanel
}
return (
<QueryClientProvider client={queryClient}>
Expand Down
19 changes: 17 additions & 2 deletions ui/src/app/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,28 @@ export async function login(userForm: { username: string, password: string }) {
return responseText
}

export async function changePassword(changePasswordForm: { authToken: string, username: string, password: string }) {
export async function changeSelfPassword(changePasswordForm: { authToken: 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 })
body: JSON.stringify({ "password": changePasswordForm.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, id: string, password: string }) {
const response = await fetch("/api/v1/accounts/" + changePasswordForm.id + "/change_password", {
method: "POST",
headers: {
'Authorization': 'Bearer ' + changePasswordForm.authToken
},
body: JSON.stringify({ "password": changePasswordForm.password })
})
const responseText = await response.text()
if (!response.ok) {
Expand Down
199 changes: 144 additions & 55 deletions ui/src/app/users/asideForm.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
import { useMutation, useQueryClient } from "react-query";
import { extractCSR, passwordIsValid } from "../utils";
import { postUser } from "../queries";
import { passwordIsValid } from "../utils";
import { changePassword, postUser } from "../queries";
import { ChangeEvent, useContext, useState } from "react";
import { AsideContext } from "../aside";
import { useAuth } from "../auth/authContext";

export default function UsersPageAsidePanel() {
const asideContext = useContext(AsideContext)
return (
<div className="p-panel" >
<div className="p-panel__header">
{asideContext.extraData == null ? (
<h4 className="p-panel__title">Add a New User</h4>
) : (
<h4 className="p-panel__title">Change User Password</h4>
)}
<div className="p-panel__controls">
<button onClick={() => asideContext.setIsOpen(false)} className="p-button--base u-no-margin--bottom has-icon"><i className="p-icon--close"></i></button>
</div>
</div>
<div className="p-panel__content">
{asideContext.extraData == null ? (
<AddNewUserForm />
) : (
<ChangePasswordForm />
)}
</div>
</div >
)
}


function AddNewUserForm() {
const auth = useAuth()
const queryClient = useQueryClient()
const asideContext = useContext(AsideContext)
const mutation = useMutation(postUser, {
onSuccess: () => {
queryClient.invalidateQueries('users')
setErrorText("")
asideContext.setIsOpen(false)
},
onError: (e: Error) => {
setErrorText(e.message)
Expand All @@ -28,65 +55,127 @@ export default function UsersPageAsidePanel() {
const handlePassword1Change = (event: ChangeEvent<HTMLInputElement>) => { setPassword1(event.target.value) }
const handlePassword2Change = (event: ChangeEvent<HTMLInputElement>) => { setPassword2(event.target.value) }
return (
<div className="p-panel" >
<div className="p-panel__header">
<h4 className="p-panel__title">Add a New User</h4>
<div className="p-panel__controls">
<button onClick={() => asideContext.setIsOpen(false)} className="p-button--base u-no-margin--bottom has-icon"><i className="p-icon--close"></i></button>
<form className={"p-form-validation " + ((!passwordIsValid(password1) && password1 != "") || (!passwordsMatch && password2 != "") ? "is-error" : "")}>
<div className="p-form__group row">
<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>
</div>
<div className="p-panel__content">
<form className={"p-form-validation " + ((!passwordIsValid(password1) && password1 != "") || (!passwordsMatch && password2 != "") ? "is-error" : "")}>
<div className="p-form__group row">
<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>
<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>
<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) ? (
</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: username, password: password1 }) }}>Submit</button>
)
}
</div>
</form>
)
}

function ChangePasswordForm() {
const auth = useAuth()
const asideContext = useContext(AsideContext)
const queryClient = useQueryClient()
const mutation = useMutation(changePassword, {
onSuccess: () => {
queryClient.invalidateQueries('users')
setErrorText("")
asideContext.setIsOpen(false)
},
onError: (e: Error) => {
setErrorText(e.message)
}
})
const [showPassword1, setShowPassword1] = useState<boolean>(false)
const [password1, setPassword1] = useState<string>("")
const [password2, setPassword2] = useState<string>("")
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 (
<form className={"p-form-validation " + ((!passwordIsValid(password1) && password1 != "") || (!passwordsMatch && password2 != "") ? "is-error" : "")}>
<div className="p-form__group row">
<label className="p-form__label">Username</label>
<input type="text" id="InputUsername" name="InputUsername" value={asideContext.extraData["user"]["username"]} disabled={true} />
<div>
<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 ? (
<>
<button className="p-button--positive" type="submit" name="submit" disabled={true}>Submit</button>
<span className="p-form-password-toggle__label">
Hide
</span>
<i className="p-icon--hide"></i>
</>
) : (
<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>
)
}
<>
<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 New 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>
</form>
}
{!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 : ""), id: asideContext.extraData["user"]["id"], password: password1 }) }}>Submit</button>
)}
</div>
</div >
</form>
)
}
2 changes: 1 addition & 1 deletion ui/src/app/users/row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function Row({ id, username, ActionMenuExpanded, setActionMenuExp
})
}
const handleChangePassword = () => {
asideContext.setExtraData({"username":username})
asideContext.setExtraData({ "user": { "id": id, "username": username } })
asideContext.setIsOpen(true)
}

Expand Down
Loading

0 comments on commit 582b3b9

Please sign in to comment.