feat: P0f 会话分享链接 + 全栈验证
- 后端: POST /sessions/:id/share 生成 HMAC 签名分享令牌 - 后端: GET /sandbox/shared/:token 公开查看分享会话 - 前端: sandbox/page.tsx 新增「复制分享链接」按钮 - 前端: /sandbox/share?token=xxx 分享查看页面 - 全栈: 后端 build + 前端 61 页 + 92 tests 全部通过
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { SandboxService } from './sandbox.service';
|
||||
|
||||
@Controller('sandbox')
|
||||
export class SandboxShareController {
|
||||
constructor(private sandboxService: SandboxService) {}
|
||||
|
||||
@Get('shared/:token')
|
||||
async viewShared(@Param('token') token: string) {
|
||||
return this.sandboxService.getSessionByShareToken(token);
|
||||
}
|
||||
}
|
||||
@@ -41,4 +41,9 @@ export class SandboxController {
|
||||
async quota(@Req() req: any) {
|
||||
return this.sandboxService.getQuota(req.user.userId);
|
||||
}
|
||||
|
||||
@Post('sessions/:id/share')
|
||||
async share(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
||||
return this.sandboxService.generateShareToken(req.user.userId, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SandboxController } from './sandbox.controller';
|
||||
import { SandboxShareController } from './sandbox-share.controller';
|
||||
import { SandboxService } from './sandbox.service';
|
||||
import { AIModule } from '../ai/ai.module';
|
||||
|
||||
@Module({
|
||||
imports: [AIModule],
|
||||
controllers: [SandboxController],
|
||||
controllers: [SandboxController, SandboxShareController],
|
||||
providers: [SandboxService],
|
||||
exports: [SandboxService],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { AIGatewayService, ChatOptions } from '../ai/ai-gateway.service';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { randomUUID, createHmac } from 'crypto';
|
||||
|
||||
@Injectable()
|
||||
export class SandboxService {
|
||||
@@ -151,4 +151,44 @@ export class SandboxService {
|
||||
});
|
||||
return { dailyLimit: user?.sandboxDaily || 10, used, remaining: (user?.sandboxDaily || 10) - used };
|
||||
}
|
||||
|
||||
async generateShareToken(userId: number, sessionId: number) {
|
||||
const session = await this.prisma.sandboxSession.findFirst({
|
||||
where: { id: sessionId, userId },
|
||||
});
|
||||
if (!session) throw new HttpException('会话不存在', HttpStatus.NOT_FOUND);
|
||||
|
||||
const secret = process.env.JWT_SECRET || 'yuzhiran-share-secret';
|
||||
const ts = Date.now().toString(36);
|
||||
const hmac = createHmac('sha256', secret).update(`${sessionId}:${ts}`).digest('hex').slice(0, 12);
|
||||
const token = Buffer.from(`${sessionId}|${ts}|${hmac}`).toString('base64url');
|
||||
return { shareUrl: `${process.env.FRONTEND_URL || 'http://localhost:3000'}/sandbox/share?token=${token}` };
|
||||
}
|
||||
|
||||
async getSessionByShareToken(token: string) {
|
||||
try {
|
||||
const decoded = Buffer.from(token, 'base64url').toString();
|
||||
const parts = decoded.split('|');
|
||||
if (parts.length !== 3) throw new Error('invalid token');
|
||||
const [sessionId, ts, hmac] = parts;
|
||||
const secret = process.env.JWT_SECRET || 'yuzhiran-share-secret';
|
||||
const expected = createHmac('sha256', secret).update(`${sessionId}:${ts}`).digest('hex').slice(0, 12);
|
||||
if (hmac !== expected) throw new Error('invalid signature');
|
||||
|
||||
const session = await this.prisma.sandboxSession.findUnique({
|
||||
where: { id: parseInt(sessionId) },
|
||||
});
|
||||
if (!session) throw new Error('session not found');
|
||||
|
||||
return {
|
||||
id: session.id,
|
||||
model: session.model,
|
||||
title: session.title,
|
||||
createdAt: session.createdAt,
|
||||
messages: JSON.parse(session.messages).filter((m: any) => m.role !== 'system'),
|
||||
};
|
||||
} catch {
|
||||
throw new HttpException('分享链接无效或已过期', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user