mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 14:15:50 +00:00 
			
		
		
		
	feat: add totp logic and ui
This commit is contained in:
		
							
								
								
									
										40
									
								
								site/src/components/auth/totp-form.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								site/src/components/auth/totp-form.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { Button, PinInput } from "@mantine/core"; | ||||
| import { useForm, zodResolver } from "@mantine/form"; | ||||
| import { z } from "zod"; | ||||
|  | ||||
| const schema = z.object({ | ||||
|   code: z.string(), | ||||
| }); | ||||
|  | ||||
| type FormValues = z.infer<typeof schema>; | ||||
|  | ||||
| interface TotpFormProps { | ||||
|   onSubmit: (values: FormValues) => void; | ||||
|   isLoading: boolean; | ||||
| } | ||||
|  | ||||
| export const TotpForm = (props: TotpFormProps) => { | ||||
|   const { onSubmit, isLoading } = props; | ||||
|  | ||||
|   const form = useForm({ | ||||
|     mode: "uncontrolled", | ||||
|     initialValues: { | ||||
|       code: "", | ||||
|     }, | ||||
|     validate: zodResolver(schema), | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <form onSubmit={form.onSubmit(onSubmit)}> | ||||
|       <PinInput | ||||
|         length={6} | ||||
|         type={"number"} | ||||
|         placeholder="" | ||||
|         {...form.getInputProps("code")} | ||||
|       /> | ||||
|       <Button type="submit" mt="xl" loading={isLoading} fullWidth> | ||||
|         Verify | ||||
|       </Button> | ||||
|     </form> | ||||
|   ); | ||||
| }; | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import React, { createContext, useContext } from "react"; | ||||
| import { UserContextSchemaType } from "../schemas/user-context-schema"; | ||||
| import axios from "axios"; | ||||
| import { UserContextSchemaType } from "../schemas/user-context-schema"; | ||||
|  | ||||
| const UserContext = createContext<UserContextSchemaType | null>(null); | ||||
|  | ||||
| @@ -15,7 +15,7 @@ export const UserContextProvider = ({ | ||||
|     isLoading, | ||||
|     error, | ||||
|   } = useQuery({ | ||||
|     queryKey: ["isLoggedIn"], | ||||
|     queryKey: ["userContext"], | ||||
|     queryFn: async () => { | ||||
|       const res = await axios.get("/api/status"); | ||||
|       return res.data; | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import { ContinuePage } from "./pages/continue-page.tsx"; | ||||
| import { NotFoundPage } from "./pages/not-found-page.tsx"; | ||||
| import { UnauthorizedPage } from "./pages/unauthorized-page.tsx"; | ||||
| import { InternalServerError } from "./pages/internal-server-error.tsx"; | ||||
| import { TotpPage } from "./pages/totp-page.tsx"; | ||||
|  | ||||
| const queryClient = new QueryClient({ | ||||
|   defaultOptions: { | ||||
| @@ -34,6 +35,7 @@ createRoot(document.getElementById("root")!).render( | ||||
|             <Routes> | ||||
|               <Route path="/" element={<App />} /> | ||||
|               <Route path="/login" element={<LoginPage />} /> | ||||
|               <Route path="/totp" element={<TotpPage />} /> | ||||
|               <Route path="/logout" element={<LogoutPage />} /> | ||||
|               <Route path="/continue" element={<ContinuePage />} /> | ||||
|               <Route path="/unauthorized" element={<UnauthorizedPage />} /> | ||||
|   | ||||
| @@ -5,10 +5,10 @@ import axios from "axios"; | ||||
| import { useUserContext } from "../context/user-context"; | ||||
| import { Navigate } from "react-router"; | ||||
| import { Layout } from "../components/layouts/layout"; | ||||
| import { isQueryValid } from "../utils/utils"; | ||||
| import { OAuthButtons } from "../components/auth/oauth-buttons"; | ||||
| import { LoginFormValues } from "../schemas/login-schema"; | ||||
| import { LoginForm } from "../components/auth/login-forn"; | ||||
| import { isQueryValid } from "../utils/utils"; | ||||
|  | ||||
| export const LoginPage = () => { | ||||
|   const queryString = window.location.search; | ||||
| @@ -37,18 +37,25 @@ export const LoginPage = () => { | ||||
|         color: "red", | ||||
|       }); | ||||
|     }, | ||||
|     onSuccess: () => { | ||||
|     onSuccess: async (data) => { | ||||
|       if (data.data.totpPending) { | ||||
|         window.location.replace(`/totp?redirect_uri=${redirectUri}`); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       notifications.show({ | ||||
|         title: "Logged in", | ||||
|         message: "Welcome back!", | ||||
|         color: "green", | ||||
|       }); | ||||
|  | ||||
|       setTimeout(() => { | ||||
|         if (!isQueryValid(redirectUri)) { | ||||
|           window.location.replace("/"); | ||||
|         } else { | ||||
|           window.location.replace(`/continue?redirect_uri=${redirectUri}`); | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         window.location.replace(`/continue?redirect_uri=${redirectUri}`); | ||||
|       }, 500); | ||||
|     }, | ||||
|   }); | ||||
|   | ||||
							
								
								
									
										62
									
								
								site/src/pages/totp-page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								site/src/pages/totp-page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| import { Navigate } from "react-router"; | ||||
| import { useUserContext } from "../context/user-context"; | ||||
| import { Title, Paper, Text } from "@mantine/core"; | ||||
| import { Layout } from "../components/layouts/layout"; | ||||
| import { TotpForm } from "../components/auth/totp-form"; | ||||
| import { useMutation } from "@tanstack/react-query"; | ||||
| import axios from "axios"; | ||||
| import { notifications } from "@mantine/notifications"; | ||||
|  | ||||
| export const TotpPage = () => { | ||||
|   const queryString = window.location.search; | ||||
|   const params = new URLSearchParams(queryString); | ||||
|   const redirectUri = params.get("redirect_uri") ?? ""; | ||||
|  | ||||
|   const { totpPending, isLoggedIn, title } = useUserContext(); | ||||
|  | ||||
|   if (isLoggedIn) { | ||||
|     return <Navigate to={`/logout`} />; | ||||
|   } | ||||
|  | ||||
|   if (!totpPending) { | ||||
|     return <Navigate to={`/login?redirect_uri=${redirectUri}`} />; | ||||
|   } | ||||
|  | ||||
|   const totpMutation = useMutation({ | ||||
|     mutationFn: async (totp: { code: string }) => { | ||||
|       await axios.post("/api/totp", totp); | ||||
|     }, | ||||
|     onError: () => { | ||||
|       notifications.show({ | ||||
|         title: "Failed to verify code", | ||||
|         message: "Please try again", | ||||
|         color: "red", | ||||
|       }); | ||||
|     }, | ||||
|     onSuccess: () => { | ||||
|       notifications.show({ | ||||
|         title: "Verified", | ||||
|         message: "Redirecting to your app", | ||||
|         color: "green", | ||||
|       }); | ||||
|       setTimeout(() => { | ||||
|         window.location.replace(`/continue?redirect_uri=${redirectUri}`); | ||||
|       }, 500); | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <Layout> | ||||
|       <Title ta="center">{title}</Title> | ||||
|       <Paper shadow="md" p="xl" mt={30} radius="md" withBorder> | ||||
|         <Text size="lg" fw={500} mb="md" ta="center"> | ||||
|           Enter your TOTP code | ||||
|         </Text> | ||||
|         <TotpForm | ||||
|           isLoading={totpMutation.isLoading} | ||||
|           onSubmit={(values) => totpMutation.mutate(values)} | ||||
|         /> | ||||
|       </Paper> | ||||
|     </Layout> | ||||
|   ); | ||||
| }; | ||||
| @@ -9,6 +9,7 @@ export const userContextSchema = z.object({ | ||||
|   disableContinue: z.boolean(), | ||||
|   title: z.string(), | ||||
|   genericName: z.string(), | ||||
|   totpPending: z.boolean(), | ||||
| }); | ||||
|  | ||||
| export type UserContextSchemaType = z.infer<typeof userContextSchema>; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Stavros
					Stavros