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,198 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useSearchParams, useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { Button } from "@/components/ui/button";
7
+ import { verifyEmail } from "@/lib/auth/actions";
8
+
9
+ export default function VerifyEmailPage() {
10
+ const searchParams = useSearchParams();
11
+ const router = useRouter();
12
+ const token = searchParams.get("token");
13
+ const email = searchParams.get("email");
14
+
15
+ const [status, setStatus] = useState<"idle" | "verifying" | "success" | "error">(
16
+ token ? "verifying" : "idle"
17
+ );
18
+ const [error, setError] = useState<string | null>(null);
19
+
20
+ useEffect(() => {
21
+ if (token && status === "verifying") {
22
+ handleVerify(token);
23
+ }
24
+ }, [token, status]);
25
+
26
+ const handleVerify = async (verificationToken: string) => {
27
+ try {
28
+ const result = await verifyEmail(verificationToken);
29
+
30
+ if (!result.success) {
31
+ setStatus("error");
32
+ setError(result.error || "Failed to verify email");
33
+ return;
34
+ }
35
+
36
+ setStatus("success");
37
+ // Redirect to login after 3 seconds
38
+ setTimeout(() => {
39
+ router.push("/login?message=Email+verified+successfully");
40
+ }, 3000);
41
+ } catch (err) {
42
+ setStatus("error");
43
+ setError("An unexpected error occurred");
44
+ console.error("Email verification error:", err);
45
+ }
46
+ };
47
+
48
+ // Waiting for user to check their email
49
+ if (status === "idle" && email) {
50
+ return (
51
+ <div className="space-y-6">
52
+ <div className="space-y-2 text-center">
53
+ <div className="mx-auto w-12 h-12 bg-blue-100 dark:bg-blue-950/20 rounded-full flex items-center justify-center">
54
+ <svg
55
+ className="w-6 h-6 text-blue-600 dark:text-blue-400"
56
+ fill="none"
57
+ viewBox="0 0 24 24"
58
+ stroke="currentColor"
59
+ >
60
+ <path
61
+ strokeLinecap="round"
62
+ strokeLinejoin="round"
63
+ strokeWidth={2}
64
+ d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
65
+ />
66
+ </svg>
67
+ </div>
68
+ <h2 className="text-2xl font-bold">Check your email</h2>
69
+ <p className="text-muted-foreground">
70
+ We&apos;ve sent a verification link to <strong>{email}</strong>
71
+ </p>
72
+ </div>
73
+
74
+ <div className="text-sm text-muted-foreground text-center space-y-2">
75
+ <p>Click the link in the email to verify your account.</p>
76
+ <p>
77
+ Didn&apos;t receive the email? Check your spam folder.
78
+ </p>
79
+ </div>
80
+
81
+ <Link href="/login">
82
+ <Button variant="outline" className="w-full">
83
+ Back to sign in
84
+ </Button>
85
+ </Link>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ // Verifying token
91
+ if (status === "verifying") {
92
+ return (
93
+ <div className="space-y-6">
94
+ <div className="space-y-2 text-center">
95
+ <div className="mx-auto w-12 h-12 flex items-center justify-center">
96
+ <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
97
+ </div>
98
+ <h2 className="text-2xl font-bold">Verifying your email</h2>
99
+ <p className="text-muted-foreground">
100
+ Please wait while we verify your email address...
101
+ </p>
102
+ </div>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ // Verification successful
108
+ if (status === "success") {
109
+ return (
110
+ <div className="space-y-6">
111
+ <div className="space-y-2 text-center">
112
+ <div className="mx-auto w-12 h-12 bg-green-100 dark:bg-green-950/20 rounded-full flex items-center justify-center">
113
+ <svg
114
+ className="w-6 h-6 text-green-600 dark:text-green-400"
115
+ fill="none"
116
+ viewBox="0 0 24 24"
117
+ stroke="currentColor"
118
+ >
119
+ <path
120
+ strokeLinecap="round"
121
+ strokeLinejoin="round"
122
+ strokeWidth={2}
123
+ d="M5 13l4 4L19 7"
124
+ />
125
+ </svg>
126
+ </div>
127
+ <h2 className="text-2xl font-bold">Email verified!</h2>
128
+ <p className="text-muted-foreground">
129
+ Your email has been verified. Redirecting you to sign in...
130
+ </p>
131
+ </div>
132
+
133
+ <Link href="/login">
134
+ <Button className="w-full">Continue to sign in</Button>
135
+ </Link>
136
+ </div>
137
+ );
138
+ }
139
+
140
+ // Verification error
141
+ if (status === "error") {
142
+ return (
143
+ <div className="space-y-6">
144
+ <div className="space-y-2 text-center">
145
+ <div className="mx-auto w-12 h-12 bg-red-100 dark:bg-red-950/20 rounded-full flex items-center justify-center">
146
+ <svg
147
+ className="w-6 h-6 text-red-600 dark:text-red-400"
148
+ fill="none"
149
+ viewBox="0 0 24 24"
150
+ stroke="currentColor"
151
+ >
152
+ <path
153
+ strokeLinecap="round"
154
+ strokeLinejoin="round"
155
+ strokeWidth={2}
156
+ d="M6 18L18 6M6 6l12 12"
157
+ />
158
+ </svg>
159
+ </div>
160
+ <h2 className="text-2xl font-bold">Verification failed</h2>
161
+ <p className="text-muted-foreground">
162
+ {error || "Unable to verify your email address"}
163
+ </p>
164
+ </div>
165
+
166
+ <div className="space-y-3">
167
+ <p className="text-sm text-muted-foreground text-center">
168
+ The verification link may have expired or already been used.
169
+ </p>
170
+
171
+ <Link href="/login">
172
+ <Button variant="outline" className="w-full">
173
+ Back to sign in
174
+ </Button>
175
+ </Link>
176
+ </div>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ // No token or email - generic page
182
+ return (
183
+ <div className="space-y-6">
184
+ <div className="space-y-2 text-center">
185
+ <h2 className="text-2xl font-bold">Verify your email</h2>
186
+ <p className="text-muted-foreground">
187
+ Please check your email for a verification link.
188
+ </p>
189
+ </div>
190
+
191
+ <Link href="/login">
192
+ <Button variant="outline" className="w-full">
193
+ Back to sign in
194
+ </Button>
195
+ </Link>
196
+ </div>
197
+ );
198
+ }
@@ -0,0 +1,152 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { AUTH_CONFIG, COOKIE_NAMES } from '@/lib/auth/config';
3
+
4
+ /**
5
+ * OAuth Callback Route Handler
6
+ *
7
+ * This route handler processes the redirect from Fastify after OAuth authentication.
8
+ * It receives the exchange_token and state as query parameters.
9
+ *
10
+ * Using a Route Handler instead of a page component because we need to:
11
+ * 1. Read cookies from the request
12
+ * 2. Set/delete cookies on the response
13
+ * 3. Redirect to the appropriate page
14
+ *
15
+ * In Next.js 15+, cookies can only be modified in Server Actions or Route Handlers,
16
+ * not in React Server Components.
17
+ */
18
+ export async function GET(request: NextRequest) {
19
+ const searchParams = request.nextUrl.searchParams;
20
+ const exchangeToken = searchParams.get('exchange_token');
21
+ const state = searchParams.get('state');
22
+ const error = searchParams.get('error');
23
+
24
+ const baseUrl = AUTH_CONFIG.appUrl;
25
+
26
+ // Handle errors from OAuth provider or Fastify
27
+ if (error) {
28
+ const errorMessage = getErrorMessage(error);
29
+ return NextResponse.redirect(
30
+ new URL(`/login?error=${encodeURIComponent(errorMessage)}`, baseUrl)
31
+ );
32
+ }
33
+
34
+ // Validate required parameters
35
+ if (!exchangeToken || !state) {
36
+ return NextResponse.redirect(
37
+ new URL('/login?error=missing_params', baseUrl)
38
+ );
39
+ }
40
+
41
+ // Read PKCE cookies from request
42
+ const storedState = request.cookies.get(COOKIE_NAMES.OAUTH_STATE)?.value;
43
+ const verifier = request.cookies.get(COOKIE_NAMES.OAUTH_PKCE_VERIFIER)?.value;
44
+
45
+ // Verify state matches (CSRF protection)
46
+ if (storedState !== state) {
47
+ const response = NextResponse.redirect(
48
+ new URL('/login?error=state_mismatch', baseUrl)
49
+ );
50
+ // Clean up cookies
51
+ response.cookies.delete(COOKIE_NAMES.OAUTH_PKCE_VERIFIER);
52
+ response.cookies.delete(COOKIE_NAMES.OAUTH_STATE);
53
+ return response;
54
+ }
55
+
56
+ // Verify we have the PKCE verifier
57
+ if (!verifier) {
58
+ const response = NextResponse.redirect(
59
+ new URL('/login?error=missing_verifier', baseUrl)
60
+ );
61
+ response.cookies.delete(COOKIE_NAMES.OAUTH_STATE);
62
+ return response;
63
+ }
64
+
65
+ try {
66
+ // Exchange token with PKCE verification
67
+ const exchangeResponse = await fetch(
68
+ `${AUTH_CONFIG.backendUrl}/api/auth/web/oauth/exchange`,
69
+ {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({
73
+ exchange_token: exchangeToken,
74
+ code_verifier: verifier,
75
+ }),
76
+ cache: 'no-store',
77
+ }
78
+ );
79
+
80
+ if (!exchangeResponse.ok) {
81
+ const errorData = await exchangeResponse
82
+ .json()
83
+ .catch(() => ({ error: 'exchange_failed' }));
84
+ const response = NextResponse.redirect(
85
+ new URL(
86
+ `/login?error=${encodeURIComponent(errorData.error || 'exchange_failed')}`,
87
+ baseUrl
88
+ )
89
+ );
90
+ // Clean up cookies
91
+ response.cookies.delete(COOKIE_NAMES.OAUTH_PKCE_VERIFIER);
92
+ response.cookies.delete(COOKIE_NAMES.OAUTH_STATE);
93
+ return response;
94
+ }
95
+
96
+ const { session_token } = await exchangeResponse.json();
97
+
98
+ // Success! Create response with session cookie and redirect to dashboard
99
+ const response = NextResponse.redirect(new URL('/dashboard', baseUrl));
100
+
101
+ // Set session cookie
102
+ response.cookies.set(COOKIE_NAMES.SESSION, session_token, {
103
+ httpOnly: true,
104
+ secure: process.env.NODE_ENV === 'production',
105
+ sameSite: 'lax',
106
+ path: '/',
107
+ maxAge: AUTH_CONFIG.sessionMaxAge,
108
+ });
109
+
110
+ // Clean up PKCE cookies
111
+ response.cookies.delete(COOKIE_NAMES.OAUTH_PKCE_VERIFIER);
112
+ response.cookies.delete(COOKIE_NAMES.OAUTH_STATE);
113
+
114
+ return response;
115
+ } catch (error) {
116
+ console.error('OAuth exchange error:', error);
117
+ const response = NextResponse.redirect(
118
+ new URL('/login?error=exchange_failed', baseUrl)
119
+ );
120
+ // Clean up cookies
121
+ response.cookies.delete(COOKIE_NAMES.OAUTH_PKCE_VERIFIER);
122
+ response.cookies.delete(COOKIE_NAMES.OAUTH_STATE);
123
+ return response;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Convert error codes to user-friendly messages
129
+ */
130
+ function getErrorMessage(error: string): string {
131
+ const errorMessages: Record<string, string> = {
132
+ invalid_state: 'Session expired. Please try again.',
133
+ missing_verifier: 'Authentication error. Please try again.',
134
+ state_mismatch: 'Security check failed. Please try again.',
135
+ exchange_failed: 'Failed to complete sign in. Please try again.',
136
+ oauth_failed: 'OAuth provider error. Please try again.',
137
+ no_session: 'Failed to create session. Please try again.',
138
+ service_unavailable: 'Service temporarily unavailable. Please try later.',
139
+ server_error: 'An unexpected error occurred. Please try again.',
140
+ missing_params: 'Invalid callback. Please try again.',
141
+ oauth_access_denied: 'Access was denied. Please try again.',
142
+ };
143
+
144
+ // Handle oauth_* prefixed errors
145
+ if (error.startsWith('oauth_')) {
146
+ const specificError = errorMessages[error];
147
+ if (specificError) return specificError;
148
+ return 'OAuth error. Please try again.';
149
+ }
150
+
151
+ return errorMessages[error] || 'An error occurred. Please try again.';
152
+ }
@@ -0,0 +1,100 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Label } from "@/components/ui/label";
9
+ import { signIn } from "@/lib/auth/actions";
10
+ import { useAuthStore } from "@/store/auth.store";
11
+
12
+ interface LoginFormProps {
13
+ redirectTo?: string;
14
+ }
15
+
16
+ export function LoginForm({ redirectTo = "/dashboard" }: LoginFormProps) {
17
+ const router = useRouter();
18
+ const setSession = useAuthStore((s) => s.setSession);
19
+ const [email, setEmail] = useState("");
20
+ const [password, setPassword] = useState("");
21
+ const [error, setError] = useState<string | null>(null);
22
+ const [isLoading, setIsLoading] = useState(false);
23
+
24
+ const handleSubmit = async (e: React.FormEvent) => {
25
+ e.preventDefault();
26
+ setError(null);
27
+ setIsLoading(true);
28
+
29
+ try {
30
+ const result = await signIn(email, password);
31
+ if (!result.success) {
32
+ setError(result.error || "Failed to sign in");
33
+ return;
34
+ }
35
+ if (result.session) setSession(result.session);
36
+ router.push(redirectTo);
37
+ router.refresh();
38
+ } catch {
39
+ setError("An unexpected error occurred");
40
+ } finally {
41
+ setIsLoading(false);
42
+ }
43
+ };
44
+
45
+ return (
46
+ <form onSubmit={handleSubmit} className="space-y-3">
47
+ <div className="space-y-1.5">
48
+ <Label htmlFor="email" className="text-sm">Email</Label>
49
+ <Input
50
+ id="email"
51
+ type="email"
52
+ value={email}
53
+ onChange={(e) => setEmail(e.target.value)}
54
+ placeholder="you@example.com"
55
+ required
56
+ disabled={isLoading}
57
+ autoComplete="email"
58
+ />
59
+ </div>
60
+
61
+ <div className="space-y-1.5">
62
+ <div className="flex items-center justify-between">
63
+ <Label htmlFor="password" className="text-sm">Password</Label>
64
+ <% if (features.authentication.passwordReset) { %>
65
+ <Link href="/forgot-password" className="text-xs text-primary hover:underline">
66
+ Forgot password?
67
+ </Link>
68
+ <% } %>
69
+ </div>
70
+ <Input
71
+ id="password"
72
+ type="password"
73
+ value={password}
74
+ onChange={(e) => setPassword(e.target.value)}
75
+ placeholder="Enter your password"
76
+ required
77
+ disabled={isLoading}
78
+ autoComplete="current-password"
79
+ />
80
+ </div>
81
+
82
+ {error && (
83
+ <div className="p-2.5 text-sm text-destructive bg-destructive/10 rounded-lg border border-destructive/20">
84
+ {error}
85
+ </div>
86
+ )}
87
+
88
+ <Button type="submit" className="w-full" disabled={isLoading}>
89
+ {isLoading ? "Signing in..." : "Sign in"}
90
+ </Button>
91
+
92
+ <p className="text-center text-sm text-muted-foreground">
93
+ Don&apos;t have an account?{" "}
94
+ <Link href="/register" className="text-primary hover:underline font-medium">
95
+ Sign up
96
+ </Link>
97
+ </p>
98
+ </form>
99
+ );
100
+ }
@@ -0,0 +1,126 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { getOAuthUrl } from "@/lib/auth/oauth";
6
+ import { AUTH_CONFIG } from "@/lib/auth/config";
7
+
8
+ type OAuthProvider = "google" | "apple" | "github";
9
+
10
+ interface OAuthButtonsProps {
11
+ mode?: "login" | "register";
12
+ }
13
+
14
+ export function OAuthButtons({ mode = "login" }: OAuthButtonsProps) {
15
+ const [loadingProvider, setLoadingProvider] = useState<OAuthProvider | null>(null);
16
+
17
+ const handleOAuth = async (provider: OAuthProvider) => {
18
+ setLoadingProvider(provider);
19
+ try {
20
+ const url = await getOAuthUrl(provider);
21
+ // Redirect to backend OAuth URL (external navigation)
22
+ window.location.href = url;
23
+ } catch (error) {
24
+ console.error(`OAuth ${provider} error:`, error);
25
+ setLoadingProvider(null);
26
+ }
27
+ };
28
+
29
+ const actionText = mode === "login" ? "Sign in" : "Sign up";
30
+
31
+ // Check which providers are enabled
32
+ const hasAnyProvider = AUTH_CONFIG.googleEnabled || AUTH_CONFIG.appleEnabled || AUTH_CONFIG.githubEnabled;
33
+
34
+ if (!hasAnyProvider) {
35
+ return null;
36
+ }
37
+
38
+ return (
39
+ <div className="space-y-3">
40
+ <div className="relative">
41
+ <div className="absolute inset-0 flex items-center">
42
+ <span className="w-full border-t" />
43
+ </div>
44
+ <div className="relative flex justify-center text-xs uppercase">
45
+ <span className="bg-card px-2 text-muted-foreground">
46
+ Or continue with
47
+ </span>
48
+ </div>
49
+ </div>
50
+
51
+ <div className="grid gap-2">
52
+ <% if (features.authentication.providers.google) { %>
53
+ <Button
54
+ type="button"
55
+ variant="outline"
56
+ onClick={() => handleOAuth("google")}
57
+ disabled={loadingProvider !== null}
58
+ className="w-full"
59
+ >
60
+ {loadingProvider === "google" ? (
61
+ <span className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
62
+ ) : (
63
+ <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
64
+ <path
65
+ fill="currentColor"
66
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
67
+ />
68
+ <path
69
+ fill="currentColor"
70
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
71
+ />
72
+ <path
73
+ fill="currentColor"
74
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
75
+ />
76
+ <path
77
+ fill="currentColor"
78
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
79
+ />
80
+ </svg>
81
+ )}
82
+ {actionText} with Google
83
+ </Button>
84
+ <% } %>
85
+
86
+ <% if (features.authentication.providers.apple) { %>
87
+ <Button
88
+ type="button"
89
+ variant="outline"
90
+ onClick={() => handleOAuth("apple")}
91
+ disabled={loadingProvider !== null}
92
+ className="w-full"
93
+ >
94
+ {loadingProvider === "apple" ? (
95
+ <span className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
96
+ ) : (
97
+ <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
98
+ <path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
99
+ </svg>
100
+ )}
101
+ {actionText} with Apple
102
+ </Button>
103
+ <% } %>
104
+
105
+ <% if (features.authentication.providers.github) { %>
106
+ <Button
107
+ type="button"
108
+ variant="outline"
109
+ onClick={() => handleOAuth("github")}
110
+ disabled={loadingProvider !== null}
111
+ className="w-full"
112
+ >
113
+ {loadingProvider === "github" ? (
114
+ <span className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
115
+ ) : (
116
+ <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
117
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
118
+ </svg>
119
+ )}
120
+ {actionText} with GitHub
121
+ </Button>
122
+ <% } %>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { resetPassword } from "@/lib/auth/actions";
7
+
8
+ interface PasswordResetFormProps {
9
+ token: string;
10
+ }
11
+
12
+ export function PasswordResetForm({ token }: PasswordResetFormProps) {
13
+ const router = useRouter();
14
+ const [password, setPassword] = useState("");
15
+ const [confirmPassword, setConfirmPassword] = useState("");
16
+ const [error, setError] = useState<string | null>(null);
17
+ const [isLoading, setIsLoading] = useState(false);
18
+
19
+ const handleSubmit = async (e: React.FormEvent) => {
20
+ e.preventDefault();
21
+ setError(null);
22
+
23
+ // Validate passwords match
24
+ if (password !== confirmPassword) {
25
+ setError("Passwords do not match");
26
+ return;
27
+ }
28
+
29
+ // Validate password strength
30
+ if (password.length < 8) {
31
+ setError("Password must be at least 8 characters");
32
+ return;
33
+ }
34
+
35
+ setIsLoading(true);
36
+
37
+ try {
38
+ const result = await resetPassword(token, password);
39
+
40
+ if (!result.success) {
41
+ setError(result.error || "Failed to reset password");
42
+ return;
43
+ }
44
+
45
+ // Redirect to login with success message
46
+ router.push("/login?message=Password+reset+successfully");
47
+ } catch (err) {
48
+ setError("An unexpected error occurred");
49
+ console.error("Password reset error:", err);
50
+ } finally {
51
+ setIsLoading(false);
52
+ }
53
+ };
54
+
55
+ return (
56
+ <form onSubmit={handleSubmit} className="space-y-4">
57
+ <div className="space-y-2">
58
+ <label htmlFor="password" className="text-sm font-medium">
59
+ New Password
60
+ </label>
61
+ <input
62
+ id="password"
63
+ type="password"
64
+ value={password}
65
+ onChange={(e) => setPassword(e.target.value)}
66
+ placeholder="Enter new password"
67
+ required
68
+ disabled={isLoading}
69
+ className="w-full px-3 py-2 border rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
70
+ />
71
+ <p className="text-xs text-muted-foreground">
72
+ Must be at least 8 characters
73
+ </p>
74
+ </div>
75
+
76
+ <div className="space-y-2">
77
+ <label htmlFor="confirmPassword" className="text-sm font-medium">
78
+ Confirm New Password
79
+ </label>
80
+ <input
81
+ id="confirmPassword"
82
+ type="password"
83
+ value={confirmPassword}
84
+ onChange={(e) => setConfirmPassword(e.target.value)}
85
+ placeholder="Confirm new password"
86
+ required
87
+ disabled={isLoading}
88
+ className="w-full px-3 py-2 border rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
89
+ />
90
+ </div>
91
+
92
+ {error && (
93
+ <div className="p-3 text-sm text-red-500 bg-red-50 dark:bg-red-950/20 rounded-md">
94
+ {error}
95
+ </div>
96
+ )}
97
+
98
+ <Button type="submit" className="w-full" disabled={isLoading}>
99
+ {isLoading ? "Resetting password..." : "Reset password"}
100
+ </Button>
101
+ </form>
102
+ );
103
+ }