From fb02d36bf78341657cc02955eb59e0cd8e383220 Mon Sep 17 00:00:00 2001 From: kayra1 Date: Mon, 25 Nov 2024 10:56:57 +0300 Subject: [PATCH 1/3] chore: rename line --- ui/src/app/(notary)/certificate_requests/table.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/(notary)/certificate_requests/table.tsx b/ui/src/app/(notary)/certificate_requests/table.tsx index 4b7ecbf..083af14 100644 --- a/ui/src/app/(notary)/certificate_requests/table.tsx +++ b/ui/src/app/(notary)/certificate_requests/table.tsx @@ -191,7 +191,7 @@ export function CertificateRequestsTable({ csrs: rows }: TableProps) { className="p-contextual-menu__link" disabled={csr_status == "Rejected"} onMouseDown={() => csr_status == "Active" ? handleRevoke(id) : handleReject(id)}> - {csr_status == "Active" ? "Revoke Certificate Request" : "Reject Certificate Request"} + {csr_status == "Active" ? "Revoke Certificate" : "Reject Certificate Request"} - - - + +
+

Create the initial admin user

+ + + + + +
) } diff --git a/ui/src/app/(auth)/layout.tsx b/ui/src/app/(auth)/layout.tsx index 5e432b2..5166d01 100644 --- a/ui/src/app/(auth)/layout.tsx +++ b/ui/src/app/(auth)/layout.tsx @@ -11,11 +11,11 @@ export default function RootLayout({ }>) { return ( - - + + {children} - - + + ); } \ No newline at end of file diff --git a/ui/src/app/(auth)/login/page.tsx b/ui/src/app/(auth)/login/page.tsx index f94c15f..492af47 100644 --- a/ui/src/app/(auth)/login/page.tsx +++ b/ui/src/app/(auth)/login/page.tsx @@ -3,7 +3,6 @@ import { getStatus, login } from "@/queries" import { useMutation, useQuery } from "@tanstack/react-query" import { useState, ChangeEvent } from "react" -import { useCookies } from "react-cookie" import { useRouter } from "next/navigation" import { useAuth } from "@/hooks/useAuth" import { statusResponse } from "@/types" @@ -13,24 +12,20 @@ import { Input, PasswordToggle, Button, Form, Notification, LoginPageLayout } fr export default function LoginPage() { const router = useRouter() const auth = useAuth() - const [cookies, setCookie, removeCookie] = useCookies(['user_token']); const statusQuery = useQuery({ queryKey: ["status"], - queryFn: () => getStatus() + queryFn: () => getStatus(), + enabled: auth.firstUserInitialized == "unknown" }) - if (!auth.firstUserCreated && (statusQuery.data && !statusQuery.data.initialized)) { - router.push("/initialize") + if (auth.firstUserInitialized == "unknown" && statusQuery.data) { + auth.setFirstUserInitialized(statusQuery.data.initialized) } const mutation = useMutation({ mutationFn: login, onSuccess: (result) => { setErrorText("") - setCookie('user_token', result?.token, { - sameSite: true, - secure: true, - expires: new Date(new Date().getTime() + 60 * 60 * 1000), - }) - router.push('/certificate_requests') + auth.login(result?.token) + router.push('/') }, onError: (e: Error) => { setErrorText(e.message) @@ -43,51 +38,49 @@ export default function LoginPage() { const handleUsernameChange = (event: ChangeEvent) => { setUsername(event.target.value) } const handlePasswordChange = (event: ChangeEvent) => { setPassword(event.target.value) } return ( - <> - -
- - - {errorText && - - {errorText.split("error: ")} - - } - - -
- + {errorText.split("error: ")} + + } + + + ) } \ No newline at end of file diff --git a/ui/src/app/(notary)/certificate_requests/asideForm.tsx b/ui/src/app/(notary)/certificate_requests/asideForm.tsx index a4f2b32..81425f6 100644 --- a/ui/src/app/(notary)/certificate_requests/asideForm.tsx +++ b/ui/src/app/(notary)/certificate_requests/asideForm.tsx @@ -1,15 +1,15 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { csrIsValid } from "@/utils"; -import { useCookies } from "react-cookie"; import { postCSR } from "@/queries"; import { ChangeEvent, useContext, useState, useEffect } from "react"; import { AsideContext } from "@/components/aside"; import { Textarea, Button, Input, Panel, Form } from "@canonical/react-components"; +import { useAuth } from "@/hooks/useAuth"; export default function CertificateRequestsAsidePanel(): JSX.Element { + const auth = useAuth() const asideContext = useContext(AsideContext); - const [cookies] = useCookies(['user_token']); const [errorText, setErrorText] = useState(""); const [CSRPEMString, setCSRPEMString] = useState(""); const queryClient = useQueryClient(); @@ -52,7 +52,7 @@ export default function CertificateRequestsAsidePanel(): JSX.Element { }; const handleSubmit = () => { - mutation.mutate({ authToken: cookies.user_token, csr: CSRPEMString }); + mutation.mutate({ authToken: auth.user ? auth.user.authToken : "", csr: CSRPEMString }); }; return ( @@ -90,7 +90,7 @@ export default function CertificateRequestsAsidePanel(): JSX.Element { appearance="positive" name="submit" disabled={!csrIsValid(CSRPEMString)} - onClick={handleSubmit} + onClick={(event) => { event?.preventDefault(); handleSubmit() }} > Submit diff --git a/ui/src/app/(notary)/certificate_requests/components.tsx b/ui/src/app/(notary)/certificate_requests/components.tsx index 97c7cc7..41c8708 100644 --- a/ui/src/app/(notary)/certificate_requests/components.tsx +++ b/ui/src/app/(notary)/certificate_requests/components.tsx @@ -2,8 +2,8 @@ import { Dispatch, SetStateAction, useState, ChangeEvent, useEffect } from "reac import { useMutation, useQueryClient } from "@tanstack/react-query" import { csrMatchesCertificate, splitBundle, validateBundle } from "@/utils" import { postCertToID } from "@/queries" -import { useCookies } from "react-cookie" import { Button, Input, Textarea, Form, Modal, Icon } from "@canonical/react-components"; +import { useAuth } from "@/hooks/useAuth" interface SubmitCertificateModalProps { id: string @@ -13,7 +13,7 @@ interface SubmitCertificateModalProps { } export function SubmitCertificateModal({ id, csr, cert, setFormOpen }: SubmitCertificateModalProps) { - const [cookies] = useCookies(['user_token']); + const auth = useAuth() const [errorText, setErrorText] = useState(""); const [certificatePEMString, setCertificatePEMString] = useState(cert); const [validationErrorText, setValidationErrorText] = useState(""); @@ -80,7 +80,7 @@ export function SubmitCertificateModal({ id, csr, cert, setFormOpen }: SubmitCer buttonRow={ <> - - + + - + : diff --git a/ui/src/hooks/useAuth.tsx b/ui/src/hooks/useAuth.tsx index c986565..45506b8 100644 --- a/ui/src/hooks/useAuth.tsx +++ b/ui/src/hooks/useAuth.tsx @@ -1,43 +1,95 @@ "use client" -import { createContext, useContext, useState, useEffect, Dispatch, SetStateAction } from 'react'; +import { createContext, useContext, useState, useEffect, useCallback, Dispatch, SetStateAction, useMemo } from 'react'; import { User } from '../types'; import { useCookies } from 'react-cookie'; import { jwtDecode } from 'jwt-decode'; import { useRouter } from 'next/navigation'; +import { useMutation } from '@tanstack/react-query'; +import { postFirstUser } from '@/queries'; type AuthContextType = { - user: User | null - firstUserCreated: boolean - setFirstUserCreated: Dispatch> + user: User | null + + login: (token: string) => void + logout: () => void + + firstUserInitialized: boolean | "unknown" + setFirstUserInitialized: Dispatch> + + initializeFirstUser: (username: string, password: string) => void + initializationError: string } -const AuthContext = createContext({ user: null, firstUserCreated: false, setFirstUserCreated: () => { } }); +const AuthContext = createContext({ + user: null, + + login: (token: string) => { }, + logout: () => { }, + + firstUserInitialized: false, + setFirstUserInitialized: () => { }, + + initializeFirstUser: (username, password) => { }, + initializationError: "" +}); + +export const useAuth = () => useContext(AuthContext); export const AuthProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { - const [cookies, setCookie, removeCookie] = useCookies(['user_token']); - const [user, setUser] = useState(null); - const [firstUserCreated, setFirstUserCreated] = useState(false) - const router = useRouter(); - - useEffect(() => { - const token = cookies.user_token; - if (token) { - 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 ( - - {children} - - ); -}; + const router = useRouter(); + const [user, setUser] = useState(null); + const [cookies, setCookie, removeCookie] = useCookies(['user_token']); + + // This section handles login/logout + const login = useCallback((value: string) => { + setCookie('user_token', value, + { + sameSite: true, + secure: true, + expires: new Date(new Date().getTime() + 60 * 60 * 1000), + } + ) + }, [setCookie]) + const logout = useCallback(() => { + removeCookie('user_token') + }, [removeCookie]) -export const useAuth = () => useContext(AuthContext); \ No newline at end of file + // This section deals with initialization + const [firstUserInitialized, setFirstUserInitialized] = useState("unknown") + const [initializationError, setInitializationError] = useState("") + const postUserMutation = useMutation({ + mutationFn: postFirstUser, + onSuccess: () => { + setInitializationError("") + setFirstUserInitialized(true) + }, + onError: (e: Error) => { + setInitializationError(e.message) + } + }) + const initializeFirstUser = useCallback((username: string, password: string) => { postUserMutation.mutate({ username: username, password: password }) }, [postUserMutation]) + + // This hook coordinates the frontend depending on the login and initialization state of the app + useEffect(() => { + const token = cookies.user_token; + if (token) { + let userObject = jwtDecode(cookies.user_token) as User + userObject.authToken = cookies.user_token + setUser(userObject); + return + } + if (!token) { + router.push('/login') + } + if (firstUserInitialized == false) { + router.push('/initialize') + } + }, [cookies.user_token, router, firstUserInitialized]); + + return ( + + {children} + + ); +}; diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 1333c4c..afec990 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -305,4 +305,11 @@ export const validateBundle = async (bundle: string) => { }) const result = await chainEngine.verify() return result.resultMessage +} + +export const retryExceptWhenUnauthorized = (failureCount: Number, error: Error): boolean => { + if (error.message.includes("401")) { + return false + } + return true } \ No newline at end of file