Files
auth-portal/apps/web/src/lib/auth.ts
T
Denozordec 09ed7f4dc4
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 1m49s
Build and Push Auth Portal Docker Image / create-release (push) Skipped
feat(auth): refactor authentication routes and enhance JWT handling
- Removed unused permission and app retrieval logic from the login process.
- Introduced a new endpoint for reissuing access tokens to handle role changes and account switches.
- Updated JWT payload handling to improve clarity and maintainability.
- Enhanced the readJwtPayload function for better decoding of JWT claims.
2026-07-19 02:22:23 +07:00

133 lines
3.8 KiB
TypeScript

import {
buildSsoRedirectUrl,
isJwtExpired,
isReturnToAllowed,
readJwtPayload,
type LoginResponse,
type MeResponse,
} from '@authportal/shared'
const TOKEN_KEY = 'authportal_token'
/** Fallback if /api/v1/auth/config unavailable (dev). Includes `private` for LAN SSO. */
export const DEFAULT_RETURN_TO_ALLOWLIST =
'.shnt.top,localhost,private,http://localhost:5173'
let returnToAllowlist: string | null = null
let returnToAllowlistPromise: Promise<string> | null = null
export async function ensureReturnToAllowlist(): Promise<string> {
if (returnToAllowlist) return returnToAllowlist
if (returnToAllowlistPromise) return returnToAllowlistPromise
returnToAllowlistPromise = (async () => {
const fromVite = import.meta.env.VITE_RETURN_TO_ALLOWLIST as
| string
| undefined
try {
const res = await fetch('/api/v1/auth/config')
if (res.ok) {
const data = (await res.json()) as { return_to_allowlist?: string }
if (data.return_to_allowlist) {
returnToAllowlist = data.return_to_allowlist
return returnToAllowlist
}
}
} catch {
/* ignore */
}
returnToAllowlist = fromVite || DEFAULT_RETURN_TO_ALLOWLIST
return returnToAllowlist
})().finally(() => {
returnToAllowlistPromise = null
})
return returnToAllowlistPromise
}
export function getToken(): string | null {
const token = localStorage.getItem(TOKEN_KEY)
if (!token) return null
if (isJwtExpired(token)) {
localStorage.removeItem(TOKEN_KEY)
return null
}
return token
}
export function setToken(token: string) {
localStorage.setItem(TOKEN_KEY, token)
}
export function clearToken() {
localStorage.removeItem(TOKEN_KEY)
}
/** True when stored JWT claims disagree with live /me (DB). */
export function jwtClaimsStaleVsMe(token: string, me: MeResponse): boolean {
const payload = readJwtPayload(token)
if (!payload) return true
if (Boolean(payload.is_admin) !== Boolean(me.is_admin)) return true
const claimApps = Array.isArray(payload.apps)
? payload.apps.map(String).sort().join(',')
: ''
const meApps = [...me.apps].map(String).sort().join(',')
if (claimApps !== meApps) return true
const claimPerms = Array.isArray(payload.permissions)
? payload.permissions.map(String).sort().join(',')
: ''
const mePerms = [...me.permissions].map(String).sort().join(',')
if (claimPerms !== mePerms) return true
return false
}
/**
* Re-mint access token from API (current DB rights) and store it.
* Always use before SSO handoff so apps never get a stale user JWT.
*/
export async function reissueAccessToken(): Promise<LoginResponse> {
const token = getToken()
if (!token) {
throw new Error('Нет сессии')
}
const res = await fetch('/api/v1/auth/reissue', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
})
if (!res.ok) {
clearToken()
throw new Error('Не удалось обновить сессию')
}
const body = (await res.json()) as LoginResponse
setToken(body.access_token)
return body
}
/** SSO open: fresh JWT → app /auth/callback. */
export async function ssoOpenApp(appBaseUrl: string): Promise<void> {
const base = appBaseUrl.replace(/\/$/, '')
const issued = await reissueAccessToken()
const callback = `${base}/auth/callback`
window.location.href = buildSsoRedirectUrl(
callback,
issued.access_token,
issued.expires_at,
)
}
/** SSO return_to handoff with fresh JWT. */
export async function ssoHandoffReturnTo(returnTo: string): Promise<boolean> {
const allowlist = await ensureReturnToAllowlist()
if (!isReturnToAllowed(returnTo, allowlist)) return false
const issued = await reissueAccessToken()
window.location.href = buildSsoRedirectUrl(
returnTo,
issued.access_token,
issued.expires_at,
)
return true
}