final logout related fixes and cookies and session
This commit is contained in:
@@ -746,7 +746,7 @@ class User(AbstractUser):
|
||||
if not self.account:
|
||||
return Site.objects.none()
|
||||
|
||||
base_sites = Site.objects.filter(account=self.account, is_active=True)
|
||||
base_sites = Site.objects.filter(account=self.account)
|
||||
|
||||
if self.role in ['owner', 'admin', 'developer'] or self.is_superuser or self.is_system_account_user():
|
||||
return base_sites
|
||||
|
||||
@@ -46,14 +46,56 @@ class RegisterView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
from .utils import generate_access_token, generate_refresh_token, get_token_expiry
|
||||
from django.contrib.auth import login
|
||||
from .utils import generate_access_token, generate_refresh_token, get_access_token_expiry, get_refresh_token_expiry
|
||||
from django.contrib.auth import login, logout
|
||||
from django.utils import timezone
|
||||
|
||||
force_logout = request.data.get('force_logout', False)
|
||||
|
||||
serializer = RegisterSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
user = serializer.save()
|
||||
|
||||
# SECURITY: Check for session contamination before login
|
||||
# If there's an existing session from a different user, handle it
|
||||
if request.session.session_key:
|
||||
existing_user_id = request.session.get('_auth_user_id')
|
||||
if existing_user_id and str(existing_user_id) != str(user.id):
|
||||
# Get existing user details
|
||||
try:
|
||||
existing_user = User.objects.get(id=existing_user_id)
|
||||
existing_email = existing_user.email
|
||||
existing_username = existing_user.username or existing_email.split('@')[0]
|
||||
except User.DoesNotExist:
|
||||
existing_email = 'Unknown user'
|
||||
existing_username = 'Unknown'
|
||||
|
||||
# If not forcing logout, return conflict info
|
||||
if not force_logout:
|
||||
return Response(
|
||||
{
|
||||
'status': 'error',
|
||||
'error': 'session_conflict',
|
||||
'message': f'You have an active session for another account ({existing_email}). Please logout first or choose to continue.',
|
||||
'existing_user': {
|
||||
'email': existing_email,
|
||||
'username': existing_username,
|
||||
'id': existing_user_id
|
||||
},
|
||||
'requested_user': {
|
||||
'email': user.email,
|
||||
'username': user.username or user.email.split('@')[0],
|
||||
'id': user.id
|
||||
}
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
# Force logout - clean existing session completely
|
||||
logout(request)
|
||||
# Clear all session data
|
||||
request.session.flush()
|
||||
|
||||
# Log the user in (create session for session authentication)
|
||||
login(request, user)
|
||||
|
||||
@@ -63,8 +105,8 @@ class RegisterView(APIView):
|
||||
# Generate JWT tokens
|
||||
access_token = generate_access_token(user, account)
|
||||
refresh_token = generate_refresh_token(user, account)
|
||||
access_expires_at = timezone.now() + get_token_expiry('access')
|
||||
refresh_expires_at = timezone.now() + get_token_expiry('refresh')
|
||||
access_expires_at = timezone.now() + get_access_token_expiry()
|
||||
refresh_expires_at = timezone.now() + get_refresh_token_expiry()
|
||||
|
||||
user_serializer = UserSerializer(user)
|
||||
return success_response(
|
||||
@@ -104,6 +146,7 @@ class LoginView(APIView):
|
||||
email = serializer.validated_data['email']
|
||||
password = serializer.validated_data['password']
|
||||
remember_me = serializer.validated_data.get('remember_me', False)
|
||||
force_logout = request.data.get('force_logout', False)
|
||||
|
||||
try:
|
||||
user = User.objects.select_related('account', 'account__plan').get(email=email)
|
||||
@@ -115,6 +158,47 @@ class LoginView(APIView):
|
||||
)
|
||||
|
||||
if user.check_password(password):
|
||||
# SECURITY: Check for session contamination before login
|
||||
# If user has a session cookie from a different user, handle it
|
||||
if request.session.session_key:
|
||||
existing_user_id = request.session.get('_auth_user_id')
|
||||
if existing_user_id and str(existing_user_id) != str(user.id):
|
||||
# Get existing user details
|
||||
try:
|
||||
existing_user = User.objects.get(id=existing_user_id)
|
||||
existing_email = existing_user.email
|
||||
existing_username = existing_user.username or existing_email.split('@')[0]
|
||||
except User.DoesNotExist:
|
||||
existing_email = 'Unknown user'
|
||||
existing_username = 'Unknown'
|
||||
|
||||
# If not forcing logout, return conflict info
|
||||
if not force_logout:
|
||||
return Response(
|
||||
{
|
||||
'status': 'error',
|
||||
'error': 'session_conflict',
|
||||
'message': f'You have an active session for another account ({existing_email}). Please logout first or choose to continue.',
|
||||
'existing_user': {
|
||||
'email': existing_email,
|
||||
'username': existing_username,
|
||||
'id': existing_user_id
|
||||
},
|
||||
'requested_user': {
|
||||
'email': user.email,
|
||||
'username': user.username or user.email.split('@')[0],
|
||||
'id': user.id
|
||||
}
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
# Force logout - clean existing session completely
|
||||
from django.contrib.auth import logout
|
||||
logout(request)
|
||||
# Clear all session data
|
||||
request.session.flush()
|
||||
|
||||
# Log the user in (create session for session authentication)
|
||||
from django.contrib.auth import login
|
||||
login(request, user)
|
||||
@@ -123,12 +207,12 @@ class LoginView(APIView):
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
# Generate JWT tokens
|
||||
from .utils import generate_access_token, generate_refresh_token, get_access_token_expiry, get_token_expiry
|
||||
from .utils import generate_access_token, generate_refresh_token, get_access_token_expiry, get_refresh_token_expiry
|
||||
from django.utils import timezone
|
||||
access_token = generate_access_token(user, account, remember_me=remember_me)
|
||||
refresh_token = generate_refresh_token(user, account)
|
||||
access_expires_at = timezone.now() + get_access_token_expiry(remember_me=remember_me)
|
||||
refresh_expires_at = timezone.now() + get_token_expiry('refresh')
|
||||
refresh_expires_at = timezone.now() + get_refresh_token_expiry()
|
||||
|
||||
# Serialize user data safely, handling missing account relationship
|
||||
try:
|
||||
@@ -274,6 +358,7 @@ class RefreshTokenView(APIView):
|
||||
account = getattr(user, 'account', None)
|
||||
|
||||
# Generate new access token
|
||||
from .utils import get_token_expiry
|
||||
access_token = generate_access_token(user, account)
|
||||
access_expires_at = get_token_expiry('access')
|
||||
|
||||
|
||||
@@ -92,15 +92,17 @@ CSRF_TRUSTED_ORIGINS = [
|
||||
USE_SECURE_COOKIES = os.getenv('USE_SECURE_COOKIES', 'False').lower() == 'true'
|
||||
SESSION_COOKIE_SECURE = USE_SECURE_COOKIES
|
||||
CSRF_COOKIE_SECURE = USE_SECURE_COOKIES
|
||||
CSRF_COOKIE_SAMESITE = 'Lax' # Match session cookie setting
|
||||
CSRF_COOKIE_DOMAIN = '.igny8.com' # Share CSRF cookie across subdomains
|
||||
|
||||
# CRITICAL: Session isolation to prevent contamination
|
||||
SESSION_COOKIE_NAME = 'igny8_sessionid' # Custom name to avoid conflicts
|
||||
SESSION_COOKIE_HTTPONLY = True # Prevent JavaScript access
|
||||
SESSION_COOKIE_SAMESITE = 'Strict' # Prevent cross-site cookie sharing
|
||||
SESSION_COOKIE_SAMESITE = 'Lax' # Changed from Strict to Lax - allows same-site top-level navigation
|
||||
SESSION_COOKIE_AGE = 3600 # 1 hour - extends on every request due to SESSION_SAVE_EVERY_REQUEST
|
||||
SESSION_SAVE_EVERY_REQUEST = True # CRITICAL: Update session on every request to prevent idle timeout
|
||||
SESSION_COOKIE_PATH = '/' # Explicit path
|
||||
# Don't set SESSION_COOKIE_DOMAIN - let it default to current domain for strict isolation
|
||||
SESSION_COOKIE_DOMAIN = '.igny8.com' # CRITICAL: Share cookie across subdomains (app.igny8.com and api.igny8.com)
|
||||
|
||||
# CRITICAL: Custom authentication backend to disable user caching
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
|
||||
@@ -24,6 +24,12 @@ export default function SignInForm() {
|
||||
const [error, setError] = useState("");
|
||||
const [logoutReason, setLogoutReason] = useState<LogoutReason | null>(null);
|
||||
const [showLogoutDetails, setShowLogoutDetails] = useState(false);
|
||||
const [sessionConflict, setSessionConflict] = useState<{
|
||||
message: string;
|
||||
existingUser: { email: string; username: string; id: number };
|
||||
requestedUser: { email: string; username: string; id: number };
|
||||
} | null>(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { login, loading } = useAuthStore();
|
||||
@@ -52,9 +58,10 @@ export default function SignInForm() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const handleSubmit = async (e: React.FormEvent, forceLogout = false) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setSessionConflict(null);
|
||||
|
||||
if (!email || !password) {
|
||||
setError("Please enter both email and password");
|
||||
@@ -62,11 +69,21 @@ export default function SignInForm() {
|
||||
}
|
||||
|
||||
try {
|
||||
await login(email, password, isChecked);
|
||||
await login(email, password, isChecked, forceLogout);
|
||||
// Redirect to the page user was trying to access, or home
|
||||
const from = (location.state as any)?.from?.pathname || "/";
|
||||
navigate(from, { replace: true });
|
||||
} catch (err: any) {
|
||||
// Handle session conflict
|
||||
if (err?.type === 'SESSION_CONFLICT') {
|
||||
setSessionConflict({
|
||||
message: err.message,
|
||||
existingUser: err.existingUser,
|
||||
requestedUser: err.requestedUser,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (err?.code === 'PLAN_REQUIRED') {
|
||||
window.location.href = 'https://igny8.com/pricing';
|
||||
return;
|
||||
@@ -74,6 +91,12 @@ export default function SignInForm() {
|
||||
setError(err.message || "Login failed. Please check your credentials.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleForceLogout = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await handleSubmit(e, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="w-full max-w-md pt-10 mx-auto">
|
||||
@@ -202,6 +225,46 @@ export default function SignInForm() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Session Conflict Alert */}
|
||||
{sessionConflict && (
|
||||
<div className="p-4 bg-orange-50 border border-orange-200 rounded-lg dark:bg-orange-900/20 dark:border-orange-800">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<svg className="w-5 h-5 text-orange-600 dark:text-orange-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<h4 className="font-semibold text-orange-800 dark:text-orange-300">
|
||||
Active Session Detected
|
||||
</h4>
|
||||
</div>
|
||||
<p className="text-sm text-orange-700 dark:text-orange-400 mb-2">
|
||||
You have an active session for:
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-orange-800 dark:text-orange-300 mb-3">
|
||||
{sessionConflict.existingUser.email}
|
||||
</p>
|
||||
<p className="text-sm text-orange-700 dark:text-orange-400 mb-4">
|
||||
You're trying to login as: <strong>{sessionConflict.requestedUser.email}</strong>
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleForceLogout}
|
||||
disabled={loading}
|
||||
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-orange-600 rounded-lg hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? 'Logging out...' : 'Logout Previous & Continue'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSessionConflict(null)}
|
||||
className="flex-1 px-4 py-2 text-sm font-medium text-orange-700 bg-orange-100 rounded-lg hover:bg-orange-200 dark:bg-orange-900/40 dark:text-orange-300 dark:hover:bg-orange-900/60 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-lg dark:bg-red-900/20 dark:text-red-400 dark:border-red-800">
|
||||
{error}
|
||||
|
||||
@@ -54,7 +54,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
isAuthenticated: false,
|
||||
loading: false, // Always start with loading false - will be set true only during login/register
|
||||
|
||||
login: async (email, password, rememberMe = false) => {
|
||||
login: async (email, password, rememberMe = false, forceLogout = false) => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'https://api.igny8.com/api';
|
||||
@@ -63,11 +63,23 @@ export const useAuthStore = create<AuthState>()(
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password, remember_me: rememberMe }),
|
||||
credentials: 'include', // Include cookies
|
||||
body: JSON.stringify({ email, password, remember_me: rememberMe, force_logout: forceLogout }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Handle session conflict (409)
|
||||
if (response.status === 409 && data.error === 'session_conflict') {
|
||||
set({ loading: false });
|
||||
throw {
|
||||
type: 'SESSION_CONFLICT',
|
||||
message: data.message,
|
||||
existingUser: data.existing_user,
|
||||
requestedUser: data.requested_user,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
const message = data.error || data.message || 'Login failed';
|
||||
if (response.status === 402) {
|
||||
|
||||
Reference in New Issue
Block a user