Replace master branch with local files
This commit is contained in:
commit
875a53f499
60 changed files with 21637 additions and 0 deletions
36
frontend/src/lib/api.js
Normal file
36
frontend/src/lib/api.js
Normal 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
99
frontend/src/lib/pgp.js
Normal 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');
|
||||
}
|
||||
128
frontend/src/lib/stores/auth.js
Normal file
128
frontend/src/lib/stores/auth.js
Normal 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
|
||||
);
|
||||
56
frontend/src/lib/websocket.js
Normal file
56
frontend/src/lib/websocket.js
Normal 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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue