Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: users table #48

Merged
merged 12 commits into from
Aug 1, 2024
85 changes: 10 additions & 75 deletions ui/src/app/aside.tsx
gruyaume marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,85 +1,20 @@
import { SetStateAction, Dispatch, useState, createContext, ChangeEvent } from "react"
import { useMutation, useQueryClient } from "react-query";
import { postCSR } from "./queries";
import { extractCSR } from "./utils";
import { useCookies } from "react-cookie";
import { SetStateAction, Dispatch, createContext, useContext, ComponentType } from "react"

type AsideContextType = {
isOpen: boolean,
setIsOpen: Dispatch<SetStateAction<boolean>>
}
export const AsideContext = createContext<AsideContextType>({ isOpen: false, setIsOpen: () => { } });

export function Aside({ isOpen, setIsOpen }: { isOpen: boolean, setIsOpen: Dispatch<SetStateAction<boolean>> }) {
const [cookies, setCookie, removeCookie] = useCookies(['user_token']);
const queryClient = useQueryClient()
const mutation = useMutation(postCSR, {
onSuccess: () => {
queryClient.invalidateQueries('csrs')
},
})
const [CSRPEMString, setCSRPEMString] = useState<string>("")
const handleTextChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setCSRPEMString(event.target.value);
}
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
if (e.target) {
if (e.target.result) {
setCSRPEMString(e.target.result.toString());
}
}
};
reader.readAsText(file);
}
};
export const AsideContext = createContext<AsideContextType>({
isOpen: false,
setIsOpen: () => { },
});

export function Aside({ FormComponent, formProps }: { FormComponent: React.ComponentType<any>, formProps: any }) {
const asideContext = useContext(AsideContext)
return (
<aside className={"l-aside" + (isOpen ? "" : " is-collapsed")} id="aside-panel" aria-label="aside-panel" >
<div className="p-panel">
<div className="p-panel__header">
<h4 className="p-panel__title">Add a New Certificate Request</h4>
<div className="p-panel__controls">
<button onClick={() => 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">
<form className="p-form p-form--stacked">
<div className="p-form__group row">
<label htmlFor="textarea">
Enter or upload the CSR in PEM format below
</label>
<textarea id="csr-textarea" name="textarea" rows={10} placeholder="-----BEGIN CERTIFICATE REQUEST-----" onChange={handleTextChange} value={CSRPEMString} />
</div>
<div className="p-form__group row">
<input type="file" name="upload" accept=".pem,.csr" onChange={handleFileChange}></input>
</div>
<div className="p-form__group row">
<SubmitCSR csrText={CSRPEMString} onClickFunc={() => mutation.mutate({ authToken: cookies.user_token, csr: CSRPEMString })} />
</div>
</form>
</div>
</div >
<aside className={"l-aside" + (asideContext.isOpen ? "" : " is-collapsed")} id="aside-panel" aria-label="aside-panel" >
<FormComponent {...formProps} />
</aside >
)
}

function SubmitCSR({ csrText, onClickFunc }: { csrText: string, onClickFunc: any }) {
let csrIsValid = false
try {
extractCSR(csrText.trim())
csrIsValid = true
}
catch { }

const validationComponent = csrText == "" ? <></> : csrIsValid ? <div><i className="p-icon--success"></i>Valid CSR</div> : <div><i className="p-icon--error"></i>Invalid CSR</div>
const buttonComponent = csrIsValid ? <button className="p-button--positive u-float-right" name="submit" onClick={onClickFunc} >Submit</button> : <button className="p-button--positive u-float-right" name="submit" disabled={true} onClick={onClickFunc} >Submit</button>
return (
<>
{validationComponent}
{buttonComponent}
</>
)
}
79 changes: 79 additions & 0 deletions ui/src/app/certificate_requests/asideForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useMutation, useQueryClient } from "react-query";
import { extractCSR } from "../utils";
import { useCookies } from "react-cookie";
import { postCSR } from "../queries";
import { ChangeEvent, useContext, useState } from "react";
import { AsideContext } from "../aside";

