support linking OAuth 2.0 user to logged-in users

This commit is contained in:
MaysWind
2025-10-31 01:22:47 +08:00
parent 8a0777be4c
commit b690316aa7
24 changed files with 172 additions and 44 deletions
+17 -5
View File
@@ -258,26 +258,35 @@ export default {
return axios.post<ApiResponse<AuthResponse>>('2fa/authorize.json', {
passcode: passcode
}, {
noAuth: true,
headers: {
Authorization: `Bearer ${token}`
}
});
} as ApiRequestConfig);
},
authorize2FAByBackupCode: ({ recoveryCode, token }: { recoveryCode: string, token: string }): ApiResponsePromise<AuthResponse> => {
return axios.post<ApiResponse<AuthResponse>>('2fa/recovery.json', {
recoveryCode: recoveryCode
}, {
noAuth: true,
headers: {
Authorization: `Bearer ${token}`
}
});
} as ApiRequestConfig);
},
authorizeOAuth2: ({ req, token }: { req: OAuth2CallbackLoginRequest, token: string }): ApiResponsePromise<AuthResponse> => {
authorizeOAuth2: ({ password, passcode, callbackToken }: { password?: string, passcode?: string, callbackToken: string }): ApiResponsePromise<AuthResponse> => {
const req: OAuth2CallbackLoginRequest = {
password,
passcode,
token: getCurrentToken() || undefined
};
return axios.post<ApiResponse<AuthResponse>>('oauth2/authorize.json', req, {
noAuth: true,
headers: {
Authorization: `Bearer ${token}`
Authorization: `Bearer ${callbackToken}`
}
});
} as ApiRequestConfig);
},
register: (req: UserRegisterRequest): ApiResponsePromise<RegisterResponse> => {
return axios.post<ApiResponse<RegisterResponse>>('register.json', req);
@@ -718,6 +727,9 @@ export default {
generateOAuth2LoginUrl: (platform: 'mobile' | 'desktop', clientSessionId: string): string => {
return `${getBasePath()}/oauth2/login?platform=${platform}&client_session_id=${clientSessionId}`;
},
generateOAuth2LinkUrl: (platform: 'mobile' | 'desktop', clientSessionId: string): string => {
return `${getBasePath()}/oauth2/login?platform=${platform}&client_session_id=${clientSessionId}&token=${getCurrentToken()}`;
},
generateQrCodeUrl: (qrCodeName: string): string => {
return `${getBasePath()}${BASE_QRCODE_PATH}/${qrCodeName}.png`;
},
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "Abfrageelemente dürfen nicht leer sein",
"query items too much": "Zu viele Abfrageelemente",
"query items have invalid item": "Ungültiges Element in Abfrageelementen",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "Benutzerdaten können nicht gelöscht werden",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "There are no query items",
"query items too much": "There are too many query items",
"query items have invalid item": "There is invalid item in query items",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "Unable to clear user data",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "--",
"query items too much": "--",
"query items have invalid item": "Hay un elemento no válido en los elementos de consulta",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "No se pueden borrar los datos del usuario",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "Il n'y a pas d'éléments de requête",
"query items too much": "Il y a trop d'éléments de requête",
"query items have invalid item": "Il y a un élément invalide dans les éléments de requête",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "Impossible d'effacer les données utilisateur",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "Non ci sono elementi di query",
"query items too much": "Ci sono troppi elementi di query",
"query items have invalid item": "C'è un elemento non valido negli elementi di query",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "Impossibile cancellare i dati utente",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "クエリ項目がありません",
"query items too much": "クエリ項目が多すぎます",
"query items have invalid item": "クエリ項目に無効な項目があります",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "ユーザーデータをクリアできません",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "쿼리 항목이 비어 있을 수 없습니다.",
"query items too much": "쿼리 항목이 너무 많습니다.",
"query items have invalid item": "쿼리 항목에 유효하지 않은 항목이 있습니다.",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "사용자 데이터를 지울 수 없습니다.",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "Geen zoekitems opgegeven",
"query items too much": "Te veel zoekitems",
"query items have invalid item": "Ongeldig item in zoekitems",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "Kan gebruikersgegevens niet wissen",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "Não há itens de consulta",
"query items too much": "Há muitos itens de consulta",
"query items have invalid item": "Há item inválido nos itens de consulta",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "Não foi possível limpar os dados de usuário",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "Нет элементов запроса",
"query items too much": "Слишком много элементов запроса",
"query items have invalid item": "В элементах запроса присутствует недопустимый элемент",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "Не удалось очистить данные пользователя",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "ไม่มีรายการสำหรับค้นหา",
"query items too much": "รายการค้นหามากเกินไป",
"query items have invalid item": "มีรายการไม่ถูกต้องในรายการค้นหา",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "ไม่สามารถลบข้อมูลผู้ใช้ได้",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "Елементи запиту не можуть бути порожніми",
"query items too much": "Занадто багато елементів запиту",
"query items have invalid item": "Запит містить недійсний елемент",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "Не вдалося очистити дані",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "Cannot retrieve OAuth 2.0 token",
"invalid oauth2 token": "Invalid OAuth 2.0 token",
"cannot retrieve user info from oauth2 provider": "Cannot retrieve user info from OAuth 2.0 provider",
"oauth2 user already bound to another user": "OAuth 2.0 user is already bound to another user",
"query items cannot be blank": "Không có mục truy vấn",
"query items too much": "Có quá nhiều mục truy vấn",
"query items have invalid item": "Có mục không hợp lệ trong các mục truy vấn",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "Không thể xóa dữ liệu người dùng",
"Third-Party Logins": "Third-Party Logins",
"Linked Time": "Linked Time",
"Link": "Link",
"Unlink": "Unlink",
"Are you sure you want to unlink this login method?": "Are you sure you want to unlink this login method?",
"Unable to retrieve third-party logins list": "Unable to retrieve third-party logins list",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "无法获取 OAuth 2.0 令牌",
"invalid oauth2 token": "无效的 OAuth 2.0 令牌",
"cannot retrieve user info from oauth2 provider": "无法从 OAuth 2.0 提供者获取用户信息",
"oauth2 user already bound to another user": "OAuth 2.0 用户已经绑定到另一个用户",
"query items cannot be blank": "请求项目不能为空",
"query items too much": "请求项目过多",
"query items have invalid item": "请求项目中有非法项目",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "无法清除用户数据",
"Third-Party Logins": "第三方登录",
"Linked Time": "关联时间",
"Link": "关联",
"Unlink": "取消关联",
"Are you sure you want to unlink this login method?": "您确定要取消关联该登录方式?",
"Unable to retrieve third-party logins list": "无法获取第三方登录列表",
+2
View File
@@ -1257,6 +1257,7 @@
"cannot retrieve oauth2 token": "無法獲取 OAuth 2.0 令牌",
"invalid oauth2 token": "無效的 OAuth 2.0 令牌",
"cannot retrieve user info from oauth2 provider": "無法從 OAuth 2.0 提供者獲取使用者資訊",
"oauth2 user already bound to another user": "OAuth 2.0 使用者已綁定到另一個使用者",
"query items cannot be blank": "查詢項目不能為空",
"query items too much": "查詢項目過多",
"query items have invalid item": "查詢項目中有非法項目",
@@ -2154,6 +2155,7 @@
"Unable to clear user data": "無法清除使用者資料",
"Third-Party Logins": "第三方登入",
"Linked Time": "連結時間",
"Link": "連結",
"Unlink": "取消連結",
"Are you sure you want to unlink this login method?": "您確定要取消連結這個登入方式?",
"Unable to retrieve third-party logins list": "無法取得第三方登入清單",
+1
View File
@@ -1,4 +1,5 @@
export interface OAuth2CallbackLoginRequest {
readonly password?: string;
readonly passcode?: string;
readonly token?: string;
}
+9 -6
View File
@@ -81,6 +81,10 @@ export const useRootStore = defineStore('root', () => {
return services.generateOAuth2LoginUrl(platform, clientSessionId);
}
function generateOAuth2LinkUrl(platform: 'mobile' | 'desktop', clientSessionId: string): string {
return services.generateOAuth2LinkUrl(platform, clientSessionId);
}
function authorize(req: UserLoginRequest): Promise<AuthResponse> {
return new Promise((resolve, reject) => {
services.authorize(req).then(response => {
@@ -191,14 +195,12 @@ export const useRootStore = defineStore('root', () => {
});
}
function authorizeOAuth2({ password, passcode, token }: { password?: string, passcode?: string, token: string }): Promise<AuthResponse> {
function authorizeOAuth2({ password, passcode, callbackToken }: { password?: string, passcode?: string, callbackToken: string }): Promise<AuthResponse> {
return new Promise((resolve, reject) => {
services.authorizeOAuth2({
req: {
password,
passcode
},
token
password,
passcode,
callbackToken
}).then(response => {
const data = response.data;
@@ -643,6 +645,7 @@ export const useRootStore = defineStore('root', () => {
// functions
setNotificationContent,
generateOAuth2LoginUrl,
generateOAuth2LinkUrl,
authorize,
authorize2FA,
authorizeOAuth2,
+2 -2
View File
@@ -212,7 +212,7 @@ function verifyAndLogin(): void {
rootStore.authorizeOAuth2({
password: password.value,
passcode: passcode.value,
token: props.token || ''
callbackToken: props.token || ''
}).then(authResponse => {
loggingInByOAuth2.value = false;
doAfterLogin(authResponse);
@@ -238,7 +238,7 @@ if (!error.value && props.platform && props.token && !props.userName) {
loggingInByOAuth2.value = true;
rootStore.authorizeOAuth2({
token: props.token
callbackToken: props.token
}).then(authResponse => {
loggingInByOAuth2.value = false;
doAfterLogin(authResponse);
@@ -105,6 +105,14 @@
<td class="text-sm">{{ thirdPartyLogin.externalUsername }}</td>
<td class="text-sm">{{ thirdPartyLogin.createdAt }}</td>
<td class="text-sm text-right">
<v-btn density="comfortable" variant="tonal"
:disabled="loggingInByOAuth2"
:href="oauth2LinkUrl"
@click="loggingInByOAuth2 = true"
v-if="!thirdPartyLogin.linked && isOAuth2Enabled() && getOAuth2Provider() === thirdPartyLogin.externalAuthType">
{{ tt('Link') }}
<v-progress-circular indeterminate size="22" class="ms-2" v-if="loggingInByOAuth2"></v-progress-circular>
</v-btn>
<v-btn density="comfortable" color="error" variant="tonal"
:disabled="loadingExternalAuth"
@click="unlinkExternalAuth(thirdPartyLogin)"
@@ -209,7 +217,8 @@ import { type TokenInfoResponse, SessionInfo } from '@/models/token.ts';
import { isEquals } from '@/lib/common.ts';
import { parseSessionInfo } from '@/lib/session.ts';
import { isOAuth2Enabled, getOIDCCustomDisplayNames, isMCPServerEnabled } from '@/lib/server_settings.ts';
import { isOAuth2Enabled, getOAuth2Provider, getOIDCCustomDisplayNames, isMCPServerEnabled } from '@/lib/server_settings.ts';
import { generateRandomUUID } from '@/lib/misc.ts';
import {
mdiRefresh,
@@ -314,6 +323,10 @@ const confirmPassword = ref<string>('');
const updatingPassword = ref<boolean>(false);
const loadingExternalAuth = ref<boolean>(true);
const loadingSession = ref<boolean>(true);
const loggingInByOAuth2 = ref<boolean>(false);
const oauth2ClientSessionId = ref<string>(generateRandomUUID());
const oauth2LinkUrl = computed<string>(() => rootStore.generateOAuth2LinkUrl('desktop', oauth2ClientSessionId.value));
const thirdPartyLogins = computed<DesktopPageLinkedThirdPartyLogin[]>(() => {
const logins: DesktopPageLinkedThirdPartyLogin[] = [];