mirror of
https://github.com/SrIzan10/vdo.ninja.git
synced 2026-05-01 11:05:24 +00:00
849 lines
25 KiB
JavaScript
849 lines
25 KiB
JavaScript
/* VDO.Ninja Authentication Client Integration */
|
|
|
|
// Configuration
|
|
const AUTH_SERVICE_URL = 'https://vdo-ninja-auth-service.vdo.workers.dev'; // Change for local dev: http://localhost:8787
|
|
|
|
// Authentication state
|
|
session.authMode = false;
|
|
session.requireAuth = false;
|
|
session.authToken = null;
|
|
session.authUser = null;
|
|
session.authStreamMapping = {};
|
|
session.handleToStream = {};
|
|
|
|
|
|
// Initialize authentication
|
|
async function initAuthentication() {
|
|
|
|
// Check URL parameters for universal token first
|
|
if (urlParams.has("universaltoken")) {
|
|
session.universalToken = urlParams.get("universaltoken");
|
|
session.authMode = true;
|
|
console.log('Universal token detected:', session.universalToken);
|
|
// Universal tokens bypass auth requirement for viewing
|
|
if (session.view || session.scene || session.solo) {
|
|
session.requireAuth = false;
|
|
console.log('Auth requirement bypassed for viewing');
|
|
}
|
|
}
|
|
|
|
// Check URL parameters
|
|
if (urlParams.has("auth") || urlParams.has("requireauth")) {
|
|
session.authMode = true;
|
|
session.requireAuth = urlParams.has("requireauth");
|
|
|
|
// Check for existing auth token in localStorage
|
|
const storedToken = localStorage.getItem('vdo_auth_token');
|
|
if (storedToken) {
|
|
try {
|
|
// Validate token is still valid
|
|
const payload = JSON.parse(atob(storedToken.split('.')[1]));
|
|
if (payload.exp > Date.now() / 1000) {
|
|
session.authToken = storedToken;
|
|
await populateUserInfo();
|
|
} else {
|
|
localStorage.removeItem('vdo_auth_token');
|
|
}
|
|
} catch (e) {
|
|
localStorage.removeItem('vdo_auth_token');
|
|
}
|
|
}
|
|
|
|
// Check for auth token in URL (after OAuth redirect)
|
|
if (urlParams.has("authtoken")) {
|
|
session.authToken = urlParams.get("authtoken");
|
|
localStorage.setItem('vdo_auth_token', session.authToken);
|
|
|
|
// Clean URL
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.delete('authtoken');
|
|
window.history.replaceState({}, document.title, url.toString());
|
|
|
|
await populateUserInfo();
|
|
}
|
|
|
|
// Check if we need to verify room requirements
|
|
if (!session.authToken && session.authMode && (urlParams.has("room") || urlParams.has("roomid") || urlParams.has("r"))) {
|
|
const roomId = urlParams.get("room") || urlParams.get("roomid") || urlParams.get("r");
|
|
if (roomId) {
|
|
// Check if this room requires auth
|
|
try {
|
|
const roomInfo = await checkRoomAccess(roomId, urlParams.has("director") || urlParams.has("dir"));
|
|
if (roomInfo.requiresAuth) {
|
|
session.requireAuth = true;
|
|
}
|
|
} catch (e) {
|
|
console.log('Could not check room requirements:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show auth UI if required and not authenticated
|
|
if (!session.authToken && (session.requireAuth || session.director)) {
|
|
// If the page is in auth mode or the director is attempting to use auth,
|
|
// encourage sign-in proactively.
|
|
showAuthUI();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show authentication UI
|
|
function showAuthUI(options = {}) {
|
|
const authContainer = document.createElement('div');
|
|
authContainer.id = 'auth-container';
|
|
authContainer.innerHTML = `
|
|
<div class="auth-modal">
|
|
<h2>Sign in to VDO.Ninja</h2>
|
|
<p>${options.message || 'Sign in to claim your personal stream ID and enable advanced features'}</p>
|
|
|
|
<div class="auth-buttons">
|
|
<button onclick="socialSignIn('google')" class="auth-button google">
|
|
<img src="./media/google.png" alt="Google">
|
|
Sign in with Google
|
|
</button>
|
|
<button onclick="socialSignIn('discord')" class="auth-button discord">
|
|
<img src="./media/discord.png" alt="Discord">
|
|
Sign in with Discord
|
|
</button>
|
|
<button onclick="socialSignIn('twitch')" class="auth-button twitch">
|
|
<img src="./media/twitch.png" alt="Twitch">
|
|
Sign in with Twitch
|
|
</button>
|
|
</div>
|
|
|
|
${(!session.requireAuth && !options.requireAuth) ? '<button onclick="skipAuth()" class="skip-auth">Continue without signing in</button>' : ''}
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(authContainer);
|
|
}
|
|
|
|
// Social sign-in handler
|
|
function socialSignIn(provider) {
|
|
const returnUrl = encodeURIComponent(window.location.href);
|
|
window.location.href = `${AUTH_SERVICE_URL}/auth/${provider}?returnUrl=${returnUrl}`;
|
|
}
|
|
|
|
// Skip authentication
|
|
function skipAuth() {
|
|
const authContainer = document.getElementById('auth-container');
|
|
if (authContainer) {
|
|
authContainer.remove();
|
|
}
|
|
session.authSkipped = true;
|
|
}
|
|
|
|
// Populate user info from auth token
|
|
async function populateUserInfo() {
|
|
if (!session.authToken) return;
|
|
|
|
try {
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/user/info`, {
|
|
headers: { 'Authorization': `Bearer ${session.authToken}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
const userInfo = await response.json();
|
|
session.authUser = userInfo;
|
|
|
|
// Auto-populate label if not set
|
|
if (!session.label && userInfo.displayName) {
|
|
session.label = userInfo.displayName;
|
|
if (document.getElementById("label_input")) {
|
|
document.getElementById("label_input").value = session.label;
|
|
}
|
|
}
|
|
|
|
// Auto-populate avatar if not set
|
|
if (!session.avatar && userInfo.avatar) {
|
|
session.avatar = userInfo.avatar;
|
|
updateAvatarDisplay();
|
|
}
|
|
|
|
// Store user handle
|
|
session.userHandle = userInfo.userHandle;
|
|
|
|
// Show user info in UI
|
|
showUserInfo(userInfo);
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to get user info:", e);
|
|
}
|
|
}
|
|
|
|
// Show user info in UI
|
|
function showUserInfo(userInfo) {
|
|
const existingDisplay = document.getElementById('user-info-display');
|
|
if (existingDisplay) {
|
|
existingDisplay.remove();
|
|
}
|
|
|
|
const userDisplay = document.createElement('div');
|
|
userDisplay.id = 'user-info-display';
|
|
userDisplay.className = 'user-info-display';
|
|
userDisplay.innerHTML = `
|
|
<img src="${userInfo.avatar || './media/default-avatar.png'}" alt="${userInfo.displayName}">
|
|
<div class="user-details">
|
|
<div class="user-name">${userInfo.displayName}</div>
|
|
<div class="user-handle">${userInfo.userHandle}</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add to appropriate location based on current view
|
|
const targetElement = document.querySelector('.header-container') || document.querySelector('.container');
|
|
if (targetElement) {
|
|
targetElement.insertBefore(userDisplay, targetElement.firstChild);
|
|
}
|
|
}
|
|
|
|
// Assign authenticated stream ID
|
|
async function assignAuthStream() {
|
|
if (!session.authToken || session.authStreamAssigned) return;
|
|
|
|
try {
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/stream/assign`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${session.authToken}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
roomId: session.roomid || 'lobby',
|
|
deviceLabel: session.streamID || 'camera',
|
|
useEncryption: false // Disabled for now until fully tested
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const assignment = await response.json();
|
|
|
|
// Store original stream ID
|
|
session.originalStreamID = session.streamID;
|
|
|
|
// Use assigned stream ID
|
|
session.streamID = assignment.streamId;
|
|
session.streamSecret = assignment.streamSecret;
|
|
session.authStreamAssigned = true;
|
|
|
|
console.log("Assigned authenticated stream:", assignment.streamId);
|
|
|
|
// Update any UI showing stream ID
|
|
updateStreamIDDisplay();
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to assign auth stream:", e);
|
|
}
|
|
}
|
|
|
|
// Generate stream authentication signature
|
|
async function generateStreamSignature() {
|
|
if (!session.streamSecret) return null;
|
|
|
|
const timestamp = Date.now();
|
|
const message = `${session.streamID}:${timestamp}`;
|
|
|
|
const encoder = new TextEncoder();
|
|
const key = await crypto.subtle.importKey(
|
|
'raw',
|
|
encoder.encode(session.streamSecret),
|
|
{ name: 'HMAC', hash: 'SHA-256' },
|
|
false,
|
|
['sign']
|
|
);
|
|
|
|
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(message));
|
|
const hexSignature = Array.from(new Uint8Array(signature))
|
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
|
|
return {
|
|
streamId: session.streamID,
|
|
userHandle: session.userHandle,
|
|
timestamp: timestamp,
|
|
signature: hexSignature
|
|
};
|
|
}
|
|
|
|
// Validate incoming stream authentication
|
|
async function validateStreamAuth(streamId, authData) {
|
|
if (!session.authToken || !authData) return true;
|
|
|
|
try {
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/stream/verify`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${session.authToken}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
streamId: streamId,
|
|
auth: authData
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
if (result.valid && result.userInfo) {
|
|
// Store user info for this stream
|
|
session.authStreamMapping[streamId] = result.userInfo;
|
|
|
|
// Update UI if this is a director view
|
|
if (session.director) {
|
|
updateStreamDisplay(streamId, result.userInfo);
|
|
}
|
|
}
|
|
return result.valid;
|
|
}
|
|
} catch (e) {
|
|
console.error("Stream validation failed:", e);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Resolve view handles (e.g., @johndoe) to stream IDs
|
|
async function resolveViewHandles(viewList) {
|
|
if (!session.authToken) return viewList;
|
|
|
|
const resolved = [];
|
|
|
|
for (const target of viewList) {
|
|
if (target.startsWith('@')) {
|
|
// User handle - resolve to current stream
|
|
try {
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/stream/user/${target}`, {
|
|
headers: { 'Authorization': `Bearer ${session.authToken}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.currentStreamId) {
|
|
resolved.push(data.currentStreamId);
|
|
// Store mapping for UI
|
|
session.handleToStream[target] = data;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(`Failed to resolve handle ${target}:`, e);
|
|
}
|
|
} else {
|
|
resolved.push(target);
|
|
}
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
// Check room access
|
|
async function checkRoomAccess(roomIdOrAlias, isDirector = false) {
|
|
console.log('Checking room access for:', roomIdOrAlias, 'with universal token:', session.universalToken);
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/room/access`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': session.authToken ? `Bearer ${session.authToken}` : '',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
room: roomIdOrAlias,
|
|
isDirector: isDirector,
|
|
universalToken: session.universalToken || null
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
console.log('Room access response:', response.status, data);
|
|
|
|
// Handle room not found case
|
|
if (response.status === 404 && data && data.error === 'Room not found') {
|
|
// In auth mode, non-existent rooms can be created by authenticated users
|
|
if (session.authToken) {
|
|
// Allow authenticated users to proceed - room will be created on first join
|
|
return {
|
|
roomId: roomIdOrAlias,
|
|
alias: roomIdOrAlias,
|
|
displayName: roomIdOrAlias,
|
|
requiresAuth: false,
|
|
hasAccess: true,
|
|
isNew: true
|
|
};
|
|
} else {
|
|
// Require auth to create new rooms
|
|
return {
|
|
alias: roomIdOrAlias,
|
|
displayName: roomIdOrAlias,
|
|
requiresAuth: true,
|
|
hasAccess: false,
|
|
accessDenied: true,
|
|
denialReason: 'Sign in to create or join this room'
|
|
};
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// Join room with authentication
|
|
async function joinRoomWithAuth(roomIdOrAlias) {
|
|
// If director is using auth mode but not signed in yet, force sign in first
|
|
if (session.director && session.authMode && !session.authToken && !session.universalToken) {
|
|
const roomLabel = roomIdOrAlias || 'this room';
|
|
showAuthUI({
|
|
message: `Sign in to manage "${roomLabel}"`,
|
|
requireAuth: true
|
|
});
|
|
return false;
|
|
}
|
|
// If we have a universal token, validate it first
|
|
if (session.universalToken) {
|
|
try {
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/room/validate-universal`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
token: session.universalToken,
|
|
roomId: roomIdOrAlias
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
if (result.valid) {
|
|
// Universal token is valid, bypass normal auth
|
|
session.roomid = roomIdOrAlias;
|
|
return true;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to validate universal token:', e);
|
|
}
|
|
}
|
|
|
|
const roomInfo = await checkRoomAccess(roomIdOrAlias, session.director);
|
|
|
|
if (roomInfo.requiresAuth && !session.authToken && !session.universalToken) {
|
|
if (session.authSkipped) {
|
|
// User already chose to skip auth, show access denied instead of auth UI
|
|
showAccessDeniedUI({
|
|
...roomInfo,
|
|
denialReason: 'This room requires authentication. Please reload the page and sign in to join.',
|
|
requestAccessUrl: null
|
|
});
|
|
return false;
|
|
} else {
|
|
// First time seeing auth requirement for this room
|
|
const displayLabel = roomInfo.displayName || roomInfo.alias || roomIdOrAlias || roomInfo.roomId || 'this room';
|
|
showAuthUI({
|
|
message: `Sign in to join "${displayLabel}"`,
|
|
requireAuth: true
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (roomInfo.accessDenied) {
|
|
showAccessDeniedUI(roomInfo);
|
|
return false;
|
|
}
|
|
|
|
// Important: For auth rooms, we need to use the original alias for hashing
|
|
// The auth service tracks by the real room ID, but VDO uses the alias
|
|
if (roomInfo.alias && roomInfo.alias === roomIdOrAlias) {
|
|
// User provided the alias, keep using it
|
|
session.roomid = roomIdOrAlias;
|
|
} else if (roomInfo.roomId === roomIdOrAlias) {
|
|
// User provided the real room ID
|
|
session.roomid = roomInfo.alias || roomIdOrAlias;
|
|
} else {
|
|
// Default case
|
|
session.roomid = roomInfo.alias || roomInfo.roomId;
|
|
}
|
|
|
|
session.roomAlias = roomInfo.alias;
|
|
session.realRoomId = roomInfo.roomId;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Show access denied UI
|
|
function showAccessDeniedUI(roomInfo) {
|
|
const modal = document.createElement('div');
|
|
modal.id = 'auth-container';
|
|
modal.innerHTML = `
|
|
<div class="auth-modal access-denied-modal">
|
|
<h3>Access Denied</h3>
|
|
<p>${roomInfo.denialReason}</p>
|
|
${roomInfo.requestAccessUrl ?
|
|
`<button onclick="requestRoomAccess('${roomInfo.roomId}')">Request Access</button>` :
|
|
'<button onclick="window.location.reload()">Go Back</button>'
|
|
}
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
// Request room access
|
|
async function requestRoomAccess(roomId) {
|
|
if (!session.authToken) {
|
|
showAuthUI({ message: 'Sign in to request access' });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/room/request-access/${roomId}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${session.authToken}`
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
alert('Access request sent! The room owner will review your request.');
|
|
document.getElementById('auth-container').remove();
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to request access:', e);
|
|
}
|
|
}
|
|
|
|
// Update stream display with user info
|
|
function updateStreamDisplay(streamId, userInfo) {
|
|
// Update control box if it exists
|
|
const controlBox = document.getElementById(`controls_${streamId}`);
|
|
if (controlBox && userInfo) {
|
|
const header = controlBox.querySelector('.header');
|
|
if (header && !header.querySelector('.user-auth-badge')) {
|
|
const badge = document.createElement('div');
|
|
badge.className = 'user-auth-badge';
|
|
badge.innerHTML = `
|
|
<img src="${userInfo.avatar}" alt="${userInfo.displayName}">
|
|
<span class="user-handle">${userInfo.userHandle}</span>
|
|
<span class="user-provider ${userInfo.provider}">${userInfo.provider}</span>
|
|
`;
|
|
header.appendChild(badge);
|
|
}
|
|
}
|
|
|
|
// Update any labels showing stream ID
|
|
const labels = document.querySelectorAll(`[data-stream-id="${streamId}"]`);
|
|
labels.forEach(label => {
|
|
if (userInfo && !label.dataset.updated) {
|
|
label.dataset.updated = 'true';
|
|
label.textContent = userInfo.displayName || userInfo.userHandle;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update avatar display
|
|
function updateAvatarDisplay() {
|
|
if (session.avatar) {
|
|
// Update any avatar displays in the UI
|
|
const avatarElements = document.querySelectorAll('.avatar-display');
|
|
avatarElements.forEach(el => {
|
|
el.src = session.avatar;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update stream ID display
|
|
function updateStreamIDDisplay() {
|
|
// Update any UI elements showing the stream ID
|
|
const streamIdElements = document.querySelectorAll('.stream-id-display');
|
|
streamIdElements.forEach(el => {
|
|
el.textContent = session.originalStreamID || session.streamID;
|
|
});
|
|
}
|
|
|
|
// Resolve any stream ID (encrypted or not) through auth service
|
|
async function resolveStream(streamId) {
|
|
if (!session.authToken && !session.universalToken) {
|
|
return { error: 'Not authenticated' };
|
|
}
|
|
|
|
try {
|
|
const headers = {
|
|
'Content-Type': 'application/json'
|
|
};
|
|
|
|
if (session.authToken) {
|
|
headers['Authorization'] = `Bearer ${session.authToken}`;
|
|
}
|
|
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/stream/resolve`, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify({
|
|
streamId: streamId,
|
|
roomId: session.roomid,
|
|
universalToken: session.universalToken
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data;
|
|
} else if (response.status === 403) {
|
|
return { error: 'Access denied' };
|
|
} else if (response.status === 404) {
|
|
return { error: 'Stream not found' };
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to resolve stream:', e);
|
|
return { error: 'Failed to resolve stream' };
|
|
}
|
|
|
|
return { error: 'Unknown error' };
|
|
}
|
|
|
|
// Get encryption key for viewing a stream
|
|
async function getStreamKey(streamId) {
|
|
if (!session.authToken) return null;
|
|
|
|
try {
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/stream/key`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${session.authToken}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
streamId: streamId,
|
|
roomId: session.roomid
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
return data;
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to get stream key:', e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Decrypt stream ID using XOR cipher
|
|
async function decryptStreamId(encryptedId, key) {
|
|
// Add padding if needed
|
|
const base64 = encryptedId
|
|
.replace(/-/g, '+')
|
|
.replace(/_/g, '/')
|
|
.padEnd(encryptedId.length + (4 - encryptedId.length % 4) % 4, '=');
|
|
|
|
const encrypted = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
|
const keyData = new TextEncoder().encode(key);
|
|
|
|
const decrypted = new Uint8Array(encrypted.length);
|
|
for (let i = 0; i < encrypted.length; i++) {
|
|
decrypted[i] = encrypted[i] ^ keyData[i % keyData.length];
|
|
}
|
|
|
|
return new TextDecoder().decode(decrypted);
|
|
}
|
|
|
|
// Heartbeat to keep stream active
|
|
function startAuthHeartbeat() {
|
|
if (!session.authToken || !session.streamID) return;
|
|
|
|
setInterval(async () => {
|
|
if (session.authToken && session.streamID && session.authStreamAssigned) {
|
|
try {
|
|
await fetch(`${AUTH_SERVICE_URL}/api/stream/heartbeat`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${session.authToken}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
streamId: session.streamID,
|
|
roomId: session.roomid || 'lobby'
|
|
})
|
|
});
|
|
} catch (e) {
|
|
console.error('Heartbeat failed:', e);
|
|
}
|
|
}
|
|
}, 30000); // Every 30 seconds
|
|
}
|
|
|
|
// Create a universal token for view/scene links
|
|
async function createUniversalToken() {
|
|
if (!session.authToken || !session.roomid) {
|
|
console.error('Must be authenticated and in a room to create universal token');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
console.log('Creating universal token for room:', session.roomid);
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/room/universal-token`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${session.authToken}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
roomId: session.roomid,
|
|
description: 'View/Scene access token'
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
session.universalViewToken = data.token;
|
|
console.log('Created universal token:', data.token);
|
|
|
|
// Update all existing solo links
|
|
updateAllSoloLinks();
|
|
|
|
return data.token;
|
|
} else {
|
|
console.error('Failed to create universal token:', response.status);
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to create universal token:', e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Update all solo link displays with new token
|
|
function updateAllSoloLinks() {
|
|
// Update all solo link inputs and displays
|
|
document.querySelectorAll('[data-sololink]').forEach(ele => {
|
|
const uuid = ele.getAttribute('data--u-u-i-d');
|
|
if (uuid && session.rpcs[uuid]) {
|
|
const soloLink = soloLinkGenerator(session.rpcs[uuid].streamID, false);
|
|
if (ele.tagName === 'INPUT') {
|
|
ele.value = soloLink;
|
|
} else if (ele.tagName === 'A') {
|
|
ele.href = soloLink;
|
|
ele.innerText = soloLink;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update director's own solo link if present
|
|
const directorLink = document.querySelector('#grabDirectorSoloLink');
|
|
if (directorLink && session.streamID) {
|
|
const soloLink = soloLinkGenerator(session.streamID, true);
|
|
directorLink.dataset.raw = soloLink;
|
|
directorLink.href = soloLink;
|
|
directorLink.innerText = soloLink;
|
|
}
|
|
|
|
// Update solo links in control boxes
|
|
document.querySelectorAll('.soloLink').forEach(ele => {
|
|
if (ele.getAttribute('value')) {
|
|
const baseUrl = ele.getAttribute('value');
|
|
// Extract stream ID from the base URL
|
|
const match = baseUrl.match(/[?&]view=([^&]+)/);
|
|
if (match && match[1]) {
|
|
const streamId = match[1];
|
|
const soloLink = soloLinkGenerator(streamId, false);
|
|
ele.href = soloLink;
|
|
ele.innerHTML = soloLink;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update room settings (access mode, allowlist)
|
|
async function updateRoomSettings(roomId, settings) {
|
|
if (!session.authToken) {
|
|
console.error('Must be authenticated to update room settings');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/room/settings/${roomId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `Bearer ${session.authToken}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(settings)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
console.log('Room settings updated');
|
|
return data;
|
|
} else {
|
|
console.error('Failed to update room settings:', response.status);
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to update room settings:', e);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Get pending access requests for a room
|
|
async function getRoomAccessRequests(roomId) {
|
|
if (!session.authToken) {
|
|
console.error('Must be authenticated to get access requests');
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/room/requests/${roomId}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${session.authToken}`
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
return await response.json();
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to get access requests:', e);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
// Approve or deny an access request
|
|
async function handleAccessRequest(roomId, userId, action) {
|
|
if (!session.authToken) {
|
|
console.error('Must be authenticated to handle access requests');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${AUTH_SERVICE_URL}/api/room/request/${roomId}/${userId}/${action}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${session.authToken}`
|
|
}
|
|
});
|
|
|
|
return response.ok;
|
|
} catch (e) {
|
|
console.error('Failed to handle access request:', e);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Export functions for use in main VDO.Ninja code
|
|
window.vdoAuth = {
|
|
init: initAuthentication,
|
|
assignStream: assignAuthStream,
|
|
generateSignature: generateStreamSignature,
|
|
validateStream: validateStreamAuth,
|
|
resolveHandles: resolveViewHandles,
|
|
checkRoomAccess: checkRoomAccess,
|
|
joinRoom: joinRoomWithAuth,
|
|
startHeartbeat: startAuthHeartbeat,
|
|
getStreamKey: getStreamKey,
|
|
decryptStreamId: decryptStreamId,
|
|
resolveStream: resolveStream,
|
|
createUniversalToken: createUniversalToken,
|
|
updateRoomSettings: updateRoomSettings,
|
|
getRoomAccessRequests: getRoomAccessRequests,
|
|
handleAccessRequest: handleAccessRequest
|
|
};
|