Add YouTube OAuth callback URL configuration and enhance token handling

This commit is contained in:
Cesar Mendivil 2026-03-01 23:38:02 -07:00
parent f866802864
commit 3a2edb1e30
5 changed files with 49 additions and 16 deletions

View File

@ -31,6 +31,10 @@ services:
# Debe coincidir con lo que tienes en developers.facebook.com
FB_OAUTH_CALLBACK_URL: "https://djmaster.nextream.sytes.net/oauth/facebook/callback.htm"
# URL EXACTA registrada en Google Console como "Authorized redirect URI"
# Debe coincidir con lo que tienes en console.cloud.google.com
YT_OAUTH_CALLBACK_URL: "https://djmaster.nextream.sytes.net/oauth2callback"
# Clave de cifrado para tokens almacenados (cámbiala en producción)
FB_ENCRYPTION_SECRET: "restreamer-ui-fb-secret-key-32x!"

View File

@ -13,6 +13,7 @@ window.__RESTREAMER_CONFIG__ = {
YTDLP_URL: "${YTDLP_URL:-}",
FB_SERVER_URL: "${FB_SERVER_URL:-}",
FB_OAUTH_CALLBACK_URL: "${FB_OAUTH_CALLBACK_URL:-}",
YT_OAUTH_CALLBACK_URL: "${YT_OAUTH_CALLBACK_URL:-}",
};
EOF

View File

@ -135,14 +135,13 @@
function sendCode() {
if (ackReceived || attempts >= maxAttempts) return;
attempts++;
// redirect_uri = esta misma página (origin + pathname), exactamente como está registrada en Google
var thisUrl = window.location.origin + window.location.pathname;
var msg = { service: 'youtube', code: code, state: state, redirect_uri: thisUrl };
try {
window.opener.postMessage(
{ service: 'youtube', code: code, state: state },
window.location.origin // mismo origen
);
window.opener.postMessage(msg, window.location.origin);
} catch (e) {
// Si falla por política de origen, intentar con '*'
try { window.opener.postMessage({ service: 'youtube', code: code, state: state }, '*'); } catch (e2) {}
try { window.opener.postMessage(msg, '*'); } catch (e2) {}
}
setTimeout(sendCode, 600);
}

View File

@ -266,10 +266,29 @@ const refreshAccount = async (accountKey) => {
}
};
/** Obtener token válido (refresca automáticamente si está expirado) */
/** Obtener token válido (refresca automáticamente si está expirado).
* Si el access_token no está en caché local (porque viene del servidor cifrado),
* lo obtiene del endpoint /yt/accounts/:key/token.
*/
const getValidToken = async (accountKey) => {
const account = getAccount(accountKey);
if (!account || !account.access_token) throw new Error('Account not connected: ' + accountKey);
let account = getAccount(accountKey);
if (!account) throw new Error('Account not connected: ' + accountKey);
// Si no hay access_token en caché local, obtenerlo del servidor
if (!account.access_token) {
const serverAccount = await getAccountToken(accountKey);
if (serverAccount && serverAccount.access_token) {
// Guardar en caché para futuras llamadas
const cache = _readCache();
if (!cache.accounts) cache.accounts = {};
cache.accounts[accountKey] = { ...(cache.accounts[accountKey] || {}), ...serverAccount };
_writeCache(cache);
account = cache.accounts[accountKey];
} else {
throw new Error('Could not retrieve token for account: ' + accountKey);
}
}
const isExpired = account.token_expiry && Date.now() > account.token_expiry - 60000;
if (isExpired) return await refreshAccount(accountKey);
return account.access_token;

View File

@ -116,8 +116,9 @@ function Service(props) {
// Cuenta conectada a ESTA publication (guardada en ytOAuth store)
const connectedAccount = settings.account_key ? ytOAuth.getAccount(settings.account_key) : null;
const isConnected = !!(connectedAccount && connectedAccount.access_token);
const isTokenExpired = isConnected && connectedAccount.token_expiry
// isConnected: tiene token local O tiene token en servidor (has_access_token)
const isConnected = !!(connectedAccount && (connectedAccount.access_token || connectedAccount.has_access_token));
const isTokenExpired = isConnected && connectedAccount && connectedAccount.token_expiry
? Date.now() > connectedAccount.token_expiry - 60000
: false;
@ -174,7 +175,12 @@ function Service(props) {
return;
}
const redirectUri = window.location.origin + '/oauth2callback';
// Usar YT_OAUTH_CALLBACK_URL si está definida (producción con dominio real)
// De lo contrario construir desde window.location.origin (desarrollo)
const _rtCfg = (typeof window !== 'undefined' && window.__RESTREAMER_CONFIG__) || {};
const redirectUri = _rtCfg.YT_OAUTH_CALLBACK_URL
? _rtCfg.YT_OAUTH_CALLBACK_URL
: window.location.origin + '/oauth2callback';
const oauthState = btoa(JSON.stringify({ service: 'youtube', ts: Date.now() }));
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth?' + new URLSearchParams({
client_id: globalCreds.client_id,
@ -237,13 +243,17 @@ function Service(props) {
if (event.data.code) {
try {
// Usar el redirect_uri que envió el callback (URL exacta de la página de callback)
// Esto garantiza que coincida exactamente con el registrado en Google Console
const callbackRedirectUri = event.data.redirect_uri || redirectUri;
// Intercambiar code por tokens vía servidor (incluye IDs de Restreamer)
const exchangeResp = await fetch('/fb-server/yt/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: event.data.code,
redirect_uri: redirectUri,
redirect_uri: callbackRedirectUri,
restreamer_channel_id: props.channelId || '',
restreamer_publication_id: props.publicationId || '',
}),
@ -263,7 +273,7 @@ function Service(props) {
code: event.data.code,
client_id: localCreds.client_id,
client_secret: localCreds.client_secret,
redirect_uri: redirectUri,
redirect_uri: callbackRedirectUri,
grant_type: 'authorization_code',
}).toString(),
});
@ -411,8 +421,8 @@ function Service(props) {
if (!bResp.ok) throw new Error('Broadcast: ' + (bData.error ? bData.error.message : `HTTP ${bResp.status}`));
const broadcastId = bData.id;
// 2) LiveStream
const sResp = await fetch(`${YT_API}/liveStreams?part=snippet,cdn,status`, {
// 2) LiveStream — contentDetails debe estar en ?part= para poder enviarlo en el body
const sResp = await fetch(`${YT_API}/liveStreams?part=snippet,cdn,status,contentDetails`, {
method: 'POST', headers,
body: JSON.stringify({
snippet: { title: settings.title },