@roboticela/devkit 4.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/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,18 @@
|
|
|
1
|
+
import type { ComponentEntry } from "./component-entry.js";
|
|
2
|
+
/** Directory containing `dist/` (the published CLI package root). */
|
|
3
|
+
export declare function getCliPackageRoot(fromImportMetaUrl?: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* On-disk component payloads: either shipped inside the npm package (`cli/registry/components`)
|
|
6
|
+
* or the monorepo sibling (`DevKit/registry/components`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function getBundledComponentsDir(fromImportMetaUrl?: string): string | null;
|
|
9
|
+
export declare function listBundledComponents(template: string | undefined, fromImportMetaUrl?: string): ComponentEntry[] | null;
|
|
10
|
+
export declare function getBundledComponentMeta(name: string, fromImportMetaUrl?: string): Record<string, unknown> | null;
|
|
11
|
+
export declare function resolveBundledVersion(name: string, requested: string, fromImportMetaUrl?: string): string | null;
|
|
12
|
+
export declare function getBundledManifest(name: string, template: string, version: string, variant: string | undefined, fromImportMetaUrl?: string): {
|
|
13
|
+
name: string;
|
|
14
|
+
template: string;
|
|
15
|
+
version: string;
|
|
16
|
+
variant: string | null;
|
|
17
|
+
manifest: Record<string, unknown>;
|
|
18
|
+
} | null;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
/** Directory containing `dist/` (the published CLI package root). */
|
|
5
|
+
export function getCliPackageRoot(fromImportMetaUrl = import.meta.url) {
|
|
6
|
+
return join(dirname(fileURLToPath(fromImportMetaUrl)), "..", "..");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* On-disk component payloads: either shipped inside the npm package (`cli/registry/components`)
|
|
10
|
+
* or the monorepo sibling (`DevKit/registry/components`).
|
|
11
|
+
*/
|
|
12
|
+
export function getBundledComponentsDir(fromImportMetaUrl = import.meta.url) {
|
|
13
|
+
const pkgRoot = getCliPackageRoot(fromImportMetaUrl);
|
|
14
|
+
const nested = join(pkgRoot, "registry", "components");
|
|
15
|
+
const sibling = join(pkgRoot, "..", "registry", "components");
|
|
16
|
+
if (existsSync(join(nested, "registry.json")))
|
|
17
|
+
return nested;
|
|
18
|
+
if (existsSync(join(sibling, "registry.json")))
|
|
19
|
+
return sibling;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
export function listBundledComponents(template, fromImportMetaUrl = import.meta.url) {
|
|
23
|
+
const root = getBundledComponentsDir(fromImportMetaUrl);
|
|
24
|
+
if (!root)
|
|
25
|
+
return null;
|
|
26
|
+
const index = JSON.parse(readFileSync(join(root, "registry.json"), "utf-8"));
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const c of index.components) {
|
|
29
|
+
if (template && !c.templates.includes(template))
|
|
30
|
+
continue;
|
|
31
|
+
const metaPath = join(root, c.name, "component.json");
|
|
32
|
+
if (!existsSync(metaPath))
|
|
33
|
+
continue;
|
|
34
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
35
|
+
out.push({
|
|
36
|
+
name: meta.name,
|
|
37
|
+
displayName: meta.displayName,
|
|
38
|
+
description: meta.description,
|
|
39
|
+
category: meta.category,
|
|
40
|
+
templates: meta.templates,
|
|
41
|
+
platforms: meta.platforms,
|
|
42
|
+
hasVariants: meta.hasVariants,
|
|
43
|
+
variants: meta.variants,
|
|
44
|
+
latestVersion: meta.versions.latest,
|
|
45
|
+
tags: meta.tags,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export function getBundledComponentMeta(name, fromImportMetaUrl = import.meta.url) {
|
|
51
|
+
const root = getBundledComponentsDir(fromImportMetaUrl);
|
|
52
|
+
if (!root)
|
|
53
|
+
return null;
|
|
54
|
+
const path = join(root, name, "component.json");
|
|
55
|
+
if (!existsSync(path))
|
|
56
|
+
return null;
|
|
57
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
58
|
+
}
|
|
59
|
+
export function resolveBundledVersion(name, requested, fromImportMetaUrl = import.meta.url) {
|
|
60
|
+
const meta = getBundledComponentMeta(name, fromImportMetaUrl);
|
|
61
|
+
if (!meta)
|
|
62
|
+
return null;
|
|
63
|
+
const versions = meta["versions"];
|
|
64
|
+
if (requested === "latest")
|
|
65
|
+
return versions.latest;
|
|
66
|
+
const found = versions.history.find((h) => h.version === requested);
|
|
67
|
+
return found ? found.version : null;
|
|
68
|
+
}
|
|
69
|
+
export function getBundledManifest(name, template, version, variant, fromImportMetaUrl = import.meta.url) {
|
|
70
|
+
const root = getBundledComponentsDir(fromImportMetaUrl);
|
|
71
|
+
if (!root)
|
|
72
|
+
return null;
|
|
73
|
+
const resolved = resolveBundledVersion(name, version, fromImportMetaUrl);
|
|
74
|
+
if (!resolved)
|
|
75
|
+
return null;
|
|
76
|
+
const base = join(root, name, template, `v${resolved.split(".")[0]}`);
|
|
77
|
+
const manifestPath = variant ? join(base, "variants", variant, "manifest.json") : join(base, "manifest.json");
|
|
78
|
+
if (!existsSync(manifestPath))
|
|
79
|
+
return null;
|
|
80
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
81
|
+
return { name, template, version: resolved, variant: variant ?? null, manifest };
|
|
82
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ComponentEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
displayName: string;
|
|
4
|
+
description: string;
|
|
5
|
+
category: string;
|
|
6
|
+
templates: string[];
|
|
7
|
+
platforms: string[];
|
|
8
|
+
hasVariants: boolean;
|
|
9
|
+
variants?: {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
description: string;
|
|
13
|
+
}[];
|
|
14
|
+
latestVersion: string;
|
|
15
|
+
tags: string[];
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface DevKitLock {
|
|
|
41
41
|
installedAt: string;
|
|
42
42
|
}>;
|
|
43
43
|
}
|
|
44
|
+
/** Stable URL for editors (works before/without the public registry host). */
|
|
45
|
+
export declare const DEVKIT_CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/Roboticela/DevKit/main/cli/schemas/devkit-config.json";
|
|
44
46
|
export declare function configExists(cwd?: string): boolean;
|
|
45
47
|
export declare function readConfig(cwd?: string): DevKitConfig;
|
|
46
48
|
export declare function writeConfig(config: DevKitConfig, cwd?: string): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -2,6 +2,8 @@ import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
const CONFIG_FILE = "devkit.config.json";
|
|
4
4
|
const LOCK_FILE = "devkit.lock.json";
|
|
5
|
+
/** Stable URL for editors (works before/without the public registry host). */
|
|
6
|
+
export const DEVKIT_CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/Roboticela/DevKit/main/cli/schemas/devkit-config.json";
|
|
5
7
|
export function configExists(cwd = process.cwd()) {
|
|
6
8
|
return existsSync(join(cwd, CONFIG_FILE));
|
|
7
9
|
}
|
|
@@ -32,7 +34,7 @@ export function isComponentInstalled(name, cwd = process.cwd()) {
|
|
|
32
34
|
}
|
|
33
35
|
export function defaultConfig(template, siteName, siteUrl) {
|
|
34
36
|
return {
|
|
35
|
-
$schema:
|
|
37
|
+
$schema: DEVKIT_CONFIG_SCHEMA_URL,
|
|
36
38
|
devkit: "1.0",
|
|
37
39
|
template,
|
|
38
40
|
site: { name: siteName, url: siteUrl },
|
package/dist/lib/installer.js
CHANGED
|
@@ -4,6 +4,7 @@ import { execSync } from "child_process";
|
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
5
|
import { x as extractTar } from "tar";
|
|
6
6
|
import { pipeline } from "stream/promises";
|
|
7
|
+
import { getBundledComponentsDir } from "./bundled-registry.js";
|
|
7
8
|
import { getManifest } from "./registry.js";
|
|
8
9
|
import { readLock, writeLock } from "./config.js";
|
|
9
10
|
import { log } from "./logger.js";
|
|
@@ -127,14 +128,15 @@ export function removeComponent(name, cwd = process.cwd()) {
|
|
|
127
128
|
}
|
|
128
129
|
async function downloadComponentFiles(name, template, version, variant, tmpDir) {
|
|
129
130
|
mkdirSync(tmpDir, { recursive: true });
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
const registryDir = join(dirname(new URL(import.meta.url).pathname), "../../../registry/components");
|
|
131
|
+
// Prefer on-disk payloads (bundled in the npm package or monorepo sibling) before HTTP tarball.
|
|
132
|
+
const registryDir = getBundledComponentsDir();
|
|
133
133
|
const major = `v${version.split(".")[0]}`;
|
|
134
|
-
const compDir =
|
|
135
|
-
?
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
const compDir = registryDir
|
|
135
|
+
? variant
|
|
136
|
+
? join(registryDir, name, template, major, "variants", variant)
|
|
137
|
+
: join(registryDir, name, template, major)
|
|
138
|
+
: "";
|
|
139
|
+
if (compDir && existsSync(compDir)) {
|
|
138
140
|
cpSync(compDir, tmpDir, { recursive: true });
|
|
139
141
|
}
|
|
140
142
|
else {
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
displayName: string;
|
|
4
|
-
description: string;
|
|
5
|
-
category: string;
|
|
6
|
-
templates: string[];
|
|
7
|
-
platforms: string[];
|
|
8
|
-
hasVariants: boolean;
|
|
9
|
-
variants?: {
|
|
10
|
-
id: string;
|
|
11
|
-
label: string;
|
|
12
|
-
description: string;
|
|
13
|
-
}[];
|
|
14
|
-
latestVersion: string;
|
|
15
|
-
tags: string[];
|
|
16
|
-
}
|
|
1
|
+
import type { ComponentEntry } from "./component-entry.js";
|
|
2
|
+
export type { ComponentEntry };
|
|
17
3
|
export declare function listComponents(template?: string): Promise<ComponentEntry[]>;
|
|
18
4
|
export declare function getComponentInfo(name: string): Promise<Record<string, unknown>>;
|
|
19
5
|
export declare function getManifest(name: string, template: string, version?: string, variant?: string): Promise<{
|
package/dist/lib/registry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getBundledComponentMeta, getBundledManifest, listBundledComponents, } from "./bundled-registry.js";
|
|
1
2
|
const REGISTRY_URL = process.env.DEVKIT_REGISTRY ?? "https://registry.devkit.roboticela.com";
|
|
2
3
|
async function get(path) {
|
|
3
4
|
const res = await fetch(`${REGISTRY_URL}${path}`);
|
|
@@ -6,14 +7,23 @@ async function get(path) {
|
|
|
6
7
|
return res.json();
|
|
7
8
|
}
|
|
8
9
|
export async function listComponents(template) {
|
|
10
|
+
const local = listBundledComponents(template);
|
|
11
|
+
if (local)
|
|
12
|
+
return local;
|
|
9
13
|
const qs = template ? `?template=${template}` : "";
|
|
10
14
|
const data = await get(`/api/v1/components${qs}`);
|
|
11
15
|
return data.components;
|
|
12
16
|
}
|
|
13
17
|
export async function getComponentInfo(name) {
|
|
18
|
+
const local = getBundledComponentMeta(name);
|
|
19
|
+
if (local)
|
|
20
|
+
return local;
|
|
14
21
|
return get(`/api/v1/components/${name}`);
|
|
15
22
|
}
|
|
16
23
|
export async function getManifest(name, template, version = "latest", variant) {
|
|
24
|
+
const local = getBundledManifest(name, template, version, variant);
|
|
25
|
+
if (local)
|
|
26
|
+
return local;
|
|
17
27
|
const qs = new URLSearchParams({ template, version });
|
|
18
28
|
if (variant)
|
|
19
29
|
qs.set("variant", variant);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roboticela/devkit",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Roboticela DevKit CLI — scaffold, extend, and theme full-stack projects with one command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,6 +53,8 @@
|
|
|
53
53
|
},
|
|
54
54
|
"files": [
|
|
55
55
|
"dist",
|
|
56
|
-
"README.md"
|
|
56
|
+
"README.md",
|
|
57
|
+
"registry",
|
|
58
|
+
"schemas"
|
|
57
59
|
]
|
|
58
60
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auth",
|
|
3
|
+
"displayName": "Authentication",
|
|
4
|
+
"description": "Complete authentication system: email/password login, registration, email verification, forgot/reset password, and Google OAuth.",
|
|
5
|
+
"category": "authentication",
|
|
6
|
+
"icon": "lock",
|
|
7
|
+
"author": "Roboticela",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"tags": ["auth", "login", "register", "oauth", "jwt", "email"],
|
|
10
|
+
"templates": ["nextjs-compact", "vite-express-tauri"],
|
|
11
|
+
"platforms": ["web", "desktop"],
|
|
12
|
+
"hasVariants": false,
|
|
13
|
+
"versions": {
|
|
14
|
+
"latest": "1.0.0",
|
|
15
|
+
"stable": "1.0.0",
|
|
16
|
+
"history": [
|
|
17
|
+
{ "version": "1.0.0", "releaseDate": "2026-04-05", "breaking": false }
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"requiredConfig": ["site.name", "site.url"],
|
|
21
|
+
"optionalConfig": [
|
|
22
|
+
"auth.google.clientId",
|
|
23
|
+
"auth.google.clientSecret",
|
|
24
|
+
"auth.smtp.host",
|
|
25
|
+
"auth.smtp.port",
|
|
26
|
+
"auth.smtp.user",
|
|
27
|
+
"auth.smtp.password"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -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
|
+
}
|