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