@techstream/quark-create-app 1.5.3 → 1.7.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/package.json +4 -2
- package/src/index.js +62 -14
- package/templates/base-project/.github/dependabot.yml +12 -0
- package/templates/base-project/.github/workflows/ci.yml +97 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/release.yml +38 -0
- package/templates/base-project/apps/web/biome.json +7 -0
- package/templates/base-project/apps/web/jsconfig.json +5 -5
- package/templates/base-project/apps/web/next.config.js +90 -1
- package/templates/base-project/apps/web/package.json +7 -7
- package/templates/base-project/apps/web/railway.json +15 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +6 -7
- package/templates/base-project/apps/web/src/app/layout.js +3 -4
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/robots.js +21 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +20 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +23 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +33 -0
- package/templates/base-project/apps/web/src/proxy.js +1 -2
- package/templates/base-project/apps/worker/package.json +5 -5
- package/templates/base-project/apps/worker/railway.json +13 -0
- package/templates/base-project/apps/worker/src/index.js +30 -12
- package/templates/base-project/apps/worker/src/index.test.js +296 -15
- package/templates/base-project/biome.json +44 -0
- package/templates/base-project/docker-compose.yml +7 -4
- package/templates/base-project/package.json +1 -1
- package/templates/base-project/packages/db/package.json +1 -1
- package/templates/base-project/packages/db/prisma/schema.prisma +1 -17
- package/templates/base-project/packages/db/prisma.config.ts +8 -10
- package/templates/base-project/packages/db/scripts/seed.js +117 -30
- package/templates/base-project/packages/db/src/client.js +1 -18
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/connection.test.js +119 -0
- package/templates/base-project/packages/db/src/queries.js +52 -118
- package/templates/base-project/packages/db/src/queries.test.js +0 -29
- package/templates/base-project/packages/db/src/schemas.js +0 -12
- package/templates/base-project/pnpm-workspace.yaml +4 -0
- package/templates/base-project/turbo.json +5 -3
- package/templates/config/package.json +2 -0
- package/templates/config/src/environment.js +270 -0
- package/templates/config/src/index.js +13 -18
- package/templates/config/src/load-config.js +135 -0
- package/templates/config/src/validate-env.js +123 -16
- package/templates/jobs/package.json +2 -2
- package/templates/jobs/src/definitions.test.js +34 -0
- package/templates/jobs/src/index.js +1 -1
- package/templates/ui/package.json +4 -4
- package/templates/ui/src/button.test.js +23 -0
- package/templates/ui/src/index.js +1 -3
- package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +0 -65
- package/templates/base-project/apps/web/src/app/api/posts/route.js +0 -95
- package/templates/ui/src/card.js +0 -14
- package/templates/ui/src/input.js +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techstream/quark-create-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"quark-create-app": "src/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"chalk": "^5.6.2",
|
|
17
|
-
"commander": "^
|
|
17
|
+
"commander": "^14.0.3",
|
|
18
18
|
"execa": "^9.6.1",
|
|
19
19
|
"fs-extra": "^11.3.3",
|
|
20
20
|
"prompts": "^2.4.2"
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
"license": "ISC",
|
|
30
30
|
"scripts": {
|
|
31
|
+
"sync-templates": "node scripts/sync-templates.js",
|
|
32
|
+
"sync-templates:check": "node scripts/sync-templates.js --check",
|
|
31
33
|
"test": "node test-cli.js",
|
|
32
34
|
"test:build": "node test-build.js",
|
|
33
35
|
"test:e2e": "node test-e2e.js",
|
package/src/index.js
CHANGED
|
@@ -474,17 +474,21 @@ program
|
|
|
474
474
|
|
|
475
475
|
// Step 8: Create .env.example file
|
|
476
476
|
console.log(chalk.cyan("\n 📋 Creating environment configuration..."));
|
|
477
|
-
const envExampleTemplate = `# ---
|
|
477
|
+
const envExampleTemplate = `# --- Environment ---
|
|
478
|
+
# Supported: development, test, staging, production (default: development)
|
|
479
|
+
# NODE_ENV=development
|
|
480
|
+
|
|
481
|
+
# --- Database Configuration ---
|
|
478
482
|
# These map to the service names in docker-compose.yml
|
|
479
483
|
# ⚠️ SECURITY WARNING: Change these default passwords in production!
|
|
480
484
|
# Generate strong passwords with: openssl rand -base64 32
|
|
481
485
|
POSTGRES_HOST=localhost
|
|
482
486
|
POSTGRES_PORT=5432
|
|
483
|
-
POSTGRES_USER
|
|
487
|
+
POSTGRES_USER=${scope}_user
|
|
484
488
|
POSTGRES_PASSWORD=CHANGE_ME_TO_STRONG_PASSWORD
|
|
485
489
|
POSTGRES_DB=${scope}_dev
|
|
486
490
|
# Optional: Set DATABASE_URL to override the dynamic construction above
|
|
487
|
-
# DATABASE_URL="postgresql
|
|
491
|
+
# DATABASE_URL="postgresql://${scope}_user:CHANGE_ME_TO_STRONG_PASSWORD@localhost:5432/${scope}_dev?schema=public"
|
|
488
492
|
|
|
489
493
|
# --- Redis Configuration ---
|
|
490
494
|
REDIS_HOST=localhost
|
|
@@ -492,18 +496,37 @@ REDIS_PORT=6379
|
|
|
492
496
|
# Optional: Set REDIS_URL to override the dynamic construction above
|
|
493
497
|
# REDIS_URL="redis://localhost:6379"
|
|
494
498
|
|
|
495
|
-
# --- Mail Configuration
|
|
499
|
+
# --- Mail Configuration ---
|
|
500
|
+
# Development: Mailpit local SMTP (defaults below work with docker-compose)
|
|
496
501
|
MAIL_HOST=localhost
|
|
497
502
|
MAIL_SMTP_PORT=1025
|
|
498
503
|
MAIL_UI_PORT=8025
|
|
499
|
-
|
|
500
|
-
#
|
|
504
|
+
|
|
505
|
+
# Production SMTP: Set these instead of MAIL_* when using a real SMTP relay
|
|
506
|
+
# SMTP_HOST=smtp.example.com
|
|
507
|
+
# SMTP_PORT=587
|
|
508
|
+
# SMTP_SECURE=true
|
|
509
|
+
# SMTP_USER=your_smtp_user
|
|
510
|
+
# SMTP_PASSWORD=your_smtp_password
|
|
511
|
+
|
|
512
|
+
# --- Email Provider ---
|
|
513
|
+
# Provider: "smtp" (default) or "resend"
|
|
514
|
+
# EMAIL_PROVIDER=smtp
|
|
515
|
+
# EMAIL_FROM=App Name <noreply@yourdomain.com>
|
|
516
|
+
|
|
517
|
+
# Resend (only when EMAIL_PROVIDER=resend)
|
|
518
|
+
# Get your API key at: https://resend.com/api-keys
|
|
519
|
+
# RESEND_API_KEY=re_xxxxxxxxxxxxx
|
|
501
520
|
|
|
502
521
|
# --- Application URL ---
|
|
503
522
|
# In development, APP_URL is derived automatically from PORT — no need to set it.
|
|
504
523
|
# In production, set this to your real domain:
|
|
505
524
|
# APP_URL=https://yourdomain.com
|
|
506
525
|
|
|
526
|
+
# --- Application Identity ---
|
|
527
|
+
# APP_NAME is used in metadata, emails, and page titles.
|
|
528
|
+
APP_NAME=${projectName}
|
|
529
|
+
|
|
507
530
|
# --- NextAuth Configuration ---
|
|
508
531
|
# ⚠️ CRITICAL: Generate a secure secret with: openssl rand -base64 32
|
|
509
532
|
# This secret is used to encrypt JWT tokens and session data
|
|
@@ -524,23 +547,36 @@ PORT=3000
|
|
|
524
547
|
# --- Worker Configuration ---
|
|
525
548
|
WORKER_CONCURRENCY=5
|
|
526
549
|
|
|
527
|
-
# --- File Storage
|
|
528
|
-
#
|
|
550
|
+
# --- File Storage ---
|
|
551
|
+
# Provider: "local" (default) or "s3" (S3-compatible: AWS S3, Cloudflare R2, MinIO)
|
|
529
552
|
STORAGE_PROVIDER=local
|
|
530
|
-
# Local storage directory (only
|
|
553
|
+
# Local storage directory (only when STORAGE_PROVIDER=local)
|
|
531
554
|
# STORAGE_LOCAL_DIR=./uploads
|
|
532
555
|
|
|
533
|
-
# S3 / Cloudflare R2
|
|
556
|
+
# S3 / Cloudflare R2 (only when STORAGE_PROVIDER=s3)
|
|
534
557
|
# S3_BUCKET=your-bucket-name
|
|
535
|
-
# S3_REGION=auto
|
|
558
|
+
# S3_REGION=auto # Use "auto" for Cloudflare R2
|
|
536
559
|
# S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
|
|
537
560
|
# S3_ACCESS_KEY_ID=your-access-key
|
|
538
561
|
# S3_SECRET_ACCESS_KEY=your-secret-key
|
|
539
562
|
# S3_PUBLIC_URL=https://your-public-bucket-domain.com
|
|
540
563
|
|
|
541
564
|
# --- Upload Limits ---
|
|
542
|
-
# UPLOAD_MAX_SIZE=10485760
|
|
565
|
+
# UPLOAD_MAX_SIZE=10485760 # Max file size in bytes (default: 10MB)
|
|
543
566
|
# UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif,image/webp,image/avif,image/svg+xml,application/pdf
|
|
567
|
+
|
|
568
|
+
# --- Rate Limiting & Security ---
|
|
569
|
+
# RATE_LIMIT_MAX=100 # Max requests per window (default: 1000 dev, 100 prod)
|
|
570
|
+
# RATE_LIMIT_WINDOW_MS=900000 # Window in ms (default: 15 minutes)
|
|
571
|
+
# API_BODY_SIZE_LIMIT=2097152 # 2MB (default)
|
|
572
|
+
# UPLOAD_SIZE_LIMIT=10485760 # 10MB proxy-level limit (default)
|
|
573
|
+
|
|
574
|
+
# --- Logging & Cache ---
|
|
575
|
+
# LOG_LEVEL=debug # debug, info, warn, error (default: debug dev, info prod)
|
|
576
|
+
# CACHE_TTL=60 # Default cache TTL in seconds (default: 60 dev, 600 prod)
|
|
577
|
+
|
|
578
|
+
# --- Database Seeding ---
|
|
579
|
+
# SEED_PROFILE=dev # Options: dev (default, includes audit logs + sample job), minimal (users only)
|
|
544
580
|
`;
|
|
545
581
|
await fs.writeFile(
|
|
546
582
|
path.join(targetDir, ".env.example"),
|
|
@@ -583,7 +619,7 @@ STORAGE_PROVIDER=local
|
|
|
583
619
|
const envContent = `# --- Database Configuration ---
|
|
584
620
|
POSTGRES_HOST=localhost
|
|
585
621
|
POSTGRES_PORT=${postgresPort}
|
|
586
|
-
POSTGRES_USER
|
|
622
|
+
POSTGRES_USER=${scope}_user
|
|
587
623
|
POSTGRES_PASSWORD=${dbPassword}
|
|
588
624
|
POSTGRES_DB=${scope}_dev
|
|
589
625
|
|
|
@@ -596,6 +632,9 @@ MAIL_HOST=localhost
|
|
|
596
632
|
MAIL_SMTP_PORT=${mailSmtpPort}
|
|
597
633
|
MAIL_UI_PORT=${mailUiPort}
|
|
598
634
|
|
|
635
|
+
# --- Application Identity ---
|
|
636
|
+
APP_NAME=${projectName}
|
|
637
|
+
|
|
599
638
|
# --- NextAuth Configuration ---
|
|
600
639
|
NEXTAUTH_SECRET=${nextAuthSecret}
|
|
601
640
|
|
|
@@ -609,6 +648,9 @@ WORKER_CONCURRENCY=5
|
|
|
609
648
|
|
|
610
649
|
# --- File Storage ---
|
|
611
650
|
STORAGE_PROVIDER=local
|
|
651
|
+
|
|
652
|
+
# --- Database Seeding ---
|
|
653
|
+
# SEED_PROFILE=dev # Options: dev (default), minimal (users only — use for production initial seed)
|
|
612
654
|
`;
|
|
613
655
|
await fs.writeFile(path.join(targetDir, ".env"), envContent);
|
|
614
656
|
console.log(
|
|
@@ -725,7 +767,13 @@ STORAGE_PROVIDER=local
|
|
|
725
767
|
console.log(chalk.white(` 1. cd ${projectName}`));
|
|
726
768
|
console.log(chalk.white(` 2. docker compose up -d`));
|
|
727
769
|
console.log(chalk.white(` 3. pnpm db:migrate`));
|
|
728
|
-
console.log(chalk.white(` 4. pnpm
|
|
770
|
+
console.log(chalk.white(` 4. pnpm db:seed`));
|
|
771
|
+
console.log(chalk.white(` 5. pnpm dev\n`));
|
|
772
|
+
console.log(
|
|
773
|
+
chalk.dim(
|
|
774
|
+
` Tip: set SEED_PROFILE=minimal in .env for a lean seed (admin user only)\n`,
|
|
775
|
+
),
|
|
776
|
+
);
|
|
729
777
|
|
|
730
778
|
console.log(chalk.cyan("Important:"));
|
|
731
779
|
console.log(
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
name: Lint
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: pnpm/action-setup@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 24
|
|
21
|
+
cache: pnpm
|
|
22
|
+
|
|
23
|
+
- run: pnpm install --frozen-lockfile
|
|
24
|
+
|
|
25
|
+
- run: pnpm lint
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
name: Test
|
|
30
|
+
services:
|
|
31
|
+
postgres:
|
|
32
|
+
image: postgres:16-alpine
|
|
33
|
+
env:
|
|
34
|
+
POSTGRES_USER: postgres
|
|
35
|
+
POSTGRES_PASSWORD: postgres
|
|
36
|
+
POSTGRES_DB: app_test
|
|
37
|
+
ports:
|
|
38
|
+
- 5432:5432
|
|
39
|
+
options: >-
|
|
40
|
+
--health-cmd="pg_isready -U postgres"
|
|
41
|
+
--health-interval=5s
|
|
42
|
+
--health-timeout=5s
|
|
43
|
+
--health-retries=5
|
|
44
|
+
redis:
|
|
45
|
+
image: redis:7-alpine
|
|
46
|
+
ports:
|
|
47
|
+
- 6379:6379
|
|
48
|
+
options: >-
|
|
49
|
+
--health-cmd="redis-cli ping"
|
|
50
|
+
--health-interval=5s
|
|
51
|
+
--health-timeout=5s
|
|
52
|
+
--health-retries=5
|
|
53
|
+
env:
|
|
54
|
+
POSTGRES_USER: postgres
|
|
55
|
+
POSTGRES_PASSWORD: postgres
|
|
56
|
+
POSTGRES_HOST: localhost
|
|
57
|
+
POSTGRES_PORT: "5432"
|
|
58
|
+
POSTGRES_DB: app_test
|
|
59
|
+
REDIS_HOST: localhost
|
|
60
|
+
REDIS_PORT: "6379"
|
|
61
|
+
NEXTAUTH_SECRET: ci-test-secret-must-be-at-least-32-characters-long
|
|
62
|
+
NODE_ENV: test
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v4
|
|
65
|
+
|
|
66
|
+
- uses: pnpm/action-setup@v4
|
|
67
|
+
|
|
68
|
+
- uses: actions/setup-node@v4
|
|
69
|
+
with:
|
|
70
|
+
node-version: 24
|
|
71
|
+
cache: pnpm
|
|
72
|
+
|
|
73
|
+
- run: pnpm install --frozen-lockfile
|
|
74
|
+
|
|
75
|
+
- run: pnpm db:generate
|
|
76
|
+
|
|
77
|
+
- run: pnpm test
|
|
78
|
+
|
|
79
|
+
build:
|
|
80
|
+
name: Build
|
|
81
|
+
needs: [lint, test]
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v4
|
|
85
|
+
|
|
86
|
+
- uses: pnpm/action-setup@v4
|
|
87
|
+
|
|
88
|
+
- uses: actions/setup-node@v4
|
|
89
|
+
with:
|
|
90
|
+
node-version: 24
|
|
91
|
+
cache: pnpm
|
|
92
|
+
|
|
93
|
+
- run: pnpm install --frozen-lockfile
|
|
94
|
+
|
|
95
|
+
- run: pnpm db:generate
|
|
96
|
+
|
|
97
|
+
- run: pnpm build
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Auto-merge Dependabot
|
|
2
|
+
|
|
3
|
+
on: pull_request
|
|
4
|
+
|
|
5
|
+
permissions:
|
|
6
|
+
contents: write
|
|
7
|
+
pull-requests: write
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
auto-merge:
|
|
11
|
+
if: github.actor == 'dependabot[bot]'
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: dependabot/fetch-metadata@v2
|
|
15
|
+
id: meta
|
|
16
|
+
|
|
17
|
+
- name: Auto-merge patch updates
|
|
18
|
+
if: steps.meta.outputs.update-type == 'version-update:semver-patch'
|
|
19
|
+
run: gh pr merge "$PR_URL" --auto --squash
|
|
20
|
+
env:
|
|
21
|
+
PR_URL: ${{ github.event.pull_request.html_url }}
|
|
22
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
notes:
|
|
7
|
+
description: "Release notes (optional — auto-generated if blank)"
|
|
8
|
+
required: false
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
release:
|
|
15
|
+
name: Tag & Release
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
|
|
22
|
+
- name: Generate date-based tag
|
|
23
|
+
id: tag
|
|
24
|
+
run: |
|
|
25
|
+
BASE="v$(date +%Y.%m.%d)"
|
|
26
|
+
EXISTING=$(git tag -l "${BASE}*" | wc -l | tr -d ' ')
|
|
27
|
+
if [ "$EXISTING" -eq "0" ]; then
|
|
28
|
+
echo "tag=${BASE}" >> "$GITHUB_OUTPUT"
|
|
29
|
+
else
|
|
30
|
+
echo "tag=${BASE}.${EXISTING}" >> "$GITHUB_OUTPUT"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
- name: Create GitHub Release
|
|
34
|
+
uses: softprops/action-gh-release@v2
|
|
35
|
+
with:
|
|
36
|
+
tag_name: ${{ steps.tag.outputs.tag }}
|
|
37
|
+
generate_release_notes: ${{ github.event.inputs.notes == '' }}
|
|
38
|
+
body: ${{ github.event.inputs.notes }}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"baseUrl": "
|
|
3
|
+
"baseUrl": "./src",
|
|
4
4
|
"paths": {
|
|
5
|
-
"@/*": ["
|
|
6
|
-
}
|
|
5
|
+
"@/*": ["./*"]
|
|
6
|
+
},
|
|
7
|
+
"jsx": "preserve"
|
|
7
8
|
},
|
|
8
|
-
"include": ["next.
|
|
9
|
-
"exclude": ["node_modules"]
|
|
9
|
+
"include": ["next.config.js"]
|
|
10
10
|
}
|
|
@@ -1,6 +1,95 @@
|
|
|
1
1
|
/** @type {import('next').NextConfig} */
|
|
2
2
|
const nextConfig = {
|
|
3
|
-
//
|
|
3
|
+
// Required for Railway deployment — produces a self-contained build
|
|
4
|
+
// at .next/standalone that can run without node_modules.
|
|
5
|
+
output: "standalone",
|
|
6
|
+
|
|
7
|
+
// Support workspace package resolution (including @techstream/quark-db which uses
|
|
8
|
+
// the Prisma driver-adapter pattern — pure JS, no native engine binary)
|
|
9
|
+
transpilePackages: [
|
|
10
|
+
"@techstream/quark-core",
|
|
11
|
+
"@techstream/quark-db",
|
|
12
|
+
"@techstream/quark-ui",
|
|
13
|
+
"@techstream/quark-jobs",
|
|
14
|
+
],
|
|
15
|
+
|
|
16
|
+
// Security headers
|
|
17
|
+
// NOTE: These are also applied by proxy.js for proxy-matched routes.
|
|
18
|
+
// Keeping them here as a fallback for routes the proxy doesn't match.
|
|
19
|
+
async headers() {
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
source: "/:path*",
|
|
23
|
+
headers: [
|
|
24
|
+
{
|
|
25
|
+
key: "X-DNS-Prefetch-Control",
|
|
26
|
+
value: "on",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "X-Frame-Options",
|
|
30
|
+
value: "SAMEORIGIN",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: "X-Content-Type-Options",
|
|
34
|
+
value: "nosniff",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "Referrer-Policy",
|
|
38
|
+
value: "strict-origin-when-cross-origin",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "Permissions-Policy",
|
|
42
|
+
value: "camera=(), microphone=(), geolocation=()",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "Content-Security-Policy",
|
|
46
|
+
value:
|
|
47
|
+
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self';",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Environment variables validation
|
|
55
|
+
env: {
|
|
56
|
+
APP_URL: process.env.APP_URL,
|
|
57
|
+
NEXTAUTH_URL: process.env.NEXTAUTH_URL || process.env.APP_URL,
|
|
58
|
+
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// Request body size limits (security)
|
|
62
|
+
experimental: {
|
|
63
|
+
// Limit request body size to prevent DoS attacks
|
|
64
|
+
// Default is 4MB, we're being explicit here
|
|
65
|
+
// Adjust based on your needs (e.g., larger for file uploads)
|
|
66
|
+
serverActions: {
|
|
67
|
+
bodySizeLimit: "2mb", // For Server Actions
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// API route configuration
|
|
72
|
+
async rewrites() {
|
|
73
|
+
return [];
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// Compiler options for production optimization
|
|
77
|
+
compiler: {
|
|
78
|
+
removeConsole:
|
|
79
|
+
process.env.NODE_ENV === "production"
|
|
80
|
+
? { exclude: ["error", "warn"] }
|
|
81
|
+
: false,
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// Image optimization configuration
|
|
85
|
+
images: {
|
|
86
|
+
domains: [],
|
|
87
|
+
formats: ["image/avif", "image/webp"],
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Production-only settings
|
|
91
|
+
poweredByHeader: false,
|
|
92
|
+
compress: true,
|
|
4
93
|
};
|
|
5
94
|
|
|
6
95
|
export default nextConfig;
|
|
@@ -16,18 +16,18 @@
|
|
|
16
16
|
"@techstream/quark-db": "workspace:*",
|
|
17
17
|
"@techstream/quark-jobs": "workspace:*",
|
|
18
18
|
"@techstream/quark-ui": "workspace:*",
|
|
19
|
-
"@prisma/client": "^7.
|
|
19
|
+
"@prisma/client": "^7.4.0",
|
|
20
20
|
"next": "16.1.6",
|
|
21
21
|
"next-auth": "5.0.0-beta.30",
|
|
22
22
|
"pg": "^8.18.0",
|
|
23
|
-
"react": "19.2.
|
|
24
|
-
"react-dom": "19.2.
|
|
23
|
+
"react": "19.2.4",
|
|
24
|
+
"react-dom": "19.2.4",
|
|
25
25
|
"zod": "^4.3.6"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"
|
|
28
|
+
"@tailwindcss/postcss": "^4.2.0",
|
|
29
|
+
"@types/node": "^25.2.3",
|
|
30
|
+
"tailwindcss": "^4.2.0",
|
|
31
|
+
"@techstream/quark-config": "workspace:*"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://railway.com/railway.schema.json",
|
|
3
|
+
"build": {
|
|
4
|
+
"builder": "RAILPACK",
|
|
5
|
+
"buildCommand": "pnpm install --frozen-lockfile && pnpm db:generate && pnpm build",
|
|
6
|
+
"watchPatterns": ["apps/web/**", "packages/**"]
|
|
7
|
+
},
|
|
8
|
+
"deploy": {
|
|
9
|
+
"startCommand": "node apps/web/.next/standalone/server.js",
|
|
10
|
+
"healthcheckPath": "/api/health",
|
|
11
|
+
"healthcheckTimeout": 30,
|
|
12
|
+
"restartPolicyType": "ON_FAILURE",
|
|
13
|
+
"restartPolicyMaxRetries": 5
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -35,20 +35,19 @@ export const POST = withCsrfProtection(async (request) => {
|
|
|
35
35
|
password: hashedPassword,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// Don't return the password
|
|
39
|
+
const { password: _, ...safeUser } = newUser;
|
|
40
|
+
|
|
41
|
+
// Enqueue welcome email (fire-and-forget — don't block the response)
|
|
39
42
|
try {
|
|
40
43
|
const emailQueue = createQueue(JOB_QUEUES.EMAIL);
|
|
41
44
|
await emailQueue.add(JOB_NAMES.SEND_WELCOME_EMAIL, {
|
|
42
45
|
userId: newUser.id,
|
|
43
46
|
});
|
|
44
|
-
} catch
|
|
45
|
-
//
|
|
46
|
-
console.error("Failed to enqueue welcome email:", emailError);
|
|
47
|
+
} catch {
|
|
48
|
+
// Non-critical — user is created even if email fails to enqueue
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
// Don't return the password
|
|
50
|
-
const { password: _, ...safeUser } = newUser;
|
|
51
|
-
|
|
52
51
|
return NextResponse.json(safeUser, { status: 201 });
|
|
53
52
|
} catch (error) {
|
|
54
53
|
return handleError(error);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
1
|
+
import { getSiteMetadata } from "../lib/seo/site-metadata.js";
|
|
2
|
+
|
|
3
|
+
export const metadata = getSiteMetadata();
|
|
5
4
|
|
|
6
5
|
export default function RootLayout({ children }) {
|
|
7
6
|
return (
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { config, getAppUrl } from "@techstream/quark-config";
|
|
2
|
+
|
|
3
|
+
export default function manifest() {
|
|
4
|
+
return {
|
|
5
|
+
name: config.appName,
|
|
6
|
+
short_name: config.appName,
|
|
7
|
+
description: config.appDescription,
|
|
8
|
+
start_url: "/",
|
|
9
|
+
display: "standalone",
|
|
10
|
+
id: getAppUrl(),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getAppUrl } from "@techstream/quark-config";
|
|
2
|
+
import { isWebsiteIndexable } from "../lib/seo/indexing.js";
|
|
3
|
+
|
|
4
|
+
export default function robots() {
|
|
5
|
+
const appUrl = getAppUrl();
|
|
6
|
+
const indexable = isWebsiteIndexable();
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
rules: indexable
|
|
10
|
+
? {
|
|
11
|
+
userAgent: "*",
|
|
12
|
+
allow: "/",
|
|
13
|
+
disallow: ["/api/"],
|
|
14
|
+
}
|
|
15
|
+
: {
|
|
16
|
+
userAgent: "*",
|
|
17
|
+
disallow: "/",
|
|
18
|
+
},
|
|
19
|
+
sitemap: indexable ? `${appUrl}/sitemap.xml` : undefined,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getAppUrl } from "@techstream/quark-config";
|
|
2
|
+
import { isWebsiteIndexable } from "../lib/seo/indexing.js";
|
|
3
|
+
|
|
4
|
+
const STATIC_ROUTES = [{ path: "/", changeFrequency: "daily", priority: 1 }];
|
|
5
|
+
|
|
6
|
+
export default function sitemap() {
|
|
7
|
+
if (!isWebsiteIndexable()) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const appUrl = getAppUrl();
|
|
12
|
+
const lastModified = new Date();
|
|
13
|
+
|
|
14
|
+
return STATIC_ROUTES.map((route) => ({
|
|
15
|
+
url: `${appUrl}${route.path}`,
|
|
16
|
+
lastModified,
|
|
17
|
+
changeFrequency: route.changeFrequency,
|
|
18
|
+
priority: route.priority,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function isWebsiteIndexable(env = process.env) {
|
|
2
|
+
return (env.NODE_ENV || "").toLowerCase() === "production";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getMetadataRobots(env = process.env) {
|
|
6
|
+
if (isWebsiteIndexable(env)) {
|
|
7
|
+
return {
|
|
8
|
+
index: true,
|
|
9
|
+
follow: true,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
index: false,
|
|
15
|
+
follow: false,
|
|
16
|
+
nocache: true,
|
|
17
|
+
googleBot: {
|
|
18
|
+
index: false,
|
|
19
|
+
follow: false,
|
|
20
|
+
noimageindex: true,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { config, getAppUrl } from "@techstream/quark-config";
|
|
2
|
+
import { getMetadataRobots } from "./indexing.js";
|
|
3
|
+
|
|
4
|
+
const appUrl = getAppUrl();
|
|
5
|
+
const { appName, appDescription } = config;
|
|
6
|
+
|
|
7
|
+
export function getSiteMetadata() {
|
|
8
|
+
return {
|
|
9
|
+
metadataBase: new URL(appUrl),
|
|
10
|
+
title: {
|
|
11
|
+
default: appName,
|
|
12
|
+
template: `%s | ${appName}`,
|
|
13
|
+
},
|
|
14
|
+
description: appDescription,
|
|
15
|
+
applicationName: appName,
|
|
16
|
+
alternates: {
|
|
17
|
+
canonical: "/",
|
|
18
|
+
},
|
|
19
|
+
openGraph: {
|
|
20
|
+
type: "website",
|
|
21
|
+
url: appUrl,
|
|
22
|
+
title: appName,
|
|
23
|
+
description: appDescription,
|
|
24
|
+
siteName: appName,
|
|
25
|
+
},
|
|
26
|
+
twitter: {
|
|
27
|
+
card: "summary",
|
|
28
|
+
title: appName,
|
|
29
|
+
description: appDescription,
|
|
30
|
+
},
|
|
31
|
+
robots: getMetadataRobots(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -151,8 +151,7 @@ export function proxy(request) {
|
|
|
151
151
|
|
|
152
152
|
// Apply rate limiting to API routes only
|
|
153
153
|
if (pathname.startsWith("/api/")) {
|
|
154
|
-
const ip =
|
|
155
|
-
request.ip || request.headers.get("x-forwarded-for") || "unknown";
|
|
154
|
+
const ip = request.ip || "unknown";
|
|
156
155
|
const rateLimitResult = checkRateLimit(ip, pathname);
|
|
157
156
|
|
|
158
157
|
if (rateLimitResult.limited) {
|