fix: complete first version issue remediation
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Post, UnauthorizedException } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentSessionUserId } from './current-session-user.decorator';
|
||||
import { AuthService, LoginDto } from './auth.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller()
|
||||
export class AuthController {
|
||||
constructor(private readonly auth: AuthService) {}
|
||||
constructor(private readonly auth: AuthService, private readonly users: UsersService) {}
|
||||
|
||||
@Get('admin/auth/captcha')
|
||||
adminCaptcha() {
|
||||
@@ -26,4 +28,12 @@ export class AuthController {
|
||||
clientLogin(@Body() body: LoginDto) {
|
||||
return this.auth.login(body, 'client');
|
||||
}
|
||||
|
||||
@Post('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 ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('AuthService', () => {
|
||||
it('allows platform admins to login admin portal', async () => {
|
||||
const users = createUsersMock('platform_admin');
|
||||
const service = new AuthService(users as never);
|
||||
await expect(loginWithCaptcha(service, 'admin')).resolves.toEqual(expect.objectContaining({ portal: 'admin' }));
|
||||
await expect(loginWithCaptcha(service, 'admin')).resolves.toEqual(expect.objectContaining({ portal: 'admin', accessToken: 'dev-token:user-1:0' }));
|
||||
expect(users.recordLoginSuccess).toHaveBeenCalledWith('user-1');
|
||||
});
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export class AuthService {
|
||||
anonymousFailures.delete(login);
|
||||
|
||||
return {
|
||||
accessToken: `dev-token-${user.id}`,
|
||||
accessToken: `dev-token:${user.id}:${user.sessionVersion ?? 0}`,
|
||||
tokenType: 'Bearer',
|
||||
portal,
|
||||
user: {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import type { SessionRequest } from './session-validation.middleware';
|
||||
|
||||
export const CurrentSessionUserId = createParamDecorator((_: unknown, context: ExecutionContext) => {
|
||||
const request = context.switchToHttp().getRequest<SessionRequest>();
|
||||
return request.sessionUserId;
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
import { SessionValidationMiddleware, type SessionRequest } from './session-validation.middleware';
|
||||
|
||||
function request(authorization?: string): SessionRequest {
|
||||
return { header: jest.fn().mockReturnValue(authorization) };
|
||||
}
|
||||
|
||||
describe('SessionValidationMiddleware', () => {
|
||||
it('accepts the current user session version and exposes the session user id', async () => {
|
||||
const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', status: 'active', deletedAt: null, sessionVersion: 3 }) } };
|
||||
const middleware = new SessionValidationMiddleware(prisma as never);
|
||||
const currentRequest = request('Bearer dev-token:user-1:3');
|
||||
const next = jest.fn();
|
||||
|
||||
await middleware.use(currentRequest, {} as never, next);
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(currentRequest.sessionUserId).toBe('user-1');
|
||||
});
|
||||
|
||||
it('rejects an old session token after the user session version changes', async () => {
|
||||
const prisma = { user: { findUnique: jest.fn().mockResolvedValue({ id: 'user-1', status: 'active', deletedAt: null, sessionVersion: 4 }) } };
|
||||
const middleware = new SessionValidationMiddleware(prisma as never);
|
||||
|
||||
await expect(middleware.use(request('Bearer dev-token:user-1:3'), {} as never, jest.fn())).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
export type SessionRequest = {
|
||||
header(name: string): string | undefined;
|
||||
sessionUserId?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class SessionValidationMiddleware implements NestMiddleware {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async use(request: SessionRequest, _: unknown, next: () => void) {
|
||||
const authorization = request.header('authorization');
|
||||
if (!authorization) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const match = /^Bearer dev-token:([^:]+):(\d+)$/.exec(authorization.trim());
|
||||
if (!match) {
|
||||
throw new UnauthorizedException('登录会话无效,请重新登录');
|
||||
}
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: match[1] },
|
||||
select: { id: true, status: true, deletedAt: true, sessionVersion: true },
|
||||
});
|
||||
if (!user || user.status !== 'active' || user.deletedAt || user.sessionVersion !== Number(match[2])) {
|
||||
throw new UnauthorizedException('登录会话已失效,请重新登录');
|
||||
}
|
||||
request.sessionUserId = user.id;
|
||||
next();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user