@dennisrongo/skills 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -16
- package/package.json +1 -1
- package/skills/nextjs-app-router/SKILL.md +228 -175
- package/skills/nextjs-app-router/references/anti-patterns.md +163 -99
- package/skills/nextjs-app-router/references/folder-layout.md +79 -46
- package/skills/nextjs-app-router/references/good-patterns.md +233 -99
- package/skills/nextjs-app-router/references/templates/api-base.md +24 -21
- package/skills/nextjs-app-router/references/templates/auth-slice.md +82 -78
- package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +58 -6
- package/skills/nextjs-app-router/references/templates/db-client.md +48 -0
- package/skills/nextjs-app-router/references/templates/env-and-utils.md +90 -23
- package/skills/nextjs-app-router/references/templates/feature-slice.md +110 -47
- package/skills/nextjs-app-router/references/templates/form-with-zod.md +23 -15
- package/skills/nextjs-app-router/references/templates/middleware.md +69 -59
- package/skills/nextjs-app-router/references/templates/next-config.md +4 -14
- package/skills/nextjs-app-router/references/templates/nextauth-config.md +178 -0
- package/skills/nextjs-app-router/references/templates/package.md +35 -5
- package/skills/nextjs-app-router/references/templates/prisma-schema.md +162 -0
- package/skills/nextjs-app-router/references/templates/providers-and-store.md +35 -21
- package/skills/nextjs-app-router/references/templates/root-layout.md +99 -20
- package/skills/nextjs-app-router/references/templates/route-group-layouts.md +46 -6
- package/skills/nextjs-app-router/references/templates/route-handler.md +168 -0
- package/skills/nextjs-app-router/references/templates/testing.md +105 -31
- package/skills/tauri-2-app/SKILL.md +381 -0
- package/skills/tauri-2-app/references/anti-patterns.md +434 -0
- package/skills/tauri-2-app/references/folder-layout.md +161 -0
- package/skills/tauri-2-app/references/good-patterns.md +477 -0
- package/skills/tauri-2-app/references/templates/build-rs.md +51 -0
- package/skills/tauri-2-app/references/templates/capabilities.md +68 -0
- package/skills/tauri-2-app/references/templates/cargo-toml.md +118 -0
- package/skills/tauri-2-app/references/templates/ci-workflow.md +99 -0
- package/skills/tauri-2-app/references/templates/encryption-mod.md +228 -0
- package/skills/tauri-2-app/references/templates/error-mod.md +126 -0
- package/skills/tauri-2-app/references/templates/lib-rs.md +98 -0
- package/skills/tauri-2-app/references/templates/macos-plist.md +89 -0
- package/skills/tauri-2-app/references/templates/main-rs.md +21 -0
- package/skills/tauri-2-app/references/templates/platform-traits.md +217 -0
- package/skills/tauri-2-app/references/templates/storage-mod.md +172 -0
- package/skills/tauri-2-app/references/templates/tauri-conf.md +136 -0
- package/skills/tauri-2-app/references/templates/use-tauri-command.md +194 -0
- package/skills/tauri-2-app/references/templates/vite-and-tsconfig.md +189 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# NextAuth (Auth.js v5) configuration
|
|
2
|
+
|
|
3
|
+
Two files, by design:
|
|
4
|
+
|
|
5
|
+
- **`src/auth.config.ts`** — Edge-safe. Providers, callbacks, page mappings. **No DB imports.** Consumed by `middleware.ts`.
|
|
6
|
+
- **`src/auth.ts`** — full config. Imports `auth.config.ts`, adds the Prisma adapter, adds `authorize` for Credentials (which needs `db` + `bcryptjs`). Consumed by Route Handlers and server components.
|
|
7
|
+
|
|
8
|
+
The split is mandatory because middleware runs on the Edge runtime, which can't import the Prisma client (Node-only). Collapsing them produces a confusing "module not found in edge runtime" error at build time.
|
|
9
|
+
|
|
10
|
+
## `src/auth.config.ts` (EDGE-SAFE)
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import type { NextAuthConfig } from 'next-auth';
|
|
14
|
+
import Credentials from 'next-auth/providers/credentials';
|
|
15
|
+
// import GitHub from 'next-auth/providers/github'; // uncomment if enabled in Step 2
|
|
16
|
+
// import Google from 'next-auth/providers/google'; // uncomment if enabled in Step 2
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
providers: [
|
|
20
|
+
Credentials({
|
|
21
|
+
// The `authorize` function lives in src/auth.ts (it needs DB + bcryptjs).
|
|
22
|
+
// Here we only declare the credentials shape so the provider is registered for type inference.
|
|
23
|
+
credentials: {
|
|
24
|
+
email: { label: 'Email', type: 'email' },
|
|
25
|
+
password: { label: 'Password', type: 'password' },
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
// GitHub({ clientId: process.env.AUTH_GITHUB_ID!, clientSecret: process.env.AUTH_GITHUB_SECRET! }),
|
|
29
|
+
// Google({ clientId: process.env.AUTH_GOOGLE_ID!, clientSecret: process.env.AUTH_GOOGLE_SECRET! }),
|
|
30
|
+
],
|
|
31
|
+
pages: {
|
|
32
|
+
signIn: '/auth/login',
|
|
33
|
+
error: '/auth/error',
|
|
34
|
+
},
|
|
35
|
+
callbacks: {
|
|
36
|
+
authorized({ auth, request: { nextUrl } }) {
|
|
37
|
+
const isLoggedIn = !!auth?.user;
|
|
38
|
+
const onPublic = nextUrl.pathname.startsWith('/auth');
|
|
39
|
+
const onApp = nextUrl.pathname.startsWith('/app');
|
|
40
|
+
const onAdmin = nextUrl.pathname.startsWith('/admin');
|
|
41
|
+
|
|
42
|
+
if (onPublic) {
|
|
43
|
+
return isLoggedIn ? Response.redirect(new URL('/app/dashboard', nextUrl)) : true;
|
|
44
|
+
}
|
|
45
|
+
if (onApp || onAdmin) {
|
|
46
|
+
return isLoggedIn; // false → redirect to pages.signIn
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
async jwt({ token, user }) {
|
|
51
|
+
if (user) {
|
|
52
|
+
token.id = user.id as string;
|
|
53
|
+
// Add `role` here if the User model has one:
|
|
54
|
+
// if ('role' in user) token.role = user.role as string;
|
|
55
|
+
}
|
|
56
|
+
return token;
|
|
57
|
+
},
|
|
58
|
+
async session({ session, token }) {
|
|
59
|
+
if (token?.id && session.user) {
|
|
60
|
+
session.user.id = token.id as string;
|
|
61
|
+
// if (token.role) session.user.role = token.role as string;
|
|
62
|
+
}
|
|
63
|
+
return session;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
} satisfies NextAuthConfig;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Edge-safety checklist for this file:**
|
|
70
|
+
|
|
71
|
+
- No `import { db } from '@/lib/db'`.
|
|
72
|
+
- No `import { PrismaAdapter } from '@auth/prisma-adapter'`.
|
|
73
|
+
- No `import bcrypt from 'bcryptjs'`.
|
|
74
|
+
- No `import { auth } from '@/auth'` (circular).
|
|
75
|
+
|
|
76
|
+
## `src/auth.ts` (full config, Node-only)
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import NextAuth from 'next-auth';
|
|
80
|
+
import Credentials from 'next-auth/providers/credentials';
|
|
81
|
+
import { PrismaAdapter } from '@auth/prisma-adapter';
|
|
82
|
+
import bcrypt from 'bcryptjs';
|
|
83
|
+
import { z } from 'zod';
|
|
84
|
+
import { db } from '@/lib/db';
|
|
85
|
+
import authConfig from './auth.config';
|
|
86
|
+
|
|
87
|
+
const credentialsSchema = z.object({
|
|
88
|
+
email: z.string().email(),
|
|
89
|
+
password: z.string().min(1),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
93
|
+
...authConfig,
|
|
94
|
+
adapter: PrismaAdapter(db),
|
|
95
|
+
// JWT strategy is REQUIRED when Credentials is one of the providers.
|
|
96
|
+
// Database sessions don't work with Credentials by NextAuth design.
|
|
97
|
+
session: { strategy: 'jwt' },
|
|
98
|
+
providers: [
|
|
99
|
+
// Re-declare Credentials here with the real `authorize` (which needs db + bcrypt).
|
|
100
|
+
Credentials({
|
|
101
|
+
credentials: {
|
|
102
|
+
email: { label: 'Email', type: 'email' },
|
|
103
|
+
password: { label: 'Password', type: 'password' },
|
|
104
|
+
},
|
|
105
|
+
async authorize(rawCredentials) {
|
|
106
|
+
const parsed = credentialsSchema.safeParse(rawCredentials);
|
|
107
|
+
if (!parsed.success) return null;
|
|
108
|
+
|
|
109
|
+
const user = await db.user.findUnique({
|
|
110
|
+
where: { email: parsed.data.email },
|
|
111
|
+
select: { id: true, email: true, name: true, image: true, password: true, role: true },
|
|
112
|
+
});
|
|
113
|
+
if (!user?.password) return null;
|
|
114
|
+
|
|
115
|
+
const ok = await bcrypt.compare(parsed.data.password, user.password);
|
|
116
|
+
if (!ok) return null;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
id: user.id,
|
|
120
|
+
email: user.email,
|
|
121
|
+
name: user.name,
|
|
122
|
+
image: user.image,
|
|
123
|
+
role: user.role,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
// Any OAuth providers from authConfig.providers carry over via the spread above —
|
|
128
|
+
// re-declare them here only if they need server-only secrets that auth.config.ts couldn't see.
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## `src/app/api/auth/[...nextauth]/route.ts`
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
export { GET, POST } from '@/auth';
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
That's the entire file. NextAuth's `handlers` is `{ GET, POST }`, and `auth.ts` re-exports them. The catch-all route gets every `/api/auth/*` request (sign-in, callback, CSRF, session, etc.).
|
|
140
|
+
|
|
141
|
+
## `src/types/next-auth.d.ts`
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import 'next-auth';
|
|
145
|
+
import 'next-auth/jwt';
|
|
146
|
+
|
|
147
|
+
declare module 'next-auth' {
|
|
148
|
+
interface Session {
|
|
149
|
+
user: {
|
|
150
|
+
id: string;
|
|
151
|
+
email: string;
|
|
152
|
+
name?: string | null;
|
|
153
|
+
image?: string | null;
|
|
154
|
+
role?: string;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface User {
|
|
159
|
+
id: string;
|
|
160
|
+
role?: string;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
declare module 'next-auth/jwt' {
|
|
165
|
+
interface JWT {
|
|
166
|
+
id: string;
|
|
167
|
+
role?: string;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Without this file, TypeScript thinks `session.user.id` is `undefined`.
|
|
173
|
+
|
|
174
|
+
## Forbidden in `auth.config.ts`
|
|
175
|
+
|
|
176
|
+
- Importing `@/lib/db`, `@/auth`, `@auth/prisma-adapter`, `bcryptjs`, or anything that pulls in Node-only modules.
|
|
177
|
+
- Defining the Credentials `authorize` here (it needs the DB).
|
|
178
|
+
- Any logic that calls `cookies()` or `headers()` from `next/headers` — those are server-only.
|
|
@@ -19,13 +19,27 @@ Resolve **every** version via context7 at scaffold time. Versions below are plac
|
|
|
19
19
|
"test": "vitest run",
|
|
20
20
|
"test:watch": "vitest",
|
|
21
21
|
"test:e2e": "playwright test",
|
|
22
|
+
"db:generate": "prisma generate",
|
|
23
|
+
"db:migrate": "prisma migrate dev",
|
|
24
|
+
"db:migrate:deploy": "prisma migrate deploy",
|
|
25
|
+
"db:studio": "prisma studio",
|
|
26
|
+
"db:seed": "tsx prisma/seed.ts",
|
|
22
27
|
"prepare": "husky"
|
|
23
28
|
},
|
|
29
|
+
"prisma": {
|
|
30
|
+
"seed": "tsx prisma/seed.ts"
|
|
31
|
+
},
|
|
24
32
|
"dependencies": {
|
|
25
33
|
"next": "^x.y.z",
|
|
26
34
|
"react": "^x.y.z",
|
|
27
35
|
"react-dom": "^x.y.z",
|
|
28
36
|
|
|
37
|
+
"next-auth": "^x.y.z",
|
|
38
|
+
"@auth/prisma-adapter": "^x.y.z",
|
|
39
|
+
"bcryptjs": "^x.y.z",
|
|
40
|
+
|
|
41
|
+
"@prisma/client": "^x.y.z",
|
|
42
|
+
|
|
29
43
|
"@reduxjs/toolkit": "^x.y.z",
|
|
30
44
|
"react-redux": "^x.y.z",
|
|
31
45
|
|
|
@@ -54,6 +68,10 @@ Resolve **every** version via context7 at scaffold time. Versions below are plac
|
|
|
54
68
|
"@types/node": "^x.y.z",
|
|
55
69
|
"@types/react": "^x.y.z",
|
|
56
70
|
"@types/react-dom": "^x.y.z",
|
|
71
|
+
"@types/bcryptjs": "^x.y.z",
|
|
72
|
+
|
|
73
|
+
"prisma": "^x.y.z",
|
|
74
|
+
"tsx": "^x.y.z",
|
|
57
75
|
|
|
58
76
|
"tailwindcss": "^x.y.z",
|
|
59
77
|
"postcss": "^x.y.z",
|
|
@@ -81,19 +99,31 @@ Resolve **every** version via context7 at scaffold time. Versions below are plac
|
|
|
81
99
|
},
|
|
82
100
|
"lint-staged": {
|
|
83
101
|
"*.{ts,tsx}": ["prettier --write", "eslint --fix"],
|
|
84
|
-
"*.{json,md,css,yml,yaml}": ["prettier --write"]
|
|
102
|
+
"*.{json,md,css,yml,yaml,prisma}": ["prettier --write"]
|
|
85
103
|
}
|
|
86
104
|
}
|
|
87
105
|
```
|
|
88
106
|
|
|
89
|
-
|
|
107
|
+
## Notes on version resolution
|
|
108
|
+
|
|
109
|
+
- **`next-auth`**: Auth.js v5 (the version with the `handlers`/`auth`/`signIn`/`signOut` exports). Check context7 for the current version line — at certain points this is published under a `@beta` tag. Quote the resolved version back to the user before installing.
|
|
110
|
+
- **`bcryptjs` vs `bcrypt` vs `@node-rs/argon2`**: pure-JS `bcryptjs` is the safest default — it works on every Node version and in serverless without native bindings. Switch to `@node-rs/argon2` only if the user explicitly asks for Argon2 and confirms their deploy target supports native bindings.
|
|
111
|
+
- **`tsx`** is for running `prisma/seed.ts` and any TypeScript scripts without a build step.
|
|
112
|
+
|
|
113
|
+
## Do NOT include
|
|
114
|
+
|
|
90
115
|
- `moment` (use `date-fns`)
|
|
91
|
-
- `styled-components` / `@emotion/*` (
|
|
116
|
+
- `styled-components` / `@emotion/*` (Tailwind only)
|
|
92
117
|
- `nprogress` (use `next-nprogress-bar`)
|
|
93
|
-
-
|
|
118
|
+
- `jsonwebtoken` or `jose` for the application layer — NextAuth handles JWT signing/verification internally
|
|
119
|
+
- `js-cookie` — NextAuth manages the session cookie
|
|
120
|
+
- A second auth library (`Clerk`, `Lucia`, `Iron Session`, etc.) alongside NextAuth
|
|
121
|
+
- A second ORM (`Drizzle`, `Kysely`, `TypeORM`) alongside Prisma
|
|
122
|
+
|
|
123
|
+
## Add only if the user opted in during Step 2
|
|
94
124
|
|
|
95
|
-
**Add only if the user opted in during Step 2:**
|
|
96
125
|
- `pusher-js` (real-time)
|
|
97
126
|
- Storybook packages
|
|
98
127
|
- `next-intl` / `next-i18next` (i18n)
|
|
99
128
|
- `@sentry/nextjs` (error tracking)
|
|
129
|
+
- `@auth/core` providers not in the default install (e.g. specific OAuth providers shipped separately)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Prisma schema (NextAuth + domain)
|
|
2
|
+
|
|
3
|
+
One `schema.prisma` file holds the NextAuth tables (so the Prisma adapter works) plus your domain models. Verify field shapes against the current `@auth/prisma-adapter` docs via context7 at scaffold time — the adapter's required columns evolve.
|
|
4
|
+
|
|
5
|
+
## `prisma/schema.prisma`
|
|
6
|
+
|
|
7
|
+
```prisma
|
|
8
|
+
// This file is the source of truth for the database schema.
|
|
9
|
+
// Every change goes through `pnpm exec prisma migrate dev --name <slug>`.
|
|
10
|
+
|
|
11
|
+
generator client {
|
|
12
|
+
provider = "prisma-client-js"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
datasource db {
|
|
16
|
+
provider = "postgresql"
|
|
17
|
+
url = env("DATABASE_URL")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---------- NextAuth tables ----------
|
|
21
|
+
// Shape required by @auth/prisma-adapter. Verify against current adapter docs.
|
|
22
|
+
|
|
23
|
+
model User {
|
|
24
|
+
id String @id @default(cuid())
|
|
25
|
+
name String?
|
|
26
|
+
email String @unique
|
|
27
|
+
emailVerified DateTime?
|
|
28
|
+
image String?
|
|
29
|
+
|
|
30
|
+
// Required when the Credentials provider is enabled.
|
|
31
|
+
// OAuth-only users won't have a password — keep it nullable.
|
|
32
|
+
password String?
|
|
33
|
+
|
|
34
|
+
// Custom: optional role for (admin) gating.
|
|
35
|
+
role String? @default("user")
|
|
36
|
+
|
|
37
|
+
accounts Account[]
|
|
38
|
+
sessions Session[]
|
|
39
|
+
|
|
40
|
+
// Domain relations:
|
|
41
|
+
customers Customer[]
|
|
42
|
+
|
|
43
|
+
createdAt DateTime @default(now())
|
|
44
|
+
updatedAt DateTime @updatedAt
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
model Account {
|
|
48
|
+
id String @id @default(cuid())
|
|
49
|
+
userId String
|
|
50
|
+
type String
|
|
51
|
+
provider String
|
|
52
|
+
providerAccountId String
|
|
53
|
+
refresh_token String? @db.Text
|
|
54
|
+
access_token String? @db.Text
|
|
55
|
+
expires_at Int?
|
|
56
|
+
token_type String?
|
|
57
|
+
scope String?
|
|
58
|
+
id_token String? @db.Text
|
|
59
|
+
session_state String?
|
|
60
|
+
|
|
61
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
62
|
+
|
|
63
|
+
@@unique([provider, providerAccountId])
|
|
64
|
+
@@index([userId])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
model Session {
|
|
68
|
+
// Present even when session.strategy = 'jwt' — the adapter still references it.
|
|
69
|
+
id String @id @default(cuid())
|
|
70
|
+
sessionToken String @unique
|
|
71
|
+
userId String
|
|
72
|
+
expires DateTime
|
|
73
|
+
|
|
74
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
75
|
+
|
|
76
|
+
@@index([userId])
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
model VerificationToken {
|
|
80
|
+
identifier String
|
|
81
|
+
token String @unique
|
|
82
|
+
expires DateTime
|
|
83
|
+
|
|
84
|
+
@@unique([identifier, token])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------- Domain models ----------
|
|
88
|
+
|
|
89
|
+
model Customer {
|
|
90
|
+
id String @id @default(cuid())
|
|
91
|
+
userId String
|
|
92
|
+
name String
|
|
93
|
+
email String
|
|
94
|
+
|
|
95
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
96
|
+
|
|
97
|
+
createdAt DateTime @default(now())
|
|
98
|
+
updatedAt DateTime @updatedAt
|
|
99
|
+
|
|
100
|
+
@@index([userId])
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## `prisma/seed.ts` (OPTIONAL — only if Credentials and a test user was requested)
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { PrismaClient } from '@prisma/client';
|
|
108
|
+
import bcrypt from 'bcryptjs';
|
|
109
|
+
|
|
110
|
+
const db = new PrismaClient();
|
|
111
|
+
|
|
112
|
+
async function main() {
|
|
113
|
+
const password = await bcrypt.hash('test1234', 10);
|
|
114
|
+
|
|
115
|
+
await db.user.upsert({
|
|
116
|
+
where: { email: 'test@example.com' },
|
|
117
|
+
update: {},
|
|
118
|
+
create: {
|
|
119
|
+
email: 'test@example.com',
|
|
120
|
+
name: 'Test User',
|
|
121
|
+
password,
|
|
122
|
+
role: 'admin',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log('Seeded test@example.com / test1234');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main()
|
|
130
|
+
.catch((e) => {
|
|
131
|
+
console.error(e);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
})
|
|
134
|
+
.finally(() => db.$disconnect());
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Run with `pnpm db:seed`. **Never seed production.**
|
|
138
|
+
|
|
139
|
+
## Migration commands
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Dev (creates a new migration + applies it):
|
|
143
|
+
pnpm exec prisma migrate dev --name init
|
|
144
|
+
|
|
145
|
+
# CI / prod (applies committed migrations without prompting):
|
|
146
|
+
pnpm exec prisma migrate deploy
|
|
147
|
+
|
|
148
|
+
# Regenerate the typed client after a schema change (migrate dev does this for you):
|
|
149
|
+
pnpm exec prisma generate
|
|
150
|
+
|
|
151
|
+
# Inspect the DB visually (dev only):
|
|
152
|
+
pnpm exec prisma studio
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Forbidden
|
|
156
|
+
|
|
157
|
+
- `prisma db push` in CI or against any shared database. Use `prisma migrate dev` (development) or `prisma migrate deploy` (everything else).
|
|
158
|
+
- Running `prisma migrate reset` without explicit user confirmation — it drops the DB.
|
|
159
|
+
- Editing migration files after they've been applied to any non-throwaway database. Create a follow-up migration instead.
|
|
160
|
+
- Adding domain tables that reference `Account` or `Session` directly. Those are NextAuth's tables; reference `User` instead.
|
|
161
|
+
- Storing OAuth tokens or sensitive secrets in domain models — they belong in `Account` (which NextAuth populates).
|
|
162
|
+
- Plain-text passwords. Always `bcrypt.hash(password, 10)` (or higher cost factor) before storing.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Providers, store, typed hooks
|
|
2
2
|
|
|
3
|
-
## `src/redux/providers.tsx` (CLIENT
|
|
3
|
+
## `src/redux/providers.tsx` (CLIENT)
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
|
+
import { SessionProvider } from 'next-auth/react';
|
|
8
9
|
import { Provider } from 'react-redux';
|
|
9
10
|
import { AppProgressBar as ProgressBar } from 'next-nprogress-bar';
|
|
10
11
|
import { Toaster } from '@/components/ui/toaster';
|
|
@@ -12,39 +13,47 @@ import { store } from './store';
|
|
|
12
13
|
|
|
13
14
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
14
15
|
return (
|
|
15
|
-
<
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
<SessionProvider>
|
|
17
|
+
<Provider store={store}>
|
|
18
|
+
{children}
|
|
19
|
+
<Toaster />
|
|
20
|
+
<ProgressBar
|
|
21
|
+
height="3px"
|
|
22
|
+
color="hsl(var(--primary))"
|
|
23
|
+
shallowRouting
|
|
24
|
+
disableSameURL
|
|
25
|
+
options={{ showSpinner: false }}
|
|
26
|
+
/>
|
|
27
|
+
</Provider>
|
|
28
|
+
</SessionProvider>
|
|
26
29
|
);
|
|
27
30
|
}
|
|
28
31
|
```
|
|
29
32
|
|
|
33
|
+
**Order matters:** `<SessionProvider>` outside so any component (including Redux-connected ones) can `useSession()`. Redux inside so RTK Query and dispatches work normally.
|
|
34
|
+
|
|
30
35
|
## `src/redux/store.ts`
|
|
31
36
|
|
|
32
37
|
```ts
|
|
33
38
|
import { combineReducers, configureStore, type Action } from '@reduxjs/toolkit';
|
|
34
39
|
import { api } from './api/api';
|
|
35
|
-
import { authApi } from './api/authApi';
|
|
36
|
-
import { authSlice } from './features/authSlice';
|
|
37
40
|
|
|
38
41
|
const combinedReducer = combineReducers({
|
|
39
42
|
[api.reducerPath]: api.reducer,
|
|
40
|
-
|
|
43
|
+
// Add additional slices here ONLY for genuinely shared async/global state.
|
|
44
|
+
// Auth/session is NOT a slice — it lives in NextAuth's SessionProvider.
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
type CombinedState = ReturnType<typeof combinedReducer>;
|
|
44
48
|
|
|
49
|
+
const SIGN_OUT = '__signout__' as const;
|
|
50
|
+
|
|
51
|
+
export const signOutEvent = { type: SIGN_OUT } as const;
|
|
52
|
+
|
|
45
53
|
const rootReducer = (state: CombinedState | undefined, action: Action): CombinedState => {
|
|
46
|
-
// Wipe the entire store
|
|
47
|
-
|
|
54
|
+
// Wipe the entire store when the app dispatches signOutEvent (called from the
|
|
55
|
+
// baseQuery 401 handler before invoking next-auth's signOut).
|
|
56
|
+
if (action.type === SIGN_OUT) {
|
|
48
57
|
return combinedReducer(undefined, action);
|
|
49
58
|
}
|
|
50
59
|
return combinedReducer(state, action);
|
|
@@ -55,8 +64,7 @@ export const store = configureStore({
|
|
|
55
64
|
middleware: (getDefaultMiddleware) =>
|
|
56
65
|
getDefaultMiddleware({
|
|
57
66
|
// Keep serializableCheck ON. If a specific value is genuinely non-serializable,
|
|
58
|
-
// add
|
|
59
|
-
// serializableCheck: { ignoredPaths: ['auth.expiresAt'] },
|
|
67
|
+
// add { ignoredPaths: ['...'] } here with a comment explaining why.
|
|
60
68
|
}).concat(api.middleware),
|
|
61
69
|
});
|
|
62
70
|
|
|
@@ -64,7 +72,7 @@ export type RootState = ReturnType<typeof store.getState>;
|
|
|
64
72
|
export type AppDispatch = typeof store.dispatch;
|
|
65
73
|
```
|
|
66
74
|
|
|
67
|
-
**No `@ts-ignore`. No `serializableCheck: false`. No commented-out reducers.**
|
|
75
|
+
**No `@ts-ignore`. No `serializableCheck: false`. No commented-out reducers. No auth slice — NextAuth owns session state.**
|
|
68
76
|
|
|
69
77
|
## `src/redux/hooks.ts`
|
|
70
78
|
|
|
@@ -76,4 +84,10 @@ export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
|
76
84
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
77
85
|
```
|
|
78
86
|
|
|
79
|
-
Components import `useAppDispatch` / `useAppSelector` — never the raw `useDispatch` / `useSelector` from `react-redux`. ESLint can enforce this with `no-restricted-imports
|
|
87
|
+
Components import `useAppDispatch` / `useAppSelector` — never the raw `useDispatch` / `useSelector` from `react-redux`. ESLint can enforce this with `no-restricted-imports`.
|
|
88
|
+
|
|
89
|
+
## Why no `authSlice`
|
|
90
|
+
|
|
91
|
+
NextAuth's `SessionProvider` is the single source of session truth. Components read it with `useSession()`. Duplicating that into a Redux slice creates two sources of truth that drift apart (the slice doesn't know when NextAuth refreshes the JWT, etc.) and doubles the work on sign-in/out.
|
|
92
|
+
|
|
93
|
+
If you need session-derived data inside a Redux slice (rare — typically you'd put it in the request itself), select from `useSession()` at the component boundary and pass it as an action payload.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Root layout,
|
|
1
|
+
# Root layout, error/not-found, login page
|
|
2
2
|
|
|
3
|
-
## `src/app/layout.tsx` (SERVER component)
|
|
3
|
+
## `src/app/layout.tsx` (SERVER component — the only one)
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
6
|
import '@/app/globals.css';
|
|
@@ -26,35 +26,51 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
**NO `'use client'`. NO
|
|
29
|
+
**NO `'use client'`. NO data fetching. NO `await db.*`.** This is the only server component in the app and its job is to render `<Providers>`.
|
|
30
30
|
|
|
31
|
-
## `src/app/page.tsx`
|
|
31
|
+
## No `src/app/page.tsx` by default
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
`middleware.ts` handles `/`:
|
|
34
|
+
- Signed-in users → `/app/dashboard` (302).
|
|
35
|
+
- Signed-out users → `/auth/login` (302).
|
|
36
|
+
|
|
37
|
+
Don't create an `app/page.tsx` unless the project has a public landing page. If it does, the page is `'use client'`:
|
|
34
38
|
|
|
35
39
|
```tsx
|
|
36
|
-
|
|
40
|
+
// src/app/page.tsx — ONLY if a marketing landing is needed
|
|
41
|
+
'use client';
|
|
42
|
+
import Link from 'next/link';
|
|
43
|
+
import { Button } from '@/components/ui/button';
|
|
37
44
|
|
|
38
|
-
export default function
|
|
39
|
-
|
|
45
|
+
export default function LandingPage() {
|
|
46
|
+
return (
|
|
47
|
+
<main className="flex min-h-screen flex-col items-center justify-center gap-6">
|
|
48
|
+
<h1 className="text-4xl font-bold">{{Project Title}}</h1>
|
|
49
|
+
<Button asChild>
|
|
50
|
+
<Link href="/auth/login">Sign in</Link>
|
|
51
|
+
</Button>
|
|
52
|
+
</main>
|
|
53
|
+
);
|
|
40
54
|
}
|
|
41
55
|
```
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
**Forbidden:**
|
|
57
|
+
**Forbidden** (do not generate):
|
|
46
58
|
|
|
47
59
|
```tsx
|
|
48
|
-
// ❌
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
// ❌ server page doing a redirect
|
|
61
|
+
import { redirect } from 'next/navigation';
|
|
62
|
+
export default function Home() { redirect('/dashboard'); }
|
|
63
|
+
|
|
64
|
+
// ❌ server page doing data fetching
|
|
65
|
+
export default async function Home() {
|
|
66
|
+
const session = await auth();
|
|
67
|
+
const data = await db.x.findMany();
|
|
68
|
+
return <Dashboard data={data} session={session} />;
|
|
55
69
|
}
|
|
56
70
|
```
|
|
57
71
|
|
|
72
|
+
The middleware handles redirects. RTK Query handles data.
|
|
73
|
+
|
|
58
74
|
## `src/app/error.tsx` (CLIENT — Next requires it)
|
|
59
75
|
|
|
60
76
|
```tsx
|
|
@@ -85,9 +101,11 @@ export default function GlobalError({
|
|
|
85
101
|
}
|
|
86
102
|
```
|
|
87
103
|
|
|
88
|
-
## `src/app/not-found.tsx` (
|
|
104
|
+
## `src/app/not-found.tsx` (CLIENT, since no SSR)
|
|
89
105
|
|
|
90
106
|
```tsx
|
|
107
|
+
'use client';
|
|
108
|
+
|
|
91
109
|
import Link from 'next/link';
|
|
92
110
|
import { Button } from '@/components/ui/button';
|
|
93
111
|
|
|
@@ -103,7 +121,68 @@ export default function NotFound() {
|
|
|
103
121
|
}
|
|
104
122
|
```
|
|
105
123
|
|
|
106
|
-
## `src/app/
|
|
124
|
+
## `src/app/(public)/auth/login/page.tsx` (CLIENT, calls NextAuth `signIn`)
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
'use client';
|
|
128
|
+
|
|
129
|
+
import { useState } from 'react';
|
|
130
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
131
|
+
import { signIn } from 'next-auth/react';
|
|
132
|
+
import { useForm } from 'react-hook-form';
|
|
133
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
134
|
+
import { z } from 'zod';
|
|
135
|
+
import { Form } from '@/components/ui/form';
|
|
136
|
+
import { Button } from '@/components/ui/button';
|
|
137
|
+
import { FormInputField } from '@/components/forms/FormInputField';
|
|
138
|
+
import { FormPasswordField } from '@/components/forms/FormPasswordField';
|
|
139
|
+
|
|
140
|
+
const loginSchema = z.object({
|
|
141
|
+
email: z.string().email(),
|
|
142
|
+
password: z.string().min(1, 'Required'),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
type LoginFormValues = z.infer<typeof loginSchema>;
|
|
146
|
+
|
|
147
|
+
export default function LoginPage() {
|
|
148
|
+
const router = useRouter();
|
|
149
|
+
const params = useSearchParams();
|
|
150
|
+
const returnTo = params.get('returnTo') ?? '/app/dashboard';
|
|
151
|
+
const [error, setError] = useState<string | null>(null);
|
|
152
|
+
|
|
153
|
+
const form = useForm<LoginFormValues>({
|
|
154
|
+
resolver: zodResolver(loginSchema),
|
|
155
|
+
defaultValues: { email: '', password: '' },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const onSubmit = async (values: LoginFormValues) => {
|
|
159
|
+
setError(null);
|
|
160
|
+
const res = await signIn('credentials', { ...values, redirect: false });
|
|
161
|
+
if (res?.error) {
|
|
162
|
+
setError('Invalid email or password.');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
router.replace(returnTo);
|
|
166
|
+
router.refresh();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Form {...form}>
|
|
171
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
172
|
+
<h1 className="text-xl font-semibold">Sign in</h1>
|
|
173
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
174
|
+
<FormInputField form={form} schema={loginSchema} fieldName="email" label="Email" type="email" autoComplete="email" />
|
|
175
|
+
<FormPasswordField form={form} schema={loginSchema} fieldName="password" label="Password" autoComplete="current-password" />
|
|
176
|
+
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
|
|
177
|
+
{form.formState.isSubmitting ? 'Signing in…' : 'Sign in'}
|
|
178
|
+
</Button>
|
|
179
|
+
</form>
|
|
180
|
+
</Form>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## `src/app/globals.css` (Tailwind + shadcn tokens)
|
|
107
186
|
|
|
108
187
|
```css
|
|
109
188
|
@tailwind base;
|