create-stackrjs 1.4.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 +154 -0
- package/dist/index.js +1135 -0
- package/package.json +62 -0
- package/template/architectures/microservices/_gitignore +26 -0
- package/template/architectures/microservices/apps/web/Dockerfile +11 -0
- package/template/architectures/microservices/apps/web/next-env.d.ts +5 -0
- package/template/architectures/microservices/apps/web/next.config.mjs +4 -0
- package/template/architectures/microservices/apps/web/package.json +25 -0
- package/template/architectures/microservices/apps/web/src/app/account/page.tsx +56 -0
- package/template/architectures/microservices/apps/web/src/app/globals.css +3 -0
- package/template/architectures/microservices/apps/web/src/app/layout.tsx +16 -0
- package/template/architectures/microservices/apps/web/src/app/login/page.tsx +70 -0
- package/template/architectures/microservices/apps/web/src/app/page.tsx +33 -0
- package/template/architectures/microservices/apps/web/src/app/register/page.tsx +71 -0
- package/template/architectures/microservices/apps/web/src/lib/api.ts +15 -0
- package/template/architectures/microservices/apps/web/tsconfig.json +23 -0
- package/template/architectures/microservices/docker-compose.yml +45 -0
- package/template/architectures/microservices/gateway/nginx.conf +20 -0
- package/template/architectures/microservices/package.json +21 -0
- package/template/architectures/microservices/services/api/Dockerfile +11 -0
- package/template/architectures/microservices/services/api/package.json +34 -0
- package/template/architectures/microservices/services/api/prisma/schema.prisma +17 -0
- package/template/architectures/microservices/services/api/prisma.config.ts +17 -0
- package/template/architectures/microservices/services/api/src/auth.ts +22 -0
- package/template/architectures/microservices/services/api/src/db.ts +12 -0
- package/template/architectures/microservices/services/api/src/index.ts +22 -0
- package/template/architectures/microservices/services/api/src/routes/auth.ts +73 -0
- package/template/architectures/microservices/services/api/tsconfig.json +16 -0
- package/template/base/_eslintrc.json +3 -0
- package/template/base/_gitignore +24 -0
- package/template/base/_prettierrc.json +6 -0
- package/template/base/next-env.d.ts +5 -0
- package/template/base/next.config.mjs +4 -0
- package/template/base/package.json +27 -0
- package/template/base/src/app/globals.css +14 -0
- package/template/base/src/app/layout.tsx +16 -0
- package/template/base/src/app/page.tsx +10 -0
- package/template/base/tsconfig.json +23 -0
- package/template/extras/account-better-auth/src/server/account.ts +52 -0
- package/template/extras/account-clerk/src/server/account.ts +64 -0
- package/template/extras/account-nextauth/src/server/account.ts +59 -0
- package/template/extras/account-supabase/src/lib/supabase/admin.ts +14 -0
- package/template/extras/account-supabase/src/server/account.ts +65 -0
- package/template/extras/admin/src/app/admin/page.tsx +55 -0
- package/template/extras/auth-pages/src/app/account/page.tsx +37 -0
- package/template/extras/auth-pages/src/app/forgot-password/page.tsx +33 -0
- package/template/extras/auth-pages/src/app/login/page.tsx +32 -0
- package/template/extras/auth-pages/src/app/page.tsx +46 -0
- package/template/extras/auth-pages/src/app/register/page.tsx +36 -0
- package/template/extras/auth-pages/src/app/reset-password/page.tsx +49 -0
- package/template/extras/auth-pages/src/components/auth/ui.tsx +57 -0
- package/template/extras/auth-pages-drizzle/src/server/actions.ts +122 -0
- package/template/extras/auth-pages-mongoose/src/server/actions.ts +121 -0
- package/template/extras/auth-pages-prisma/src/server/actions.ts +117 -0
- package/template/extras/better-auth/prisma/schema.prisma +70 -0
- package/template/extras/better-auth/src/app/account/page.tsx +42 -0
- package/template/extras/better-auth/src/app/api/auth/[...all]/route.ts +5 -0
- package/template/extras/better-auth/src/app/login/page.tsx +42 -0
- package/template/extras/better-auth/src/app/register/page.tsx +44 -0
- package/template/extras/better-auth/src/components/auth/ui.tsx +58 -0
- package/template/extras/better-auth/src/lib/auth-client.ts +7 -0
- package/template/extras/better-auth/src/server/auth.ts +13 -0
- package/template/extras/clerk/middleware.ts +14 -0
- package/template/extras/clerk/src/app/account/page.tsx +30 -0
- package/template/extras/clerk/src/app/layout.tsx +19 -0
- package/template/extras/clerk/src/app/page.tsx +40 -0
- package/template/extras/clerk/src/app/sign-in/[[...sign-in]]/page.tsx +9 -0
- package/template/extras/clerk/src/app/sign-up/[[...sign-up]]/page.tsx +9 -0
- package/template/extras/dashboard/src/app/dashboard/page.tsx +48 -0
- package/template/extras/databases/mongodb/docker-compose.yml +13 -0
- package/template/extras/databases/mysql/docker-compose.yml +14 -0
- package/template/extras/databases/postgres/docker-compose.yml +15 -0
- package/template/extras/drizzle/mysql/drizzle.config.ts +9 -0
- package/template/extras/drizzle/mysql/src/server/db.ts +11 -0
- package/template/extras/drizzle/mysql/src/server/schema.ts +17 -0
- package/template/extras/drizzle/postgres/drizzle.config.ts +9 -0
- package/template/extras/drizzle/postgres/src/server/db.ts +11 -0
- package/template/extras/drizzle/postgres/src/server/schema.ts +20 -0
- package/template/extras/landing/src/app/page.tsx +53 -0
- package/template/extras/mongoose/src/server/db.ts +12 -0
- package/template/extras/mongoose/src/server/models.ts +25 -0
- package/template/extras/nextauth/middleware.ts +11 -0
- package/template/extras/nextauth/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/template/extras/nextauth/src/server/auth.config.ts +39 -0
- package/template/extras/nextauth/src/server/roles.ts +5 -0
- package/template/extras/nextauth/src/types/next-auth.d.ts +22 -0
- package/template/extras/nextauth-drizzle/src/server/auth.ts +44 -0
- package/template/extras/nextauth-mongoose/src/server/auth.ts +42 -0
- package/template/extras/nextauth-prisma/src/server/auth.ts +43 -0
- package/template/extras/prisma/base/prisma/schema.prisma +67 -0
- package/template/extras/prisma/base/prisma.config.ts +10 -0
- package/template/extras/prisma/mysql/src/server/db.ts +25 -0
- package/template/extras/prisma/postgres/src/server/db.ts +18 -0
- package/template/extras/shadcn/components.json +20 -0
- package/template/extras/shadcn/postcss.config.mjs +7 -0
- package/template/extras/shadcn/src/app/globals.css +59 -0
- package/template/extras/shadcn/src/components/ui/button.tsx +42 -0
- package/template/extras/shadcn/src/components/ui/card.tsx +54 -0
- package/template/extras/shadcn/src/components/ui/input.tsx +19 -0
- package/template/extras/shadcn/src/lib/utils.ts +7 -0
- package/template/extras/shadcn/tailwind.config.ts +71 -0
- package/template/extras/supabase/middleware.ts +13 -0
- package/template/extras/supabase/src/app/account/page.tsx +32 -0
- package/template/extras/supabase/src/app/login/page.tsx +47 -0
- package/template/extras/supabase/src/app/register/page.tsx +52 -0
- package/template/extras/supabase/src/components/auth/sign-out-button.tsx +25 -0
- package/template/extras/supabase/src/components/auth/ui.tsx +58 -0
- package/template/extras/supabase/src/lib/supabase/client.ts +9 -0
- package/template/extras/supabase/src/lib/supabase/middleware.ts +34 -0
- package/template/extras/supabase/src/lib/supabase/server.ts +29 -0
- package/template/extras/tailwind/postcss.config.mjs +7 -0
- package/template/extras/tailwind/src/app/globals.css +3 -0
- package/template/extras/tailwind/tailwind.config.ts +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-stackrjs",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Scaffold a runnable full-stack Next.js project in one command",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-stackrjs": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.18.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"smoke": "tsx scripts/smoke.ts",
|
|
24
|
+
"release": "semantic-release",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"cli",
|
|
29
|
+
"scaffold",
|
|
30
|
+
"nextjs",
|
|
31
|
+
"starter",
|
|
32
|
+
"fullstack",
|
|
33
|
+
"create",
|
|
34
|
+
"generator"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/Tristan-stack/stackr.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/Tristan-stack/stackr/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/Tristan-stack/stackr#readme",
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@clack/prompts": "^0.7.0",
|
|
50
|
+
"picocolors": "^1.0.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
54
|
+
"@semantic-release/git": "^10.0.1",
|
|
55
|
+
"@types/node": "^20.14.0",
|
|
56
|
+
"semantic-release": "^24.2.0",
|
|
57
|
+
"tsup": "^8.3.5",
|
|
58
|
+
"tsx": "^4.19.2",
|
|
59
|
+
"typescript": "^5.7.2",
|
|
60
|
+
"vitest": "^2.1.8"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
.pnp
|
|
3
|
+
.pnp.js
|
|
4
|
+
|
|
5
|
+
# next.js
|
|
6
|
+
.next/
|
|
7
|
+
out/
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
|
|
11
|
+
# misc
|
|
12
|
+
.DS_Store
|
|
13
|
+
*.pem
|
|
14
|
+
*.log
|
|
15
|
+
|
|
16
|
+
# env files
|
|
17
|
+
.env
|
|
18
|
+
.env*.local
|
|
19
|
+
|
|
20
|
+
# typescript
|
|
21
|
+
next-env.d.ts
|
|
22
|
+
*.tsbuildinfo
|
|
23
|
+
|
|
24
|
+
# prisma
|
|
25
|
+
services/api/prisma/migrations/dev.db*
|
|
26
|
+
services/api/src/generated
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/web",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "next lint",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"next": "^15.1.3",
|
|
14
|
+
"react": "^18.3.1",
|
|
15
|
+
"react-dom": "^18.3.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^20.14.0",
|
|
19
|
+
"@types/react": "^18.3.12",
|
|
20
|
+
"@types/react-dom": "^18.3.1",
|
|
21
|
+
"eslint": "^8.57.1",
|
|
22
|
+
"eslint-config-next": "^15.1.3",
|
|
23
|
+
"typescript": "^5.7.2"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { api } from "@/lib/api";
|
|
6
|
+
|
|
7
|
+
interface Me {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function AccountPage() {
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const [me, setMe] = useState<Me | null>(null);
|
|
15
|
+
const [loading, setLoading] = useState(true);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let active = true;
|
|
19
|
+
api("/auth/me").then(async (res) => {
|
|
20
|
+
if (!active) return;
|
|
21
|
+
if (res.ok) setMe(await res.json());
|
|
22
|
+
else router.replace("/login");
|
|
23
|
+
setLoading(false);
|
|
24
|
+
});
|
|
25
|
+
return () => {
|
|
26
|
+
active = false;
|
|
27
|
+
};
|
|
28
|
+
}, [router]);
|
|
29
|
+
|
|
30
|
+
async function logout() {
|
|
31
|
+
await api("/auth/logout", { method: "POST" });
|
|
32
|
+
router.replace("/login");
|
|
33
|
+
router.refresh();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (loading) {
|
|
37
|
+
return <main className="flex min-h-screen items-center justify-center p-8">Loading…</main>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<main className="mx-auto flex min-h-screen max-w-sm flex-col justify-center gap-6 p-8">
|
|
42
|
+
<h1 className="text-2xl font-bold">Your account</h1>
|
|
43
|
+
<p className="text-gray-600">
|
|
44
|
+
Signed in as <strong>{me?.email}</strong>. This page is protected — it reads your session
|
|
45
|
+
from the API service.
|
|
46
|
+
</p>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={logout}
|
|
50
|
+
className="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-100"
|
|
51
|
+
>
|
|
52
|
+
Sign out
|
|
53
|
+
</button>
|
|
54
|
+
</main>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import "./globals.css";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Stackr app",
|
|
7
|
+
description: "Generated by Stackr (microservices)",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
11
|
+
return (
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<body className="min-h-screen bg-gray-50 text-gray-900 antialiased">{children}</body>
|
|
14
|
+
</html>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { api } from "@/lib/api";
|
|
7
|
+
|
|
8
|
+
export default function LoginPage() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const [email, setEmail] = useState("");
|
|
11
|
+
const [password, setPassword] = useState("");
|
|
12
|
+
const [error, setError] = useState<string | null>(null);
|
|
13
|
+
const [pending, setPending] = useState(false);
|
|
14
|
+
|
|
15
|
+
async function onSubmit(e: React.FormEvent) {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
setPending(true);
|
|
18
|
+
setError(null);
|
|
19
|
+
const res = await api("/auth/login", {
|
|
20
|
+
method: "POST",
|
|
21
|
+
body: JSON.stringify({ email, password }),
|
|
22
|
+
});
|
|
23
|
+
setPending(false);
|
|
24
|
+
if (res.ok) {
|
|
25
|
+
router.push("/account");
|
|
26
|
+
router.refresh();
|
|
27
|
+
} else {
|
|
28
|
+
const data = await res.json().catch(() => ({}));
|
|
29
|
+
setError(data.error ?? "Login failed.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<main className="mx-auto flex min-h-screen max-w-sm flex-col justify-center gap-6 p-8">
|
|
35
|
+
<h1 className="text-2xl font-bold">Sign in</h1>
|
|
36
|
+
<form onSubmit={onSubmit} className="flex flex-col gap-3">
|
|
37
|
+
<input
|
|
38
|
+
type="email"
|
|
39
|
+
required
|
|
40
|
+
placeholder="you@example.com"
|
|
41
|
+
value={email}
|
|
42
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
43
|
+
className="rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
44
|
+
/>
|
|
45
|
+
<input
|
|
46
|
+
type="password"
|
|
47
|
+
required
|
|
48
|
+
placeholder="Password"
|
|
49
|
+
value={password}
|
|
50
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
51
|
+
className="rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
52
|
+
/>
|
|
53
|
+
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
54
|
+
<button
|
|
55
|
+
type="submit"
|
|
56
|
+
disabled={pending}
|
|
57
|
+
className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700 disabled:opacity-50"
|
|
58
|
+
>
|
|
59
|
+
{pending ? "Signing in…" : "Sign in"}
|
|
60
|
+
</button>
|
|
61
|
+
</form>
|
|
62
|
+
<p className="text-sm text-gray-600">
|
|
63
|
+
No account?{" "}
|
|
64
|
+
<Link href="/register" className="underline">
|
|
65
|
+
Register
|
|
66
|
+
</Link>
|
|
67
|
+
</p>
|
|
68
|
+
</main>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function Home() {
|
|
4
|
+
return (
|
|
5
|
+
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-6 p-8 text-center">
|
|
6
|
+
<h1 className="text-4xl font-bold tracking-tight">It works 🎉</h1>
|
|
7
|
+
<p className="text-gray-600">
|
|
8
|
+
This is the <strong>web</strong> service. It talks to the <strong>api</strong> service
|
|
9
|
+
through the gateway. Register an account, then sign in.
|
|
10
|
+
</p>
|
|
11
|
+
<div className="flex gap-3">
|
|
12
|
+
<Link
|
|
13
|
+
href="/register"
|
|
14
|
+
className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700"
|
|
15
|
+
>
|
|
16
|
+
Register
|
|
17
|
+
</Link>
|
|
18
|
+
<Link
|
|
19
|
+
href="/login"
|
|
20
|
+
className="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-100"
|
|
21
|
+
>
|
|
22
|
+
Sign in
|
|
23
|
+
</Link>
|
|
24
|
+
<Link
|
|
25
|
+
href="/account"
|
|
26
|
+
className="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-100"
|
|
27
|
+
>
|
|
28
|
+
Account
|
|
29
|
+
</Link>
|
|
30
|
+
</div>
|
|
31
|
+
</main>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { api } from "@/lib/api";
|
|
7
|
+
|
|
8
|
+
export default function RegisterPage() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const [email, setEmail] = useState("");
|
|
11
|
+
const [password, setPassword] = useState("");
|
|
12
|
+
const [error, setError] = useState<string | null>(null);
|
|
13
|
+
const [pending, setPending] = useState(false);
|
|
14
|
+
|
|
15
|
+
async function onSubmit(e: React.FormEvent) {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
setPending(true);
|
|
18
|
+
setError(null);
|
|
19
|
+
const res = await api("/auth/register", {
|
|
20
|
+
method: "POST",
|
|
21
|
+
body: JSON.stringify({ email, password }),
|
|
22
|
+
});
|
|
23
|
+
setPending(false);
|
|
24
|
+
if (res.ok) {
|
|
25
|
+
router.push("/account");
|
|
26
|
+
router.refresh();
|
|
27
|
+
} else {
|
|
28
|
+
const data = await res.json().catch(() => ({}));
|
|
29
|
+
setError(data.error ?? "Registration failed.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<main className="mx-auto flex min-h-screen max-w-sm flex-col justify-center gap-6 p-8">
|
|
35
|
+
<h1 className="text-2xl font-bold">Create your account</h1>
|
|
36
|
+
<form onSubmit={onSubmit} className="flex flex-col gap-3">
|
|
37
|
+
<input
|
|
38
|
+
type="email"
|
|
39
|
+
required
|
|
40
|
+
placeholder="you@example.com"
|
|
41
|
+
value={email}
|
|
42
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
43
|
+
className="rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
44
|
+
/>
|
|
45
|
+
<input
|
|
46
|
+
type="password"
|
|
47
|
+
required
|
|
48
|
+
minLength={8}
|
|
49
|
+
placeholder="Password (min 8 characters)"
|
|
50
|
+
value={password}
|
|
51
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
52
|
+
className="rounded-md border border-gray-300 px-3 py-2 text-sm"
|
|
53
|
+
/>
|
|
54
|
+
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
55
|
+
<button
|
|
56
|
+
type="submit"
|
|
57
|
+
disabled={pending}
|
|
58
|
+
className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700 disabled:opacity-50"
|
|
59
|
+
>
|
|
60
|
+
{pending ? "Creating…" : "Create account"}
|
|
61
|
+
</button>
|
|
62
|
+
</form>
|
|
63
|
+
<p className="text-sm text-gray-600">
|
|
64
|
+
Already registered?{" "}
|
|
65
|
+
<Link href="/login" className="underline">
|
|
66
|
+
Sign in
|
|
67
|
+
</Link>
|
|
68
|
+
</p>
|
|
69
|
+
</main>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny fetch wrapper for talking to the API service. In the browser, requests go
|
|
3
|
+
* through the gateway (NEXT_PUBLIC_API_URL, e.g. http://localhost:8080/api),
|
|
4
|
+
* which proxies /api/* to the API service. `credentials: "include"` carries the
|
|
5
|
+
* httpOnly session cookie.
|
|
6
|
+
*/
|
|
7
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
|
|
8
|
+
|
|
9
|
+
export async function api(path: string, init?: RequestInit): Promise<Response> {
|
|
10
|
+
return fetch(`${API_URL}${path}`, {
|
|
11
|
+
...init,
|
|
12
|
+
credentials: "include",
|
|
13
|
+
headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./src/*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
services:
|
|
2
|
+
db:
|
|
3
|
+
image: postgres:16
|
|
4
|
+
restart: unless-stopped
|
|
5
|
+
environment:
|
|
6
|
+
POSTGRES_USER: postgres
|
|
7
|
+
POSTGRES_PASSWORD: postgres
|
|
8
|
+
POSTGRES_DB: app
|
|
9
|
+
ports:
|
|
10
|
+
- "5432:5432"
|
|
11
|
+
volumes:
|
|
12
|
+
- db_data:/var/lib/postgresql/data
|
|
13
|
+
|
|
14
|
+
api:
|
|
15
|
+
build: ./services/api
|
|
16
|
+
restart: unless-stopped
|
|
17
|
+
environment:
|
|
18
|
+
DATABASE_URL: postgresql://postgres:postgres@db:5432/app?schema=public
|
|
19
|
+
AUTH_SECRET: ${AUTH_SECRET:-dev-insecure-change-me}
|
|
20
|
+
PORT: "3001"
|
|
21
|
+
depends_on:
|
|
22
|
+
- db
|
|
23
|
+
|
|
24
|
+
web:
|
|
25
|
+
build: ./apps/web
|
|
26
|
+
restart: unless-stopped
|
|
27
|
+
environment:
|
|
28
|
+
# The browser reaches the API through the gateway (see gateway/nginx.conf).
|
|
29
|
+
NEXT_PUBLIC_API_URL: http://localhost:8080/api
|
|
30
|
+
depends_on:
|
|
31
|
+
- api
|
|
32
|
+
|
|
33
|
+
gateway:
|
|
34
|
+
image: nginx:1.27-alpine
|
|
35
|
+
restart: unless-stopped
|
|
36
|
+
ports:
|
|
37
|
+
- "8080:80"
|
|
38
|
+
volumes:
|
|
39
|
+
- ./gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
|
40
|
+
depends_on:
|
|
41
|
+
- web
|
|
42
|
+
- api
|
|
43
|
+
|
|
44
|
+
volumes:
|
|
45
|
+
db_data:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Reverse proxy: a single public origin in front of the web app and the API.
|
|
2
|
+
# Requests to /api/* are forwarded to the API service (prefix stripped); every
|
|
3
|
+
# other path goes to the Next.js web app.
|
|
4
|
+
server {
|
|
5
|
+
listen 80;
|
|
6
|
+
|
|
7
|
+
location /api/ {
|
|
8
|
+
proxy_pass http://api:3001/;
|
|
9
|
+
proxy_set_header Host $host;
|
|
10
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
11
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
location / {
|
|
15
|
+
proxy_pass http://web:3000;
|
|
16
|
+
proxy_set_header Host $host;
|
|
17
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
18
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"workspaces": [
|
|
6
|
+
"apps/*",
|
|
7
|
+
"services/*"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "npm run dev --workspaces --if-present",
|
|
11
|
+
"build": "npm run build --workspaces --if-present",
|
|
12
|
+
"dev:web": "npm run dev --workspace apps/web",
|
|
13
|
+
"dev:api": "npm run dev --workspace services/api",
|
|
14
|
+
"db:migrate": "npm run db:migrate --workspace services/api",
|
|
15
|
+
"db:studio": "npm run db:studio --workspace services/api",
|
|
16
|
+
"compose:up": "docker compose up -d",
|
|
17
|
+
"compose:down": "docker compose down"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {},
|
|
20
|
+
"devDependencies": {}
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app/api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/index.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"db:migrate": "prisma migrate dev",
|
|
12
|
+
"db:studio": "prisma studio",
|
|
13
|
+
"postinstall": "prisma generate"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@hono/node-server": "^1.13.7",
|
|
17
|
+
"@prisma/adapter-pg": "^7.8.0",
|
|
18
|
+
"@prisma/client": "^7.8.0",
|
|
19
|
+
"bcryptjs": "^2.4.3",
|
|
20
|
+
"hono": "^4.6.14",
|
|
21
|
+
"jose": "^5.9.6",
|
|
22
|
+
"pg": "^8.13.1",
|
|
23
|
+
"zod": "^3.24.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/bcryptjs": "^2.4.6",
|
|
27
|
+
"@types/node": "^20.14.0",
|
|
28
|
+
"@types/pg": "^8.11.10",
|
|
29
|
+
"dotenv": "^16.4.7",
|
|
30
|
+
"prisma": "^7.8.0",
|
|
31
|
+
"tsx": "^4.19.2",
|
|
32
|
+
"typescript": "^5.7.2"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client"
|
|
3
|
+
output = "../src/generated/prisma"
|
|
4
|
+
moduleFormat = "esm"
|
|
5
|
+
importFileExtension = "js"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
datasource db {
|
|
9
|
+
provider = "postgresql"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
model User {
|
|
13
|
+
id String @id @default(cuid())
|
|
14
|
+
email String @unique
|
|
15
|
+
passwordHash String
|
|
16
|
+
createdAt DateTime @default(now())
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { config } from "dotenv";
|
|
4
|
+
import { defineConfig, env } from "prisma/config";
|
|
5
|
+
|
|
6
|
+
// The workspace `.env` lives at the monorepo root (two levels up), so load it
|
|
7
|
+
// explicitly rather than relying on the current working directory.
|
|
8
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
9
|
+
config({ path: path.join(root, ".env") });
|
|
10
|
+
|
|
11
|
+
// Prisma 7 reads connection config from here (not the schema). The API service
|
|
12
|
+
// owns its database; the URL comes from the workspace `.env`.
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
schema: "prisma/schema.prisma",
|
|
15
|
+
migrations: { path: "prisma/migrations" },
|
|
16
|
+
datasource: { url: env("DATABASE_URL") },
|
|
17
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
2
|
+
|
|
3
|
+
const secret = new TextEncoder().encode(process.env.AUTH_SECRET ?? "dev-insecure-change-me");
|
|
4
|
+
|
|
5
|
+
/** Sign a 7-day session token for a user id. */
|
|
6
|
+
export async function signSession(userId: string): Promise<string> {
|
|
7
|
+
return new SignJWT({ sub: userId })
|
|
8
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
9
|
+
.setIssuedAt()
|
|
10
|
+
.setExpirationTime("7d")
|
|
11
|
+
.sign(secret);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Verify a session token, returning the user id or null when invalid/expired. */
|
|
15
|
+
export async function verifySession(token: string): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
const { payload } = await jwtVerify(token, secret);
|
|
18
|
+
return typeof payload.sub === "string" ? payload.sub : null;
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PrismaClient } from "./generated/prisma/client.js";
|
|
2
|
+
import { PrismaPg } from "@prisma/adapter-pg";
|
|
3
|
+
|
|
4
|
+
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
|
|
5
|
+
|
|
6
|
+
// Reuse a single PrismaClient across hot reloads in development. Prisma 7 connects
|
|
7
|
+
// through a driver adapter (connection string from `.env`).
|
|
8
|
+
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
|
9
|
+
|
|
10
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });
|
|
11
|
+
|
|
12
|
+
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { cors } from "hono/cors";
|
|
4
|
+
import { authRoutes } from "./routes/auth.js";
|
|
5
|
+
|
|
6
|
+
const app = new Hono();
|
|
7
|
+
|
|
8
|
+
// Allow the web app's origin to send/receive the session cookie.
|
|
9
|
+
app.use(
|
|
10
|
+
"*",
|
|
11
|
+
cors({
|
|
12
|
+
origin: (origin) => origin ?? "*",
|
|
13
|
+
credentials: true,
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
18
|
+
app.route("/auth", authRoutes);
|
|
19
|
+
|
|
20
|
+
const port = Number(process.env.PORT ?? 3001);
|
|
21
|
+
serve({ fetch: app.fetch, port });
|
|
22
|
+
console.log(`API service listening on :${port}`);
|