fix: login với Google Oauth lỗi token invite
@@ -1739,6 +1739,7 @@ class TourController {
|
|||||||
async joinByToken(@Body() body: { token: string }, @Req() req: any) {
|
async joinByToken(@Body() body: { token: string }, @Req() req: any) {
|
||||||
const { token } = body;
|
const { token } = body;
|
||||||
console.log('[joinByToken] Request received, token length:', token ? token.length : 0);
|
console.log('[joinByToken] Request received, token length:', token ? token.length : 0);
|
||||||
|
console.log('[joinByToken] Token received (full):', token);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.error('[joinByToken] No token provided');
|
console.error('[joinByToken] No token provided');
|
||||||
@@ -1756,6 +1757,11 @@ class TourController {
|
|||||||
// Check how many invitations exist in database for debugging
|
// Check how many invitations exist in database for debugging
|
||||||
const totalInvitations = await this.prisma.tourInvitation.count();
|
const totalInvitations = await this.prisma.tourInvitation.count();
|
||||||
console.log('[joinByToken] Total invitations in database:', totalInvitations);
|
console.log('[joinByToken] Total invitations in database:', totalInvitations);
|
||||||
|
|
||||||
|
// List all tokens in DB for comparison (only in dev)
|
||||||
|
const allInvitations = await this.prisma.tourInvitation.findMany({ select: { token: true, email: true, tourId: true } });
|
||||||
|
console.log('[joinByToken] All invitation tokens:', allInvitations);
|
||||||
|
|
||||||
throw new NotFoundException('Lời mời không hợp lệ hoặc đã bị hủy.');
|
throw new NotFoundException('Lời mời không hợp lệ hoặc đã bị hủy.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 233 KiB |
@@ -6,8 +6,8 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||||
<title>Travel Planner</title>
|
<title>Travel Planner</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BwN9EkVY.js"></script>
|
<script type="module" crossorigin src="/assets/index-BX4qY0bE.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-sqDjZKsa.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-pT9caalc.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { X, Mail, Lock, ArrowRight, Loader2, LogIn } from 'lucide-react';
|
import { X, Mail, Lock, ArrowRight, Loader2, LogIn } from 'lucide-react';
|
||||||
import { useNotification } from '@/hooks/useNotification';
|
import { useNotification } from '@/hooks/useNotification';
|
||||||
|
|
||||||
@@ -23,6 +23,12 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const notify = useNotification();
|
const notify = useNotification();
|
||||||
|
|
||||||
|
// Ref to store the latest inviteToken to avoid stale closure issue
|
||||||
|
const inviteTokenRef = useRef(inviteToken);
|
||||||
|
useEffect(() => {
|
||||||
|
inviteTokenRef.current = inviteToken;
|
||||||
|
}, [inviteToken]);
|
||||||
|
|
||||||
const handleGoogleLogin = async (googleResponse: any) => {
|
const handleGoogleLogin = async (googleResponse: any) => {
|
||||||
setError('');
|
setError('');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -41,16 +47,39 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
|
|||||||
localStorage.setItem('token', data.access_token);
|
localStorage.setItem('token', data.access_token);
|
||||||
localStorage.setItem('user', JSON.stringify(data.user));
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
|
||||||
// Attempt to join tour with the token
|
// Use the latest inviteToken from ref
|
||||||
|
let currentToken = inviteTokenRef.current;
|
||||||
|
console.log('[JoinTourLogin] Attempting to join with token:', currentToken?.substring(0, 10) + '...');
|
||||||
|
|
||||||
|
// Also check pendingInviteToken as fallback
|
||||||
|
let pendingToken = localStorage.getItem('pendingInviteToken');
|
||||||
|
|
||||||
|
// If no token found, try to get from URL
|
||||||
|
if (!currentToken && !pendingToken) {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const urlToken = params.get('token');
|
||||||
|
if (urlToken) {
|
||||||
|
currentToken = urlToken;
|
||||||
|
localStorage.setItem('pendingInviteToken', urlToken);
|
||||||
|
console.log('[JoinTourLogin] Using token from URL params:', urlToken.substring(0, 10) + '...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenToUse = currentToken || pendingToken;
|
||||||
|
|
||||||
|
if (!tokenToUse) {
|
||||||
|
console.error('[JoinTourLogin] No invite token available');
|
||||||
|
throw new Error('Không có mã lời mời để tham gia tour');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[JoinTourLogin] Attempting to join with token:', inviteToken.substring(0, 10) + '...');
|
|
||||||
const joinRes = await fetch(`/api/v1/tours/join-by-token`, {
|
const joinRes = await fetch(`/api/v1/tours/join-by-token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${data.access_token}`,
|
Authorization: `Bearer ${data.access_token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ token: inviteToken }),
|
body: JSON.stringify({ token: tokenToUse }),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[JoinTourLogin] Join response status:', joinRes.status);
|
console.log('[JoinTourLogin] Join response status:', joinRes.status);
|
||||||
@@ -69,6 +98,9 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
|
|||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
} else {
|
} else {
|
||||||
|
// Token invalid or expired - clear it and show error
|
||||||
|
console.log('[JoinTourLogin] Token invalid, clearing from storage');
|
||||||
|
localStorage.removeItem('pendingInviteToken');
|
||||||
// Email mismatch or other error
|
// Email mismatch or other error
|
||||||
const errorMessage = joinData.message || `Không thể gia nhập tour (HTTP ${joinRes.status})`;
|
const errorMessage = joinData.message || `Không thể gia nhập tour (HTTP ${joinRes.status})`;
|
||||||
console.error('[JoinTourLogin] Join failed:', errorMessage);
|
console.error('[JoinTourLogin] Join failed:', errorMessage);
|
||||||
@@ -107,7 +139,9 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
|
|||||||
localStorage.setItem('token', data.access_token);
|
localStorage.setItem('token', data.access_token);
|
||||||
localStorage.setItem('user', JSON.stringify(data.user));
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
|
||||||
// Attempt to join tour
|
// Use the latest inviteToken from ref
|
||||||
|
const tokenToUse = inviteTokenRef.current || localStorage.getItem('pendingInviteToken');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const joinRes = await fetch(`/api/v1/tours/join-by-token`, {
|
const joinRes = await fetch(`/api/v1/tours/join-by-token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -115,7 +149,7 @@ export const JoinTourLoginModal: React.FC<JoinTourLoginModalProps> = ({
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${data.access_token}`,
|
Authorization: `Bearer ${data.access_token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ token: inviteToken }),
|
body: JSON.stringify({ token: tokenToUse }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const joinData = await joinRes.json().catch(() => ({}));
|
const joinData = await joinRes.json().catch(() => ({}));
|
||||||
|
|||||||
@@ -31,13 +31,69 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose, onSwitc
|
|||||||
|
|
||||||
localStorage.setItem('token', data.access_token);
|
localStorage.setItem('token', data.access_token);
|
||||||
localStorage.setItem('user', JSON.stringify(data.user));
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
// Remove guest tokens to ensure clean real user session
|
||||||
|
localStorage.removeItem('guest_token');
|
||||||
|
localStorage.removeItem('guest_user');
|
||||||
console.log('[OAuth] Google login successful');
|
console.log('[OAuth] Google login successful');
|
||||||
|
|
||||||
|
// Check if there's a pending invite token to join tour
|
||||||
|
// Try localStorage first, then fallback to URL parameter
|
||||||
|
let pendingInviteToken = localStorage.getItem('pendingInviteToken');
|
||||||
|
console.log('[OAuth] pendingInviteToken from localStorage:', pendingInviteToken ? pendingInviteToken.substring(0, 20) + '...' : 'none');
|
||||||
|
|
||||||
|
// If no token in localStorage, try to get from URL (in case of race condition)
|
||||||
|
if (!pendingInviteToken) {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const urlToken = params.get('token');
|
||||||
|
if (urlToken) {
|
||||||
|
pendingInviteToken = urlToken;
|
||||||
|
localStorage.setItem('pendingInviteToken', urlToken);
|
||||||
|
console.log('[OAuth] Using token from URL params:', urlToken.substring(0, 20) + '...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingInviteToken) {
|
||||||
|
try {
|
||||||
|
const joinRes = await fetch(`/api/v1/tours/join-by-token`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${data.access_token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ token: pendingInviteToken }),
|
||||||
|
});
|
||||||
|
const joinData = await joinRes.json().catch(() => ({}));
|
||||||
|
if (joinRes.ok) {
|
||||||
|
localStorage.removeItem('pendingInviteToken');
|
||||||
|
if (onLoginSuccess) {
|
||||||
|
onLoginSuccess(data.user);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
// If invitation is invalid/expired, clear it and redirect to dashboard
|
||||||
|
console.log('[OAuth] Token invalid or expired, clearing and redirecting to dashboard');
|
||||||
|
localStorage.removeItem('pendingInviteToken');
|
||||||
|
if (onLoginSuccess) {
|
||||||
|
onLoginSuccess(data.user);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
} catch (joinErr: any) {
|
||||||
|
console.error('[OAuth] Join tour after login failed:', joinErr);
|
||||||
|
localStorage.removeItem('pendingInviteToken');
|
||||||
|
// Still redirect to dashboard on error
|
||||||
|
if (onLoginSuccess) {
|
||||||
|
onLoginSuccess(data.user);
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
// Regular login - no auto-join for tour
|
// Regular login - no auto-join for tour
|
||||||
if (onLoginSuccess) {
|
if (onLoginSuccess) {
|
||||||
onLoginSuccess(data.user);
|
onLoginSuccess(data.user);
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[OAuth] Login error:', err);
|
console.error('[OAuth] Login error:', err);
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
|||||||
@@ -20,6 +20,21 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
|
|||||||
// Guard chống React StrictMode chạy effect 2 lần trong dev mode
|
// Guard chống React StrictMode chạy effect 2 lần trong dev mode
|
||||||
const hasJoinedRef = useRef(false);
|
const hasJoinedRef = useRef(false);
|
||||||
|
|
||||||
|
// Sync inviteToken from URL when modal opens to ensure fresh value
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoginOpen) {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const token = params.get('token');
|
||||||
|
if (token) {
|
||||||
|
setInviteToken(token);
|
||||||
|
localStorage.setItem('pendingInviteToken', token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isLoginOpen]);
|
||||||
|
|
||||||
|
// Check for real authenticated user (not guest)
|
||||||
|
const hasRealToken = !!localStorage.getItem('token') && !localStorage.getItem('guest_token');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const token = params.get('token');
|
const token = params.get('token');
|
||||||
@@ -28,9 +43,10 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
|
|||||||
if (token) {
|
if (token) {
|
||||||
localStorage.setItem('pendingInviteToken', token);
|
localStorage.setItem('pendingInviteToken', token);
|
||||||
|
|
||||||
// Nếu đã đăng nhập, tự động thực hiện join
|
// Nếu đã đăng nhập (có token thực, không phải guest), tự động thực hiện join
|
||||||
const systemToken = localStorage.getItem('token');
|
const systemToken = localStorage.getItem('token');
|
||||||
if (systemToken && !hasJoinedRef.current) {
|
const guestToken = localStorage.getItem('guest_token');
|
||||||
|
if (systemToken && !guestToken && !hasJoinedRef.current) {
|
||||||
hasJoinedRef.current = true;
|
hasJoinedRef.current = true;
|
||||||
handleJoinTour(token, systemToken);
|
handleJoinTour(token, systemToken);
|
||||||
}
|
}
|
||||||
@@ -82,7 +98,7 @@ export const JoinTourPage: React.FC<JoinTourPageProps> = ({ onLoginSuccess, onGo
|
|||||||
onLoginSuccess(user);
|
onLoginSuccess(user);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLoggedIn = !!localStorage.getItem('token');
|
const isLoggedIn = !!localStorage.getItem('token') && !localStorage.getItem('guest_token');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8 font-sans">
|
<div className="min-h-screen flex flex-col items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8 font-sans">
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
|||||||
const [touchOffsetX, setTouchOffsetX] = useState(0);
|
const [touchOffsetX, setTouchOffsetX] = useState(0);
|
||||||
const touchStartXRef = useRef(0);
|
const touchStartXRef = useRef(0);
|
||||||
const isSwipingRef = useRef(false);
|
const isSwipingRef = useRef(false);
|
||||||
|
const panScale = 1.25;
|
||||||
|
const maxPanX = typeof window !== 'undefined' ? -(window.innerWidth * (panScale - 1)) : -250;
|
||||||
|
|
||||||
const isLoggedIn = !!localStorage.getItem('token') && !localStorage.getItem('guest_token');
|
const isLoggedIn = !!localStorage.getItem('token') && !localStorage.getItem('guest_token');
|
||||||
|
|
||||||
@@ -50,20 +52,16 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
|||||||
const currentX = e.touches[0].clientX;
|
const currentX = e.touches[0].clientX;
|
||||||
const diffX = currentX - touchStartXRef.current;
|
const diffX = currentX - touchStartXRef.current;
|
||||||
|
|
||||||
// Clamp the translation to [-70, 70] to ensure the background never shows white/black gaps
|
// Allow panning within bounds (for panorama viewing)
|
||||||
const clampedX = Math.max(-70, Math.min(70, diffX));
|
const clampedX = Math.max(maxPanX, Math.min(0, diffX));
|
||||||
setTouchOffsetX(clampedX);
|
setTouchOffsetX(clampedX);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchEnd = () => {
|
const handleTouchEnd = () => {
|
||||||
if (!isSwipingRef.current) return;
|
if (!isSwipingRef.current) return;
|
||||||
isSwipingRef.current = false;
|
isSwipingRef.current = false;
|
||||||
// Threshold lowered to 50px for better swipe responsiveness on mobile screens
|
|
||||||
if (touchOffsetX > 50 && publicPhotos.length > 0) {
|
// Reset panning position when touch ends
|
||||||
setCurrentBgIndex((prev) => (prev - 1 + publicPhotos.length) % publicPhotos.length);
|
|
||||||
} else if (touchOffsetX < -50 && publicPhotos.length > 0) {
|
|
||||||
setCurrentBgIndex((prev) => (prev + 1) % publicPhotos.length);
|
|
||||||
}
|
|
||||||
setTouchOffsetX(0);
|
setTouchOffsetX(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -265,7 +263,7 @@ notify({ title: 'Thành công!', message: 'Ảnh của bạn đã được tải
|
|||||||
onTouchEnd={handleTouchEnd}
|
onTouchEnd={handleTouchEnd}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-y-0 left-[-15%] right-[-15%] cursor-grab active:cursor-grabbing"
|
className="absolute inset-y-0 left-0 right-0 cursor-grab active:cursor-grabbing"
|
||||||
style={{
|
style={{
|
||||||
transform: `translateX(${touchOffsetX}px)`,
|
transform: `translateX(${touchOffsetX}px)`,
|
||||||
transition: touchOffsetX === 0 ? 'transform 0.3s ease-out' : 'none',
|
transition: touchOffsetX === 0 ? 'transform 0.3s ease-out' : 'none',
|
||||||
@@ -280,11 +278,12 @@ notify({ title: 'Thành công!', message: 'Ảnh của bạn đã được tải
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||||
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||||
className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-[1200ms] ease-in-out animate-panning ${
|
className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-[1200ms] ease-in-out ${
|
||||||
!isLoggedIn ? 'select-none pointer-events-none' : ''
|
!isLoggedIn ? 'select-none pointer-events-none' : ''
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
opacity: fade1 ? 0.8 : 0,
|
opacity: fade1 ? 0.8 : 0,
|
||||||
|
transform: `scale(${panScale}) translateX(-${(panScale - 1) * 50}%)`,
|
||||||
}}
|
}}
|
||||||
alt="Travel Background 1"
|
alt="Travel Background 1"
|
||||||
/>
|
/>
|
||||||
@@ -298,11 +297,12 @@ notify({ title: 'Thành công!', message: 'Ảnh của bạn đã được tải
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||||
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
|
||||||
className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-[1200ms] ease-in-out animate-panning ${
|
className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-[1200ms] ease-in-out ${
|
||||||
!isLoggedIn ? 'select-none pointer-events-none' : ''
|
!isLoggedIn ? 'select-none pointer-events-none' : ''
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
opacity: fade2 ? 0.8 : 0,
|
opacity: fade2 ? 0.8 : 0,
|
||||||
|
transform: `scale(${panScale}) translateX(-${(panScale - 1) * 50}%)`,
|
||||||
}}
|
}}
|
||||||
alt="Travel Background 2"
|
alt="Travel Background 2"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) => {
|
|||||||
// Tự động gia nhập tour nếu có pendingInviteToken
|
// Tự động gia nhập tour nếu có pendingInviteToken
|
||||||
const pendingInviteToken = localStorage.getItem('pendingInviteToken');
|
const pendingInviteToken = localStorage.getItem('pendingInviteToken');
|
||||||
if (pendingInviteToken) {
|
if (pendingInviteToken) {
|
||||||
|
console.log('[SignupPage] Auto-joining tour with pending token after Google signup');
|
||||||
await fetch(`/api/v1/tours/join-by-token`, {
|
await fetch(`/api/v1/tours/join-by-token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -50,6 +51,13 @@ const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) => {
|
|||||||
Authorization: `Bearer ${data.access_token}`,
|
Authorization: `Bearer ${data.access_token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ token: pendingInviteToken }),
|
body: JSON.stringify({ token: pendingInviteToken }),
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
localStorage.removeItem('pendingInviteToken');
|
||||||
|
console.log('[SignupPage] Successfully joined tour after Google signup');
|
||||||
|
} else {
|
||||||
|
console.warn('[SignupPage] Failed to join tour after Google signup:', res.status);
|
||||||
|
}
|
||||||
}).catch((e) => console.error('Lỗi tự động gia nhập:', e));
|
}).catch((e) => console.error('Lỗi tự động gia nhập:', e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export default defineConfig(({ mode }) => {
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin-allow-popups',
|
||||||
|
'Cross-Origin-Embedder-Policy': 'unsafe-none',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||