ebm-skills 1.0.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 +44 -0
- package/bin/cli.js +150 -0
- package/package.json +22 -0
- package/skills/ebm-auth/REFERENCE.md +299 -0
- package/skills/ebm-auth/SKILL.md +38 -0
- package/skills/ebm-form/REFERENCE.md +365 -0
- package/skills/ebm-form/SKILL.md +45 -0
- package/skills/ebm-init/REFERENCE.md +264 -0
- package/skills/ebm-init/SKILL.md +36 -0
- package/skills/ebm-table/REFERENCE.md +337 -0
- package/skills/ebm-table/SKILL.md +37 -0
- package/skills/ebm-thai/REFERENCE.md +127 -0
- package/skills/ebm-thai/SKILL.md +29 -0
- package/skills/ebm-upload/REFERENCE.md +521 -0
- package/skills/ebm-upload/SKILL.md +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# ebm-skills
|
|
2
|
+
|
|
3
|
+
Claude Code skills for EBM Next.js projects.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx ebm-skills
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Install specific skills only:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx ebm-skills --only init,auth,table
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Restart Claude Code after installing.
|
|
18
|
+
|
|
19
|
+
## Skills
|
|
20
|
+
|
|
21
|
+
| Command | What it does |
|
|
22
|
+
|---------|-------------|
|
|
23
|
+
| `/ebm-init` | New project wizard (create-next-app + Tailwind + UI lib + DB) |
|
|
24
|
+
| `/ebm-auth` | Full auth system — JWT, RBAC, signin/forgot-password/user management pages |
|
|
25
|
+
| `/ebm-table` | Server-side data table with pagination, search, and sort |
|
|
26
|
+
| `/ebm-form` | Create/edit or search/filter form with React Hook Form + Zod |
|
|
27
|
+
| `/ebm-upload` | Drag & drop file upload (local disk or S3-compatible) |
|
|
28
|
+
| `/ebm-thai` | Scan & fix informal Thai UI text using formal Thai glossary |
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- [Claude Code](https://claude.ai/download) installed
|
|
33
|
+
- Node.js 18+
|
|
34
|
+
|
|
35
|
+
## Typical workflow
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
/ebm-init → scaffold new project
|
|
39
|
+
/ebm-auth → add auth system
|
|
40
|
+
/ebm-table → add data tables per feature
|
|
41
|
+
/ebm-form → add create/edit forms per feature
|
|
42
|
+
/ebm-upload → add file upload if needed
|
|
43
|
+
/ebm-thai → fix Thai language formality before review
|
|
44
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cp, mkdir, readFile, writeFile, access } from 'fs/promises'
|
|
3
|
+
import { join, dirname } from 'path'
|
|
4
|
+
import { homedir } from 'os'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const ALL_SKILLS = ['ebm-init', 'ebm-auth', 'ebm-table', 'ebm-form', 'ebm-upload', 'ebm-thai']
|
|
9
|
+
const packageSkillsDir = join(__dirname, '..', 'skills')
|
|
10
|
+
|
|
11
|
+
const onlyIndex = process.argv.indexOf('--only')
|
|
12
|
+
const selectedNames = onlyIndex !== -1
|
|
13
|
+
? process.argv[onlyIndex + 1].split(',').map(s => s.trim().replace(/^ebm-/, ''))
|
|
14
|
+
: null
|
|
15
|
+
const skills = selectedNames
|
|
16
|
+
? selectedNames.map(s => `ebm-${s}`).filter(s => ALL_SKILLS.includes(s))
|
|
17
|
+
: ALL_SKILLS
|
|
18
|
+
|
|
19
|
+
async function pathExists(p) {
|
|
20
|
+
try { await access(p); return true } catch { return false }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function convertToCursorMdc(skillDir) {
|
|
24
|
+
const skillMd = await readFile(join(skillDir, 'SKILL.md'), 'utf8')
|
|
25
|
+
const m = skillMd.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/m)
|
|
26
|
+
if (!m) return skillMd
|
|
27
|
+
const descMatch = m[1].match(/description:\s*(.+)/)
|
|
28
|
+
const description = descMatch ? descMatch[1].trim() : ''
|
|
29
|
+
return `---\ndescription: ${description}\nalwaysApply: false\n---\n${m[2]}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Read all stdin lines upfront (supports both interactive TTY and piped input)
|
|
33
|
+
async function readStdinLines() {
|
|
34
|
+
if (process.stdin.isTTY) return null // interactive — use per-question readline
|
|
35
|
+
return new Promise(resolve => {
|
|
36
|
+
const chunks = []
|
|
37
|
+
process.stdin.on('data', d => chunks.push(d))
|
|
38
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString().split(/\r?\n/).map(l => l.trim())))
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function makeAsker(lines) {
|
|
43
|
+
if (lines) {
|
|
44
|
+
// piped mode — pop lines from queue
|
|
45
|
+
let i = 0
|
|
46
|
+
return (q) => {
|
|
47
|
+
const ans = lines[i++] ?? ''
|
|
48
|
+
process.stdout.write(q + ans + '\n')
|
|
49
|
+
return Promise.resolve(ans)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// interactive mode — readline
|
|
53
|
+
const { createInterface } = await import('readline')
|
|
54
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
55
|
+
const ask = (q) => new Promise(resolve => rl.question(q, ans => resolve(ans.trim())))
|
|
56
|
+
ask.close = () => rl.close()
|
|
57
|
+
return ask
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
const lines = await readStdinLines()
|
|
62
|
+
const ask = await makeAsker(lines)
|
|
63
|
+
|
|
64
|
+
console.log('\nebm-skills installer\n')
|
|
65
|
+
console.log('Which platforms? (enter numbers separated by comma)\n')
|
|
66
|
+
console.log(' 1) Claude Code (~/.claude/skills/)')
|
|
67
|
+
console.log(' 2) Antigravity (~/.gemini/antigravity/skills/)')
|
|
68
|
+
console.log(' 3) Cursor (.cursor/rules/ or ~/.cursor/rules/)')
|
|
69
|
+
console.log()
|
|
70
|
+
|
|
71
|
+
const platformInput = await ask('Platforms [1,2,3]: ')
|
|
72
|
+
const selected = platformInput.split(',').map(s => s.trim()).filter(s => ['1','2','3'].includes(s))
|
|
73
|
+
|
|
74
|
+
if (selected.length === 0) {
|
|
75
|
+
ask.close?.()
|
|
76
|
+
console.error('No platform selected. Exiting.')
|
|
77
|
+
process.exit(1)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const targets = []
|
|
81
|
+
|
|
82
|
+
if (selected.includes('1')) {
|
|
83
|
+
const claudeDir = join(homedir(), '.claude')
|
|
84
|
+
if (!(await pathExists(claudeDir))) {
|
|
85
|
+
console.warn('⚠ ~/.claude not found — skipping Claude Code')
|
|
86
|
+
} else {
|
|
87
|
+
targets.push({ name: 'Claude Code', dest: join(claudeDir, 'skills'), type: 'claude' })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (selected.includes('2')) {
|
|
92
|
+
targets.push({
|
|
93
|
+
name: 'Antigravity',
|
|
94
|
+
dest: join(homedir(), '.gemini', 'antigravity', 'skills'),
|
|
95
|
+
type: 'antigravity'
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (selected.includes('3')) {
|
|
100
|
+
const scope = await ask('Cursor scope — (g)lobal ~/.cursor/rules/ or (w)orkspace .cursor/rules/? [g/w]: ')
|
|
101
|
+
const isWorkspace = scope.toLowerCase().startsWith('w')
|
|
102
|
+
const dest = isWorkspace
|
|
103
|
+
? join(process.cwd(), '.cursor', 'rules')
|
|
104
|
+
: join(homedir(), '.cursor', 'rules')
|
|
105
|
+
targets.push({ name: `Cursor (${isWorkspace ? 'workspace' : 'global'})`, dest, type: 'cursor' })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
ask.close?.()
|
|
109
|
+
|
|
110
|
+
if (targets.length === 0) {
|
|
111
|
+
console.error('No valid targets. Exiting.')
|
|
112
|
+
process.exit(1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log()
|
|
116
|
+
|
|
117
|
+
for (const target of targets) {
|
|
118
|
+
console.log(`Installing to ${target.name}...`)
|
|
119
|
+
const installed = []
|
|
120
|
+
const failed = []
|
|
121
|
+
|
|
122
|
+
for (const skill of skills) {
|
|
123
|
+
const src = join(packageSkillsDir, skill)
|
|
124
|
+
try {
|
|
125
|
+
if (target.type === 'cursor') {
|
|
126
|
+
await mkdir(target.dest, { recursive: true })
|
|
127
|
+
const mdc = await convertToCursorMdc(src)
|
|
128
|
+
await writeFile(join(target.dest, `${skill}.mdc`), mdc, 'utf8')
|
|
129
|
+
} else {
|
|
130
|
+
const dest = join(target.dest, skill)
|
|
131
|
+
await mkdir(dest, { recursive: true })
|
|
132
|
+
await cp(src, dest, { recursive: true })
|
|
133
|
+
}
|
|
134
|
+
console.log(` ✓ /${skill}`)
|
|
135
|
+
installed.push(skill)
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(` ✗ ${skill}: ${err.message}`)
|
|
138
|
+
failed.push(skill)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(` → ${installed.length} skill(s) installed to ${target.dest}\n`)
|
|
143
|
+
if (failed.length > 0) console.error(` Failed: ${failed.join(', ')}\n`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log('Done! Restart your editor to activate the skills.')
|
|
147
|
+
console.log(`\nAvailable commands: ${skills.map(s => `/${s}`).join(' ')}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
main().catch(err => { console.error(err); process.exit(1) })
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ebm-skills",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Claude Code skills for EBM Next.js projects — scaffold auth, tables, forms, uploads, and more",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ebm-skills": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"skills"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude-code",
|
|
15
|
+
"antigravity",
|
|
16
|
+
"cursor",
|
|
17
|
+
"nextjs",
|
|
18
|
+
"scaffold",
|
|
19
|
+
"ebm"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# ebm-auth Reference
|
|
2
|
+
|
|
3
|
+
## Config detection
|
|
4
|
+
|
|
5
|
+
Read `ebm.config.json` before generating anything:
|
|
6
|
+
```json
|
|
7
|
+
{
|
|
8
|
+
"projectType": "landing | backoffice | both",
|
|
9
|
+
"colorMode": "dark | light | system",
|
|
10
|
+
"primaryColor": "#3b82f6",
|
|
11
|
+
"uiLib": "shadcn | antd | mui | none"
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
If file missing → ask questions → save to `ebm.config.json`.
|
|
15
|
+
|
|
16
|
+
UI generation rules:
|
|
17
|
+
- **shadcn/ui**: use `<Button>`, `<Input>`, `<Card>` components
|
|
18
|
+
- **Ant Design**: use `<Form>`, `<Input>`, `<Button>`, `<Table>` components
|
|
19
|
+
- **MUI**: use `<TextField>`, `<Button>`, `<DataGrid>` components
|
|
20
|
+
- **none / fallback**: generate raw Tailwind (dark slate theme)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Mode A — Next.js App Router (Fullstack)
|
|
25
|
+
|
|
26
|
+
### Auth logic files
|
|
27
|
+
|
|
28
|
+
| File | Purpose |
|
|
29
|
+
|------|---------|
|
|
30
|
+
| `src/app/api/auth/[...nextauth]/route.ts` | NextAuth handler |
|
|
31
|
+
| `src/app/api/auth/[...nextauth]/auth-options.ts` | JWT config, providers, callbacks |
|
|
32
|
+
| `src/middleware.ts` | Route protection, first-login redirect |
|
|
33
|
+
| `src/lib/auth.ts` | hashPassword, comparePasswords, findUserByEmail |
|
|
34
|
+
| `src/lib/authorisation.ts` | CASL buildAbilityFor |
|
|
35
|
+
| `src/lib/permission.ts` | getDataPermission (DB query) |
|
|
36
|
+
| `src/lib/prisma.ts` | Prisma client singleton |
|
|
37
|
+
| `src/services/AuthProvider.tsx` | SessionProvider client wrapper |
|
|
38
|
+
| `src/services/AbilityProvider.tsx` | CASL ability context, syncs on session change |
|
|
39
|
+
| `src/store/useAbilityStore.ts` | Zustand store for abilities |
|
|
40
|
+
| `types/next-auth.d.ts` | Extended Session/JWT types |
|
|
41
|
+
|
|
42
|
+
### auth-options.ts key config
|
|
43
|
+
```typescript
|
|
44
|
+
// JWT strategy, 30-day session
|
|
45
|
+
// Credentials provider: email + password
|
|
46
|
+
// Brute force: 5 attempts → 1-minute block (in-memory Map)
|
|
47
|
+
// Session callback: embed id, email, name, phone_number,
|
|
48
|
+
// department_id, operation_hash[], reset_password, ad flags
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### types/next-auth.d.ts
|
|
52
|
+
```typescript
|
|
53
|
+
declare module 'next-auth' {
|
|
54
|
+
interface Session {
|
|
55
|
+
user: {
|
|
56
|
+
id: number; email: string; name: string
|
|
57
|
+
phone_number: string; department_id: number
|
|
58
|
+
operation_hash: string[]; reset_password: boolean; ad: boolean
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### middleware.ts
|
|
65
|
+
```typescript
|
|
66
|
+
// withAuth — public routes: /signin, /forgot-password, /reset-password
|
|
67
|
+
// reset_password === false → redirect to /change-password-first
|
|
68
|
+
// no token → redirect to /signin
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## UI Pages
|
|
74
|
+
|
|
75
|
+
### Route mapping by project type
|
|
76
|
+
|
|
77
|
+
| Page | backoffice route | landing route |
|
|
78
|
+
|------|-----------------|---------------|
|
|
79
|
+
| Sign in | `/signin` | `/signin` |
|
|
80
|
+
| Forgot password | `/forgot-password` | `/forgot-password` |
|
|
81
|
+
| Reset password | `/reset-password` | `/reset-password` |
|
|
82
|
+
| First login | `/change-password-first` | `/change-password-first` |
|
|
83
|
+
| User management | `/dashboard/users` | `/users` |
|
|
84
|
+
| User profile (own) | `/dashboard/profile` | `/profile` |
|
|
85
|
+
| User detail (admin) | `/dashboard/users/[id]` | `/users/[id]` |
|
|
86
|
+
|
|
87
|
+
### Sign-in page
|
|
88
|
+
- Email + password form
|
|
89
|
+
- Error states: invalid credentials, TOO_MANY_ATTEMPTS
|
|
90
|
+
- Link to forgot password
|
|
91
|
+
- Calls `signIn('credentials', { email, password, redirect: false })`
|
|
92
|
+
|
|
93
|
+
### Forgot password page
|
|
94
|
+
- Email input form
|
|
95
|
+
- Calls `POST /api/auth/resetpassword/request`
|
|
96
|
+
- Show success message after submit
|
|
97
|
+
|
|
98
|
+
### Reset password page
|
|
99
|
+
- New password + confirm password fields
|
|
100
|
+
- Reads `token` from URL query params
|
|
101
|
+
- Calls `POST /api/auth/resetpassword/confirm`
|
|
102
|
+
|
|
103
|
+
### First login page (`/change-password-first`)
|
|
104
|
+
- New password + confirm password
|
|
105
|
+
- Calls `PUT /api/change-password-first`
|
|
106
|
+
- Updates session `reset_password: true` on success
|
|
107
|
+
- Redirects to `/dashboard` or `/` based on project type
|
|
108
|
+
|
|
109
|
+
### User management (`/dashboard/users` or `/users`)
|
|
110
|
+
|
|
111
|
+
**List page:**
|
|
112
|
+
- Table: name, email, role, department, status
|
|
113
|
+
- Search/filter by name or email
|
|
114
|
+
- Actions: edit, delete, reset password
|
|
115
|
+
- "Add user" button → create modal/page
|
|
116
|
+
|
|
117
|
+
**Create/Edit page (`/users/new`, `/users/[id]/edit`):**
|
|
118
|
+
- Fields: name, email, password (create only), phone_number
|
|
119
|
+
- Role selector (dropdown from DB)
|
|
120
|
+
- Department selector (dropdown from DB)
|
|
121
|
+
- reset_password toggle
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// API routes needed
|
|
125
|
+
// GET /api/users → list users with role + department
|
|
126
|
+
// POST /api/users → create user
|
|
127
|
+
// GET /api/users/[id] → get single user
|
|
128
|
+
// PUT /api/users/[id] → update user
|
|
129
|
+
// DELETE /api/users/[id] → delete user
|
|
130
|
+
// PUT /api/users/[id]/reset-password → force password reset
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### User profile — own (`/dashboard/profile` or `/profile`)
|
|
134
|
+
- Display: name, email, phone_number, role, department
|
|
135
|
+
- Edit: name, phone_number
|
|
136
|
+
- Change password section: current → new → confirm
|
|
137
|
+
- Calls `PUT /api/auth/passwordsetting`
|
|
138
|
+
|
|
139
|
+
### User detail — admin (`/dashboard/users/[id]` or `/users/[id]`)
|
|
140
|
+
- Display all user fields: name, email, phone, role, department
|
|
141
|
+
- Permissions list: all operation_hash entries as badges
|
|
142
|
+
- Edit button → goes to edit page
|
|
143
|
+
- Reset password button → calls reset-password API
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Seed file (always generate)
|
|
148
|
+
|
|
149
|
+
`prisma/seed.ts` with `tsx` runner:
|
|
150
|
+
```typescript
|
|
151
|
+
// Operations: USER/ROLE/DATA/REPORT/SETTING/DEPARTMENT/FILE × VIEW/ADD/EDIT/DELETE/DOWNLOAD/UPLOAD/OPERATE/RESET_PASSWORD
|
|
152
|
+
// Role: primaryAdmin → all operations
|
|
153
|
+
// Department: Administration
|
|
154
|
+
// User: admin@example.com / Admin@1234, reset_password: true
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Add to `package.json`:
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"scripts": { "seed": "tsx prisma/seed.ts" },
|
|
161
|
+
"prisma": { "seed": "tsx prisma/seed.ts" }
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
devDependencies: `tsx` (NOT ts-node — breaks on Windows)
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Optional: Azure AD
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import AzureADProvider from 'next-auth/providers/azure-ad'
|
|
172
|
+
// First AD login: create user in DB with default role/department
|
|
173
|
+
// Set ad: true flag in session
|
|
174
|
+
```
|
|
175
|
+
Env vars: `AZURE_AD_CLIENT_ID`, `AZURE_AD_CLIENT_SECRET`, `AZURE_AD_TENANT_ID`
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Optional: Email Password Reset
|
|
180
|
+
|
|
181
|
+
Files:
|
|
182
|
+
- `src/app/api/auth/resetpassword/request/route.ts` — 32-byte token, 15min expiry, send email
|
|
183
|
+
- `src/app/api/auth/resetpassword/confirm/route.ts` — validate token, hash password, delete token
|
|
184
|
+
|
|
185
|
+
Env vars: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Mode B — Next.js (FE) + FastAPI (BE)
|
|
190
|
+
|
|
191
|
+
### FastAPI files
|
|
192
|
+
|
|
193
|
+
| File | Purpose |
|
|
194
|
+
|------|---------|
|
|
195
|
+
| `auth/router.py` | `/auth/login`, `/auth/logout`, `/auth/refresh` |
|
|
196
|
+
| `auth/dependencies.py` | `get_current_user` dependency |
|
|
197
|
+
| `auth/utils.py` | hash_password, verify_password, create_access_token |
|
|
198
|
+
| `auth/models.py` | User, Role, Permission, Department models |
|
|
199
|
+
| `auth/permissions.py` | casbin enforcer, buildAbilityFor |
|
|
200
|
+
| `auth/schemas.py` | LoginRequest, TokenResponse, UserOut |
|
|
201
|
+
|
|
202
|
+
### Next.js files (Mode B)
|
|
203
|
+
|
|
204
|
+
| File | Purpose |
|
|
205
|
+
|------|---------|
|
|
206
|
+
| `src/app/signin/page.tsx` + `sign-in-form.tsx` | Sign-in UI |
|
|
207
|
+
| `src/lib/auth-client.ts` | API calls to FastAPI, httpOnly cookie storage |
|
|
208
|
+
| `src/middleware.ts` | Redirect to /signin if no token |
|
|
209
|
+
|
|
210
|
+
UI pages for Mode B follow the same route mapping above but call FastAPI endpoints instead of NextAuth.
|
|
211
|
+
|
|
212
|
+
### auth/utils.py
|
|
213
|
+
```python
|
|
214
|
+
from passlib.context import CryptContext
|
|
215
|
+
from jose import jwt
|
|
216
|
+
from datetime import datetime, timedelta
|
|
217
|
+
|
|
218
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
219
|
+
|
|
220
|
+
def hash_password(password: str) -> str:
|
|
221
|
+
return pwd_context.hash(password)
|
|
222
|
+
|
|
223
|
+
def verify_password(plain: str, hashed: str) -> bool:
|
|
224
|
+
return pwd_context.verify(plain, hashed)
|
|
225
|
+
|
|
226
|
+
def create_access_token(data: dict, expires_delta=None) -> str:
|
|
227
|
+
to_encode = data.copy()
|
|
228
|
+
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=30))
|
|
229
|
+
to_encode.update({"exp": expire})
|
|
230
|
+
return jwt.encode(to_encode, SECRET_KEY, algorithm="HS256")
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## RBAC Reference
|
|
236
|
+
|
|
237
|
+
### Permission format: `SUBJECT__ACTION`
|
|
238
|
+
Subjects: `USER ROLE DATA REPORT SETTING DEPARTMENT FILE`
|
|
239
|
+
Actions: `VIEW ADD EDIT DELETE DOWNLOAD UPLOAD OPERATE RESET_PASSWORD`
|
|
240
|
+
|
|
241
|
+
### DB Schema
|
|
242
|
+
```sql
|
|
243
|
+
users (id, email, password_hash, name, phone_number, department_id, role_id, reset_password, ad, created_at)
|
|
244
|
+
roles (id, name)
|
|
245
|
+
operations (id, name) -- e.g. "DATA__VIEW"
|
|
246
|
+
role_operations (role_id, operation_id)
|
|
247
|
+
departments (id, name)
|
|
248
|
+
department_operations (department_id, operation_id)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## .env.example additions
|
|
254
|
+
|
|
255
|
+
### Mode A
|
|
256
|
+
```env
|
|
257
|
+
NEXTAUTH_SECRET=your-secret-here
|
|
258
|
+
NEXTAUTH_URL=http://localhost:3000
|
|
259
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
|
260
|
+
# Azure AD (optional)
|
|
261
|
+
AZURE_AD_CLIENT_ID=
|
|
262
|
+
AZURE_AD_CLIENT_SECRET=
|
|
263
|
+
AZURE_AD_TENANT_ID=
|
|
264
|
+
# Email Reset (optional)
|
|
265
|
+
SMTP_HOST=
|
|
266
|
+
SMTP_PORT=587
|
|
267
|
+
SMTP_USER=
|
|
268
|
+
SMTP_PASS=
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Mode B
|
|
272
|
+
```env
|
|
273
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
|
274
|
+
SECRET_KEY=your-secret-here
|
|
275
|
+
ACCESS_TOKEN_EXPIRE_MINUTES=43200
|
|
276
|
+
NEXT_PUBLIC_API_URL=http://localhost:8000
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Post-generation summary template
|
|
282
|
+
```
|
|
283
|
+
✓ Generated [N] auth logic files
|
|
284
|
+
✓ Generated [N] UI pages
|
|
285
|
+
⚠ [N] conflicts → written as *.auth.* (merge manually)
|
|
286
|
+
📋 Env vars appended to .env.example
|
|
287
|
+
|
|
288
|
+
Default credentials:
|
|
289
|
+
Email: admin@example.com
|
|
290
|
+
Password: Admin@1234
|
|
291
|
+
|
|
292
|
+
Next steps:
|
|
293
|
+
1. npm install next-auth bcryptjs @casl/ability zustand
|
|
294
|
+
2. npm install -D @types/bcryptjs tsx
|
|
295
|
+
3. Copy .env.example → .env.local and fill in values
|
|
296
|
+
4. npx prisma migrate dev --name init
|
|
297
|
+
5. npx prisma db seed
|
|
298
|
+
6. npm run dev
|
|
299
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ebm-auth
|
|
3
|
+
description: Generate a full authentication system for a Next.js project — JWT sessions, RBAC with CASL, brute-force protection, and complete UI pages (signin, forgot-password, reset-password, first-login, user management, user profile). Reads project config from ebm.config.json. Supports Next.js fullstack (NextAuth) and Next.js + FastAPI. Use when user invokes /ebm-auth or asks to add authentication to an existing project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /ebm-auth
|
|
7
|
+
|
|
8
|
+
### Step 1 — Read config
|
|
9
|
+
```
|
|
10
|
+
ebm.config.json exists? → use projectType, uiLib, primaryColor
|
|
11
|
+
else → ask questions → save to ebm.config.json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Step 2 — Detect stack
|
|
15
|
+
```
|
|
16
|
+
Has next-auth → Mode A (Next.js fullstack)
|
|
17
|
+
Has FastAPI indicators → Mode B (Next.js + FastAPI)
|
|
18
|
+
Unclear → ask
|
|
19
|
+
ORM: @prisma/client → Prisma | drizzle-orm → Drizzle | sqlalchemy → SQLAlchemy | else ask
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Step 3 — Ask optional modules
|
|
23
|
+
- Azure AD SSO?
|
|
24
|
+
- Email-based password reset?
|
|
25
|
+
|
|
26
|
+
### Step 4 — Generate auth logic + UI pages
|
|
27
|
+
See [REFERENCE.md](REFERENCE.md) for all files, templates, routes.
|
|
28
|
+
|
|
29
|
+
### Step 5 — Post-generation rules
|
|
30
|
+
- Append to `.env.example` (create if missing)
|
|
31
|
+
- `tsconfig.json`: ensure `"@/*": ["./src/*"]` paths exist
|
|
32
|
+
- Conflict: file exists → write `filename.auth.ext` + merge comment
|
|
33
|
+
- Seed: always `tsx prisma/seed.ts` (Windows-safe, never ts-node)
|
|
34
|
+
- Print summary + next steps
|
|
35
|
+
|
|
36
|
+
## Shared rules
|
|
37
|
+
- Path alias: `@/*` → `./src/*` always
|
|
38
|
+
- Thai UI text: use formal Thai — see `/ebm-thai` glossary
|