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.
Files changed (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +642 -0
  3. package/bin/cli.js +12 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +113 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config/dependencies.d.ts +82 -0
  9. package/dist/config/dependencies.d.ts.map +1 -0
  10. package/dist/config/dependencies.js +82 -0
  11. package/dist/config/dependencies.js.map +1 -0
  12. package/dist/config/presets.d.ts +3 -0
  13. package/dist/config/presets.d.ts.map +1 -0
  14. package/dist/config/presets.js +174 -0
  15. package/dist/config/presets.js.map +1 -0
  16. package/dist/generators/index.d.ts +40 -0
  17. package/dist/generators/index.d.ts.map +1 -0
  18. package/dist/generators/index.js +130 -0
  19. package/dist/generators/index.js.map +1 -0
  20. package/dist/generators/onboarding.d.ts +8 -0
  21. package/dist/generators/onboarding.d.ts.map +1 -0
  22. package/dist/generators/onboarding.js +141 -0
  23. package/dist/generators/onboarding.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +65 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/prompts/features.d.ts +14 -0
  29. package/dist/prompts/features.d.ts.map +1 -0
  30. package/dist/prompts/features.js +96 -0
  31. package/dist/prompts/features.js.map +1 -0
  32. package/dist/prompts/index.d.ts +3 -0
  33. package/dist/prompts/index.d.ts.map +1 -0
  34. package/dist/prompts/index.js +93 -0
  35. package/dist/prompts/index.js.map +1 -0
  36. package/dist/prompts/onboarding.d.ts +6 -0
  37. package/dist/prompts/onboarding.d.ts.map +1 -0
  38. package/dist/prompts/onboarding.js +37 -0
  39. package/dist/prompts/onboarding.js.map +1 -0
  40. package/dist/prompts/orm.d.ts +3 -0
  41. package/dist/prompts/orm.d.ts.map +1 -0
  42. package/dist/prompts/orm.js +23 -0
  43. package/dist/prompts/orm.js.map +1 -0
  44. package/dist/prompts/packageManager.d.ts +2 -0
  45. package/dist/prompts/packageManager.d.ts.map +1 -0
  46. package/dist/prompts/packageManager.js +18 -0
  47. package/dist/prompts/packageManager.js.map +1 -0
  48. package/dist/prompts/platform.d.ts +3 -0
  49. package/dist/prompts/platform.d.ts.map +1 -0
  50. package/dist/prompts/platform.js +21 -0
  51. package/dist/prompts/platform.js.map +1 -0
  52. package/dist/prompts/preset.d.ts +4 -0
  53. package/dist/prompts/preset.d.ts.map +1 -0
  54. package/dist/prompts/preset.js +165 -0
  55. package/dist/prompts/preset.js.map +1 -0
  56. package/dist/prompts/project.d.ts +2 -0
  57. package/dist/prompts/project.d.ts.map +1 -0
  58. package/dist/prompts/project.js +27 -0
  59. package/dist/prompts/project.js.map +1 -0
  60. package/dist/prompts/sdks.d.ts +2 -0
  61. package/dist/prompts/sdks.d.ts.map +1 -0
  62. package/dist/prompts/sdks.js +46 -0
  63. package/dist/prompts/sdks.js.map +1 -0
  64. package/dist/types/index.d.ts +77 -0
  65. package/dist/types/index.d.ts.map +1 -0
  66. package/dist/types/index.js +25 -0
  67. package/dist/types/index.js.map +1 -0
  68. package/dist/utils/cleanup.d.ts +5 -0
  69. package/dist/utils/cleanup.d.ts.map +1 -0
  70. package/dist/utils/cleanup.js +38 -0
  71. package/dist/utils/cleanup.js.map +1 -0
  72. package/dist/utils/copy.d.ts +10 -0
  73. package/dist/utils/copy.d.ts.map +1 -0
  74. package/dist/utils/copy.js +53 -0
  75. package/dist/utils/copy.js.map +1 -0
  76. package/dist/utils/errors.d.ts +33 -0
  77. package/dist/utils/errors.d.ts.map +1 -0
  78. package/dist/utils/errors.js +136 -0
  79. package/dist/utils/errors.js.map +1 -0
  80. package/dist/utils/git.d.ts +5 -0
  81. package/dist/utils/git.d.ts.map +1 -0
  82. package/dist/utils/git.js +33 -0
  83. package/dist/utils/git.js.map +1 -0
  84. package/dist/utils/logger.d.ts +9 -0
  85. package/dist/utils/logger.d.ts.map +1 -0
  86. package/dist/utils/logger.js +22 -0
  87. package/dist/utils/logger.js.map +1 -0
  88. package/dist/utils/package.d.ts +16 -0
  89. package/dist/utils/package.d.ts.map +1 -0
  90. package/dist/utils/package.js +86 -0
  91. package/dist/utils/package.js.map +1 -0
  92. package/dist/utils/system-validation.d.ts +9 -0
  93. package/dist/utils/system-validation.d.ts.map +1 -0
  94. package/dist/utils/system-validation.js +31 -0
  95. package/dist/utils/system-validation.js.map +1 -0
  96. package/dist/utils/template.d.ts +20 -0
  97. package/dist/utils/template.d.ts.map +1 -0
  98. package/dist/utils/template.js +234 -0
  99. package/dist/utils/template.js.map +1 -0
  100. package/dist/utils/validation.d.ts +8 -0
  101. package/dist/utils/validation.d.ts.map +1 -0
  102. package/dist/utils/validation.js +94 -0
  103. package/dist/utils/validation.js.map +1 -0
  104. package/package.json +96 -0
  105. package/templates/base/backend/.dockerignore.ejs +62 -0
  106. package/templates/base/backend/.env.example.ejs +116 -0
  107. package/templates/base/backend/Dockerfile.ejs +142 -0
  108. package/templates/base/backend/controllers/event-queue/index.ts +20 -0
  109. package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
  110. package/templates/base/backend/controllers/rest-api/index.ts +48 -0
  111. package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
  112. package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
  113. package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
  114. package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
  115. package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
  116. package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
  117. package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
  118. package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
  119. package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
  120. package/templates/base/backend/domain/device-session/schema.ts +72 -0
  121. package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
  122. package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
  123. package/templates/base/backend/domain/session/schema.ts +29 -0
  124. package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
  125. package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
  126. package/templates/base/backend/domain/user/schema.ts +14 -0
  127. package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
  128. package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
  129. package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
  130. package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
  131. package/templates/base/backend/lib/constants.ts.ejs +29 -0
  132. package/templates/base/backend/package.json.ejs +50 -0
  133. package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
  134. package/templates/base/backend/prisma.config.prisma.ts +12 -0
  135. package/templates/base/backend/tsconfig.json +39 -0
  136. package/templates/base/backend/utils/db.drizzle.ts +41 -0
  137. package/templates/base/backend/utils/db.prisma.ts +51 -0
  138. package/templates/base/backend/utils/email.ts.ejs +35 -0
  139. package/templates/base/backend/utils/errors.ts +348 -0
  140. package/templates/base/backend/utils/redis.ts.ejs +279 -0
  141. package/templates/base/mobile/.env.example.ejs +35 -0
  142. package/templates/base/mobile/.gitignore.ejs +167 -0
  143. package/templates/base/mobile/app/+not-found.tsx +85 -0
  144. package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
  145. package/templates/base/mobile/app.json.ejs +88 -0
  146. package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
  147. package/templates/base/mobile/assets/images/favicon.png +0 -0
  148. package/templates/base/mobile/assets/images/icon.png +0 -0
  149. package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
  150. package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
  151. package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
  152. package/templates/base/mobile/assets/images/paywall_image.png +0 -0
  153. package/templates/base/mobile/assets/images/splash.png +0 -0
  154. package/templates/base/mobile/eas.json.ejs +49 -0
  155. package/templates/base/mobile/metro.config.js +9 -0
  156. package/templates/base/mobile/package.json.ejs +53 -0
  157. package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
  158. package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
  159. package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
  160. package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
  161. package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
  162. package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
  163. package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
  164. package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
  165. package/templates/base/mobile/src/components/ui/index.ts +6 -0
  166. package/templates/base/mobile/src/constants/Theme.ts +163 -0
  167. package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
  168. package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
  169. package/templates/base/mobile/src/services/api.ts.ejs +71 -0
  170. package/templates/base/mobile/src/services/errorService.ts +179 -0
  171. package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
  172. package/templates/base/mobile/src/store/index.ts.ejs +18 -0
  173. package/templates/base/mobile/src/store/ui.store.ts +100 -0
  174. package/templates/base/mobile/src/utils/formatters.ts +105 -0
  175. package/templates/base/mobile/src/utils/logger.ts +73 -0
  176. package/templates/base/mobile/src/utils/responsive.ts +234 -0
  177. package/templates/base/mobile/tsconfig.json +32 -0
  178. package/templates/base/web/.env.example.ejs +26 -0
  179. package/templates/base/web/components.json +22 -0
  180. package/templates/base/web/eslint.config.mjs +18 -0
  181. package/templates/base/web/next.config.ts +7 -0
  182. package/templates/base/web/package.json.ejs +35 -0
  183. package/templates/base/web/postcss.config.mjs +7 -0
  184. package/templates/base/web/public/.gitkeep +0 -0
  185. package/templates/base/web/public/file.svg +1 -0
  186. package/templates/base/web/public/globe.svg +1 -0
  187. package/templates/base/web/public/next.svg +1 -0
  188. package/templates/base/web/public/vercel.svg +1 -0
  189. package/templates/base/web/public/window.svg +1 -0
  190. package/templates/base/web/src/app/favicon.ico +0 -0
  191. package/templates/base/web/src/app/globals.css +152 -0
  192. package/templates/base/web/src/app/layout.tsx.ejs +54 -0
  193. package/templates/base/web/src/app/page.tsx.ejs +92 -0
  194. package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
  195. package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
  196. package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
  197. package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
  198. package/templates/base/web/src/components/theme-toggle.tsx +34 -0
  199. package/templates/base/web/src/components/ui/button.tsx +62 -0
  200. package/templates/base/web/src/components/ui/card.tsx +92 -0
  201. package/templates/base/web/src/components/ui/input.tsx +21 -0
  202. package/templates/base/web/src/components/ui/label.tsx +24 -0
  203. package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
  204. package/templates/base/web/src/components/ui/spinner.tsx +20 -0
  205. package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
  206. package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
  207. package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
  208. package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
  209. package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
  210. package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
  211. package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
  212. package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
  213. package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
  214. package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
  215. package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
  216. package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
  217. package/templates/base/web/src/lib/utils.ts +6 -0
  218. package/templates/base/web/src/proxy.ts.ejs +66 -0
  219. package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
  220. package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
  221. package/templates/base/web/tsconfig.json +34 -0
  222. package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
  223. package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
  224. package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
  225. package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
  226. package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
  227. package/templates/features/mobile/auth/components/auth/index.ts +2 -0
  228. package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
  229. package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
  230. package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
  231. package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
  232. package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
  233. package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
  234. package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
  235. package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
  236. package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
  237. package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
  238. package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
  239. package/templates/features/web/.gitkeep +0 -0
  240. package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
  241. package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
  242. package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
  243. package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
  244. package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
  245. package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
  246. package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
  247. package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
  248. package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
  249. package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
  250. package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
  251. package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
  252. package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
  253. package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
  254. package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
  255. package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
  256. package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
  257. package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
  258. package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
  259. package/templates/integrations/mobile/att/services/attService.ts +84 -0
  260. package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
  261. package/templates/integrations/mobile/att/store/att.store.ts +162 -0
  262. package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
  263. package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
  264. package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
  265. package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
  266. package/templates/integrations/web/.gitkeep +0 -0
  267. package/templates/shared/.env.example.ejs +21 -0
  268. package/templates/shared/.gitignore.ejs +145 -0
  269. package/templates/shared/README.md.ejs +134 -0
  270. package/templates/shared/docker-compose.prod.yml.ejs +120 -0
  271. package/templates/shared/docker-compose.yml.ejs +129 -0
  272. package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
  273. package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
  274. 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
+ }