forgedev 1.3.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.
Files changed (46) hide show
  1. package/bin/chainproof.js +1 -1
  2. package/bin/devforge.js +1 -1
  3. package/package.json +1 -1
  4. package/src/chainproof-bridge.js +9 -2
  5. package/src/ci-mode.js +3 -4
  6. package/src/claude-configurator.js +114 -58
  7. package/src/composer.js +17 -128
  8. package/src/doctor-checks-chainproof.js +14 -11
  9. package/src/doctor-checks.js +3 -19
  10. package/src/index.js +6 -34
  11. package/src/init-mode.js +14 -3
  12. package/src/recommender.js +319 -310
  13. package/src/uat-generator.js +105 -93
  14. package/src/update.js +56 -12
  15. package/src/utils.js +245 -116
  16. package/templates/auth/jwt-custom/backend/app/api/auth.py.template +39 -45
  17. package/templates/auth/jwt-custom/backend/app/core/security.py.template +44 -37
  18. package/templates/backend/express/package.json.template +35 -33
  19. package/templates/backend/express/src/lib/prisma.ts.template +32 -0
  20. package/templates/backend/express/src/routes/health.ts.template +35 -27
  21. package/templates/backend/fastapi/backend/app/main.py.template +67 -60
  22. package/templates/backend/fastapi/backend/requirements.txt.template +18 -16
  23. package/templates/backend/hono/package.json.template +33 -31
  24. package/templates/backend/hono/src/lib/prisma.ts.template +32 -0
  25. package/templates/backend/hono/src/routes/health.ts.template +38 -27
  26. package/templates/base/.gitignore.template +32 -29
  27. package/templates/claude-code/commands/workflows.md +52 -52
  28. package/templates/database/prisma-postgres/prisma/schema.prisma.template +19 -18
  29. package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +39 -40
  30. package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +38 -36
  31. package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +50 -48
  32. package/templates/frontend/nextjs/package.json.template +45 -43
  33. package/templates/frontend/remix/package.json.template +41 -39
  34. package/templates/infra/docker/.dockerignore.template +16 -0
  35. package/templates/infra/docker-compose/docker-compose.yml.template +22 -19
  36. package/templates/infra/github-actions/.github/workflows/ci.yml.template +61 -52
  37. package/templates/infra/k8s/k8s/deployment.yml.template +70 -0
  38. package/templates/infra/k8s/k8s/hpa.yml.template +24 -0
  39. package/templates/infra/k8s/k8s/ingress.yml.template +26 -0
  40. package/templates/infra/k8s/k8s/kustomization.yml.template +13 -0
  41. package/templates/infra/k8s/k8s/namespace.yml.template +4 -0
  42. package/templates/infra/k8s/k8s/networkpolicy.yml.template +41 -0
  43. package/templates/infra/k8s/k8s/secrets.yml.template +10 -0
  44. package/templates/infra/k8s/k8s/service.yml.template +15 -0
  45. package/templates/testing/load/k6/README.md.template +48 -0
  46. package/templates/testing/load/k6/load-test.js.template +57 -0
