|
1 | 1 | // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. |
2 | 2 |
|
3 | | -import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'; |
4 | | -import { useEffect, useState } from 'react'; |
5 | | -import { useClient } from '@objectstack/client-react'; |
6 | | -import { Button } from '@/components/ui/button'; |
7 | | -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; |
8 | | -import { Input } from '@/components/ui/input'; |
9 | | -import { Label } from '@/components/ui/label'; |
10 | | -import { toast } from '@/hooks/use-toast'; |
11 | | -import { useSession } from '@/hooks/useSession'; |
12 | | -import { useProjects } from '@/hooks/useProjects'; |
13 | | -import { SocialSignInButtons } from '@/components/auth/social-sign-in-buttons'; |
14 | | -import { GalleryVerticalEnd } from 'lucide-react'; |
| 3 | +/** |
| 4 | + * Studio's `/login` route — kept only for backwards-compatible URLs. |
| 5 | + * |
| 6 | + * Studio delegates all auth UI to the Account SPA at `/_account/login`. |
| 7 | + * Visitors landing here are redirected immediately, preserving any |
| 8 | + * `?redirect=...` they brought along. |
| 9 | + */ |
| 10 | + |
| 11 | +import { createFileRoute, useSearch } from '@tanstack/react-router'; |
| 12 | +import { useEffect } from 'react'; |
| 13 | +import { gotoAccountLogin } from '@/lib/auth-redirect'; |
15 | 14 |
|
16 | 15 | export const Route = createFileRoute('/login')({ |
17 | 16 | validateSearch: (search: Record<string, unknown>): { redirect?: string } => { |
18 | 17 | const r = search.redirect; |
19 | 18 | return typeof r === 'string' ? { redirect: r } : {}; |
20 | 19 | }, |
21 | | - component: LoginPage, |
| 20 | + component: LoginRedirect, |
22 | 21 | }); |
23 | 22 |
|
24 | | -function isSafeRedirect(target: string | undefined): target is string { |
25 | | - return !!target && target.startsWith('/') && !target.startsWith('//'); |
26 | | -} |
27 | | - |
28 | | -function LoginPage() { |
29 | | - const navigate = useNavigate(); |
30 | | - const { redirect } = Route.useSearch(); |
31 | | - const client = useClient() as any; |
32 | | - const { session, user, refresh } = useSession(); |
33 | | - const { projects, loading: projectsLoading } = useProjects(); |
34 | | - const [email, setEmail] = useState(''); |
35 | | - const [password, setPassword] = useState(''); |
36 | | - const [submitting, setSubmitting] = useState(false); |
37 | | - |
| 23 | +function LoginRedirect() { |
| 24 | + const { redirect } = useSearch({ from: '/login' }); |
38 | 25 | useEffect(() => { |
39 | | - if (!user) return; |
40 | | - if (isSafeRedirect(redirect)) { |
41 | | - const base = import.meta.env.BASE_URL.replace(/\/$/, ''); |
42 | | - window.location.assign(base + redirect); |
43 | | - return; |
44 | | - } |
45 | | - if (!session?.activeOrganizationId) { |
46 | | - navigate({ to: '/orgs' }); |
47 | | - return; |
48 | | - } |
49 | | - if (projectsLoading) return; |
50 | | - |
51 | | - const lastProjectId = localStorage.getItem('objectstack.lastProjectId'); |
52 | | - const targetProject = |
53 | | - (lastProjectId && projects.find((p) => p.id === lastProjectId)) || |
54 | | - projects.find((p) => p.is_default) || |
55 | | - projects[0]; |
56 | | - |
57 | | - if (targetProject) { |
58 | | - navigate({ |
59 | | - to: '/projects/$projectId', |
60 | | - params: { projectId: targetProject.id }, |
61 | | - }); |
62 | | - } else { |
63 | | - navigate({ to: '/projects' }); |
64 | | - } |
65 | | - }, [user, session, projects, projectsLoading, navigate, redirect]); |
66 | | - |
67 | | - const handleSubmit = async (e: React.FormEvent) => { |
68 | | - e.preventDefault(); |
69 | | - if (!client?.auth) return; |
70 | | - setSubmitting(true); |
71 | | - try { |
72 | | - await client.auth.login({ type: 'email', email, password }); |
73 | | - await refresh(); |
74 | | - toast({ title: 'Welcome back' }); |
75 | | - } catch (err) { |
76 | | - toast({ |
77 | | - title: 'Sign in failed', |
78 | | - description: (err as Error).message, |
79 | | - variant: 'destructive', |
80 | | - }); |
81 | | - } finally { |
82 | | - setSubmitting(false); |
83 | | - } |
84 | | - }; |
85 | | - |
86 | | - return ( |
87 | | - <div className="flex min-h-svh w-full flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10"> |
88 | | - <div className="flex w-full max-w-sm flex-col gap-6"> |
89 | | - <a href="#" className="flex items-center gap-2 self-center font-medium"> |
90 | | - <div className="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground"> |
91 | | - <GalleryVerticalEnd className="size-4" /> |
92 | | - </div> |
93 | | - ObjectStack |
94 | | - </a> |
95 | | - <div className="flex flex-col gap-6"> |
96 | | - <Card> |
97 | | - <CardHeader className="text-center"> |
98 | | - <CardTitle className="text-xl">Welcome back</CardTitle> |
99 | | - <CardDescription>Access your ObjectStack Studio workspace.</CardDescription> |
100 | | - </CardHeader> |
101 | | - <CardContent> |
102 | | - <form onSubmit={handleSubmit}> |
103 | | - <div className="flex flex-col gap-4"> |
104 | | - <SocialSignInButtons mode="sign-in" /> |
105 | | - <div className="flex flex-col gap-2"> |
106 | | - <Label htmlFor="email">Email</Label> |
107 | | - <Input |
108 | | - id="email" |
109 | | - type="email" |
110 | | - placeholder="m@example.com" |
111 | | - autoComplete="email" |
112 | | - required |
113 | | - value={email} |
114 | | - onChange={(e) => setEmail(e.target.value)} |
115 | | - /> |
116 | | - </div> |
117 | | - <div className="flex flex-col gap-2"> |
118 | | - <div className="flex items-center"> |
119 | | - <Label htmlFor="password">Password</Label> |
120 | | - <Link |
121 | | - to="/forgot-password" |
122 | | - className="ml-auto text-sm underline-offset-4 hover:underline" |
123 | | - > |
124 | | - Forgot your password? |
125 | | - </Link> |
126 | | - </div> |
127 | | - <Input |
128 | | - id="password" |
129 | | - type="password" |
130 | | - autoComplete="current-password" |
131 | | - required |
132 | | - value={password} |
133 | | - onChange={(e) => setPassword(e.target.value)} |
134 | | - /> |
135 | | - </div> |
136 | | - <Button type="submit" className="w-full" disabled={submitting}> |
137 | | - {submitting ? 'Signing in…' : 'Login'} |
138 | | - </Button> |
139 | | - <p className="text-center text-sm text-muted-foreground"> |
140 | | - Don't have an account?{' '} |
141 | | - <Link to="/register" className="underline underline-offset-4 hover:text-primary"> |
142 | | - Sign up |
143 | | - </Link> |
144 | | - </p> |
145 | | - </div> |
146 | | - </form> |
147 | | - </CardContent> |
148 | | - </Card> |
149 | | - <p className="px-6 text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 [&_a]:hover:text-primary"> |
150 | | - By clicking continue, you agree to our{' '} |
151 | | - <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>. |
152 | | - </p> |
153 | | - </div> |
154 | | - </div> |
155 | | - </div> |
156 | | - ); |
| 26 | + gotoAccountLogin(redirect); |
| 27 | + }, [redirect]); |
| 28 | + return null; |
157 | 29 | } |
0 commit comments