Cleaned up login/logout flows.
This commit is contained in:
parent
10e55f63b5
commit
b324276173
@ -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',
|
||||||
@ -129,12 +129,13 @@ 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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user