create-stackr 0.2.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/LICENSE +21 -0
- package/README.md +642 -0
- package/bin/cli.js +12 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/dependencies.d.ts +82 -0
- package/dist/config/dependencies.d.ts.map +1 -0
- package/dist/config/dependencies.js +82 -0
- package/dist/config/dependencies.js.map +1 -0
- package/dist/config/presets.d.ts +3 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/presets.js +174 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/generators/index.d.ts +40 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +130 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/onboarding.d.ts +8 -0
- package/dist/generators/onboarding.d.ts.map +1 -0
- package/dist/generators/onboarding.js +141 -0
- package/dist/generators/onboarding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/features.d.ts +14 -0
- package/dist/prompts/features.d.ts.map +1 -0
- package/dist/prompts/features.js +96 -0
- package/dist/prompts/features.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +93 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/onboarding.d.ts +6 -0
- package/dist/prompts/onboarding.d.ts.map +1 -0
- package/dist/prompts/onboarding.js +37 -0
- package/dist/prompts/onboarding.js.map +1 -0
- package/dist/prompts/orm.d.ts +3 -0
- package/dist/prompts/orm.d.ts.map +1 -0
- package/dist/prompts/orm.js +23 -0
- package/dist/prompts/orm.js.map +1 -0
- package/dist/prompts/packageManager.d.ts +2 -0
- package/dist/prompts/packageManager.d.ts.map +1 -0
- package/dist/prompts/packageManager.js +18 -0
- package/dist/prompts/packageManager.js.map +1 -0
- package/dist/prompts/platform.d.ts +3 -0
- package/dist/prompts/platform.d.ts.map +1 -0
- package/dist/prompts/platform.js +21 -0
- package/dist/prompts/platform.js.map +1 -0
- package/dist/prompts/preset.d.ts +4 -0
- package/dist/prompts/preset.d.ts.map +1 -0
- package/dist/prompts/preset.js +165 -0
- package/dist/prompts/preset.js.map +1 -0
- package/dist/prompts/project.d.ts +2 -0
- package/dist/prompts/project.d.ts.map +1 -0
- package/dist/prompts/project.js +27 -0
- package/dist/prompts/project.js.map +1 -0
- package/dist/prompts/sdks.d.ts +2 -0
- package/dist/prompts/sdks.d.ts.map +1 -0
- package/dist/prompts/sdks.js +46 -0
- package/dist/prompts/sdks.js.map +1 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cleanup.d.ts +5 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +38 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/copy.d.ts +10 -0
- package/dist/utils/copy.d.ts.map +1 -0
- package/dist/utils/copy.js +53 -0
- package/dist/utils/copy.js.map +1 -0
- package/dist/utils/errors.d.ts +33 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +136 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +33 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package.d.ts +16 -0
- package/dist/utils/package.d.ts.map +1 -0
- package/dist/utils/package.js +86 -0
- package/dist/utils/package.js.map +1 -0
- package/dist/utils/system-validation.d.ts +9 -0
- package/dist/utils/system-validation.d.ts.map +1 -0
- package/dist/utils/system-validation.js +31 -0
- package/dist/utils/system-validation.js.map +1 -0
- package/dist/utils/template.d.ts +20 -0
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +234 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +94 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +96 -0
- package/templates/base/backend/.dockerignore.ejs +62 -0
- package/templates/base/backend/.env.example.ejs +116 -0
- package/templates/base/backend/Dockerfile.ejs +142 -0
- package/templates/base/backend/controllers/event-queue/index.ts +20 -0
- package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
- package/templates/base/backend/controllers/rest-api/index.ts +48 -0
- package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
- package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
- package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
- package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
- package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
- package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
- package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
- package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
- package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
- package/templates/base/backend/domain/device-session/schema.ts +72 -0
- package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
- package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
- package/templates/base/backend/domain/session/schema.ts +29 -0
- package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
- package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
- package/templates/base/backend/domain/user/schema.ts +14 -0
- package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
- package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
- package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
- package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
- package/templates/base/backend/lib/constants.ts.ejs +29 -0
- package/templates/base/backend/package.json.ejs +50 -0
- package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
- package/templates/base/backend/prisma.config.prisma.ts +12 -0
- package/templates/base/backend/tsconfig.json +39 -0
- package/templates/base/backend/utils/db.drizzle.ts +41 -0
- package/templates/base/backend/utils/db.prisma.ts +51 -0
- package/templates/base/backend/utils/email.ts.ejs +35 -0
- package/templates/base/backend/utils/errors.ts +348 -0
- package/templates/base/backend/utils/redis.ts.ejs +279 -0
- package/templates/base/mobile/.env.example.ejs +35 -0
- package/templates/base/mobile/.gitignore.ejs +167 -0
- package/templates/base/mobile/app/+not-found.tsx +85 -0
- package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
- package/templates/base/mobile/app.json.ejs +88 -0
- package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
- package/templates/base/mobile/assets/images/favicon.png +0 -0
- package/templates/base/mobile/assets/images/icon.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
- package/templates/base/mobile/assets/images/paywall_image.png +0 -0
- package/templates/base/mobile/assets/images/splash.png +0 -0
- package/templates/base/mobile/eas.json.ejs +49 -0
- package/templates/base/mobile/metro.config.js +9 -0
- package/templates/base/mobile/package.json.ejs +53 -0
- package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
- package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
- package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
- package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
- package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
- package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
- package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
- package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
- package/templates/base/mobile/src/components/ui/index.ts +6 -0
- package/templates/base/mobile/src/constants/Theme.ts +163 -0
- package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
- package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
- package/templates/base/mobile/src/services/api.ts.ejs +71 -0
- package/templates/base/mobile/src/services/errorService.ts +179 -0
- package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
- package/templates/base/mobile/src/store/index.ts.ejs +18 -0
- package/templates/base/mobile/src/store/ui.store.ts +100 -0
- package/templates/base/mobile/src/utils/formatters.ts +105 -0
- package/templates/base/mobile/src/utils/logger.ts +73 -0
- package/templates/base/mobile/src/utils/responsive.ts +234 -0
- package/templates/base/mobile/tsconfig.json +32 -0
- package/templates/base/web/.env.example.ejs +26 -0
- package/templates/base/web/components.json +22 -0
- package/templates/base/web/eslint.config.mjs +18 -0
- package/templates/base/web/next.config.ts +7 -0
- package/templates/base/web/package.json.ejs +35 -0
- package/templates/base/web/postcss.config.mjs +7 -0
- package/templates/base/web/public/.gitkeep +0 -0
- package/templates/base/web/public/file.svg +1 -0
- package/templates/base/web/public/globe.svg +1 -0
- package/templates/base/web/public/next.svg +1 -0
- package/templates/base/web/public/vercel.svg +1 -0
- package/templates/base/web/public/window.svg +1 -0
- package/templates/base/web/src/app/favicon.ico +0 -0
- package/templates/base/web/src/app/globals.css +152 -0
- package/templates/base/web/src/app/layout.tsx.ejs +54 -0
- package/templates/base/web/src/app/page.tsx.ejs +92 -0
- package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
- package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
- package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
- package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
- package/templates/base/web/src/components/theme-toggle.tsx +34 -0
- package/templates/base/web/src/components/ui/button.tsx +62 -0
- package/templates/base/web/src/components/ui/card.tsx +92 -0
- package/templates/base/web/src/components/ui/input.tsx +21 -0
- package/templates/base/web/src/components/ui/label.tsx +24 -0
- package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/web/src/components/ui/spinner.tsx +20 -0
- package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
- package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
- package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
- package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
- package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
- package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
- package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
- package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
- package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
- package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
- package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
- package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
- package/templates/base/web/src/lib/utils.ts +6 -0
- package/templates/base/web/src/proxy.ts.ejs +66 -0
- package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
- package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
- package/templates/base/web/tsconfig.json +34 -0
- package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
- package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
- package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
- package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
- package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
- package/templates/features/mobile/auth/components/auth/index.ts +2 -0
- package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
- package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
- package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
- package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
- package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
- package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
- package/templates/features/web/.gitkeep +0 -0
- package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
- package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
- package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
- package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
- package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
- package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
- package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
- package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
- package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
- package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
- package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
- package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
- package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
- package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
- package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
- package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
- package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
- package/templates/integrations/mobile/att/services/attService.ts +84 -0
- package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
- package/templates/integrations/mobile/att/store/att.store.ts +162 -0
- package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
- package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
- package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
- package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
- package/templates/integrations/web/.gitkeep +0 -0
- package/templates/shared/.env.example.ejs +21 -0
- package/templates/shared/.gitignore.ejs +145 -0
- package/templates/shared/README.md.ejs +134 -0
- package/templates/shared/docker-compose.prod.yml.ejs +120 -0
- package/templates/shared/docker-compose.yml.ejs +129 -0
- package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
- package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
- package/templates/shared/scripts/setup.sh.ejs +979 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Simplified Multi-stage Dockerfile for Node.js/Fastify Backend
|
|
2
|
+
# Clean architecture with explicit service stages
|
|
3
|
+
# Supports npm, yarn, and bun package managers
|
|
4
|
+
|
|
5
|
+
# =============================================================================
|
|
6
|
+
# Base Stage - Common setup for all services
|
|
7
|
+
# =============================================================================
|
|
8
|
+
<% if (packageManager === 'bun') { %>FROM oven/bun:alpine AS base<% } else { %>FROM node:alpine AS base<% } %>
|
|
9
|
+
|
|
10
|
+
# Install essential packages
|
|
11
|
+
RUN apk --no-cache add dumb-init ca-certificates
|
|
12
|
+
|
|
13
|
+
# Set working directory
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
|
|
16
|
+
# Copy package files and lock file for dependency installation
|
|
17
|
+
COPY package*.json ./
|
|
18
|
+
<% if (packageManager === 'bun') { %>COPY bun.lock* ./<% } else if (packageManager === 'npm') { %>COPY package-lock.json ./<% } else { %>COPY yarn.lock ./<% } %>
|
|
19
|
+
<% if (backend.orm === 'prisma') { %>
|
|
20
|
+
COPY prisma/schema.prisma ./prisma/
|
|
21
|
+
COPY prisma.config.ts ./
|
|
22
|
+
<% } else if (backend.orm === 'drizzle') { %>
|
|
23
|
+
COPY drizzle/schema.ts ./drizzle/
|
|
24
|
+
COPY drizzle.config.ts ./
|
|
25
|
+
<% } %>
|
|
26
|
+
COPY .env ./
|
|
27
|
+
|
|
28
|
+
# Install dependencies and generate ORM client
|
|
29
|
+
<% if (packageManager === 'bun') { %>RUN bun install && bun run db:generate<% } else if (packageManager === 'npm') { %>RUN npm ci && npm run db:generate<% } else { %>RUN yarn install --frozen-lockfile && yarn db:generate<% } %>
|
|
30
|
+
|
|
31
|
+
# Copy source code
|
|
32
|
+
COPY . .
|
|
33
|
+
|
|
34
|
+
# =============================================================================
|
|
35
|
+
# Development Stages - For local development with hot reload
|
|
36
|
+
# =============================================================================
|
|
37
|
+
FROM base AS rest-api-dev
|
|
38
|
+
EXPOSE 8080
|
|
39
|
+
<% if (packageManager === 'bun') { %>CMD ["dumb-init", "bun", "run", "dev:rest-api"]<% } else if (packageManager === 'npm') { %>CMD ["dumb-init", "npm", "run", "dev:rest-api"]<% } else { %>CMD ["dumb-init", "yarn", "dev:rest-api"]<% } %>
|
|
40
|
+
|
|
41
|
+
FROM base AS event-queue-dev
|
|
42
|
+
<% if (packageManager === 'bun') { %>CMD ["dumb-init", "bun", "run", "dev:event-queue"]<% } else if (packageManager === 'npm') { %>CMD ["dumb-init", "npm", "run", "dev:event-queue"]<% } else { %>CMD ["dumb-init", "yarn", "dev:event-queue"]<% } %>
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Production Stages - Optimized for production deployment
|
|
46
|
+
# =============================================================================
|
|
47
|
+
<% if (packageManager === 'bun') { %>FROM oven/bun:alpine AS rest-api-prod<% } else { %>FROM node:alpine AS rest-api-prod<% } %>
|
|
48
|
+
|
|
49
|
+
# Install essential packages and create non-root user
|
|
50
|
+
RUN apk --no-cache add dumb-init ca-certificates \
|
|
51
|
+
<% if (packageManager === 'bun') { %> && addgroup -g 1001 -S bunjs \
|
|
52
|
+
&& adduser -S backend -u 1001 -G bunjs<% } else { %> && addgroup -g 1001 -S nodejs \
|
|
53
|
+
&& adduser -S backend -u 1001 -G nodejs<% } %>
|
|
54
|
+
|
|
55
|
+
# Set production environment
|
|
56
|
+
ENV NODE_ENV=production
|
|
57
|
+
WORKDIR /app
|
|
58
|
+
|
|
59
|
+
# Copy package files and lock file first (for better layer caching)
|
|
60
|
+
COPY package*.json ./
|
|
61
|
+
<% if (packageManager === 'bun') { %>COPY bun.lock* ./<% } else if (packageManager === 'npm') { %>COPY package-lock.json ./<% } else { %>COPY yarn.lock ./<% } %>
|
|
62
|
+
|
|
63
|
+
# Install production dependencies only (clean install)
|
|
64
|
+
<% if (packageManager === 'bun') { %>RUN bun install --production<% } else if (packageManager === 'npm') { %>RUN npm ci --production<% } else { %>RUN yarn install --production --frozen-lockfile<% } %>
|
|
65
|
+
|
|
66
|
+
# Copy ORM schema, config, and generate client
|
|
67
|
+
<% if (backend.orm === 'prisma') { %>
|
|
68
|
+
COPY prisma/schema.prisma ./prisma/
|
|
69
|
+
COPY prisma.config.ts ./
|
|
70
|
+
<% } else if (backend.orm === 'drizzle') { %>
|
|
71
|
+
COPY drizzle/schema.ts ./drizzle/
|
|
72
|
+
COPY drizzle.config.ts ./
|
|
73
|
+
<% } %>
|
|
74
|
+
COPY .env ./
|
|
75
|
+
<% if (packageManager === 'bun') { %>RUN bun run db:generate<% } else if (packageManager === 'npm') { %>RUN npm run db:generate<% } else { %>RUN yarn db:generate<% } %>
|
|
76
|
+
|
|
77
|
+
# Copy source code
|
|
78
|
+
COPY . .
|
|
79
|
+
|
|
80
|
+
# Build the TypeScript application
|
|
81
|
+
<% if (packageManager === 'bun') { %>RUN bun run build<% } else if (packageManager === 'npm') { %>RUN npm run build<% } else { %>RUN yarn build<% } %>
|
|
82
|
+
|
|
83
|
+
# Change ownership to non-root user
|
|
84
|
+
<% if (packageManager === 'bun') { %>RUN chown -R backend:bunjs /app<% } else { %>RUN chown -R backend:nodejs /app<% } %>
|
|
85
|
+
|
|
86
|
+
# Switch to non-root user
|
|
87
|
+
USER backend
|
|
88
|
+
|
|
89
|
+
# Expose port and add health check
|
|
90
|
+
EXPOSE 8080
|
|
91
|
+
<% if (packageManager === 'bun') { %>HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
92
|
+
CMD bun -e "const res = await fetch('http://localhost:8080/'); process.exit(res.ok ? 0 : 1)"<% } else { %>HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
93
|
+
CMD node -e "fetch('http://localhost:8080/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"<% } %>
|
|
94
|
+
|
|
95
|
+
# Start the REST API service
|
|
96
|
+
<% if (packageManager === 'bun') { %>CMD ["dumb-init", "bun", "run", "start:rest-api"]<% } else if (packageManager === 'npm') { %>CMD ["dumb-init", "npm", "run", "start:rest-api"]<% } else { %>CMD ["dumb-init", "yarn", "start:rest-api"]<% } %>
|
|
97
|
+
|
|
98
|
+
# =============================================================================
|
|
99
|
+
<% if (packageManager === 'bun') { %>FROM oven/bun:alpine AS event-queue-prod<% } else { %>FROM node:alpine AS event-queue-prod<% } %>
|
|
100
|
+
|
|
101
|
+
# Install essential packages and create non-root user
|
|
102
|
+
RUN apk --no-cache add dumb-init ca-certificates \
|
|
103
|
+
<% if (packageManager === 'bun') { %> && addgroup -g 1001 -S bunjs \
|
|
104
|
+
&& adduser -S backend -u 1001 -G bunjs<% } else { %> && addgroup -g 1001 -S nodejs \
|
|
105
|
+
&& adduser -S backend -u 1001 -G nodejs<% } %>
|
|
106
|
+
|
|
107
|
+
# Set production environment
|
|
108
|
+
ENV NODE_ENV=production
|
|
109
|
+
WORKDIR /app
|
|
110
|
+
|
|
111
|
+
# Copy package files and lock file first (for better layer caching)
|
|
112
|
+
COPY package*.json ./
|
|
113
|
+
<% if (packageManager === 'bun') { %>COPY bun.lock* ./<% } else if (packageManager === 'npm') { %>COPY package-lock.json ./<% } else { %>COPY yarn.lock ./<% } %>
|
|
114
|
+
|
|
115
|
+
# Install production dependencies only (clean install)
|
|
116
|
+
<% if (packageManager === 'bun') { %>RUN bun install --production<% } else if (packageManager === 'npm') { %>RUN npm ci --production<% } else { %>RUN yarn install --production --frozen-lockfile<% } %>
|
|
117
|
+
|
|
118
|
+
# Copy ORM schema, config, and generate client
|
|
119
|
+
<% if (backend.orm === 'prisma') { %>
|
|
120
|
+
COPY prisma/schema.prisma ./prisma/
|
|
121
|
+
COPY prisma.config.ts ./
|
|
122
|
+
<% } else if (backend.orm === 'drizzle') { %>
|
|
123
|
+
COPY drizzle/schema.ts ./drizzle/
|
|
124
|
+
COPY drizzle.config.ts ./
|
|
125
|
+
<% } %>
|
|
126
|
+
COPY .env ./
|
|
127
|
+
<% if (packageManager === 'bun') { %>RUN bun run db:generate<% } else if (packageManager === 'npm') { %>RUN npm run db:generate<% } else { %>RUN yarn db:generate<% } %>
|
|
128
|
+
|
|
129
|
+
# Copy source code
|
|
130
|
+
COPY . .
|
|
131
|
+
|
|
132
|
+
# Build the TypeScript application
|
|
133
|
+
<% if (packageManager === 'bun') { %>RUN bun run build<% } else if (packageManager === 'npm') { %>RUN npm run build<% } else { %>RUN yarn build<% } %>
|
|
134
|
+
|
|
135
|
+
# Change ownership to non-root user
|
|
136
|
+
<% if (packageManager === 'bun') { %>RUN chown -R backend:bunjs /app<% } else { %>RUN chown -R backend:nodejs /app<% } %>
|
|
137
|
+
|
|
138
|
+
# Switch to non-root user
|
|
139
|
+
USER backend
|
|
140
|
+
|
|
141
|
+
# Start the event queue service
|
|
142
|
+
<% if (packageManager === 'bun') { %>CMD ["dumb-init", "bun", "run", "start:event-queue"]<% } else if (packageManager === 'npm') { %>CMD ["dumb-init", "npm", "run", "start:event-queue"]<% } else { %>CMD ["dumb-init", "yarn", "start:event-queue"]<% } %>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
dotenv.config({ path: ".env" });
|
|
3
|
+
|
|
4
|
+
import { Redis } from "ioredis";
|
|
5
|
+
import { createUserWorker } from "./workers/user";
|
|
6
|
+
|
|
7
|
+
const redisConnection = new Redis({
|
|
8
|
+
host: process.env.REDIS_HOST || "localhost",
|
|
9
|
+
port: parseInt(process.env.REDIS_PORT || "6379"),
|
|
10
|
+
password: process.env.REDIS_PASSWORD || "",
|
|
11
|
+
maxRetriesPerRequest: null,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Create a account worker
|
|
15
|
+
const userWorker = createUserWorker(redisConnection, {
|
|
16
|
+
concurrency: 20,
|
|
17
|
+
autorun: true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
console.log("User worker started");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Worker, Job } from "bullmq";
|
|
2
|
+
import { Redis, Cluster as RedisCluster } from "ioredis";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* User Worker
|
|
6
|
+
*
|
|
7
|
+
* Note: User creation/authentication is handled by Better Auth.
|
|
8
|
+
* This worker handles async user-related tasks like sending welcome emails,
|
|
9
|
+
* syncing with external services, etc.
|
|
10
|
+
*/
|
|
11
|
+
export const createUserWorker = (
|
|
12
|
+
connection: Redis | RedisCluster,
|
|
13
|
+
options: {
|
|
14
|
+
concurrency?: number;
|
|
15
|
+
autorun?: boolean;
|
|
16
|
+
}
|
|
17
|
+
) => {
|
|
18
|
+
return new Worker(
|
|
19
|
+
"user",
|
|
20
|
+
async (job: Job) => {
|
|
21
|
+
switch (job.name) {
|
|
22
|
+
case "send_welcome_email":
|
|
23
|
+
// TODO: Implement welcome email sending
|
|
24
|
+
console.log(`Sending welcome email to user: ${job.data.userId}`);
|
|
25
|
+
break;
|
|
26
|
+
case "sync_user_profile":
|
|
27
|
+
// TODO: Sync user profile with external services
|
|
28
|
+
console.log(`Syncing profile for user: ${job.data.userId}`);
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
console.log(`Unknown job: ${job.name}`);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
connection,
|
|
36
|
+
...options,
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import server from "./server";
|
|
2
|
+
|
|
3
|
+
const start = async () => {
|
|
4
|
+
try {
|
|
5
|
+
const host = server.config.API_HOST || '0.0.0.0';
|
|
6
|
+
const port = parseInt(server.config.API_PORT || '8080');
|
|
7
|
+
|
|
8
|
+
await server.listen({
|
|
9
|
+
host,
|
|
10
|
+
port,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
console.log(`🚀 Server is running at http://${host}:${port}`);
|
|
14
|
+
console.log(`📚 API Documentation available at http://${host}:${port}/documentation (if enabled)`);
|
|
15
|
+
|
|
16
|
+
} catch (error) {
|
|
17
|
+
server.log.error(error);
|
|
18
|
+
console.error('❌ Failed to start server:', error);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Handle graceful shutdown
|
|
24
|
+
process.on('SIGINT', async () => {
|
|
25
|
+
console.log('\n🛑 Received SIGINT, gracefully shutting down...');
|
|
26
|
+
try {
|
|
27
|
+
await server.close();
|
|
28
|
+
console.log('✅ Server closed successfully');
|
|
29
|
+
process.exit(0);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('❌ Error during shutdown:', error);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
process.on('SIGTERM', async () => {
|
|
37
|
+
console.log('\n🛑 Received SIGTERM, gracefully shutting down...');
|
|
38
|
+
try {
|
|
39
|
+
await server.close();
|
|
40
|
+
console.log('✅ Server closed successfully');
|
|
41
|
+
process.exit(0);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('❌ Error during shutdown:', error);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
start();
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthFastifyRequest,
|
|
3
|
+
DeviceSessionFastifyRequest,
|
|
4
|
+
AuthOrDeviceSessionFastifyRequest,
|
|
5
|
+
} from "fastify";
|
|
6
|
+
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from "fastify";
|
|
7
|
+
import fp from "fastify-plugin";
|
|
8
|
+
import { auth, Session } from "../../../lib/auth";
|
|
9
|
+
import { validateDeviceSession, updateDeviceSessionActivity } from "../../../domain/device-session/repository";
|
|
10
|
+
import { DeviceSession } from "../../../domain/device-session/schema";
|
|
11
|
+
import { ErrorFactory, normalizeError } from "../../../utils/errors";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert Node.js/Fastify headers to Web API Headers object for BetterAuth
|
|
15
|
+
*/
|
|
16
|
+
function toHeaders(requestHeaders: FastifyRequest["headers"]): Headers {
|
|
17
|
+
const headers = new Headers();
|
|
18
|
+
Object.entries(requestHeaders).forEach(([key, value]) => {
|
|
19
|
+
if (value) headers.set(key, Array.isArray(value) ? value[0] : value);
|
|
20
|
+
});
|
|
21
|
+
return headers;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const authPlugin: FastifyPluginAsync = async (server) => {
|
|
25
|
+
// Authenticated user middleware (using BetterAuth session)
|
|
26
|
+
// Uses cookie-based authentication for both mobile and web clients
|
|
27
|
+
server.decorate(
|
|
28
|
+
"requireAuth",
|
|
29
|
+
async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
|
|
30
|
+
try {
|
|
31
|
+
// Validate session via BetterAuth (reads cookies from headers)
|
|
32
|
+
const session = await auth.api.getSession({
|
|
33
|
+
headers: toHeaders(request.headers),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!session || !session.user) {
|
|
37
|
+
throw ErrorFactory.unauthorized();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Attach to request
|
|
41
|
+
(request as AuthFastifyRequest).user = session.user;
|
|
42
|
+
(request as AuthFastifyRequest).session = session.session;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const appError = normalizeError(error);
|
|
45
|
+
const response = appError.toApiResponse(request.id, request.url);
|
|
46
|
+
return reply.status(appError.statusCode).send(response);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Anonymous device session middleware (for device-based sessions before auth)
|
|
52
|
+
server.decorate(
|
|
53
|
+
"requireDeviceSession",
|
|
54
|
+
async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
|
|
55
|
+
try {
|
|
56
|
+
const sessionTokenHeader = request.headers["x-device-session-token"] as string;
|
|
57
|
+
if (!sessionTokenHeader) {
|
|
58
|
+
throw ErrorFactory.tokenMissing();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const deviceSession = await validateDeviceSession(sessionTokenHeader);
|
|
62
|
+
if (!deviceSession) {
|
|
63
|
+
throw ErrorFactory.sessionNotFound();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await updateDeviceSessionActivity(sessionTokenHeader);
|
|
67
|
+
|
|
68
|
+
(request as DeviceSessionFastifyRequest).deviceSession = deviceSession;
|
|
69
|
+
(request as DeviceSessionFastifyRequest).sessionToken = sessionTokenHeader;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const appError = normalizeError(error);
|
|
72
|
+
const response = appError.toApiResponse(request.id, request.url);
|
|
73
|
+
return reply.status(appError.statusCode).send(response);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Flexible auth: accepts either BetterAuth session or device session
|
|
79
|
+
server.decorate(
|
|
80
|
+
"requireAuthOrDeviceSession",
|
|
81
|
+
async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
|
|
82
|
+
try {
|
|
83
|
+
// Try BetterAuth session first (cookie-based)
|
|
84
|
+
try {
|
|
85
|
+
const session = await auth.api.getSession({
|
|
86
|
+
headers: toHeaders(request.headers),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (session && session.user) {
|
|
90
|
+
(request as AuthOrDeviceSessionFastifyRequest).user = session.user;
|
|
91
|
+
(request as AuthOrDeviceSessionFastifyRequest).session = session.session;
|
|
92
|
+
(request as AuthOrDeviceSessionFastifyRequest).authType = "user";
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// BetterAuth session not found, try device session
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Try device session
|
|
100
|
+
const sessionTokenHeader = request.headers["x-device-session-token"] as string;
|
|
101
|
+
if (sessionTokenHeader) {
|
|
102
|
+
const deviceSession = await validateDeviceSession(sessionTokenHeader);
|
|
103
|
+
if (deviceSession) {
|
|
104
|
+
await updateDeviceSessionActivity(sessionTokenHeader);
|
|
105
|
+
(request as AuthOrDeviceSessionFastifyRequest).deviceSession = deviceSession;
|
|
106
|
+
(request as AuthOrDeviceSessionFastifyRequest).sessionToken = sessionTokenHeader;
|
|
107
|
+
(request as AuthOrDeviceSessionFastifyRequest).authType = "device";
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw ErrorFactory.unauthorized();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const appError = normalizeError(error);
|
|
115
|
+
const response = appError.toApiResponse(request.id, request.url);
|
|
116
|
+
return reply.status(appError.statusCode).send(response);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Extend Fastify module with auth types and middleware decorators
|
|
123
|
+
declare module "fastify" {
|
|
124
|
+
// For authenticated users (BetterAuth session)
|
|
125
|
+
export interface AuthFastifyRequest extends FastifyRequest {
|
|
126
|
+
user: Session["user"];
|
|
127
|
+
session: Session["session"];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// For device/anonymous sessions (before user authentication)
|
|
131
|
+
export interface DeviceSessionFastifyRequest extends FastifyRequest {
|
|
132
|
+
deviceSession: DeviceSession;
|
|
133
|
+
sessionToken: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// For endpoints that accept either auth type
|
|
137
|
+
export interface AuthOrDeviceSessionFastifyRequest extends FastifyRequest {
|
|
138
|
+
user?: Session["user"];
|
|
139
|
+
session?: Session["session"];
|
|
140
|
+
deviceSession?: DeviceSession;
|
|
141
|
+
sessionToken?: string;
|
|
142
|
+
authType: "user" | "device";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface FastifyInstance {
|
|
146
|
+
requireAuth: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
147
|
+
requireDeviceSession: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
148
|
+
requireAuthOrDeviceSession: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default fp(authPlugin);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import { FastifyPluginAsync } from "fastify";
|
|
3
|
+
import { Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import Ajv from "ajv";
|
|
5
|
+
|
|
6
|
+
import dotenv from "dotenv";
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
export enum NodeEnv {
|
|
10
|
+
development = "development",
|
|
11
|
+
test = "test",
|
|
12
|
+
production = "production",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ConfigSchema = Type.Object({
|
|
16
|
+
NODE_ENV: Type.Optional(Type.Enum(NodeEnv)),
|
|
17
|
+
LOG_LEVEL: Type.Optional(Type.String()),
|
|
18
|
+
API_HOST: Type.Optional(Type.String()),
|
|
19
|
+
API_PORT: Type.Optional(Type.String()),
|
|
20
|
+
DATABASE_URL: Type.String(),
|
|
21
|
+
JWT_SECRET: Type.Optional(Type.String()),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const ajv = new Ajv({
|
|
25
|
+
allErrors: true,
|
|
26
|
+
removeAdditional: true,
|
|
27
|
+
useDefaults: true,
|
|
28
|
+
coerceTypes: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type Config = Static<typeof ConfigSchema>;
|
|
32
|
+
|
|
33
|
+
const configPlugin: FastifyPluginAsync = async (server) => {
|
|
34
|
+
// Set defaults for optional fields
|
|
35
|
+
const configWithDefaults = {
|
|
36
|
+
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
37
|
+
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
|
38
|
+
API_HOST: process.env.API_HOST || '0.0.0.0',
|
|
39
|
+
API_PORT: process.env.API_PORT || '8080',
|
|
40
|
+
DATABASE_URL: process.env.DATABASE_URL,
|
|
41
|
+
JWT_SECRET: process.env.JWT_SECRET || 'your-jwt-secret-here-change-in-production',
|
|
42
|
+
...process.env,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const validate = ajv.compile(ConfigSchema);
|
|
46
|
+
const valid = validate(configWithDefaults);
|
|
47
|
+
|
|
48
|
+
if (!valid) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
".env file validation failed - " +
|
|
51
|
+
JSON.stringify(validate.errors, null, 2)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
server.decorate("config", configWithDefaults as Config);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
declare module "fastify" {
|
|
59
|
+
interface FastifyInstance {
|
|
60
|
+
config: Config;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default fp(configPlugin);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import { FastifyPluginAsync, FastifyError, FastifyRequest, FastifyReply } from "fastify";
|
|
3
|
+
import { AppError, normalizeError, shouldIncludeErrorDetails, ErrorFactory } from "../../../utils/errors";
|
|
4
|
+
|
|
5
|
+
const errorHandlerPlugin: FastifyPluginAsync = async (server) => {
|
|
6
|
+
// Global error handler
|
|
7
|
+
server.setErrorHandler(async (error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
|
|
8
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
9
|
+
|
|
10
|
+
// Log the error for debugging
|
|
11
|
+
server.log.error({
|
|
12
|
+
error: {
|
|
13
|
+
message: error.message,
|
|
14
|
+
stack: error.stack,
|
|
15
|
+
statusCode: error.statusCode,
|
|
16
|
+
validation: error.validation,
|
|
17
|
+
},
|
|
18
|
+
request: {
|
|
19
|
+
id: request.id,
|
|
20
|
+
method: request.method,
|
|
21
|
+
url: request.url,
|
|
22
|
+
headers: request.headers,
|
|
23
|
+
body: request.body,
|
|
24
|
+
},
|
|
25
|
+
}, 'Request error occurred');
|
|
26
|
+
|
|
27
|
+
let appError: AppError;
|
|
28
|
+
|
|
29
|
+
// Handle different types of errors
|
|
30
|
+
if (error instanceof AppError) {
|
|
31
|
+
// Already an AppError, use as-is
|
|
32
|
+
appError = error;
|
|
33
|
+
} else if (error.statusCode === 400 && error.validation) {
|
|
34
|
+
// Fastify validation error
|
|
35
|
+
appError = ErrorFactory.validationFailed({
|
|
36
|
+
fields: error.validation,
|
|
37
|
+
originalMessage: error.message
|
|
38
|
+
});
|
|
39
|
+
} else if (error.statusCode === 401) {
|
|
40
|
+
// Unauthorized error
|
|
41
|
+
appError = ErrorFactory.tokenInvalid({ originalMessage: error.message });
|
|
42
|
+
} else if (error.statusCode === 403) {
|
|
43
|
+
// Forbidden error
|
|
44
|
+
appError = ErrorFactory.permissionDenied();
|
|
45
|
+
} else if (error.statusCode === 404) {
|
|
46
|
+
// Not found error
|
|
47
|
+
appError = ErrorFactory.resourceNotFound('resource');
|
|
48
|
+
} else if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
|
|
49
|
+
// Other client errors
|
|
50
|
+
appError = ErrorFactory.clientError(
|
|
51
|
+
error.message || 'Bad request',
|
|
52
|
+
error.statusCode
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
// Server errors or unknown errors
|
|
56
|
+
appError = normalizeError(error);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Determine if we should include error details
|
|
60
|
+
const includeDetails = shouldIncludeErrorDetails(appError, isDevelopment);
|
|
61
|
+
|
|
62
|
+
// Create the response
|
|
63
|
+
const response = appError.toApiResponse(request.id, request.url);
|
|
64
|
+
|
|
65
|
+
// Remove details if they shouldn't be included
|
|
66
|
+
if (!includeDetails) {
|
|
67
|
+
delete response.error.details;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Add stack trace in development for critical errors
|
|
71
|
+
if (isDevelopment && appError.severity === 'critical') {
|
|
72
|
+
response.error.details = {
|
|
73
|
+
...response.error.details,
|
|
74
|
+
stack: error.stack
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Send the error response
|
|
79
|
+
return reply.status(appError.statusCode).send(response);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Handle 404 errors for routes that don't exist
|
|
83
|
+
server.setNotFoundHandler(async (request: FastifyRequest, reply: FastifyReply) => {
|
|
84
|
+
const appError = ErrorFactory.routeNotFound(request.method, request.url);
|
|
85
|
+
const response = appError.toApiResponse(request.id, request.url);
|
|
86
|
+
return reply.status(404).send(response);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Add request ID generation for better error tracking
|
|
90
|
+
server.addHook('onRequest', async (request) => {
|
|
91
|
+
// Generate a unique request ID if not already present
|
|
92
|
+
if (!request.id) {
|
|
93
|
+
request.id = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Add response time tracking
|
|
98
|
+
server.addHook('onRequest', async (request) => {
|
|
99
|
+
(request as any).startTime = Date.now();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
server.addHook('onResponse', async (request, reply) => {
|
|
103
|
+
const responseTime = Date.now() - ((request as any).startTime || Date.now());
|
|
104
|
+
server.log.info({
|
|
105
|
+
request: {
|
|
106
|
+
id: request.id,
|
|
107
|
+
method: request.method,
|
|
108
|
+
url: request.url,
|
|
109
|
+
},
|
|
110
|
+
response: {
|
|
111
|
+
statusCode: reply.statusCode,
|
|
112
|
+
responseTime: `${responseTime}ms`
|
|
113
|
+
}
|
|
114
|
+
}, 'Request completed');
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export default fp(errorHandlerPlugin);
|