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,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'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'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'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
|
+
}
|