export default function UploadCSRAsidePanel(): JSX.Element {
const asideContext = useContext(AsideContext)
const [cookies, setCookie, removeCookie] = useCookies(['user_token']);
const queryClient = useQueryClient()
const mutation = useMutation(postCSR, {
onSuccess: () => {
queryClient.invalidateQueries('csrs')
},
})
const [CSRPEMString, setCSRPEMString] = useState<string>("")
const handleTextChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setCSRPEMString(event.target.value);
}
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
if (e.target) {
if (e.target.result) {
setCSRPEMString(e.target.result.toString());
}
}
};
reader.readAsText(file);
}
};
return (
<div className="p-panel" >
<div className="p-panel__header">
<h4 className="p-panel__title">Add a New Certificate Request</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">
<form className="p-form p-form--stacked">
<div className="p-form__group row">
<label htmlFor="textarea">
Enter or upload the CSR in PEM format below
</label>
<textarea id="csr-textarea" name="textarea" rows={10} placeholder="-----BEGIN CERTIFICATE REQUEST-----" onChange={handleTextChange} value={CSRPEMString} />
</div>
<div className="p-form__group row">
<input type="file" name="upload" accept=".pem,.csr" onChange={handleFileChange}></input>
</div>
<div className="p-form__group row">
<SubmitCSR csrText={CSRPEMString} onClickFunc={() => mutation.mutate({ authToken: cookies.user_token, csr: CSRPEMString })} />
</div>
</form>
</div>
</div >
)
}

