feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { Controller, Post, Get, Body, UseGuards, Req } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthService } from './auth.service';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
|
||||
@ApiTags('认证')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Post('register')
|
||||
async register(@Body() body: RegisterDto) {
|
||||
return this.authService.register(body);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() body: { account: string; password: string }) {
|
||||
return this.authService.login(body.account, body.password);
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
async refresh(@Body() body: { accessToken: string }) {
|
||||
return this.authService.refreshAccessToken(body.accessToken);
|
||||
}
|
||||
|
||||
@Get('profile')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@ApiBearerAuth()
|
||||
async profile(@Req() req: any) {
|
||||
return this.authService.getProfile(req.user.userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.registerAsync({
|
||||
useFactory: (config: ConfigService) => ({
|
||||
secret: config.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: config.get('JWT_EXPIRES_IN') || '2h' },
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
exports: [AuthService, JwtModule],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async register(data: { phone?: string; email?: string; password: string; nickname?: string }) {
|
||||
const passwordHash = await bcrypt.hash(data.password, 10);
|
||||
const user = await this.prisma.user.create({
|
||||
data: {
|
||||
phone: data.phone,
|
||||
email: data.email,
|
||||
passwordHash,
|
||||
nickname: data.nickname || data.phone || data.email?.split('@')[0],
|
||||
},
|
||||
});
|
||||
return this.generateTokens(user.id);
|
||||
}
|
||||
|
||||
async login(account: string, password: string) {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ phone: account }, { email: account }],
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !user.passwordHash) {
|
||||
throw new UnauthorizedException('账号或密码错误');
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.passwordHash);
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedException('账号或密码错误');
|
||||
}
|
||||
|
||||
await this.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { lastLoginAt: new Date() },
|
||||
});
|
||||
|
||||
return this.generateTokens(user.id);
|
||||
}
|
||||
|
||||
async refreshAccessToken(accessToken: string) {
|
||||
try {
|
||||
const payload = this.jwtService.verify(accessToken, { ignoreExpiration: true });
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: payload.sub, deletedAt: null },
|
||||
});
|
||||
if (!user || user.status !== 'ACTIVE') {
|
||||
throw new UnauthorizedException('用户不可用');
|
||||
}
|
||||
return this.generateTokens(user.id);
|
||||
} catch {
|
||||
throw new UnauthorizedException('Token 无效');
|
||||
}
|
||||
}
|
||||
|
||||
async getProfile(userId: number) {
|
||||
return this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
phone: true,
|
||||
email: true,
|
||||
nickname: true,
|
||||
avatar: true,
|
||||
status: true,
|
||||
memberPlan: true,
|
||||
memberExpire: true,
|
||||
sandboxDaily: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private generateTokens(userId: number) {
|
||||
const payload = { sub: userId };
|
||||
return {
|
||||
accessToken: this.jwtService.sign(payload, { expiresIn: '2h' }),
|
||||
refreshToken: this.jwtService.sign(payload, { expiresIn: '7d' }),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class RegisterDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
@IsNotEmpty({ message: '密码不能为空' })
|
||||
@IsString()
|
||||
@MinLength(6, { message: '密码至少6位' })
|
||||
password: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nickname?: string;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
private config: ConfigService,
|
||||
private prisma: PrismaService,
|
||||
) {
|
||||
const secret = config.get<string>('JWT_SECRET') || 'yuzhiran-ai-default-secret';
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: secret,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: { sub: number; type?: string }) {
|
||||
if (payload.type === 'admin') {
|
||||
const admin = await this.prisma.adminUser.findUnique({ where: { id: payload.sub } });
|
||||
if (!admin || admin.status !== 'ACTIVE') {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return { userId: payload.sub, isAdmin: true };
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.findUnique({ where: { id: payload.sub } });
|
||||
if (!user || user.deletedAt) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return { userId: payload.sub, isAdmin: false };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// @ts-nocheck
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
|
||||
jest.mock('bcryptjs', () => ({
|
||||
compare: jest.fn(),
|
||||
hash: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockPrisma = {
|
||||
user: {
|
||||
create: jest.fn().mockResolvedValue({ id: 1 }),
|
||||
findFirst: jest.fn().mockResolvedValue(null),
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
update: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
};
|
||||
|
||||
const mockJwtService = {
|
||||
sign: jest.fn().mockReturnValue('mock-token'),
|
||||
verify: jest.fn().mockReturnValue({ sub: 1 }),
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
AuthService,
|
||||
{ provide: PrismaService, useValue: mockPrisma },
|
||||
{ provide: JwtService, useValue: mockJwtService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user