diff --git a/src/app/components/Icons/Google.vue b/src/app/components/Icons/Google.vue new file mode 100644 index 00000000..e51adeea --- /dev/null +++ b/src/app/components/Icons/Google.vue @@ -0,0 +1,247 @@ + diff --git a/src/app/pages/login.vue b/src/app/pages/login.vue index a8daaef8..c701a068 100644 --- a/src/app/pages/login.vue +++ b/src/app/pages/login.vue @@ -12,40 +12,25 @@ - - - - - - - - - {{ $t('login.signInWithGoogle') }} - +
+ + + + {{ $t('login.signInWithGoogle') }} + - -
-
- {{ - $t('login.or') - }} -
+ +
+
+ {{ + $t('login.or') + }} +
+
diff --git a/src/server/api/auth/[provider]/callback.get.ts b/src/server/api/auth/[provider]/callback.get.ts index 5a3b30ee..5e2a5fe4 100644 --- a/src/server/api/auth/[provider]/callback.get.ts +++ b/src/server/api/auth/[provider]/callback.get.ts @@ -28,7 +28,7 @@ export default defineEventHandler(async (event) => { if (!subject) { throw createError({ statusCode: 400, - statusMessage: 'Cant get subject', + statusMessage: "Can't get subject", }); } @@ -56,7 +56,7 @@ export default defineEventHandler(async (event) => { provider, userInfo.sub, userInfo.email, - userInfo.name || userInfo.email + userInfo.preferred_username || userInfo.name || userInfo.email ); if (!result.success) { @@ -66,6 +66,12 @@ export default defineEventHandler(async (event) => { statusMessage: 'User disabled', }); } + if (result.error === 'USER_ALREADY_LINKED') { + throw createError({ + statusCode: 401, + statusMessage: 'User already linked with different account or provider', + }); + } throw createError({ statusCode: 500, statusMessage: 'Unexpected error', @@ -73,7 +79,7 @@ export default defineEventHandler(async (event) => { } // Create session - await session.update({ + const data = await session.update({ userId: result.user.id, oauth_nonce: undefined, oauth_state: undefined, @@ -81,7 +87,7 @@ export default defineEventHandler(async (event) => { }); SERVER_DEBUG( - `New OAuth Session for ${provider} ${result.user.id} (${result.user.username})` + `New OAuth Session: ${data.id} for ${result.user.id} (${result.user.username}) with ${provider}` ); return sendRedirect(event, '/'); diff --git a/src/server/api/auth/[provider]/index.get.ts b/src/server/api/auth/[provider]/index.get.ts index c4a14258..9a69c383 100644 --- a/src/server/api/auth/[provider]/index.get.ts +++ b/src/server/api/auth/[provider]/index.get.ts @@ -13,6 +13,7 @@ export default defineEventHandler(async (event) => { const state = client.randomState(); const parameters: Record = { + ...providerConfig.params, redirect_uri: redirectUri, scope: providerConfig.scope, code_challenge: codeChallenge, diff --git a/src/server/api/auth/methods.get.ts b/src/server/api/auth/methods.get.ts index f6d94be5..5e71a52e 100644 --- a/src/server/api/auth/methods.get.ts +++ b/src/server/api/auth/methods.get.ts @@ -1,5 +1,13 @@ export default defineEventHandler(() => { return { - google: OAUTH_GOOGLE_ENV.ENABLED, + providers: WG_ENV.OAUTH_PROVIDERS?.reduce( + (acc, curr) => { + acc[curr] = true; + return acc; + }, + {} as Record + ), + oauthEnabled: + WG_ENV.OAUTH_PROVIDERS !== undefined && WG_ENV.OAUTH_PROVIDERS.length > 0, }; }); diff --git a/src/server/database/repositories/user/service.ts b/src/server/database/repositories/user/service.ts index 4a44635c..1b9d6adb 100644 --- a/src/server/database/repositories/user/service.ts +++ b/src/server/database/repositories/user/service.ts @@ -127,6 +127,12 @@ export class UserService { if (!existingUser.enabled) { return { success: false as const, error: 'USER_DISABLED' as const }; } + if (existingUser.oauthProvider && existingUser.oauthId) { + return { + success: false as const, + error: 'USER_ALREADY_LINKED' as const, + }; + } await this.#db .update(user) .set({ oauthProvider: provider, oauthId: oauthId }) diff --git a/src/server/utils/config.ts b/src/server/utils/config.ts index 51a10350..28375e17 100644 --- a/src/server/utils/config.ts +++ b/src/server/utils/config.ts @@ -39,17 +39,10 @@ export const WG_ENV = { DISABLE_IPV6: process.env.DISABLE_IPV6 === 'true', WG_EXECUTABLE: await detectAwg(), DISABLE_VERSION_CHECK: process.env.DISABLE_VERSION_CHECK === 'true', -}; - -export const OAUTH_GOOGLE_ENV = { - /** Enable Google OAuth login */ - ENABLED: process.env.OAUTH_GOOGLE_ENABLED === 'true', - /** Google OAuth Client ID */ - CLIENT_ID: process.env.OAUTH_GOOGLE_CLIENT_ID || '', - /** Google OAuth Client Secret */ - CLIENT_SECRET: process.env.OAUTH_GOOGLE_CLIENT_SECRET || '', - /** Allowed email domain (optional, e.g. "example.com") */ - ALLOWED_DOMAIN: process.env.OAUTH_GOOGLE_ALLOWED_DOMAIN || '', + OAUTH_PROVIDERS: process.env.OAUTH_PROVIDERS?.split(',') + .map((v) => v.trim()) + .filter((v) => isValidOauthProvider(v)) + .filter((v) => isConfiguredOauthProvider(OAUTH_PROVIDERS[v])), }; export const WG_INITIAL_ENV = { diff --git a/src/server/utils/oauth.ts b/src/server/utils/oauth.ts index 0b81588e..1be1220d 100644 --- a/src/server/utils/oauth.ts +++ b/src/server/utils/oauth.ts @@ -1,10 +1,16 @@ import type { H3Event } from 'h3'; import { discovery } from 'openid-client'; -const OAUTH_PROVIDERS = { +export const OAUTH_PROVIDERS = { google: { server: 'https://accounts.google.com', - scope: 'openid email', + scope: 'openid email profile', + clientId: process.env.OAUTH_GOOGLE_CLIENT_ID, + clientSecret: process.env.OAUTH_GOOGLE_CLIENT_SECRET, + params: { + access_type: 'online', + prompt: 'select_account', + }, }, }; @@ -19,6 +25,18 @@ export function isValidOauthProvider( return false; } +export function isConfiguredOauthProvider( + oauthProvider: (typeof OAUTH_PROVIDERS)[OAUTH_PROVIDER] +): oauthProvider is (typeof OAUTH_PROVIDERS)[OAUTH_PROVIDER] & { + clientId: string; + clientSecret: string; +} { + if (!oauthProvider.clientId || !oauthProvider.clientSecret) { + return false; + } + return true; +} + export async function buildOauthConfig(event: H3Event) { const provider = getRouterParam(event, 'provider'); if (!provider || !isValidOauthProvider(provider)) { @@ -29,11 +47,19 @@ export async function buildOauthConfig(event: H3Event) { } const oauthProvider = OAUTH_PROVIDERS[provider]; + + if (!isConfiguredOauthProvider(oauthProvider)) { + throw createError({ + statusCode: 500, + statusMessage: 'Provider is not configured', + }); + } + const config = await discovery( new URL(oauthProvider.server), - OAUTH_GOOGLE_ENV.CLIENT_ID, + oauthProvider.clientId, { - client_secret: OAUTH_GOOGLE_ENV.CLIENT_SECRET, + client_secret: oauthProvider.clientSecret, } );