refactor: strengthen client boundaries and quality gates
This commit is contained in:
@@ -20,19 +20,29 @@ type CookieResponse = {
|
||||
@ApiTags('auth')
|
||||
@Controller()
|
||||
export class AuthController {
|
||||
constructor(private readonly auth: AuthService, private readonly users: UsersService, private readonly sessions: SessionService, private readonly prisma: PrismaService, private readonly security: SecurityDetectionService) {}
|
||||
constructor(
|
||||
private readonly auth: AuthService,
|
||||
private readonly users: UsersService,
|
||||
private readonly sessions: SessionService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly security: SecurityDetectionService,
|
||||
) {}
|
||||
|
||||
@Get('admin/auth/captcha')
|
||||
adminCaptcha() {
|
||||
return this.auth.createCaptcha();
|
||||
adminCaptcha(@Req() request: SessionRequest) {
|
||||
return this.auth.createCaptcha(this.sourceIp(request));
|
||||
}
|
||||
|
||||
@Post('admin/auth/login')
|
||||
@UsePipes(strictValidationPipe)
|
||||
async adminLogin(@Body() body: LoginDto, @Req() request: SessionRequest, @Res({ passthrough: true }) response: CookieResponse) {
|
||||
async adminLogin(
|
||||
@Body() body: LoginDto,
|
||||
@Req() request: SessionRequest,
|
||||
@Res({ passthrough: true }) response: CookieResponse,
|
||||
) {
|
||||
let result: Awaited<ReturnType<AuthService['login']>>;
|
||||
try {
|
||||
result = await this.auth.login(body, 'admin');
|
||||
result = await this.auth.login(body, 'admin', this.sourceIp(request));
|
||||
} catch (error) {
|
||||
await this.recordLoginFailure('admin_login_failure', body.login, request).catch(() => undefined);
|
||||
throw error;
|
||||
@@ -41,16 +51,20 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Get('client/auth/captcha')
|
||||
clientCaptcha() {
|
||||
return this.auth.createCaptcha();
|
||||
clientCaptcha(@Req() request: SessionRequest) {
|
||||
return this.auth.createCaptcha(this.sourceIp(request));
|
||||
}
|
||||
|
||||
@Post('client/auth/login')
|
||||
@UsePipes(strictValidationPipe)
|
||||
async clientLogin(@Body() body: LoginDto, @Req() request: SessionRequest, @Res({ passthrough: true }) response: CookieResponse) {
|
||||
async clientLogin(
|
||||
@Body() body: LoginDto,
|
||||
@Req() request: SessionRequest,
|
||||
@Res({ passthrough: true }) response: CookieResponse,
|
||||
) {
|
||||
let result: Awaited<ReturnType<AuthService['login']>>;
|
||||
try {
|
||||
result = await this.auth.login(body, 'client');
|
||||
result = await this.auth.login(body, 'client', this.sourceIp(request));
|
||||
} catch (error) {
|
||||
await this.recordLoginFailure('client_login_failure', body.login, request).catch(() => undefined);
|
||||
throw error;
|
||||
@@ -94,17 +108,23 @@ export class AuthController {
|
||||
async lock(@Req() request: SessionRequest) {
|
||||
this.assertSession(request);
|
||||
const record = await this.sessions.lock(request.sessionToken!);
|
||||
if (record) await this.writeLog(request, 'auth.session_locked', { portal: record.portal, reason: 'client_idle_timer' });
|
||||
if (record)
|
||||
await this.writeLog(request, 'auth.session_locked', { portal: record.portal, reason: 'client_idle_timer' });
|
||||
return { locked: Boolean(record) };
|
||||
}
|
||||
|
||||
@Post(['admin/auth/session/unlock', 'client/auth/session/unlock'])
|
||||
@UsePipes(strictValidationPipe)
|
||||
async unlock(@Req() request: SessionRequest, @Body() body: PasswordVerificationDto, @Res({ passthrough: true }) response: CookieResponse) {
|
||||
async unlock(
|
||||
@Req() request: SessionRequest,
|
||||
@Body() body: PasswordVerificationDto,
|
||||
@Res({ passthrough: true }) response: CookieResponse,
|
||||
) {
|
||||
const { password } = body;
|
||||
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: '锁定时间过长,请重新登录' });
|
||||
if (result.status !== 'active' || !('token' in result))
|
||||
throw new UnauthorizedException({ code: 'SESSION_LOCK_TIMEOUT', message: '锁定时间过长,请重新登录' });
|
||||
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);
|
||||
@@ -124,7 +144,8 @@ export class AuthController {
|
||||
@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 });
|
||||
if (request.sessionUserId)
|
||||
await this.writeLog(request, 'auth.session_logged_out', { portal: request.authSession?.portal });
|
||||
this.clearCookie(response, request.authSession?.portal);
|
||||
return { success: true };
|
||||
}
|
||||
@@ -136,17 +157,32 @@ export class AuthController {
|
||||
return this.users.changeOwnPassword(userId, body.currentPassword, body.password);
|
||||
}
|
||||
|
||||
private async finishLogin(result: Awaited<ReturnType<AuthService['login']>>, request: SessionRequest, response: CookieResponse) {
|
||||
private async finishLogin(
|
||||
result: Awaited<ReturnType<AuthService['login']>>,
|
||||
request: SessionRequest,
|
||||
response: CookieResponse,
|
||||
) {
|
||||
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 } },
|
||||
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 },
|
||||
},
|
||||
});
|
||||
const { sessionToken: _, ...publicResult } = result;
|
||||
return publicResult;
|
||||
}
|
||||
|
||||
private recordLoginFailure(ruleCode: 'admin_login_failure' | 'client_login_failure', account: string, request: SessionRequest) {
|
||||
private recordLoginFailure(
|
||||
ruleCode: 'admin_login_failure' | 'client_login_failure',
|
||||
account: string,
|
||||
request: SessionRequest,
|
||||
) {
|
||||
return this.security.recordEvent({
|
||||
ruleCode,
|
||||
sourceIp: requestContext.getStore()?.ipAddress ?? '127.0.0.1',
|
||||
@@ -157,9 +193,19 @@ export class AuthController {
|
||||
});
|
||||
}
|
||||
|
||||
private sourceIp(request: SessionRequest) {
|
||||
return requestContext.getStore()?.ipAddress ?? '127.0.0.1';
|
||||
}
|
||||
|
||||
private writeLog(request: SessionRequest, action: string, detail: Record<string, unknown>) {
|
||||
return this.prisma.operationLog.create({
|
||||
data: { userId: request.sessionUserId, action, resource: 'auth_session', userAgent: request.header('user-agent'), detail: JSON.parse(JSON.stringify(detail)) as Prisma.InputJsonValue },
|
||||
data: {
|
||||
userId: request.sessionUserId,
|
||||
action,
|
||||
resource: 'auth_session',
|
||||
userAgent: request.header('user-agent'),
|
||||
detail: JSON.parse(JSON.stringify(detail)) as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user