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,180 @@
1
+ import type { AuthFastifyRequest } from "fastify";
2
+ import { FastifyPluginAsync, FastifyRequest } from "fastify";
3
+ import { auth } from "../../../lib/auth";
4
+ import { Type } from "@sinclair/typebox";
5
+ import { updateUserProfile, deleteUser } from "../../../domain/user/repository";
6
+ import { findUserSessionById, revokeSessionByToken } from "../../../domain/session/repository";
7
+
8
+ /**
9
+ * Convert Fastify request to Fetch API Request for BetterAuth
10
+ * BetterAuth expects a standard Fetch API Request object
11
+ */
12
+ function toFetchRequest(request: FastifyRequest): Request {
13
+ // Construct full URL
14
+ const url = `${request.protocol}://${request.hostname}${request.url}`;
15
+
16
+ // Convert Fastify headers to Fetch API Headers
17
+ const headers = new Headers();
18
+ Object.entries(request.headers).forEach(([key, value]) => {
19
+ if (value) {
20
+ headers.set(key, Array.isArray(value) ? value[0] : value);
21
+ }
22
+ });
23
+
24
+ // Create Fetch API compatible Request
25
+ // Note: GET and HEAD requests cannot have a body
26
+ const hasBody = request.method !== "GET" && request.method !== "HEAD" && request.body;
27
+
28
+ return new Request(url, {
29
+ method: request.method,
30
+ headers,
31
+ body: hasBody ? JSON.stringify(request.body) : undefined,
32
+ });
33
+ }
34
+
35
+ const authRoutes: FastifyPluginAsync = async (server) => {
36
+ // Mount BetterAuth handler for all /* routes under this plugin
37
+ // Since this plugin is registered at /api/auth, BetterAuth handles /api/auth/*
38
+ // BetterAuth handles: sign-in, sign-up, sign-out, oauth callbacks, etc.
39
+ server.all("/*", async (request, reply) => {
40
+ // Convert Fastify request to Fetch API Request for BetterAuth
41
+ const fetchRequest = toFetchRequest(request);
42
+ const response = await auth.handler(fetchRequest);
43
+
44
+ // Copy response headers
45
+ response.headers.forEach((value, key) => {
46
+ reply.header(key, value);
47
+ });
48
+
49
+ // Send response body
50
+ // Use text() to get the response body as BetterAuth may return JSON or other content
51
+ const body = await response.text();
52
+ reply.status(response.status).send(body);
53
+ });
54
+
55
+ // Keep custom endpoints that extend BetterAuth functionality
56
+
57
+ // Get current user (uses BetterAuth session)
58
+ server.get(
59
+ "/me",
60
+ {
61
+ onRequest: server.requireAuth,
62
+ schema: {
63
+ response: {
64
+ 200: Type.Object({
65
+ id: Type.String(),
66
+ email: Type.String(),
67
+ name: Type.Optional(Type.Union([Type.String(), Type.Null()])),
68
+ emailVerified: Type.Boolean(),
69
+ image: Type.Optional(Type.Union([Type.String(), Type.Null()])),
70
+ createdAt: Type.String(),
71
+ updatedAt: Type.String(),
72
+ }),
73
+ },
74
+ },
75
+ },
76
+ async (request, reply) => {
77
+ // user is attached by requireAuth middleware
78
+ const { user } = request as AuthFastifyRequest;
79
+ return reply.send({
80
+ id: user.id,
81
+ email: user.email,
82
+ name: user.name,
83
+ emailVerified: user.emailVerified,
84
+ image: user.image,
85
+ createdAt: user.createdAt.toISOString(),
86
+ updatedAt: user.updatedAt.toISOString(),
87
+ });
88
+ }
89
+ );
90
+
91
+ // Update profile (custom endpoint, BetterAuth doesn't provide this)
92
+ server.put(
93
+ "/profile",
94
+ {
95
+ onRequest: server.requireAuth,
96
+ schema: {
97
+ body: Type.Object({
98
+ name: Type.Optional(Type.String()),
99
+ }),
100
+ },
101
+ },
102
+ async (request, reply) => {
103
+ const { user } = request as AuthFastifyRequest;
104
+ const { name } = request.body as { name?: string };
105
+
106
+ const updatedUser = await updateUserProfile(user.id, { name });
107
+
108
+ return reply.send({
109
+ id: updatedUser.id,
110
+ email: updatedUser.email,
111
+ name: updatedUser.name,
112
+ emailVerified: updatedUser.emailVerified,
113
+ image: updatedUser.image,
114
+ createdAt: updatedUser.createdAt.toISOString(),
115
+ updatedAt: updatedUser.updatedAt.toISOString(),
116
+ });
117
+ }
118
+ );
119
+
120
+ // Delete account
121
+ server.delete(
122
+ "/account",
123
+ {
124
+ onRequest: server.requireAuth,
125
+ },
126
+ async (request, reply) => {
127
+ const { user } = request as AuthFastifyRequest;
128
+
129
+ await deleteUser(user.id);
130
+
131
+ return reply.send({ message: "Account deleted successfully" });
132
+ }
133
+ );
134
+
135
+ // Revoke a specific session by ID (BFF endpoint)
136
+ // Better Auth's native revokeSession requires the token, but list-sessions
137
+ // doesn't expose tokens for security. This endpoint looks up the token by ID
138
+ // and calls the native Better Auth revocation.
139
+ server.post(
140
+ "/revoke-session-by-id",
141
+ {
142
+ onRequest: server.requireAuth,
143
+ schema: {
144
+ body: Type.Object({
145
+ sessionId: Type.String(),
146
+ }),
147
+ },
148
+ },
149
+ async (request, reply) => {
150
+ const { user } = request as AuthFastifyRequest;
151
+ const { sessionId } = request.body as { sessionId: string };
152
+
153
+ // Find the session (validates it belongs to user)
154
+ const session = await findUserSessionById(sessionId, user.id);
155
+
156
+ if (!session) {
157
+ return reply.status(404).send({ error: "Session not found" });
158
+ }
159
+
160
+ // Revoke using Better Auth's native method
161
+ const response = await revokeSessionByToken(
162
+ session.token,
163
+ request.headers as unknown as Headers
164
+ );
165
+
166
+ return reply.send(response);
167
+ }
168
+ );
169
+
170
+ // Health check
171
+ server.get("/health", async (_request, reply) => {
172
+ return reply.send({
173
+ status: "ok",
174
+ timestamp: new Date().toISOString(),
175
+ service: "auth",
176
+ });
177
+ });
178
+ };
179
+
180
+ export default authRoutes;
@@ -0,0 +1,197 @@
1
+ import type { DeviceSessionFastifyRequest } from "fastify";
2
+ import { FastifyPluginAsync } from "fastify";
3
+ import {
4
+ CreateDeviceSessionBodySchema,
5
+ CreateDeviceSessionResponseSchema,
6
+ ValidateDeviceSessionBodySchema,
7
+ DeviceSessionValidationResponseSchema,
8
+ UpdateDeviceSessionActivityBodySchema,
9
+ DeviceSessionMigrationEligibilityResponseSchema,
10
+ } from "../../../domain/device-session/schema";
11
+ import {
12
+ createDeviceSession,
13
+ validateDeviceSession,
14
+ updateDeviceSessionActivity,
15
+ validateDeviceSessionMigrationEligibility,
16
+ deleteDeviceSession,
17
+ cleanupExpiredDeviceSessions,
18
+ } from "../../../domain/device-session/repository";
19
+ import { Type } from "@sinclair/typebox";
20
+
21
+ const deviceSessionRoutes: FastifyPluginAsync = async (server) => {
22
+ // Create new anonymous device session
23
+ server.post<{
24
+ Body: typeof CreateDeviceSessionBodySchema._type;
25
+ Reply: typeof CreateDeviceSessionResponseSchema._type;
26
+ }>(
27
+ "/",
28
+ {
29
+ schema: {
30
+ body: CreateDeviceSessionBodySchema,
31
+ response: {
32
+ 201: CreateDeviceSessionResponseSchema,
33
+ },
34
+ },
35
+ },
36
+ async (request, reply) => {
37
+ const { deviceId } = request.body as typeof CreateDeviceSessionBodySchema._type;
38
+
39
+ const result = await createDeviceSession({ deviceId });
40
+ return reply.status(201).send(result);
41
+ }
42
+ );
43
+
44
+ // Validate device session token
45
+ server.post<{
46
+ Body: typeof ValidateDeviceSessionBodySchema._type;
47
+ Reply: typeof DeviceSessionValidationResponseSchema._type;
48
+ }>(
49
+ "/validate",
50
+ {
51
+ schema: {
52
+ body: ValidateDeviceSessionBodySchema,
53
+ response: {
54
+ 200: DeviceSessionValidationResponseSchema,
55
+ },
56
+ },
57
+ },
58
+ async (request, reply) => {
59
+ const { sessionToken } = request.body as typeof ValidateDeviceSessionBodySchema._type;
60
+
61
+ const session = await validateDeviceSession(sessionToken);
62
+ return reply.status(200).send({
63
+ valid: !!session,
64
+ session: session || undefined,
65
+ });
66
+ }
67
+ );
68
+
69
+ // Update device session activity (heartbeat)
70
+ server.put<{
71
+ Body: typeof UpdateDeviceSessionActivityBodySchema._type;
72
+ }>(
73
+ "/activity",
74
+ {
75
+ schema: {
76
+ body: UpdateDeviceSessionActivityBodySchema,
77
+ response: {
78
+ 200: Type.Object({
79
+ message: Type.String(),
80
+ }),
81
+ },
82
+ },
83
+ },
84
+ async (request, reply) => {
85
+ const { sessionToken } = request.body as typeof UpdateDeviceSessionActivityBodySchema._type;
86
+
87
+ await updateDeviceSessionActivity(sessionToken);
88
+ return reply.status(200).send({ message: "Device session activity updated" });
89
+ }
90
+ );
91
+
92
+ // Get device session info (requires device session auth)
93
+ server.get<{
94
+ Reply: typeof DeviceSessionMigrationEligibilityResponseSchema._type;
95
+ }>(
96
+ "/info",
97
+ {
98
+ onRequest: server.requireDeviceSession,
99
+ schema: {
100
+ response: {
101
+ 200: DeviceSessionMigrationEligibilityResponseSchema,
102
+ },
103
+ },
104
+ },
105
+ async (request, reply) => {
106
+ const sessionRequest = request as DeviceSessionFastifyRequest;
107
+ const sessionToken = sessionRequest.sessionToken;
108
+
109
+ const eligibility = await validateDeviceSessionMigrationEligibility(sessionToken);
110
+ return reply.status(200).send(eligibility);
111
+ }
112
+ );
113
+
114
+ // Check migration eligibility
115
+ server.post<{
116
+ Body: typeof ValidateDeviceSessionBodySchema._type;
117
+ Reply: typeof DeviceSessionMigrationEligibilityResponseSchema._type;
118
+ }>(
119
+ "/migration-eligibility",
120
+ {
121
+ schema: {
122
+ body: ValidateDeviceSessionBodySchema,
123
+ response: {
124
+ 200: DeviceSessionMigrationEligibilityResponseSchema,
125
+ },
126
+ },
127
+ },
128
+ async (request, reply) => {
129
+ const { sessionToken } = request.body as typeof ValidateDeviceSessionBodySchema._type;
130
+
131
+ const eligibility = await validateDeviceSessionMigrationEligibility(sessionToken);
132
+ return reply.status(200).send(eligibility);
133
+ }
134
+ );
135
+
136
+ // Delete device session
137
+ server.delete<{
138
+ Body: typeof ValidateDeviceSessionBodySchema._type;
139
+ }>(
140
+ "/",
141
+ {
142
+ schema: {
143
+ body: ValidateDeviceSessionBodySchema,
144
+ response: {
145
+ 200: Type.Object({
146
+ message: Type.String(),
147
+ }),
148
+ },
149
+ },
150
+ },
151
+ async (request, reply) => {
152
+ const { sessionToken } = request.body as typeof ValidateDeviceSessionBodySchema._type;
153
+
154
+ await deleteDeviceSession(sessionToken);
155
+ return reply.status(200).send({ message: "Device session deleted successfully" });
156
+ }
157
+ );
158
+
159
+ // Device session cleanup endpoint (for admin/cron use)
160
+ server.delete<{
161
+ Reply: {
162
+ message: string;
163
+ deletedCount: number;
164
+ };
165
+ }>(
166
+ "/cleanup",
167
+ {
168
+ // Note: In production, this should be protected by admin auth or API key
169
+ schema: {
170
+ response: {
171
+ 200: Type.Object({
172
+ message: Type.String(),
173
+ deletedCount: Type.Number(),
174
+ }),
175
+ },
176
+ },
177
+ },
178
+ async (request, reply) => {
179
+ const deletedCount = await cleanupExpiredDeviceSessions();
180
+ return reply.status(200).send({
181
+ message: `Cleaned up ${deletedCount} expired device sessions`,
182
+ deletedCount,
183
+ });
184
+ }
185
+ );
186
+
187
+ // Health check endpoint
188
+ server.get("/health", async (request, reply) => {
189
+ return reply.code(200).send({
190
+ status: "ok",
191
+ timestamp: new Date().toISOString(),
192
+ service: "device-session"
193
+ });
194
+ });
195
+ };
196
+
197
+ export default deviceSessionRoutes;