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,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": [
|
|
5
|
+
"ES2022"
|
|
6
|
+
],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleResolution": "node",
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"allowJs": true,
|
|
12
|
+
"outDir": "./dist",
|
|
13
|
+
"rootDir": ".",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"declarationMap": true,
|
|
17
|
+
"incremental": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true,
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"isolatedModules": true,
|
|
22
|
+
"noEmit": false,
|
|
23
|
+
"paths": {
|
|
24
|
+
"@/*": [
|
|
25
|
+
"./*"
|
|
26
|
+
],
|
|
27
|
+
"@/domain/*": [
|
|
28
|
+
"./domain/*"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"include": [
|
|
33
|
+
"**/*.ts"
|
|
34
|
+
],
|
|
35
|
+
"exclude": [
|
|
36
|
+
"node_modules",
|
|
37
|
+
"dist"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
2
|
+
import { Pool } from 'pg';
|
|
3
|
+
import * as schema from '../drizzle/schema';
|
|
4
|
+
|
|
5
|
+
// Create connection pool
|
|
6
|
+
const pool = new Pool({
|
|
7
|
+
connectionString: process.env.DATABASE_URL!,
|
|
8
|
+
max: 20,
|
|
9
|
+
idleTimeoutMillis: 30000,
|
|
10
|
+
connectionTimeoutMillis: 2000,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Create drizzle instance with schema
|
|
14
|
+
export const db = drizzle(pool, { schema });
|
|
15
|
+
|
|
16
|
+
// Export schema for type inference
|
|
17
|
+
export { schema };
|
|
18
|
+
|
|
19
|
+
// Test connection on startup
|
|
20
|
+
pool.connect()
|
|
21
|
+
.then((client) => {
|
|
22
|
+
console.log('Database connected successfully');
|
|
23
|
+
client.release();
|
|
24
|
+
})
|
|
25
|
+
.catch((err) => {
|
|
26
|
+
console.error('Database connection failed:', err);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Gracefully close pool on process termination
|
|
31
|
+
process.on('SIGINT', async () => {
|
|
32
|
+
await pool.end();
|
|
33
|
+
console.log('Database connection pool closed.');
|
|
34
|
+
process.exit(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
process.on('SIGTERM', async () => {
|
|
38
|
+
await pool.end();
|
|
39
|
+
console.log('Database connection pool closed.');
|
|
40
|
+
process.exit(0);
|
|
41
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PrismaClient } from "../prisma/generated/prisma/client";
|
|
2
|
+
import { PrismaPg } from "@prisma/adapter-pg";
|
|
3
|
+
|
|
4
|
+
let db: PrismaClient;
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
var __db: PrismaClient | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Create adapter with connection string
|
|
11
|
+
const createAdapter = () =>
|
|
12
|
+
new PrismaPg({
|
|
13
|
+
connectionString: process.env.DATABASE_URL,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Use global instance in development to prevent exhausting database connections during hot reloads
|
|
17
|
+
if (process.env.NODE_ENV === "production") {
|
|
18
|
+
db = new PrismaClient({
|
|
19
|
+
adapter: createAdapter(),
|
|
20
|
+
});
|
|
21
|
+
} else {
|
|
22
|
+
if (!global.__db) {
|
|
23
|
+
global.__db = new PrismaClient({
|
|
24
|
+
adapter: createAdapter(),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
db = global.__db;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Connect to database
|
|
31
|
+
db.$connect()
|
|
32
|
+
.then(() => console.log("✅ Database connected successfully"))
|
|
33
|
+
.catch((err: any) => {
|
|
34
|
+
console.error("❌ Database connection failed:", err);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Gracefully disconnect on process termination
|
|
39
|
+
process.on("SIGINT", async () => {
|
|
40
|
+
await db.$disconnect();
|
|
41
|
+
console.log("Database connection closed.");
|
|
42
|
+
process.exit(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
process.on("SIGTERM", async () => {
|
|
46
|
+
await db.$disconnect();
|
|
47
|
+
console.log("Database connection closed.");
|
|
48
|
+
process.exit(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export { db };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<% if (features.authentication.emailVerification || features.authentication.passwordReset) { %>
|
|
2
|
+
import nodemailer from "nodemailer";
|
|
3
|
+
|
|
4
|
+
const transporter = nodemailer.createTransport({
|
|
5
|
+
host: process.env.SMTP_HOST,
|
|
6
|
+
port: parseInt(process.env.SMTP_PORT || "587"),
|
|
7
|
+
secure: process.env.SMTP_PORT === "465",
|
|
8
|
+
auth: {
|
|
9
|
+
user: process.env.SMTP_USER,
|
|
10
|
+
pass: process.env.SMTP_PASS,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
interface SendEmailOptions {
|
|
15
|
+
to: string;
|
|
16
|
+
subject: string;
|
|
17
|
+
html: string;
|
|
18
|
+
text?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function sendEmail({ to, subject, html, text }: SendEmailOptions): Promise<void> {
|
|
22
|
+
await transporter.sendMail({
|
|
23
|
+
from: process.env.EMAIL_FROM || "noreply@example.com",
|
|
24
|
+
to,
|
|
25
|
+
subject,
|
|
26
|
+
html,
|
|
27
|
+
text: text || html.replace(/<[^>]*>/g, ""),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
<% } else { %>
|
|
31
|
+
// Email service not enabled - emailVerification and passwordReset are disabled
|
|
32
|
+
export async function sendEmail(): Promise<void> {
|
|
33
|
+
throw new Error("Email service not configured");
|
|
34
|
+
}
|
|
35
|
+
<% } %>
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { FastifyError } from 'fastify';
|
|
2
|
+
|
|
3
|
+
// Error codes enum for consistent error identification
|
|
4
|
+
export enum ErrorCode {
|
|
5
|
+
// Authentication & Authorization
|
|
6
|
+
AUTH_UNAUTHORIZED = 'AUTH_UNAUTHORIZED',
|
|
7
|
+
AUTH_TOKEN_MISSING = 'AUTH_TOKEN_MISSING',
|
|
8
|
+
AUTH_TOKEN_INVALID = 'AUTH_TOKEN_INVALID',
|
|
9
|
+
AUTH_TOKEN_EXPIRED = 'AUTH_TOKEN_EXPIRED',
|
|
10
|
+
AUTH_USER_NOT_FOUND = 'AUTH_USER_NOT_FOUND',
|
|
11
|
+
AUTH_INVALID_CREDENTIALS = 'AUTH_INVALID_CREDENTIALS',
|
|
12
|
+
AUTH_PERMISSION_DENIED = 'AUTH_PERMISSION_DENIED',
|
|
13
|
+
|
|
14
|
+
// Session Management
|
|
15
|
+
SESSION_NOT_FOUND = 'SESSION_NOT_FOUND',
|
|
16
|
+
SESSION_EXPIRED = 'SESSION_EXPIRED',
|
|
17
|
+
SESSION_INVALID = 'SESSION_INVALID',
|
|
18
|
+
SESSION_MIGRATION_FAILED = 'SESSION_MIGRATION_FAILED',
|
|
19
|
+
|
|
20
|
+
// Validation
|
|
21
|
+
VALIDATION_FAILED = 'VALIDATION_FAILED',
|
|
22
|
+
VALIDATION_MISSING_FIELD = 'VALIDATION_MISSING_FIELD',
|
|
23
|
+
VALIDATION_INVALID_FORMAT = 'VALIDATION_INVALID_FORMAT',
|
|
24
|
+
|
|
25
|
+
// Business Logic
|
|
26
|
+
USER_ALREADY_EXISTS = 'USER_ALREADY_EXISTS',
|
|
27
|
+
USERNAME_ALREADY_EXISTS = 'USERNAME_ALREADY_EXISTS',
|
|
28
|
+
EMAIL_ALREADY_EXISTS = 'EMAIL_ALREADY_EXISTS',
|
|
29
|
+
|
|
30
|
+
// System Errors
|
|
31
|
+
DATABASE_ERROR = 'DATABASE_ERROR',
|
|
32
|
+
EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
|
|
33
|
+
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
|
34
|
+
|
|
35
|
+
// Network & Connection
|
|
36
|
+
NETWORK_ERROR = 'NETWORK_ERROR',
|
|
37
|
+
TIMEOUT_ERROR = 'TIMEOUT_ERROR',
|
|
38
|
+
|
|
39
|
+
// Resource Not Found
|
|
40
|
+
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
|
|
41
|
+
ROUTE_NOT_FOUND = 'ROUTE_NOT_FOUND',
|
|
42
|
+
|
|
43
|
+
// Client Errors
|
|
44
|
+
CLIENT_ERROR = 'CLIENT_ERROR',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Error severity levels
|
|
48
|
+
export enum ErrorSeverity {
|
|
49
|
+
LOW = 'low',
|
|
50
|
+
MEDIUM = 'medium',
|
|
51
|
+
HIGH = 'high',
|
|
52
|
+
CRITICAL = 'critical',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Standardized error response interface
|
|
56
|
+
export interface ApiErrorResponse {
|
|
57
|
+
error: {
|
|
58
|
+
code: ErrorCode;
|
|
59
|
+
message: string;
|
|
60
|
+
details?: any;
|
|
61
|
+
timestamp: string;
|
|
62
|
+
requestId?: string;
|
|
63
|
+
path?: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Custom application error class
|
|
68
|
+
export class AppError extends Error {
|
|
69
|
+
public readonly code: ErrorCode;
|
|
70
|
+
public readonly statusCode: number;
|
|
71
|
+
public readonly severity: ErrorSeverity;
|
|
72
|
+
public readonly details?: any;
|
|
73
|
+
public readonly isOperational: boolean;
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
code: ErrorCode,
|
|
77
|
+
message: string,
|
|
78
|
+
statusCode: number = 500,
|
|
79
|
+
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
|
|
80
|
+
details?: any,
|
|
81
|
+
isOperational: boolean = true
|
|
82
|
+
) {
|
|
83
|
+
super(message);
|
|
84
|
+
|
|
85
|
+
this.code = code;
|
|
86
|
+
this.statusCode = statusCode;
|
|
87
|
+
this.severity = severity;
|
|
88
|
+
this.details = details;
|
|
89
|
+
this.isOperational = isOperational;
|
|
90
|
+
|
|
91
|
+
// Maintains proper stack trace for where our error was thrown
|
|
92
|
+
Error.captureStackTrace(this, this.constructor);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Convert to API response format
|
|
96
|
+
toApiResponse(requestId?: string, path?: string): ApiErrorResponse {
|
|
97
|
+
return {
|
|
98
|
+
error: {
|
|
99
|
+
code: this.code,
|
|
100
|
+
message: this.message,
|
|
101
|
+
details: this.details,
|
|
102
|
+
timestamp: new Date().toISOString(),
|
|
103
|
+
requestId,
|
|
104
|
+
path,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Factory functions for common errors
|
|
111
|
+
export class ErrorFactory {
|
|
112
|
+
// Authentication errors
|
|
113
|
+
static unauthorized(message: string = 'Authentication required'): AppError {
|
|
114
|
+
return new AppError(
|
|
115
|
+
ErrorCode.AUTH_UNAUTHORIZED,
|
|
116
|
+
message,
|
|
117
|
+
401,
|
|
118
|
+
ErrorSeverity.MEDIUM
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static tokenMissing(): AppError {
|
|
123
|
+
return new AppError(
|
|
124
|
+
ErrorCode.AUTH_TOKEN_MISSING,
|
|
125
|
+
'Authentication token is required',
|
|
126
|
+
401,
|
|
127
|
+
ErrorSeverity.MEDIUM
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static tokenInvalid(details?: any): AppError {
|
|
132
|
+
return new AppError(
|
|
133
|
+
ErrorCode.AUTH_TOKEN_INVALID,
|
|
134
|
+
'Authentication token is invalid',
|
|
135
|
+
401,
|
|
136
|
+
ErrorSeverity.MEDIUM,
|
|
137
|
+
details
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static tokenExpired(): AppError {
|
|
142
|
+
return new AppError(
|
|
143
|
+
ErrorCode.AUTH_TOKEN_EXPIRED,
|
|
144
|
+
'Authentication token has expired',
|
|
145
|
+
401,
|
|
146
|
+
ErrorSeverity.MEDIUM
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static userNotFound(): AppError {
|
|
151
|
+
return new AppError(
|
|
152
|
+
ErrorCode.AUTH_USER_NOT_FOUND,
|
|
153
|
+
'User not found or no longer active',
|
|
154
|
+
401,
|
|
155
|
+
ErrorSeverity.MEDIUM
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static invalidCredentials(): AppError {
|
|
160
|
+
return new AppError(
|
|
161
|
+
ErrorCode.AUTH_INVALID_CREDENTIALS,
|
|
162
|
+
'Invalid email or password',
|
|
163
|
+
401,
|
|
164
|
+
ErrorSeverity.MEDIUM
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static permissionDenied(): AppError {
|
|
169
|
+
return new AppError(
|
|
170
|
+
ErrorCode.AUTH_PERMISSION_DENIED,
|
|
171
|
+
'You do not have permission to access this resource',
|
|
172
|
+
403,
|
|
173
|
+
ErrorSeverity.MEDIUM
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Session errors
|
|
178
|
+
static sessionNotFound(): AppError {
|
|
179
|
+
return new AppError(
|
|
180
|
+
ErrorCode.SESSION_NOT_FOUND,
|
|
181
|
+
'Session not found or expired',
|
|
182
|
+
401,
|
|
183
|
+
ErrorSeverity.MEDIUM
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static sessionExpired(): AppError {
|
|
188
|
+
return new AppError(
|
|
189
|
+
ErrorCode.SESSION_EXPIRED,
|
|
190
|
+
'Session has expired',
|
|
191
|
+
401,
|
|
192
|
+
ErrorSeverity.MEDIUM
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
static sessionInvalid(details?: any): AppError {
|
|
197
|
+
return new AppError(
|
|
198
|
+
ErrorCode.SESSION_INVALID,
|
|
199
|
+
'Session is invalid',
|
|
200
|
+
401,
|
|
201
|
+
ErrorSeverity.MEDIUM,
|
|
202
|
+
details
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
static sessionMigrationFailed(details?: any): AppError {
|
|
207
|
+
return new AppError(
|
|
208
|
+
ErrorCode.SESSION_MIGRATION_FAILED,
|
|
209
|
+
'Failed to migrate session to user account',
|
|
210
|
+
500,
|
|
211
|
+
ErrorSeverity.HIGH,
|
|
212
|
+
details
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Validation errors
|
|
217
|
+
static validationFailed(details: any): AppError {
|
|
218
|
+
return new AppError(
|
|
219
|
+
ErrorCode.VALIDATION_FAILED,
|
|
220
|
+
'Request validation failed',
|
|
221
|
+
400,
|
|
222
|
+
ErrorSeverity.LOW,
|
|
223
|
+
details
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static validationError(message: string): AppError {
|
|
228
|
+
return new AppError(
|
|
229
|
+
ErrorCode.VALIDATION_FAILED,
|
|
230
|
+
message,
|
|
231
|
+
400,
|
|
232
|
+
ErrorSeverity.LOW
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Business logic errors
|
|
237
|
+
static userAlreadyExists(): AppError {
|
|
238
|
+
return new AppError(
|
|
239
|
+
ErrorCode.USER_ALREADY_EXISTS,
|
|
240
|
+
'A user with this email already exists',
|
|
241
|
+
409,
|
|
242
|
+
ErrorSeverity.LOW
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static usernameAlreadyExists(): AppError {
|
|
247
|
+
return new AppError(
|
|
248
|
+
ErrorCode.USERNAME_ALREADY_EXISTS,
|
|
249
|
+
'This username is already taken',
|
|
250
|
+
409,
|
|
251
|
+
ErrorSeverity.LOW
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// System errors
|
|
256
|
+
static databaseError(details?: any): AppError {
|
|
257
|
+
return new AppError(
|
|
258
|
+
ErrorCode.DATABASE_ERROR,
|
|
259
|
+
'A database error occurred',
|
|
260
|
+
500,
|
|
261
|
+
ErrorSeverity.HIGH,
|
|
262
|
+
details,
|
|
263
|
+
false // Not operational - system issue
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
static internalServerError(details?: any): AppError {
|
|
268
|
+
return new AppError(
|
|
269
|
+
ErrorCode.INTERNAL_SERVER_ERROR,
|
|
270
|
+
'An internal server error occurred',
|
|
271
|
+
500,
|
|
272
|
+
ErrorSeverity.CRITICAL,
|
|
273
|
+
details,
|
|
274
|
+
false
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
static externalServiceError(service: string, details?: any): AppError {
|
|
279
|
+
return new AppError(
|
|
280
|
+
ErrorCode.EXTERNAL_SERVICE_ERROR,
|
|
281
|
+
`External service (${service}) is unavailable`,
|
|
282
|
+
503,
|
|
283
|
+
ErrorSeverity.HIGH,
|
|
284
|
+
details
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Resource errors
|
|
289
|
+
static resourceNotFound(resource: string = 'resource'): AppError {
|
|
290
|
+
return new AppError(
|
|
291
|
+
ErrorCode.RESOURCE_NOT_FOUND,
|
|
292
|
+
`The requested ${resource} was not found`,
|
|
293
|
+
404,
|
|
294
|
+
ErrorSeverity.LOW
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
static routeNotFound(method: string, path: string): AppError {
|
|
299
|
+
return new AppError(
|
|
300
|
+
ErrorCode.ROUTE_NOT_FOUND,
|
|
301
|
+
`Route ${method} ${path} not found`,
|
|
302
|
+
404,
|
|
303
|
+
ErrorSeverity.LOW,
|
|
304
|
+
{ method, path }
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
static clientError(message: string = 'Bad request', statusCode: number = 400): AppError {
|
|
309
|
+
return new AppError(
|
|
310
|
+
ErrorCode.CLIENT_ERROR,
|
|
311
|
+
message,
|
|
312
|
+
statusCode,
|
|
313
|
+
ErrorSeverity.LOW
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Helper function to determine if an error should include details in the response
|
|
319
|
+
export function shouldIncludeErrorDetails(error: AppError, isDevelopment: boolean): boolean {
|
|
320
|
+
// Always include details for operational errors in development
|
|
321
|
+
if (isDevelopment && error.isOperational) return true;
|
|
322
|
+
|
|
323
|
+
// Never include details for non-operational (system) errors in production
|
|
324
|
+
if (!isDevelopment && !error.isOperational) return false;
|
|
325
|
+
|
|
326
|
+
// Include details for low/medium severity operational errors
|
|
327
|
+
return error.isOperational && [ErrorSeverity.LOW, ErrorSeverity.MEDIUM].includes(error.severity);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Helper to convert unknown errors to AppError
|
|
331
|
+
export function normalizeError(error: unknown): AppError {
|
|
332
|
+
if (error instanceof AppError) {
|
|
333
|
+
return error;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (error instanceof Error) {
|
|
337
|
+
return new AppError(
|
|
338
|
+
ErrorCode.INTERNAL_SERVER_ERROR,
|
|
339
|
+
error.message,
|
|
340
|
+
500,
|
|
341
|
+
ErrorSeverity.CRITICAL,
|
|
342
|
+
{ originalError: error.message },
|
|
343
|
+
false
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return ErrorFactory.internalServerError({ originalError: String(error) });
|
|
348
|
+
}
|