mirror of https://github.com/wg-easy/wg-easy
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
130 lines
3.0 KiB
130 lines
3.0 KiB
import { z } from 'zod';
|
|
|
|
const CallbackQuerySchema = z.object({
|
|
code: z.string().min(1),
|
|
state: z.string().min(1),
|
|
});
|
|
|
|
interface GoogleTokenResponse {
|
|
access_token: string;
|
|
id_token: string;
|
|
token_type: string;
|
|
expires_in: number;
|
|
}
|
|
|
|
interface GoogleUserInfo {
|
|
sub: string;
|
|
email: string;
|
|
email_verified: boolean;
|
|
name: string;
|
|
picture?: string;
|
|
}
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
if (!OAUTH_GOOGLE_ENV.ENABLED) {
|
|
throw createError({
|
|
statusCode: 404,
|
|
statusMessage: 'Google OAuth is not enabled',
|
|
});
|
|
}
|
|
|
|
const query = await getValidatedQuery(
|
|
event,
|
|
validateZod(CallbackQuerySchema, event)
|
|
);
|
|
|
|
// Verify state to prevent CSRF
|
|
const session = await useWGSession(event);
|
|
if (!session.data.oauthState || session.data.oauthState !== query.state) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: 'Invalid OAuth state',
|
|
});
|
|
}
|
|
|
|
// Clear the state
|
|
await session.update({ oauthState: undefined });
|
|
|
|
const host = getRequestHost(event);
|
|
const protocol = WG_ENV.INSECURE ? 'http' : 'https';
|
|
const redirectUri = `${protocol}://${host}/api/auth/google/callback`;
|
|
|
|
// Exchange code for tokens
|
|
const tokenResponse = await $fetch<GoogleTokenResponse>(
|
|
'https://oauth2.googleapis.com/token',
|
|
{
|
|
method: 'POST',
|
|
body: {
|
|
code: query.code,
|
|
client_id: OAUTH_GOOGLE_ENV.CLIENT_ID,
|
|
client_secret: OAUTH_GOOGLE_ENV.CLIENT_SECRET,
|
|
redirect_uri: redirectUri,
|
|
grant_type: 'authorization_code',
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!tokenResponse.access_token) {
|
|
throw createError({
|
|
statusCode: 401,
|
|
statusMessage: 'Failed to obtain access token from Google',
|
|
});
|
|
}
|
|
|
|
// Get user info from Google
|
|
const userInfo = await $fetch<GoogleUserInfo>(
|
|
'https://www.googleapis.com/oauth2/v3/userinfo',
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${tokenResponse.access_token}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!userInfo.email_verified) {
|
|
throw createError({
|
|
statusCode: 401,
|
|
statusMessage: 'Google email is not verified',
|
|
});
|
|
}
|
|
|
|
// Check allowed domain if configured
|
|
if (OAUTH_GOOGLE_ENV.ALLOWED_DOMAIN) {
|
|
const emailDomain = userInfo.email.split('@')[1];
|
|
if (emailDomain !== OAUTH_GOOGLE_ENV.ALLOWED_DOMAIN) {
|
|
throw createError({
|
|
statusCode: 403,
|
|
statusMessage: 'Email domain is not allowed',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Find or create user
|
|
const result = await Database.users.findOrCreateByGoogle(
|
|
userInfo.sub,
|
|
userInfo.email,
|
|
userInfo.name
|
|
);
|
|
|
|
if (!result.success) {
|
|
if (result.error === 'USER_DISABLED') {
|
|
throw createError({
|
|
statusCode: 401,
|
|
statusMessage: 'User disabled',
|
|
});
|
|
}
|
|
throw createError({
|
|
statusCode: 500,
|
|
statusMessage: 'Unexpected error',
|
|
});
|
|
}
|
|
|
|
// Create session
|
|
await session.update({ userId: result.user.id });
|
|
|
|
SERVER_DEBUG(
|
|
`New Google OAuth Session for ${result.user.id} (${result.user.username})`
|
|
);
|
|
|
|
return sendRedirect(event, '/');
|
|
});
|
|
|