function SubmitCSR({ csrText, onClickFunc }: { csrText: string, onClickFunc: any }) {
let csrIsValid = false
try {
extractCSR(csrText.trim())
csrIsValid = true
}
catch { }

const validationComponent = csrText == "" ? <></> : csrIsValid ? <div><i className="p-icon--success"></i>Valid CSR</div> : <div><i className="p-icon--error"></i>Invalid CSR</div>
const buttonComponent = csrIsValid ? <button className="p-button--positive u-float-right" name="submit" onClick={onClickFunc} >Submit</button> : <button className="p-button--positive u-float-right" name="submit" disabled={true} onClick={onClickFunc} >Submit</button>
return (
<>
{validationComponent}
{buttonComponent}
</>
)
}
4 changes: 2 additions & 2 deletions ui/src/app/certificate_requests/row.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, Dispatch, SetStateAction, useEffect, useRef } from "react"
import { UseMutationResult, useMutation, useQueryClient } from "react-query"
import { extractCSR, extractCert } from "../utils"
import { RequiredParams, deleteCSR, rejectCSR, revokeCertificate } from "../queries"
import { RequiredCSRParams, deleteCSR, rejectCSR, revokeCertificate } from "../queries"
import { ConfirmationModal, SubmitCertificateModal, SuccessNotification } from "./components"
import "./../globals.scss"
import { useCookies } from "react-cookie"
Expand Down Expand Up @@ -43,7 +43,7 @@ export default function Row({ id, csr, certificate, ActionMenuExpanded, setActio
const revokeMutation = useMutation(revokeCertificate, {
onSuccess: () => queryClient.invalidateQueries('csrs')
})
const mutationFunc = (mutation: UseMutationResult<any, unknown, RequiredParams, unknown>, params: RequiredParams) => {
const mutationFunc = (mutation: UseMutationResult<any, unknown, RequiredCSRParams, unknown>, params: RequiredCSRParams) => {
mutation.mutate(params)
}

Expand Down
4 changes: 2 additions & 2 deletions ui/src/app/change_password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function ChangePasswordPage() {
}}>
<div className="p-panel__content">
<div className="u-fixed-width">
<form className={"p-form-validation " + ( (!passwordIsValid(password1) && password1 != "") || (!passwordsMatch && password2 != "") ? "is-error" : "")}>
<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>
Expand All @@ -73,7 +73,7 @@ export default function ChangePasswordPage() {
</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) && 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">
Expand Down
50 changes: 34 additions & 16 deletions ui/src/app/nav.tsx
gruyaume marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import Image from "next/image";
import { Aside, AsideContext } from "./aside";
import { AccountTab } from "./login"
import { usePathname } from "next/navigation";
import { useAuth } from "./auth/authContext";
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()
return (
<header className={sidebarVisible ? "l-navigation" : "l-navigation is-collapsed"}>
<div className="l-navigation__drawer">
Expand All @@ -31,6 +35,16 @@ export function SideBar({ activePath, sidebarVisible, setSidebarVisible }: { act
</span>
</a>
</li>
{auth.user?.permissions == 1 &&
<li className="p-side-navigation__item">
<a className="p-side-navigation__link" href="/users" aria-current={activePath.startsWith("/users") ? "page" : "false"} style={{ cursor: "pointer" }}>
<i className="p-icon--user is-light p-side-navigation__icon"></i>
<span className="p-side-navigation__label">
<span className="p-side-navigation__label">Users</span>
</span>
</a>
</li>
}
</ul>
<ul className="p-side-navigation__list" style={{ bottom: 0, position: "absolute", width: "100%" }}>
<li className="p-side-navigation__item" >
Expand Down Expand Up @@ -91,25 +105,29 @@ export default function Navigation({
const shouldRenderNavigation = !noNavRoutes.includes(activePath);
const [sidebarVisible, setSidebarVisible] = useState<boolean>(true)
const [asideOpen, setAsideOpen] = useState<boolean>(false)
let asideForm = UploadCSRAsidePanel
if (activePath == "/users") {
asideForm = UploadUserAsidePanel
}
return (
<QueryClientProvider client={queryClient}>
<div className="l-application" role="presentation">
{
shouldRenderNavigation ? (
<>
<TopBar setSidebarVisible={setSidebarVisible} />
<SideBar activePath={activePath} sidebarVisible={sidebarVisible} setSidebarVisible={setSidebarVisible} />
</>
) : (
<></>
)
}
<main className="l-main">
<AsideContext.Provider value={{ isOpen: asideOpen, setIsOpen: setAsideOpen }}>
{children}
</AsideContext.Provider>
</main>
<Aside isOpen={asideOpen} setIsOpen={setAsideOpen} />
<AsideContext.Provider value={{ isOpen: asideOpen, setIsOpen: setAsideOpen }}>
{
shouldRenderNavigation ? (
<>
<TopBar setSidebarVisible={setSidebarVisible} />
<SideBar activePath={activePath} sidebarVisible={sidebarVisible} setSidebarVisible={setSidebarVisible} />
</>
) : (
<></>
)
}
<main className="l-main">
{children}
</main>
<Aside FormComponent={asideForm} formProps={null} />
</AsideContext.Provider>
</div >
</QueryClientProvider>
)
Expand Down
47 changes: 41 additions & 6 deletions ui/src/app/queries.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CSREntry } from "./types"
import { CSREntry, UserEntry } from "./types"
import { HTTPStatus } from "./utils"

export type RequiredParams = {
export type RequiredCSRParams = {
id: string
authToken: string
csr?: string
Expand Down Expand Up @@ -36,7 +36,7 @@ export async function postCSR(params: { authToken: string, csr: string }) {
return response.json()
}

export async function postCertToID(params: RequiredParams) {
export async function postCertToID(params: RequiredCSRParams) {
if (!params.cert) {
throw new Error('Certificate not provided')
}
Expand All @@ -54,7 +54,7 @@ export async function postCertToID(params: RequiredParams) {
return response.json()
}

export async function deleteCSR(params: RequiredParams) {
export async function deleteCSR(params: RequiredCSRParams) {
const response = await fetch("/api/v1/certificate_requests/" + params.id, {
method: 'delete',
headers: {
Expand All @@ -67,7 +67,7 @@ export async function deleteCSR(params: RequiredParams) {
return response.json()
}

export async function rejectCSR(params: RequiredParams) {
export async function rejectCSR(params: RequiredCSRParams) {
const response = await fetch("/api/v1/certificate_requests/" + params.id + "/certificate/reject", {
method: 'post',
headers: {
Expand All @@ -80,7 +80,7 @@ export async function rejectCSR(params: RequiredParams) {
return response.json()
}

export async function revokeCertificate(params: RequiredParams) {
export async function revokeCertificate(params: RequiredCSRParams) {
const response = await fetch("/api/v1/certificate_requests/" + params.id + "/certificate/reject", {
method: 'post',
headers: {
Expand Down Expand Up @@ -119,4 +119,39 @@ export async function changePassword(changePasswordForm: { authToken: string, us
throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${responseText}`)
}
return responseText
}

export async function getUsers(params: { authToken: string }): Promise<UserEntry[]> {
const response = await fetch("/api/v1/accounts", {
headers: { "Authorization": "Bearer " + params.authToken }
})
if (!response.ok) {
throw new Error(`${response.status}: ${HTTPStatus(response.status)}`)
}
return response.json()
}

export async function deleteUser(params: { authToken: string, id: string }) {
const response = await fetch("/api/v1/accounts/" + params.id, {
method: 'delete',
headers: {
'Authorization': "Bearer " + params.authToken
}
})
if (!response.ok) {
throw new Error(`${response.status}: ${HTTPStatus(response.status)}`)
}
return response.json()
}

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 })
})
const responseText = await response.text()
if (!response.ok) {
throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${responseText}`)
}
return responseText
}
5 changes: 5 additions & 0 deletions ui/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ export type User = {
permissions: number
username: string
authToken: string
}

export type UserEntry = {
id: number
username: string
}
Loading
Loading