implement authentication and authorization
This commit is contained in:
@@ -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}`)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user