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.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import {
|
||||
allPermissionKeys,
|
||||
normalizePermissionKeys,
|
||||
tenantsClaimForUser,
|
||||
type LoginResponse,
|
||||
} from '@authportal/shared'
|
||||
import {
|
||||
getAppSwitcherConfig,
|
||||
getUserApps,
|
||||
getUserPermissions,
|
||||
type UserRow,
|
||||
} from '@authportal/db'
|
||||
import { toMe } from '../plugins/auth-guards.js'
|
||||
|
||||
/** Mint access JWT + Me from current DB state (login / reissue). */
|
||||
export function issueAccessToken(
|
||||
app: FastifyInstance,
|
||||
user: UserRow,
|
||||
): LoginResponse {
|
||||
const apps = getUserApps(app.db, user.id)
|
||||
let permissions = normalizePermissionKeys(getUserPermissions(app.db, user.id))
|
||||
if (user.isAdmin) {
|
||||
permissions = allPermissionKeys()
|
||||
}
|
||||
const switcher = getAppSwitcherConfig(app.db)
|
||||
const tenants = tenantsClaimForUser(switcher, apps)
|
||||
const me = toMe(user, apps, permissions)
|
||||
const expiresAt = new Date(
|
||||
Date.now() + app.config.jwtTtlHours * 60 * 60 * 1000,
|
||||
)
|
||||
const accessToken = app.jwt.sign(
|
||||
{
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
apps,
|
||||
permissions,
|
||||
tenants,
|
||||
bgp_tenant_id: tenants.bgp,
|
||||
is_admin: user.isAdmin,
|
||||
iss: app.config.issuer,
|
||||
},
|
||||
{ expiresIn: `${app.config.jwtTtlHours}h` },
|
||||
)
|
||||
return {
|
||||
access_token: accessToken,
|
||||
expires_at: expiresAt.toISOString(),
|
||||
token_type: 'Bearer',
|
||||
user: me,
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type UserRow,
|
||||
} from '@authportal/db'
|
||||
import type { AppId, MeResponse } from '@authportal/shared'
|
||||
import { APP_IDS, normalizePermissionKeys } from '@authportal/shared'
|
||||
import { APP_IDS, allPermissionKeys, normalizePermissionKeys } from '@authportal/shared'
|
||||
|
||||
export type AuthUser = {
|
||||
id: string
|
||||
@@ -72,9 +72,12 @@ export function loadAuthUser(
|
||||
user: UserRow,
|
||||
): AuthUser {
|
||||
const apps = getUserApps(request.server.db, user.id)
|
||||
const permissions = normalizePermissionKeys(
|
||||
let permissions = normalizePermissionKeys(
|
||||
getUserPermissions(request.server.db, user.id),
|
||||
)
|
||||
if (user.isAdmin) {
|
||||
permissions = allPermissionKeys()
|
||||
}
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
|
||||
+27
-43
@@ -1,25 +1,22 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { hash, verify } from '@node-rs/argon2'
|
||||
import { verify } from '@node-rs/argon2'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import {
|
||||
PERMISSION_CATALOG,
|
||||
allPermissionKeys,
|
||||
appsMetaFromSwitcher,
|
||||
loginRequestSchema,
|
||||
normalizePermissionKeys,
|
||||
publicAppSwitcherConfig,
|
||||
tenantsClaimForUser,
|
||||
type LoginResponse,
|
||||
} from '@authportal/shared'
|
||||
import {
|
||||
createRefreshSession,
|
||||
getAppSwitcherConfig,
|
||||
getUserApps,
|
||||
getUserByEmail,
|
||||
getUserPermissions,
|
||||
getUserById,
|
||||
revokeRefreshSession,
|
||||
} from '@authportal/db'
|
||||
import { requireAuth, toMe } from '../plugins/auth-guards.js'
|
||||
import { requireAuth } from '../plugins/auth-guards.js'
|
||||
import { issueAccessToken } from '../lib/issue-access-token.js'
|
||||
|
||||
const REFRESH_COOKIE = 'refresh_token'
|
||||
|
||||
@@ -55,36 +52,7 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
const apps = getUserApps(app.db, user.id)
|
||||
let permissions = normalizePermissionKeys(
|
||||
getUserPermissions(app.db, user.id),
|
||||
)
|
||||
// Portal admin gets full catalog in JWT so apps can rely on permissions
|
||||
// even when UI also checks is_admin.
|
||||
if (user.isAdmin) {
|
||||
permissions = allPermissionKeys()
|
||||
}
|
||||
const switcher = getAppSwitcherConfig(app.db)
|
||||
const tenants = tenantsClaimForUser(switcher, apps)
|
||||
const me = toMe(user, apps, permissions)
|
||||
|
||||
const expiresAt = new Date(
|
||||
Date.now() + app.config.jwtTtlHours * 60 * 60 * 1000,
|
||||
)
|
||||
const accessToken = app.jwt.sign(
|
||||
{
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
apps,
|
||||
permissions,
|
||||
tenants,
|
||||
bgp_tenant_id: tenants.bgp,
|
||||
is_admin: user.isAdmin,
|
||||
iss: app.config.issuer,
|
||||
},
|
||||
{ expiresIn: `${app.config.jwtTtlHours}h` },
|
||||
)
|
||||
const body = issueAccessToken(app, user)
|
||||
|
||||
const refreshRaw = randomBytes(32).toString('hex')
|
||||
const refreshExpires = new Date(
|
||||
@@ -97,16 +65,32 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
|
||||
`${REFRESH_COOKIE}=${refreshRaw}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${app.config.refreshTtlDays * 86400}${app.config.isProd ? '; Secure' : ''}`,
|
||||
)
|
||||
|
||||
const body: LoginResponse = {
|
||||
access_token: accessToken,
|
||||
expires_at: expiresAt.toISOString(),
|
||||
token_type: 'Bearer',
|
||||
user: me,
|
||||
}
|
||||
return body
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Re-mint access JWT from DB (apps/permissions/is_admin/tenants).
|
||||
* Fixes stale localStorage JWT after role changes or account switch.
|
||||
*/
|
||||
app.post(
|
||||
'/api/v1/auth/reissue',
|
||||
{
|
||||
onRequest: requireAuth,
|
||||
config: { rateLimit: { max: 60, timeWindow: '1 minute' } },
|
||||
},
|
||||
async (request, reply) => {
|
||||
const auth = request.authUser!
|
||||
const user = getUserById(app.db, auth.id)
|
||||
if (!user || user.disabled) {
|
||||
return reply.status(401).send({
|
||||
error: { code: 'UNAUTHORIZED', message: 'Пользователь недоступен' },
|
||||
})
|
||||
}
|
||||
return issueAccessToken(app, user)
|
||||
},
|
||||
)
|
||||
|
||||
app.post('/api/v1/auth/logout', async (request, reply) => {
|
||||
const cookie = request.headers.cookie ?? ''
|
||||
const match = cookie.match(new RegExp(`${REFRESH_COOKIE}=([^;]+)`))
|
||||
|
||||
Reference in New Issue
Block a user