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,334 @@
1
+ 'use server';
2
+
3
+ import {
4
+ setSessionCookie,
5
+ getSessionToken,
6
+ clearSessionCookie,
7
+ getDeviceSessionToken,
8
+ } from './cookies';
9
+ import { AUTH_CONFIG, BETTER_AUTH_COOKIE_NAME } from './config';
10
+
11
+ // Types
12
+ export interface User {
13
+ id: string;
14
+ email: string;
15
+ name: string | null;
16
+ emailVerified: boolean;
17
+ image: string | null;
18
+ createdAt: string;
19
+ updatedAt: string;
20
+ }
21
+
22
+ export interface Session {
23
+ id: string;
24
+ userId: string;
25
+ token: string;
26
+ expiresAt: string;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ ipAddress?: string;
30
+ userAgent?: string;
31
+ }
32
+
33
+ export interface AuthSession {
34
+ user: User;
35
+ session: Session;
36
+ }
37
+
38
+ interface SignInResult {
39
+ success: boolean;
40
+ error?: string;
41
+ session?: AuthSession;
42
+ }
43
+
44
+ interface SignUpResult {
45
+ success: boolean;
46
+ error?: string;
47
+ <% if (features.authentication.emailVerification) { %>
48
+ needsEmailVerification?: boolean;
49
+ <% } %>
50
+ }
51
+
52
+ /**
53
+ * Build headers for Fastify requests
54
+ * Includes session token in Better Auth cookie format and Origin for CSRF protection.
55
+ * Exported for reuse by other modules (e.g., sessions.ts).
56
+ */
57
+ export async function buildAuthHeaders(): Promise<HeadersInit> {
58
+ const headers: Record<string, string> = {
59
+ 'Content-Type': 'application/json',
60
+ // Origin header required for Better Auth CSRF protection
61
+ // Server Actions run in Node.js and don't automatically include Origin
62
+ 'Origin': AUTH_CONFIG.appUrl,
63
+ };
64
+
65
+ const sessionToken = await getSessionToken();
66
+ if (sessionToken) {
67
+ headers['Cookie'] = `${BETTER_AUTH_COOKIE_NAME}=${sessionToken}`;
68
+ }
69
+
70
+ const deviceToken = await getDeviceSessionToken();
71
+ if (deviceToken) {
72
+ headers['X-Device-Session-Token'] = deviceToken;
73
+ }
74
+
75
+ return headers;
76
+ }
77
+
78
+ /**
79
+ * Sign in with email and password
80
+ */
81
+ export async function signIn(email: string, password: string): Promise<SignInResult> {
82
+ try {
83
+ const response = await fetch(`${AUTH_CONFIG.backendUrl}/api/auth/sign-in/email`, {
84
+ method: 'POST',
85
+ headers: await buildAuthHeaders(),
86
+ body: JSON.stringify({ email, password }),
87
+ cache: 'no-store',
88
+ });
89
+
90
+ if (!response.ok) {
91
+ const error = await response.json().catch(() => ({}));
92
+ return {
93
+ success: false,
94
+ error: error.message || 'Invalid email or password',
95
+ };
96
+ }
97
+
98
+ // Extract session token from Set-Cookie header
99
+ const sessionToken = extractSessionToken(response);
100
+
101
+ if (!sessionToken) {
102
+ return { success: false, error: 'No session token received' };
103
+ }
104
+
105
+ // Set session cookie
106
+ await setSessionCookie(sessionToken);
107
+
108
+ // Get session data
109
+ const session = await getSession();
110
+
111
+ return { success: true, session: session || undefined };
112
+ } catch (error) {
113
+ console.error('Sign in error:', error);
114
+ return { success: false, error: 'An error occurred during sign in' };
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Sign up with email and password
120
+ */
121
+ export async function signUp(
122
+ email: string,
123
+ password: string,
124
+ name?: string
125
+ ): Promise<SignUpResult> {
126
+ try {
127
+ const response = await fetch(`${AUTH_CONFIG.backendUrl}/api/auth/sign-up/email`, {
128
+ method: 'POST',
129
+ headers: await buildAuthHeaders(),
130
+ body: JSON.stringify({ email, password, name }),
131
+ cache: 'no-store',
132
+ });
133
+
134
+ if (!response.ok) {
135
+ const error = await response.json().catch(() => ({}));
136
+ return {
137
+ success: false,
138
+ error: error.message || 'Failed to create account',
139
+ };
140
+ }
141
+
142
+ <% if (features.authentication.emailVerification) { %>
143
+ const data = await response.json();
144
+
145
+ // Check if email verification is required
146
+ if (data.emailVerificationRequired) {
147
+ return { success: true, needsEmailVerification: true };
148
+ }
149
+ <% } %>
150
+
151
+ // Extract and set session token
152
+ const sessionToken = extractSessionToken(response);
153
+
154
+ if (sessionToken) {
155
+ await setSessionCookie(sessionToken);
156
+ }
157
+
158
+ return { success: true };
159
+ } catch (error) {
160
+ console.error('Sign up error:', error);
161
+ return { success: false, error: 'An error occurred during sign up' };
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Sign out current session
167
+ */
168
+ export async function signOut(): Promise<{ success: boolean }> {
169
+ try {
170
+ await fetch(`${AUTH_CONFIG.backendUrl}/api/auth/sign-out`, {
171
+ method: 'POST',
172
+ headers: await buildAuthHeaders(),
173
+ body: JSON.stringify({}),
174
+ cache: 'no-store',
175
+ });
176
+ } catch (error) {
177
+ console.error('Sign out error:', error);
178
+ }
179
+
180
+ // Always clear local cookie
181
+ await clearSessionCookie();
182
+
183
+ return { success: true };
184
+ }
185
+
186
+ /**
187
+ * Get current session (validates with backend)
188
+ *
189
+ * Note: For client-side caching, use the AuthProvider which
190
+ * maintains a cache to avoid hitting the backend on every render.
191
+ */
192
+ export async function getSession(): Promise<AuthSession | null> {
193
+ const sessionToken = await getSessionToken();
194
+
195
+ if (!sessionToken) {
196
+ return null;
197
+ }
198
+
199
+ try {
200
+ const response = await fetch(`${AUTH_CONFIG.backendUrl}/api/auth/get-session`, {
201
+ method: 'GET',
202
+ headers: await buildAuthHeaders(),
203
+ cache: 'no-store',
204
+ });
205
+
206
+ if (!response.ok) {
207
+ // Session invalid, clear cookie
208
+ await clearSessionCookie();
209
+ return null;
210
+ }
211
+
212
+ const data = await response.json();
213
+ return data as AuthSession;
214
+ } catch (error) {
215
+ console.error('Get session error:', error);
216
+ return null;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Request password reset email
222
+ */
223
+ export async function requestPasswordReset(
224
+ email: string
225
+ ): Promise<{ success: boolean; error?: string }> {
226
+ try {
227
+ const response = await fetch(`${AUTH_CONFIG.backendUrl}/api/auth/forget-password`, {
228
+ method: 'POST',
229
+ headers: { 'Content-Type': 'application/json' },
230
+ body: JSON.stringify({
231
+ email,
232
+ redirectTo: `${AUTH_CONFIG.appUrl}/reset-password`,
233
+ }),
234
+ cache: 'no-store',
235
+ });
236
+
237
+ if (!response.ok) {
238
+ const error = await response.json().catch(() => ({}));
239
+ return { success: false, error: error.message || 'Failed to send reset email' };
240
+ }
241
+
242
+ return { success: true };
243
+ } catch (error) {
244
+ console.error('Password reset request error:', error);
245
+ return { success: false, error: 'An error occurred' };
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Reset password with token
251
+ */
252
+ export async function resetPassword(
253
+ token: string,
254
+ newPassword: string
255
+ ): Promise<{ success: boolean; error?: string }> {
256
+ try {
257
+ const response = await fetch(`${AUTH_CONFIG.backendUrl}/api/auth/reset-password`, {
258
+ method: 'POST',
259
+ headers: { 'Content-Type': 'application/json' },
260
+ body: JSON.stringify({ token, newPassword }),
261
+ cache: 'no-store',
262
+ });
263
+
264
+ if (!response.ok) {
265
+ const error = await response.json().catch(() => ({}));
266
+ return { success: false, error: error.message || 'Failed to reset password' };
267
+ }
268
+
269
+ return { success: true };
270
+ } catch (error) {
271
+ console.error('Password reset error:', error);
272
+ return { success: false, error: 'An error occurred' };
273
+ }
274
+ }
275
+
276
+ <% if (features.authentication.emailVerification) { %>
277
+ /**
278
+ * Verify email with token
279
+ */
280
+ export async function verifyEmail(token: string): Promise<{ success: boolean; error?: string }> {
281
+ try {
282
+ const response = await fetch(`${AUTH_CONFIG.backendUrl}/api/auth/verify-email`, {
283
+ method: 'POST',
284
+ headers: { 'Content-Type': 'application/json' },
285
+ body: JSON.stringify({ token }),
286
+ cache: 'no-store',
287
+ });
288
+
289
+ if (!response.ok) {
290
+ const error = await response.json().catch(() => ({}));
291
+ return { success: false, error: error.message || 'Failed to verify email' };
292
+ }
293
+
294
+ return { success: true };
295
+ } catch (error) {
296
+ console.error('Email verification error:', error);
297
+ return { success: false, error: 'An error occurred' };
298
+ }
299
+ }
300
+ <% } %>
301
+
302
+ /**
303
+ * Extract session token from Set-Cookie header
304
+ *
305
+ * IMPORTANT: In Node.js 20+, use headers.getSetCookie() to get an array of cookies.
306
+ * The headers.get("set-cookie") method joins cookies with commas, which breaks
307
+ * parsing because cookie attributes like Expires contain commas (e.g., "Thu, 01 Jan 2025").
308
+ */
309
+ function extractSessionToken(response: Response): string | null {
310
+ // Use getSetCookie() for proper cookie array handling (Node.js 20+)
311
+ // This returns an array of individual Set-Cookie header values
312
+ const setCookies = response.headers.getSetCookie?.() ?? [];
313
+
314
+ // Fallback for older Node.js versions or environments without getSetCookie
315
+ if (setCookies.length === 0) {
316
+ const setCookieHeader = response.headers.get('set-cookie');
317
+ if (!setCookieHeader) return null;
318
+
319
+ // Manual parsing fallback - split on ", " followed by a cookie name pattern
320
+ // This handles most cases but getSetCookie() is preferred
321
+ const cookiePattern = new RegExp(`${BETTER_AUTH_COOKIE_NAME}=([^;]+)`);
322
+ const match = setCookieHeader.match(cookiePattern);
323
+ return match ? match[1] : null;
324
+ }
325
+
326
+ for (const cookie of setCookies) {
327
+ const match = cookie.match(new RegExp(`${BETTER_AUTH_COOKIE_NAME}=([^;]+)`));
328
+ if (match) {
329
+ return match[1];
330
+ }
331
+ }
332
+
333
+ return null;
334
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Auth configuration and constants
3
+ *
4
+ * Centralized configuration for authentication.
5
+ * Cookie names are constants to avoid magic strings scattered across the codebase.
6
+ */
7
+
8
+ export const AUTH_CONFIG = {
9
+ // Public URL of the web app (for OAuth callbacks)
10
+ appUrl: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
11
+
12
+ // Backend URL for server-to-server calls
13
+ // In development, you can use the Next.js proxy or direct URL
14
+ backendUrl: process.env.BACKEND_URL || "http://localhost:8080",
15
+
16
+ // Session cookie max age in seconds (should match Fastify config)
17
+ sessionMaxAge: 60 * 60 * 24 * 7, // 7 days
18
+
19
+ // Device session cookie max age
20
+ deviceSessionMaxAge: 60 * 60 * 24 * 30, // 30 days
21
+
22
+ // Device session heartbeat interval (client-side)
23
+ deviceHeartbeatInterval: 5 * 60 * 1000, // 5 minutes
24
+
25
+ // Enabled OAuth providers (from template config)
26
+ <% if (features.authentication.providers.google) { %>
27
+ googleEnabled: true,
28
+ <% } else { %>
29
+ googleEnabled: false,
30
+ <% } %>
31
+ <% if (features.authentication.providers.apple) { %>
32
+ appleEnabled: true,
33
+ <% } else { %>
34
+ appleEnabled: false,
35
+ <% } %>
36
+ <% if (features.authentication.providers.github) { %>
37
+ githubEnabled: true,
38
+ <% } else { %>
39
+ githubEnabled: false,
40
+ <% } %>
41
+ } as const;
42
+
43
+ /**
44
+ * Cookie names used throughout the auth system
45
+ * Centralized to ensure consistency and make updates easier
46
+ */
47
+ export const COOKIE_NAMES = {
48
+ // Session token from Better Auth (stored after sign in)
49
+ SESSION: "session_token",
50
+
51
+ // Device session token (for anonymous sessions)
52
+ DEVICE_SESSION: "device_session_token",
53
+
54
+ // OAuth PKCE verifier (temporary, during OAuth flow)
55
+ OAUTH_PKCE_VERIFIER: "oauth_pkce_verifier",
56
+
57
+ // OAuth state for CSRF protection (temporary, during OAuth flow)
58
+ OAUTH_STATE: "oauth_state",
59
+ } as const;
60
+
61
+ /**
62
+ * Better Auth cookie name (used when reading cookies set by Fastify)
63
+ * This must match the cookie name that Better Auth uses
64
+ */
65
+ export const BETTER_AUTH_COOKIE_NAME = "better-auth.session_token";
@@ -0,0 +1,74 @@
1
+ import { cookies } from 'next/headers';
2
+ import { AUTH_CONFIG, COOKIE_NAMES } from './config';
3
+
4
+ /**
5
+ * Set the session cookie after successful authentication
6
+ */
7
+ export async function setSessionCookie(sessionToken: string): Promise<void> {
8
+ const cookieStore = await cookies();
9
+
10
+ cookieStore.set(COOKIE_NAMES.SESSION, sessionToken, {
11
+ httpOnly: true,
12
+ secure: process.env.NODE_ENV === 'production',
13
+ sameSite: 'lax',
14
+ path: '/',
15
+ maxAge: AUTH_CONFIG.sessionMaxAge,
16
+ });
17
+ }
18
+
19
+ /**
20
+ * Get the session token from cookies
21
+ */
22
+ export async function getSessionToken(): Promise<string | undefined> {
23
+ const cookieStore = await cookies();
24
+ return cookieStore.get(COOKIE_NAMES.SESSION)?.value;
25
+ }
26
+
27
+ /**
28
+ * Clear the session cookie (for sign out)
29
+ */
30
+ export async function clearSessionCookie(): Promise<void> {
31
+ const cookieStore = await cookies();
32
+ cookieStore.delete(COOKIE_NAMES.SESSION);
33
+ }
34
+
35
+ /**
36
+ * Clear all auth-related cookies
37
+ */
38
+ export async function clearAuthCookies(): Promise<void> {
39
+ const cookieStore = await cookies();
40
+ cookieStore.delete(COOKIE_NAMES.SESSION);
41
+ cookieStore.delete(COOKIE_NAMES.OAUTH_PKCE_VERIFIER);
42
+ cookieStore.delete(COOKIE_NAMES.OAUTH_STATE);
43
+ }
44
+
45
+ /**
46
+ * Set device session cookie
47
+ */
48
+ export async function setDeviceSessionCookie(token: string): Promise<void> {
49
+ const cookieStore = await cookies();
50
+
51
+ cookieStore.set(COOKIE_NAMES.DEVICE_SESSION, token, {
52
+ httpOnly: true,
53
+ secure: process.env.NODE_ENV === 'production',
54
+ sameSite: 'lax',
55
+ path: '/',
56
+ maxAge: AUTH_CONFIG.deviceSessionMaxAge,
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Get device session token from cookies
62
+ */
63
+ export async function getDeviceSessionToken(): Promise<string | undefined> {
64
+ const cookieStore = await cookies();
65
+ return cookieStore.get(COOKIE_NAMES.DEVICE_SESSION)?.value;
66
+ }
67
+
68
+ /**
69
+ * Clear device session cookie
70
+ */
71
+ export async function clearDeviceSessionCookie(): Promise<void> {
72
+ const cookieStore = await cookies();
73
+ cookieStore.delete(COOKIE_NAMES.DEVICE_SESSION);
74
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Auth module exports
3
+ *
4
+ * This barrel file provides a clean public API for the auth module.
5
+ * Import from '@/lib/auth' instead of individual files.
6
+ */
7
+
8
+ // Configuration
9
+ export { AUTH_CONFIG, COOKIE_NAMES, BETTER_AUTH_COOKIE_NAME } from "./config";
10
+
11
+ // Server actions for authentication
12
+ export {
13
+ signIn,
14
+ signUp,
15
+ signOut,
16
+ getSession,
17
+ requestPasswordReset,
18
+ resetPassword,
19
+ buildAuthHeaders,
20
+ <% if (features.authentication.emailVerification) { %>
21
+ verifyEmail,
22
+ <% } %>
23
+ type User,
24
+ type Session,
25
+ type AuthSession,
26
+ } from "./actions";
27
+
28
+ // OAuth actions
29
+ export { initiateOAuth, getOAuthUrl } from "./oauth";
30
+
31
+ // Cookie utilities (server-side only)
32
+ export {
33
+ setSessionCookie,
34
+ getSessionToken,
35
+ clearSessionCookie,
36
+ clearAuthCookies,
37
+ } from "./cookies";
38
+
39
+ // PKCE utilities (server-side only)
40
+ export { generatePKCE, verifyPKCE } from "./pkce";
@@ -0,0 +1,72 @@
1
+ 'use server';
2
+
3
+ import { cookies } from 'next/headers';
4
+ import crypto from 'crypto';
5
+ import { generatePKCE } from './pkce';
6
+ import { AUTH_CONFIG, COOKIE_NAMES } from './config';
7
+
8
+ type OAuthProvider = 'google' | 'apple' | 'github';
9
+
10
+ interface InitiateOAuthResult {
11
+ url: string;
12
+ }
13
+
14
+ /**
15
+ * Initiate OAuth flow with PKCE
16
+ *
17
+ * Generates PKCE parameters, stores verifier in secure cookie,
18
+ * and returns the OAuth initiation URL.
19
+ *
20
+ * The browser will redirect to this URL, which starts the OAuth flow.
21
+ */
22
+ export async function initiateOAuth(provider: OAuthProvider): Promise<InitiateOAuthResult> {
23
+ const { verifier, challenge } = generatePKCE();
24
+ const state = crypto.randomUUID();
25
+
26
+ const cookieStore = await cookies();
27
+
28
+ // Store verifier in httpOnly cookie
29
+ // This cookie travels with the browser and is read during the callback
30
+ cookieStore.set(COOKIE_NAMES.OAUTH_PKCE_VERIFIER, verifier, {
31
+ httpOnly: true,
32
+ secure: process.env.NODE_ENV === 'production',
33
+ sameSite: 'lax',
34
+ path: '/',
35
+ maxAge: 600, // 10 minutes
36
+ });
37
+
38
+ // Store state for CSRF verification
39
+ cookieStore.set(COOKIE_NAMES.OAUTH_STATE, state, {
40
+ httpOnly: true,
41
+ secure: process.env.NODE_ENV === 'production',
42
+ sameSite: 'lax',
43
+ path: '/',
44
+ maxAge: 600,
45
+ });
46
+
47
+ // Build OAuth initiation URL
48
+ const callbackUrl = `${AUTH_CONFIG.appUrl}/auth/callback`;
49
+
50
+ const params = new URLSearchParams({
51
+ provider,
52
+ code_challenge: challenge,
53
+ code_challenge_method: 'S256',
54
+ state,
55
+ callback_url: callbackUrl,
56
+ });
57
+
58
+ const url = `${AUTH_CONFIG.backendUrl}/api/auth/web/oauth/init?${params}`;
59
+
60
+ return { url };
61
+ }
62
+
63
+ /**
64
+ * Get OAuth URL for direct navigation
65
+ *
66
+ * This is a convenience wrapper that can be used by OAuth buttons
67
+ * to get the URL for client-side redirect.
68
+ */
69
+ export async function getOAuthUrl(provider: OAuthProvider): Promise<string> {
70
+ const result = await initiateOAuth(provider);
71
+ return result.url;
72
+ }
@@ -0,0 +1,48 @@
1
+ import crypto from 'crypto';
2
+
3
+ /**
4
+ * Generate PKCE (Proof Key for Code Exchange) parameters
5
+ *
6
+ * PKCE prevents authorization code interception attacks by requiring
7
+ * the client to prove it initiated the OAuth request.
8
+ *
9
+ * The verifier is stored in a httpOnly cookie and sent during token exchange.
10
+ * The challenge is sent during OAuth initiation and stored server-side.
11
+ * When exchanging the token, the server verifies SHA256(verifier) === challenge.
12
+ *
13
+ * @returns Object containing verifier and challenge
14
+ */
15
+ export function generatePKCE(): { verifier: string; challenge: string } {
16
+ // Generate 32 bytes of random data for verifier (256 bits of entropy)
17
+ // This exceeds the PKCE spec minimum of 43 characters
18
+ const verifierBuffer = crypto.randomBytes(32);
19
+ const verifier = verifierBuffer.toString('base64url');
20
+
21
+ // SHA256 hash the verifier for the challenge
22
+ const challengeBuffer = crypto.createHash('sha256').update(verifier).digest();
23
+ const challenge = challengeBuffer.toString('base64url');
24
+
25
+ return { verifier, challenge };
26
+ }
27
+
28
+ /**
29
+ * Verify a PKCE verifier against a challenge
30
+ *
31
+ * This is primarily used for testing. In production, the verification
32
+ * happens on the Fastify server during token exchange.
33
+ *
34
+ * @param verifier - The original random string
35
+ * @param challenge - The SHA256 hash to verify against
36
+ * @returns true if the verifier produces the challenge
37
+ */
38
+ export function verifyPKCE(verifier: string, challenge: string): boolean {
39
+ const computedChallenge = crypto.createHash('sha256').update(verifier).digest('base64url');
40
+
41
+ // Use timing-safe comparison to prevent timing attacks
42
+ try {
43
+ return crypto.timingSafeEqual(Buffer.from(computedChallenge), Buffer.from(challenge));
44
+ } catch {
45
+ // Buffers have different lengths
46
+ return false;
47
+ }
48
+ }