quality / commitlint (push) Skipped
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m11s
quality / api (push) Successful in 47s
CD / quality (push) Successful in 2m10s
CD / publish (push) Successful in 1m50s
Альтернатива паролю на портале; SSO приложений без изменений. Co-authored-by: Cursor <[email protected]>
65 lines
1.8 KiB
TypeScript
65 lines
1.8 KiB
TypeScript
import type { FastifyInstance, FastifyReply } from 'fastify'
|
|
import { randomBytes } from 'node:crypto'
|
|
import {
|
|
createRefreshSession,
|
|
touchLastLogin,
|
|
type UserRow,
|
|
} from '@authportal/db'
|
|
import { issueAccessToken } from './issue-access-token.js'
|
|
import { safeAudit } from './audit.js'
|
|
import { targetAppFromReturnTo } from './target-app.js'
|
|
|
|
export const REFRESH_COOKIE = 'refresh_token'
|
|
|
|
export function completeLogin(
|
|
app: FastifyInstance,
|
|
reply: FastifyReply,
|
|
user: UserRow,
|
|
opts: {
|
|
method: 'password' | 'passkey'
|
|
returnTo?: string
|
|
ip: string | null
|
|
userAgent: string | null
|
|
},
|
|
) {
|
|
const body = issueAccessToken(app, user)
|
|
const refreshRaw = randomBytes(32).toString('hex')
|
|
const refreshExpires = new Date(
|
|
Date.now() + app.config.refreshTtlDays * 24 * 60 * 60 * 1000,
|
|
)
|
|
createRefreshSession(app.db, user.id, refreshRaw, refreshExpires, {
|
|
ip: opts.ip,
|
|
userAgent: opts.userAgent,
|
|
})
|
|
touchLastLogin(app.db, user.id, opts.ip)
|
|
|
|
reply.header(
|
|
'Set-Cookie',
|
|
`${REFRESH_COOKIE}=${refreshRaw}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${app.config.refreshTtlDays * 86400}${app.config.isProd ? '; Secure' : ''}`,
|
|
)
|
|
|
|
const targetApp = targetAppFromReturnTo(opts.returnTo)
|
|
const isPasskey = opts.method === 'passkey'
|
|
safeAudit(app, {
|
|
action: isPasskey ? 'auth.passkey_login' : 'auth.login',
|
|
severity: 'info',
|
|
actorUserId: user.id,
|
|
actorEmail: user.email,
|
|
actorName: user.name,
|
|
targetType: 'session',
|
|
targetId: user.id,
|
|
summary: isPasskey
|
|
? `Вход (passkey): ${user.email}`
|
|
: `Вход: ${user.email}`,
|
|
details: {
|
|
method: opts.method,
|
|
user_agent: opts.userAgent,
|
|
return_to: opts.returnTo ?? null,
|
|
target_app: targetApp,
|
|
},
|
|
ip: opts.ip,
|
|
})
|
|
|
|
return body
|
|
}
|