feat: opt web ux

This commit is contained in:
2026-04-02 22:10:24 +08:00
parent 143b1e8c4b
commit 35511eb877
16 changed files with 1251 additions and 383 deletions

View File

@@ -4,6 +4,8 @@ import { prisma } from '@agent-fox/shared';
import { hashPassword, verifyPassword } from '../lib/password.js';
import { generateTokenPair, verifyRefreshToken } from '../lib/jwt.js';
import { requireAuth } from '../middleware/auth.js';
import { generateApiKey } from '../lib/api-key.js';
import { encryptApiKey, decryptApiKey } from '../lib/crypto.js';
const router: RouterType = Router();
@@ -150,4 +152,86 @@ router.get('/me', requireAuth, async (req, res) => {
res.json({ success: true, data: user });
});
// --- API Key Management ---
router.get('/api-key/status', requireAuth, async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: req.user!.userId },
select: { apiKeyPrefix: true, apiKeyHash: true },
});
res.json({
success: true,
data: { hasKey: !!user?.apiKeyHash, prefix: user?.apiKeyPrefix || null },
});
});
router.post('/api-key/generate', requireAuth, async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: req.user!.userId },
select: { apiKeyHash: true },
});
if (user?.apiKeyHash) {
res.status(400).json({ success: false, error: { code: 'ALREADY_EXISTS', message: 'API key already exists. Use rotate to replace it.' } });
return;
}
const { raw, hash } = generateApiKey();
const encrypted = encryptApiKey(raw);
const prefix = raw.slice(0, 12);
await prisma.user.update({
where: { id: req.user!.userId },
data: { apiKeyHash: hash, apiKeyEncrypted: encrypted, apiKeyPrefix: prefix },
});
res.json({ success: true, data: { apiKey: raw } });
});
router.post('/api-key/rotate', requireAuth, async (req, res) => {
const { raw, hash } = generateApiKey();
const encrypted = encryptApiKey(raw);
const prefix = raw.slice(0, 12);
await prisma.user.update({
where: { id: req.user!.userId },
data: { apiKeyHash: hash, apiKeyEncrypted: encrypted, apiKeyPrefix: prefix },
});
res.json({ success: true, data: { apiKey: raw } });
});
const revealSchema = z.object({ password: z.string() });
router.post('/api-key/reveal', requireAuth, async (req, res) => {
const parsed = revealSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Password is required' } });
return;
}
const user = await prisma.user.findUnique({
where: { id: req.user!.userId },
select: { passwordHash: true, apiKeyEncrypted: true },
});
if (!user?.passwordHash) {
res.status(400).json({ success: false, error: { code: 'NO_PASSWORD', message: 'No password set' } });
return;
}
const valid = await verifyPassword(parsed.data.password, user.passwordHash);
if (!valid) {
res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Incorrect password' } });
return;
}
if (!user.apiKeyEncrypted) {
res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'No API key generated' } });
return;
}
const apiKey = decryptApiKey(user.apiKeyEncrypted);
res.json({ success: true, data: { apiKey } });
});
export default router;