- 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.
133 lines
3.8 KiB
TypeScript
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
|
|
}
|