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,166 @@
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 { useSession } from "@/hooks/use-session";
8
+ import type { User } from "@/lib/auth/actions";
9
+
10
+ interface DashboardClientProps {
11
+ user: User;
12
+ }
13
+
14
+ export function DashboardClient({ user }: DashboardClientProps) {
15
+ const router = useRouter();
16
+ const { signOut } = useSession();
17
+ const [isSigningOut, setIsSigningOut] = useState(false);
18
+
19
+ const handleSignOut = async () => {
20
+ setIsSigningOut(true);
21
+ try {
22
+ await signOut();
23
+ router.push("/login");
24
+ router.refresh();
25
+ } catch (error) {
26
+ console.error("Sign out error:", error);
27
+ setIsSigningOut(false);
28
+ }
29
+ };
30
+
31
+ return (
32
+ <div className="space-y-6">
33
+ {/* User Profile Card */}
34
+ <div className="rounded-xl border bg-card/50 dark:bg-transparent dark:border-border/50 p-6 shadow-sm dark:shadow-none">
35
+ <div className="flex items-start gap-4">
36
+ {/* Avatar */}
37
+ {user.image ? (
38
+ <img
39
+ src={user.image}
40
+ alt={user.name || "Profile"}
41
+ className="w-16 h-16 rounded-full object-cover ring-2 ring-border"
42
+ />
43
+ ) : (
44
+ <div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center ring-2 ring-border">
45
+ <span className="text-xl font-semibold text-primary">
46
+ {(user.name || user.email).charAt(0).toUpperCase()}
47
+ </span>
48
+ </div>
49
+ )}
50
+
51
+ {/* User Details */}
52
+ <div className="flex-1 space-y-1.5">
53
+ {user.name && (
54
+ <h2 className="text-lg font-semibold tracking-tight">{user.name}</h2>
55
+ )}
56
+ <p className="text-muted-foreground">{user.email}</p>
57
+
58
+ {/* Email Verification Status */}
59
+ <div className="flex items-center gap-2 pt-1">
60
+ {user.emailVerified ? (
61
+ <span className="inline-flex items-center gap-1.5 text-sm text-green-600 dark:text-green-400">
62
+ <svg
63
+ className="w-4 h-4"
64
+ fill="currentColor"
65
+ viewBox="0 0 20 20"
66
+ >
67
+ <path
68
+ fillRule="evenodd"
69
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
70
+ clipRule="evenodd"
71
+ />
72
+ </svg>
73
+ Email verified
74
+ </span>
75
+ ) : (
76
+ <span className="inline-flex items-center gap-1.5 text-sm text-amber-600 dark:text-amber-400">
77
+ <svg
78
+ className="w-4 h-4"
79
+ fill="currentColor"
80
+ viewBox="0 0 20 20"
81
+ >
82
+ <path
83
+ fillRule="evenodd"
84
+ d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
85
+ clipRule="evenodd"
86
+ />
87
+ </svg>
88
+ Email not verified
89
+ </span>
90
+ )}
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <% if (features.sessionManagement) { %>
97
+ {/* Quick Links Card */}
98
+ <div className="rounded-xl border bg-card/50 dark:bg-transparent dark:border-border/50 shadow-sm dark:shadow-none">
99
+ <div className="px-6 py-4 border-b dark:border-border/50">
100
+ <h3 className="text-base font-medium">Quick Links</h3>
101
+ </div>
102
+ <div className="p-2">
103
+ <Link
104
+ href="/settings/sessions"
105
+ className="flex items-center justify-between p-3 rounded-lg hover:bg-muted/50 transition-colors group"
106
+ >
107
+ <div className="flex items-center gap-3">
108
+ <div className="w-9 h-9 rounded-lg bg-muted/50 dark:bg-muted/30 flex items-center justify-center group-hover:bg-muted transition-colors">
109
+ <svg
110
+ className="w-5 h-5 text-muted-foreground"
111
+ fill="none"
112
+ viewBox="0 0 24 24"
113
+ stroke="currentColor"
114
+ >
115
+ <path
116
+ strokeLinecap="round"
117
+ strokeLinejoin="round"
118
+ strokeWidth={2}
119
+ d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
120
+ />
121
+ </svg>
122
+ </div>
123
+ <div>
124
+ <span className="font-medium">Active Sessions</span>
125
+ <p className="text-sm text-muted-foreground">Manage your devices</p>
126
+ </div>
127
+ </div>
128
+ <svg
129
+ className="w-5 h-5 text-muted-foreground group-hover:translate-x-0.5 transition-transform"
130
+ fill="none"
131
+ viewBox="0 0 24 24"
132
+ stroke="currentColor"
133
+ >
134
+ <path
135
+ strokeLinecap="round"
136
+ strokeLinejoin="round"
137
+ strokeWidth={2}
138
+ d="M9 5l7 7-7 7"
139
+ />
140
+ </svg>
141
+ </Link>
142
+ </div>
143
+ </div>
144
+ <% } %>
145
+
146
+ {/* Sign Out Section */}
147
+ <div className="rounded-xl border border-dashed bg-muted/30 dark:bg-transparent dark:border-border/50 p-6">
148
+ <div className="flex items-center justify-between">
149
+ <div>
150
+ <p className="font-medium">Sign out</p>
151
+ <p className="text-sm text-muted-foreground">
152
+ End your current session
153
+ </p>
154
+ </div>
155
+ <Button
156
+ variant="destructive"
157
+ onClick={handleSignOut}
158
+ disabled={isSigningOut}
159
+ >
160
+ {isSigningOut ? "Signing out..." : "Sign out"}
161
+ </Button>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ );
166
+ }
@@ -0,0 +1,24 @@
1
+ import { redirect } from "next/navigation";
2
+ import { getSession } from "@/lib/auth/actions";
3
+ import { DashboardClient } from "./dashboard-client";
4
+
5
+ export default async function DashboardPage() {
6
+ const session = await getSession();
7
+ if (!session) {
8
+ redirect("/login?redirect=/dashboard");
9
+ }
10
+
11
+ return (
12
+ <div className="space-y-8">
13
+ {/* Page Header */}
14
+ <div className="space-y-1">
15
+ <h1 className="text-2xl font-bold tracking-tight">Dashboard</h1>
16
+ <p className="text-muted-foreground">
17
+ Welcome back, {session.user.name || session.user.email}
18
+ </p>
19
+ </div>
20
+
21
+ <DashboardClient user={session.user} />
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,43 @@
1
+ import Link from "next/link";
2
+ import { ThemeToggle } from "@/components/theme-toggle";
3
+
4
+ export default function AppLayout({ children }: { children: React.ReactNode }) {
5
+ return (
6
+ <div className="min-h-screen flex flex-col bg-background">
7
+ {/* App Header - matching landing page style */}
8
+ <header className="w-full border-b bg-background/80 backdrop-blur-sm sticky top-0 z-50">
9
+ <div className="max-w-4xl mx-auto px-6 h-16 flex items-center justify-between">
10
+ <div className="flex items-center gap-8">
11
+ <Link href="/dashboard" className="text-xl font-semibold tracking-tight">
12
+ <%= projectName %>
13
+ </Link>
14
+ <nav className="hidden sm:flex items-center gap-6">
15
+ <Link
16
+ href="/dashboard"
17
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
18
+ >
19
+ Dashboard
20
+ </Link>
21
+ <% if (features.sessionManagement) { %>
22
+ <Link
23
+ href="/settings/sessions"
24
+ className="text-sm text-muted-foreground hover:text-foreground transition-colors"
25
+ >
26
+ Sessions
27
+ </Link>
28
+ <% } %>
29
+ </nav>
30
+ </div>
31
+ <ThemeToggle />
32
+ </div>
33
+ </header>
34
+
35
+ {/* Main content area */}
36
+ <main className="flex-1">
37
+ <div className="max-w-4xl mx-auto px-6 py-8">
38
+ {children}
39
+ </div>
40
+ </main>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,29 @@
1
+ import { redirect } from "next/navigation";
2
+ import { getSession } from "@/lib/auth/actions";
3
+ import { listSessions } from "@/lib/auth/sessions";
4
+ import { SessionsClient } from "./sessions-client";
5
+
6
+ export default async function SessionsPage() {
7
+ const authSession = await getSession();
8
+ if (!authSession) {
9
+ redirect("/login?redirect=/settings/sessions");
10
+ }
11
+
12
+ const { sessions } = await listSessions();
13
+
14
+ return (
15
+ <div className="space-y-8">
16
+ <div className="space-y-1">
17
+ <h1 className="text-2xl font-bold tracking-tight">Active Sessions</h1>
18
+ <p className="text-muted-foreground">
19
+ Manage your active sessions across devices. You can revoke access to any session except your current one.
20
+ </p>
21
+ </div>
22
+
23
+ <SessionsClient
24
+ sessions={sessions}
25
+ currentSessionId={authSession.session.id}
26
+ />
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { SessionCard } from "@/components/settings/session-card";
5
+ import { type SessionInfo } from "@/lib/auth/sessions";
6
+
7
+ interface SessionsClientProps {
8
+ sessions: SessionInfo[];
9
+ currentSessionId?: string;
10
+ }
11
+
12
+ export function SessionsClient({ sessions, currentSessionId }: SessionsClientProps) {
13
+ const otherSessions = sessions.filter((s) => s.id !== currentSessionId);
14
+ const currentSession = sessions.find((s) => s.id === currentSessionId);
15
+
16
+ const handleRevokeAll = () => {
17
+ // Session revocation requires WebSocket support for proper real-time notification
18
+ // Without it, the revoked devices would experience broken states (401 errors, failed forms)
19
+ alert("Session revocation will be available after WebSocket support is implemented.");
20
+ };
21
+
22
+ if (sessions.length === 0) {
23
+ return (
24
+ <div className="text-center py-8 text-muted-foreground">
25
+ No active sessions found.
26
+ </div>
27
+ );
28
+ }
29
+
30
+ return (
31
+ <div className="space-y-6">
32
+ {/* Revoke All Button */}
33
+ {otherSessions.length > 0 && (
34
+ <div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
35
+ <div>
36
+ <p className="font-medium">Sign out of other sessions</p>
37
+ <p className="text-sm text-muted-foreground">
38
+ This will revoke access from {otherSessions.length} other{" "}
39
+ {otherSessions.length === 1 ? "session" : "sessions"}.
40
+ </p>
41
+ </div>
42
+ <Button
43
+ variant="destructive"
44
+ onClick={handleRevokeAll}
45
+ >
46
+ Revoke all
47
+ </Button>
48
+ </div>
49
+ )}
50
+
51
+ {/* Session List */}
52
+ <div className="space-y-3">
53
+ {/* Current Session First */}
54
+ {currentSession && (
55
+ <SessionCard
56
+ session={currentSession}
57
+ isCurrentSession={true}
58
+ />
59
+ )}
60
+
61
+ {/* Other Sessions */}
62
+ {otherSessions.map((session) => (
63
+ <SessionCard
64
+ key={session.id}
65
+ session={session}
66
+ isCurrentSession={false}
67
+ />
68
+ ))}
69
+ </div>
70
+
71
+ {/* Session Count */}
72
+ <p className="text-sm text-muted-foreground text-center">
73
+ {sessions.length} active {sessions.length === 1 ? "session" : "sessions"}
74
+ </p>
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,127 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import Link from "next/link";
5
+ import { Button } from "@/components/ui/button";
6
+ import { requestPasswordReset } from "@/lib/auth/actions";
7
+
8
+ export default function ForgotPasswordPage() {
9
+ const [email, setEmail] = useState("");
10
+ const [error, setError] = useState<string | null>(null);
11
+ const [isLoading, setIsLoading] = useState(false);
12
+ const [isSuccess, setIsSuccess] = useState(false);
13
+
14
+ const handleSubmit = async (e: React.FormEvent) => {
15
+ e.preventDefault();
16
+ setError(null);
17
+ setIsLoading(true);
18
+
19
+ try {
20
+ const result = await requestPasswordReset(email);
21
+
22
+ if (!result.success) {
23
+ setError(result.error || "Failed to send reset email");
24
+ return;
25
+ }
26
+
27
+ setIsSuccess(true);
28
+ } catch (err) {
29
+ setError("An unexpected error occurred");
30
+ console.error("Password reset request error:", err);
31
+ } finally {
32
+ setIsLoading(false);
33
+ }
34
+ };
35
+
36
+ if (isSuccess) {
37
+ return (
38
+ <div className="space-y-6">
39
+ <div className="space-y-2 text-center">
40
+ <div className="mx-auto w-12 h-12 bg-green-100 dark:bg-green-950/20 rounded-full flex items-center justify-center">
41
+ <svg
42
+ className="w-6 h-6 text-green-600 dark:text-green-400"
43
+ fill="none"
44
+ viewBox="0 0 24 24"
45
+ stroke="currentColor"
46
+ >
47
+ <path
48
+ strokeLinecap="round"
49
+ strokeLinejoin="round"
50
+ strokeWidth={2}
51
+ d="M5 13l4 4L19 7"
52
+ />
53
+ </svg>
54
+ </div>
55
+ <h2 className="text-2xl font-bold">Check your email</h2>
56
+ <p className="text-muted-foreground">
57
+ We&apos;ve sent a password reset link to <strong>{email}</strong>
58
+ </p>
59
+ </div>
60
+
61
+ <div className="space-y-3">
62
+ <p className="text-sm text-muted-foreground text-center">
63
+ Didn&apos;t receive the email? Check your spam folder or{" "}
64
+ <button
65
+ onClick={() => setIsSuccess(false)}
66
+ className="text-primary hover:underline"
67
+ >
68
+ try again
69
+ </button>
70
+ </p>
71
+
72
+ <Link href="/login">
73
+ <Button variant="outline" className="w-full">
74
+ Back to sign in
75
+ </Button>
76
+ </Link>
77
+ </div>
78
+ </div>
79
+ );
80
+ }
81
+
82
+ return (
83
+ <div className="space-y-6">
84
+ <div className="space-y-2 text-center">
85
+ <h2 className="text-2xl font-bold">Forgot password?</h2>
86
+ <p className="text-muted-foreground">
87
+ Enter your email and we&apos;ll send you a reset link
88
+ </p>
89
+ </div>
90
+
91
+ <form onSubmit={handleSubmit} className="space-y-4">
92
+ <div className="space-y-2">
93
+ <label htmlFor="email" className="text-sm font-medium">
94
+ Email
95
+ </label>
96
+ <input
97
+ id="email"
98
+ type="email"
99
+ value={email}
100
+ onChange={(e) => setEmail(e.target.value)}
101
+ placeholder="you@example.com"
102
+ required
103
+ disabled={isLoading}
104
+ className="w-full px-3 py-2 border rounded-md bg-background focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
105
+ />
106
+ </div>
107
+
108
+ {error && (
109
+ <div className="p-3 text-sm text-red-500 bg-red-50 dark:bg-red-950/20 rounded-md">
110
+ {error}
111
+ </div>
112
+ )}
113
+
114
+ <Button type="submit" className="w-full" disabled={isLoading}>
115
+ {isLoading ? "Sending..." : "Send reset link"}
116
+ </Button>
117
+ </form>
118
+
119
+ <p className="text-center text-sm text-muted-foreground">
120
+ Remember your password?{" "}
121
+ <Link href="/login" className="text-primary hover:underline">
122
+ Sign in
123
+ </Link>
124
+ </p>
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,32 @@
1
+ import Link from "next/link";
2
+ import { ThemeToggle } from "@/components/theme-toggle";
3
+
4
+ export default function AuthLayout({ children }: { children: React.ReactNode }) {
5
+ return (
6
+ <div className="min-h-screen flex flex-col bg-background">
7
+ {/* Header matching landing page style */}
8
+ <header className="w-full border-b bg-background/80 backdrop-blur-sm sticky top-0 z-50">
9
+ <div className="max-w-6xl mx-auto px-6 h-16 flex items-center justify-between">
10
+ <Link href="/" className="text-xl font-semibold tracking-tight">
11
+ <%= projectName %>
12
+ </Link>
13
+ <ThemeToggle />
14
+ </div>
15
+ </header>
16
+
17
+ {/* Centered auth card */}
18
+ <main className="flex-1 flex items-center justify-center px-4 py-12">
19
+ <div className="w-full max-w-md space-y-6">
20
+ {/* Custom auth card - clean in both modes */}
21
+ <div className="rounded-xl border bg-card/50 dark:bg-transparent dark:border-border/50 p-6 shadow-sm dark:shadow-none">
22
+ {children}
23
+ </div>
24
+
25
+ <p className="text-center text-sm text-muted-foreground">
26
+ &copy; {new Date().getFullYear()} <%= projectName %>. All rights reserved.
27
+ </p>
28
+ </div>
29
+ </main>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,35 @@
1
+ import { LoginForm } from "@/components/auth/login-form";
2
+ import { OAuthButtons } from "@/components/auth/oauth-buttons";
3
+
4
+ interface LoginPageProps {
5
+ searchParams: Promise<{
6
+ redirect?: string;
7
+ error?: string;
8
+ }>;
9
+ }
10
+
11
+ export default async function LoginPage({ searchParams }: LoginPageProps) {
12
+ const params = await searchParams;
13
+ const { redirect, error } = params;
14
+
15
+ return (
16
+ <div className="space-y-6">
17
+ <div className="space-y-2 text-center">
18
+ <h2 className="text-2xl font-bold">Welcome back</h2>
19
+ <p className="text-muted-foreground">
20
+ Sign in to your account to continue
21
+ </p>
22
+ </div>
23
+
24
+ {error && (
25
+ <div className="p-3 text-sm text-red-500 bg-red-50 dark:bg-red-950/20 rounded-md">
26
+ {decodeURIComponent(error)}
27
+ </div>
28
+ )}
29
+
30
+ <LoginForm redirectTo={redirect || "/dashboard"} />
31
+
32
+ <OAuthButtons mode="login" />
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,19 @@
1
+ import { RegisterForm } from "@/components/auth/register-form";
2
+ import { OAuthButtons } from "@/components/auth/oauth-buttons";
3
+
4
+ export default function RegisterPage() {
5
+ return (
6
+ <div className="space-y-6">
7
+ <div className="space-y-2 text-center">
8
+ <h2 className="text-2xl font-bold">Create an account</h2>
9
+ <p className="text-muted-foreground">
10
+ Enter your details to get started
11
+ </p>
12
+ </div>
13
+
14
+ <RegisterForm />
15
+
16
+ <OAuthButtons mode="register" />
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,40 @@
1
+ import { redirect } from "next/navigation";
2
+ import Link from "next/link";
3
+ import { Button } from "@/components/ui/button";
4
+ import { PasswordResetForm } from "@/components/auth/password-reset-form";
5
+
6
+ interface ResetPasswordPageProps {
7
+ searchParams: Promise<{
8
+ token?: string;
9
+ }>;
10
+ }
11
+
12
+ export default async function ResetPasswordPage({ searchParams }: ResetPasswordPageProps) {
13
+ const params = await searchParams;
14
+ const { token } = params;
15
+
16
+ // Redirect if no token provided
17
+ if (!token) {
18
+ redirect("/forgot-password");
19
+ }
20
+
21
+ return (
22
+ <div className="space-y-6">
23
+ <div className="space-y-2 text-center">
24
+ <h2 className="text-2xl font-bold">Reset your password</h2>
25
+ <p className="text-muted-foreground">
26
+ Enter your new password below
27
+ </p>
28
+ </div>
29
+
30
+ <PasswordResetForm token={token} />
31
+
32
+ <p className="text-center text-sm text-muted-foreground">
33
+ Remember your password?{" "}
34
+ <Link href="/login" className="text-primary hover:underline">
35
+ Sign in
36
+ </Link>
37
+ </p>
38
+ </div>
39
+ );
40
+ }