diff --git a/ui/package-lock.json b/ui/package-lock.json index 59d1069..3d687ff 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -7962,6 +7962,21 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.15", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz", + "integrity": "sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/ui/src/app/(auth)/initialize/page.tsx b/ui/src/app/(auth)/initialize/page.tsx index f72388f..af5617b 100644 --- a/ui/src/app/(auth)/initialize/page.tsx +++ b/ui/src/app/(auth)/initialize/page.tsx @@ -1,54 +1,13 @@ "use client" import { passwordIsValid } from "@/utils" -import { statusResponse } from "@/types" -import { getStatus, login, postFirstUser } from "@/queries" -import { useAuth } from "@/app/auth/authContext" import { Input, PasswordToggle, Button, Form, LoginPageLayout } from "@canonical/react-components"; -import { useMutation, useQuery } from "@tanstack/react-query" import { useState, ChangeEvent } from "react" -import { useRouter } from "next/navigation" -import { useCookies } from "react-cookie" - +import { useAuth } from "@/hooks/useAuth"; export default function Initialize() { - const router = useRouter() const auth = useAuth() - const [cookies, setCookie, removeCookie] = useCookies(['user_token']); - const statusQuery = useQuery({ - queryKey: ["status"], - queryFn: () => getStatus(), - }) - if (statusQuery.data && statusQuery.data.initialized) { - auth.setFirstUserCreated(true) - router.push("/login") - } - const loginMutation = 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') - }, - onError: (e: Error) => { - setErrorText(e.message) - } - }) - const postUserMutation = useMutation({ - mutationFn: postFirstUser, - onSuccess: () => { - setErrorText("") - auth.setFirstUserCreated(true) - loginMutation.mutate({ username: username, password: password1 }) - }, - onError: (e: Error) => { - setErrorText(e.message) - } - }) + const [username, setUsername] = useState("") const [password1, setPassword1] = useState("") const [password2, setPassword2] = useState("") @@ -56,58 +15,55 @@ export default function Initialize() { const password1Error = password1 && !passwordIsValid(password1) ? "Password is not valid" : "" const password2Error = password2 && !passwordsMatch ? "Passwords do not match" : "" - const [errorText, setErrorText] = useState("") const handleUsernameChange = (event: ChangeEvent) => { setUsername(event.target.value) } const handlePassword1Change = (event: ChangeEvent) => { setPassword1(event.target.value) } const handlePassword2Change = (event: ChangeEvent) => { setPassword2(event.target.value) } return ( - <> - -
-

Create the initial admin user

- - - - - -
- + +
+

Create the initial admin user

+ + + + + +
) } diff --git a/ui/src/app/(auth)/layout.tsx b/ui/src/app/(auth)/layout.tsx index 6e6c5fd..5166d01 100644 --- a/ui/src/app/(auth)/layout.tsx +++ b/ui/src/app/(auth)/layout.tsx @@ -1,6 +1,6 @@ "use client" import '@/globals.scss' -import { AuthProvider } from "@/app/auth/authContext"; +import { AuthProvider } from "@/hooks/useAuth"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient() @@ -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 7ccb745..492af47 100644 --- a/ui/src/app/(auth)/login/page.tsx +++ b/ui/src/app/(auth)/login/page.tsx @@ -3,9 +3,8 @@ 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 "@/app/auth/authContext" +import { useAuth } from "@/hooks/useAuth" import { statusResponse } from "@/types" import { Input, PasswordToggle, Button, Form, Notification, LoginPageLayout } from "@canonical/react-components"; @@ -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 new file mode 100644 index 0000000..45506b8 --- /dev/null +++ b/ui/src/hooks/useAuth.tsx @@ -0,0 +1,95 @@ +"use client" + +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 + + login: (token: string) => void + logout: () => void + + firstUserInitialized: boolean | "unknown" + setFirstUserInitialized: Dispatch> + + initializeFirstUser: (username: string, password: string) => void + initializationError: string +} + +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 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]) + + // 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