P8 平台轻量化改造 + SSG修复 + 编程导师 + 文档完善

- Prisma: Tool 模型加 affiliateLink;免费用户沙盒 10→5 次/日
- 后端: Tools API + /admin/tools CRUD 5 端点;Practices 完整模块
- 导航: 主菜单隐藏企业版/社区(URL 可访问)
- 首页: 重定位为 AI 工具指南;新增精选工具区块;Feature 重写
- 工具页: affiliateLink 绿色推荐 Badge
- SSG 修复: config.ts 构建时直连 localhost:4000,页面 108→127
- 沙盒: 新增编程导师场景(苏格拉底教学法)
- 练习系统: Practices 多场景练习(含结构化评分)
- 技能广场: 6 个付费 Skill(标题大师/回款助手等)
- 管理后台: Models/Posts/Practices CRUD 页面
- 文档: README + progress.md 全面更新;AGENTS.md 同步定位
- 清理: .env.example 移除;tsbuildinfo gitignore
This commit is contained in:
yuzhiran-dev
2026-06-18 18:14:07 +08:00
parent fb8152401b
commit 0f215d2aad
103 changed files with 5240 additions and 778 deletions
+41 -7
View File
@@ -1,6 +1,7 @@
import { Controller, Post, Get, Body, UseGuards, Req } from '@nestjs/common';
import { Controller, Post, Get, Body, UseGuards, Req, Res } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { Throttle } from '@nestjs/throttler';
import { AuthService } from './auth.service';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
@@ -10,19 +11,39 @@ import { LoginDto } from './dto/login.dto';
export class AuthController {
constructor(private authService: AuthService) {}
private setTokenCookie(res: any, accessToken: string) {
const isProd = process.env.NODE_ENV === 'production';
res.cookie('token', accessToken, {
httpOnly: true,
secure: isProd,
sameSite: 'lax',
path: '/',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
}
@Post('register')
async register(@Body() body: RegisterDto) {
return this.authService.register(body);
@Throttle({ default: { limit: 3, ttl: 60000 } })
async register(@Body() body: RegisterDto, @Res({ passthrough: true }) res: any) {
const tokens = await this.authService.register(body);
this.setTokenCookie(res, tokens.accessToken);
return tokens;
}
@Post('login')
async login(@Body() body: LoginDto) {
return this.authService.login(body.account, body.password);
@Throttle({ default: { limit: 5, ttl: 60000 } })
async login(@Body() body: LoginDto, @Res({ passthrough: true }) res: any) {
const tokens = await this.authService.login(body.account, body.password);
this.setTokenCookie(res, tokens.accessToken);
return tokens;
}
@Post('refresh')
async refresh(@Body() body: { accessToken: string }) {
return this.authService.refreshAccessToken(body.accessToken);
@Throttle({ default: { limit: 10, ttl: 60000 } })
async refresh(@Body() body: { accessToken: string }, @Res({ passthrough: true }) res: any) {
const tokens = await this.authService.refreshAccessToken(body.accessToken);
this.setTokenCookie(res, tokens.accessToken);
return tokens;
}
@Get('profile')
@@ -31,4 +52,17 @@ export class AuthController {
async profile(@Req() req: any) {
return this.authService.getProfile(req.user.userId);
}
@Get('verify')
@UseGuards(AuthGuard('jwt'))
async verify(@Req() req: any) {
const profile = await this.authService.getProfile(req.user.userId);
return { ...profile, accessToken: req.headers.authorization?.replace('Bearer ', '') || '' };
}
@Post('logout')
async logout(@Res({ passthrough: true }) res: any) {
res.cookie('token', '', { httpOnly: true, maxAge: 0, path: '/' });
return { message: '已退出' };
}
}
+8 -2
View File
@@ -10,9 +10,15 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
private config: ConfigService,
private prisma: PrismaService,
) {
const secret = config.get<string>('JWT_SECRET') || 'yuzhiran-ai-default-secret';
const secret = config.get<string>('JWT_SECRET');
if (!secret) {
throw new Error('JWT_SECRET environment variable is required');
}
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
jwtFromRequest: ExtractJwt.fromExtractors([
ExtractJwt.fromAuthHeaderAsBearerToken(),
(req) => req?.cookies?.token || null,
]),
ignoreExpiration: false,
secretOrKey: secret,
});