From 6c58cd3c2e299c20b72083130ef624eacfb41e06 Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Mon, 16 Mar 2026 00:04:00 +0800 Subject: [PATCH] fix: add localStorage fallback for OAuth2 session state on mobile browsers (#900) * fix: add localStorage fallback for OAuth2 session state on mobile browsers Some mobile browsers (Safari ITP, WebViews) lose sessionStorage during cross-origin OAuth2 redirects. Add localStorage fallback via computed wrapper that dual-writes on set and reads sessionStorage-first on get. Also cleanup state in finally block to ensure one-time consumption. Co-Authored-By: Claude Opus 4.6 * fix: i18n for 'code not found' in OAuth2 callback Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- frontend/src/store/index.js | 14 +++++++-- .../src/views/user/UserOauth2Callback.vue | 29 +++++++++++-------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 8d76d63a..0a57deff 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -112,8 +112,18 @@ export const useGlobalState = createGlobalState( ); const telegramApp = ref(window.Telegram?.WebApp || {}); const isTelegram = ref(!!window.Telegram?.WebApp?.initData); - const userOauth2SessionState = useSessionStorage('userOauth2SessionState', ''); - const userOauth2SessionClientID = useSessionStorage('userOauth2SessionClientID', ''); + const _oauth2StateSession = useSessionStorage('userOauth2SessionState', ''); + const _oauth2StateFallback = useStorage('userOauth2SessionState_fb', ''); + const userOauth2SessionState = computed({ + get: () => _oauth2StateSession.value || _oauth2StateFallback.value, + set: (v) => { _oauth2StateSession.value = v; _oauth2StateFallback.value = v; } + }); + const _oauth2ClientIDSession = useSessionStorage('userOauth2SessionClientID', ''); + const _oauth2ClientIDFallback = useStorage('userOauth2SessionClientID_fb', ''); + const userOauth2SessionClientID = computed({ + get: () => _oauth2ClientIDSession.value || _oauth2ClientIDFallback.value, + set: (v) => { _oauth2ClientIDSession.value = v; _oauth2ClientIDFallback.value = v; } + }); const browserFingerprint = ref(''); return { isDark, diff --git a/frontend/src/views/user/UserOauth2Callback.vue b/frontend/src/views/user/UserOauth2Callback.vue index 7fe81f8e..61fb2943 100644 --- a/frontend/src/views/user/UserOauth2Callback.vue +++ b/frontend/src/views/user/UserOauth2Callback.vue @@ -19,28 +19,30 @@ const { t } = useI18n({ en: { logging: 'Logging in...', stateNotMatch: 'state not match', + codeNotFound: 'code not found', }, zh: { logging: '登录中...', stateNotMatch: 'state 不匹配', + codeNotFound: '未找到授权码', } } }); onMounted(async () => { - const state = route.query.state; - if (state != userOauth2SessionState.value) { - console.error('state not match'); - message.error(t('stateNotMatch')); - return; - } - const code = route.query.code; - if (!code) { - console.error('code not found'); - message.error('code not found'); - return; - } try { + const state = route.query.state; + if (state != userOauth2SessionState.value) { + console.error('state not match'); + message.error(t('stateNotMatch')); + return; + } + const code = route.query.code; + if (!code) { + console.error('code not found'); + message.error(t('codeNotFound')); + return; + } const res = await api.fetch(`/user_api/oauth2/callback`, { method: 'POST', body: JSON.stringify({ @@ -53,6 +55,9 @@ onMounted(async () => { } catch (error) { console.error(error); message.error(error.message || 'error'); + } finally { + userOauth2SessionState.value = ''; + userOauth2SessionClientID.value = ''; } });