@techstream/quark-create-app 1.5.3 → 1.6.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 +52 -9
- 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 +86 -1
- package/templates/base-project/apps/web/package.json +4 -4
- 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 +4 -4
- package/templates/base-project/apps/worker/src/index.js +26 -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 +3 -2
- package/templates/base-project/packages/db/scripts/seed.js +117 -30
- 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 +10 -18
- package/templates/config/src/load-config.js +135 -0
- package/templates/config/src/validate-env.js +60 -2
- 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.6.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,7 +474,11 @@ 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
|
|
@@ -492,13 +496,30 @@ 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
504
|
# Optional: Set MAIL_SMTP_URL to override the dynamic construction above
|
|
500
505
|
# MAIL_SMTP_URL="smtp://localhost:1025"
|
|
501
506
|
|
|
507
|
+
# Production SMTP: Set these instead of MAIL_* when using a real SMTP relay
|
|
508
|
+
# SMTP_HOST=smtp.example.com
|
|
509
|
+
# SMTP_PORT=587
|
|
510
|
+
# SMTP_SECURE=true
|
|
511
|
+
# SMTP_USER=your_smtp_user
|
|
512
|
+
# SMTP_PASSWORD=your_smtp_password
|
|
513
|
+
|
|
514
|
+
# --- Email Provider ---
|
|
515
|
+
# Provider: "smtp" (default) or "resend"
|
|
516
|
+
# EMAIL_PROVIDER=smtp
|
|
517
|
+
# EMAIL_FROM=App Name <noreply@yourdomain.com>
|
|
518
|
+
|
|
519
|
+
# Resend (only when EMAIL_PROVIDER=resend)
|
|
520
|
+
# Get your API key at: https://resend.com/api-keys
|
|
521
|
+
# RESEND_API_KEY=re_xxxxxxxxxxxxx
|
|
522
|
+
|
|
502
523
|
# --- Application URL ---
|
|
503
524
|
# In development, APP_URL is derived automatically from PORT — no need to set it.
|
|
504
525
|
# In production, set this to your real domain:
|
|
@@ -524,23 +545,36 @@ PORT=3000
|
|
|
524
545
|
# --- Worker Configuration ---
|
|
525
546
|
WORKER_CONCURRENCY=5
|
|
526
547
|
|
|
527
|
-
# --- File Storage
|
|
528
|
-
#
|
|
548
|
+
# --- File Storage ---
|
|
549
|
+
# Provider: "local" (default) or "s3" (S3-compatible: AWS S3, Cloudflare R2, MinIO)
|
|
529
550
|
STORAGE_PROVIDER=local
|
|
530
|
-
# Local storage directory (only
|
|
551
|
+
# Local storage directory (only when STORAGE_PROVIDER=local)
|
|
531
552
|
# STORAGE_LOCAL_DIR=./uploads
|
|
532
553
|
|
|
533
|
-
# S3 / Cloudflare R2
|
|
554
|
+
# S3 / Cloudflare R2 (only when STORAGE_PROVIDER=s3)
|
|
534
555
|
# S3_BUCKET=your-bucket-name
|
|
535
|
-
# S3_REGION=auto
|
|
556
|
+
# S3_REGION=auto # Use "auto" for Cloudflare R2
|
|
536
557
|
# S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
|
|
537
558
|
# S3_ACCESS_KEY_ID=your-access-key
|
|
538
559
|
# S3_SECRET_ACCESS_KEY=your-secret-key
|
|
539
560
|
# S3_PUBLIC_URL=https://your-public-bucket-domain.com
|
|
540
561
|
|
|
541
562
|
# --- Upload Limits ---
|
|
542
|
-
# UPLOAD_MAX_SIZE=10485760
|
|
563
|
+
# UPLOAD_MAX_SIZE=10485760 # Max file size in bytes (default: 10MB)
|
|
543
564
|
# UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif,image/webp,image/avif,image/svg+xml,application/pdf
|
|
565
|
+
|
|
566
|
+
# --- Rate Limiting & Security ---
|
|
567
|
+
# RATE_LIMIT_MAX=100 # Max requests per window (default: 1000 dev, 100 prod)
|
|
568
|
+
# RATE_LIMIT_WINDOW_MS=900000 # Window in ms (default: 15 minutes)
|
|
569
|
+
# API_BODY_SIZE_LIMIT=2097152 # 2MB (default)
|
|
570
|
+
# UPLOAD_SIZE_LIMIT=10485760 # 10MB proxy-level limit (default)
|
|
571
|
+
|
|
572
|
+
# --- Logging & Cache ---
|
|
573
|
+
# LOG_LEVEL=debug # debug, info, warn, error (default: debug dev, info prod)
|
|
574
|
+
# CACHE_TTL=60 # Default cache TTL in seconds (default: 60 dev, 600 prod)
|
|
575
|
+
|
|
576
|
+
# --- Database Seeding ---
|
|
577
|
+
# SEED_PROFILE=dev # Options: dev (default, includes audit logs + sample job), minimal (users only)
|
|
544
578
|
`;
|
|
545
579
|
await fs.writeFile(
|
|
546
580
|
path.join(targetDir, ".env.example"),
|
|
@@ -609,6 +643,9 @@ WORKER_CONCURRENCY=5
|
|
|
609
643
|
|
|
610
644
|
# --- File Storage ---
|
|
611
645
|
STORAGE_PROVIDER=local
|
|
646
|
+
|
|
647
|
+
# --- Database Seeding ---
|
|
648
|
+
# SEED_PROFILE=dev # Options: dev (default), minimal (users only — use for production initial seed)
|
|
612
649
|
`;
|
|
613
650
|
await fs.writeFile(path.join(targetDir, ".env"), envContent);
|
|
614
651
|
console.log(
|
|
@@ -725,7 +762,13 @@ STORAGE_PROVIDER=local
|
|
|
725
762
|
console.log(chalk.white(` 1. cd ${projectName}`));
|
|
726
763
|
console.log(chalk.white(` 2. docker compose up -d`));
|
|
727
764
|
console.log(chalk.white(` 3. pnpm db:migrate`));
|
|
728
|
-
console.log(chalk.white(` 4. pnpm
|
|
765
|
+
console.log(chalk.white(` 4. pnpm db:seed`));
|
|
766
|
+
console.log(chalk.white(` 5. pnpm dev\n`));
|
|
767
|
+
console.log(
|
|
768
|
+
chalk.dim(
|
|
769
|
+
` Tip: set SEED_PROFILE=minimal in .env for a lean seed (admin user only)\n`,
|
|
770
|
+
),
|
|
771
|
+
);
|
|
729
772
|
|
|
730
773
|
console.log(chalk.cyan("Important:"));
|
|
731
774
|
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,91 @@
|
|
|
1
1
|
/** @type {import('next').NextConfig} */
|
|
2
2
|
const nextConfig = {
|
|
3
|
-
//
|
|
3
|
+
// Support workspace package resolution (including @techstream/quark-db which uses
|
|
4
|
+
// the Prisma driver-adapter pattern — pure JS, no native engine binary)
|
|
5
|
+
transpilePackages: [
|
|
6
|
+
"@techstream/quark-core",
|
|
7
|
+
"@techstream/quark-db",
|
|
8
|
+
"@techstream/quark-ui",
|
|
9
|
+
"@techstream/quark-jobs",
|
|
10
|
+
],
|
|
11
|
+
|
|
12
|
+
// Security headers
|
|
13
|
+
// NOTE: These are also applied by proxy.js for proxy-matched routes.
|
|
14
|
+
// Keeping them here as a fallback for routes the proxy doesn't match.
|
|
15
|
+
async headers() {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
source: "/:path*",
|
|
19
|
+
headers: [
|
|
20
|
+
{
|
|
21
|
+
key: "X-DNS-Prefetch-Control",
|
|
22
|
+
value: "on",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "X-Frame-Options",
|
|
26
|
+
value: "SAMEORIGIN",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "X-Content-Type-Options",
|
|
30
|
+
value: "nosniff",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: "Referrer-Policy",
|
|
34
|
+
value: "strict-origin-when-cross-origin",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "Permissions-Policy",
|
|
38
|
+
value: "camera=(), microphone=(), geolocation=()",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "Content-Security-Policy",
|
|
42
|
+
value:
|
|
43
|
+
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self';",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Environment variables validation
|
|
51
|
+
env: {
|
|
52
|
+
APP_URL: process.env.APP_URL,
|
|
53
|
+
NEXTAUTH_URL: process.env.NEXTAUTH_URL || process.env.APP_URL,
|
|
54
|
+
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Request body size limits (security)
|
|
58
|
+
experimental: {
|
|
59
|
+
// Limit request body size to prevent DoS attacks
|
|
60
|
+
// Default is 4MB, we're being explicit here
|
|
61
|
+
// Adjust based on your needs (e.g., larger for file uploads)
|
|
62
|
+
serverActions: {
|
|
63
|
+
bodySizeLimit: "2mb", // For Server Actions
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// API route configuration
|
|
68
|
+
async rewrites() {
|
|
69
|
+
return [];
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Compiler options for production optimization
|
|
73
|
+
compiler: {
|
|
74
|
+
removeConsole:
|
|
75
|
+
process.env.NODE_ENV === "production"
|
|
76
|
+
? { exclude: ["error", "warn"] }
|
|
77
|
+
: false,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Image optimization configuration
|
|
81
|
+
images: {
|
|
82
|
+
domains: [],
|
|
83
|
+
formats: ["image/avif", "image/webp"],
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Production-only settings
|
|
87
|
+
poweredByHeader: false,
|
|
88
|
+
compress: true,
|
|
4
89
|
};
|
|
5
90
|
|
|
6
91
|
export default nextConfig;
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"zod": "^4.3.6"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"
|
|
28
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
29
|
+
"@types/node": "^20.19.33",
|
|
30
|
+
"tailwindcss": "^4.1.18",
|
|
31
|
+
"@techstream/quark-config": "workspace:*"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -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) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techstream/quark-worker",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"type": "module",
|
|
5
4
|
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
6
|
"description": "",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"scripts": {
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"@techstream/quark-core": "^1.0.0",
|
|
19
19
|
"@techstream/quark-db": "workspace:*",
|
|
20
20
|
"@techstream/quark-jobs": "workspace:*",
|
|
21
|
-
"bullmq": "^5.
|
|
21
|
+
"bullmq": "^5.67.3"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@techstream/quark-config": "workspace:*",
|
|
25
|
-
"@types/node": "^24.10.
|
|
26
|
-
"tsx": "^4.
|
|
25
|
+
"@types/node": "^24.10.12",
|
|
26
|
+
"tsx": "^4.21.0"
|
|
27
27
|
}
|
|
28
28
|
}
|