feat: implement force logout event and update navigation redirects
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 1m37s

This commit is contained in:
2026-03-05 09:52:19 -05:00
parent 992dd8423f
commit ccfc710753
8 changed files with 38 additions and 10 deletions

View File

@@ -14,6 +14,10 @@ from werkzeug.security import generate_password_hash, check_password_hash
from api.utils import sanitize_email from api.utils import sanitize_email
from config.paths import get_user_image_dir from config.paths import get_user_image_dir
from events.sse import send_event_to_user
from events.types.event import Event
from events.types.event_types import EventType
from events.types.payload import Payload
from api.error_codes import ( from api.error_codes import (
MISSING_FIELDS, EMAIL_EXISTS, MISSING_TOKEN, INVALID_TOKEN, TOKEN_TIMESTAMP_MISSING, MISSING_FIELDS, EMAIL_EXISTS, MISSING_TOKEN, INVALID_TOKEN, TOKEN_TIMESTAMP_MISSING,
@@ -359,6 +363,9 @@ def reset_password():
# Invalidate ALL refresh tokens for this user # Invalidate ALL refresh tokens for this user
refresh_tokens_db.remove(TokenQuery.user_id == user.id) refresh_tokens_db.remove(TokenQuery.user_id == user.id)
# Notify all active sessions (other tabs/devices) to sign out immediately
send_event_to_user(user.id, Event(EventType.FORCE_LOGOUT.value, Payload({})))
resp = jsonify({'message': 'Password has been reset'}) resp = jsonify({'message': 'Password has been reset'})
_clear_auth_cookies(resp) _clear_auth_cookies(resp)
return resp, 200 return resp, 200

View File

@@ -27,3 +27,5 @@ class EventType(Enum):
CHORE_SCHEDULE_MODIFIED = "chore_schedule_modified" CHORE_SCHEDULE_MODIFIED = "chore_schedule_modified"
CHORE_TIME_EXTENDED = "chore_time_extended" CHORE_TIME_EXTENDED = "chore_time_extended"
CHILD_CHORE_CONFIRMATION = "child_chore_confirmation" CHILD_CHORE_CONFIRMATION = "child_chore_confirmation"
FORCE_LOGOUT = "force_logout"

View File

@@ -28,7 +28,7 @@ function handleUnauthorizedResponse(): void {
unauthorizedRedirectHandler() unauthorizedRedirectHandler()
return return
} }
window.location.assign('/auth') window.location.assign('/')
} }
/** /**

View File

@@ -1,9 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue' import { ref, watch, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useBackendEvents } from '@/common/backendEvents' import { useBackendEvents } from '@/common/backendEvents'
import { currentUserId } from '@/stores/auth' import { currentUserId, logoutUser } from '@/stores/auth'
import { eventBus } from '@/common/eventBus'
const userId = ref(currentUserId.value) const userId = ref(currentUserId.value)
const router = useRouter()
watch(currentUserId, (id) => { watch(currentUserId, (id) => {
userId.value = id userId.value = id
@@ -11,6 +14,19 @@ watch(currentUserId, (id) => {
// Always call useBackendEvents in setup, passing the reactive userId // Always call useBackendEvents in setup, passing the reactive userId
useBackendEvents(userId) useBackendEvents(userId)
function handleForceLogout() {
logoutUser()
router.push('/')
}
onMounted(() => {
eventBus.on('force_logout', handleForceLogout)
})
onUnmounted(() => {
eventBus.off('force_logout', handleForceLogout)
})
</script> </script>
<template> <template>

View File

@@ -3,7 +3,7 @@
<!-- Sticky nav --> <!-- Sticky nav -->
<nav class="landing-nav" :class="{ 'landing-nav-scrolled': scrolled }"> <nav class="landing-nav" :class="{ 'landing-nav-scrolled': scrolled }">
<div class="nav-inner"> <div class="nav-inner">
<img src="/images/c_logo.png" alt="Chorly" class="nav-logo" /> <img src="/images/c_logo.png" alt="Chorly" class="nav-logo" @click="scrollToTop" />
<button class="nav-signin" @click="goToLogin">Sign In</button> <button class="nav-signin" @click="goToLogin">Sign In</button>
</div> </div>
</nav> </nav>
@@ -34,6 +34,10 @@ function goToLogin() {
router.push({ name: 'Login' }) router.push({ name: 'Login' })
} }
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
onMounted(() => { onMounted(() => {
window.addEventListener('scroll', onScroll, { passive: true }) window.addEventListener('scroll', onScroll, { passive: true })
}) })
@@ -82,6 +86,7 @@ onUnmounted(() => {
.nav-logo { .nav-logo {
height: 34px; height: 34px;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.35)); filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.35));
cursor: pointer;
} }
.nav-signin { .nav-signin {

View File

@@ -341,7 +341,7 @@ function handleDeleteSuccess() {
}).finally(() => { }).finally(() => {
// Clear client-side auth and redirect, regardless of logout response // Clear client-side auth and redirect, regardless of logout response
logoutUser() logoutUser()
router.push('/auth/login') router.push('/')
}) })
} }

View File

@@ -225,7 +225,7 @@ async function signOut() {
try { try {
await fetch('/api/auth/logout', { method: 'POST' }) await fetch('/api/auth/logout', { method: 'POST' })
logoutUser() logoutUser()
router.push('/auth') router.push('/')
} catch { } catch {
// Optionally show error // Optionally show error
} }

View File

@@ -21,10 +21,8 @@ const router = useRouter()
const route = useRoute() const route = useRoute()
const handleBack = () => { const handleBack = () => {
// route to the auth landing page instead of using browser history router.push({ name: 'LandingPage' }).catch(() => {
router.push({ name: 'AuthLanding' }).catch(() => { window.location.href = '/'
// fallback to a safe path if named route isn't available
window.location.href = '/auth'
}) })
} }