Files
lck-control-backend/src/routes/providers/accounts.ts
omigamedev 08cca68086 Custom RTMP saved accounts, CUSTOM destination prepare, debug logging
- Add POST /providers/accounts/custom-rtmp endpoint for saved RTMP servers
- Encrypt rtmpUrl/streamKey in accessTokenEnc/refreshTokenEnc fields
- Decrypt and return rtmpUrl/streamKey in GET /providers/accounts for CUSTOM_RTMP
- Skip token revocation on DELETE for CUSTOM_RTMP accounts
- Decrypt CUSTOM_RTMP credentials into CUSTOM destinations on plan create/update
- Handle CUSTOM destinations in prepare lifecycle (already READY, skip provider auth)
- Add debug logging for plan operations and user upsert
2026-03-01 10:50:28 +01:00

133 lines
4.1 KiB
TypeScript

import { FastifyPluginAsync } from 'fastify';
import { randomUUID } from 'node:crypto';
import { requireAuth } from '../../middleware/require-auth.js';
import { decrypt, encrypt } 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, CreateCustomRtmpBody } from '../../types/api.js';
const accountRoutes: FastifyPluginAsync = async (fastify) => {
// GET /providers/accounts — list linked accounts (no tokens, except CUSTOM_RTMP)
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) => {
const base: LinkedAccountResponse = {
id: a.id,
serviceId: a.serviceId,
displayName: a.displayName,
accountId: a.accountId,
avatarUrl: a.avatarUrl,
};
if (a.serviceId === 'CUSTOM_RTMP') {
base.rtmpUrl = decrypt(a.accessTokenEnc, a.accessTokenIv);
base.streamKey = decrypt(a.refreshTokenEnc, a.refreshTokenIv);
}
return base;
});
return response;
});
// POST /providers/accounts/custom-rtmp — create a custom RTMP account
fastify.post<{ Body: CreateCustomRtmpBody }>('/providers/accounts/custom-rtmp', {
preHandler: [requireAuth],
schema: {
body: {
type: 'object',
required: ['displayName', 'rtmpUrl', 'streamKey'],
properties: {
displayName: { type: 'string', minLength: 1, maxLength: 200 },
rtmpUrl: { type: 'string', minLength: 1, maxLength: 500 },
streamKey: { type: 'string', minLength: 1, maxLength: 500 },
},
additionalProperties: false,
},
},
}, async (request, reply) => {
const { displayName, rtmpUrl, streamKey } = request.body;
const urlEnc = encrypt(rtmpUrl);
const keyEnc = encrypt(streamKey);
const account = await fastify.prisma.linkedAccount.create({
data: {
userId: request.userId,
serviceId: 'CUSTOM_RTMP',
accountId: randomUUID(),
displayName,
avatarUrl: null,
accessTokenEnc: urlEnc.ciphertext,
accessTokenIv: urlEnc.iv,
refreshTokenEnc: keyEnc.ciphertext,
refreshTokenIv: keyEnc.iv,
tokenExpiresAt: new Date('2099-12-31T00:00:00Z'),
},
});
const response: LinkedAccountResponse = {
id: account.id,
serviceId: account.serviceId,
displayName: account.displayName,
accountId: account.accountId,
avatarUrl: account.avatarUrl,
rtmpUrl,
streamKey,
};
reply.status(201).send(response);
});
// 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: ['id'],
properties: {
id: { type: 'string' },
},
},
},
}, async (request, reply) => {
const account = await fastify.prisma.linkedAccount.findFirst({
where: {
id: request.params.id,
userId: request.userId,
},
});
if (!account) {
throw new AppError(404, 'Account not linked');
}
// Best-effort revoke tokens at the provider (skip for CUSTOM_RTMP)
if (account.serviceId !== 'CUSTOM_RTMP') {
try {
const accessToken = decrypt(account.accessTokenEnc, account.accessTokenIv);
if (account.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;