implement authentication and authorization

This commit is contained in:
Yuri
2026-01-01 15:36:38 -05:00
parent 46c68813ec
commit e1a197f2be
16 changed files with 1704 additions and 2 deletions
+20
View File
@@ -13,6 +13,12 @@ const API = {
'Content-Type': 'application/json'
}
};
// Add authentication token if available
const token = localStorage.getItem('authToken');
if (token) {
options.headers['Authorization'] = `Bearer ${token}`;
}
if (data) {
options.body = JSON.stringify(data);
@@ -30,6 +36,12 @@ const API = {
}
if (!response.ok) {
// If unauthorized, redirect to login
if (response.status === 401) {
localStorage.removeItem('authToken');
window.location.href = '/login.html';
return;
}
throw new Error(result.error || `Server responded with ${response.status}`);
}
@@ -120,6 +132,14 @@ const API = {
update: (data) => API.request('PUT', '/settings', data),
reset: () => API.request('DELETE', '/settings'),
getDefaults: () => API.request('GET', '/settings/defaults')
},
// Users (admin only)
users: {
getAll: () => API.request('GET', '/auth/users'),
create: (data) => API.request('POST', '/auth/users', data),
update: (id, data) => API.request('PUT', `/auth/users/${id}`, data),
delete: (id) => API.request('DELETE', `/auth/users/${id}`)
}
};
+80
View File
@@ -6,6 +6,7 @@ class App {
constructor() {
this.currentPage = 'home';
this.pages = {};
this.currentUser = null;
// Initialize components
this.player = new VideoPlayer();
@@ -24,6 +25,9 @@ class App {
}
async init() {
// Check authentication first
await this.checkAuth();
// Mobile menu toggle
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const navbarMenu = document.getElementById('navbar-menu');
@@ -110,6 +114,82 @@ class App {
console.log('NodeCast TV initialized');
}
async checkAuth() {
const token = localStorage.getItem('authToken');
if (!token) {
// No token, redirect to login
window.location.href = '/login.html';
return;
}
try {
// Verify token with server
const response = await fetch('/api/auth/me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Invalid token');
}
this.currentUser = await response.json();
// Hide settings for viewers
if (this.currentUser.role === 'viewer') {
const settingsLink = document.querySelector('.nav-link[data-page="settings"]');
if (settingsLink) {
settingsLink.style.display = 'none';
}
}
// Add logout button to navbar
this.addLogoutButton();
} catch (err) {
console.error('Authentication error:', err);
localStorage.removeItem('authToken');
window.location.href = '/login.html';
}
}
addLogoutButton() {
const navbar = document.querySelector('.navbar-menu');
if (!navbar || document.getElementById('logout-btn')) return;
const logoutLink = document.createElement('a');
logoutLink.href = '#';
logoutLink.className = 'nav-link';
logoutLink.id = 'logout-btn';
logoutLink.innerHTML = `
<span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/>
</svg></span>
<span>Logout</span>
`;
logoutLink.addEventListener('click', async (e) => {
e.preventDefault();
const token = localStorage.getItem('authToken');
if (token) {
await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
}
localStorage.removeItem('authToken');
window.location.href = '/login.html';
});
navbar.appendChild(logoutLink);
}
navigateTo(pageName) {
// Update nav
document.querySelectorAll('.nav-link').forEach(link => {
+144
View File
@@ -0,0 +1,144 @@
/**
* Auth Manager - Frontend authentication state management
*/
const Auth = {
currentUser: null,
/**
* Initialize auth - check setup status and current user
*/
async init() {
try {
// Check if setup is required
const setupStatus = await API.auth.checkSetup();
if (setupStatus.setupRequired) {
this.showSetup();
return false;
}
// Check if user is logged in
if (API.getToken()) {
try {
this.currentUser = await API.auth.me();
return true;
} catch (error) {
// Token invalid, clear it
console.log('Token invalid, showing login');
API.setToken(null);
this.showLogin();
return false;
}
} else {
this.showLogin();
return false;
}
} catch (error) {
console.error('Auth initialization failed:', error);
// On any error, show login (don't loop)
this.showLogin();
return false;
}
},
/**
* Show setup screen
*/
showSetup() {
document.getElementById('setup-screen').classList.add('active');
document.getElementById('login-screen').classList.remove('active');
document.getElementById('app').classList.remove('active');
},
/**
* Show login screen
*/
showLogin() {
document.getElementById('setup-screen').classList.remove('active');
document.getElementById('login-screen').classList.add('active');
document.getElementById('app').classList.remove('active');
},
/**
* Show main app
*/
showApp() {
document.getElementById('setup-screen').classList.remove('active');
document.getElementById('login-screen').classList.remove('active');
document.getElementById('app').classList.add('active');
// Hide settings tab if viewer
if (!this.isAdmin()) {
const settingsLink = document.querySelector('[data-page="settings"]');
if (settingsLink) {
settingsLink.parentElement.style.display = 'none';
}
}
},
/**
* Setup initial admin user
*/
async setup(username, password) {
try {
const result = await API.auth.setup(username, password);
API.setToken(result.token);
this.currentUser = result.user;
this.showApp();
return true;
} catch (error) {
throw error;
}
},
/**
* Login user
*/
async login(username, password) {
try {
const result = await API.auth.login(username, password);
API.setToken(result.token);
this.currentUser = result.user;
this.showApp();
return true;
} catch (error) {
throw error;
}
},
/**
* Logout user
*/
async logout() {
try {
await API.auth.logout();
} catch (error) {
console.error('Logout error:', error);
} finally {
API.setToken(null);
this.currentUser = null;
this.showLogin();
}
},
/**
* Check if current user is admin
*/
isAdmin() {
return this.currentUser && this.currentUser.role === 'admin';
},
/**
* Check if current user is viewer
*/
isViewer() {
return this.currentUser && this.currentUser.role === 'viewer';
},
/**
* Get current user
*/
getCurrentUser() {
return this.currentUser;
}
};
+5
View File
@@ -3,6 +3,11 @@
* Handles HLS video playback with custom controls
*/
// Check if device is mobile
function isMobile() {
return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
class VideoPlayer {
constructor() {
this.video = document.getElementById('video-player');
+14
View File
@@ -0,0 +1,14 @@
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const response = await API.request('POST', '/auth/login', { username, password });
localStorage.setItem('sessionToken', response.token);
window.location.href = '/';
} catch (err) {
document.getElementById('login-error').textContent = 'Login failed: ' + err.message;
}
});
+109
View File
@@ -19,6 +19,9 @@ class SettingsPage {
// Player settings
this.initPlayerSettings();
// User management (admin only)
this.initUserManagement();
}
initPlayerSettings() {
@@ -130,6 +133,99 @@ class SettingsPage {
}
}
initUserManagement() {
// User tab visibility is handled in show() method
// when currentUser is available
// Handle add user form
const addUserForm = document.getElementById('add-user-form');
if (addUserForm) {
addUserForm.addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('new-username').value;
const password = document.getElementById('new-password').value;
const role = document.getElementById('new-role').value;
try {
await API.users.create({ username, password, role });
alert('User created successfully!');
addUserForm.reset();
this.loadUsers();
} catch (err) {
alert('Error creating user: ' + err.message);
}
});
}
}
async loadUsers() {
const userList = document.getElementById('user-list');
if (!userList) return;
try {
const users = await API.users.getAll();
if (users.length === 0) {
userList.innerHTML = '<tr><td colspan="4" class="hint">No users found</td></tr>';
return;
}
userList.innerHTML = users.map(user => `
<tr>
<td>${user.username}</td>
<td><span class="badge badge-${user.role === 'admin' ? 'primary' : 'secondary'}">${user.role}</span></td>
<td>${user.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="window.app.pages.settings.editUser(${user.id})">Edit</button>
<button class="btn btn-sm btn-error" onclick="window.app.pages.settings.deleteUser(${user.id}, '${user.username}')">Delete</button>
</td>
</tr>
`).join('');
} catch (err) {
console.error('Error loading users:', err);
userList.innerHTML = '<tr><td colspan="4" class="hint">Error loading users</td></tr>';
}
}
async editUser(userId) {
const username = prompt('Enter new username (leave blank to keep current):');
const password = prompt('Enter new password (leave blank to keep current):');
const role = prompt('Enter role (admin or viewer, leave blank to keep current):');
const updates = {};
if (username) updates.username = username;
if (password) updates.password = password;
if (role) updates.role = role;
if (Object.keys(updates).length === 0) {
alert('No changes made');
return;
}
try {
await API.users.update(userId, updates);
alert('User updated successfully!');
this.loadUsers();
} catch (err) {
alert('Error updating user: ' + err.message);
}
}
async deleteUser(userId, username) {
if (!confirm(`Are you sure you want to delete user "${username}"?`)) {
return;
}
try {
await API.users.delete(userId);
alert('User deleted successfully!');
this.loadUsers();
} catch (err) {
alert('Error deleting user: ' + err.message);
}
}
switchTab(tabName) {
this.tabs.forEach(t => t.classList.toggle('active', t.dataset.tab === tabName));
this.tabContents.forEach(c => c.classList.toggle('active', c.id === `tab-${tabName}`));
@@ -138,9 +234,22 @@ class SettingsPage {
if (tabName === 'content') {
this.app.sourceManager.loadContentSources();
}
// Load users when switching to users tab
if (tabName === 'users') {
this.loadUsers();
}
}
async show() {
// Show users tab for admin
if (this.app.currentUser && this.app.currentUser.role === 'admin') {
const usersTab = document.getElementById('users-tab');
if (usersTab) {
usersTab.style.display = 'block';
}
}
// Load sources when page is shown
await this.app.sourceManager.loadSources();