forgedev 1.4.0 → 1.4.1
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/package.json +1 -1
- package/src/chainproof-bridge.js +9 -2
- package/src/ci-mode.js +3 -4
- package/src/composer.js +228 -242
- package/src/doctor-checks-chainproof.js +14 -11
- package/src/doctor-checks.js +3 -19
- package/src/index.js +6 -34
- package/src/recommender.js +319 -319
- package/src/uat-generator.js +105 -93
- package/src/utils.js +245 -214
- package/templates/auth/jwt-custom/backend/app/api/auth.py.template +39 -45
- package/templates/auth/jwt-custom/backend/app/core/security.py.template +44 -37
- package/templates/backend/express/package.json.template +35 -33
- package/templates/backend/express/src/lib/prisma.ts.template +32 -0
- package/templates/backend/express/src/routes/health.ts.template +35 -27
- package/templates/backend/fastapi/backend/app/main.py.template +67 -60
- package/templates/backend/fastapi/backend/requirements.txt.template +18 -16
- package/templates/backend/hono/package.json.template +33 -31
- package/templates/backend/hono/src/lib/prisma.ts.template +32 -0
- package/templates/backend/hono/src/routes/health.ts.template +38 -27
- package/templates/base/.gitignore.template +32 -32
- package/templates/claude-code/commands/workflows.md +52 -52
- package/templates/database/prisma-postgres/prisma/schema.prisma.template +19 -18
- package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +39 -40
- package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +38 -36
- package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +50 -48
- package/templates/frontend/nextjs/package.json.template +45 -43
- package/templates/frontend/remix/package.json.template +41 -39
- package/templates/infra/docker/.dockerignore.template +16 -0
- package/templates/infra/docker-compose/docker-compose.yml.template +22 -19
- package/templates/infra/github-actions/.github/workflows/ci.yml.template +61 -52
- package/templates/infra/k8s/k8s/deployment.yml.template +70 -70
- package/templates/infra/k8s/k8s/hpa.yml.template +24 -24
- package/templates/infra/k8s/k8s/ingress.yml.template +26 -26
- package/templates/infra/k8s/k8s/kustomization.yml.template +13 -13
- package/templates/infra/k8s/k8s/namespace.yml.template +4 -4
- package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -41
- package/templates/infra/k8s/k8s/secrets.yml.template +10 -10
- package/templates/infra/k8s/k8s/service.yml.template +15 -15
- package/templates/testing/load/k6/README.md.template +48 -48
- package/templates/testing/load/k6/load-test.js.template +57 -57
package/src/utils.js
CHANGED
|
@@ -1,214 +1,245 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
|
|
9
|
-
export const ROOT_DIR = path.resolve(__dirname, '..');
|
|
10
|
-
|
|
11
|
-
export const log = {
|
|
12
|
-
info: (msg) => console.log(chalk.cyan(msg)),
|
|
13
|
-
success: (msg) => console.log(chalk.green(msg)),
|
|
14
|
-
warn: (msg) => console.error(chalk.yellow(msg)),
|
|
15
|
-
error: (msg) => console.error(chalk.red(msg)),
|
|
16
|
-
step: (n, total, msg) => console.log(chalk.blue(`[${n}/${total}] ${msg}`)),
|
|
17
|
-
dim: (msg) => console.log(chalk.dim(msg)),
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function ensureDir(dirPath) {
|
|
21
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function writeFile(dest, content) {
|
|
25
|
-
ensureDir(path.dirname(dest));
|
|
26
|
-
fs.writeFileSync(dest, content, 'utf-8');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function readTemplate(templatePath) {
|
|
30
|
-
return fs.readFileSync(templatePath, 'utf-8');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function replaceVars(content, vars) {
|
|
34
|
-
return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
35
|
-
return key in vars ? vars[key] : match;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function toPascalCase(str) {
|
|
40
|
-
return str
|
|
41
|
-
.split(/[-_\s]+/)
|
|
42
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
43
|
-
.join('');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function copyEnvCmd() {
|
|
47
|
-
return process.platform === 'win32'
|
|
48
|
-
? 'copy .env.example .env'
|
|
49
|
-
: 'cp .env.example .env';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function toSnakeCase(str) {
|
|
53
|
-
return str.replace(/[-\s]+/g, '_').toLowerCase();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function toKebabCase(str) {
|
|
57
|
-
return str.replace(/[\s_]+/g, '-').toLowerCase();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ───
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
cd
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
npx
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
npx
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export const ROOT_DIR = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
export const log = {
|
|
12
|
+
info: (msg) => console.log(chalk.cyan(msg)),
|
|
13
|
+
success: (msg) => console.log(chalk.green(msg)),
|
|
14
|
+
warn: (msg) => console.error(chalk.yellow(msg)),
|
|
15
|
+
error: (msg) => console.error(chalk.red(msg)),
|
|
16
|
+
step: (n, total, msg) => console.log(chalk.blue(`[${n}/${total}] ${msg}`)),
|
|
17
|
+
dim: (msg) => console.log(chalk.dim(msg)),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function ensureDir(dirPath) {
|
|
21
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writeFile(dest, content) {
|
|
25
|
+
ensureDir(path.dirname(dest));
|
|
26
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readTemplate(templatePath) {
|
|
30
|
+
return fs.readFileSync(templatePath, 'utf-8');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function replaceVars(content, vars) {
|
|
34
|
+
return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
35
|
+
return key in vars ? vars[key] : match;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function toPascalCase(str) {
|
|
40
|
+
return str
|
|
41
|
+
.split(/[-_\s]+/)
|
|
42
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
43
|
+
.join('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function copyEnvCmd() {
|
|
47
|
+
return process.platform === 'win32'
|
|
48
|
+
? 'copy .env.example .env'
|
|
49
|
+
: 'cp .env.example .env';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function toSnakeCase(str) {
|
|
53
|
+
return str.replace(/[-\s]+/g, '_').toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function toKebabCase(str) {
|
|
57
|
+
return str.replace(/[\s_]+/g, '-').toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Shared directory walker ─────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export const DEFAULT_SKIP_DIRS = ['node_modules', '.next', '__pycache__', '.git', 'venv', '.venv', 'dist', 'build'];
|
|
63
|
+
|
|
64
|
+
export function walkDir(dir, { ext, skipDirs = [], maxDepth = 20 } = {}, _depth = 0) {
|
|
65
|
+
if (_depth > maxDepth) return [];
|
|
66
|
+
const results = [];
|
|
67
|
+
let entries;
|
|
68
|
+
try {
|
|
69
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (err?.code !== 'ENOENT') {
|
|
72
|
+
log.warn(`walkDir: skipping ${dir} (${err?.code || err?.message})`);
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.isSymbolicLink()) continue;
|
|
78
|
+
const fullPath = path.join(dir, entry.name);
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
if (skipDirs.includes(entry.name)) continue;
|
|
81
|
+
results.push(...walkDir(fullPath, { ext, skipDirs, maxDepth }, _depth + 1));
|
|
82
|
+
} else {
|
|
83
|
+
if (!ext || entry.name.endsWith(ext)) {
|
|
84
|
+
results.push(fullPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Single source of truth for all per-stack metadata ──────────────
|
|
92
|
+
// Adding a new stack? Add one entry here. Nothing else to touch in utils.js.
|
|
93
|
+
|
|
94
|
+
const STACK_METADATA = {
|
|
95
|
+
'nextjs-fullstack': {
|
|
96
|
+
description: 'Next.js full-stack application with TypeScript, Tailwind CSS, Prisma, and PostgreSQL',
|
|
97
|
+
extraIgnores: '',
|
|
98
|
+
port: '3000',
|
|
99
|
+
commands: {
|
|
100
|
+
LINT_COMMAND: 'npx eslint .',
|
|
101
|
+
TYPE_CHECK_COMMAND: 'npx tsc --noEmit',
|
|
102
|
+
TEST_COMMAND: 'npx vitest run',
|
|
103
|
+
BUILD_COMMAND: 'npm run build',
|
|
104
|
+
DEV_COMMAND: 'npm run dev',
|
|
105
|
+
},
|
|
106
|
+
setupCommands: () => `npm install
|
|
107
|
+
${copyEnvCmd()}
|
|
108
|
+
npx prisma db push
|
|
109
|
+
npm run dev`,
|
|
110
|
+
availableScripts: `- \`npm run dev\`: Start development server
|
|
111
|
+
- \`npm run build\`: Production build
|
|
112
|
+
- \`npm run lint\`: Run ESLint
|
|
113
|
+
- \`npx prisma studio\`: Database GUI
|
|
114
|
+
- \`npx vitest\`: Run unit tests
|
|
115
|
+
- \`npx playwright test\`: Run E2E tests`,
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
'fastapi-backend': {
|
|
119
|
+
description: 'FastAPI backend service with SQLAlchemy, PostgreSQL, and Alembic',
|
|
120
|
+
extraIgnores: '\n# Python\n__pycache__/\n*.pyc\nvenv/\n.venv/\n*.egg-info/',
|
|
121
|
+
port: '8000',
|
|
122
|
+
commands: {
|
|
123
|
+
LINT_COMMAND: 'ruff check .',
|
|
124
|
+
TYPE_CHECK_COMMAND: 'pyright',
|
|
125
|
+
TEST_COMMAND: 'pytest',
|
|
126
|
+
BUILD_COMMAND: 'docker build -t app .',
|
|
127
|
+
DEV_COMMAND: 'uvicorn app.main:app --reload',
|
|
128
|
+
},
|
|
129
|
+
setupCommands: () => `cd backend
|
|
130
|
+
python -m venv venv
|
|
131
|
+
source venv/bin/activate # Windows: venv\\Scripts\\activate
|
|
132
|
+
pip install -r requirements.txt
|
|
133
|
+
${copyEnvCmd()}
|
|
134
|
+
uvicorn app.main:app --reload`,
|
|
135
|
+
availableScripts: `- \`uvicorn app.main:app --reload\`: Start dev server
|
|
136
|
+
- \`pytest\`: Run tests
|
|
137
|
+
- \`ruff check .\`: Run linter
|
|
138
|
+
- \`alembic upgrade head\`: Run migrations`,
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
'polyglot-fullstack': {
|
|
142
|
+
description: 'Full-stack application with Next.js frontend and FastAPI backend',
|
|
143
|
+
extraIgnores: '\n# Python\n__pycache__/\n*.pyc\nvenv/\n.venv/\n*.egg-info/',
|
|
144
|
+
port: '3000',
|
|
145
|
+
commands: {
|
|
146
|
+
LINT_COMMAND: 'cd frontend && npx eslint . && cd ../backend && ruff check .',
|
|
147
|
+
TYPE_CHECK_COMMAND: 'cd frontend && npx tsc --noEmit',
|
|
148
|
+
TEST_COMMAND: 'cd frontend && npx vitest run && cd ../backend && pytest',
|
|
149
|
+
BUILD_COMMAND: 'docker compose build',
|
|
150
|
+
DEV_COMMAND: 'docker compose up',
|
|
151
|
+
},
|
|
152
|
+
setupCommands: () => `docker compose up -d postgres
|
|
153
|
+
# Frontend
|
|
154
|
+
cd frontend && npm install && npm run dev
|
|
155
|
+
# Backend
|
|
156
|
+
cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload`,
|
|
157
|
+
availableScripts: `- \`docker compose up\`: Start all services
|
|
158
|
+
- \`docker compose up -d postgres\`: Start database only
|
|
159
|
+
- Frontend: \`cd frontend && npm run dev\`
|
|
160
|
+
- Backend: \`cd backend && uvicorn app.main:app --reload\``,
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
'react-express': {
|
|
164
|
+
description: 'Full-stack application with React (Vite) frontend and Express backend',
|
|
165
|
+
extraIgnores: '\ndist/',
|
|
166
|
+
port: '3001', // Express backend port (frontend runs on Vite :5173)
|
|
167
|
+
commands: {
|
|
168
|
+
LINT_COMMAND: 'cd frontend && npx eslint . && cd ../backend && npx eslint .',
|
|
169
|
+
TYPE_CHECK_COMMAND: 'cd frontend && npx tsc --noEmit && cd ../backend && npx tsc --noEmit',
|
|
170
|
+
TEST_COMMAND: 'cd frontend && npx vitest run && cd ../backend && npx vitest run',
|
|
171
|
+
BUILD_COMMAND: 'cd frontend && npm run build && cd ../backend && npm run build',
|
|
172
|
+
DEV_COMMAND: 'npx concurrently "cd frontend && npm run dev" "cd backend && npm run dev"',
|
|
173
|
+
},
|
|
174
|
+
setupCommands: () => `# Frontend
|
|
175
|
+
cd frontend && npm install && npm run dev
|
|
176
|
+
# Backend (in a separate terminal)
|
|
177
|
+
cd backend && npm install && ${copyEnvCmd()} && npx prisma db push && npm run dev`,
|
|
178
|
+
availableScripts: `- Frontend: \`cd frontend && npm run dev\` (Vite dev server)
|
|
179
|
+
- Backend: \`cd backend && npm run dev\` (Express with tsx watch)
|
|
180
|
+
- \`cd backend && npm run build\`: Build backend for production
|
|
181
|
+
- \`cd frontend && npm run build\`: Build frontend for production
|
|
182
|
+
- \`cd backend && npx prisma studio\`: Database GUI
|
|
183
|
+
- \`cd frontend && npx vitest\`: Run frontend tests`,
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
'remix-fullstack': {
|
|
187
|
+
description: 'Full-stack Remix application with Vite, Tailwind CSS, and PostgreSQL',
|
|
188
|
+
extraIgnores: '\nbuild/',
|
|
189
|
+
port: '3000',
|
|
190
|
+
commands: {
|
|
191
|
+
LINT_COMMAND: 'npx eslint .',
|
|
192
|
+
TYPE_CHECK_COMMAND: 'npx tsc --noEmit',
|
|
193
|
+
TEST_COMMAND: 'npx vitest run',
|
|
194
|
+
BUILD_COMMAND: 'npm run build',
|
|
195
|
+
DEV_COMMAND: 'npm run dev',
|
|
196
|
+
},
|
|
197
|
+
setupCommands: () => `npm install
|
|
198
|
+
${copyEnvCmd()}
|
|
199
|
+
npx prisma db push
|
|
200
|
+
npm run dev`,
|
|
201
|
+
availableScripts: `- \`npm run dev\`: Start Remix dev server
|
|
202
|
+
- \`npm run build\`: Production build
|
|
203
|
+
- \`npm run start\`: Start production server
|
|
204
|
+
- \`npm run lint\`: Run ESLint
|
|
205
|
+
- \`npx prisma studio\`: Database GUI
|
|
206
|
+
- \`npx vitest\`: Run unit tests`,
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
'hono-api': {
|
|
210
|
+
description: 'Hono API service with TypeScript, Prisma, and PostgreSQL',
|
|
211
|
+
extraIgnores: '\ndist/',
|
|
212
|
+
port: '3000',
|
|
213
|
+
commands: {
|
|
214
|
+
LINT_COMMAND: 'npx eslint .',
|
|
215
|
+
TYPE_CHECK_COMMAND: 'npx tsc --noEmit',
|
|
216
|
+
TEST_COMMAND: 'npx vitest run',
|
|
217
|
+
BUILD_COMMAND: 'npm run build',
|
|
218
|
+
DEV_COMMAND: 'npm run dev',
|
|
219
|
+
},
|
|
220
|
+
setupCommands: () => `npm install
|
|
221
|
+
${copyEnvCmd()}
|
|
222
|
+
npx prisma db push
|
|
223
|
+
npm run dev`,
|
|
224
|
+
availableScripts: `- \`npm run dev\`: Start Hono dev server (tsx watch)
|
|
225
|
+
- \`npm run build\`: Compile TypeScript
|
|
226
|
+
- \`npm run start\`: Start production server
|
|
227
|
+
- \`npm run lint\`: Run ESLint
|
|
228
|
+
- \`npx prisma studio\`: Database GUI
|
|
229
|
+
- \`npx vitest\`: Run unit tests`,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export function getStackMetadata(stackId) {
|
|
234
|
+
const meta = STACK_METADATA[stackId];
|
|
235
|
+
if (!meta) {
|
|
236
|
+
log.warn(`No stack metadata for "${stackId}" — using empty defaults`);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return meta;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function getStackCommands(stackId) {
|
|
243
|
+
const meta = STACK_METADATA[stackId];
|
|
244
|
+
return meta ? { ...meta.commands } : {};
|
|
245
|
+
}
|
|
@@ -1,45 +1,39 @@
|
|
|
1
|
-
from fastapi import APIRouter, HTTPException
|
|
2
|
-
from pydantic import BaseModel, EmailStr
|
|
3
|
-
|
|
4
|
-
from app.core.security import create_access_token, hash_password, verify_password
|
|
5
|
-
|
|
6
|
-
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class LoginRequest(BaseModel):
|
|
10
|
-
email: EmailStr
|
|
11
|
-
password: str
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class RegisterRequest(BaseModel):
|
|
15
|
-
email: EmailStr
|
|
16
|
-
password: str
|
|
17
|
-
name: str
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class TokenResponse(BaseModel):
|
|
21
|
-
access_token: str
|
|
22
|
-
token_type: str = "bearer"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@router.post("/login"
|
|
26
|
-
async def login(request: LoginRequest):
|
|
27
|
-
# TODO: Replace with actual database lookup
|
|
28
|
-
# user = await get_user_by_email(request.email)
|
|
29
|
-
# if not user or not verify_password(request.password, user.hashed_password):
|
|
30
|
-
# raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
31
|
-
raise HTTPException(status_code=501, detail="Implement user lookup")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# hashed = hash_password(request.password)
|
|
41
|
-
# user = await create_user(email=request.email, name=request.name, hashed_password=hashed)
|
|
42
|
-
raise HTTPException(status_code=501, detail="Implement user creation")
|
|
43
|
-
|
|
44
|
-
token = create_access_token(data={"sub": request.email})
|
|
45
|
-
return TokenResponse(access_token=token)
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel, EmailStr
|
|
3
|
+
|
|
4
|
+
from app.core.security import create_access_token, hash_password, verify_password # noqa: F401 — used when implementing TODOs below
|
|
5
|
+
|
|
6
|
+
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LoginRequest(BaseModel):
|
|
10
|
+
email: EmailStr
|
|
11
|
+
password: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RegisterRequest(BaseModel):
|
|
15
|
+
email: EmailStr
|
|
16
|
+
password: str
|
|
17
|
+
name: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TokenResponse(BaseModel):
|
|
21
|
+
access_token: str
|
|
22
|
+
token_type: str = "bearer"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.post("/login")
|
|
26
|
+
async def login(request: LoginRequest) -> TokenResponse:
|
|
27
|
+
# TODO: Replace with actual database lookup
|
|
28
|
+
# user = await get_user_by_email(request.email)
|
|
29
|
+
# if not user or not verify_password(request.password, user.hashed_password):
|
|
30
|
+
# raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
31
|
+
raise HTTPException(status_code=501, detail="Implement user lookup")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.post("/register")
|
|
35
|
+
async def register(request: RegisterRequest) -> TokenResponse:
|
|
36
|
+
# TODO: Replace with actual database operations
|
|
37
|
+
# hashed = hash_password(request.password)
|
|
38
|
+
# user = await create_user(email=request.email, name=request.name, hashed_password=hashed)
|
|
39
|
+
raise HTTPException(status_code=501, detail="Implement user creation")
|
|
@@ -1,37 +1,44 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from datetime import datetime, timedelta, timezone
|
|
3
|
-
|
|
4
|
-
from jose import JWTError, jwt
|
|
5
|
-
from passlib.context import CryptContext
|
|
6
|
-
|
|
7
|
-
from app.core.config import settings
|
|
8
|
-
|
|
9
|
-
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
|
|
4
|
+
from jose import JWTError, jwt
|
|
5
|
+
from passlib.context import CryptContext
|
|
6
|
+
|
|
7
|
+
from app.core.config import settings
|
|
8
|
+
|
|
9
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
10
|
+
|
|
11
|
+
ALGORITHM = "HS256"
|
|
12
|
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_secret_key() -> str:
|
|
16
|
+
key = os.environ.get("JWT_SECRET_KEY", "")
|
|
17
|
+
if not key or len(key) < 32:
|
|
18
|
+
raise RuntimeError(
|
|
19
|
+
"JWT_SECRET_KEY must be set and at least 32 characters. "
|
|
20
|
+
'Generate one with: python -c "import secrets; print(secrets.token_urlsafe(32))"'
|
|
21
|
+
)
|
|
22
|
+
return key
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
26
|
+
return pwd_context.verify(plain_password, hashed_password)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def hash_password(password: str) -> str:
|
|
30
|
+
return pwd_context.hash(password)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
|
|
34
|
+
to_encode = data.copy()
|
|
35
|
+
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
|
|
36
|
+
to_encode.update({"exp": expire})
|
|
37
|
+
return jwt.encode(to_encode, _get_secret_key(), algorithm=ALGORITHM)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def decode_access_token(token: str) -> dict | None:
|
|
41
|
+
try:
|
|
42
|
+
return jwt.decode(token, _get_secret_key(), algorithms=[ALGORITHM])
|
|
43
|
+
except JWTError:
|
|
44
|
+
return None
|