Replace master branch with local files

This commit is contained in:
doomtube 2025-08-03 21:53:15 -04:00
commit 875a53f499
60 changed files with 21637 additions and 0 deletions

36
frontend/src/lib/api.js Normal file
View file

@ -0,0 +1,36 @@
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost/api';
async function fetchAPI(endpoint, options = {}) {
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
credentials: 'include', // Always include credentials
});
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
return response.json();
}
export async function getStreamKey() {
return fetchAPI('/stream/key');
}
export async function regenerateStreamKey() {
return fetchAPI('/stream/key/regenerate', {
method: 'POST',
});
}
export async function validateStreamKey(key) {
return fetchAPI(`/stream/validate/${key}`);
}
export async function getStreamStats(streamKey) {
return fetchAPI(`/stream/stats/${streamKey}`);
}

99
frontend/src/lib/pgp.js Normal file
View file

@ -0,0 +1,99 @@
// Client-side PGP utilities - wraps openpgp for browser-only usage
export async function generateKeyPair(username, passphrase = '') {
if (typeof window === 'undefined') {
throw new Error('PGP operations can only be performed in the browser');
}
const { generateKey, readKey } = await import('openpgp');
const { privateKey, publicKey } = await generateKey({
type: 'rsa',
rsaBits: 2048,
userIDs: [{ name: username }],
passphrase
});
const key = await readKey({ armoredKey: publicKey });
const fingerprint = key.getFingerprint();
return {
privateKey,
publicKey,
fingerprint
};
}
export async function getFingerprint(publicKey) {
if (typeof window === 'undefined') return null;
try {
const { readKey } = await import('openpgp');
const key = await readKey({ armoredKey: publicKey });
return key.getFingerprint();
} catch (error) {
console.error('Error getting fingerprint:', error);
return null;
}
}
export async function signMessage(message, privateKeyArmored, passphrase = '') {
if (typeof window === 'undefined') {
throw new Error('PGP operations can only be performed in the browser');
}
const { decryptKey, readPrivateKey, createMessage, sign } = await import('openpgp');
const privateKey = await decryptKey({
privateKey: await readPrivateKey({ armoredKey: privateKeyArmored }),
passphrase
});
const unsignedMessage = await createMessage({ text: message });
const signature = await sign({
message: unsignedMessage,
signingKeys: privateKey,
detached: true
});
return signature;
}
export async function verifySignature(message, signature, publicKeyArmored) {
if (typeof window === 'undefined') return false;
try {
const { readKey, readSignature, createMessage, verify } = await import('openpgp');
const publicKey = await readKey({ armoredKey: publicKeyArmored });
const signatureObj = await readSignature({ armoredSignature: signature });
const messageObj = await createMessage({ text: message });
const verificationResult = await verify({
message: messageObj,
signature: signatureObj,
verificationKeys: publicKey
});
const { verified } = verificationResult.signatures[0];
return await verified;
} catch (error) {
console.error('Signature verification error:', error);
return false;
}
}
export function storePrivateKey(privateKey) {
if (typeof window === 'undefined') return;
localStorage.setItem('pgp_private_key', privateKey);
}
export function getStoredPrivateKey() {
if (typeof window === 'undefined') return null;
return localStorage.getItem('pgp_private_key');
}
export function removeStoredPrivateKey() {
if (typeof window === 'undefined') return;
localStorage.removeItem('pgp_private_key');
}

View file

@ -0,0 +1,128 @@
import { writable, derived } from 'svelte/store';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
function createAuthStore() {
const { subscribe, set, update } = writable({
user: null,
token: null,
loading: true
});
return {
subscribe,
async init() {
if (!browser) return;
const token = localStorage.getItem('auth_token');
if (!token) {
set({ user: null, token: null, loading: false });
return;
}
try {
const response = await fetch('/api/user/me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
set({ user: data.user, token, loading: false });
} else {
localStorage.removeItem('auth_token');
set({ user: null, token: null, loading: false });
}
} catch (error) {
console.error('Auth init error:', error);
set({ user: null, token: null, loading: false });
}
},
async login(credentials) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const data = await response.json();
if (response.ok && data.success) {
localStorage.setItem('auth_token', data.token);
set({ user: data.user, token: data.token, loading: false });
goto('/');
return { success: true };
}
return { success: false, error: data.error || 'Invalid credentials' };
},
async loginWithPgp(username, signature, challenge) {
const response = await fetch('/api/auth/pgp-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, signature, challenge })
});
const data = await response.json();
if (response.ok && data.success) {
localStorage.setItem('auth_token', data.token);
set({ user: data.user, token: data.token, loading: false });
goto('/');
return { success: true };
}
return { success: false, error: data.error || 'Invalid signature' };
},
async register(userData) {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
const data = await response.json();
if (response.ok && data.success) {
return { success: true, userId: data.userId };
}
return { success: false, error: data.error || 'Registration failed' };
},
updateUser(userData) {
update(state => ({
...state,
user: userData
}));
},
logout() {
localStorage.removeItem('auth_token');
set({ user: null, token: null, loading: false });
goto('/login');
}
};
}
export const auth = createAuthStore();
export const isAuthenticated = derived(
auth,
$auth => !!$auth.user
);
export const isAdmin = derived(
auth,
$auth => $auth.user?.isAdmin || false
);
export const isStreamer = derived(
auth,
$auth => $auth.user?.isStreamer || false
);

View file

@ -0,0 +1,56 @@
let ws = null;
let reconnectTimeout = null;
const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost/ws';
export function connectWebSocket(onMessage) {
if (ws?.readyState === WebSocket.OPEN) return;
// WebSocket doesn't support withCredentials, but cookies are sent automatically
// on same-origin requests
ws = new WebSocket(`${WS_URL}/stream`);
ws.onopen = () => {
console.log('WebSocket connected');
ws?.send(JSON.stringify({ type: 'subscribe' }));
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
onMessage(data);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('WebSocket disconnected');
// Reconnect after 5 seconds
reconnectTimeout = setTimeout(() => {
connectWebSocket(onMessage);
}, 5000);
};
}
export function disconnectWebSocket() {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
if (ws) {
ws.close();
ws = null;
}
}
export function sendMessage(message) {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
}