Phases 2-4: Auth, providers, stream management

This commit is contained in:
2026-02-23 15:32:24 +01:00
parent 8ea3279c3b
commit 538c24c58f
14 changed files with 1530 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import { FastifyPluginAsync } from 'fastify';
import { requireAuth } from '../../middleware/require-auth.js';
import { decrypt } from '../../services/crypto.service.js';
import { revokeYouTubeToken } from '../../services/youtube.service.js';
import { revokeTwitchToken } from '../../services/twitch.service.js';
import { AppError } from '../../plugins/error-handler.js';
import type { LinkedAccountResponse } from '../../types/api.js';
const accountRoutes: FastifyPluginAsync = async (fastify) => {
// GET /providers/accounts — list linked accounts (no tokens)
fastify.get('/providers/accounts', {
preHandler: [requireAuth],
}, async (request) => {
const accounts = await fastify.prisma.linkedAccount.findMany({
where: { userId: request.userId },
});
const response: LinkedAccountResponse[] = accounts.map((a) => ({
id: a.id,
serviceId: a.serviceId,
displayName: a.displayName,
accountId: a.accountId,
avatarUrl: a.avatarUrl,
}));
return response;
});
// DELETE /providers/:serviceId — revoke tokens and unlink
fastify.delete<{ Params: { serviceId: string } }>('/providers/:serviceId', {
preHandler: [requireAuth],
schema: {
params: {
type: 'object',
required: ['serviceId'],
properties: {
serviceId: { type: 'string', enum: ['YOUTUBE', 'TWITCH'] },
},
},
},
}, async (request, reply) => {
const { serviceId } = request.params;
const account = await fastify.prisma.linkedAccount.findUnique({
where: {
userId_serviceId: {
userId: request.userId,
serviceId,
},
},
});
if (!account) {
throw new AppError(404, 'Account not linked');
}
// Best-effort revoke tokens at the provider
try {
const accessToken = decrypt(account.accessTokenEnc, account.accessTokenIv);
if (serviceId === 'YOUTUBE') {
await revokeYouTubeToken(accessToken);
} else {
await revokeTwitchToken(accessToken);
}
} catch {
// Revocation failure is non-fatal
}
await fastify.prisma.linkedAccount.delete({
where: { id: account.id },
});
reply.status(200).send({ success: true });
});
};
export default accountRoutes;