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.
- package/LICENSE +21 -0
- package/README.md +642 -0
- package/bin/cli.js +12 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/dependencies.d.ts +82 -0
- package/dist/config/dependencies.d.ts.map +1 -0
- package/dist/config/dependencies.js +82 -0
- package/dist/config/dependencies.js.map +1 -0
- package/dist/config/presets.d.ts +3 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/presets.js +174 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/generators/index.d.ts +40 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +130 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/onboarding.d.ts +8 -0
- package/dist/generators/onboarding.d.ts.map +1 -0
- package/dist/generators/onboarding.js +141 -0
- package/dist/generators/onboarding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/features.d.ts +14 -0
- package/dist/prompts/features.d.ts.map +1 -0
- package/dist/prompts/features.js +96 -0
- package/dist/prompts/features.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +93 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/onboarding.d.ts +6 -0
- package/dist/prompts/onboarding.d.ts.map +1 -0
- package/dist/prompts/onboarding.js +37 -0
- package/dist/prompts/onboarding.js.map +1 -0
- package/dist/prompts/orm.d.ts +3 -0
- package/dist/prompts/orm.d.ts.map +1 -0
- package/dist/prompts/orm.js +23 -0
- package/dist/prompts/orm.js.map +1 -0
- package/dist/prompts/packageManager.d.ts +2 -0
- package/dist/prompts/packageManager.d.ts.map +1 -0
- package/dist/prompts/packageManager.js +18 -0
- package/dist/prompts/packageManager.js.map +1 -0
- package/dist/prompts/platform.d.ts +3 -0
- package/dist/prompts/platform.d.ts.map +1 -0
- package/dist/prompts/platform.js +21 -0
- package/dist/prompts/platform.js.map +1 -0
- package/dist/prompts/preset.d.ts +4 -0
- package/dist/prompts/preset.d.ts.map +1 -0
- package/dist/prompts/preset.js +165 -0
- package/dist/prompts/preset.js.map +1 -0
- package/dist/prompts/project.d.ts +2 -0
- package/dist/prompts/project.d.ts.map +1 -0
- package/dist/prompts/project.js +27 -0
- package/dist/prompts/project.js.map +1 -0
- package/dist/prompts/sdks.d.ts +2 -0
- package/dist/prompts/sdks.d.ts.map +1 -0
- package/dist/prompts/sdks.js +46 -0
- package/dist/prompts/sdks.js.map +1 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cleanup.d.ts +5 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +38 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/copy.d.ts +10 -0
- package/dist/utils/copy.d.ts.map +1 -0
- package/dist/utils/copy.js +53 -0
- package/dist/utils/copy.js.map +1 -0
- package/dist/utils/errors.d.ts +33 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +136 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +33 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package.d.ts +16 -0
- package/dist/utils/package.d.ts.map +1 -0
- package/dist/utils/package.js +86 -0
- package/dist/utils/package.js.map +1 -0
- package/dist/utils/system-validation.d.ts +9 -0
- package/dist/utils/system-validation.d.ts.map +1 -0
- package/dist/utils/system-validation.js +31 -0
- package/dist/utils/system-validation.js.map +1 -0
- package/dist/utils/template.d.ts +20 -0
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +234 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +94 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +96 -0
- package/templates/base/backend/.dockerignore.ejs +62 -0
- package/templates/base/backend/.env.example.ejs +116 -0
- package/templates/base/backend/Dockerfile.ejs +142 -0
- package/templates/base/backend/controllers/event-queue/index.ts +20 -0
- package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
- package/templates/base/backend/controllers/rest-api/index.ts +48 -0
- package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
- package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
- package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
- package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
- package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
- package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
- package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
- package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
- package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
- package/templates/base/backend/domain/device-session/schema.ts +72 -0
- package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
- package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
- package/templates/base/backend/domain/session/schema.ts +29 -0
- package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
- package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
- package/templates/base/backend/domain/user/schema.ts +14 -0
- package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
- package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
- package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
- package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
- package/templates/base/backend/lib/constants.ts.ejs +29 -0
- package/templates/base/backend/package.json.ejs +50 -0
- package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
- package/templates/base/backend/prisma.config.prisma.ts +12 -0
- package/templates/base/backend/tsconfig.json +39 -0
- package/templates/base/backend/utils/db.drizzle.ts +41 -0
- package/templates/base/backend/utils/db.prisma.ts +51 -0
- package/templates/base/backend/utils/email.ts.ejs +35 -0
- package/templates/base/backend/utils/errors.ts +348 -0
- package/templates/base/backend/utils/redis.ts.ejs +279 -0
- package/templates/base/mobile/.env.example.ejs +35 -0
- package/templates/base/mobile/.gitignore.ejs +167 -0
- package/templates/base/mobile/app/+not-found.tsx +85 -0
- package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
- package/templates/base/mobile/app.json.ejs +88 -0
- package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
- package/templates/base/mobile/assets/images/favicon.png +0 -0
- package/templates/base/mobile/assets/images/icon.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
- package/templates/base/mobile/assets/images/paywall_image.png +0 -0
- package/templates/base/mobile/assets/images/splash.png +0 -0
- package/templates/base/mobile/eas.json.ejs +49 -0
- package/templates/base/mobile/metro.config.js +9 -0
- package/templates/base/mobile/package.json.ejs +53 -0
- package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
- package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
- package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
- package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
- package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
- package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
- package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
- package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
- package/templates/base/mobile/src/components/ui/index.ts +6 -0
- package/templates/base/mobile/src/constants/Theme.ts +163 -0
- package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
- package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
- package/templates/base/mobile/src/services/api.ts.ejs +71 -0
- package/templates/base/mobile/src/services/errorService.ts +179 -0
- package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
- package/templates/base/mobile/src/store/index.ts.ejs +18 -0
- package/templates/base/mobile/src/store/ui.store.ts +100 -0
- package/templates/base/mobile/src/utils/formatters.ts +105 -0
- package/templates/base/mobile/src/utils/logger.ts +73 -0
- package/templates/base/mobile/src/utils/responsive.ts +234 -0
- package/templates/base/mobile/tsconfig.json +32 -0
- package/templates/base/web/.env.example.ejs +26 -0
- package/templates/base/web/components.json +22 -0
- package/templates/base/web/eslint.config.mjs +18 -0
- package/templates/base/web/next.config.ts +7 -0
- package/templates/base/web/package.json.ejs +35 -0
- package/templates/base/web/postcss.config.mjs +7 -0
- package/templates/base/web/public/.gitkeep +0 -0
- package/templates/base/web/public/file.svg +1 -0
- package/templates/base/web/public/globe.svg +1 -0
- package/templates/base/web/public/next.svg +1 -0
- package/templates/base/web/public/vercel.svg +1 -0
- package/templates/base/web/public/window.svg +1 -0
- package/templates/base/web/src/app/favicon.ico +0 -0
- package/templates/base/web/src/app/globals.css +152 -0
- package/templates/base/web/src/app/layout.tsx.ejs +54 -0
- package/templates/base/web/src/app/page.tsx.ejs +92 -0
- package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
- package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
- package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
- package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
- package/templates/base/web/src/components/theme-toggle.tsx +34 -0
- package/templates/base/web/src/components/ui/button.tsx +62 -0
- package/templates/base/web/src/components/ui/card.tsx +92 -0
- package/templates/base/web/src/components/ui/input.tsx +21 -0
- package/templates/base/web/src/components/ui/label.tsx +24 -0
- package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/web/src/components/ui/spinner.tsx +20 -0
- package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
- package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
- package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
- package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
- package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
- package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
- package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
- package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
- package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
- package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
- package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
- package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
- package/templates/base/web/src/lib/utils.ts +6 -0
- package/templates/base/web/src/proxy.ts.ejs +66 -0
- package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
- package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
- package/templates/base/web/tsconfig.json +34 -0
- package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
- package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
- package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
- package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
- package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
- package/templates/features/mobile/auth/components/auth/index.ts +2 -0
- package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
- package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
- package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
- package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
- package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
- package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
- package/templates/features/web/.gitkeep +0 -0
- package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
- package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
- package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
- package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
- package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
- package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
- package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
- package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
- package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
- package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
- package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
- package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
- package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
- package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
- package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
- package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
- package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
- package/templates/integrations/mobile/att/services/attService.ts +84 -0
- package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
- package/templates/integrations/mobile/att/store/att.store.ts +162 -0
- package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
- package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
- package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
- package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
- package/templates/integrations/web/.gitkeep +0 -0
- package/templates/shared/.env.example.ejs +21 -0
- package/templates/shared/.gitignore.ejs +145 -0
- package/templates/shared/README.md.ejs +134 -0
- package/templates/shared/docker-compose.prod.yml.ejs +120 -0
- package/templates/shared/docker-compose.yml.ejs +129 -0
- package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
- package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
- 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'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'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'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
|
+
© {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
|
+
}
|