@helloleo/plugins 0.2.0-beta.5 → 0.2.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +531 -547
- package/dist/templates/tanstack-start-2026-06-18/package.json +1 -0
- package/dist/templates/tanstack-start-2026-06-18/src/routes/README.md +1 -7
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/templates/tanstack-start-2026-06-18/package.json +1 -0
- package/src/templates/tanstack-start-2026-06-18/src/routes/README.md +1 -7
- package/src/tools/inspect-http.ts +0 -10
- package/src/tools/scaffold.ts +1 -0
- package/src/tools/setup-authentication/auth-files.ts +132 -0
- package/src/tools/setup-authentication/constants.ts +19 -0
- package/src/tools/setup-authentication/db-schema.ts +96 -0
- package/src/tools/setup-authentication/index.ts +119 -0
- package/src/tools/setup-authentication/package-json.ts +17 -0
- package/src/tools/setup-authentication/pages.ts +241 -0
- package/src/tools/setup-password-auth.ts +0 -599
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { OAuthProvider } from "./constants.ts";
|
|
2
|
+
import { PROVIDER_LABELS } from "./constants.ts";
|
|
3
|
+
|
|
4
|
+
function socialButtonsJsx(oauth: OAuthProvider[]): string {
|
|
5
|
+
return oauth
|
|
6
|
+
.map(
|
|
7
|
+
(p) => ` <Button
|
|
8
|
+
type="button"
|
|
9
|
+
variant="outline"
|
|
10
|
+
className="w-full"
|
|
11
|
+
onClick={() => authClient.signIn.social({ provider: '${p}', callbackURL: '/' })}
|
|
12
|
+
>
|
|
13
|
+
Continue with ${PROVIDER_LABELS[p]}
|
|
14
|
+
</Button>`,
|
|
15
|
+
)
|
|
16
|
+
.join("\n");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function authedLayoutTsx(): string {
|
|
20
|
+
return `import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
21
|
+
import { getSession } from '#/lib/auth-server'
|
|
22
|
+
|
|
23
|
+
// Pathless guard -- no URL segment of its own. Put protected pages under
|
|
24
|
+
// src/routes/_authed/ (e.g. _authed/dashboard.tsx -> /dashboard) and they
|
|
25
|
+
// inherit this beforeLoad. See src/routes/README.md, "Authenticated routes".
|
|
26
|
+
export const Route = createFileRoute('/_authed')({
|
|
27
|
+
beforeLoad: async () => {
|
|
28
|
+
const session = await getSession()
|
|
29
|
+
if (!session) throw redirect({ to: '/login' })
|
|
30
|
+
return { user: session.user }
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loginPageTsx(
|
|
37
|
+
hasPassword: boolean,
|
|
38
|
+
oauth: OAuthProvider[],
|
|
39
|
+
): string {
|
|
40
|
+
const social = oauth.length ? `\n${socialButtonsJsx(oauth)}\n` : "";
|
|
41
|
+
|
|
42
|
+
const divider =
|
|
43
|
+
oauth.length && hasPassword
|
|
44
|
+
? `
|
|
45
|
+
<div className="relative">
|
|
46
|
+
<div className="absolute inset-0 flex items-center">
|
|
47
|
+
<span className="w-full border-t" />
|
|
48
|
+
</div>
|
|
49
|
+
<div className="relative flex justify-center text-xs uppercase">
|
|
50
|
+
<span className="bg-card px-2 text-muted-foreground">Or continue with email</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
`
|
|
54
|
+
: "";
|
|
55
|
+
|
|
56
|
+
const passwordForm = hasPassword
|
|
57
|
+
? `
|
|
58
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
59
|
+
<div className="space-y-2">
|
|
60
|
+
<Label htmlFor="email">Email</Label>
|
|
61
|
+
<Input id="email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoComplete="email" />
|
|
62
|
+
</div>
|
|
63
|
+
<div className="space-y-2">
|
|
64
|
+
<Label htmlFor="password">Password</Label>
|
|
65
|
+
<Input id="password" type="password" value={password} onChange={e => setPassword(e.target.value)} required autoComplete="current-password" />
|
|
66
|
+
</div>
|
|
67
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
68
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
69
|
+
{loading ? 'Logging in…' : 'Log in'}
|
|
70
|
+
</Button>
|
|
71
|
+
<p className="text-sm text-center text-muted-foreground">
|
|
72
|
+
No account? <Link to="/signup" className="underline">Sign up</Link>
|
|
73
|
+
</p>
|
|
74
|
+
</form>
|
|
75
|
+
`
|
|
76
|
+
: "";
|
|
77
|
+
|
|
78
|
+
const stateAndHandler = hasPassword
|
|
79
|
+
? `
|
|
80
|
+
const router = useRouter()
|
|
81
|
+
const [email, setEmail] = useState('')
|
|
82
|
+
const [password, setPassword] = useState('')
|
|
83
|
+
const [error, setError] = useState('')
|
|
84
|
+
const [loading, setLoading] = useState(false)
|
|
85
|
+
|
|
86
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
87
|
+
e.preventDefault()
|
|
88
|
+
setError('')
|
|
89
|
+
setLoading(true)
|
|
90
|
+
const { error } = await authClient.signIn.email({ email, password })
|
|
91
|
+
setLoading(false)
|
|
92
|
+
if (error) {
|
|
93
|
+
setError(error.message ?? 'Something went wrong.')
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
router.navigate({ to: '/' })
|
|
97
|
+
}
|
|
98
|
+
`
|
|
99
|
+
: "";
|
|
100
|
+
|
|
101
|
+
const imports = [
|
|
102
|
+
hasPassword
|
|
103
|
+
? "import { createFileRoute, Link, useRouter } from '@tanstack/react-router'"
|
|
104
|
+
: "import { createFileRoute } from '@tanstack/react-router'",
|
|
105
|
+
hasPassword ? "import { useState } from 'react'" : "",
|
|
106
|
+
"import { authClient } from '#/lib/auth-client'",
|
|
107
|
+
"import { Button } from '#/components/ui/button'",
|
|
108
|
+
hasPassword ? "import { Input } from '#/components/ui/input'" : "",
|
|
109
|
+
hasPassword ? "import { Label } from '#/components/ui/label'" : "",
|
|
110
|
+
"import { Card, CardContent, CardHeader, CardTitle } from '#/components/ui/card'",
|
|
111
|
+
]
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.join("\n");
|
|
114
|
+
|
|
115
|
+
return `${imports}
|
|
116
|
+
|
|
117
|
+
export const Route = createFileRoute('/login')({
|
|
118
|
+
component: LoginPage,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
function LoginPage() {${stateAndHandler}
|
|
122
|
+
return (
|
|
123
|
+
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
|
124
|
+
<Card className="w-full max-w-sm">
|
|
125
|
+
<CardHeader>
|
|
126
|
+
<CardTitle>Log in</CardTitle>
|
|
127
|
+
</CardHeader>
|
|
128
|
+
<CardContent className="space-y-4">${social}${divider}${passwordForm}
|
|
129
|
+
</CardContent>
|
|
130
|
+
</Card>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function signupPageTsx(): string {
|
|
138
|
+
return `import { createFileRoute, Link, useRouter } from '@tanstack/react-router'
|
|
139
|
+
import { useState } from 'react'
|
|
140
|
+
import { authClient } from '#/lib/auth-client'
|
|
141
|
+
import { Button } from '#/components/ui/button'
|
|
142
|
+
import { Input } from '#/components/ui/input'
|
|
143
|
+
import { Label } from '#/components/ui/label'
|
|
144
|
+
import { Card, CardContent, CardHeader, CardTitle } from '#/components/ui/card'
|
|
145
|
+
|
|
146
|
+
export const Route = createFileRoute('/signup')({
|
|
147
|
+
component: SignupPage,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
function SignupPage() {
|
|
151
|
+
const router = useRouter()
|
|
152
|
+
const [name, setName] = useState('')
|
|
153
|
+
const [email, setEmail] = useState('')
|
|
154
|
+
const [password, setPassword] = useState('')
|
|
155
|
+
const [error, setError] = useState('')
|
|
156
|
+
const [loading, setLoading] = useState(false)
|
|
157
|
+
|
|
158
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
159
|
+
e.preventDefault()
|
|
160
|
+
setError('')
|
|
161
|
+
setLoading(true)
|
|
162
|
+
const { error } = await authClient.signUp.email({ name, email, password })
|
|
163
|
+
setLoading(false)
|
|
164
|
+
if (error) {
|
|
165
|
+
setError(error.message ?? 'Something went wrong.')
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
router.navigate({ to: '/' })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
|
173
|
+
<Card className="w-full max-w-sm">
|
|
174
|
+
<CardHeader>
|
|
175
|
+
<CardTitle>Sign up</CardTitle>
|
|
176
|
+
</CardHeader>
|
|
177
|
+
<CardContent>
|
|
178
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
179
|
+
<div className="space-y-2">
|
|
180
|
+
<Label htmlFor="name">Name</Label>
|
|
181
|
+
<Input id="name" type="text" value={name} onChange={e => setName(e.target.value)} required autoComplete="name" />
|
|
182
|
+
</div>
|
|
183
|
+
<div className="space-y-2">
|
|
184
|
+
<Label htmlFor="email">Email</Label>
|
|
185
|
+
<Input id="email" type="email" value={email} onChange={e => setEmail(e.target.value)} required autoComplete="email" />
|
|
186
|
+
</div>
|
|
187
|
+
<div className="space-y-2">
|
|
188
|
+
<Label htmlFor="password">Password</Label>
|
|
189
|
+
<Input id="password" type="password" value={password} onChange={e => setPassword(e.target.value)} required autoComplete="new-password" minLength={8} />
|
|
190
|
+
</div>
|
|
191
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
192
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
193
|
+
{loading ? 'Signing up…' : 'Sign up'}
|
|
194
|
+
</Button>
|
|
195
|
+
<p className="text-sm text-center text-muted-foreground">
|
|
196
|
+
Already have an account? <Link to="/login" className="underline">Log in</Link>
|
|
197
|
+
</p>
|
|
198
|
+
</form>
|
|
199
|
+
</CardContent>
|
|
200
|
+
</Card>
|
|
201
|
+
</div>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function dashboardPageTsx(): string {
|
|
208
|
+
return `import { createFileRoute, useRouter } from '@tanstack/react-router'
|
|
209
|
+
import { authClient } from '#/lib/auth-client'
|
|
210
|
+
import { Button } from '#/components/ui/button'
|
|
211
|
+
|
|
212
|
+
// Example protected page -- _authed.tsx needs at least one child route or
|
|
213
|
+
// it collides with index.tsx on path "/" and \`tsr generate\` fails. Replace
|
|
214
|
+
// this, or add more pages alongside it under src/routes/_authed/.
|
|
215
|
+
export const Route = createFileRoute('/_authed/dashboard')({
|
|
216
|
+
component: DashboardPage,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
function DashboardPage() {
|
|
220
|
+
const router = useRouter()
|
|
221
|
+
const { data: session } = authClient.useSession()
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div className="min-h-screen flex items-center justify-center bg-background p-4">
|
|
225
|
+
<div className="text-center space-y-4">
|
|
226
|
+
<p className="text-lg">Signed in as {session?.user.email}</p>
|
|
227
|
+
<Button
|
|
228
|
+
onClick={() =>
|
|
229
|
+
authClient.signOut({
|
|
230
|
+
fetchOptions: { onSuccess: () => router.navigate({ to: '/login' }) },
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
>
|
|
234
|
+
Sign out
|
|
235
|
+
</Button>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
`;
|
|
241
|
+
}
|