@roboticela/devkit 3.0.0 → 4.1.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/README.md +12 -0
- package/dist/commands/add.js +103 -4
- package/dist/commands/eject.d.ts +1 -0
- package/dist/commands/eject.js +60 -0
- package/dist/index.js +10 -19
- package/dist/lib/bundled-registry.d.ts +18 -0
- package/dist/lib/bundled-registry.js +82 -0
- package/dist/lib/component-entry.d.ts +16 -0
- package/dist/lib/component-entry.js +1 -0
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.js +3 -1
- package/dist/lib/installer.js +9 -7
- package/dist/lib/registry.d.ts +2 -16
- package/dist/lib/registry.js +10 -0
- package/package.json +4 -2
- package/registry/components/auth/component.json +29 -0
- package/registry/components/auth/nextjs-compact/v1/files/app/api/auth/[...route]/route.ts +140 -0
- package/registry/components/auth/nextjs-compact/v1/files/app/auth/forgot-password/page.tsx +150 -0
- package/registry/components/auth/nextjs-compact/v1/files/app/auth/login/page.tsx +45 -0
- package/registry/components/auth/nextjs-compact/v1/files/app/auth/register/page.tsx +45 -0
- package/registry/components/auth/nextjs-compact/v1/files/app/auth/reset-password/page.tsx +139 -0
- package/registry/components/auth/nextjs-compact/v1/files/components/auth/AuthProvider.tsx +89 -0
- package/registry/components/auth/nextjs-compact/v1/files/components/auth/LoginForm.tsx +123 -0
- package/registry/components/auth/nextjs-compact/v1/files/components/auth/RegisterForm.tsx +106 -0
- package/registry/components/auth/nextjs-compact/v1/files/lib/auth/authService.ts +43 -0
- package/registry/components/auth/nextjs-compact/v1/manifest.json +37 -0
- package/registry/components/auth/vite-express-tauri/v1/files/server/middleware/requireAuth.ts +14 -0
- package/registry/components/auth/vite-express-tauri/v1/files/server/routes/auth.ts +83 -0
- package/registry/components/auth/vite-express-tauri/v1/files/server/services/jwtService.ts +19 -0
- package/registry/components/auth/vite-express-tauri/v1/files/src/contexts/AuthContext.tsx +72 -0
- package/registry/components/auth/vite-express-tauri/v1/manifest.json +47 -0
- package/registry/components/hero-section/component.json +28 -0
- package/registry/components/hero-section/nextjs-compact/v1/variants/centered/files/components/hero/HeroSection.tsx +97 -0
- package/registry/components/hero-section/nextjs-compact/v1/variants/centered/manifest.json +17 -0
- package/registry/components/hero-section/nextjs-compact/v1/variants/gradient-mesh/files/components/hero/HeroSection.tsx +146 -0
- package/registry/components/hero-section/nextjs-compact/v1/variants/gradient-mesh/manifest.json +17 -0
- package/registry/components/hero-section/nextjs-compact/v1/variants/split-image/files/components/hero/HeroSection.tsx +110 -0
- package/registry/components/hero-section/nextjs-compact/v1/variants/split-image/manifest.json +17 -0
- package/registry/components/registry.json +31 -0
- package/schemas/devkit-config.json +63 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { signToken, verifyToken } from "@/lib/auth/authService";
|
|
3
|
+
|
|
4
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
function json(data: unknown, status = 200) {
|
|
7
|
+
return NextResponse.json(data, { status });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function err(message: string, status = 400) {
|
|
11
|
+
return json({ message }, status);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ── Route dispatcher ─────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export async function POST(req: NextRequest, { params }: { params: Promise<{ route: string[] }> }) {
|
|
17
|
+
const { route } = await params;
|
|
18
|
+
const action = route.join("/");
|
|
19
|
+
|
|
20
|
+
if (action === "login") return handleLogin(req);
|
|
21
|
+
if (action === "register") return handleRegister(req);
|
|
22
|
+
if (action === "logout") return handleLogout();
|
|
23
|
+
if (action === "refresh") return handleRefresh(req);
|
|
24
|
+
if (action === "forgot-password") return handleForgotPassword(req);
|
|
25
|
+
if (action === "reset-password") return handleResetPassword(req);
|
|
26
|
+
|
|
27
|
+
return err("Not found", 404);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function GET(req: NextRequest, { params }: { params: Promise<{ route: string[] }> }) {
|
|
31
|
+
const { route } = await params;
|
|
32
|
+
const action = route.join("/");
|
|
33
|
+
|
|
34
|
+
if (action === "me") return handleMe(req);
|
|
35
|
+
|
|
36
|
+
return err("Not found", 404);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Handlers ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
async function handleLogin(req: NextRequest) {
|
|
42
|
+
const { email, password } = await req.json();
|
|
43
|
+
if (!email || !password) return err("Email and password are required");
|
|
44
|
+
|
|
45
|
+
// Replace with your real user lookup + bcrypt.compare
|
|
46
|
+
const user = await findUserByCredentials(email, password);
|
|
47
|
+
if (!user) return err("Invalid email or password", 401);
|
|
48
|
+
|
|
49
|
+
const token = await signToken({ sub: user.id, email: user.email });
|
|
50
|
+
return json({ user, token });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function handleRegister(req: NextRequest) {
|
|
54
|
+
const { name, email, password } = await req.json();
|
|
55
|
+
if (!name || !email || !password) return err("Name, email, and password are required");
|
|
56
|
+
if (password.length < 8) return err("Password must be at least 8 characters");
|
|
57
|
+
|
|
58
|
+
// Replace with your real user creation logic
|
|
59
|
+
const user = await createUser(name, email, password);
|
|
60
|
+
const token = await signToken({ sub: user.id, email: user.email });
|
|
61
|
+
return json({ user, token }, 201);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function handleLogout() {
|
|
65
|
+
const res = json({ ok: true });
|
|
66
|
+
res.cookies.delete("refresh_token");
|
|
67
|
+
return res;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function handleRefresh(req: NextRequest) {
|
|
71
|
+
const refreshToken = req.cookies.get("refresh_token")?.value;
|
|
72
|
+
if (!refreshToken) return err("No refresh token", 401);
|
|
73
|
+
try {
|
|
74
|
+
const payload = await verifyToken(refreshToken);
|
|
75
|
+
const accessToken = await signToken({ sub: payload.sub, email: payload.email });
|
|
76
|
+
return json({ token: accessToken });
|
|
77
|
+
} catch {
|
|
78
|
+
return err("Invalid refresh token", 401);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleForgotPassword(req: NextRequest) {
|
|
83
|
+
const { email } = await req.json();
|
|
84
|
+
if (!email) return err("Email is required");
|
|
85
|
+
// Replace: look up user, generate reset token, send email
|
|
86
|
+
await sendPasswordResetEmail(email);
|
|
87
|
+
return json({ ok: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function handleResetPassword(req: NextRequest) {
|
|
91
|
+
const { token, password } = await req.json();
|
|
92
|
+
if (!token || !password) return err("Token and password are required");
|
|
93
|
+
// Replace: verify token, update user password
|
|
94
|
+
await resetUserPassword(token, password);
|
|
95
|
+
return json({ ok: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleMe(req: NextRequest) {
|
|
99
|
+
const authHeader = req.headers.get("authorization");
|
|
100
|
+
const token = authHeader?.replace("Bearer ", "");
|
|
101
|
+
if (!token) return err("Unauthorized", 401);
|
|
102
|
+
try {
|
|
103
|
+
const payload = await verifyToken(token);
|
|
104
|
+
const user = await findUserById(payload.sub);
|
|
105
|
+
if (!user) return err("User not found", 404);
|
|
106
|
+
return json({ user });
|
|
107
|
+
} catch {
|
|
108
|
+
return err("Invalid token", 401);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Stubs (replace with real DB calls) ───────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
async function findUserByCredentials(email: string, _password: string) {
|
|
115
|
+
// TODO: look up user in DB, compare password with bcrypt
|
|
116
|
+
void email;
|
|
117
|
+
return null as { id: string; name: string; email: string } | null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function createUser(name: string, email: string, _password: string) {
|
|
121
|
+
// TODO: hash password with bcrypt, insert into DB
|
|
122
|
+
void name; void email;
|
|
123
|
+
return { id: "stub", name, email };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function findUserById(id: string) {
|
|
127
|
+
// TODO: look up user in DB by id
|
|
128
|
+
void id;
|
|
129
|
+
return null as { id: string; name: string; email: string } | null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function sendPasswordResetEmail(email: string) {
|
|
133
|
+
// TODO: generate token, store in DB, send via nodemailer
|
|
134
|
+
void email;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function resetUserPassword(token: string, password: string) {
|
|
138
|
+
// TODO: verify token from DB, hash new password, update user
|
|
139
|
+
void token; void password;
|
|
140
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
export default function ForgotPasswordPage() {
|
|
6
|
+
const [email, setEmail] = useState("");
|
|
7
|
+
const [submitted, setSubmitted] = useState(false);
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
const [error, setError] = useState("");
|
|
10
|
+
|
|
11
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
setLoading(true);
|
|
14
|
+
setError("");
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch("/api/auth/forgot-password", {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
body: JSON.stringify({ email }),
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
const d = await res.json();
|
|
23
|
+
throw new Error(d.message || "Failed to send reset email");
|
|
24
|
+
}
|
|
25
|
+
setSubmitted(true);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
setError(err instanceof Error ? err.message : "Something went wrong");
|
|
28
|
+
} finally {
|
|
29
|
+
setLoading(false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<main
|
|
35
|
+
style={{
|
|
36
|
+
minHeight: "100vh",
|
|
37
|
+
display: "flex",
|
|
38
|
+
alignItems: "center",
|
|
39
|
+
justifyContent: "center",
|
|
40
|
+
background: "var(--color-bg-subtle)",
|
|
41
|
+
padding: "var(--space-4)",
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<div
|
|
45
|
+
style={{
|
|
46
|
+
width: "100%",
|
|
47
|
+
maxWidth: "420px",
|
|
48
|
+
background: "var(--color-bg)",
|
|
49
|
+
borderRadius: "var(--radius-xl)",
|
|
50
|
+
border: "1px solid var(--color-border)",
|
|
51
|
+
padding: "var(--space-8)",
|
|
52
|
+
boxShadow: "var(--shadow-lg)",
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{submitted ? (
|
|
56
|
+
<div style={{ textAlign: "center" }}>
|
|
57
|
+
<div style={{ fontSize: "3rem", marginBottom: "var(--space-4)" }}>📬</div>
|
|
58
|
+
<h1 style={{ fontSize: "var(--text-xl)", fontWeight: "var(--weight-bold)", color: "var(--color-text)" }}>
|
|
59
|
+
Check your email
|
|
60
|
+
</h1>
|
|
61
|
+
<p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
|
|
62
|
+
We sent a password reset link to <strong>{email}</strong>
|
|
63
|
+
</p>
|
|
64
|
+
<a
|
|
65
|
+
href="/auth/login"
|
|
66
|
+
style={{
|
|
67
|
+
display: "inline-block",
|
|
68
|
+
marginTop: "var(--space-6)",
|
|
69
|
+
color: "var(--color-primary)",
|
|
70
|
+
fontSize: "var(--text-sm)",
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
← Back to sign in
|
|
74
|
+
</a>
|
|
75
|
+
</div>
|
|
76
|
+
) : (
|
|
77
|
+
<>
|
|
78
|
+
<div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
|
|
79
|
+
<h1 style={{ fontSize: "var(--text-2xl)", fontWeight: "var(--weight-bold)", color: "var(--color-text)" }}>
|
|
80
|
+
Forgot password?
|
|
81
|
+
</h1>
|
|
82
|
+
<p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
|
|
83
|
+
Enter your email and we'll send you a reset link.
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{error && (
|
|
88
|
+
<div
|
|
89
|
+
style={{
|
|
90
|
+
marginBottom: "var(--space-4)",
|
|
91
|
+
padding: "var(--space-3) var(--space-4)",
|
|
92
|
+
borderRadius: "var(--radius-md)",
|
|
93
|
+
background: "var(--color-error-subtle)",
|
|
94
|
+
color: "var(--color-error-text)",
|
|
95
|
+
fontSize: "var(--text-sm)",
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{error}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "var(--space-4)" }}>
|
|
103
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-2)" }}>
|
|
104
|
+
<label htmlFor="email" style={{ fontSize: "var(--text-sm)", fontWeight: "var(--weight-medium)", color: "var(--color-text)" }}>
|
|
105
|
+
Email
|
|
106
|
+
</label>
|
|
107
|
+
<input
|
|
108
|
+
id="email"
|
|
109
|
+
type="email"
|
|
110
|
+
required
|
|
111
|
+
value={email}
|
|
112
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
113
|
+
style={{
|
|
114
|
+
padding: "var(--space-3) var(--space-4)",
|
|
115
|
+
borderRadius: "var(--radius-md)",
|
|
116
|
+
border: "1px solid var(--color-border)",
|
|
117
|
+
background: "var(--color-bg)",
|
|
118
|
+
color: "var(--color-text)",
|
|
119
|
+
fontSize: "var(--text-base)",
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
<button
|
|
124
|
+
type="submit"
|
|
125
|
+
disabled={loading}
|
|
126
|
+
style={{
|
|
127
|
+
padding: "var(--space-3) var(--space-4)",
|
|
128
|
+
borderRadius: "var(--radius-md)",
|
|
129
|
+
background: loading ? "var(--color-bg-muted)" : "var(--color-primary)",
|
|
130
|
+
color: "var(--color-primary-text)",
|
|
131
|
+
fontWeight: "var(--weight-semibold)",
|
|
132
|
+
border: "none",
|
|
133
|
+
cursor: loading ? "not-allowed" : "pointer",
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{loading ? "Sending…" : "Send reset link"}
|
|
137
|
+
</button>
|
|
138
|
+
<a
|
|
139
|
+
href="/auth/login"
|
|
140
|
+
style={{ textAlign: "center", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}
|
|
141
|
+
>
|
|
142
|
+
← Back to sign in
|
|
143
|
+
</a>
|
|
144
|
+
</form>
|
|
145
|
+
</>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
</main>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { LoginForm } from "@/components/auth/LoginForm";
|
|
2
|
+
|
|
3
|
+
export default function LoginPage() {
|
|
4
|
+
return (
|
|
5
|
+
<main
|
|
6
|
+
style={{
|
|
7
|
+
minHeight: "100vh",
|
|
8
|
+
display: "flex",
|
|
9
|
+
alignItems: "center",
|
|
10
|
+
justifyContent: "center",
|
|
11
|
+
background: "var(--color-bg-subtle)",
|
|
12
|
+
padding: "var(--space-4)",
|
|
13
|
+
}}
|
|
14
|
+
>
|
|
15
|
+
<div
|
|
16
|
+
style={{
|
|
17
|
+
width: "100%",
|
|
18
|
+
maxWidth: "420px",
|
|
19
|
+
background: "var(--color-bg)",
|
|
20
|
+
borderRadius: "var(--radius-xl)",
|
|
21
|
+
border: "1px solid var(--color-border)",
|
|
22
|
+
padding: "var(--space-8)",
|
|
23
|
+
boxShadow: "var(--shadow-lg)",
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
|
|
27
|
+
<h1
|
|
28
|
+
style={{
|
|
29
|
+
fontSize: "var(--text-2xl)",
|
|
30
|
+
fontWeight: "var(--weight-bold)",
|
|
31
|
+
color: "var(--color-text)",
|
|
32
|
+
fontFamily: "var(--font-display)",
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
Welcome back
|
|
36
|
+
</h1>
|
|
37
|
+
<p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
|
|
38
|
+
Sign in to your account
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
<LoginForm />
|
|
42
|
+
</div>
|
|
43
|
+
</main>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { RegisterForm } from "@/components/auth/RegisterForm";
|
|
2
|
+
|
|
3
|
+
export default function RegisterPage() {
|
|
4
|
+
return (
|
|
5
|
+
<main
|
|
6
|
+
style={{
|
|
7
|
+
minHeight: "100vh",
|
|
8
|
+
display: "flex",
|
|
9
|
+
alignItems: "center",
|
|
10
|
+
justifyContent: "center",
|
|
11
|
+
background: "var(--color-bg-subtle)",
|
|
12
|
+
padding: "var(--space-4)",
|
|
13
|
+
}}
|
|
14
|
+
>
|
|
15
|
+
<div
|
|
16
|
+
style={{
|
|
17
|
+
width: "100%",
|
|
18
|
+
maxWidth: "420px",
|
|
19
|
+
background: "var(--color-bg)",
|
|
20
|
+
borderRadius: "var(--radius-xl)",
|
|
21
|
+
border: "1px solid var(--color-border)",
|
|
22
|
+
padding: "var(--space-8)",
|
|
23
|
+
boxShadow: "var(--shadow-lg)",
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
|
|
27
|
+
<h1
|
|
28
|
+
style={{
|
|
29
|
+
fontSize: "var(--text-2xl)",
|
|
30
|
+
fontWeight: "var(--weight-bold)",
|
|
31
|
+
color: "var(--color-text)",
|
|
32
|
+
fontFamily: "var(--font-display)",
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
Create an account
|
|
36
|
+
</h1>
|
|
37
|
+
<p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
|
|
38
|
+
Get started for free
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
<RegisterForm />
|
|
42
|
+
</div>
|
|
43
|
+
</main>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, Suspense } from "react";
|
|
4
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
function ResetForm() {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
const params = useSearchParams();
|
|
9
|
+
const token = params.get("token") ?? "";
|
|
10
|
+
const [password, setPassword] = useState("");
|
|
11
|
+
const [confirm, setConfirm] = useState("");
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
const [error, setError] = useState("");
|
|
14
|
+
|
|
15
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
if (password !== confirm) { setError("Passwords do not match"); return; }
|
|
18
|
+
setLoading(true);
|
|
19
|
+
setError("");
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch("/api/auth/reset-password", {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
body: JSON.stringify({ token, password }),
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const d = await res.json();
|
|
28
|
+
throw new Error(d.message || "Failed to reset password");
|
|
29
|
+
}
|
|
30
|
+
router.push("/auth/login?reset=success");
|
|
31
|
+
} catch (err) {
|
|
32
|
+
setError(err instanceof Error ? err.message : "Something went wrong");
|
|
33
|
+
} finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!token) {
|
|
39
|
+
return (
|
|
40
|
+
<p style={{ color: "var(--color-error)", textAlign: "center" }}>
|
|
41
|
+
Invalid or missing reset token.
|
|
42
|
+
</p>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "var(--space-4)" }}>
|
|
48
|
+
{error && (
|
|
49
|
+
<div
|
|
50
|
+
style={{
|
|
51
|
+
padding: "var(--space-3) var(--space-4)",
|
|
52
|
+
borderRadius: "var(--radius-md)",
|
|
53
|
+
background: "var(--color-error-subtle)",
|
|
54
|
+
color: "var(--color-error-text)",
|
|
55
|
+
fontSize: "var(--text-sm)",
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{error}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
{(["New password", "Confirm password"] as const).map((label) => (
|
|
62
|
+
<div key={label} style={{ display: "flex", flexDirection: "column", gap: "var(--space-2)" }}>
|
|
63
|
+
<label style={{ fontSize: "var(--text-sm)", fontWeight: "var(--weight-medium)", color: "var(--color-text)" }}>
|
|
64
|
+
{label}
|
|
65
|
+
</label>
|
|
66
|
+
<input
|
|
67
|
+
type="password"
|
|
68
|
+
required
|
|
69
|
+
minLength={8}
|
|
70
|
+
value={label === "New password" ? password : confirm}
|
|
71
|
+
onChange={(e) => label === "New password" ? setPassword(e.target.value) : setConfirm(e.target.value)}
|
|
72
|
+
style={{
|
|
73
|
+
padding: "var(--space-3) var(--space-4)",
|
|
74
|
+
borderRadius: "var(--radius-md)",
|
|
75
|
+
border: "1px solid var(--color-border)",
|
|
76
|
+
background: "var(--color-bg)",
|
|
77
|
+
color: "var(--color-text)",
|
|
78
|
+
fontSize: "var(--text-base)",
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
))}
|
|
83
|
+
<button
|
|
84
|
+
type="submit"
|
|
85
|
+
disabled={loading}
|
|
86
|
+
style={{
|
|
87
|
+
padding: "var(--space-3) var(--space-4)",
|
|
88
|
+
borderRadius: "var(--radius-md)",
|
|
89
|
+
background: loading ? "var(--color-bg-muted)" : "var(--color-primary)",
|
|
90
|
+
color: "var(--color-primary-text)",
|
|
91
|
+
fontWeight: "var(--weight-semibold)",
|
|
92
|
+
border: "none",
|
|
93
|
+
cursor: loading ? "not-allowed" : "pointer",
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{loading ? "Resetting…" : "Reset password"}
|
|
97
|
+
</button>
|
|
98
|
+
</form>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default function ResetPasswordPage() {
|
|
103
|
+
return (
|
|
104
|
+
<main
|
|
105
|
+
style={{
|
|
106
|
+
minHeight: "100vh",
|
|
107
|
+
display: "flex",
|
|
108
|
+
alignItems: "center",
|
|
109
|
+
justifyContent: "center",
|
|
110
|
+
background: "var(--color-bg-subtle)",
|
|
111
|
+
padding: "var(--space-4)",
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<div
|
|
115
|
+
style={{
|
|
116
|
+
width: "100%",
|
|
117
|
+
maxWidth: "420px",
|
|
118
|
+
background: "var(--color-bg)",
|
|
119
|
+
borderRadius: "var(--radius-xl)",
|
|
120
|
+
border: "1px solid var(--color-border)",
|
|
121
|
+
padding: "var(--space-8)",
|
|
122
|
+
boxShadow: "var(--shadow-lg)",
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<div style={{ textAlign: "center", marginBottom: "var(--space-8)" }}>
|
|
126
|
+
<h1 style={{ fontSize: "var(--text-2xl)", fontWeight: "var(--weight-bold)", color: "var(--color-text)" }}>
|
|
127
|
+
Reset password
|
|
128
|
+
</h1>
|
|
129
|
+
<p style={{ marginTop: "var(--space-2)", fontSize: "var(--text-sm)", color: "var(--color-text-muted)" }}>
|
|
130
|
+
Choose a new password for your account.
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
<Suspense fallback={<p>Loading…</p>}>
|
|
134
|
+
<ResetForm />
|
|
135
|
+
</Suspense>
|
|
136
|
+
</div>
|
|
137
|
+
</main>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
interface User {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
name: string;
|
|
9
|
+
avatar?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AuthContextValue {
|
|
13
|
+
user: User | null;
|
|
14
|
+
loading: boolean;
|
|
15
|
+
login: (email: string, password: string) => Promise<void>;
|
|
16
|
+
register: (name: string, email: string, password: string) => Promise<void>;
|
|
17
|
+
logout: () => Promise<void>;
|
|
18
|
+
refresh: () => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
22
|
+
|
|
23
|
+
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
24
|
+
const [user, setUser] = useState<User | null>(null);
|
|
25
|
+
const [loading, setLoading] = useState(true);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
refresh().finally(() => setLoading(false));
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
async function refresh() {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch("/api/auth/me");
|
|
34
|
+
if (res.ok) {
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
setUser(data.user);
|
|
37
|
+
} else {
|
|
38
|
+
setUser(null);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
setUser(null);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function login(email: string, password: string) {
|
|
46
|
+
const res = await fetch("/api/auth/login", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify({ email, password }),
|
|
50
|
+
});
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const err = await res.json();
|
|
53
|
+
throw new Error(err.message || "Login failed");
|
|
54
|
+
}
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
setUser(data.user);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function register(name: string, email: string, password: string) {
|
|
60
|
+
const res = await fetch("/api/auth/register", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify({ name, email, password }),
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const err = await res.json();
|
|
67
|
+
throw new Error(err.message || "Registration failed");
|
|
68
|
+
}
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
setUser(data.user);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function logout() {
|
|
74
|
+
await fetch("/api/auth/logout", { method: "POST" });
|
|
75
|
+
setUser(null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<AuthContext.Provider value={{ user, loading, login, register, logout, refresh }}>
|
|
80
|
+
{children}
|
|
81
|
+
</AuthContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useAuth() {
|
|
86
|
+
const ctx = useContext(AuthContext);
|
|
87
|
+
if (!ctx) throw new Error("useAuth must be used inside <AuthProvider>");
|
|
88
|
+
return ctx;
|
|
89
|
+
}
|