package/src/utils.js CHANGED
@@ -1,116 +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
- export function getStackCommands(stackId) {
61
- if (stackId === 'nextjs-fullstack') {
62
- return {
63
- LINT_COMMAND: 'npx eslint .',
64
- TYPE_CHECK_COMMAND: 'npx tsc --noEmit',
65
- TEST_COMMAND: 'npx vitest run',
66
- BUILD_COMMAND: 'npm run build',
67
- DEV_COMMAND: 'npm run dev',
68
- };
69
- }
70
- if (stackId === 'fastapi-backend') {
71
- return {
72
- LINT_COMMAND: 'ruff check .',
73
- TYPE_CHECK_COMMAND: 'pyright',
74
- TEST_COMMAND: 'pytest',
75
- BUILD_COMMAND: 'docker build -t app .',
76
- DEV_COMMAND: 'uvicorn app.main:app --reload',
77
- };
78
- }
79
- if (stackId === 'polyglot-fullstack') {
80
- return {
81
- LINT_COMMAND: 'cd frontend && npx eslint . && cd ../backend && ruff check .',
82
- TYPE_CHECK_COMMAND: 'cd frontend && npx tsc --noEmit',
83
- TEST_COMMAND: 'cd frontend && npx vitest run && cd ../backend && pytest',
84
- BUILD_COMMAND: 'docker compose build',
85
- DEV_COMMAND: 'docker compose up',
86
- };
87
- }
88
- if (stackId === 'react-express') {
89
- return {
90
- LINT_COMMAND: 'cd frontend && npx eslint . && cd ../backend && npx eslint .',
91
- TYPE_CHECK_COMMAND: 'cd frontend && npx tsc --noEmit && cd ../backend && npx tsc --noEmit',
92
- TEST_COMMAND: 'cd frontend && npx vitest run && cd ../backend && npx vitest run',
93
- BUILD_COMMAND: 'cd frontend && npm run build && cd ../backend && npm run build',
94
- DEV_COMMAND: 'npx concurrently "cd frontend && npm run dev" "cd backend && npm run dev"',
95
- };
96
- }
97
- if (stackId === 'remix-fullstack') {
98
- return {
99
- LINT_COMMAND: 'npx eslint .',
100
- TYPE_CHECK_COMMAND: 'npx tsc --noEmit',
101
- TEST_COMMAND: 'npx vitest run',
102
- BUILD_COMMAND: 'npm run build',
103
- DEV_COMMAND: 'npm run dev',
104
- };
105
- }
106
- if (stackId === 'hono-api') {
107
- return {
108
- LINT_COMMAND: 'npx eslint .',
109
- TYPE_CHECK_COMMAND: 'npx tsc --noEmit',
110
- TEST_COMMAND: 'npx vitest run',
111
- BUILD_COMMAND: 'npm run build',
112
- DEV_COMMAND: 'npm run dev',
113
- };
114
- }
115
- return {};
116
- }
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", response_model=TokenResponse)
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
- token = create_access_token(data={"sub": request.email})
34
- return TokenResponse(access_token=token)
35
-
36
-
37
- @router.post("/register", response_model=TokenResponse)
38
- async def register(request: RegisterRequest):
39
- # TODO: Replace with actual database operations
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
- SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
12
- if not SECRET_KEY:
13
- raise RuntimeError("JWT_SECRET_KEY environment variable is required. Set it in your .env file.")
14
- ALGORITHM = "HS256"
15
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
16
-
17
-
18
- def verify_password(plain_password: str, hashed_password: str) -> bool:
19
- return pwd_context.verify(plain_password, hashed_password)
20
-
21
-
22
- def hash_password(password: str) -> str:
23
- return pwd_context.hash(password)
24
-
25
-
26
- def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
27
- to_encode = data.copy()
28
- expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
29
- to_encode.update({"exp": expire})
30
- return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
31
-
32
-
33
- def decode_access_token(token: str) -> dict | None:
34
- try:
35
- return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
36
- except JWTError:
37
- return None
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
@@ -1,33 +1,35 @@
1
- {
2
- "name": "{{PROJECT_NAME}}-server",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "dev": "tsx watch src/index.ts",
8
- "build": "tsc",
9
- "start": "node dist/index.js",
10
- "lint": "eslint .",
11
- "typecheck": "tsc --noEmit",
12
- "test": "vitest run",
13
- "test:watch": "vitest",
14
- "db:push": "prisma db push",
15
- "db:studio": "prisma studio",
16
- "db:generate": "prisma generate"
17
- },
18
- "dependencies": {
19
- "express": "^5.1.0",
20
- "cors": "^2.8.5",
21
- "@prisma/client": "^6.6.0"
22
- },
23
- "devDependencies": {
24
- "typescript": "^5.8.3",
25
- "@types/node": "^22.14.0",
26
- "@types/express": "^5.0.0",
27
- "@types/cors": "^2.8.17",
28
- "tsx": "^4.19.0",
29
- "eslint": "^9.25.0",
30
- "prisma": "^6.6.0",
31
- "vitest": "^3.1.1"
32
- }
33
- }
1
+ {
2
+ "name": "{{PROJECT_NAME}}-server",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/index.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "lint": "eslint .",
11
+ "typecheck": "tsc --noEmit",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "db:push": "prisma db push",
15
+ "db:migrate": "prisma migrate dev",
16
+ "db:migrate:deploy": "prisma migrate deploy",
17
+ "db:studio": "prisma studio",
18
+ "db:generate": "prisma generate"
19
+ },
20
+ "dependencies": {
21
+ "express": "^5.1.0",
22
+ "cors": "^2.8.5",
23
+ "@prisma/client": "^6.6.0"
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "^5.8.3",
27
+ "@types/node": "^22.14.0",
28
+ "@types/express": "^5.0.0",
29
+ "@types/cors": "^2.8.17",
30
+ "tsx": "^4.19.0",
31
+ "eslint": "^9.25.0",
32
+ "prisma": "^6.6.0",
33
+ "vitest": "^3.1.1"
34
+ }
35
+ }
@@ -0,0 +1,32 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ const MAX_RETRIES = 3;
4
+ const RETRY_DELAY_MS = 1000;
5
+
6
+ async function connectWithRetry(client: PrismaClient, retries = MAX_RETRIES): Promise<void> {
7
+ for (let attempt = 1; attempt <= retries; attempt++) {
8
+ try {
9
+ await client.$connect();
10
+ return;
11
+ } catch (error) {
12
+ if (attempt === retries) throw error;
13
+ const delay = RETRY_DELAY_MS * Math.pow(2, attempt - 1);
14
+ console.warn(`Database connection attempt ${attempt} failed. Retrying in ${delay}ms...`);
15
+ await new Promise((resolve) => setTimeout(resolve, delay));
16
+ }
17
+ }
18
+ }
19
+
20
+ export const prisma = new PrismaClient({
21
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
22
+ });
23
+
24
+ // Graceful shutdown
25
+ const shutdownHandler = async () => {
26
+ await prisma.$disconnect();
27
+ };
28
+
29
+ process.on('SIGTERM', shutdownHandler);
30
+ process.on('SIGINT', shutdownHandler);
31
+
32
+ export { connectWithRetry };