diff --git a/client/bun.lock b/client/bun.lock index 680ee8f..bfc7317 100644 --- a/client/bun.lock +++ b/client/bun.lock @@ -9,6 +9,7 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@marsidev/react-turnstile": "^1.4.0", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -27,6 +28,7 @@ "@tanstack/react-router": "^1.141.6", "@tanstack/react-router-devtools": "^1.141.6", "@tanstack/react-table": "^8.21.3", + "@tanstack/zod-adapter": "^1.143.4", "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -264,6 +266,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.4.0", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-3aR7mh4lATeayWt6GjWuYyLjM0GL148z7/ZQl0rLKGpDYIrWgoU2PYsdAdA9fzH+JysW3Q2OaPfHvv66cwcAZg=="], + "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], @@ -488,6 +492,8 @@ "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.141.0", "", {}, "sha512-CJrWtr6L9TVzEImm9S7dQINx+xJcYP/aDkIi6gnaWtIgbZs1pnzsE0yJc2noqXZ+yAOqLx3TBGpBEs9tS0P9/A=="], + "@tanstack/zod-adapter": ["@tanstack/zod-adapter@1.143.4", "", { "peerDependencies": { "@tanstack/react-router": ">=1.43.2", "zod": "^3.23.8" } }, "sha512-yrdxNCKPaMjIXM5ZFf3jWNtGlOEZWh2nPdN5NQagkOrYK/l87SZRASB/vFerBXupXPaXvEL8C0qzb894trHW5w=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], diff --git a/client/package.json b/client/package.json index e47309b..db0da18 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,7 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@marsidev/react-turnstile": "^1.4.0", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -32,6 +33,7 @@ "@tanstack/react-router": "^1.141.6", "@tanstack/react-router-devtools": "^1.141.6", "@tanstack/react-table": "^8.21.3", + "@tanstack/zod-adapter": "^1.143.4", "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/client/src/app/dashboard/data.json b/client/src/app/dashboard/data.json deleted file mode 100644 index ec08736..0000000 --- a/client/src/app/dashboard/data.json +++ /dev/null @@ -1,614 +0,0 @@ -[ - { - "id": 1, - "header": "Cover page", - "type": "Cover page", - "status": "In Process", - "target": "18", - "limit": "5", - "reviewer": "Eddie Lake" - }, - { - "id": 2, - "header": "Table of contents", - "type": "Table of contents", - "status": "Done", - "target": "29", - "limit": "24", - "reviewer": "Eddie Lake" - }, - { - "id": 3, - "header": "Executive summary", - "type": "Narrative", - "status": "Done", - "target": "10", - "limit": "13", - "reviewer": "Eddie Lake" - }, - { - "id": 4, - "header": "Technical approach", - "type": "Narrative", - "status": "Done", - "target": "27", - "limit": "23", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 5, - "header": "Design", - "type": "Narrative", - "status": "In Process", - "target": "2", - "limit": "16", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 6, - "header": "Capabilities", - "type": "Narrative", - "status": "In Process", - "target": "20", - "limit": "8", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 7, - "header": "Integration with existing systems", - "type": "Narrative", - "status": "In Process", - "target": "19", - "limit": "21", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 8, - "header": "Innovation and Advantages", - "type": "Narrative", - "status": "Done", - "target": "25", - "limit": "26", - "reviewer": "Assign reviewer" - }, - { - "id": 9, - "header": "Overview of EMR's Innovative Solutions", - "type": "Technical content", - "status": "Done", - "target": "7", - "limit": "23", - "reviewer": "Assign reviewer" - }, - { - "id": 10, - "header": "Advanced Algorithms and Machine Learning", - "type": "Narrative", - "status": "Done", - "target": "30", - "limit": "28", - "reviewer": "Assign reviewer" - }, - { - "id": 11, - "header": "Adaptive Communication Protocols", - "type": "Narrative", - "status": "Done", - "target": "9", - "limit": "31", - "reviewer": "Assign reviewer" - }, - { - "id": 12, - "header": "Advantages Over Current Technologies", - "type": "Narrative", - "status": "Done", - "target": "12", - "limit": "0", - "reviewer": "Assign reviewer" - }, - { - "id": 13, - "header": "Past Performance", - "type": "Narrative", - "status": "Done", - "target": "22", - "limit": "33", - "reviewer": "Assign reviewer" - }, - { - "id": 14, - "header": "Customer Feedback and Satisfaction Levels", - "type": "Narrative", - "status": "Done", - "target": "15", - "limit": "34", - "reviewer": "Assign reviewer" - }, - { - "id": 15, - "header": "Implementation Challenges and Solutions", - "type": "Narrative", - "status": "Done", - "target": "3", - "limit": "35", - "reviewer": "Assign reviewer" - }, - { - "id": 16, - "header": "Security Measures and Data Protection Policies", - "type": "Narrative", - "status": "In Process", - "target": "6", - "limit": "36", - "reviewer": "Assign reviewer" - }, - { - "id": 17, - "header": "Scalability and Future Proofing", - "type": "Narrative", - "status": "Done", - "target": "4", - "limit": "37", - "reviewer": "Assign reviewer" - }, - { - "id": 18, - "header": "Cost-Benefit Analysis", - "type": "Plain language", - "status": "Done", - "target": "14", - "limit": "38", - "reviewer": "Assign reviewer" - }, - { - "id": 19, - "header": "User Training and Onboarding Experience", - "type": "Narrative", - "status": "Done", - "target": "17", - "limit": "39", - "reviewer": "Assign reviewer" - }, - { - "id": 20, - "header": "Future Development Roadmap", - "type": "Narrative", - "status": "Done", - "target": "11", - "limit": "40", - "reviewer": "Assign reviewer" - }, - { - "id": 21, - "header": "System Architecture Overview", - "type": "Technical content", - "status": "In Process", - "target": "24", - "limit": "18", - "reviewer": "Maya Johnson" - }, - { - "id": 22, - "header": "Risk Management Plan", - "type": "Narrative", - "status": "Done", - "target": "15", - "limit": "22", - "reviewer": "Carlos Rodriguez" - }, - { - "id": 23, - "header": "Compliance Documentation", - "type": "Legal", - "status": "In Process", - "target": "31", - "limit": "27", - "reviewer": "Sarah Chen" - }, - { - "id": 24, - "header": "API Documentation", - "type": "Technical content", - "status": "Done", - "target": "8", - "limit": "12", - "reviewer": "Raj Patel" - }, - { - "id": 25, - "header": "User Interface Mockups", - "type": "Visual", - "status": "In Process", - "target": "19", - "limit": "25", - "reviewer": "Leila Ahmadi" - }, - { - "id": 26, - "header": "Database Schema", - "type": "Technical content", - "status": "Done", - "target": "22", - "limit": "20", - "reviewer": "Thomas Wilson" - }, - { - "id": 27, - "header": "Testing Methodology", - "type": "Technical content", - "status": "In Process", - "target": "17", - "limit": "14", - "reviewer": "Assign reviewer" - }, - { - "id": 28, - "header": "Deployment Strategy", - "type": "Narrative", - "status": "Done", - "target": "26", - "limit": "30", - "reviewer": "Eddie Lake" - }, - { - "id": 29, - "header": "Budget Breakdown", - "type": "Financial", - "status": "In Process", - "target": "13", - "limit": "16", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 30, - "header": "Market Analysis", - "type": "Research", - "status": "Done", - "target": "29", - "limit": "32", - "reviewer": "Sophia Martinez" - }, - { - "id": 31, - "header": "Competitor Comparison", - "type": "Research", - "status": "In Process", - "target": "21", - "limit": "19", - "reviewer": "Assign reviewer" - }, - { - "id": 32, - "header": "Maintenance Plan", - "type": "Technical content", - "status": "Done", - "target": "16", - "limit": "23", - "reviewer": "Alex Thompson" - }, - { - "id": 33, - "header": "User Personas", - "type": "Research", - "status": "In Process", - "target": "27", - "limit": "24", - "reviewer": "Nina Patel" - }, - { - "id": 34, - "header": "Accessibility Compliance", - "type": "Legal", - "status": "Done", - "target": "18", - "limit": "21", - "reviewer": "Assign reviewer" - }, - { - "id": 35, - "header": "Performance Metrics", - "type": "Technical content", - "status": "In Process", - "target": "23", - "limit": "26", - "reviewer": "David Kim" - }, - { - "id": 36, - "header": "Disaster Recovery Plan", - "type": "Technical content", - "status": "Done", - "target": "14", - "limit": "17", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 37, - "header": "Third-party Integrations", - "type": "Technical content", - "status": "In Process", - "target": "25", - "limit": "28", - "reviewer": "Eddie Lake" - }, - { - "id": 38, - "header": "User Feedback Summary", - "type": "Research", - "status": "Done", - "target": "20", - "limit": "15", - "reviewer": "Assign reviewer" - }, - { - "id": 39, - "header": "Localization Strategy", - "type": "Narrative", - "status": "In Process", - "target": "12", - "limit": "19", - "reviewer": "Maria Garcia" - }, - { - "id": 40, - "header": "Mobile Compatibility", - "type": "Technical content", - "status": "Done", - "target": "28", - "limit": "31", - "reviewer": "James Wilson" - }, - { - "id": 41, - "header": "Data Migration Plan", - "type": "Technical content", - "status": "In Process", - "target": "19", - "limit": "22", - "reviewer": "Assign reviewer" - }, - { - "id": 42, - "header": "Quality Assurance Protocols", - "type": "Technical content", - "status": "Done", - "target": "30", - "limit": "33", - "reviewer": "Priya Singh" - }, - { - "id": 43, - "header": "Stakeholder Analysis", - "type": "Research", - "status": "In Process", - "target": "11", - "limit": "14", - "reviewer": "Eddie Lake" - }, - { - "id": 44, - "header": "Environmental Impact Assessment", - "type": "Research", - "status": "Done", - "target": "24", - "limit": "27", - "reviewer": "Assign reviewer" - }, - { - "id": 45, - "header": "Intellectual Property Rights", - "type": "Legal", - "status": "In Process", - "target": "17", - "limit": "20", - "reviewer": "Sarah Johnson" - }, - { - "id": 46, - "header": "Customer Support Framework", - "type": "Narrative", - "status": "Done", - "target": "22", - "limit": "25", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 47, - "header": "Version Control Strategy", - "type": "Technical content", - "status": "In Process", - "target": "15", - "limit": "18", - "reviewer": "Assign reviewer" - }, - { - "id": 48, - "header": "Continuous Integration Pipeline", - "type": "Technical content", - "status": "Done", - "target": "26", - "limit": "29", - "reviewer": "Michael Chen" - }, - { - "id": 49, - "header": "Regulatory Compliance", - "type": "Legal", - "status": "In Process", - "target": "13", - "limit": "16", - "reviewer": "Assign reviewer" - }, - { - "id": 50, - "header": "User Authentication System", - "type": "Technical content", - "status": "Done", - "target": "28", - "limit": "31", - "reviewer": "Eddie Lake" - }, - { - "id": 51, - "header": "Data Analytics Framework", - "type": "Technical content", - "status": "In Process", - "target": "21", - "limit": "24", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 52, - "header": "Cloud Infrastructure", - "type": "Technical content", - "status": "Done", - "target": "16", - "limit": "19", - "reviewer": "Assign reviewer" - }, - { - "id": 53, - "header": "Network Security Measures", - "type": "Technical content", - "status": "In Process", - "target": "29", - "limit": "32", - "reviewer": "Lisa Wong" - }, - { - "id": 54, - "header": "Project Timeline", - "type": "Planning", - "status": "Done", - "target": "14", - "limit": "17", - "reviewer": "Eddie Lake" - }, - { - "id": 55, - "header": "Resource Allocation", - "type": "Planning", - "status": "In Process", - "target": "27", - "limit": "30", - "reviewer": "Assign reviewer" - }, - { - "id": 56, - "header": "Team Structure and Roles", - "type": "Planning", - "status": "Done", - "target": "20", - "limit": "23", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 57, - "header": "Communication Protocols", - "type": "Planning", - "status": "In Process", - "target": "15", - "limit": "18", - "reviewer": "Assign reviewer" - }, - { - "id": 58, - "header": "Success Metrics", - "type": "Planning", - "status": "Done", - "target": "30", - "limit": "33", - "reviewer": "Eddie Lake" - }, - { - "id": 59, - "header": "Internationalization Support", - "type": "Technical content", - "status": "In Process", - "target": "23", - "limit": "26", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 60, - "header": "Backup and Recovery Procedures", - "type": "Technical content", - "status": "Done", - "target": "18", - "limit": "21", - "reviewer": "Assign reviewer" - }, - { - "id": 61, - "header": "Monitoring and Alerting System", - "type": "Technical content", - "status": "In Process", - "target": "25", - "limit": "28", - "reviewer": "Daniel Park" - }, - { - "id": 62, - "header": "Code Review Guidelines", - "type": "Technical content", - "status": "Done", - "target": "12", - "limit": "15", - "reviewer": "Eddie Lake" - }, - { - "id": 63, - "header": "Documentation Standards", - "type": "Technical content", - "status": "In Process", - "target": "27", - "limit": "30", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 64, - "header": "Release Management Process", - "type": "Planning", - "status": "Done", - "target": "22", - "limit": "25", - "reviewer": "Assign reviewer" - }, - { - "id": 65, - "header": "Feature Prioritization Matrix", - "type": "Planning", - "status": "In Process", - "target": "19", - "limit": "22", - "reviewer": "Emma Davis" - }, - { - "id": 66, - "header": "Technical Debt Assessment", - "type": "Technical content", - "status": "Done", - "target": "24", - "limit": "27", - "reviewer": "Eddie Lake" - }, - { - "id": 67, - "header": "Capacity Planning", - "type": "Planning", - "status": "In Process", - "target": "21", - "limit": "24", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 68, - "header": "Service Level Agreements", - "type": "Legal", - "status": "Done", - "target": "26", - "limit": "29", - "reviewer": "Assign reviewer" - } -] diff --git a/client/src/components/login-form.tsx b/client/src/components/login-form.tsx index 5f5fe2a..4ceaa92 100644 --- a/client/src/components/login-form.tsx +++ b/client/src/components/login-form.tsx @@ -1,3 +1,8 @@ +import type { TurnstileInstance } from '@marsidev/react-turnstile'; +import { Turnstile } from '@marsidev/react-turnstile'; +import { useNavigate } from '@tanstack/react-router'; +import { useRef, useState } from 'react'; +import { toast } from 'sonner'; import NixOSLogo from '@/assets/nixos.svg?react'; import { Button } from '@/components/ui/button'; import { @@ -6,42 +11,70 @@ import { FieldLabel, } from '@/components/ui/field'; import { Input } from '@/components/ui/input'; +import { useGetMagicLink } from '@/hooks/data/useGetMagicLink'; import { cn } from '@/lib/utils'; export function LoginForm({ className, ...props }: React.ComponentProps<'div'>) { + const formRef = useRef(null); + const turnstileRef = useRef(null); + const [token, setToken] = useState(null); + const { mutateAsync, isPending } = useGetMagicLink(); + const navigate = useNavigate(); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(formRef.current!); + const email = formData.get('email')! as string; + mutateAsync({ email, turnstile_token: token! }).then(() => { + void navigate({ to: '/magicLinkSent', search: { email } }); + }).catch((error) => { + console.error(error); + toast.error('请求登录链接失败'); + turnstileRef.current?.reset(); + }); + }; + return (
-
+
- -
- -
- Nix CN Meetup #2 -
+
+ +
+ Nix CN Meetup #2

欢迎来到 Nix CN Meetup #2

参会登记Email - +
+ { + setToken(token); + }} + />
); } diff --git a/client/src/hooks/data/useGetMagicLink.ts b/client/src/hooks/data/useGetMagicLink.ts new file mode 100644 index 0000000..4f2c7c3 --- /dev/null +++ b/client/src/hooks/data/useGetMagicLink.ts @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; +import { axiosClient } from '@/lib/axios'; + +interface GetMagicLinkPayload { + email: string; + turnstile_token: string; +} + +export function useGetMagicLink() { + return useMutation({ + mutationFn: async (payload: GetMagicLinkPayload) => { + return axiosClient.post('/auth/magic', payload); + }, + }); +} diff --git a/client/src/hooks/data/useValidateMagicLink.ts b/client/src/hooks/data/useValidateMagicLink.ts new file mode 100644 index 0000000..2e59ea9 --- /dev/null +++ b/client/src/hooks/data/useValidateMagicLink.ts @@ -0,0 +1,11 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import { axiosClient } from '@/lib/axios'; + +export function useValidateMagicLink(ticket: string) { + return useSuspenseQuery({ + queryKey: ['validateMagicLink', ticket], + queryFn: async () => { + return axiosClient.get<{ jwt_token: string; email: string }>('/auth/magic/verify', { params: { token: ticket } }); + }, + }); +} diff --git a/client/src/lib/axios.ts b/client/src/lib/axios.ts index 15f0ada..3791c5b 100644 --- a/client/src/lib/axios.ts +++ b/client/src/lib/axios.ts @@ -4,7 +4,7 @@ import { router } from '@/lib/router'; import { getToken, hasToken } from './token'; export const axiosClient = axios.create({ - baseURL: '/api/', + baseURL: '/api/v1/', }); axiosClient.interceptors.request.use((config) => { diff --git a/client/src/routeTree.gen.ts b/client/src/routeTree.gen.ts index 6a71146..fa081d6 100644 --- a/client/src/routeTree.gen.ts +++ b/client/src/routeTree.gen.ts @@ -9,10 +9,16 @@ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. import { Route as rootRouteImport } from './routes/__root' +import { Route as MagicLinkSentRouteImport } from './routes/magicLinkSent' import { Route as LoginRouteImport } from './routes/login' import { Route as SidebarLayoutRouteImport } from './routes/_sidebarLayout' import { Route as SidebarLayoutIndexRouteImport } from './routes/_sidebarLayout/index' +const MagicLinkSentRoute = MagicLinkSentRouteImport.update({ + id: '/magicLinkSent', + path: '/magicLinkSent', + getParentRoute: () => rootRouteImport, +} as any) const LoginRoute = LoginRouteImport.update({ id: '/login', path: '/login', @@ -30,33 +36,49 @@ const SidebarLayoutIndexRoute = SidebarLayoutIndexRouteImport.update({ export interface FileRoutesByFullPath { '/login': typeof LoginRoute + '/magicLinkSent': typeof MagicLinkSentRoute '/': typeof SidebarLayoutIndexRoute } export interface FileRoutesByTo { '/login': typeof LoginRoute + '/magicLinkSent': typeof MagicLinkSentRoute '/': typeof SidebarLayoutIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/_sidebarLayout': typeof SidebarLayoutRouteWithChildren '/login': typeof LoginRoute + '/magicLinkSent': typeof MagicLinkSentRoute '/_sidebarLayout/': typeof SidebarLayoutIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/login' | '/' + fullPaths: '/login' | '/magicLinkSent' | '/' fileRoutesByTo: FileRoutesByTo - to: '/login' | '/' - id: '__root__' | '/_sidebarLayout' | '/login' | '/_sidebarLayout/' + to: '/login' | '/magicLinkSent' | '/' + id: + | '__root__' + | '/_sidebarLayout' + | '/login' + | '/magicLinkSent' + | '/_sidebarLayout/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { SidebarLayoutRoute: typeof SidebarLayoutRouteWithChildren LoginRoute: typeof LoginRoute + MagicLinkSentRoute: typeof MagicLinkSentRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/magicLinkSent': { + id: '/magicLinkSent' + path: '/magicLinkSent' + fullPath: '/magicLinkSent' + preLoaderRoute: typeof MagicLinkSentRouteImport + parentRoute: typeof rootRouteImport + } '/login': { id: '/login' path: '/login' @@ -96,6 +118,7 @@ const SidebarLayoutRouteWithChildren = SidebarLayoutRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { SidebarLayoutRoute: SidebarLayoutRouteWithChildren, LoginRoute: LoginRoute, + MagicLinkSentRoute: MagicLinkSentRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/client/src/routes/__root.tsx b/client/src/routes/__root.tsx index 332e0d7..2dc2995 100644 --- a/client/src/routes/__root.tsx +++ b/client/src/routes/__root.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createRootRoute, Outlet } from '@tanstack/react-router'; import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'; import { ThemeProvider } from '@/components/theme-provider'; +import { Toaster } from '@/components/ui/sonner'; import '@/index.css'; const queryClient = new QueryClient(); @@ -15,6 +16,7 @@ function RootLayout() { + ); } diff --git a/client/src/routes/login.tsx b/client/src/routes/login.tsx index 5710ac0..133c35a 100644 --- a/client/src/routes/login.tsx +++ b/client/src/routes/login.tsx @@ -1,15 +1,34 @@ -import { createFileRoute } from '@tanstack/react-router'; +import { createFileRoute, Navigate } from '@tanstack/react-router'; +import { zodValidator } from '@tanstack/zod-adapter'; +import z from 'zod'; import { LoginForm } from '@/components/login-form'; +import { useValidateMagicLink } from '@/hooks/data/useValidateMagicLink'; +import { setToken } from '@/lib/token'; + +const loginMagicLinkReceiverSchema = z.object({ + ticket: z.string().optional(), +}); export const Route = createFileRoute('/login')({ component: RouteComponent, + validateSearch: zodValidator(loginMagicLinkReceiverSchema), }); +function ReceiveMagicLinkComponent() { + const { ticket } = Route.useSearch(); + const { data } = useValidateMagicLink(ticket!); + + setToken(data.data.jwt_token); + + return ; +} + function RouteComponent() { + const { ticket } = Route.useSearch(); return (
- + {ticket === undefined ? : }
); diff --git a/client/src/routes/magicLinkSent.tsx b/client/src/routes/magicLinkSent.tsx new file mode 100644 index 0000000..c7a3392 --- /dev/null +++ b/client/src/routes/magicLinkSent.tsx @@ -0,0 +1,33 @@ +import { createFileRoute, Navigate } from '@tanstack/react-router'; +import { zodValidator } from '@tanstack/zod-adapter'; +import z from 'zod'; +import NixOSLogo from '@/assets/nixos.svg?react'; + +const paramsSchema = z.object({ + email: z.string().optional(), +}); + +export const Route = createFileRoute('/magicLinkSent')({ + component: RouteComponent, + validateSearch: zodValidator(paramsSchema), +}); + +function RouteComponent() { + const { email } = Route.useSearch(); + return email !== undefined + ? ( +
+ +
+ ) + : ; +} diff --git a/client/vite.config.ts b/client/vite.config.ts index 35c3dfb..bfd5f59 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -21,4 +21,10 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, + server: { + proxy: { + '/api': 'http://10.0.0.10:8000', + }, + allowedHosts: ['dev.sne.moe'], + }, });