Cleaned up login/logout flows.

This commit is contained in:
Leo Vasanko 2025-09-02 19:08:16 -06:00
parent 10e55f63b5
commit b324276173
8 changed files with 46 additions and 18 deletions

View File

@ -22,8 +22,12 @@ import PermissionDeniedView from '@/components/PermissionDeniedView.vue'
const store = useAuthStore()
onMounted(async () => {
// Detect restricted mode: any path not starting with /auth/
if (!location.pathname.startsWith('/auth/')) {
// Detect restricted mode:
// 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)
}
// Load branding / settings first (non-blocking for auth flow)

View File

@ -27,9 +27,9 @@ const handleLogin = async () => {
await authStore.authenticate()
authStore.showMessage('Authentication successful!', 'success', 2000)
if (authStore.restrictedMode) {
// In restricted mode after successful auth show permission denied (no profile outside /auth/)
authStore.currentView = 'permission-denied'
} else if (location.pathname.startsWith('/auth/')) {
// Restricted mode: reload so the app re-mounts and selectView() applies (will become permission denied)
location.reload()
} else if (location.pathname === '/auth/') {
authStore.currentView = 'profile'
} else {
location.reload()

View File

@ -32,7 +32,6 @@ function back() {
}
async function logout() {
await authStore.logout()
authStore.currentView = 'login'
}
</script>
<style scoped>

View File

@ -144,7 +144,6 @@ const deleteCredential = async (credentialId) => {
const logout = async () => {
await authStore.logout()
authStore.currentView = 'login'
}
const isAdmin = computed(() => !!(authStore.userInfo?.is_global_admin || authStore.userInfo?.is_org_admin))

View File

@ -8,7 +8,7 @@ export const useAuthStore = defineStore('auth', {
settings: null, // Server provided settings (/auth/settings)
isLoading: false,
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
currentView: 'login',
@ -129,12 +129,13 @@ export const useAuthStore = defineStore('auth', {
},
async logout() {
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) {
console.error('Logout error:', error)
this.showMessage(error.message, 'error')
}
this.userInfo = null
},
}
})

View File

@ -1,8 +1,8 @@
import logging
from uuid import UUID, uuid4
from fastapi import Body, Cookie, FastAPI, HTTPException
from fastapi.responses import FileResponse, JSONResponse
from fastapi import Body, Cookie, FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from ..authsession import expires
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"})
@app.get("")
def adminapp_slashmissing(request: Request):
print("HERE")
return RedirectResponse(url=request.url_for("adminapp"))
@app.get("/")
async def admin_frontend(auth=Cookie(None)):
async def adminapp(auth=Cookie(None)):
try:
await authz.verify(auth, ["auth:admin", "auth:org:*"], match=permutil.has_any)
return FileResponse(frontend.file("admin/index.html"))

View File

@ -1,7 +1,11 @@
import logging
from fastapi import HTTPException
from ..util import permutil
logger = logging.getLogger(__name__)
async def verify(auth: str | None, perm: list[str], match=permutil.has_all):
"""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")
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")
return ctx

View File

@ -46,10 +46,12 @@ async def lifespan(app: FastAPI): # pragma: no cover - startup path
app = FastAPI(lifespan=lifespan)
app.mount("/auth/admin", admin.app)
app.mount("/auth/api", api.app)
app.mount("/auth/ws", ws.app)
app.mount("/auth/assets", StaticFiles(directory=frontend.file("assets")), name="assets")
app.mount("/auth/admin/", admin.app)
app.mount("/auth/api/", api.app)
app.mount("/auth/ws/", ws.app)
app.mount(
"/auth/assets/", StaticFiles(directory=frontend.file("assets")), name="assets"
)
@app.get("/auth/")
@ -61,6 +63,9 @@ async def frontapp():
@app.get("/auth/{reset}")
async def reset_link(request: Request, reset: str):
"""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):
raise HTTPException(status_code=404)
url = request.url_for("frontapp").include_query_params(reset=reset)