All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 1m48s
176 lines
5.3 KiB
TypeScript
176 lines
5.3 KiB
TypeScript
import { ref } from 'vue'
|
|
|
|
const hasLocalStorage =
|
|
typeof localStorage !== 'undefined' && typeof localStorage.getItem === 'function'
|
|
const AUTH_SYNC_EVENT_KEY = 'authSyncEvent'
|
|
const PARENT_AUTH_KEY = 'parentAuth'
|
|
const PARENT_AUTH_EXPIRY_NON_PERSISTENT = 60_000 // 1 minute
|
|
const PARENT_AUTH_EXPIRY_PERSISTENT = 172_800_000 // 2 days
|
|
|
|
// --- Parent auth expiry state ---
|
|
export const isParentAuthenticated = ref(false)
|
|
export const isParentPersistent = ref(false)
|
|
export const parentAuthExpiresAt = ref<number | null>(null)
|
|
|
|
// --- Background expiry watcher ---
|
|
// Declared before the init block so startParentExpiryWatcher is safe to call during restore.
|
|
let expiryWatcherIntervalId: ReturnType<typeof setInterval> | null = null
|
|
|
|
function runExpiryCheck() {
|
|
if (parentAuthExpiresAt.value !== null && Date.now() >= parentAuthExpiresAt.value) {
|
|
applyParentLoggedOutState()
|
|
if (typeof window !== 'undefined') {
|
|
window.location.href = '/child'
|
|
}
|
|
}
|
|
}
|
|
|
|
export function startParentExpiryWatcher() {
|
|
stopParentExpiryWatcher()
|
|
expiryWatcherIntervalId = setInterval(runExpiryCheck, 15_000)
|
|
}
|
|
|
|
export function stopParentExpiryWatcher() {
|
|
if (expiryWatcherIntervalId !== null) {
|
|
clearInterval(expiryWatcherIntervalId)
|
|
expiryWatcherIntervalId = null
|
|
}
|
|
}
|
|
|
|
// Restore persistent parent auth from localStorage on store init
|
|
if (hasLocalStorage) {
|
|
try {
|
|
const stored = localStorage.getItem(PARENT_AUTH_KEY)
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored) as { expiresAt: number }
|
|
if (parsed.expiresAt && Date.now() < parsed.expiresAt) {
|
|
parentAuthExpiresAt.value = parsed.expiresAt
|
|
isParentPersistent.value = true
|
|
isParentAuthenticated.value = true
|
|
startParentExpiryWatcher()
|
|
} else {
|
|
localStorage.removeItem(PARENT_AUTH_KEY)
|
|
}
|
|
}
|
|
} catch {
|
|
localStorage.removeItem(PARENT_AUTH_KEY)
|
|
}
|
|
}
|
|
|
|
export const isUserLoggedIn = ref(false)
|
|
export const isAuthReady = ref(false)
|
|
export const currentUserId = ref('')
|
|
export const suppressForceLogout = ref(false)
|
|
let authSyncInitialized = false
|
|
|
|
/**
|
|
* Explicitly checks whether parent auth has expired and clears it if so.
|
|
* Called by the router guard before allowing /parent routes.
|
|
*/
|
|
export function enforceParentExpiry() {
|
|
runExpiryCheck()
|
|
}
|
|
|
|
export function authenticateParent(persistent: boolean) {
|
|
const duration = persistent ? PARENT_AUTH_EXPIRY_PERSISTENT : PARENT_AUTH_EXPIRY_NON_PERSISTENT
|
|
parentAuthExpiresAt.value = Date.now() + duration
|
|
isParentPersistent.value = persistent
|
|
isParentAuthenticated.value = true
|
|
if (persistent && hasLocalStorage && typeof localStorage.setItem === 'function') {
|
|
localStorage.setItem(PARENT_AUTH_KEY, JSON.stringify({ expiresAt: parentAuthExpiresAt.value }))
|
|
}
|
|
startParentExpiryWatcher()
|
|
}
|
|
|
|
export function logoutParent() {
|
|
if (hasLocalStorage && typeof localStorage.removeItem === 'function') {
|
|
localStorage.removeItem(PARENT_AUTH_KEY)
|
|
}
|
|
applyParentLoggedOutState()
|
|
broadcastParentLogoutEvent()
|
|
}
|
|
|
|
function applyParentLoggedOutState() {
|
|
parentAuthExpiresAt.value = null
|
|
isParentPersistent.value = false
|
|
isParentAuthenticated.value = false
|
|
stopParentExpiryWatcher()
|
|
}
|
|
|
|
function broadcastParentLogoutEvent() {
|
|
if (!hasLocalStorage || typeof localStorage.setItem !== 'function') return
|
|
localStorage.setItem(
|
|
AUTH_SYNC_EVENT_KEY,
|
|
JSON.stringify({ type: 'parent_logout', at: Date.now() }),
|
|
)
|
|
}
|
|
|
|
export function loginUser() {
|
|
isUserLoggedIn.value = true
|
|
// Always start in child mode after login. Also explicitly remove persistent parent
|
|
// auth from localStorage so a fresh login always starts in child mode.
|
|
if (hasLocalStorage && typeof localStorage.removeItem === 'function') {
|
|
localStorage.removeItem(PARENT_AUTH_KEY)
|
|
}
|
|
applyParentLoggedOutState()
|
|
}
|
|
|
|
function applyLoggedOutState() {
|
|
isUserLoggedIn.value = false
|
|
currentUserId.value = ''
|
|
applyParentLoggedOutState()
|
|
}
|
|
|
|
function broadcastLogoutEvent() {
|
|
if (!hasLocalStorage || typeof localStorage.setItem !== 'function') return
|
|
localStorage.setItem(AUTH_SYNC_EVENT_KEY, JSON.stringify({ type: 'logout', at: Date.now() }))
|
|
}
|
|
|
|
export function logoutUser() {
|
|
applyLoggedOutState()
|
|
broadcastLogoutEvent()
|
|
}
|
|
|
|
export function initAuthSync() {
|
|
if (authSyncInitialized || typeof window === 'undefined') return
|
|
authSyncInitialized = true
|
|
|
|
window.addEventListener('storage', (event) => {
|
|
if (event.key !== AUTH_SYNC_EVENT_KEY || !event.newValue) return
|
|
try {
|
|
const payload = JSON.parse(event.newValue)
|
|
if (payload?.type === 'logout') {
|
|
applyLoggedOutState()
|
|
if (!window.location.pathname.startsWith('/auth') && window.location.pathname !== '/') {
|
|
window.location.href = '/'
|
|
}
|
|
} else if (payload?.type === 'parent_logout') {
|
|
applyParentLoggedOutState()
|
|
if (window.location.pathname.startsWith('/parent')) {
|
|
window.location.href = '/child'
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore malformed sync events.
|
|
}
|
|
})
|
|
}
|
|
|
|
export async function checkAuth() {
|
|
try {
|
|
const res = await fetch('/api/auth/me', { method: 'GET' })
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
currentUserId.value = data.id
|
|
isUserLoggedIn.value = true
|
|
} else {
|
|
isUserLoggedIn.value = false
|
|
currentUserId.value = ''
|
|
}
|
|
} catch {
|
|
isUserLoggedIn.value = false
|
|
currentUserId.value = ''
|
|
}
|
|
isAuthReady.value = true
|
|
}
|