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,248 @@
1
+ import { db } from "../../utils/db";
2
+ import { ErrorFactory } from "../../utils/errors";
3
+ import {
4
+ DeviceSession,
5
+ CreateDeviceSessionBody,
6
+ CreateDeviceSessionResponse,
7
+ DeviceSessionMigrationEligibilityResponse,
8
+ } from "./schema";
9
+
10
+ // Generate a UUID v4 for session tokens
11
+ const generateSessionToken = (): string => {
12
+ return crypto.randomUUID();
13
+ };
14
+
15
+ export const createDeviceSession = async (data: CreateDeviceSessionBody): Promise<CreateDeviceSessionResponse> => {
16
+ const { deviceId } = data;
17
+
18
+ try {
19
+ const sessionToken = generateSessionToken();
20
+
21
+ const session = await db.deviceSession.create({
22
+ data: {
23
+ deviceId,
24
+ sessionToken,
25
+ preferredCurrency: 'USD',
26
+ migrated: false,
27
+ lastActiveAt: new Date(),
28
+ },
29
+ select: {
30
+ id: true,
31
+ deviceId: true,
32
+ sessionToken: true,
33
+ createdAt: true,
34
+ lastActiveAt: true,
35
+ migrated: true,
36
+ migratedToUserId: true,
37
+ preferredCurrency: true,
38
+ },
39
+ });
40
+
41
+ return {
42
+ session: {
43
+ ...session,
44
+ createdAt: session.createdAt.toISOString(),
45
+ lastActiveAt: session.lastActiveAt.toISOString(),
46
+ },
47
+ sessionToken,
48
+ };
49
+ } catch (error) {
50
+ throw ErrorFactory.databaseError({
51
+ operation: 'createDeviceSession',
52
+ deviceId,
53
+ originalError: error instanceof Error ? error.message : String(error)
54
+ });
55
+ }
56
+ };
57
+
58
+ export const validateDeviceSession = async (sessionToken: string): Promise<DeviceSession | null> => {
59
+ try {
60
+ const session = await db.deviceSession.findUnique({
61
+ where: { sessionToken },
62
+ select: {
63
+ id: true,
64
+ deviceId: true,
65
+ sessionToken: true,
66
+ createdAt: true,
67
+ lastActiveAt: true,
68
+ migrated: true,
69
+ migratedToUserId: true,
70
+ preferredCurrency: true,
71
+ },
72
+ });
73
+
74
+ if (!session) {
75
+ return null;
76
+ }
77
+
78
+ // Check if session is expired (30 days of inactivity)
79
+ const thirtyDaysAgo = new Date();
80
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
81
+
82
+ if (session.lastActiveAt < thirtyDaysAgo) {
83
+ // Session expired, delete it
84
+ await deleteDeviceSession(sessionToken);
85
+ return null;
86
+ }
87
+
88
+ return {
89
+ ...session,
90
+ createdAt: session.createdAt.toISOString(),
91
+ lastActiveAt: session.lastActiveAt.toISOString(),
92
+ };
93
+ } catch (error) {
94
+ throw ErrorFactory.databaseError({
95
+ operation: 'validateDeviceSession',
96
+ sessionToken,
97
+ originalError: error instanceof Error ? error.message : String(error)
98
+ });
99
+ }
100
+ };
101
+
102
+ export const updateDeviceSessionActivity = async (sessionToken: string): Promise<void> => {
103
+ try {
104
+ await db.deviceSession.update({
105
+ where: { sessionToken },
106
+ data: { lastActiveAt: new Date() },
107
+ });
108
+ } catch (error) {
109
+ throw ErrorFactory.databaseError({
110
+ operation: 'updateDeviceSessionActivity',
111
+ sessionToken,
112
+ originalError: error instanceof Error ? error.message : String(error)
113
+ });
114
+ }
115
+ };
116
+
117
+ export const deleteDeviceSession = async (sessionToken: string): Promise<void> => {
118
+ try {
119
+ await db.deviceSession.delete({
120
+ where: { sessionToken },
121
+ });
122
+ } catch (error) {
123
+ // Don't throw error if session doesn't exist
124
+ if (error instanceof Error && error.message.includes('Record to delete does not exist')) {
125
+ return;
126
+ }
127
+
128
+ throw ErrorFactory.databaseError({
129
+ operation: 'deleteDeviceSession',
130
+ sessionToken,
131
+ originalError: error instanceof Error ? error.message : String(error)
132
+ });
133
+ }
134
+ };
135
+
136
+ export const validateDeviceSessionMigrationEligibility = async (sessionToken: string): Promise<DeviceSessionMigrationEligibilityResponse> => {
137
+ try {
138
+ const session = await validateDeviceSession(sessionToken);
139
+
140
+ if (!session) {
141
+ return {
142
+ canMigrate: false,
143
+ reason: 'Device session not found or expired',
144
+ };
145
+ }
146
+
147
+ if (session.migrated) {
148
+ return {
149
+ canMigrate: false,
150
+ reason: 'Device session already migrated to user account',
151
+ };
152
+ }
153
+
154
+ return {
155
+ canMigrate: true,
156
+ };
157
+ } catch (error) {
158
+ throw ErrorFactory.databaseError({
159
+ operation: 'validateDeviceSessionMigrationEligibility',
160
+ sessionToken,
161
+ originalError: error instanceof Error ? error.message : String(error)
162
+ });
163
+ }
164
+ };
165
+
166
+ /**
167
+ * Migrate a device session to a user account
168
+ * This is called after a user signs up through BetterAuth to link their device session
169
+ */
170
+ export const migrateDeviceSessionToUser = async (sessionToken: string, userId: string): Promise<void> => {
171
+ try {
172
+ await db.deviceSession.update({
173
+ where: { sessionToken },
174
+ data: {
175
+ migrated: true,
176
+ migratedToUserId: userId,
177
+ },
178
+ });
179
+ } catch (error) {
180
+ throw ErrorFactory.databaseError({
181
+ operation: 'migrateDeviceSessionToUser',
182
+ sessionToken,
183
+ userId,
184
+ originalError: error instanceof Error ? error.message : String(error)
185
+ });
186
+ }
187
+ };
188
+
189
+ export const cleanupExpiredDeviceSessions = async (): Promise<number> => {
190
+ try {
191
+ // Delete sessions older than 30 days of inactivity
192
+ const thirtyDaysAgo = new Date();
193
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
194
+
195
+ const result = await db.deviceSession.deleteMany({
196
+ where: {
197
+ lastActiveAt: {
198
+ lt: thirtyDaysAgo,
199
+ },
200
+ },
201
+ });
202
+
203
+ return result.count;
204
+ } catch (error) {
205
+ throw ErrorFactory.databaseError({
206
+ operation: 'cleanupExpiredDeviceSessions',
207
+ originalError: error instanceof Error ? error.message : String(error)
208
+ });
209
+ }
210
+ };
211
+
212
+ export const getDeviceSessionById = async (sessionId: string): Promise<DeviceSession | null> => {
213
+ try {
214
+ const session = await db.deviceSession.findUnique({
215
+ where: { id: sessionId },
216
+ select: {
217
+ id: true,
218
+ deviceId: true,
219
+ sessionToken: true,
220
+ createdAt: true,
221
+ lastActiveAt: true,
222
+ migrated: true,
223
+ migratedToUserId: true,
224
+ preferredCurrency: true,
225
+ },
226
+ });
227
+
228
+ if (!session) {
229
+ return null;
230
+ }
231
+
232
+ return {
233
+ ...session,
234
+ createdAt: session.createdAt.toISOString(),
235
+ lastActiveAt: session.lastActiveAt.toISOString(),
236
+ };
237
+ } catch (error) {
238
+ throw ErrorFactory.databaseError({
239
+ operation: 'getDeviceSessionById',
240
+ sessionId,
241
+ originalError: error instanceof Error ? error.message : String(error)
242
+ });
243
+ }
244
+ };
245
+
246
+ export const getDeviceSessionByToken = async (sessionToken: string): Promise<DeviceSession | null> => {
247
+ return validateDeviceSession(sessionToken);
248
+ };
@@ -0,0 +1,72 @@
1
+ import { Static, Type } from "@sinclair/typebox";
2
+
3
+ // Core DeviceSession entity schema (anonymous device sessions before authentication)
4
+ export const DeviceSessionSchema = Type.Object({
5
+ id: Type.String(),
6
+ deviceId: Type.String(),
7
+ sessionToken: Type.String(),
8
+ createdAt: Type.String({ format: 'date-time' }),
9
+ lastActiveAt: Type.String({ format: 'date-time' }),
10
+ migrated: Type.Boolean(),
11
+ migratedToUserId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
12
+ preferredCurrency: Type.String({ default: 'USD' }),
13
+ });
14
+
15
+ export type DeviceSession = Static<typeof DeviceSessionSchema>;
16
+
17
+ // Create device session request schema
18
+ export const CreateDeviceSessionBodySchema = Type.Object({
19
+ deviceId: Type.String({
20
+ minLength: 1,
21
+ maxLength: 128,
22
+ pattern: "^[a-zA-Z0-9_-]+$" // Allow alphanumeric, underscore, and hyphen
23
+ }),
24
+ });
25
+
26
+ export type CreateDeviceSessionBody = Static<typeof CreateDeviceSessionBodySchema>;
27
+
28
+ // Create device session response schema
29
+ export const CreateDeviceSessionResponseSchema = Type.Object({
30
+ session: DeviceSessionSchema,
31
+ sessionToken: Type.String(),
32
+ });
33
+
34
+ export type CreateDeviceSessionResponse = Static<typeof CreateDeviceSessionResponseSchema>;
35
+
36
+ // Validate device session request schema (for session token validation)
37
+ export const ValidateDeviceSessionBodySchema = Type.Object({
38
+ sessionToken: Type.String({
39
+ minLength: 36,
40
+ maxLength: 36,
41
+ pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$" // UUID format
42
+ }),
43
+ });
44
+
45
+ export type ValidateDeviceSessionBody = Static<typeof ValidateDeviceSessionBodySchema>;
46
+
47
+ // Device session validation response schema
48
+ export const DeviceSessionValidationResponseSchema = Type.Object({
49
+ valid: Type.Boolean(),
50
+ session: Type.Optional(DeviceSessionSchema),
51
+ });
52
+
53
+ export type DeviceSessionValidationResponse = Static<typeof DeviceSessionValidationResponseSchema>;
54
+
55
+ // Update device session activity request schema
56
+ export const UpdateDeviceSessionActivityBodySchema = Type.Object({
57
+ sessionToken: Type.String({
58
+ minLength: 36,
59
+ maxLength: 36,
60
+ pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"
61
+ }),
62
+ });
63
+
64
+ export type UpdateDeviceSessionActivityBody = Static<typeof UpdateDeviceSessionActivityBodySchema>;
65
+
66
+ // Device session migration eligibility response schema
67
+ export const DeviceSessionMigrationEligibilityResponseSchema = Type.Object({
68
+ canMigrate: Type.Boolean(),
69
+ reason: Type.Optional(Type.String()),
70
+ });
71
+
72
+ export type DeviceSessionMigrationEligibilityResponse = Static<typeof DeviceSessionMigrationEligibilityResponseSchema>;
@@ -0,0 +1,72 @@
1
+ import { eq, and } from 'drizzle-orm';
2
+ import { db, schema } from "../../utils/db";
3
+ import { auth } from "../../lib/auth";
4
+ import { ErrorFactory } from "../../utils/errors";
5
+ import type { SessionWithToken, RevokeSessionResponse } from "./schema";
6
+
7
+ /**
8
+ * Session Repository (Drizzle)
9
+ *
10
+ * Provides session management operations for the BFF pattern.
11
+ * Note: Session creation/authentication is handled by BetterAuth.
12
+ */
13
+
14
+ /**
15
+ * Find a session by ID that belongs to the specified user
16
+ */
17
+ export const findUserSessionById = async (
18
+ sessionId: string,
19
+ userId: string
20
+ ): Promise<SessionWithToken | null> => {
21
+ try {
22
+ const [session] = await db
23
+ .select()
24
+ .from(schema.session)
25
+ .where(and(eq(schema.session.id, sessionId), eq(schema.session.userId, userId)))
26
+ .limit(1);
27
+
28
+ if (!session) {
29
+ return null;
30
+ }
31
+
32
+ return {
33
+ id: session.id,
34
+ userId: session.userId,
35
+ token: session.token,
36
+ expiresAt: session.expiresAt.toISOString(),
37
+ createdAt: session.createdAt.toISOString(),
38
+ updatedAt: session.updatedAt.toISOString(),
39
+ ipAddress: session.ipAddress,
40
+ userAgent: session.userAgent,
41
+ };
42
+ } catch (error) {
43
+ throw ErrorFactory.databaseError({
44
+ operation: 'findUserSessionById',
45
+ sessionId,
46
+ userId,
47
+ originalError: error instanceof Error ? error.message : String(error),
48
+ });
49
+ }
50
+ };
51
+
52
+ /**
53
+ * Revoke a session using Better Auth's native revocation
54
+ */
55
+ export const revokeSessionByToken = async (
56
+ token: string,
57
+ headers: Headers
58
+ ): Promise<RevokeSessionResponse> => {
59
+ try {
60
+ const response = await auth.api.revokeSession({
61
+ headers,
62
+ body: { token },
63
+ });
64
+
65
+ return response as RevokeSessionResponse;
66
+ } catch (error) {
67
+ throw ErrorFactory.databaseError({
68
+ operation: 'revokeSessionByToken',
69
+ originalError: error instanceof Error ? error.message : String(error),
70
+ });
71
+ }
72
+ };
@@ -0,0 +1,72 @@
1
+ import { db } from "../../utils/db";
2
+ import { auth } from "../../lib/auth";
3
+ import { ErrorFactory } from "../../utils/errors";
4
+ import type { SessionWithToken, RevokeSessionResponse } from "./schema";
5
+
6
+ /**
7
+ * Session Repository (Prisma)
8
+ *
9
+ * Provides session management operations for the BFF pattern.
10
+ * Note: Session creation/authentication is handled by BetterAuth.
11
+ */
12
+
13
+ /**
14
+ * Find a session by ID that belongs to the specified user
15
+ */
16
+ export const findUserSessionById = async (
17
+ sessionId: string,
18
+ userId: string
19
+ ): Promise<SessionWithToken | null> => {
20
+ try {
21
+ const session = await db.session.findFirst({
22
+ where: {
23
+ id: sessionId,
24
+ userId: userId,
25
+ },
26
+ });
27
+
28
+ if (!session) {
29
+ return null;
30
+ }
31
+
32
+ return {
33
+ id: session.id,
34
+ userId: session.userId,
35
+ token: session.token,
36
+ expiresAt: session.expiresAt.toISOString(),
37
+ createdAt: session.createdAt.toISOString(),
38
+ updatedAt: session.updatedAt.toISOString(),
39
+ ipAddress: session.ipAddress,
40
+ userAgent: session.userAgent,
41
+ };
42
+ } catch (error) {
43
+ throw ErrorFactory.databaseError({
44
+ operation: 'findUserSessionById',
45
+ sessionId,
46
+ userId,
47
+ originalError: error instanceof Error ? error.message : String(error),
48
+ });
49
+ }
50
+ };
51
+
52
+ /**
53
+ * Revoke a session using Better Auth's native revocation
54
+ */
55
+ export const revokeSessionByToken = async (
56
+ token: string,
57
+ headers: Headers
58
+ ): Promise<RevokeSessionResponse> => {
59
+ try {
60
+ const response = await auth.api.revokeSession({
61
+ headers,
62
+ body: { token },
63
+ });
64
+
65
+ return response as RevokeSessionResponse;
66
+ } catch (error) {
67
+ throw ErrorFactory.databaseError({
68
+ operation: 'revokeSessionByToken',
69
+ originalError: error instanceof Error ? error.message : String(error),
70
+ });
71
+ }
72
+ };
@@ -0,0 +1,29 @@
1
+ import { Type, Static } from '@sinclair/typebox';
2
+
3
+ /**
4
+ * Session Domain Schema
5
+ *
6
+ * Schemas for BFF session operations (session management from web frontend).
7
+ * Note: Session creation/authentication is handled by BetterAuth.
8
+ */
9
+
10
+ // Session with token (used internally for revocation)
11
+ export const SessionWithTokenSchema = Type.Object({
12
+ id: Type.String(),
13
+ userId: Type.String(),
14
+ token: Type.String(),
15
+ expiresAt: Type.String({ format: 'date-time' }),
16
+ createdAt: Type.String({ format: 'date-time' }),
17
+ updatedAt: Type.String({ format: 'date-time' }),
18
+ ipAddress: Type.Optional(Type.Union([Type.String(), Type.Null()])),
19
+ userAgent: Type.Optional(Type.Union([Type.String(), Type.Null()])),
20
+ });
21
+
22
+ export type SessionWithToken = Static<typeof SessionWithTokenSchema>;
23
+
24
+ // Revoke session response
25
+ export const RevokeSessionResponseSchema = Type.Object({
26
+ status: Type.Boolean(),
27
+ });
28
+
29
+ export type RevokeSessionResponse = Static<typeof RevokeSessionResponseSchema>;
@@ -0,0 +1,127 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { db, schema } from "../../utils/db";
3
+ import { ErrorFactory } from "../../utils/errors";
4
+
5
+ /**
6
+ * User Repository (Drizzle)
7
+ *
8
+ * Note: User creation and authentication is handled by BetterAuth.
9
+ * This repository provides helper functions for user lookups and profile updates.
10
+ */
11
+
12
+ export const getUser = async (id: string) => {
13
+ try {
14
+ const [user] = await db
15
+ .select({
16
+ id: schema.user.id,
17
+ email: schema.user.email,
18
+ emailVerified: schema.user.emailVerified,
19
+ name: schema.user.name,
20
+ image: schema.user.image,
21
+ createdAt: schema.user.createdAt,
22
+ updatedAt: schema.user.updatedAt,
23
+ })
24
+ .from(schema.user)
25
+ .where(eq(schema.user.id, id))
26
+ .limit(1);
27
+
28
+ return user || null;
29
+ } catch (error) {
30
+ throw ErrorFactory.databaseError({
31
+ operation: 'getUser',
32
+ userId: id,
33
+ originalError: error instanceof Error ? error.message : String(error)
34
+ });
35
+ }
36
+ };
37
+
38
+ export const getUserByEmail = async (email: string) => {
39
+ try {
40
+ const [user] = await db
41
+ .select({
42
+ id: schema.user.id,
43
+ email: schema.user.email,
44
+ emailVerified: schema.user.emailVerified,
45
+ name: schema.user.name,
46
+ image: schema.user.image,
47
+ createdAt: schema.user.createdAt,
48
+ updatedAt: schema.user.updatedAt,
49
+ })
50
+ .from(schema.user)
51
+ .where(eq(schema.user.email, email))
52
+ .limit(1);
53
+
54
+ return user || null;
55
+ } catch (error) {
56
+ throw ErrorFactory.databaseError({
57
+ operation: 'getUserByEmail',
58
+ email,
59
+ originalError: error instanceof Error ? error.message : String(error)
60
+ });
61
+ }
62
+ };
63
+
64
+ export const isUserExistByEmail = async (email: string): Promise<boolean> => {
65
+ try {
66
+ const [result] = await db
67
+ .select({ id: schema.user.id })
68
+ .from(schema.user)
69
+ .where(eq(schema.user.email, email))
70
+ .limit(1);
71
+
72
+ return !!result;
73
+ } catch (error) {
74
+ throw ErrorFactory.databaseError({
75
+ operation: 'isUserExistByEmail',
76
+ email,
77
+ originalError: error instanceof Error ? error.message : String(error)
78
+ });
79
+ }
80
+ };
81
+
82
+ export const updateUserProfile = async (
83
+ userId: string,
84
+ data: { name?: string }
85
+ ) => {
86
+ try {
87
+ const [user] = await db
88
+ .update(schema.user)
89
+ .set({
90
+ ...data,
91
+ updatedAt: new Date(),
92
+ })
93
+ .where(eq(schema.user.id, userId))
94
+ .returning({
95
+ id: schema.user.id,
96
+ email: schema.user.email,
97
+ emailVerified: schema.user.emailVerified,
98
+ name: schema.user.name,
99
+ image: schema.user.image,
100
+ createdAt: schema.user.createdAt,
101
+ updatedAt: schema.user.updatedAt,
102
+ });
103
+
104
+ return user;
105
+ } catch (error) {
106
+ throw ErrorFactory.databaseError({
107
+ operation: 'updateUserProfile',
108
+ userId,
109
+ updateData: data,
110
+ originalError: error instanceof Error ? error.message : String(error)
111
+ });
112
+ }
113
+ };
114
+
115
+ export const deleteUser = async (userId: string): Promise<void> => {
116
+ try {
117
+ await db
118
+ .delete(schema.user)
119
+ .where(eq(schema.user.id, userId));
120
+ } catch (error) {
121
+ throw ErrorFactory.databaseError({
122
+ operation: 'deleteUser',
123
+ userId,
124
+ originalError: error instanceof Error ? error.message : String(error)
125
+ });
126
+ }
127
+ };