Add YouTube OAuth callback URL configuration and enhance token handling
This commit is contained in:
parent
f866802864
commit
3a2edb1e30
@ -31,6 +31,10 @@ services:
|
|||||||
# Debe coincidir con lo que tienes en developers.facebook.com
|
# Debe coincidir con lo que tienes en developers.facebook.com
|
||||||
FB_OAUTH_CALLBACK_URL: "https://djmaster.nextream.sytes.net/oauth/facebook/callback.htm"
|
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)
|
# Clave de cifrado para tokens almacenados (cámbiala en producción)
|
||||||
FB_ENCRYPTION_SECRET: "restreamer-ui-fb-secret-key-32x!"
|
FB_ENCRYPTION_SECRET: "restreamer-ui-fb-secret-key-32x!"
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ window.__RESTREAMER_CONFIG__ = {
|
|||||||
YTDLP_URL: "${YTDLP_URL:-}",
|
YTDLP_URL: "${YTDLP_URL:-}",
|
||||||
FB_SERVER_URL: "${FB_SERVER_URL:-}",
|
FB_SERVER_URL: "${FB_SERVER_URL:-}",
|
||||||
FB_OAUTH_CALLBACK_URL: "${FB_OAUTH_CALLBACK_URL:-}",
|
FB_OAUTH_CALLBACK_URL: "${FB_OAUTH_CALLBACK_URL:-}",
|
||||||
|
YT_OAUTH_CALLBACK_URL: "${YT_OAUTH_CALLBACK_URL:-}",
|
||||||
};
|
};
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@ -135,14 +135,13 @@
|
|||||||
function sendCode() {
|
function sendCode() {
|
||||||
if (ackReceived || attempts >= maxAttempts) return;
|
if (ackReceived || attempts >= maxAttempts) return;
|
||||||
attempts++;
|
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 {
|
try {
|
||||||
window.opener.postMessage(
|
window.opener.postMessage(msg, window.location.origin);
|
||||||
{ service: 'youtube', code: code, state: state },
|
|
||||||
window.location.origin // mismo origen
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Si falla por política de origen, intentar con '*'
|
try { window.opener.postMessage(msg, '*'); } catch (e2) {}
|
||||||
try { window.opener.postMessage({ service: 'youtube', code: code, state: state }, '*'); } catch (e2) {}
|
|
||||||
}
|
}
|
||||||
setTimeout(sendCode, 600);
|
setTimeout(sendCode, 600);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 getValidToken = async (accountKey) => {
|
||||||
const account = getAccount(accountKey);
|
let account = getAccount(accountKey);
|
||||||
if (!account || !account.access_token) throw new Error('Account not connected: ' + 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;
|
const isExpired = account.token_expiry && Date.now() > account.token_expiry - 60000;
|
||||||
if (isExpired) return await refreshAccount(accountKey);
|
if (isExpired) return await refreshAccount(accountKey);
|
||||||
return account.access_token;
|
return account.access_token;
|
||||||
|
|||||||
@ -116,8 +116,9 @@ function Service(props) {
|
|||||||
|
|
||||||
// Cuenta conectada a ESTA publication (guardada en ytOAuth store)
|
// Cuenta conectada a ESTA publication (guardada en ytOAuth store)
|
||||||
const connectedAccount = settings.account_key ? ytOAuth.getAccount(settings.account_key) : null;
|
const connectedAccount = settings.account_key ? ytOAuth.getAccount(settings.account_key) : null;
|
||||||
const isConnected = !!(connectedAccount && connectedAccount.access_token);
|
// isConnected: tiene token local O tiene token en servidor (has_access_token)
|
||||||
const isTokenExpired = isConnected && connectedAccount.token_expiry
|
const isConnected = !!(connectedAccount && (connectedAccount.access_token || connectedAccount.has_access_token));
|
||||||
|
const isTokenExpired = isConnected && connectedAccount && connectedAccount.token_expiry
|
||||||
? Date.now() > connectedAccount.token_expiry - 60000
|
? Date.now() > connectedAccount.token_expiry - 60000
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
@ -174,7 +175,12 @@ function Service(props) {
|
|||||||
return;
|
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 oauthState = btoa(JSON.stringify({ service: 'youtube', ts: Date.now() }));
|
||||||
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth?' + new URLSearchParams({
|
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth?' + new URLSearchParams({
|
||||||
client_id: globalCreds.client_id,
|
client_id: globalCreds.client_id,
|
||||||
@ -237,13 +243,17 @@ function Service(props) {
|
|||||||
|
|
||||||
if (event.data.code) {
|
if (event.data.code) {
|
||||||
try {
|
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)
|
// Intercambiar code por tokens vía servidor (incluye IDs de Restreamer)
|
||||||
const exchangeResp = await fetch('/fb-server/yt/exchange', {
|
const exchangeResp = await fetch('/fb-server/yt/exchange', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
code: event.data.code,
|
code: event.data.code,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: callbackRedirectUri,
|
||||||
restreamer_channel_id: props.channelId || '',
|
restreamer_channel_id: props.channelId || '',
|
||||||
restreamer_publication_id: props.publicationId || '',
|
restreamer_publication_id: props.publicationId || '',
|
||||||
}),
|
}),
|
||||||
@ -263,7 +273,7 @@ function Service(props) {
|
|||||||
code: event.data.code,
|
code: event.data.code,
|
||||||
client_id: localCreds.client_id,
|
client_id: localCreds.client_id,
|
||||||
client_secret: localCreds.client_secret,
|
client_secret: localCreds.client_secret,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: callbackRedirectUri,
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
}).toString(),
|
}).toString(),
|
||||||
});
|
});
|
||||||
@ -411,8 +421,8 @@ function Service(props) {
|
|||||||
if (!bResp.ok) throw new Error('Broadcast: ' + (bData.error ? bData.error.message : `HTTP ${bResp.status}`));
|
if (!bResp.ok) throw new Error('Broadcast: ' + (bData.error ? bData.error.message : `HTTP ${bResp.status}`));
|
||||||
const broadcastId = bData.id;
|
const broadcastId = bData.id;
|
||||||
|
|
||||||
// 2) LiveStream
|
// 2) LiveStream — contentDetails debe estar en ?part= para poder enviarlo en el body
|
||||||
const sResp = await fetch(`${YT_API}/liveStreams?part=snippet,cdn,status`, {
|
const sResp = await fetch(`${YT_API}/liveStreams?part=snippet,cdn,status,contentDetails`, {
|
||||||
method: 'POST', headers,
|
method: 'POST', headers,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
snippet: { title: settings.title },
|
snippet: { title: settings.title },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user