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,
}
);