Multi-account support and streaming fixes

- Change LinkedAccount unique constraint to (userId, serviceId, accountId)
- Add linkedAccountId to StreamDestination for per-account targeting
- OAuth callbacks upsert by accountId so different accounts create new rows
- Delete endpoint changed to /providers/accounts/:id
- getDecryptedToken resolves tokens by linkedAccountId instead of serviceId
- /start transition wrapped in try-catch (enableAutoStart compatibility)
- /end always attempts YouTube complete transition regardless of plan status
- autoDetectEndedPlans loads tokens per-destination
This commit is contained in:
2026-02-26 19:06:05 +01:00
parent 7351003c6b
commit cff7cdc58a
7 changed files with 208 additions and 71 deletions

View File

@@ -26,27 +26,23 @@ const accountRoutes: FastifyPluginAsync = async (fastify) => {
return response;
});
// DELETE /providers/:serviceId — revoke tokens and unlink
fastify.delete<{ Params: { serviceId: string } }>('/providers/:serviceId', {
// DELETE /providers/accounts/:id — revoke tokens and unlink by account ID
fastify.delete<{ Params: { id: string } }>('/providers/accounts/:id', {
preHandler: [requireAuth],
schema: {
params: {
type: 'object',
required: ['serviceId'],
required: ['id'],
properties: {
serviceId: { type: 'string', enum: ['YOUTUBE', 'TWITCH'] },
id: { type: 'string' },
},
},
},
}, async (request, reply) => {
const { serviceId } = request.params;
const account = await fastify.prisma.linkedAccount.findUnique({
const account = await fastify.prisma.linkedAccount.findFirst({
where: {
userId_serviceId: {
userId: request.userId,
serviceId,
},
id: request.params.id,
userId: request.userId,
},
});
@@ -57,7 +53,7 @@ const accountRoutes: FastifyPluginAsync = async (fastify) => {
// Best-effort revoke tokens at the provider
try {
const accessToken = decrypt(account.accessTokenEnc, account.accessTokenIv);
if (serviceId === 'YOUTUBE') {
if (account.serviceId === 'YOUTUBE') {
await revokeYouTubeToken(accessToken);
} else {
await revokeTwitchToken(accessToken);