feat: harden platform workflows and UI governance

This commit is contained in:
hectorzhao
2026-07-22 14:14:55 +08:00
parent ef957f7daa
commit 0f223f7f91
80 changed files with 4958 additions and 764 deletions
+46 -17
View File
@@ -2,7 +2,7 @@ import { Body, Controller, Get, Post, Req, Res, UnauthorizedException } from '@n
import { ApiTags } from '@nestjs/swagger';
import { CurrentSessionUserId } from './current-session-user.decorator';
import { AuthService, LoginDto } from './auth.service';
import { SessionService } from './session.service';
import { DEVELOPMENT_SESSION_COOKIE_NAME, SESSION_COOKIE_NAME, SessionPortal, SessionService } from './session.service';
import type { SessionRequest } from './session-validation.middleware';
import { UsersService } from '../users/users.service';
import { PrismaService } from '../prisma/prisma.service';
@@ -38,13 +38,31 @@ export class AuthController {
return this.finishLogin(await this.auth.login(body, 'client'), request, response);
}
@Get('auth/session')
currentSession(@Req() request: SessionRequest) {
@Get(['admin/auth/session', 'client/auth/session'])
async currentSession(@Req() request: SessionRequest) {
this.assertSession(request);
return this.sessions.publicSession(request.authSession!);
const user = await this.prisma.user.findUniqueOrThrow({
where: { id: request.sessionUserId! },
include: { tenant: true, roles: { include: { role: true } } },
});
return {
portal: request.authSession!.portal,
locked: Boolean(request.authSession!.lockedAt),
user: {
id: user.id,
tenantId: user.tenantId,
tenantName: user.tenant?.name,
username: user.username,
email: user.email,
phone: user.phone,
displayName: user.displayName,
roles: user.roles.map((item) => item.role.code),
},
...this.sessions.publicSession(request.authSession!),
};
}
@Post('auth/session/touch')
@Post(['admin/auth/session/touch', 'client/auth/session/touch'])
async touch(@Req() request: SessionRequest) {
this.assertSession(request);
const result = await this.sessions.touch(request.sessionToken!);
@@ -52,7 +70,7 @@ export class AuthController {
return this.sessions.publicSession(result.record);
}
@Post('auth/session/lock')
@Post(['admin/auth/session/lock', 'client/auth/session/lock'])
async lock(@Req() request: SessionRequest) {
this.assertSession(request);
const record = await this.sessions.lock(request.sessionToken!);
@@ -60,17 +78,17 @@ export class AuthController {
return { locked: Boolean(record) };
}
@Post('auth/session/unlock')
@Post(['admin/auth/session/unlock', 'client/auth/session/unlock'])
async unlock(@Req() request: SessionRequest, @Body('password') password: string, @Res({ passthrough: true }) response: CookieResponse) {
this.assertSession(request);
const result = await this.auth.unlock(request.sessionToken!, request.sessionUserId!, password);
if (result.status !== 'active' || !('token' in result)) throw new UnauthorizedException({ code: 'SESSION_LOCK_TIMEOUT', message: '锁定时间过长,请重新登录' });
this.setCookie(response, result.token);
this.setCookie(response, result.record.portal, result.token);
await this.writeLog(request, 'auth.session_unlocked', { portal: result.record.portal });
return this.sessions.publicSession(result.record);
}
@Post('auth/reauthenticate')
@Post(['admin/auth/reauthenticate', 'client/auth/reauthenticate'])
async reauthenticate(@Req() request: SessionRequest, @Body('password') password: string) {
this.assertSession(request);
const result = await this.auth.reauthenticate(request.sessionToken!, request.sessionUserId!, password);
@@ -79,22 +97,23 @@ export class AuthController {
return this.sessions.publicSession(result.record);
}
@Post('auth/logout')
@Post(['admin/auth/logout', 'client/auth/logout'])
async logout(@Req() request: SessionRequest, @Res({ passthrough: true }) response: CookieResponse) {
if (request.sessionToken) await this.sessions.remove(request.sessionToken);
if (request.sessionUserId) await this.writeLog(request, 'auth.session_logged_out', { portal: request.authSession?.portal });
this.clearCookie(response);
this.clearCookie(response, request.authSession?.portal);
return { success: true };
}
@Post('auth/password')
@Post(['admin/auth/password', 'client/auth/password'])
changeOwnPassword(@CurrentSessionUserId() userId: string | undefined, @Body() body: { currentPassword?: string; password?: string }) {
if (!userId) throw new UnauthorizedException('登录会话无效,请重新登录');
return this.users.changeOwnPassword(userId, body.currentPassword ?? '', body.password ?? '');
}
private async finishLogin(result: Awaited<ReturnType<AuthService['login']>>, request: SessionRequest, response: CookieResponse) {
this.setCookie(response, result.sessionToken);
this.setCookie(response, result.portal, result.sessionToken);
this.clearLegacyCookie(response);
await this.prisma.operationLog.create({
data: { userId: result.user.id, tenantId: result.user.tenantId, action: 'auth.session_created', resource: 'auth_session', userAgent: request.header('user-agent'), detail: { portal: result.portal } },
});
@@ -114,8 +133,8 @@ export class AuthController {
}
}
private setCookie(response: CookieResponse, token: string) {
response.cookie(this.sessions.cookieName, token, {
private setCookie(response: CookieResponse, portal: SessionPortal, token: string) {
response.cookie(this.sessions.cookieName(portal), token, {
httpOnly: true,
secure: this.sessions.cookieSecure,
sameSite: 'lax',
@@ -123,8 +142,18 @@ export class AuthController {
});
}
private clearCookie(response: CookieResponse) {
response.clearCookie(this.sessions.cookieName, {
private clearCookie(response: CookieResponse, portal?: SessionPortal) {
if (!portal) return;
response.clearCookie(this.sessions.cookieName(portal), {
httpOnly: true,
secure: this.sessions.cookieSecure,
sameSite: 'lax',
path: '/',
});
}
private clearLegacyCookie(response: CookieResponse) {
response.clearCookie(this.sessions.cookieSecure ? SESSION_COOKIE_NAME : DEVELOPMENT_SESSION_COOKIE_NAME, {
httpOnly: true,
secure: this.sessions.cookieSecure,
sameSite: 'lax',