@main12/auth-login 0.3.0 → 0.3.2
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/README.md +254 -43
- package/dist/auth/application/hooks/useLoginFlow.js +18 -2
- package/dist/auth/application/hooks/useVerifyOtpFlow.js +17 -2
- package/dist/auth/application/services/authService.d.ts +1 -0
- package/dist/auth/application/services/authService.js +2 -1
- package/dist/components/AuthClientInit.d.ts +27 -0
- package/dist/components/AuthClientInit.js +29 -0
- package/dist/components/AuthLayout.js +8 -2
- package/dist/components/AuthPages.d.ts +11 -1
- package/dist/components/AuthPages.js +20 -2
- package/dist/components/AuthPagesServer.d.ts +17 -0
- package/dist/components/AuthPagesServer.js +30 -0
- package/dist/components/pages/ForgotPasswordPageHero.js +1 -1
- package/dist/components/pages/LoginPageHero.js +2 -2
- package/dist/components/pages/LoginPageTailwind.js +25 -6
- package/dist/components/pages/SetPasswordPageHero.js +4 -2
- package/dist/components/pages/SignupPageTailwind.js +23 -4
- package/dist/components/pages/VerifyOtpPageHero.js +1 -0
- package/dist/components/ui/index.js +4 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +5 -1
- package/dist/endpoints/authEndpoints.js +113 -95
- package/dist/exports/client.d.ts +2 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/rsc.d.ts +2 -0
- package/dist/exports/rsc.js +2 -0
- package/dist/index.d.ts +35 -1
- package/dist/index.js +105 -19
- package/dist/proxy.js +18 -3
- package/package.json +23 -50
- package/dist/endpoints/googleOAuth.d.ts +0 -10
- package/dist/endpoints/googleOAuth.js +0 -144
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { OAuth2Plugin } from 'payload-oauth2';
|
|
1
2
|
import { authEndpoints } from './endpoints/authEndpoints.js';
|
|
2
|
-
import { googleOAuthEndpoints } from './endpoints/googleOAuth.js';
|
|
3
3
|
import { pluginConfig } from './config.js';
|
|
4
4
|
function resolveGoogleConfig(providers) {
|
|
5
5
|
const google = providers?.google;
|
|
@@ -11,22 +11,32 @@ function resolveGoogleConfig(providers) {
|
|
|
11
11
|
clientSecret: ''
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
-
// Explicit config
|
|
15
|
-
|
|
14
|
+
// Explicit config object — user intends to enable Google OAuth
|
|
15
|
+
// Resolve credentials from explicit values or fall back to env vars
|
|
16
|
+
if (typeof google === 'object') {
|
|
17
|
+
const clientId = google.clientId || process.env.GOOGLE_CLIENT_ID || '';
|
|
18
|
+
const clientSecret = google.clientSecret || process.env.GOOGLE_CLIENT_SECRET || '';
|
|
16
19
|
return {
|
|
17
20
|
enabled: true,
|
|
18
|
-
clientId
|
|
19
|
-
clientSecret
|
|
21
|
+
clientId,
|
|
22
|
+
clientSecret
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// google: true — auto-detect from env
|
|
26
|
+
if (google === true) {
|
|
27
|
+
const envId = process.env.GOOGLE_CLIENT_ID || '';
|
|
28
|
+
const envSecret = process.env.GOOGLE_CLIENT_SECRET || '';
|
|
29
|
+
return {
|
|
30
|
+
enabled: !!(envId && envSecret),
|
|
31
|
+
clientId: envId,
|
|
32
|
+
clientSecret: envSecret
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// Not configured at all — disabled
|
|
26
36
|
return {
|
|
27
|
-
enabled:
|
|
28
|
-
clientId:
|
|
29
|
-
clientSecret:
|
|
37
|
+
enabled: false,
|
|
38
|
+
clientId: '',
|
|
39
|
+
clientSecret: ''
|
|
30
40
|
};
|
|
31
41
|
}
|
|
32
42
|
/**
|
|
@@ -38,7 +48,7 @@ function resolveGoogleConfig(providers) {
|
|
|
38
48
|
process.env.GOOGLE_CLIENT_SECRET = googleConfig.clientSecret;
|
|
39
49
|
}
|
|
40
50
|
}
|
|
41
|
-
export const authLoginPlugin = (options = {})=>(config)=>{
|
|
51
|
+
export const authLoginPlugin = (options = {})=>async (config)=>{
|
|
42
52
|
if (options.enabled === false) return config;
|
|
43
53
|
// Resolve provider config
|
|
44
54
|
const googleConfig = resolveGoogleConfig(options.providers);
|
|
@@ -51,6 +61,15 @@ export const authLoginPlugin = (options = {})=>(config)=>{
|
|
|
51
61
|
pluginConfig.Logo = options.logo;
|
|
52
62
|
}
|
|
53
63
|
pluginConfig.googleOAuthEnabled = googleConfig.enabled;
|
|
64
|
+
// Set env vars so RSC components can read config across module boundaries
|
|
65
|
+
process.env.AUTH_PLUGIN_STYLE = pluginConfig.style;
|
|
66
|
+
process.env.AUTH_PLUGIN_GOOGLE_OAUTH = String(pluginConfig.googleOAuthEnabled);
|
|
67
|
+
process.env.AUTH_PLUGIN_ALLOW_SIGNUP = String(options.allowSignup !== false);
|
|
68
|
+
process.env.AUTH_PLUGIN_PASSWORD_LOGIN = String(options.passwordLogin !== false);
|
|
69
|
+
process.env.AUTH_PLUGIN_OTP_LOGIN = String(options.otpLogin !== false);
|
|
70
|
+
pluginConfig.passwordLogin = options.passwordLogin !== false;
|
|
71
|
+
pluginConfig.otpLogin = options.otpLogin !== false;
|
|
72
|
+
process.env.AUTH_PLUGIN_ROUTE_REDIRECTS = String(pluginConfig.routeRedirects);
|
|
54
73
|
// Route redirects config
|
|
55
74
|
if (options.routeRedirects) {
|
|
56
75
|
pluginConfig.routeRedirects = true;
|
|
@@ -58,17 +77,84 @@ export const authLoginPlugin = (options = {})=>(config)=>{
|
|
|
58
77
|
pluginConfig.authBasePath = options.routeRedirects.basePath;
|
|
59
78
|
}
|
|
60
79
|
}
|
|
80
|
+
// Register hidden auth-otps collection for OTP storage
|
|
81
|
+
config.collections = [
|
|
82
|
+
...config.collections || [],
|
|
83
|
+
{
|
|
84
|
+
slug: 'auth-otps',
|
|
85
|
+
admin: {
|
|
86
|
+
hidden: true
|
|
87
|
+
},
|
|
88
|
+
fields: [
|
|
89
|
+
{
|
|
90
|
+
name: 'email',
|
|
91
|
+
type: 'email',
|
|
92
|
+
required: true,
|
|
93
|
+
index: true
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'hash',
|
|
97
|
+
type: 'text',
|
|
98
|
+
required: true
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'purpose',
|
|
102
|
+
type: 'text'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'attempts',
|
|
106
|
+
type: 'number',
|
|
107
|
+
defaultValue: 0
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'expiresAt',
|
|
111
|
+
type: 'text'
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
];
|
|
61
116
|
// Register auth API endpoints
|
|
62
117
|
config.endpoints = [
|
|
63
118
|
...config.endpoints || [],
|
|
64
119
|
...authEndpoints
|
|
65
120
|
];
|
|
66
|
-
// Register Google OAuth
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
121
|
+
// Register Google OAuth via payload-oauth2 if enabled
|
|
122
|
+
if (googleConfig.enabled) {
|
|
123
|
+
const googleOpts = typeof options.providers?.google === 'object' ? options.providers.google : {};
|
|
124
|
+
const serverURL = options.domain || process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000';
|
|
125
|
+
config = await OAuth2Plugin({
|
|
126
|
+
enabled: true,
|
|
127
|
+
strategyName: 'google',
|
|
128
|
+
useEmailAsIdentity: true,
|
|
129
|
+
serverURL,
|
|
130
|
+
clientId: googleConfig.clientId,
|
|
131
|
+
clientSecret: googleConfig.clientSecret,
|
|
132
|
+
tokenEndpoint: 'https://oauth2.googleapis.com/token',
|
|
133
|
+
providerAuthorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
134
|
+
scopes: [
|
|
135
|
+
'openid',
|
|
136
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
137
|
+
'https://www.googleapis.com/auth/userinfo.profile'
|
|
138
|
+
],
|
|
139
|
+
authorizePath: '/oauth/google',
|
|
140
|
+
callbackPath: '/oauth/google/callback',
|
|
141
|
+
prompt: googleOpts.prompt || 'select_account',
|
|
142
|
+
getUserInfo: async (accessToken)=>{
|
|
143
|
+
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
|
|
144
|
+
headers: {
|
|
145
|
+
Authorization: `Bearer ${accessToken}`
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const user = await response.json();
|
|
149
|
+
return {
|
|
150
|
+
email: user.email,
|
|
151
|
+
sub: user.sub,
|
|
152
|
+
name: user.name
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
successRedirect: ()=>googleOpts.successRedirect || '/admin',
|
|
156
|
+
failureRedirect: ()=>googleOpts.failureRedirect || '/login?error=Google login failed'
|
|
157
|
+
})(config);
|
|
72
158
|
}
|
|
73
159
|
// Chain onInit
|
|
74
160
|
const incomingOnInit = config.onInit;
|
package/dist/proxy.js
CHANGED
|
@@ -29,11 +29,25 @@ const AUTH_ROUTES = [
|
|
|
29
29
|
const base = (basePath ?? pluginConfig.authBasePath).replace(/\/$/, '');
|
|
30
30
|
const routeSet = new Set(routes);
|
|
31
31
|
return function proxy(request) {
|
|
32
|
-
//
|
|
33
|
-
|
|
32
|
+
// Check route redirects — use env var (set by plugin) as it survives module boundaries
|
|
33
|
+
const redirectsEnabled = pluginConfig.routeRedirects || process.env.AUTH_PLUGIN_ROUTE_REDIRECTS === 'true';
|
|
34
|
+
if (!redirectsEnabled && !basePath) {
|
|
34
35
|
return NextResponse.next();
|
|
35
36
|
}
|
|
36
37
|
const { pathname } = request.nextUrl;
|
|
38
|
+
// If user is already logged in, redirect away from auth pages (login, signup, etc.)
|
|
39
|
+
const token = request.cookies.get('payload-token')?.value;
|
|
40
|
+
if (token) {
|
|
41
|
+
const segment = pathname.replace(/^\//, '').replace(/\/$/, '');
|
|
42
|
+
const isAuthPage = routeSet.has(segment) || routeSet.has(pathname.replace(`${base}/`, ''));
|
|
43
|
+
if (isAuthPage || pathname.startsWith(`${base}/login`) || pathname.startsWith(`${base}/signup`)) {
|
|
44
|
+
const redirect = request.nextUrl.searchParams.get('redirect') || '/';
|
|
45
|
+
const url = request.nextUrl.clone();
|
|
46
|
+
url.pathname = redirect;
|
|
47
|
+
url.search = '';
|
|
48
|
+
return NextResponse.redirect(url);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
37
51
|
// Always redirect /admin/login to the plugin login page
|
|
38
52
|
if (pathname === '/admin/login' || pathname === '/admin/login/') {
|
|
39
53
|
const url = request.nextUrl.clone();
|
|
@@ -57,6 +71,7 @@ export const config = {
|
|
|
57
71
|
'/signup',
|
|
58
72
|
'/forgot-password',
|
|
59
73
|
'/verify-otp',
|
|
60
|
-
'/set-password'
|
|
74
|
+
'/set-password',
|
|
75
|
+
'/auth/:path*'
|
|
61
76
|
]
|
|
62
77
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@main12/auth-login",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Reusable Payload CMS auth plugin — login, signup, OTP, forgot password, branded emails, Powered by Main12",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,11 +19,6 @@
|
|
|
19
19
|
"import": "./dist/exports/rsc.js",
|
|
20
20
|
"types": "./dist/exports/rsc.d.ts",
|
|
21
21
|
"default": "./dist/exports/rsc.js"
|
|
22
|
-
},
|
|
23
|
-
"./proxy": {
|
|
24
|
-
"import": "./dist/proxy.js",
|
|
25
|
-
"types": "./dist/proxy.d.ts",
|
|
26
|
-
"default": "./dist/proxy.js"
|
|
27
22
|
}
|
|
28
23
|
},
|
|
29
24
|
"main": "./dist/index.js",
|
|
@@ -31,21 +26,6 @@
|
|
|
31
26
|
"files": [
|
|
32
27
|
"dist"
|
|
33
28
|
],
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc && pnpm build:fix-esm-imports",
|
|
36
|
-
"build:fix-esm-imports": "node ./scripts/fix-esm-imports.mjs",
|
|
37
|
-
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
38
|
-
"build:types": "tsc --outDir dist",
|
|
39
|
-
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
40
|
-
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
41
|
-
"dev": "next dev dev --turbo",
|
|
42
|
-
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
43
|
-
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
44
|
-
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
45
|
-
"lint": "eslint",
|
|
46
|
-
"lint:fix": "eslint ./src --fix",
|
|
47
|
-
"test": "vitest"
|
|
48
|
-
},
|
|
49
29
|
"devDependencies": {
|
|
50
30
|
"@eslint/eslintrc": "^3.2.0",
|
|
51
31
|
"@heroui/react": "^3.2.2",
|
|
@@ -95,33 +75,26 @@
|
|
|
95
75
|
"engines": {
|
|
96
76
|
"node": ">=18.20.2"
|
|
97
77
|
},
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
},
|
|
105
|
-
"./client": {
|
|
106
|
-
"import": "./dist/exports/client.js",
|
|
107
|
-
"types": "./dist/exports/client.d.ts",
|
|
108
|
-
"default": "./dist/exports/client.js"
|
|
109
|
-
},
|
|
110
|
-
"./rsc": {
|
|
111
|
-
"import": "./dist/exports/rsc.js",
|
|
112
|
-
"types": "./dist/exports/rsc.d.ts",
|
|
113
|
-
"default": "./dist/exports/rsc.js"
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
"main": "./dist/index.js",
|
|
117
|
-
"types": "./dist/index.d.ts"
|
|
118
|
-
},
|
|
119
|
-
"pnpm": {
|
|
120
|
-
"onlyBuiltDependencies": [
|
|
121
|
-
"@swc/core",
|
|
122
|
-
"sharp",
|
|
123
|
-
"esbuild"
|
|
124
|
-
]
|
|
78
|
+
"registry": "https://registry.npmjs.org/",
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"@main12/brevo-adapter": "^0.1.1",
|
|
81
|
+
"axios": "^1.20.0",
|
|
82
|
+
"jose": "^6.2.12",
|
|
83
|
+
"payload-oauth2": "^1.0.21"
|
|
125
84
|
},
|
|
126
|
-
"
|
|
127
|
-
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc && pnpm build:fix-esm-imports",
|
|
87
|
+
"build:fix-esm-imports": "node ./scripts/fix-esm-imports.mjs",
|
|
88
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
89
|
+
"build:types": "tsc --outDir dist",
|
|
90
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
91
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
92
|
+
"dev": "next dev dev --turbo",
|
|
93
|
+
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
94
|
+
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
95
|
+
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
96
|
+
"lint": "eslint",
|
|
97
|
+
"lint:fix": "eslint ./src --fix",
|
|
98
|
+
"test": "vitest"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { Endpoint } from 'payload';
|
|
2
|
-
/**
|
|
3
|
-
* GET /api/auth/oauth/google — start Google OAuth flow
|
|
4
|
-
*/
|
|
5
|
-
export declare const googleOAuthStart: Endpoint;
|
|
6
|
-
/**
|
|
7
|
-
* GET /api/auth/oauth/google/callback — handle Google's redirect back
|
|
8
|
-
*/
|
|
9
|
-
export declare const googleOAuthCallback: Endpoint;
|
|
10
|
-
export declare const googleOAuthEndpoints: Endpoint[];
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Google OAuth 2.0 — zero dependencies, pure REST.
|
|
3
|
-
*
|
|
4
|
-
* Flow:
|
|
5
|
-
* 1. GET /api/auth/oauth/google → Google consent screen
|
|
6
|
-
* 2. GET /api/auth/oauth/google/callback → exchange code, get profile, login, redirect
|
|
7
|
-
*
|
|
8
|
-
* Requires env vars: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
|
|
9
|
-
*/ const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
10
|
-
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
11
|
-
const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo';
|
|
12
|
-
function env(key, fb = '') {
|
|
13
|
-
return process.env[key] || fb;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* GET /api/auth/oauth/google — start Google OAuth flow
|
|
17
|
-
*/ export const googleOAuthStart = {
|
|
18
|
-
path: '/api/auth/oauth/google',
|
|
19
|
-
method: 'get',
|
|
20
|
-
handler: async (req)=>{
|
|
21
|
-
const clientId = env('GOOGLE_CLIENT_ID');
|
|
22
|
-
if (!clientId) return Response.json({
|
|
23
|
-
error: 'Google OAuth not configured'
|
|
24
|
-
}, {
|
|
25
|
-
status: 501
|
|
26
|
-
});
|
|
27
|
-
const url = new URL(req.url || 'http://localhost');
|
|
28
|
-
const redirect = url.searchParams.get('redirect') || '/';
|
|
29
|
-
const base = env('NEXT_PUBLIC_SERVER_URL', 'http://localhost:3000');
|
|
30
|
-
const params = new URLSearchParams({
|
|
31
|
-
client_id: clientId,
|
|
32
|
-
redirect_uri: `${base}/api/auth/oauth/google/callback`,
|
|
33
|
-
response_type: 'code',
|
|
34
|
-
scope: 'openid email profile',
|
|
35
|
-
access_type: 'online',
|
|
36
|
-
state: encodeURIComponent(redirect)
|
|
37
|
-
});
|
|
38
|
-
return Response.redirect(`${GOOGLE_AUTH_URL}?${params.toString()}`, 302);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
/**
|
|
42
|
-
* GET /api/auth/oauth/google/callback — handle Google's redirect back
|
|
43
|
-
*/ export const googleOAuthCallback = {
|
|
44
|
-
path: '/api/auth/oauth/google/callback',
|
|
45
|
-
method: 'get',
|
|
46
|
-
handler: async (req)=>{
|
|
47
|
-
const url = new URL(req.url || 'http://localhost');
|
|
48
|
-
const code = url.searchParams.get('code');
|
|
49
|
-
const state = url.searchParams.get('state') || '/';
|
|
50
|
-
const redirectTo = decodeURIComponent(state);
|
|
51
|
-
if (!code) return Response.redirect(`/login?error=${url.searchParams.get('error') || 'oauth_failed'}`, 302);
|
|
52
|
-
const clientId = env('GOOGLE_CLIENT_ID');
|
|
53
|
-
const clientSecret = env('GOOGLE_CLIENT_SECRET');
|
|
54
|
-
const base = env('NEXT_PUBLIC_SERVER_URL', 'http://localhost:3000');
|
|
55
|
-
try {
|
|
56
|
-
// 1. Exchange code for access token
|
|
57
|
-
const tokenRes = await fetch(GOOGLE_TOKEN_URL, {
|
|
58
|
-
method: 'POST',
|
|
59
|
-
headers: {
|
|
60
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
61
|
-
},
|
|
62
|
-
body: new URLSearchParams({
|
|
63
|
-
code,
|
|
64
|
-
client_id: clientId,
|
|
65
|
-
client_secret: clientSecret,
|
|
66
|
-
redirect_uri: `${base}/api/auth/oauth/google/callback`,
|
|
67
|
-
grant_type: 'authorization_code'
|
|
68
|
-
})
|
|
69
|
-
});
|
|
70
|
-
if (!tokenRes.ok) return Response.redirect(`/login?error=oauth_failed`, 302);
|
|
71
|
-
const tokens = await tokenRes.json();
|
|
72
|
-
const accessToken = tokens.access_token;
|
|
73
|
-
// 2. Get Google profile
|
|
74
|
-
const profileRes = await fetch(GOOGLE_USERINFO_URL, {
|
|
75
|
-
headers: {
|
|
76
|
-
Authorization: `Bearer ${accessToken}`
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
if (!profileRes.ok) return Response.redirect(`/login?error=oauth_failed`, 302);
|
|
80
|
-
const profile = await profileRes.json();
|
|
81
|
-
const email = profile.email?.toLowerCase();
|
|
82
|
-
const name = profile.name || email?.split('@')[0];
|
|
83
|
-
if (!email) return Response.redirect(`/login?error=oauth_failed`, 302);
|
|
84
|
-
// 3. Find or create user
|
|
85
|
-
const users = await req.payload.find({
|
|
86
|
-
collection: 'users',
|
|
87
|
-
where: {
|
|
88
|
-
email: {
|
|
89
|
-
equals: email
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
limit: 1
|
|
93
|
-
});
|
|
94
|
-
let userId;
|
|
95
|
-
let userPw;
|
|
96
|
-
if (users.docs?.length) {
|
|
97
|
-
userId = users.docs[0].id;
|
|
98
|
-
// Set a known temp password so we can login
|
|
99
|
-
userPw = `g_tmp_${Date.now()}`;
|
|
100
|
-
await req.payload.update({
|
|
101
|
-
collection: 'users',
|
|
102
|
-
id: userId,
|
|
103
|
-
data: {
|
|
104
|
-
password: userPw
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
} else {
|
|
108
|
-
userPw = `g_new_${Date.now()}`;
|
|
109
|
-
const newUser = await req.payload.create({
|
|
110
|
-
collection: 'users',
|
|
111
|
-
data: {
|
|
112
|
-
email,
|
|
113
|
-
name,
|
|
114
|
-
password: userPw,
|
|
115
|
-
authProvider: 'google'
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
userId = newUser.id;
|
|
119
|
-
}
|
|
120
|
-
// 4. Login
|
|
121
|
-
const loginResult = await req.payload.login({
|
|
122
|
-
collection: 'users',
|
|
123
|
-
data: {
|
|
124
|
-
email,
|
|
125
|
-
password: userPw
|
|
126
|
-
},
|
|
127
|
-
req: req
|
|
128
|
-
});
|
|
129
|
-
// 5. Set cookie + redirect
|
|
130
|
-
const response = Response.redirect(redirectTo, 302);
|
|
131
|
-
if (loginResult.token) {
|
|
132
|
-
response.headers.set('Set-Cookie', `payload-token=${loginResult.token}; Path=/; HttpOnly; SameSite=Lax` + `${process.env.NODE_ENV === 'production' ? '; Secure' : ''}` + `; Max-Age=${loginResult.exp || 7200}`);
|
|
133
|
-
}
|
|
134
|
-
return response;
|
|
135
|
-
} catch (err) {
|
|
136
|
-
console.error('[auth-login] Google OAuth error:', err);
|
|
137
|
-
return Response.redirect(`/login?error=oauth_failed`, 302);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
export const googleOAuthEndpoints = [
|
|
142
|
-
googleOAuthStart,
|
|
143
|
-
googleOAuthCallback
|
|
144
|
-
];
|