- 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
95 lines
3.1 KiB
TypeScript
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;
|