nextworks 0.1.0-alpha.0 → 0.1.0-alpha.10
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 +61 -2
- package/dist/cli_manifests/auth_manifest.json +7 -1
- package/dist/cli_manifests/blocks_manifest.json +7 -6
- package/dist/cli_manifests/data_manifest.json +4 -2
- package/dist/cli_manifests/forms_manifest.json +6 -3
- package/dist/commands/blocks.d.ts +1 -0
- package/dist/commands/blocks.d.ts.map +1 -1
- package/dist/commands/blocks.js +82 -10
- package/dist/commands/blocks.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/kits/auth-core/{README.md → .nextworks/docs/AUTH_CORE_README.md} +35 -0
- package/dist/kits/auth-core/.nextworks/docs/AUTH_QUICKSTART.md +244 -0
- package/dist/kits/auth-core/LICENSE +21 -0
- package/dist/kits/auth-core/app/(protected)/admin/posts/page.tsx +29 -0
- package/dist/kits/auth-core/app/(protected)/admin/users/page.tsx +29 -0
- package/dist/kits/auth-core/app/api/users/[id]/route.ts +127 -0
- package/dist/kits/auth-core/components/ui/button.tsx +84 -17
- package/dist/kits/auth-core/components/ui/input.tsx +5 -3
- package/dist/kits/auth-core/components/ui/label.tsx +10 -4
- package/dist/kits/auth-core/lib/prisma.ts +10 -0
- package/dist/kits/blocks/.nextworks/docs/BLOCKS_QUICKSTART.md +193 -0
- package/dist/kits/blocks/{README.md → .nextworks/docs/BLOCKS_README.md} +12 -0
- package/dist/kits/blocks/.nextworks/docs/THEME_GUIDE.md +223 -0
- package/dist/kits/blocks/LICENSE +21 -0
- package/dist/kits/data/.nextworks/docs/DATA_QUICKSTART.md +112 -0
- package/dist/kits/data/{README.md → .nextworks/docs/DATA_README.md} +37 -0
- package/dist/kits/data/LICENSE +21 -0
- package/dist/kits/data/lib/prisma.ts +10 -0
- package/dist/kits/forms/.nextworks/docs/FORMS_QUICKSTART.md +85 -0
- package/dist/kits/forms/{README.md → .nextworks/docs/FORMS_README.md} +12 -0
- package/dist/kits/forms/LICENSE +21 -0
- package/package.json +4 -1
- package/dist/.gitkeep +0 -0
- package/dist/kits/blocks/components/ui/button_bck.tsx +0 -93
- package/dist/kits/blocks/lib/themes_old.ts +0 -37
- package/dist/kits/blocks/notes/THEMING_CONVERSION_SUMMARY.md +0 -14
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Auth Quickstart
|
|
2
|
+
|
|
3
|
+
Follow these steps to get email/password auth (and optional GitHub OAuth) running in under 5 minutes.
|
|
4
|
+
|
|
5
|
+
If you are using the `nextworks` CLI in your own app, the recommended alpha setup is to install Blocks with sections and templates first, then Auth Core:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx nextworks add blocks --sections --templates
|
|
9
|
+
npx nextworks add auth-core
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Then follow the steps below inside your app.
|
|
13
|
+
|
|
14
|
+
## 1) Copy environment variables
|
|
15
|
+
|
|
16
|
+
Create a `.env` file based on `.env.example`:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
cp .env.example .env
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Fill in values for the following environment variables used by the Auth kit:
|
|
23
|
+
|
|
24
|
+
- DATABASE_URL — PostgreSQL connection string
|
|
25
|
+
- NEXTAUTH_URL — URL of your app (e.g. http://localhost:3000 in dev)
|
|
26
|
+
- NEXTAUTH_SECRET — a strong random string (used to sign NextAuth tokens)
|
|
27
|
+
|
|
28
|
+
Optional / password reset / email provider vars:
|
|
29
|
+
|
|
30
|
+
- GITHUB_ID — (optional) GitHub OAuth client id
|
|
31
|
+
- GITHUB_SECRET — (optional) GitHub OAuth client secret
|
|
32
|
+
- NEXTWORKS_ENABLE_PASSWORD_RESET — set to `1` to enable the dev password reset scaffold (default: disabled)
|
|
33
|
+
- NEXTWORKS_USE_DEV_EMAIL — set to `1` to enable Ethereal dev email transport (for local testing only)
|
|
34
|
+
- SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, NOREPLY_EMAIL — when using a real SMTP provider (required in production if enable password reset)
|
|
35
|
+
- NODE_ENV — (production/dev) used to guard password-reset behavior
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
|
|
39
|
+
- The Auth kit will only enable the GitHub provider when both GITHUB_ID and GITHUB_SECRET are present.
|
|
40
|
+
- Password reset remains disabled by default; enable cautiously and only after configuring a real mail provider in production.
|
|
41
|
+
|
|
42
|
+
## 2) Install and generate Prisma
|
|
43
|
+
|
|
44
|
+
If you are using the monorepo directly:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install
|
|
48
|
+
npx prisma generate
|
|
49
|
+
npx prisma migrate dev -n init
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If you installed Auth via the CLI (`npx nextworks add auth-core`), the schema and scripts are already in place in your app — you still need to run:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @prisma/client prisma
|
|
56
|
+
npx prisma generate
|
|
57
|
+
npx prisma migrate dev -n init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This applies the Prisma schema and generates the Prisma client.
|
|
61
|
+
|
|
62
|
+
## 3) Start the dev server
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
npm run dev
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Visit:
|
|
69
|
+
|
|
70
|
+
- http://localhost:3000/auth/signup to create a user
|
|
71
|
+
- http://localhost:3000/auth/login to sign in
|
|
72
|
+
- http://localhost:3000/dashboard after login
|
|
73
|
+
|
|
74
|
+
## 4) Optional: GitHub OAuth
|
|
75
|
+
|
|
76
|
+
If you provided GITHUB_ID and GITHUB_SECRET in `.env`, the GitHub provider will be enabled and the GitHub button will appear on the forms. If not provided, the button will be hidden and only email/password is available.
|
|
77
|
+
|
|
78
|
+
## 5) Roles (optional)
|
|
79
|
+
|
|
80
|
+
The `User` model has a `role` field (default: `user`). Role is propagated to the JWT and session so you can gate pages/components. You can later add admin tooling or seed scripts as needed.
|
|
81
|
+
|
|
82
|
+
## 6) Seed and promote admin scripts
|
|
83
|
+
|
|
84
|
+
- `scripts/seed-demo.mjs` (already in the repo) creates a demo admin user and sample posts for quick demos.
|
|
85
|
+
- `scripts/promote-admin.mjs` (new) is idempotent and promotes an existing user to `admin`:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
node ./scripts/promote-admin.mjs admin@example.com
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 7) Forgot password (development scaffold)
|
|
92
|
+
|
|
93
|
+
This is a development scaffold that is disabled by default. To enable the password reset feature, set:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
NEXTWORKS_ENABLE_PASSWORD_RESET=1
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- POST `/api/auth/forgot-password` accepts `{ email }`. When enabled the endpoint generates a one-time token stored in the database and (in development) prints a reset link to the server console. The endpoint always returns a generic success message to avoid user enumeration.
|
|
100
|
+
- GET `/api/auth/reset-password?token=...` validates a token (only when enabled).
|
|
101
|
+
- POST `/api/auth/reset-password` accepts `{ token, password, confirmPassword }` and updates the user password while invalidating the token (only when enabled).
|
|
102
|
+
|
|
103
|
+
Security notes:
|
|
104
|
+
|
|
105
|
+
- The scaffold is intentionally disabled by default; enable it only for local testing or after hardening.
|
|
106
|
+
- The forgot-password scaffold uses an in-memory rate limiter for demo purposes — replace it with a centralized rate limiter (Redis, API gateway) in production.
|
|
107
|
+
- Always use a real email provider (SMTP, SendGrid, etc.) in production and never log reset tokens in server logs.
|
|
108
|
+
|
|
109
|
+
Migration & upgrade notes
|
|
110
|
+
|
|
111
|
+
- A recent schema migration changed PasswordReset to store tokenHash (SHA-256) instead of the raw token. Steps we executed:
|
|
112
|
+
1. Added `tokenHash` (nullable) to the PasswordReset model and made `token` nullable in Prisma.
|
|
113
|
+
2. Generated a migration that adds the tokenHash column and a unique index on it.
|
|
114
|
+
3. Ran a one-off script `scripts/populate-tokenhash.mjs` to compute SHA-256 hashes for previous tokens and fill `tokenHash`.
|
|
115
|
+
4. Applied the migration and regenerated the Prisma client (`npx prisma generate`).
|
|
116
|
+
|
|
117
|
+
- To enable password reset behavior in your environment, set:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
NEXTWORKS_ENABLE_PASSWORD_RESET=1
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- Dev email helper (optional): there is an optional dev email helper (nodemailer + Ethereal) that can be enabled with `NEXTWORKS_USE_DEV_EMAIL=1`. When enabled (and NEXTWORKS_ENABLE_PASSWORD_RESET=1), forgot-password will send an Ethereal email and the server will log only the Ethereal preview URL (no plaintext token in logs). This helper is strictly opt-in for development only. Use a real email provider in production.
|
|
124
|
+
|
|
125
|
+
- Production hardening: Password reset should only be enabled in production when a mail provider is configured. To harden the scaffold in production, ensure the following environment variables are set for SMTP (or equivalent for other providers):
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
SMTP_HOST=smtp.example.com
|
|
129
|
+
SMTP_PORT=587
|
|
130
|
+
SMTP_USER=user@example.com
|
|
131
|
+
SMTP_PASS=supersecret
|
|
132
|
+
NOREPLY_EMAIL=no-reply@example.com
|
|
133
|
+
NEXTWORKS_ENABLE_PASSWORD_RESET=1
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
When NEXTWORKS_ENABLE_PASSWORD_RESET=1 and NODE_ENV=production the server will refuse to enable password reset unless a mail provider is configured. The server will not log reset tokens or URLs in production logs.
|
|
137
|
+
|
|
138
|
+
Security checklist before enabling reset in production:
|
|
139
|
+
|
|
140
|
+
- Set a strong NEXTAUTH_SECRET
|
|
141
|
+
- Use HTTPS/TLS
|
|
142
|
+
- Replace in-memory rate limiting with a centralized store (Redis/API Gateway)
|
|
143
|
+
- Configure and test a real mail provider (SMTP, SendGrid, etc.)
|
|
144
|
+
- Ensure password reset tokens are expired/cleaned up periodically
|
|
145
|
+
- Review logging to avoid token leakage
|
|
146
|
+
|
|
147
|
+
- Promotion script: to promote an existing user to admin, run:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
node ./scripts/promote-admin.mjs user@example.com
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- Important: Before turning password reset on in production, ensure you:
|
|
154
|
+
- Replace the in-memory rate limiter with a centralized solution (Redis or API Gateway).
|
|
155
|
+
- Configure a real email provider and remove console logging of tokens.
|
|
156
|
+
- Ensure tokens are cleaned up periodically (see scripts/prune-password-resets.mjs stub).
|
|
157
|
+
- Set a secure NEXTAUTH_SECRET and use HTTPS in production.
|
|
158
|
+
|
|
159
|
+
## Files & kit manifest (what to look at)
|
|
160
|
+
|
|
161
|
+
If you want to inspect or package the Auth kit, the primary files and routes are listed below. This is a minimal manifest intended for documentation and CLI packaging reference — adjust paths if you extract the kit into another project.
|
|
162
|
+
|
|
163
|
+
Key files
|
|
164
|
+
|
|
165
|
+
- NextAuth handler & callbacks: lib/auth.ts
|
|
166
|
+
- Prisma client: lib/prisma.ts
|
|
167
|
+
- Auth helper utilities: lib/auth-helpers.ts, lib/hash.ts
|
|
168
|
+
- Email providers: lib/email/index.ts, lib/email/dev-transport.ts, lib/email/provider-smtp.ts
|
|
169
|
+
- Validation helpers: lib/validation/forms.ts
|
|
170
|
+
- Form error mapping: lib/forms/map-errors.ts
|
|
171
|
+
- UI components used by Auth: components/auth/_ and components/ui/_ (login-form.tsx, signup-form.tsx, button/input/label etc.)
|
|
172
|
+
- Pages & API routes:
|
|
173
|
+
- app/auth/login/page.tsx
|
|
174
|
+
- app/auth/signup/page.tsx
|
|
175
|
+
- app/auth/forgot-password/page.tsx
|
|
176
|
+
- app/auth/reset-password/page.tsx
|
|
177
|
+
- app/auth/verify-email/page.tsx
|
|
178
|
+
- app/api/auth/[...nextauth]/route.ts
|
|
179
|
+
- app/api/auth/forgot-password/route.ts
|
|
180
|
+
- app/api/auth/reset-password/route.ts
|
|
181
|
+
- app/api/auth/send-verify-email/route.ts
|
|
182
|
+
- app/api/auth/providers/route.ts
|
|
183
|
+
- app/api/signup/route.ts
|
|
184
|
+
- Prisma schema & auth models: prisma/schema.prisma and prisma/auth-models.prisma
|
|
185
|
+
- Seed & maintenance scripts: scripts/seed-demo.mjs, scripts/promote-admin.mjs, scripts/populate-tokenhash.mjs
|
|
186
|
+
|
|
187
|
+
Minimal manifest (JSON)
|
|
188
|
+
|
|
189
|
+
{
|
|
190
|
+
"name": "auth-core",
|
|
191
|
+
"files": [
|
|
192
|
+
"lib/auth.ts",
|
|
193
|
+
"lib/prisma.ts",
|
|
194
|
+
"lib/auth-helpers.ts",
|
|
195
|
+
"lib/hash.ts",
|
|
196
|
+
"lib/forms/map-errors.ts",
|
|
197
|
+
"lib/validation/forms.ts",
|
|
198
|
+
"lib/email/index.ts",
|
|
199
|
+
"components/auth/login-form.tsx",
|
|
200
|
+
"components/auth/signup-form.tsx",
|
|
201
|
+
"components/auth/logout-button.tsx",
|
|
202
|
+
"components/session-provider.tsx",
|
|
203
|
+
"app/auth/login/page.tsx",
|
|
204
|
+
"app/auth/signup/page.tsx",
|
|
205
|
+
"app/api/auth/[...nextauth]/route.ts",
|
|
206
|
+
"app/api/signup/route.ts",
|
|
207
|
+
"prisma/schema.prisma",
|
|
208
|
+
"scripts/seed-demo.mjs",
|
|
209
|
+
"scripts/promote-admin.mjs"
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
## Where to customize NextAuth behavior
|
|
214
|
+
|
|
215
|
+
- The NextAuth configuration and handlers live in lib/auth.ts. Common customizations include:
|
|
216
|
+
- Adjusting providers (add/remove OAuth providers).
|
|
217
|
+
- Modifying session and jwt callbacks to include or remove fields on the token/session.
|
|
218
|
+
- Customizing pages (e.g., pages.signIn) or redirect logic.
|
|
219
|
+
|
|
220
|
+
- Notes: lib/auth.ts already persists user.id and role into the JWT and exposes them on session.user for server components. If you change what is exposed on the session, ensure server-side checks (RBAC) are updated accordingly.
|
|
221
|
+
|
|
222
|
+
## CLI kit source (if packaging later)
|
|
223
|
+
|
|
224
|
+
- A kit skeleton for packaging already exists in the repo at: `cli/kits/auth-core/` — it contains a compact copy of the core auth files (components, lib, prisma snippets) used by the CLI during development. Use that folder as a starting point when you implement the CLI packaging step.
|
|
225
|
+
|
|
226
|
+
## Post-install checklist (Auth)
|
|
227
|
+
|
|
228
|
+
1. Copy `.env.example` → `.env` and set DATABASE_URL, NEXTAUTH_URL, NEXTAUTH_SECRET (and GITHUB_ID/GITHUB_SECRET if using GitHub OAuth).
|
|
229
|
+
2. Generate Prisma client and run migrations:
|
|
230
|
+
|
|
231
|
+
npx prisma generate
|
|
232
|
+
npx prisma migrate dev -n init
|
|
233
|
+
|
|
234
|
+
3. (Optional) Seed demo data for quick testing:
|
|
235
|
+
|
|
236
|
+
SEED_ADMIN_EMAIL=admin@example.com SEED_ADMIN_PASSWORD=password123 node ./scripts/seed-demo.mjs
|
|
237
|
+
|
|
238
|
+
4. Start dev server:
|
|
239
|
+
|
|
240
|
+
npm run dev
|
|
241
|
+
|
|
242
|
+
5. Visit: `/auth/signup`, `/auth/login` and `/dashboard` for protected pages.
|
|
243
|
+
|
|
244
|
+
If you want, I can also create a short docs/AUTH_MANIFEST.json file containing the JSON manifest above in the repo (useful for CLI packaging). I will not create CLI code or copy files unless you ask.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jakob Bro Liebe Hansen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
|
|
5
|
+
export default function AdminPostsPlaceholderPage() {
|
|
6
|
+
return (
|
|
7
|
+
<main className="mx-auto max-w-2xl px-4 py-12">
|
|
8
|
+
<h1 className="text-2xl font-semibold text-foreground">
|
|
9
|
+
Posts admin requires the Data kit
|
|
10
|
+
</h1>
|
|
11
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
12
|
+
This project has the auth-core kit installed, which provides
|
|
13
|
+
authentication and basic admin scaffolding.
|
|
14
|
+
<br />
|
|
15
|
+
To create and manage posts, install the Data kit and wire up its admin
|
|
16
|
+
routes into your app.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<div className="mt-6 flex items-center gap-3 text-sm">
|
|
20
|
+
<Link
|
|
21
|
+
href="/dashboard"
|
|
22
|
+
className="text-primary underline-offset-4 hover:underline"
|
|
23
|
+
>
|
|
24
|
+
Back to dashboard
|
|
25
|
+
</Link>
|
|
26
|
+
</div>
|
|
27
|
+
</main>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
|
|
5
|
+
export default function AdminUsersPlaceholderPage() {
|
|
6
|
+
return (
|
|
7
|
+
<main className="mx-auto max-w-2xl px-4 py-12">
|
|
8
|
+
<h1 className="text-2xl font-semibold text-foreground">
|
|
9
|
+
Users admin requires the Data kit
|
|
10
|
+
</h1>
|
|
11
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
12
|
+
This project has the auth-core kit installed, which provides
|
|
13
|
+
authentication and basic admin scaffolding.
|
|
14
|
+
<br />
|
|
15
|
+
To manage users (list, filter, edit, etc.), install the Data kit and
|
|
16
|
+
add its admin routes to your app.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<div className="mt-6 flex items-center gap-3 text-sm">
|
|
20
|
+
<Link
|
|
21
|
+
href="/dashboard"
|
|
22
|
+
className="text-primary underline-offset-4 hover:underline"
|
|
23
|
+
>
|
|
24
|
+
Back to dashboard
|
|
25
|
+
</Link>
|
|
26
|
+
</div>
|
|
27
|
+
</main>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { prisma } from "@/lib/prisma";
|
|
3
|
+
import type { Prisma } from "@prisma/client";
|
|
4
|
+
import { jsonOk, jsonFail } from "@/lib/server/result";
|
|
5
|
+
import { getServerSession } from "next-auth";
|
|
6
|
+
import { authOptions } from "@/lib/auth";
|
|
7
|
+
|
|
8
|
+
// Ensure Prisma runs in Node.js (not Edge)
|
|
9
|
+
export const runtime = "nodejs";
|
|
10
|
+
|
|
11
|
+
type RouteContext = { params: Promise<{ id: string }> };
|
|
12
|
+
|
|
13
|
+
// Type guards/helpers (kept minimal here)
|
|
14
|
+
const hasErrorCode = (e: unknown, code: string): boolean =>
|
|
15
|
+
typeof e === "object" &&
|
|
16
|
+
e !== null &&
|
|
17
|
+
"code" in e &&
|
|
18
|
+
typeof (e as { code?: unknown }).code === "string" &&
|
|
19
|
+
(e as { code: string }).code === code;
|
|
20
|
+
|
|
21
|
+
export async function GET(_req: NextRequest, { params }: RouteContext) {
|
|
22
|
+
try {
|
|
23
|
+
const { id } = await params; // async params in Next.js 15
|
|
24
|
+
|
|
25
|
+
const user = await prisma.user.findUnique({
|
|
26
|
+
where: { id },
|
|
27
|
+
select: { id: true, name: true, email: true, role: true },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (!user) {
|
|
31
|
+
return jsonFail("Not found", { status: 404 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return jsonOk(user);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error("GET /api/users/[id] error:", e);
|
|
37
|
+
return jsonFail("Failed to fetch user", { status: 500 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function PUT(req: NextRequest, { params }: RouteContext) {
|
|
42
|
+
try {
|
|
43
|
+
// Only admins may update other users. Allow self-update as well.
|
|
44
|
+
const session = await getServerSession(authOptions);
|
|
45
|
+
if (!session?.user) return jsonFail("Unauthorized", { status: 401 });
|
|
46
|
+
|
|
47
|
+
const { id } = await params;
|
|
48
|
+
const isAdmin = (session.user as { role?: string }).role === "admin";
|
|
49
|
+
const isSelf = (session.user as { id?: string }).id === id;
|
|
50
|
+
if (!isAdmin && !isSelf) return jsonFail("Forbidden", { status: 403 });
|
|
51
|
+
|
|
52
|
+
const body: unknown = await req.json();
|
|
53
|
+
if (typeof body !== "object" || body === null) {
|
|
54
|
+
return jsonFail("Body must be a JSON object", { status: 400 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate with Zod (imported lazily to keep this route light)
|
|
58
|
+
const { userUpdateSchema } = await import("@/lib/validation/forms");
|
|
59
|
+
try {
|
|
60
|
+
const parsed = userUpdateSchema.parse(body);
|
|
61
|
+
|
|
62
|
+
// Build Prisma-compatible update object
|
|
63
|
+
const data: Prisma.UserUpdateInput = {};
|
|
64
|
+
if (parsed.name !== undefined)
|
|
65
|
+
data.name = parsed.name === "" ? null : parsed.name;
|
|
66
|
+
if (parsed.email !== undefined) data.email = parsed.email;
|
|
67
|
+
if (parsed.image !== undefined)
|
|
68
|
+
data.image = parsed.image === "" ? null : parsed.image;
|
|
69
|
+
if (parsed.password !== undefined) {
|
|
70
|
+
// Hash password before saving
|
|
71
|
+
const { hashPassword } = await import("@/lib/hash");
|
|
72
|
+
data.password = await hashPassword(parsed.password as string);
|
|
73
|
+
}
|
|
74
|
+
if (parsed.emailVerified !== undefined)
|
|
75
|
+
data.emailVerified = parsed.emailVerified as any;
|
|
76
|
+
|
|
77
|
+
const updated = await prisma.user.update({
|
|
78
|
+
where: { id },
|
|
79
|
+
data,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return jsonOk(updated, { status: 200, message: "User updated" });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
// Zod errors
|
|
85
|
+
if (err && typeof err === "object" && "issues" in (err as any)) {
|
|
86
|
+
const { jsonFromZod } = await import("@/lib/server/result");
|
|
87
|
+
return jsonFromZod(err as any, {
|
|
88
|
+
status: 400,
|
|
89
|
+
message: "Validation failed",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error("PUT /api/users/[id] error:", e);
|
|
96
|
+
if (hasErrorCode(e, "P2025")) {
|
|
97
|
+
return jsonFail("Not found", { status: 404 });
|
|
98
|
+
}
|
|
99
|
+
return jsonFail("Failed to update user", { status: 500 });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function DELETE(_req: NextRequest, { params }: RouteContext) {
|
|
104
|
+
try {
|
|
105
|
+
const { requireAdminApi } = await import("@/lib/auth-helpers");
|
|
106
|
+
const session = await requireAdminApi();
|
|
107
|
+
if (!session) return jsonFail("Forbidden", { status: 403 });
|
|
108
|
+
|
|
109
|
+
const { id } = await params;
|
|
110
|
+
|
|
111
|
+
// Clean up dependent records first to avoid FK violations
|
|
112
|
+
await prisma.$transaction([
|
|
113
|
+
prisma.account.deleteMany({ where: { userId: id } }),
|
|
114
|
+
prisma.session.deleteMany({ where: { userId: id } }),
|
|
115
|
+
prisma.post.deleteMany({ where: { authorId: id } }),
|
|
116
|
+
prisma.user.delete({ where: { id } }),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
return jsonOk({ ok: true }, { status: 200, message: "User deleted" });
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.error("DELETE /api/users/[id] error:", e);
|
|
122
|
+
if (hasErrorCode(e, "P2025")) {
|
|
123
|
+
return jsonFail("Not found", { status: 404 });
|
|
124
|
+
}
|
|
125
|
+
return jsonFail("Failed to delete user", { status: 500 });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -5,24 +5,27 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
5
5
|
import { cn } from "@/lib/utils";
|
|
6
6
|
|
|
7
7
|
const buttonVariants = cva(
|
|
8
|
-
"inline-flex items-center justify-center
|
|
8
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
12
|
-
default:
|
|
12
|
+
default:
|
|
13
|
+
"bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
|
|
13
14
|
destructive:
|
|
14
|
-
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
15
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 shadow-xs",
|
|
15
16
|
outline:
|
|
16
|
-
"
|
|
17
|
-
secondary:
|
|
18
|
-
|
|
17
|
+
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
|
|
20
|
+
ghost:
|
|
21
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
19
22
|
link: "text-primary underline-offset-4 hover:underline",
|
|
20
23
|
},
|
|
21
24
|
size: {
|
|
22
|
-
default: "h-
|
|
23
|
-
sm: "h-
|
|
24
|
-
lg: "h-
|
|
25
|
-
icon: "
|
|
25
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
26
|
+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
|
27
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
28
|
+
icon: "size-9",
|
|
26
29
|
},
|
|
27
30
|
},
|
|
28
31
|
defaultVariants: {
|
|
@@ -32,24 +35,88 @@ const buttonVariants = cva(
|
|
|
32
35
|
},
|
|
33
36
|
);
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
type ButtonProps = React.ComponentProps<"button"> &
|
|
39
|
+
VariantProps<typeof buttonVariants> & {
|
|
40
|
+
asChild?: boolean;
|
|
41
|
+
/** When true, bypasses tokenized buttonVariants so callers fully control classes */
|
|
42
|
+
unstyled?: boolean;
|
|
43
|
+
/** Opt-in: force inline CSS var styles for color/bg/border/ring */
|
|
44
|
+
forceInlineVars?: boolean;
|
|
45
|
+
};
|
|
40
46
|
|
|
41
47
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
42
|
-
(
|
|
48
|
+
(
|
|
49
|
+
{
|
|
50
|
+
className,
|
|
51
|
+
variant,
|
|
52
|
+
size,
|
|
53
|
+
asChild = false,
|
|
54
|
+
unstyled = false,
|
|
55
|
+
forceInlineVars = false,
|
|
56
|
+
...props
|
|
57
|
+
},
|
|
58
|
+
ref,
|
|
59
|
+
) => {
|
|
43
60
|
const Comp = asChild ? Slot : "button";
|
|
61
|
+
|
|
62
|
+
// Use caller-provided style; only inject inline var-driven colors when explicitly requested
|
|
63
|
+
const incomingStyle =
|
|
64
|
+
(props.style as React.CSSProperties | undefined) ?? undefined;
|
|
65
|
+
const finalStyle =
|
|
66
|
+
forceInlineVars && !unstyled
|
|
67
|
+
? {
|
|
68
|
+
...incomingStyle,
|
|
69
|
+
color: "var(--btn-fg)",
|
|
70
|
+
backgroundColor: "var(--btn-bg)",
|
|
71
|
+
borderColor: "var(--btn-border)",
|
|
72
|
+
"--tw-ring-color": "var(--btn-ring)",
|
|
73
|
+
}
|
|
74
|
+
: incomingStyle;
|
|
75
|
+
|
|
76
|
+
// Only enable CSS variable hooks when explicitly requested via inline vars
|
|
77
|
+
// or when the caller sets any [--btn-*] classes in className.
|
|
78
|
+
const wantsVarHooks =
|
|
79
|
+
!unstyled &&
|
|
80
|
+
(forceInlineVars ||
|
|
81
|
+
(typeof className === "string" && className.includes("[--btn-")));
|
|
82
|
+
|
|
44
83
|
return (
|
|
45
84
|
<Comp
|
|
46
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
47
85
|
ref={ref}
|
|
86
|
+
data-slot="button"
|
|
87
|
+
className={
|
|
88
|
+
unstyled
|
|
89
|
+
? cn(
|
|
90
|
+
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50",
|
|
91
|
+
className,
|
|
92
|
+
)
|
|
93
|
+
: cn(
|
|
94
|
+
buttonVariants({ variant, size }),
|
|
95
|
+
wantsVarHooks && [
|
|
96
|
+
// Color var hooks (apply only when CSS vars are provided)
|
|
97
|
+
"text-[var(--btn-fg)]",
|
|
98
|
+
"bg-[var(--btn-bg)]",
|
|
99
|
+
"hover:bg-[var(--btn-hover-bg)]",
|
|
100
|
+
"hover:text-[var(--btn-hover-fg)]",
|
|
101
|
+
// explicit dark variants to compete with dark: utilities from variants like outline
|
|
102
|
+
"dark:bg-[var(--btn-bg)]",
|
|
103
|
+
"dark:hover:bg-[var(--btn-hover-bg)]",
|
|
104
|
+
"dark:hover:text-[var(--btn-hover-fg)]",
|
|
105
|
+
// Focus ring and border hooks
|
|
106
|
+
"focus-visible:ring-[var(--btn-ring)]",
|
|
107
|
+
"border-[var(--btn-border)]",
|
|
108
|
+
"dark:border-[var(--btn-border)]",
|
|
109
|
+
],
|
|
110
|
+
className,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
style={finalStyle}
|
|
48
114
|
{...props}
|
|
49
115
|
/>
|
|
50
116
|
);
|
|
51
117
|
},
|
|
52
118
|
);
|
|
119
|
+
|
|
53
120
|
Button.displayName = "Button";
|
|
54
121
|
|
|
55
122
|
export { Button, buttonVariants };
|
|
@@ -2,8 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
|
|
3
3
|
import { cn } from "@/lib/utils";
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
5
|
+
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
|
7
6
|
|
|
8
7
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
9
8
|
({ className, type, ...props }, ref) => {
|
|
@@ -11,7 +10,10 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
11
10
|
<input
|
|
12
11
|
type={type}
|
|
13
12
|
className={cn(
|
|
14
|
-
|
|
13
|
+
// Base structural + token fallbacks
|
|
14
|
+
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/30 dark:aria-invalid:focus-visible:ring-destructive/40 flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
15
|
+
// CSS variable hooks (preset-first). When vars are unset, these are ignored and tokens above apply.
|
|
16
|
+
"focus-visible:ring-offset-background border-[var(--input-border)] bg-[var(--input-bg)] text-[var(--input-fg)] placeholder:text-[var(--input-placeholder)] focus-visible:ring-[var(--input-focus-ring,var(--ring))] focus-visible:ring-offset-2",
|
|
15
17
|
className,
|
|
16
18
|
)}
|
|
17
19
|
ref={ref}
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
1
|
import * as React from "react";
|
|
4
2
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
4
|
|
|
6
5
|
import { cn } from "@/lib/utils";
|
|
7
6
|
|
|
7
|
+
const labelVariants = cva(
|
|
8
|
+
"text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
9
|
+
);
|
|
10
|
+
|
|
8
11
|
const Label = React.forwardRef<
|
|
9
12
|
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
10
|
-
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
|
14
|
+
VariantProps<typeof labelVariants>
|
|
11
15
|
>(({ className, ...props }, ref) => (
|
|
12
16
|
<LabelPrimitive.Root
|
|
13
17
|
ref={ref}
|
|
14
18
|
className={cn(
|
|
15
|
-
|
|
19
|
+
labelVariants(),
|
|
20
|
+
// Optional color override via var; falls back to inherited color when unset
|
|
21
|
+
"text-[var(--label-fg)]",
|
|
16
22
|
className,
|
|
17
23
|
)}
|
|
18
24
|
{...props}
|
|
@@ -13,3 +13,13 @@ export const prisma =
|
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
|
16
|
+
|
|
17
|
+
if (process.env.NODE_ENV === "development" && process.platform === "win32") {
|
|
18
|
+
// Dev-only hint for Windows users hitting Prisma + Turbopack symlink issues
|
|
19
|
+
// (e.g. "create symlink to ../../../../node_modules/@prisma/client" with os error 1314).
|
|
20
|
+
// This is intentionally a console.warn so it surfaces clearly but only in dev.
|
|
21
|
+
// See docs/AUTH_QUICKSTART.md for full details.
|
|
22
|
+
console.warn(
|
|
23
|
+
"[nextworks][auth-core] On Windows with Next 16+ and Prisma, enable Windows Developer Mode or run your dev server from an elevated terminal to avoid Prisma symlink errors (os error 1314). See AUTH_QUICKSTART.md for details."
|
|
24
|
+
);
|
|
25
|
+
}
|