Files
lck-control-backend/src/routes/auth/meta.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

95 lines
3.1 KiB
TypeScript

import { FastifyPluginAsync } from 'fastify';
import { randomUUID } from 'node:crypto';
import { verifyUserProof, fetchOculusProfile } from '../../services/meta-auth.service.js';
import { signAccessToken } from '../../plugins/auth.js';
import { hashToken } from '../../services/crypto.service.js';
import { config } from '../../config.js';
import { AppError } from '../../plugins/error-handler.js';
import type { MetaCallbackBody, AuthTokensResponse } from '../../types/api.js';
const metaAuthRoutes: FastifyPluginAsync = async (fastify) => {
fastify.post<{ Body: MetaCallbackBody }>('/auth/meta/callback', {
config: {
rateLimit: { max: 10, timeWindow: '1 minute' },
},
schema: {
body: {
type: 'object',
required: ['userId'],
properties: {
userId: { type: 'string', minLength: 1 },
nonce: { type: 'string' },
deviceInfo: { type: 'string' },
},
additionalProperties: false,
},
},
}, async (request, reply) => {
const { userId, nonce, deviceInfo } = request.body;
request.log.info({ userId, hasNonce: !!nonce, deviceInfo }, 'Meta callback received');
// Nonce validation requires a numeric user ID from Meta's graph API.
// When DUC hasn't granted numeric ID access, the SDK returns oculusId (a string username).
const isNumericId = /^\d+$/.test(userId);
if (nonce && isNumericId) {
const isValid = await verifyUserProof(userId, nonce);
if (!isValid) {
throw new AppError(401, 'Invalid user proof');
}
request.log.info('Nonce verified successfully');
} else {
request.log.warn({ isNumericId, hasNonce: !!nonce },
'Skipping nonce verification (non-numeric userId or missing nonce)');
}
// Try to fetch profile from Oculus graph; fall back to userId as display name
let metaId = userId;
let displayName = userId;
if (isNumericId) {
try {
const profile = await fetchOculusProfile(userId);
metaId = profile.metaId;
displayName = profile.displayName;
} catch (e) {
request.log.warn(e, 'Failed to fetch Oculus profile');
}
}
// Upsert user
const existingUser = await fastify.prisma.user.findUnique({ where: { metaId } });
request.log.info({ metaId, userId: existingUser?.id, isNew: !existingUser }, 'User upsert');
const user = await fastify.prisma.user.upsert({
where: { metaId },
update: { displayName },
create: { metaId, displayName },
});
// Create session with hashed refresh token
const refreshToken = randomUUID();
const expiresAt = new Date(Date.now() + config.jwt.refreshTtl * 1000);
await fastify.prisma.session.create({
data: {
userId: user.id,
refreshToken: hashToken(refreshToken),
expiresAt,
deviceInfo: deviceInfo ?? null,
},
});
// Sign JWT
const accessToken = await signAccessToken(user.id);
const response: AuthTokensResponse = {
accessToken,
refreshToken,
expiresIn: config.jwt.accessTtl,
};
reply.status(200).send(response);
});
};
export default metaAuthRoutes;