Cleaned up login/logout flows.
This commit is contained in:
		| @@ -22,8 +22,12 @@ import PermissionDeniedView from '@/components/PermissionDeniedView.vue' | |||||||
| const store = useAuthStore() | const store = useAuthStore() | ||||||
|  |  | ||||||
| onMounted(async () => { | onMounted(async () => { | ||||||
|   // Detect restricted mode: any path not starting with /auth/ |   // Detect restricted mode: | ||||||
|   if (!location.pathname.startsWith('/auth/')) { |   // We only allow full functionality on the exact /auth/ (or /auth) path. | ||||||
|  |   // Any other path (including /, /foo, /auth/admin, etc.) is treated as restricted | ||||||
|  |   // so the app will only show login or permission denied views. | ||||||
|  |   const path = location.pathname | ||||||
|  |   if (!(path === '/auth/' || path === '/auth')) { | ||||||
|     store.setRestrictedMode(true) |     store.setRestrictedMode(true) | ||||||
|   } |   } | ||||||
|   // Load branding / settings first (non-blocking for auth flow) |   // Load branding / settings first (non-blocking for auth flow) | ||||||
|   | |||||||
| @@ -27,9 +27,9 @@ const handleLogin = async () => { | |||||||
|     await authStore.authenticate() |     await authStore.authenticate() | ||||||
|     authStore.showMessage('Authentication successful!', 'success', 2000) |     authStore.showMessage('Authentication successful!', 'success', 2000) | ||||||
|     if (authStore.restrictedMode) { |     if (authStore.restrictedMode) { | ||||||
|       // In restricted mode after successful auth show permission denied (no profile outside /auth/) |       // Restricted mode: reload so the app re-mounts and selectView() applies (will become permission denied) | ||||||
|       authStore.currentView = 'permission-denied' |       location.reload() | ||||||
|     } else if (location.pathname.startsWith('/auth/')) { |     } else if (location.pathname === '/auth/') { | ||||||
|       authStore.currentView = 'profile' |       authStore.currentView = 'profile' | ||||||
|     } else { |     } else { | ||||||
|       location.reload() |       location.reload() | ||||||
|   | |||||||
| @@ -32,7 +32,6 @@ function back() { | |||||||
| } | } | ||||||
| async function logout() { | async function logout() { | ||||||
|   await authStore.logout() |   await authStore.logout() | ||||||
|   authStore.currentView = 'login' |  | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
| <style scoped> | <style scoped> | ||||||
|   | |||||||
| @@ -144,7 +144,6 @@ const deleteCredential = async (credentialId) => { | |||||||
|  |  | ||||||
| const logout = async () => { | const logout = async () => { | ||||||
|   await authStore.logout() |   await authStore.logout() | ||||||
|   authStore.currentView = 'login' |  | ||||||
| } | } | ||||||
|  |  | ||||||
| const isAdmin = computed(() => !!(authStore.userInfo?.is_global_admin || authStore.userInfo?.is_org_admin)) | const isAdmin = computed(() => !!(authStore.userInfo?.is_global_admin || authStore.userInfo?.is_org_admin)) | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const useAuthStore = defineStore('auth', { | |||||||
|     settings: null, // Server provided settings (/auth/settings) |     settings: null, // Server provided settings (/auth/settings) | ||||||
|     isLoading: false, |     isLoading: false, | ||||||
|     resetToken: null, // transient reset token |     resetToken: null, // transient reset token | ||||||
|     restrictedMode: false, // If true, app loaded outside /auth/ and should restrict to login or permission denied |     restrictedMode: false, // Anywhere other than /auth/: restrict to login or permission denied | ||||||
|  |  | ||||||
|     // UI State |     // UI State | ||||||
|     currentView: 'login', |     currentView: 'login', | ||||||
| @@ -130,11 +130,12 @@ export const useAuthStore = defineStore('auth', { | |||||||
|     async logout() { |     async logout() { | ||||||
|       try { |       try { | ||||||
|         await fetch('/auth/api/logout', {method: 'POST'}) |         await fetch('/auth/api/logout', {method: 'POST'}) | ||||||
|  |         this.userInfo = null | ||||||
|  |         if (this.restrictedMode) location.reload() | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         console.error('Logout error:', error) |         console.error('Logout error:', error) | ||||||
|  |         this.showMessage(error.message, 'error') | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       this.userInfo = null |  | ||||||
|     }, |     }, | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import logging | import logging | ||||||
| from uuid import UUID, uuid4 | from uuid import UUID, uuid4 | ||||||
|  |  | ||||||
| from fastapi import Body, Cookie, FastAPI, HTTPException | from fastapi import Body, Cookie, FastAPI, HTTPException, Request | ||||||
| from fastapi.responses import FileResponse, JSONResponse | from fastapi.responses import FileResponse, JSONResponse, RedirectResponse | ||||||
|  |  | ||||||
| from ..authsession import expires | from ..authsession import expires | ||||||
| from ..globals import db | from ..globals import db | ||||||
| @@ -24,8 +24,14 @@ async def general_exception_handler(_request, exc: Exception): | |||||||
|     return JSONResponse(status_code=500, content={"detail": "Internal server error"}) |     return JSONResponse(status_code=500, content={"detail": "Internal server error"}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.get("") | ||||||
|  | def adminapp_slashmissing(request: Request): | ||||||
|  |     print("HERE") | ||||||
|  |     return RedirectResponse(url=request.url_for("adminapp")) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/") | @app.get("/") | ||||||
| async def admin_frontend(auth=Cookie(None)): | async def adminapp(auth=Cookie(None)): | ||||||
|     try: |     try: | ||||||
|         await authz.verify(auth, ["auth:admin", "auth:org:*"], match=permutil.has_any) |         await authz.verify(auth, ["auth:admin", "auth:org:*"], match=permutil.has_any) | ||||||
|         return FileResponse(frontend.file("admin/index.html")) |         return FileResponse(frontend.file("admin/index.html")) | ||||||
|   | |||||||
| @@ -1,7 +1,11 @@ | |||||||
|  | import logging | ||||||
|  |  | ||||||
| from fastapi import HTTPException | from fastapi import HTTPException | ||||||
|  |  | ||||||
| from ..util import permutil | from ..util import permutil | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def verify(auth: str | None, perm: list[str], match=permutil.has_all): | async def verify(auth: str | None, perm: list[str], match=permutil.has_all): | ||||||
|     """Validate session token and optional list of required permissions. |     """Validate session token and optional list of required permissions. | ||||||
| @@ -20,6 +24,16 @@ async def verify(auth: str | None, perm: list[str], match=permutil.has_all): | |||||||
|         raise HTTPException(status_code=401, detail="Session not found") |         raise HTTPException(status_code=401, detail="Session not found") | ||||||
|  |  | ||||||
|     if not match(ctx, perm): |     if not match(ctx, perm): | ||||||
|  |         # Determine which permissions are missing for clearer diagnostics | ||||||
|  |         missing = sorted(set(perm) - set(ctx.role.permissions)) | ||||||
|  |         logger.warning( | ||||||
|  |             "Permission denied: user=%s role=%s missing=%s required=%s granted=%s",  # noqa: E501 | ||||||
|  |             getattr(ctx.user, "uuid", "?"), | ||||||
|  |             getattr(ctx.role, "display_name", "?"), | ||||||
|  |             missing, | ||||||
|  |             perm, | ||||||
|  |             ctx.role.permissions, | ||||||
|  |         ) | ||||||
|         raise HTTPException(status_code=403, detail="Permission required") |         raise HTTPException(status_code=403, detail="Permission required") | ||||||
|  |  | ||||||
|     return ctx |     return ctx | ||||||
|   | |||||||
| @@ -46,10 +46,12 @@ async def lifespan(app: FastAPI):  # pragma: no cover - startup path | |||||||
|  |  | ||||||
|  |  | ||||||
| app = FastAPI(lifespan=lifespan) | app = FastAPI(lifespan=lifespan) | ||||||
| app.mount("/auth/admin", admin.app) | app.mount("/auth/admin/", admin.app) | ||||||
| app.mount("/auth/api", api.app) | app.mount("/auth/api/", api.app) | ||||||
| app.mount("/auth/ws", ws.app) | app.mount("/auth/ws/", ws.app) | ||||||
| app.mount("/auth/assets", StaticFiles(directory=frontend.file("assets")), name="assets") | app.mount( | ||||||
|  |     "/auth/assets/", StaticFiles(directory=frontend.file("assets")), name="assets" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.get("/auth/") | @app.get("/auth/") | ||||||
| @@ -61,6 +63,9 @@ async def frontapp(): | |||||||
| @app.get("/auth/{reset}") | @app.get("/auth/{reset}") | ||||||
| async def reset_link(request: Request, reset: str): | async def reset_link(request: Request, reset: str): | ||||||
|     """Pretty URL for reset links.""" |     """Pretty URL for reset links.""" | ||||||
|  |     if reset == "admin": | ||||||
|  |         # Admin app missing trailing slash lands here, be friendly to user | ||||||
|  |         return RedirectResponse(request.url_for("adminapp"), status_code=303) | ||||||
|     if not passphrase.is_well_formed(reset): |     if not passphrase.is_well_formed(reset): | ||||||
|         raise HTTPException(status_code=404) |         raise HTTPException(status_code=404) | ||||||
|     url = request.url_for("frontapp").include_query_params(reset=reset) |     url = request.url_for("frontapp").include_query_params(reset=reset) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Leo Vasanko
					Leo Vasanko