@rudderjs/auth 0.2.0
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/LICENSE +21 -0
- package/README.md +109 -0
- package/boost/guidelines.md +137 -0
- package/dist/auth-manager.d.ts +40 -0
- package/dist/auth-manager.d.ts.map +1 -0
- package/dist/auth-manager.js +85 -0
- package/dist/auth-manager.js.map +1 -0
- package/dist/contracts.d.ts +27 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +3 -0
- package/dist/contracts.js.map +1 -0
- package/dist/gate.d.ts +49 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +181 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/password-reset.d.ts +56 -0
- package/dist/password-reset.d.ts.map +1 -0
- package/dist/password-reset.js +101 -0
- package/dist/password-reset.js.map +1 -0
- package/dist/providers.d.ts +20 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +41 -0
- package/dist/providers.js.map +1 -0
- package/dist/session-guard.d.ts +21 -0
- package/dist/session-guard.d.ts.map +1 -0
- package/dist/session-guard.js +52 -0
- package/dist/session-guard.js.map +1 -0
- package/dist/verification.d.ts +58 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +93 -0
- package/dist/verification.js.map +1 -0
- package/package.json +57 -0
- package/pages/react/forgot-password/+Page.tsx +64 -0
- package/pages/react/login/+Page.tsx +70 -0
- package/pages/react/login/+guard.ts +15 -0
- package/pages/react/register/+Page.tsx +78 -0
- package/pages/react/register/+guard.ts +15 -0
- package/pages/react/reset-password/+Page.tsx +118 -0
- package/pages/solid/forgot-password/+Page.tsx +62 -0
- package/pages/solid/login/+Page.tsx +66 -0
- package/pages/solid/login/+guard.ts +15 -0
- package/pages/solid/register/+Page.tsx +72 -0
- package/pages/solid/register/+guard.ts +15 -0
- package/pages/solid/reset-password/+Page.tsx +94 -0
- package/pages/vue/forgot-password/+Page.vue +60 -0
- package/pages/vue/login/+Page.vue +63 -0
- package/pages/vue/login/+guard.ts +15 -0
- package/pages/vue/register/+Page.vue +68 -0
- package/pages/vue/register/+guard.ts +15 -0
- package/pages/vue/reset-password/+Page.vue +93 -0
- package/schema/auth.drizzle.mysql.ts +48 -0
- package/schema/auth.drizzle.pg.ts +48 -0
- package/schema/auth.drizzle.sqlite.ts +48 -0
- package/schema/auth.prisma +50 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import '@/index.css'
|
|
2
|
+
import { createSignal } from 'solid-js'
|
|
3
|
+
|
|
4
|
+
export default function ForgotPasswordPage() {
|
|
5
|
+
const [email, setEmail] = createSignal('')
|
|
6
|
+
const [error, setError] = createSignal('')
|
|
7
|
+
const [success, setSuccess] = createSignal('')
|
|
8
|
+
const [loading, setLoading] = createSignal(false)
|
|
9
|
+
|
|
10
|
+
async function handleSubmit(e: Event) {
|
|
11
|
+
e.preventDefault()
|
|
12
|
+
setError('')
|
|
13
|
+
setSuccess('')
|
|
14
|
+
setLoading(true)
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch('/api/auth/request-password-reset', {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify({ email: email(), redirectTo: '/reset-password' }),
|
|
20
|
+
})
|
|
21
|
+
if (res.ok) {
|
|
22
|
+
setSuccess('If an account exists with that email, a password reset link has been sent.')
|
|
23
|
+
} else {
|
|
24
|
+
const body = await res.json().catch(() => ({})) as { message?: string }
|
|
25
|
+
setError(body.message ?? 'Something went wrong. Please try again.')
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
setError('Something went wrong. Please try again.')
|
|
29
|
+
}
|
|
30
|
+
setLoading(false)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div class="flex min-h-svh items-center justify-center p-4">
|
|
35
|
+
<div class="w-full max-w-sm space-y-6">
|
|
36
|
+
<div class="text-center">
|
|
37
|
+
<h1 class="text-2xl font-bold">Forgot password</h1>
|
|
38
|
+
<p class="text-sm text-gray-500 mt-1">Enter your email to receive a reset link</p>
|
|
39
|
+
</div>
|
|
40
|
+
<form onSubmit={handleSubmit} class="space-y-4 rounded-lg border p-6 shadow-sm">
|
|
41
|
+
{error() && <p class="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{error()}</p>}
|
|
42
|
+
{success() && <p class="rounded-md bg-green-50 px-3 py-2 text-sm text-green-600">{success()}</p>}
|
|
43
|
+
<div>
|
|
44
|
+
<label class="block text-sm font-medium mb-1" for="email">Email</label>
|
|
45
|
+
<input id="email" type="email" placeholder="you@example.com"
|
|
46
|
+
value={email()} onInput={e => setEmail(e.currentTarget.value)}
|
|
47
|
+
required autocomplete="email"
|
|
48
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
49
|
+
</div>
|
|
50
|
+
<button type="submit" disabled={loading()}
|
|
51
|
+
class="w-full rounded-md bg-black px-4 py-2 text-sm font-medium text-white hover:bg-black/90 disabled:opacity-50">
|
|
52
|
+
{loading() ? 'Sending...' : 'Send reset link'}
|
|
53
|
+
</button>
|
|
54
|
+
<p class="text-center text-sm text-gray-500">
|
|
55
|
+
Remember your password?{' '}
|
|
56
|
+
<a href="/login" class="underline hover:text-black">Sign in</a>
|
|
57
|
+
</p>
|
|
58
|
+
</form>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import '@/index.css'
|
|
2
|
+
import { createSignal } from 'solid-js'
|
|
3
|
+
import { navigate } from 'vike/client/router'
|
|
4
|
+
|
|
5
|
+
export default function LoginPage() {
|
|
6
|
+
const [email, setEmail] = createSignal('')
|
|
7
|
+
const [password, setPassword] = createSignal('')
|
|
8
|
+
const [error, setError] = createSignal('')
|
|
9
|
+
const [loading, setLoading] = createSignal(false)
|
|
10
|
+
|
|
11
|
+
async function handleSubmit(e: Event) {
|
|
12
|
+
e.preventDefault()
|
|
13
|
+
setError('')
|
|
14
|
+
setLoading(true)
|
|
15
|
+
const res = await fetch('/api/auth/sign-in/email', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: JSON.stringify({ email: email(), password: password() }),
|
|
19
|
+
})
|
|
20
|
+
if (res.ok) {
|
|
21
|
+
const params = new URLSearchParams(window.location.search)
|
|
22
|
+
const redirect = params.get('redirect')
|
|
23
|
+
await navigate(redirect && redirect.startsWith('/') ? redirect : '/')
|
|
24
|
+
} else {
|
|
25
|
+
const body = await res.json().catch(() => ({})) as { message?: string }
|
|
26
|
+
setError(body.message ?? 'Invalid email or password.')
|
|
27
|
+
}
|
|
28
|
+
setLoading(false)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div class="flex min-h-svh items-center justify-center p-4">
|
|
33
|
+
<div class="w-full max-w-sm space-y-6">
|
|
34
|
+
<div class="text-center">
|
|
35
|
+
<h1 class="text-2xl font-bold">Welcome back</h1>
|
|
36
|
+
<p class="text-sm text-gray-500 mt-1">Sign in to your account</p>
|
|
37
|
+
</div>
|
|
38
|
+
<form onSubmit={handleSubmit} class="space-y-4 rounded-lg border p-6 shadow-sm">
|
|
39
|
+
{error() && <p class="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{error()}</p>}
|
|
40
|
+
<div>
|
|
41
|
+
<label class="block text-sm font-medium mb-1" for="email">Email</label>
|
|
42
|
+
<input id="email" type="email" placeholder="you@example.com"
|
|
43
|
+
value={email()} onInput={e => setEmail(e.currentTarget.value)}
|
|
44
|
+
required autocomplete="email"
|
|
45
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<label class="block text-sm font-medium mb-1" for="password">Password</label>
|
|
49
|
+
<input id="password" type="password" placeholder="••••••••"
|
|
50
|
+
value={password()} onInput={e => setPassword(e.currentTarget.value)}
|
|
51
|
+
required autocomplete="current-password"
|
|
52
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
53
|
+
</div>
|
|
54
|
+
<button type="submit" disabled={loading()}
|
|
55
|
+
class="w-full rounded-md bg-black px-4 py-2 text-sm font-medium text-white hover:bg-black/90 disabled:opacity-50">
|
|
56
|
+
{loading() ? 'Signing in…' : 'Sign in'}
|
|
57
|
+
</button>
|
|
58
|
+
<p class="text-center text-sm text-gray-500">
|
|
59
|
+
Don't have an account?{' '}
|
|
60
|
+
<a href="/register" class="underline hover:text-black">Register</a>
|
|
61
|
+
</p>
|
|
62
|
+
</form>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { redirect } from 'vike/abort'
|
|
2
|
+
import type { GuardAsync } from 'vike/types'
|
|
3
|
+
import type { BetterAuthInstance } from '@rudderjs/auth'
|
|
4
|
+
|
|
5
|
+
export const guard: GuardAsync = async (pageContext): ReturnType<GuardAsync> => {
|
|
6
|
+
// import.meta.env.SSR is a Vite compile-time constant — tree-shaken from client bundle
|
|
7
|
+
if (!import.meta.env.SSR) return
|
|
8
|
+
const { app } = await import('@rudderjs/core')
|
|
9
|
+
const auth = app().make<BetterAuthInstance>('auth')
|
|
10
|
+
const session = await auth.api.getSession({
|
|
11
|
+
headers: new Headers(pageContext.headers ?? {}),
|
|
12
|
+
})
|
|
13
|
+
// Already logged in — redirect to home
|
|
14
|
+
if (session?.user) throw redirect('/')
|
|
15
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import '@/index.css'
|
|
2
|
+
import { createSignal } from 'solid-js'
|
|
3
|
+
import { navigate } from 'vike/client/router'
|
|
4
|
+
|
|
5
|
+
export default function RegisterPage() {
|
|
6
|
+
const [name, setName] = createSignal('')
|
|
7
|
+
const [email, setEmail] = createSignal('')
|
|
8
|
+
const [password, setPassword] = createSignal('')
|
|
9
|
+
const [error, setError] = createSignal('')
|
|
10
|
+
const [loading, setLoading] = createSignal(false)
|
|
11
|
+
|
|
12
|
+
async function handleSubmit(e: Event) {
|
|
13
|
+
e.preventDefault()
|
|
14
|
+
setError('')
|
|
15
|
+
setLoading(true)
|
|
16
|
+
const res = await fetch('/api/auth/sign-up/email', {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify({ name: name(), email: email(), password: password() }),
|
|
20
|
+
})
|
|
21
|
+
if (res.ok) {
|
|
22
|
+
await navigate('/')
|
|
23
|
+
} else {
|
|
24
|
+
const body = await res.json().catch(() => ({})) as { message?: string }
|
|
25
|
+
setError(body.message ?? 'Could not create account. Please try again.')
|
|
26
|
+
}
|
|
27
|
+
setLoading(false)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div class="flex min-h-svh items-center justify-center p-4">
|
|
32
|
+
<div class="w-full max-w-sm space-y-6">
|
|
33
|
+
<div class="text-center">
|
|
34
|
+
<h1 class="text-2xl font-bold">Create an account</h1>
|
|
35
|
+
<p class="text-sm text-gray-500 mt-1">Get started in seconds</p>
|
|
36
|
+
</div>
|
|
37
|
+
<form onSubmit={handleSubmit} class="space-y-4 rounded-lg border p-6 shadow-sm">
|
|
38
|
+
{error() && <p class="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{error()}</p>}
|
|
39
|
+
<div>
|
|
40
|
+
<label class="block text-sm font-medium mb-1" for="name">Name</label>
|
|
41
|
+
<input id="name" type="text" placeholder="Alice Smith"
|
|
42
|
+
value={name()} onInput={e => setName(e.currentTarget.value)}
|
|
43
|
+
required autocomplete="name"
|
|
44
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<label class="block text-sm font-medium mb-1" for="email">Email</label>
|
|
48
|
+
<input id="email" type="email" placeholder="you@example.com"
|
|
49
|
+
value={email()} onInput={e => setEmail(e.currentTarget.value)}
|
|
50
|
+
required autocomplete="email"
|
|
51
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
52
|
+
</div>
|
|
53
|
+
<div>
|
|
54
|
+
<label class="block text-sm font-medium mb-1" for="password">Password</label>
|
|
55
|
+
<input id="password" type="password" placeholder="••••••••"
|
|
56
|
+
value={password()} onInput={e => setPassword(e.currentTarget.value)}
|
|
57
|
+
required autocomplete="new-password" minLength={8}
|
|
58
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
59
|
+
</div>
|
|
60
|
+
<button type="submit" disabled={loading()}
|
|
61
|
+
class="w-full rounded-md bg-black px-4 py-2 text-sm font-medium text-white hover:bg-black/90 disabled:opacity-50">
|
|
62
|
+
{loading() ? 'Creating account…' : 'Create account'}
|
|
63
|
+
</button>
|
|
64
|
+
<p class="text-center text-sm text-gray-500">
|
|
65
|
+
Already have an account?{' '}
|
|
66
|
+
<a href="/login" class="underline hover:text-black">Sign in</a>
|
|
67
|
+
</p>
|
|
68
|
+
</form>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { redirect } from 'vike/abort'
|
|
2
|
+
import type { GuardAsync } from 'vike/types'
|
|
3
|
+
import type { BetterAuthInstance } from '@rudderjs/auth'
|
|
4
|
+
|
|
5
|
+
export const guard: GuardAsync = async (pageContext): ReturnType<GuardAsync> => {
|
|
6
|
+
// import.meta.env.SSR is a Vite compile-time constant — tree-shaken from client bundle
|
|
7
|
+
if (!import.meta.env.SSR) return
|
|
8
|
+
const { app } = await import('@rudderjs/core')
|
|
9
|
+
const auth = app().make<BetterAuthInstance>('auth')
|
|
10
|
+
const session = await auth.api.getSession({
|
|
11
|
+
headers: new Headers(pageContext.headers ?? {}),
|
|
12
|
+
})
|
|
13
|
+
// Already registered and logged in — redirect to home
|
|
14
|
+
if (session?.user) throw redirect('/')
|
|
15
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import '@/index.css'
|
|
2
|
+
import { createSignal, Show } from 'solid-js'
|
|
3
|
+
|
|
4
|
+
export default function ResetPasswordPage() {
|
|
5
|
+
const [password, setPassword] = createSignal('')
|
|
6
|
+
const [confirmPassword, setConfirm] = createSignal('')
|
|
7
|
+
const [error, setError] = createSignal('')
|
|
8
|
+
const [success, setSuccess] = createSignal('')
|
|
9
|
+
const [loading, setLoading] = createSignal(false)
|
|
10
|
+
|
|
11
|
+
const params = new URLSearchParams(window.location.search)
|
|
12
|
+
const token = params.get('token')
|
|
13
|
+
|
|
14
|
+
async function handleSubmit(e: Event) {
|
|
15
|
+
e.preventDefault()
|
|
16
|
+
setError('')
|
|
17
|
+
setSuccess('')
|
|
18
|
+
|
|
19
|
+
if (password() !== confirmPassword()) {
|
|
20
|
+
setError('Passwords do not match.')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setLoading(true)
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch('/api/auth/reset-password', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ token, newPassword: password() }),
|
|
30
|
+
})
|
|
31
|
+
if (res.ok) {
|
|
32
|
+
setSuccess('Your password has been reset successfully.')
|
|
33
|
+
} else {
|
|
34
|
+
const body = await res.json().catch(() => ({})) as { message?: string }
|
|
35
|
+
setError(body.message ?? 'Invalid or expired token.')
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
setError('Something went wrong. Please try again.')
|
|
39
|
+
}
|
|
40
|
+
setLoading(false)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div class="flex min-h-svh items-center justify-center p-4">
|
|
45
|
+
<div class="w-full max-w-sm space-y-6">
|
|
46
|
+
<Show when={token} fallback={
|
|
47
|
+
<div class="space-y-4 rounded-lg border p-6 shadow-sm">
|
|
48
|
+
<p class="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">Missing reset token.</p>
|
|
49
|
+
<p class="text-center text-sm text-gray-500">
|
|
50
|
+
<a href="/forgot-password" class="underline hover:text-black">Request a new reset link</a>
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
}>
|
|
54
|
+
<div class="text-center">
|
|
55
|
+
<h1 class="text-2xl font-bold">Reset password</h1>
|
|
56
|
+
<p class="text-sm text-gray-500 mt-1">Enter your new password</p>
|
|
57
|
+
</div>
|
|
58
|
+
<form onSubmit={handleSubmit} class="space-y-4 rounded-lg border p-6 shadow-sm">
|
|
59
|
+
{error() && <p class="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{error()}</p>}
|
|
60
|
+
<Show when={success()} fallback={
|
|
61
|
+
<>
|
|
62
|
+
<div>
|
|
63
|
+
<label class="block text-sm font-medium mb-1" for="password">New password</label>
|
|
64
|
+
<input id="password" type="password" placeholder="••••••••"
|
|
65
|
+
value={password()} onInput={e => setPassword(e.currentTarget.value)}
|
|
66
|
+
required minLength={8} autocomplete="new-password"
|
|
67
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<label class="block text-sm font-medium mb-1" for="confirm-password">Confirm password</label>
|
|
71
|
+
<input id="confirm-password" type="password" placeholder="••••••••"
|
|
72
|
+
value={confirmPassword()} onInput={e => setConfirm(e.currentTarget.value)}
|
|
73
|
+
required minLength={8} autocomplete="new-password"
|
|
74
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
75
|
+
</div>
|
|
76
|
+
<button type="submit" disabled={loading()}
|
|
77
|
+
class="w-full rounded-md bg-black px-4 py-2 text-sm font-medium text-white hover:bg-black/90 disabled:opacity-50">
|
|
78
|
+
{loading() ? 'Resetting...' : 'Reset password'}
|
|
79
|
+
</button>
|
|
80
|
+
</>
|
|
81
|
+
}>
|
|
82
|
+
<div class="space-y-2">
|
|
83
|
+
<p class="rounded-md bg-green-50 px-3 py-2 text-sm text-green-600">{success()}</p>
|
|
84
|
+
<p class="text-center text-sm text-gray-500">
|
|
85
|
+
<a href="/login" class="underline hover:text-black">Sign in</a>
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
</Show>
|
|
89
|
+
</form>
|
|
90
|
+
</Show>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import '@/index.css'
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
|
|
5
|
+
const email = ref('')
|
|
6
|
+
const error = ref('')
|
|
7
|
+
const success = ref('')
|
|
8
|
+
const loading = ref(false)
|
|
9
|
+
|
|
10
|
+
async function handleSubmit() {
|
|
11
|
+
error.value = ''
|
|
12
|
+
success.value = ''
|
|
13
|
+
loading.value = true
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch('/api/auth/request-password-reset', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: JSON.stringify({ email: email.value, redirectTo: '/reset-password' }),
|
|
19
|
+
})
|
|
20
|
+
if (res.ok) {
|
|
21
|
+
success.value = 'If an account exists with that email, a password reset link has been sent.'
|
|
22
|
+
} else {
|
|
23
|
+
const body = await res.json().catch(() => ({})) as { message?: string }
|
|
24
|
+
error.value = body.message ?? 'Something went wrong. Please try again.'
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
error.value = 'Something went wrong. Please try again.'
|
|
28
|
+
}
|
|
29
|
+
loading.value = false
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div class="flex min-h-svh items-center justify-center p-4">
|
|
35
|
+
<div class="w-full max-w-sm space-y-6">
|
|
36
|
+
<div class="text-center">
|
|
37
|
+
<h1 class="text-2xl font-bold">Forgot password</h1>
|
|
38
|
+
<p class="text-sm text-gray-500 mt-1">Enter your email to receive a reset link</p>
|
|
39
|
+
</div>
|
|
40
|
+
<form @submit.prevent="handleSubmit" class="space-y-4 rounded-lg border p-6 shadow-sm">
|
|
41
|
+
<p v-if="error" class="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{{ error }}</p>
|
|
42
|
+
<p v-if="success" class="rounded-md bg-green-50 px-3 py-2 text-sm text-green-600">{{ success }}</p>
|
|
43
|
+
<div>
|
|
44
|
+
<label class="block text-sm font-medium mb-1" for="email">Email</label>
|
|
45
|
+
<input id="email" v-model="email" type="email" placeholder="you@example.com"
|
|
46
|
+
required autocomplete="email"
|
|
47
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
48
|
+
</div>
|
|
49
|
+
<button type="submit" :disabled="loading"
|
|
50
|
+
class="w-full rounded-md bg-black px-4 py-2 text-sm font-medium text-white hover:bg-black/90 disabled:opacity-50">
|
|
51
|
+
{{ loading ? 'Sending...' : 'Send reset link' }}
|
|
52
|
+
</button>
|
|
53
|
+
<p class="text-center text-sm text-gray-500">
|
|
54
|
+
Remember your password?
|
|
55
|
+
<a href="/login" class="underline hover:text-black">Sign in</a>
|
|
56
|
+
</p>
|
|
57
|
+
</form>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import '@/index.css'
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
import { navigate } from 'vike/client/router'
|
|
5
|
+
|
|
6
|
+
const email = ref('')
|
|
7
|
+
const password = ref('')
|
|
8
|
+
const error = ref('')
|
|
9
|
+
const loading = ref(false)
|
|
10
|
+
|
|
11
|
+
async function handleSubmit() {
|
|
12
|
+
error.value = ''
|
|
13
|
+
loading.value = true
|
|
14
|
+
const res = await fetch('/api/auth/sign-in/email', {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
body: JSON.stringify({ email: email.value, password: password.value }),
|
|
18
|
+
})
|
|
19
|
+
if (res.ok) {
|
|
20
|
+
const params = new URLSearchParams(window.location.search)
|
|
21
|
+
const redirect = params.get('redirect')
|
|
22
|
+
await navigate(redirect && redirect.startsWith('/') ? redirect : '/')
|
|
23
|
+
} else {
|
|
24
|
+
const body = await res.json().catch(() => ({})) as { message?: string }
|
|
25
|
+
error.value = body.message ?? 'Invalid email or password.'
|
|
26
|
+
}
|
|
27
|
+
loading.value = false
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="flex min-h-svh items-center justify-center p-4">
|
|
33
|
+
<div class="w-full max-w-sm space-y-6">
|
|
34
|
+
<div class="text-center">
|
|
35
|
+
<h1 class="text-2xl font-bold">Welcome back</h1>
|
|
36
|
+
<p class="text-sm text-gray-500 mt-1">Sign in to your account</p>
|
|
37
|
+
</div>
|
|
38
|
+
<form @submit.prevent="handleSubmit" class="space-y-4 rounded-lg border p-6 shadow-sm">
|
|
39
|
+
<p v-if="error" class="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{{ error }}</p>
|
|
40
|
+
<div>
|
|
41
|
+
<label class="block text-sm font-medium mb-1" for="email">Email</label>
|
|
42
|
+
<input id="email" v-model="email" type="email" placeholder="you@example.com"
|
|
43
|
+
required autocomplete="email"
|
|
44
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<label class="block text-sm font-medium mb-1" for="password">Password</label>
|
|
48
|
+
<input id="password" v-model="password" type="password" placeholder="••••••••"
|
|
49
|
+
required autocomplete="current-password"
|
|
50
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
51
|
+
</div>
|
|
52
|
+
<button type="submit" :disabled="loading"
|
|
53
|
+
class="w-full rounded-md bg-black px-4 py-2 text-sm font-medium text-white hover:bg-black/90 disabled:opacity-50">
|
|
54
|
+
{{ loading ? 'Signing in…' : 'Sign in' }}
|
|
55
|
+
</button>
|
|
56
|
+
<p class="text-center text-sm text-gray-500">
|
|
57
|
+
Don't have an account?
|
|
58
|
+
<a href="/register" class="underline hover:text-black">Register</a>
|
|
59
|
+
</p>
|
|
60
|
+
</form>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { redirect } from 'vike/abort'
|
|
2
|
+
import type { GuardAsync } from 'vike/types'
|
|
3
|
+
import type { BetterAuthInstance } from '@rudderjs/auth'
|
|
4
|
+
|
|
5
|
+
export const guard: GuardAsync = async (pageContext): ReturnType<GuardAsync> => {
|
|
6
|
+
// import.meta.env.SSR is a Vite compile-time constant — tree-shaken from client bundle
|
|
7
|
+
if (!import.meta.env.SSR) return
|
|
8
|
+
const { app } = await import('@rudderjs/core')
|
|
9
|
+
const auth = app().make<BetterAuthInstance>('auth')
|
|
10
|
+
const session = await auth.api.getSession({
|
|
11
|
+
headers: new Headers(pageContext.headers ?? {}),
|
|
12
|
+
})
|
|
13
|
+
// Already logged in — redirect to home
|
|
14
|
+
if (session?.user) throw redirect('/')
|
|
15
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import '@/index.css'
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
import { navigate } from 'vike/client/router'
|
|
5
|
+
|
|
6
|
+
const name = ref('')
|
|
7
|
+
const email = ref('')
|
|
8
|
+
const password = ref('')
|
|
9
|
+
const error = ref('')
|
|
10
|
+
const loading = ref(false)
|
|
11
|
+
|
|
12
|
+
async function handleSubmit() {
|
|
13
|
+
error.value = ''
|
|
14
|
+
loading.value = true
|
|
15
|
+
const res = await fetch('/api/auth/sign-up/email', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: JSON.stringify({ name: name.value, email: email.value, password: password.value }),
|
|
19
|
+
})
|
|
20
|
+
if (res.ok) {
|
|
21
|
+
await navigate('/')
|
|
22
|
+
} else {
|
|
23
|
+
const body = await res.json().catch(() => ({})) as { message?: string }
|
|
24
|
+
error.value = body.message ?? 'Could not create account. Please try again.'
|
|
25
|
+
}
|
|
26
|
+
loading.value = false
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="flex min-h-svh items-center justify-center p-4">
|
|
32
|
+
<div class="w-full max-w-sm space-y-6">
|
|
33
|
+
<div class="text-center">
|
|
34
|
+
<h1 class="text-2xl font-bold">Create an account</h1>
|
|
35
|
+
<p class="text-sm text-gray-500 mt-1">Get started in seconds</p>
|
|
36
|
+
</div>
|
|
37
|
+
<form @submit.prevent="handleSubmit" class="space-y-4 rounded-lg border p-6 shadow-sm">
|
|
38
|
+
<p v-if="error" class="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{{ error }}</p>
|
|
39
|
+
<div>
|
|
40
|
+
<label class="block text-sm font-medium mb-1" for="name">Name</label>
|
|
41
|
+
<input id="name" v-model="name" type="text" placeholder="Alice Smith"
|
|
42
|
+
required autocomplete="name"
|
|
43
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
44
|
+
</div>
|
|
45
|
+
<div>
|
|
46
|
+
<label class="block text-sm font-medium mb-1" for="email">Email</label>
|
|
47
|
+
<input id="email" v-model="email" type="email" placeholder="you@example.com"
|
|
48
|
+
required autocomplete="email"
|
|
49
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<label class="block text-sm font-medium mb-1" for="password">Password</label>
|
|
53
|
+
<input id="password" v-model="password" type="password" placeholder="••••••••"
|
|
54
|
+
required autocomplete="new-password" minlength="8"
|
|
55
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
56
|
+
</div>
|
|
57
|
+
<button type="submit" :disabled="loading"
|
|
58
|
+
class="w-full rounded-md bg-black px-4 py-2 text-sm font-medium text-white hover:bg-black/90 disabled:opacity-50">
|
|
59
|
+
{{ loading ? 'Creating account…' : 'Create account' }}
|
|
60
|
+
</button>
|
|
61
|
+
<p class="text-center text-sm text-gray-500">
|
|
62
|
+
Already have an account?
|
|
63
|
+
<a href="/login" class="underline hover:text-black">Sign in</a>
|
|
64
|
+
</p>
|
|
65
|
+
</form>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { redirect } from 'vike/abort'
|
|
2
|
+
import type { GuardAsync } from 'vike/types'
|
|
3
|
+
import type { BetterAuthInstance } from '@rudderjs/auth'
|
|
4
|
+
|
|
5
|
+
export const guard: GuardAsync = async (pageContext): ReturnType<GuardAsync> => {
|
|
6
|
+
// import.meta.env.SSR is a Vite compile-time constant — tree-shaken from client bundle
|
|
7
|
+
if (!import.meta.env.SSR) return
|
|
8
|
+
const { app } = await import('@rudderjs/core')
|
|
9
|
+
const auth = app().make<BetterAuthInstance>('auth')
|
|
10
|
+
const session = await auth.api.getSession({
|
|
11
|
+
headers: new Headers(pageContext.headers ?? {}),
|
|
12
|
+
})
|
|
13
|
+
// Already registered and logged in — redirect to home
|
|
14
|
+
if (session?.user) throw redirect('/')
|
|
15
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import '@/index.css'
|
|
3
|
+
import { ref, computed } from 'vue'
|
|
4
|
+
|
|
5
|
+
const password = ref('')
|
|
6
|
+
const confirmPassword = ref('')
|
|
7
|
+
const error = ref('')
|
|
8
|
+
const success = ref('')
|
|
9
|
+
const loading = ref(false)
|
|
10
|
+
|
|
11
|
+
const params = new URLSearchParams(window.location.search)
|
|
12
|
+
const token = params.get('token')
|
|
13
|
+
|
|
14
|
+
const hasToken = computed(() => !!token)
|
|
15
|
+
|
|
16
|
+
async function handleSubmit() {
|
|
17
|
+
error.value = ''
|
|
18
|
+
success.value = ''
|
|
19
|
+
|
|
20
|
+
if (password.value !== confirmPassword.value) {
|
|
21
|
+
error.value = 'Passwords do not match.'
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
loading.value = true
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch('/api/auth/reset-password', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify({ token, newPassword: password.value }),
|
|
31
|
+
})
|
|
32
|
+
if (res.ok) {
|
|
33
|
+
success.value = 'Your password has been reset successfully.'
|
|
34
|
+
} else {
|
|
35
|
+
const body = await res.json().catch(() => ({})) as { message?: string }
|
|
36
|
+
error.value = body.message ?? 'Invalid or expired token.'
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
error.value = 'Something went wrong. Please try again.'
|
|
40
|
+
}
|
|
41
|
+
loading.value = false
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div class="flex min-h-svh items-center justify-center p-4">
|
|
47
|
+
<div class="w-full max-w-sm space-y-6">
|
|
48
|
+
<template v-if="!hasToken">
|
|
49
|
+
<div class="space-y-4 rounded-lg border p-6 shadow-sm">
|
|
50
|
+
<p class="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">Missing reset token.</p>
|
|
51
|
+
<p class="text-center text-sm text-gray-500">
|
|
52
|
+
<a href="/forgot-password" class="underline hover:text-black">Request a new reset link</a>
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
<template v-else>
|
|
57
|
+
<div class="text-center">
|
|
58
|
+
<h1 class="text-2xl font-bold">Reset password</h1>
|
|
59
|
+
<p class="text-sm text-gray-500 mt-1">Enter your new password</p>
|
|
60
|
+
</div>
|
|
61
|
+
<form @submit.prevent="handleSubmit" class="space-y-4 rounded-lg border p-6 shadow-sm">
|
|
62
|
+
<p v-if="error" class="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{{ error }}</p>
|
|
63
|
+
<template v-if="success">
|
|
64
|
+
<div class="space-y-2">
|
|
65
|
+
<p class="rounded-md bg-green-50 px-3 py-2 text-sm text-green-600">{{ success }}</p>
|
|
66
|
+
<p class="text-center text-sm text-gray-500">
|
|
67
|
+
<a href="/login" class="underline hover:text-black">Sign in</a>
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
<template v-else>
|
|
72
|
+
<div>
|
|
73
|
+
<label class="block text-sm font-medium mb-1" for="password">New password</label>
|
|
74
|
+
<input id="password" v-model="password" type="password" placeholder="••••••••"
|
|
75
|
+
required minlength="8" autocomplete="new-password"
|
|
76
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
77
|
+
</div>
|
|
78
|
+
<div>
|
|
79
|
+
<label class="block text-sm font-medium mb-1" for="confirm-password">Confirm password</label>
|
|
80
|
+
<input id="confirm-password" v-model="confirmPassword" type="password" placeholder="••••••••"
|
|
81
|
+
required minlength="8" autocomplete="new-password"
|
|
82
|
+
class="w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-black" />
|
|
83
|
+
</div>
|
|
84
|
+
<button type="submit" :disabled="loading"
|
|
85
|
+
class="w-full rounded-md bg-black px-4 py-2 text-sm font-medium text-white hover:bg-black/90 disabled:opacity-50">
|
|
86
|
+
{{ loading ? 'Resetting...' : 'Reset password' }}
|
|
87
|
+
</button>
|
|
88
|
+
</template>
|
|
89
|
+
</form>
|
|
90
|
+
</template>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|