forgedev 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/CLAUDE.md +38 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/bin/devforge.js +4 -0
- package/package.json +33 -0
- package/src/claude-configurator.js +260 -0
- package/src/cli.js +119 -0
- package/src/composer.js +214 -0
- package/src/doctor-checks.js +743 -0
- package/src/doctor-prompts.js +295 -0
- package/src/doctor.js +281 -0
- package/src/guided.js +315 -0
- package/src/index.js +148 -0
- package/src/init-mode.js +134 -0
- package/src/prompts.js +155 -0
- package/src/recommender.js +186 -0
- package/src/scanner.js +368 -0
- package/src/uat-generator.js +189 -0
- package/src/utils.js +57 -0
- package/templates/auth/jwt-custom/backend/app/api/auth.py.template +45 -0
- package/templates/auth/jwt-custom/backend/app/api/deps.py.template +16 -0
- package/templates/auth/jwt-custom/backend/app/core/security.py.template +34 -0
- package/templates/auth/nextauth/src/app/api/auth/[...nextauth]/route.ts.template +3 -0
- package/templates/auth/nextauth/src/lib/auth.ts.template +30 -0
- package/templates/auth/nextauth/src/middleware.ts.template +14 -0
- package/templates/backend/fastapi/backend/Dockerfile.template +12 -0
- package/templates/backend/fastapi/backend/app/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/api/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/api/health.py.template +32 -0
- package/templates/backend/fastapi/backend/app/core/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/core/config.py.template +25 -0
- package/templates/backend/fastapi/backend/app/core/errors.py +37 -0
- package/templates/backend/fastapi/backend/app/core/retry.py +32 -0
- package/templates/backend/fastapi/backend/app/main.py.template +58 -0
- package/templates/backend/fastapi/backend/app/models/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/schemas/__init__.py +0 -0
- package/templates/backend/fastapi/backend/pyproject.toml.template +19 -0
- package/templates/backend/fastapi/backend/requirements.txt.template +14 -0
- package/templates/base/.gitignore.template +29 -0
- package/templates/base/README.md.template +25 -0
- package/templates/claude-code/agents/code-quality-reviewer.md +41 -0
- package/templates/claude-code/agents/production-readiness.md +55 -0
- package/templates/claude-code/agents/security-reviewer.md +41 -0
- package/templates/claude-code/agents/spec-validator.md +34 -0
- package/templates/claude-code/agents/uat-validator.md +37 -0
- package/templates/claude-code/claude-md/base.md +33 -0
- package/templates/claude-code/claude-md/fastapi.md +12 -0
- package/templates/claude-code/claude-md/fullstack.md +12 -0
- package/templates/claude-code/claude-md/nextjs.md +11 -0
- package/templates/claude-code/commands/audit-security.md +11 -0
- package/templates/claude-code/commands/audit-spec.md +9 -0
- package/templates/claude-code/commands/audit-wiring.md +17 -0
- package/templates/claude-code/commands/done.md +19 -0
- package/templates/claude-code/commands/generate-prd.md +45 -0
- package/templates/claude-code/commands/generate-uat.md +35 -0
- package/templates/claude-code/commands/help.md +26 -0
- package/templates/claude-code/commands/next.md +20 -0
- package/templates/claude-code/commands/optimize-claude-md.md +31 -0
- package/templates/claude-code/commands/pre-pr.md +19 -0
- package/templates/claude-code/commands/run-uat.md +21 -0
- package/templates/claude-code/commands/status.md +24 -0
- package/templates/claude-code/commands/verify-all.md +11 -0
- package/templates/claude-code/hooks/polyglot.json +36 -0
- package/templates/claude-code/hooks/python.json +36 -0
- package/templates/claude-code/hooks/scripts/autofix-polyglot.sh +16 -0
- package/templates/claude-code/hooks/scripts/autofix-python.sh +14 -0
- package/templates/claude-code/hooks/scripts/autofix-typescript.sh +14 -0
- package/templates/claude-code/hooks/scripts/guard-protected-files.sh +21 -0
- package/templates/claude-code/hooks/typescript.json +36 -0
- package/templates/claude-code/skills/ai-prompts/SKILL.md +43 -0
- package/templates/claude-code/skills/fastapi/SKILL.md +38 -0
- package/templates/claude-code/skills/nextjs/SKILL.md +39 -0
- package/templates/claude-code/skills/playwright/SKILL.md +37 -0
- package/templates/claude-code/skills/security-api/SKILL.md +47 -0
- package/templates/claude-code/skills/security-web/SKILL.md +41 -0
- package/templates/database/prisma-postgres/.env.example +1 -0
- package/templates/database/prisma-postgres/prisma/schema.prisma.template +18 -0
- package/templates/database/sqlalchemy-postgres/.env.example +1 -0
- package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +40 -0
- package/templates/database/sqlalchemy-postgres/backend/alembic/versions/.gitkeep +0 -0
- package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +36 -0
- package/templates/database/sqlalchemy-postgres/backend/app/db/__init__.py +0 -0
- package/templates/database/sqlalchemy-postgres/backend/app/db/base.py +5 -0
- package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +48 -0
- package/templates/frontend/nextjs/next.config.ts.template +7 -0
- package/templates/frontend/nextjs/package.json.template +41 -0
- package/templates/frontend/nextjs/postcss.config.mjs +7 -0
- package/templates/frontend/nextjs/src/app/api/health/route.ts.template +10 -0
- package/templates/frontend/nextjs/src/app/globals.css +1 -0
- package/templates/frontend/nextjs/src/app/layout.tsx.template +22 -0
- package/templates/frontend/nextjs/src/app/page.tsx.template +10 -0
- package/templates/frontend/nextjs/src/lib/db.ts.template +40 -0
- package/templates/frontend/nextjs/src/lib/errors.ts +28 -0
- package/templates/frontend/nextjs/src/lib/utils.ts +6 -0
- package/templates/frontend/nextjs/tsconfig.json +23 -0
- package/templates/infra/docker-compose/docker-compose.yml.template +19 -0
- package/templates/testing/playwright/e2e/example.spec.ts.template +15 -0
- package/templates/testing/playwright/playwright.config.ts.template +22 -0
- package/templates/testing/vitest/src/__tests__/example.test.ts.template +12 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Security (API)
|
|
3
|
+
description: API security best practices
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# API Security
|
|
7
|
+
|
|
8
|
+
## Authentication & Authorization
|
|
9
|
+
- Use JWT tokens with short expiration (15-30 min)
|
|
10
|
+
- Implement refresh token rotation
|
|
11
|
+
- Hash passwords with bcrypt (passlib)
|
|
12
|
+
- Use `Depends(get_current_user)` on all protected endpoints
|
|
13
|
+
- Implement role-based access control (RBAC)
|
|
14
|
+
|
|
15
|
+
## Input Validation
|
|
16
|
+
- Validate all input with Pydantic models
|
|
17
|
+
- Set max lengths on string fields
|
|
18
|
+
- Validate email formats, URLs, phone numbers
|
|
19
|
+
- Reject unexpected fields (Pydantic does this by default)
|
|
20
|
+
- Validate file uploads (size, type, extension)
|
|
21
|
+
|
|
22
|
+
## SQL Injection Prevention
|
|
23
|
+
- Use SQLAlchemy ORM — never raw SQL strings
|
|
24
|
+
- If raw SQL needed, use `text()` with bound parameters
|
|
25
|
+
- Never interpolate user input into queries
|
|
26
|
+
|
|
27
|
+
## Rate Limiting
|
|
28
|
+
- Implement per-IP rate limiting on auth endpoints
|
|
29
|
+
- Use sliding window or token bucket algorithms
|
|
30
|
+
- Return `429 Too Many Requests` with `Retry-After` header
|
|
31
|
+
|
|
32
|
+
## CORS
|
|
33
|
+
- Whitelist specific origins, never use `*` in production
|
|
34
|
+
- Restrict allowed methods and headers
|
|
35
|
+
- Set `allow_credentials=True` only when needed
|
|
36
|
+
|
|
37
|
+
## Error Handling
|
|
38
|
+
- Never expose stack traces to clients
|
|
39
|
+
- Use generic error messages for auth failures
|
|
40
|
+
- Log detailed errors server-side only
|
|
41
|
+
- Return structured error responses: `{ error: { code, message } }`
|
|
42
|
+
|
|
43
|
+
## Secrets Management
|
|
44
|
+
- Store secrets in environment variables, never in code
|
|
45
|
+
- Use `.env.example` for documentation (no real values)
|
|
46
|
+
- Rotate secrets regularly
|
|
47
|
+
- Never log secrets or include in error responses
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Security (Web)
|
|
3
|
+
description: Web application security best practices
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Web Application Security
|
|
7
|
+
|
|
8
|
+
## XSS Prevention
|
|
9
|
+
- React escapes by default — never use `dangerouslySetInnerHTML`
|
|
10
|
+
- Sanitize user input before rendering
|
|
11
|
+
- Use Content Security Policy headers
|
|
12
|
+
- Validate URLs before using in `href` or `src`
|
|
13
|
+
|
|
14
|
+
## CSRF Protection
|
|
15
|
+
- Use SameSite cookies (Lax or Strict)
|
|
16
|
+
- NextAuth handles CSRF tokens automatically
|
|
17
|
+
- For custom forms, include CSRF tokens in hidden fields
|
|
18
|
+
|
|
19
|
+
## Authentication
|
|
20
|
+
- Store tokens in httpOnly cookies, never localStorage
|
|
21
|
+
- Use secure, SameSite cookies in production
|
|
22
|
+
- Implement proper session expiration
|
|
23
|
+
- Rate limit login attempts
|
|
24
|
+
|
|
25
|
+
## Headers
|
|
26
|
+
- Set `X-Content-Type-Options: nosniff`
|
|
27
|
+
- Set `X-Frame-Options: DENY`
|
|
28
|
+
- Set `Strict-Transport-Security` for HTTPS
|
|
29
|
+
- Configure CSP to restrict script sources
|
|
30
|
+
|
|
31
|
+
## Input Validation
|
|
32
|
+
- Validate all user input with Zod schemas
|
|
33
|
+
- Validate on both client and server
|
|
34
|
+
- Never trust client-side validation alone
|
|
35
|
+
- Sanitize file uploads (check MIME type, size, extension)
|
|
36
|
+
|
|
37
|
+
## Data Exposure
|
|
38
|
+
- Never return sensitive fields (passwords, tokens) in API responses
|
|
39
|
+
- Use Prisma `select` or `omit` to control returned fields
|
|
40
|
+
- Never log sensitive data
|
|
41
|
+
- Strip internal IDs from client-facing responses when possible
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME_SNAKE}}"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "postgresql"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Add your models below
|
|
11
|
+
// Example:
|
|
12
|
+
// model User {
|
|
13
|
+
// id String @id @default(cuid())
|
|
14
|
+
// email String @unique
|
|
15
|
+
// name String?
|
|
16
|
+
// createdAt DateTime @default(now())
|
|
17
|
+
// updatedAt DateTime @updatedAt
|
|
18
|
+
// }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/{{PROJECT_NAME_SNAKE}}"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from logging.config import fileConfig
|
|
3
|
+
|
|
4
|
+
from alembic import context
|
|
5
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
6
|
+
|
|
7
|
+
from app.core.config import settings
|
|
8
|
+
from app.db.base import Base
|
|
9
|
+
|
|
10
|
+
config = context.config
|
|
11
|
+
if config.config_file_name is not None:
|
|
12
|
+
fileConfig(config.config_file_name)
|
|
13
|
+
|
|
14
|
+
target_metadata = Base.metadata
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_migrations_offline() -> None:
|
|
18
|
+
url = settings.database_url
|
|
19
|
+
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
|
|
20
|
+
with context.begin_transaction():
|
|
21
|
+
context.run_migrations()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def do_run_migrations(connection):
|
|
25
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
26
|
+
with context.begin_transaction():
|
|
27
|
+
context.run_migrations()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def run_migrations_online() -> None:
|
|
31
|
+
connectable = create_async_engine(settings.database_url)
|
|
32
|
+
async with connectable.connect() as connection:
|
|
33
|
+
await connection.run_sync(do_run_migrations)
|
|
34
|
+
await connectable.dispose()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if context.is_offline_mode():
|
|
38
|
+
run_migrations_offline()
|
|
39
|
+
else:
|
|
40
|
+
asyncio.run(run_migrations_online())
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[alembic]
|
|
2
|
+
script_location = alembic
|
|
3
|
+
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/{{PROJECT_NAME_SNAKE}}
|
|
4
|
+
|
|
5
|
+
[loggers]
|
|
6
|
+
keys = root,sqlalchemy,alembic
|
|
7
|
+
|
|
8
|
+
[handlers]
|
|
9
|
+
keys = console
|
|
10
|
+
|
|
11
|
+
[formatters]
|
|
12
|
+
keys = generic
|
|
13
|
+
|
|
14
|
+
[logger_root]
|
|
15
|
+
level = WARN
|
|
16
|
+
handlers = console
|
|
17
|
+
|
|
18
|
+
[logger_sqlalchemy]
|
|
19
|
+
level = WARN
|
|
20
|
+
handlers =
|
|
21
|
+
qualname = sqlalchemy.engine
|
|
22
|
+
|
|
23
|
+
[logger_alembic]
|
|
24
|
+
level = INFO
|
|
25
|
+
handlers =
|
|
26
|
+
qualname = alembic
|
|
27
|
+
|
|
28
|
+
[handler_console]
|
|
29
|
+
class = StreamHandler
|
|
30
|
+
args = (sys.stderr,)
|
|
31
|
+
level = NOTSET
|
|
32
|
+
formatter = generic
|
|
33
|
+
|
|
34
|
+
[formatter_generic]
|
|
35
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
36
|
+
datefmt = %H:%M:%S
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
5
|
+
|
|
6
|
+
from app.core.config import settings
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
engine = create_async_engine(
|
|
11
|
+
settings.database_url,
|
|
12
|
+
echo=settings.debug,
|
|
13
|
+
pool_size=5,
|
|
14
|
+
max_overflow=10,
|
|
15
|
+
pool_pre_ping=True,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def connect_with_retry(
|
|
22
|
+
engine, max_retries: int = 3, base_delay: float = 1.0
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Attempt database connection with exponential backoff."""
|
|
25
|
+
for attempt in range(1, max_retries + 1):
|
|
26
|
+
try:
|
|
27
|
+
async with engine.connect() as conn:
|
|
28
|
+
await conn.execute(
|
|
29
|
+
__import__("sqlalchemy").text("SELECT 1")
|
|
30
|
+
)
|
|
31
|
+
logger.info("Database connected successfully")
|
|
32
|
+
return
|
|
33
|
+
except Exception as e:
|
|
34
|
+
if attempt == max_retries:
|
|
35
|
+
logger.error(f"Database connection failed after {max_retries} attempts: {e}")
|
|
36
|
+
raise
|
|
37
|
+
delay = base_delay * (2 ** (attempt - 1))
|
|
38
|
+
logger.warning(
|
|
39
|
+
f"Database connection attempt {attempt}/{max_retries} failed. "
|
|
40
|
+
f"Retrying in {delay:.1f}s..."
|
|
41
|
+
)
|
|
42
|
+
await asyncio.sleep(delay)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def get_db():
|
|
46
|
+
"""Dependency for FastAPI endpoints."""
|
|
47
|
+
async with async_session() as session:
|
|
48
|
+
yield session
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"test:e2e": "playwright test",
|
|
14
|
+
"db:push": "prisma db push",
|
|
15
|
+
"db:studio": "prisma studio",
|
|
16
|
+
"db:generate": "prisma generate"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"next": "^15.3.0",
|
|
20
|
+
"react": "^19.1.0",
|
|
21
|
+
"react-dom": "^19.1.0",
|
|
22
|
+
"@prisma/client": "^6.6.0",
|
|
23
|
+
"clsx": "^2.1.1",
|
|
24
|
+
"tailwind-merge": "^3.2.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.8.3",
|
|
28
|
+
"@types/node": "^22.14.0",
|
|
29
|
+
"@types/react": "^19.1.0",
|
|
30
|
+
"@types/react-dom": "^19.1.0",
|
|
31
|
+
"tailwindcss": "^4.1.3",
|
|
32
|
+
"@tailwindcss/postcss": "^4.1.3",
|
|
33
|
+
"postcss": "^8.5.3",
|
|
34
|
+
"eslint": "^9.25.0",
|
|
35
|
+
"eslint-config-next": "^15.3.0",
|
|
36
|
+
"prisma": "^6.6.0",
|
|
37
|
+
"vitest": "^3.1.1",
|
|
38
|
+
"@playwright/test": "^1.52.0",
|
|
39
|
+
"@testing-library/react": "^16.3.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { Inter } from 'next/font/google';
|
|
3
|
+
import './globals.css';
|
|
4
|
+
|
|
5
|
+
const inter = Inter({ subsets: ['latin'] });
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: '{{PROJECT_NAME_PASCAL}}',
|
|
9
|
+
description: '{{STACK_DESCRIPTION}}',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<body className={inter.className}>{children}</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default function Home() {
|
|
2
|
+
return (
|
|
3
|
+
<main className="flex min-h-screen flex-col items-center justify-center p-24">
|
|
4
|
+
<h1 className="text-4xl font-bold mb-4">{{PROJECT_NAME_PASCAL}}</h1>
|
|
5
|
+
<p className="text-lg text-gray-600">
|
|
6
|
+
Your project is ready. Start building!
|
|
7
|
+
</p>
|
|
8
|
+
</main>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client';
|
|
2
|
+
|
|
3
|
+
const MAX_RETRIES = 3;
|
|
4
|
+
const RETRY_DELAY_MS = 1000;
|
|
5
|
+
|
|
6
|
+
async function connectWithRetry(prisma: PrismaClient, retries = MAX_RETRIES): Promise<void> {
|
|
7
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
8
|
+
try {
|
|
9
|
+
await prisma.$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
|
+
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
|
|
21
|
+
|
|
22
|
+
export const prisma =
|
|
23
|
+
globalForPrisma.prisma ??
|
|
24
|
+
new PrismaClient({
|
|
25
|
+
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
29
|
+
globalForPrisma.prisma = prisma;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Graceful shutdown
|
|
33
|
+
const shutdownHandler = async () => {
|
|
34
|
+
await prisma.$disconnect();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
process.on('SIGTERM', shutdownHandler);
|
|
38
|
+
process.on('SIGINT', shutdownHandler);
|
|
39
|
+
|
|
40
|
+
export { connectWithRetry };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
export class AppError extends Error {
|
|
4
|
+
constructor(
|
|
5
|
+
message: string,
|
|
6
|
+
public statusCode: number = 500,
|
|
7
|
+
public code: string = 'INTERNAL_ERROR'
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'AppError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function errorResponse(error: unknown) {
|
|
15
|
+
if (error instanceof AppError) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ error: { code: error.code, message: error.message } },
|
|
18
|
+
{ status: error.statusCode }
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Never leak internal error details
|
|
23
|
+
console.error('Unhandled error:', error);
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred' } },
|
|
26
|
+
{ status: 500 }
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./src/*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
services:
|
|
2
|
+
postgres:
|
|
3
|
+
image: postgres:17-alpine
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_DB: {{PROJECT_NAME_SNAKE}}
|
|
6
|
+
POSTGRES_USER: postgres
|
|
7
|
+
POSTGRES_PASSWORD: postgres
|
|
8
|
+
ports:
|
|
9
|
+
- "5432:5432"
|
|
10
|
+
volumes:
|
|
11
|
+
- postgres_data:/var/lib/postgresql/data
|
|
12
|
+
healthcheck:
|
|
13
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
14
|
+
interval: 5s
|
|
15
|
+
timeout: 5s
|
|
16
|
+
retries: 5
|
|
17
|
+
|
|
18
|
+
volumes:
|
|
19
|
+
postgres_data:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
test.describe('{{PROJECT_NAME_PASCAL}} E2E', () => {
|
|
4
|
+
test('home page loads', async ({ page }) => {
|
|
5
|
+
await page.goto('/');
|
|
6
|
+
await expect(page).toHaveTitle(/{{PROJECT_NAME_PASCAL}}/);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('health endpoint responds', async ({ request }) => {
|
|
10
|
+
const response = await request.get('/api/health');
|
|
11
|
+
expect(response.ok()).toBeTruthy();
|
|
12
|
+
const body = await response.json();
|
|
13
|
+
expect(body.status).toBe('ok');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
testDir: './e2e',
|
|
5
|
+
fullyParallel: true,
|
|
6
|
+
forbidOnly: !!process.env.CI,
|
|
7
|
+
retries: process.env.CI ? 2 : 0,
|
|
8
|
+
workers: process.env.CI ? 1 : undefined,
|
|
9
|
+
reporter: 'html',
|
|
10
|
+
use: {
|
|
11
|
+
baseURL: 'http://localhost:3000',
|
|
12
|
+
trace: 'on-first-retry',
|
|
13
|
+
},
|
|
14
|
+
projects: [
|
|
15
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
16
|
+
],
|
|
17
|
+
webServer: {
|
|
18
|
+
command: 'npm run dev',
|
|
19
|
+
url: 'http://localhost:3000',
|
|
20
|
+
reuseExistingServer: !process.env.CI,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('{{PROJECT_NAME_PASCAL}}', () => {
|
|
4
|
+
it('should pass a basic test', () => {
|
|
5
|
+
expect(1 + 1).toBe(2);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('should have the project name defined', () => {
|
|
9
|
+
const projectName = '{{PROJECT_NAME}}';
|
|
10
|
+
expect(projectName).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
});
|