mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix: support VK ID OAuth 2.1 device_id in frontend
- Extract device_id from VK callback URL query params - Pass device_id through store → API → backend - Add useRef guard to prevent useEffect double-fire - Fix URL validation catch-block anti-pattern in Login - Improve error extraction in OAuthCallback (show Error.message)
This commit is contained in:
@@ -174,6 +174,7 @@ export const authApi = {
|
||||
provider: string,
|
||||
code: string,
|
||||
state: string,
|
||||
deviceId?: string | null,
|
||||
campaignSlug?: string | null,
|
||||
referralCode?: string | null,
|
||||
): Promise<AuthResponse> => {
|
||||
@@ -182,6 +183,7 @@ export const authApi = {
|
||||
{
|
||||
code,
|
||||
state,
|
||||
device_id: deviceId || undefined,
|
||||
campaign_slug: campaignSlug || undefined,
|
||||
referral_code: referralCode || undefined,
|
||||
},
|
||||
|
||||
@@ -127,12 +127,13 @@ export default function Login() {
|
||||
const { authorize_url, state } = await authApi.getOAuthAuthorizeUrl(provider);
|
||||
|
||||
// Validate redirect URL — only allow HTTPS to prevent open redirect
|
||||
let parsed: URL;
|
||||
try {
|
||||
const parsed = new URL(authorize_url);
|
||||
if (parsed.protocol !== 'https:') {
|
||||
parsed = new URL(authorize_url);
|
||||
} catch {
|
||||
throw new Error('Invalid OAuth redirect URL');
|
||||
}
|
||||
} catch {
|
||||
if (parsed.protocol !== 'https:') {
|
||||
throw new Error('Invalid OAuth redirect URL');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
@@ -28,6 +28,7 @@ export default function OAuthCallback() {
|
||||
const [error, setError] = useState('');
|
||||
const loginWithOAuth = useAuthStore((state) => state.loginWithOAuth);
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const hasRun = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
@@ -35,9 +36,15 @@ export default function OAuthCallback() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent double-fire from React StrictMode or dependency changes
|
||||
if (hasRun.current) return;
|
||||
hasRun.current = true;
|
||||
|
||||
const authenticate = async () => {
|
||||
const code = searchParams.get('code');
|
||||
const urlState = searchParams.get('state');
|
||||
// VK ID returns device_id in callback URL (required for token exchange)
|
||||
const deviceId = searchParams.get('device_id');
|
||||
|
||||
if (!code || !urlState) {
|
||||
setError(t('auth.oauthError', 'Authorization was denied or failed'));
|
||||
@@ -58,14 +65,13 @@ export default function OAuthCallback() {
|
||||
}
|
||||
|
||||
try {
|
||||
await loginWithOAuth(saved.provider, code, urlState);
|
||||
await loginWithOAuth(saved.provider, code, urlState, deviceId);
|
||||
navigate('/', { replace: true });
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { detail?: string } } };
|
||||
setError(
|
||||
error.response?.data?.detail ||
|
||||
t('auth.oauthError', 'Authorization was denied or failed'),
|
||||
);
|
||||
const detail =
|
||||
(err as { response?: { data?: { detail?: string } } })?.response?.data?.detail ??
|
||||
(err instanceof Error ? err.message : null);
|
||||
setError(detail || t('auth.oauthError', 'Authorization was denied or failed'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,12 @@ interface AuthState {
|
||||
loginWithTelegram: (initData: string) => Promise<void>;
|
||||
loginWithTelegramWidget: (data: TelegramWidgetData) => Promise<void>;
|
||||
loginWithEmail: (email: string, password: string) => Promise<void>;
|
||||
loginWithOAuth: (provider: string, code: string, state: string) => Promise<void>;
|
||||
loginWithOAuth: (
|
||||
provider: string,
|
||||
code: string,
|
||||
state: string,
|
||||
deviceId?: string | null,
|
||||
) => Promise<void>;
|
||||
registerWithEmail: (
|
||||
email: string,
|
||||
password: string,
|
||||
@@ -295,13 +300,14 @@ export const useAuthStore = create<AuthState>()(
|
||||
await get().checkAdminStatus();
|
||||
},
|
||||
|
||||
loginWithOAuth: async (provider, code, state) => {
|
||||
loginWithOAuth: async (provider, code, state, deviceId) => {
|
||||
const campaignSlug = consumeCampaignSlug();
|
||||
const referralCode = consumeReferralCode();
|
||||
const response = await authApi.oauthCallback(
|
||||
provider,
|
||||
code,
|
||||
state,
|
||||
deviceId,
|
||||
campaignSlug,
|
||||
referralCode,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user