mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +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,
|
provider: string,
|
||||||
code: string,
|
code: string,
|
||||||
state: string,
|
state: string,
|
||||||
|
deviceId?: string | null,
|
||||||
campaignSlug?: string | null,
|
campaignSlug?: string | null,
|
||||||
referralCode?: string | null,
|
referralCode?: string | null,
|
||||||
): Promise<AuthResponse> => {
|
): Promise<AuthResponse> => {
|
||||||
@@ -182,6 +183,7 @@ export const authApi = {
|
|||||||
{
|
{
|
||||||
code,
|
code,
|
||||||
state,
|
state,
|
||||||
|
device_id: deviceId || undefined,
|
||||||
campaign_slug: campaignSlug || undefined,
|
campaign_slug: campaignSlug || undefined,
|
||||||
referral_code: referralCode || undefined,
|
referral_code: referralCode || undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -127,14 +127,15 @@ export default function Login() {
|
|||||||
const { authorize_url, state } = await authApi.getOAuthAuthorizeUrl(provider);
|
const { authorize_url, state } = await authApi.getOAuthAuthorizeUrl(provider);
|
||||||
|
|
||||||
// Validate redirect URL — only allow HTTPS to prevent open redirect
|
// Validate redirect URL — only allow HTTPS to prevent open redirect
|
||||||
|
let parsed: URL;
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(authorize_url);
|
parsed = new URL(authorize_url);
|
||||||
if (parsed.protocol !== 'https:') {
|
|
||||||
throw new Error('Invalid OAuth redirect URL');
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error('Invalid OAuth redirect URL');
|
throw new Error('Invalid OAuth redirect URL');
|
||||||
}
|
}
|
||||||
|
if (parsed.protocol !== 'https:') {
|
||||||
|
throw new Error('Invalid OAuth redirect URL');
|
||||||
|
}
|
||||||
|
|
||||||
saveOAuthState(state, provider);
|
saveOAuthState(state, provider);
|
||||||
window.location.href = authorize_url;
|
window.location.href = authorize_url;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router';
|
import { useNavigate, useSearchParams } from 'react-router';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuthStore } from '../store/auth';
|
import { useAuthStore } from '../store/auth';
|
||||||
@@ -28,6 +28,7 @@ export default function OAuthCallback() {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const loginWithOAuth = useAuthStore((state) => state.loginWithOAuth);
|
const loginWithOAuth = useAuthStore((state) => state.loginWithOAuth);
|
||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
|
const hasRun = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
@@ -35,9 +36,15 @@ export default function OAuthCallback() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent double-fire from React StrictMode or dependency changes
|
||||||
|
if (hasRun.current) return;
|
||||||
|
hasRun.current = true;
|
||||||
|
|
||||||
const authenticate = async () => {
|
const authenticate = async () => {
|
||||||
const code = searchParams.get('code');
|
const code = searchParams.get('code');
|
||||||
const urlState = searchParams.get('state');
|
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) {
|
if (!code || !urlState) {
|
||||||
setError(t('auth.oauthError', 'Authorization was denied or failed'));
|
setError(t('auth.oauthError', 'Authorization was denied or failed'));
|
||||||
@@ -58,14 +65,13 @@ export default function OAuthCallback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loginWithOAuth(saved.provider, code, urlState);
|
await loginWithOAuth(saved.provider, code, urlState, deviceId);
|
||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const error = err as { response?: { data?: { detail?: string } } };
|
const detail =
|
||||||
setError(
|
(err as { response?: { data?: { detail?: string } } })?.response?.data?.detail ??
|
||||||
error.response?.data?.detail ||
|
(err instanceof Error ? err.message : null);
|
||||||
t('auth.oauthError', 'Authorization was denied or failed'),
|
setError(detail || t('auth.oauthError', 'Authorization was denied or failed'));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,12 @@ interface AuthState {
|
|||||||
loginWithTelegram: (initData: string) => Promise<void>;
|
loginWithTelegram: (initData: string) => Promise<void>;
|
||||||
loginWithTelegramWidget: (data: TelegramWidgetData) => Promise<void>;
|
loginWithTelegramWidget: (data: TelegramWidgetData) => Promise<void>;
|
||||||
loginWithEmail: (email: string, password: string) => 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: (
|
registerWithEmail: (
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
@@ -295,13 +300,14 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
await get().checkAdminStatus();
|
await get().checkAdminStatus();
|
||||||
},
|
},
|
||||||
|
|
||||||
loginWithOAuth: async (provider, code, state) => {
|
loginWithOAuth: async (provider, code, state, deviceId) => {
|
||||||
const campaignSlug = consumeCampaignSlug();
|
const campaignSlug = consumeCampaignSlug();
|
||||||
const referralCode = consumeReferralCode();
|
const referralCode = consumeReferralCode();
|
||||||
const response = await authApi.oauthCallback(
|
const response = await authApi.oauthCallback(
|
||||||
provider,
|
provider,
|
||||||
code,
|
code,
|
||||||
state,
|
state,
|
||||||
|
deviceId,
|
||||||
campaignSlug,
|
campaignSlug,
|
||||||
referralCode,
|
referralCode,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user