Phases 2-4: Auth, providers, stream management
This commit is contained in:
77
src/routes/providers/accounts.ts
Normal file
77
src/routes/providers/accounts.ts
Normal 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;
|
||||
Reference in New Issue
Block a user