Files
2025-02-18 14:07:52 +00:00

878 lines
32 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>User Management</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="AIOStremio">
<meta name="description" content="A private AIOStremio instance.
https://github.com/Viren070/AIOStremio">
<meta name="keywords" content="aiostremio">
<meta name="robots" content="noindex, nofollow">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="language" content="English">
<style>
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--text-color: #ffffff;
--input-bg: #333;
--border-color: #444;
--stats-bg: #242424;
--button-bg: #2563eb;
--button-hover: #1d4ed8;
--delete-button: #dc2626;
--delete-button-hover: #b91c1c;
--dropdown-bg: #242424;
--success-button: #059669;
--success-button-hover: #047857;
}
}
@media (prefers-color-scheme: light) {
:root {
--bg-color: #ffffff;
--text-color: #1a1a1a;
--input-bg: #ffffff;
--border-color: #e5e7eb;
--stats-bg: #f8f9fa;
--button-bg: #2563eb;
--button-hover: #1d4ed8;
--delete-button: #dc2626;
--delete-button-hover: #b91c1c;
--dropdown-bg: #ffffff;
--success-button: #059669;
--success-button-hover: #047857;
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: var(--bg-color);
color: var(--text-color);
line-height: 1.5;
transition: background-color 0.3s, color 0.3s;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
input, select {
padding: 0.75rem;
width: 100%;
max-width: 400px;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
background: var(--input-bg);
color: var(--text-color);
font-size: 1rem;
transition: border-color 0.3s;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.5rem 0;
}
.checkbox-group input[type="checkbox"] {
width: auto;
margin: 0;
cursor: pointer;
}
.checkbox-group label {
margin: 0;
cursor: pointer;
user-select: none;
font-weight: normal;
}
input:focus, select:focus {
outline: none;
border-color: var(--button-bg);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
button {
padding: 0.75rem 1.5rem;
background-color: var(--button-bg);
color: white;
border: none;
border-radius: 0.5rem;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: background-color 0.3s, transform 0.1s;
line-height: 1;
box-sizing: border-box;
}
button:hover {
background-color: var(--button-hover);
}
button:active {
transform: translateY(1px);
}
.stats {
margin: 2rem 0;
padding: 1.5rem;
background-color: var(--stats-bg);
border: 1px solid var(--border-color);
border-radius: 0.75rem;
}
.user-list {
margin-top: 3rem;
}
.user-list ul {
list-style: none;
padding: 0;
}
.user-list li {
padding: 1.5rem;
border: 1px solid var(--border-color);
border-radius: 0.75rem;
margin-bottom: 1rem;
background: var(--stats-bg);
}
.recent-history {
margin-top: 1rem;
padding: 0;
background: var(--bg-color);
border-radius: 0.75rem;
border: 1px solid var(--border-color);
display: none;
width: 100%;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.recent-history.show {
display: block;
animation: slideDown 0.2s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.history-list {
list-style: none;
padding: 1rem;
margin: 0;
max-height: 400px;
overflow-y: auto;
background: var(--bg-color);
scrollbar-width: thin;
scrollbar-color: var(--border-color) transparent;
}
.history-list::-webkit-scrollbar {
width: 6px;
}
.history-list::-webkit-scrollbar-track {
background: transparent;
}
.history-list::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 3px;
}
.history-toggle {
background: var(--button-bg);
color: white;
border: none;
border-radius: 0.5rem;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s ease;
flex: 1 1 0;
min-width: 150px;
margin: 0;
padding: 0.75rem 1.5rem;
}
.history-toggle:hover {
background-color: var(--button-hover);
}
.history-toggle:active {
transform: translateY(1px);
}
.history-item {
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
font-size: 0.9rem;
transition: background-color 0.2s ease;
}
.history-item:last-child {
margin-bottom: 0;
}
.history-item:hover {
background-color: var(--stats-bg);
}
.history-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
}
.history-title {
font-weight: 500;
color: var(--text-color);
flex: 1;
}
.history-episode {
background: var(--button-bg);
color: white;
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.02em;
margin-left: auto;
flex-shrink: 0;
}
.history-time {
color: var(--text-color);
opacity: 0.6;
font-size: 0.8rem;
margin-top: 0.25rem;
display: block;
}
.username {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--text-color);
display: block;
}
.user-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.user-actions > button,
.user-actions > .services-dropdown > button {
flex: 1 1 0;
min-width: 120px;
height: 42px;
padding: 0 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
/* History and addon buttons */
.user-actions > [id^="history-btn-"],
.user-actions > .services-dropdown {
flex-basis: calc(50% - 0.25rem);
order: 2;
margin-top: 0.5rem;
}
/* Main action buttons */
.user-actions > button:not([id^="history-btn-"]):not(:last-child) {
order: 1;
}
.user-actions > button:last-child {
order: 1;
}
.services-dropdown {
position: relative;
display: inline-block;
margin: 0;
flex: 1 1 0;
}
.services-dropdown button {
width: 100%;
}
.manage-user-btn {
display: none;
}
@media (max-width: 768px) {
.user-list li {
display: block;
}
.user-actions {
display: none;
}
.user-actions.show {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1rem;
}
.user-actions > button,
.user-actions > .services-dropdown {
flex: 1 1 auto;
min-width: 100%;
order: unset;
margin-top: 0;
flex-basis: auto;
height: 42px;
}
.user-actions > button {
min-height: 42px;
display: flex;
align-items: center;
justify-content: center;
}
.username {
text-align: center;
margin-bottom: 1.25rem;
}
.manage-user-btn {
display: flex !important;
width: 100%;
margin-top: 1rem;
background-color: var(--button-bg);
color: white;
border: none;
border-radius: 0.5rem;
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
align-items: center;
justify-content: center;
}
.manage-user-btn:hover {
background-color: var(--button-hover);
}
}
.services-content {
display: none;
position: absolute;
background-color: var(--dropdown-bg);
min-width: 200px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
border-radius: 0.5rem;
padding: 1rem;
z-index: 1;
border: 1px solid var(--border-color);
right: 0;
}
.services-content.show {
display: block;
}
.services-content label {
display: flex;
align-items: center;
margin: 0.5rem 0;
cursor: pointer;
}
.services-content input[type="checkbox"] {
width: auto;
margin-right: 0.5rem;
}
button[style*="background-color: #dc3545"] {
background-color: var(--delete-button) !important;
}
button[style*="background-color: #dc3545"]:hover {
background-color: var(--delete-button-hover) !important;
}
button[style*="background-color: #28a745"] {
background-color: var(--success-button) !important;
}
button[style*="background-color: #28a745"]:hover {
background-color: var(--success-button-hover) !important;
}
@media (max-width: 480px) {
body {
padding: 1rem;
}
.services-content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 300px;
max-height: 80vh;
overflow-y: auto;
}
}
</style>
</head>
<body>
<h1>User Management</h1>
<form id="userForm">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label>Settings:</label>
<div class="checkbox-group">
<input type="checkbox" id="proxy_streams" name="proxy_streams" checked>
<label for="proxy_streams">Enable Proxy Streams</label>
<small style="display: block; color: var(--text-color); opacity: 0.7; margin-left: 1.8rem;">Encrypts URLs and proxies the stream to hide API keys and bypass IP restrictions</small>
</div>
<div class="checkbox-group">
<input type="checkbox" id="vidi_mode" name="vidi_mode">
<label for="vidi_mode">Enable Vidi Mode</label>
<small style="display: block; color: var(--text-color); opacity: 0.7; margin-left: 1.8rem;">Shows the addon name in the description</small>
</div>
<div class="checkbox-group">
<input type="checkbox" id="simple_format" name="simple_format">
<label for="simple_format">Enable Simple Format</label>
<small style="display: block; color: var(--text-color); opacity: 0.7; margin-left: 1.8rem;">Hides confusing torrent filenames and shows a easy to understand description</small>
</div>
<div class="checkbox-group">
<input type="checkbox" id="one_per_quality" name="one_per_quality">
<label for="one_per_quality">Best Per Resolution</label>
<small style="display: block; color: var(--text-color); opacity: 0.7; margin-left: 1.8rem;">Shows the best available stream for each resolution and hides the rest</small>
</div>
<div class="checkbox-group">
<input type="checkbox" id="cached_only" name="cached_only">
<label for="cached_only">Cached Content Only</label>
<small style="display: block; color: var(--text-color); opacity: 0.7; margin-left: 1.8rem;">Shows only cached content that is immediately available</small>
</div>
</div>
<button type="submit">Add User</button>
</form>
<div class="user-list">
<ul>
{% for username, details in users.items() %}
<li data-username="{{ username }}">
<span class="username">{{ username }}{% if user_last_active[username] != "never" %} (Active {{ user_last_active[username] }}){% endif %}</span>
<button class="manage-user-btn" onclick="toggleUserActions('{{ username }}')">Manage User</button>
<div class="user-actions">
{% if user_histories[username] %}
<button onclick="toggleHistory('{{ username }}')" id="history-btn-{{ username }}">
Show History
</button>
{% endif %}
<button onclick="toggleProxy('{{ username }}')" id="proxy-btn-{{ username }}">
Proxy: {{ 'On' if details.proxy_streams else 'Off' }}
</button>
<button onclick="toggleVidiMode('{{ username }}')" id="vidi-btn-{{ username }}">
Vidi Mode: {{ 'On' if details.vidi_mode else 'Off' }}
</button>
<button onclick="toggleSimpleFormat('{{ username }}')" id="simple-btn-{{ username }}">
Simple Format: {{ 'On' if details.simple_format else 'Off' }}
</button>
<button onclick="toggleOnePerQuality('{{ username }}')" id="quality-btn-{{ username }}">
Best Per Resolution: {{ 'On' if details.one_per_quality else 'Off' }}
</button>
<button onclick="toggleCachedOnly('{{ username }}')" id="cached-btn-{{ username }}">
Cached Only: {{ 'On' if details.cached_only else 'Off' }}
</button>
<button onclick="deleteUser('{{ username }}')" style="background-color: #dc3545;">Delete User</button>
<div class="services-dropdown">
<button type="button" onclick="toggleServices('{{ username }}')" style="background-color: #28a745;">Toggle Addons</button>
<div id="services-{{ username }}" class="services-content">
<form id="services-form-{{ username }}" onsubmit="return false;">
<div id="services-checkboxes-{{ username }}">
<!-- Services will be populated by JavaScript -->
<p>Loading services...</p>
</div>
<button type="submit" style="margin-top: 10px;">Save Services</button>
</form>
</div>
</div>
</div>
{% if user_histories[username] %}
<div class="recent-history" id="history-{{ username }}">
<ul class="history-list">
{% for entry in user_histories[username][:25] %}
<li class="history-item">
<div class="history-title-row">
<span class="history-title">
{{ entry.get('title', 'Unknown Title') }}
{% if entry.get('type') == 'series' and entry.get('season') and entry.get('episode') %}
<span class="history-episode">S{{ entry.get('season') }}E{{ entry.get('episode') }}</span>
{% elif entry.get('type') == 'movie' %}
<span class="history-episode">MOVIE</span>
{% endif %}
</span>
</div>
<span class="history-time">{{ entry.get('timestamp', 'Unknown') }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
<script>
let availableServices = [];
// Fetch available services when page loads
async function fetchAvailableServices() {
try {
const response = await fetch('/admin/available_services');
if (!response.ok) {
throw new Error(`Failed to fetch services: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.log('Available services response:', data);
if (!data.services || !Array.isArray(data.services)) {
console.error('Invalid services data format:', data);
throw new Error('Invalid services data format received from server');
}
availableServices = data.services;
console.log('Processed available services:', availableServices);
if (availableServices.length === 0) {
console.warn('No services available from server');
}
// Initialize service checkboxes for all users
const users = document.querySelectorAll('.services-dropdown');
console.log('Found users to process:', users.length);
users.forEach(user => {
const userElement = user.closest('li');
if (!userElement) {
console.warn('Could not find li element for user dropdown');
return;
}
const username = userElement.getAttribute('data-username');
if (!username) {
console.warn('Could not find username in data attribute');
return;
}
console.log('Processing services for user:', username);
populateServiceCheckboxes(username);
});
} catch (error) {
console.error('Error in fetchAvailableServices:', error);
// Display error in UI
const users = document.querySelectorAll('.services-dropdown');
users.forEach(user => {
const container = user.querySelector('.services-content');
if (container) {
container.innerHTML = `<p style="color: red;">Error loading services: ${error.message}</p>`;
}
});
}
}
function populateServiceCheckboxes(username) {
const container = document.getElementById(`services-checkboxes-${username}`);
if (!container) {
console.error(`Container lookup failed for user ${username}. Full element ID: services-checkboxes-${username}`);
return;
}
fetch(`/admin/user_services/${username}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
const userServices = data.enabled_services || [];
if (availableServices.length === 0) {
container.innerHTML = '<p>No services available</p>';
return;
}
container.innerHTML = `
<p style="font-style: italic; color: #666; margin-bottom: 8px;">Note: If no addons are selected they will all be enabled.</p>
${availableServices.map(service => `
<label>
<input type="checkbox" name="services" value="${service}"
${userServices.includes(service) ? 'checked' : ''}>
${service}
</label>
`).join('')}`;
})
.catch(error => {
console.error(`Error loading services for ${username}:`, error);
container.innerHTML = `<p style="color: red;">Error loading services: ${error.message}</p>`;
});
}
function toggleServices(username) {
const dropdown = document.getElementById(`services-${username}`);
dropdown.classList.toggle('show');
}
window.onclick = function(event) {
if (!event.target.matches('.services-dropdown button') &&
!event.target.matches('.services-content *')) {
const dropdowns = document.getElementsByClassName('services-content');
for (const dropdown of dropdowns) {
if (dropdown.classList.contains('show')) {
dropdown.classList.remove('show');
}
}
}
}
document.addEventListener('submit', async function(e) {
if (e.target.id.startsWith('services-form-')) {
e.preventDefault();
const username = e.target.id.replace('services-form-', '');
const formData = new FormData(e.target);
const services = formData.getAll('services');
try {
const response = await fetch(`/admin/update_services/${username}`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.status === 'success') {
alert('Services updated successfully');
toggleServices(username); // Close the dropdown
} else {
throw new Error(result.message || 'Failed to update services');
}
} catch (error) {
console.error(`Error updating services for ${username}:`, error);
alert(`Error updating services: ${error.message}`);
}
}
});
fetchAvailableServices();
document.getElementById('userForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('/admin/add_user', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
alert(result.message);
if (result.status === 'success') {
location.reload();
}
} else {
alert(result.detail || 'Error adding user');
}
} catch (error) {
alert('Network error occurred while adding user');
}
});
async function toggleProxy(username) {
try {
const response = await fetch(`/admin/toggle_proxy/${username}`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.status === 'success') {
const proxyButton = document.getElementById(`proxy-btn-${username}`);
proxyButton.textContent = `Proxy: ${result.proxy_streams ? 'On' : 'Off'}`;
} else {
alert(result.detail || 'Error toggling proxy setting');
}
} catch (error) {
console.error(`Error toggling proxy for ${username}:`, error);
alert('Network error occurred while toggling proxy setting');
}
}
async function toggleVidiMode(username) {
try {
const response = await fetch(`/admin/toggle_vidi_mode/${username}`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.status === 'success') {
const vidiButton = document.getElementById(`vidi-btn-${username}`);
vidiButton.textContent = `Vidi Mode: ${result.vidi_mode ? 'On' : 'Off'}`;
} else {
alert(result.detail || 'Error toggling Vidi mode');
}
} catch (error) {
console.error(`Error toggling Vidi mode for ${username}:`, error);
alert('Network error occurred while toggling Vidi mode');
}
}
async function toggleSimpleFormat(username) {
try {
const response = await fetch(`/admin/toggle_simple_format/${username}`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.status === 'success') {
const simpleButton = document.getElementById(`simple-btn-${username}`);
simpleButton.textContent = `Simple Format: ${result.simple_format ? 'On' : 'Off'}`;
} else {
alert(result.detail || 'Error toggling simple format');
}
} catch (error) {
console.error(`Error toggling simple format for ${username}:`, error);
alert('Network error occurred while toggling simple format');
}
}
async function toggleOnePerQuality(username) {
try {
const response = await fetch(`/admin/toggle_one_per_quality/${username}`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.status === 'success') {
const qualityButton = document.getElementById(`quality-btn-${username}`);
qualityButton.textContent = `Best Per Resolution: ${result.one_per_quality ? 'On' : 'Off'}`;
} else {
alert(result.detail || 'Error toggling quality setting');
}
} catch (error) {
console.error(`Error toggling one per quality for ${username}:`, error);
alert('Network error occurred while toggling quality setting');
}
}
async function toggleCachedOnly(username) {
try {
const response = await fetch(`/admin/toggle_cached_only/${username}`, {
method: 'POST'
});
const result = await response.json();
if (response.ok && result.status === 'success') {
const cachedButton = document.getElementById(`cached-btn-${username}`);
cachedButton.textContent = `Cached Only: ${result.cached_only ? 'On' : 'Off'}`;
} else {
alert(result.detail || 'Error toggling cached only setting');
}
} catch (error) {
console.error(`Error toggling cached only for ${username}:`, error);
alert('Network error occurred while toggling cached only setting');
}
}
async function deleteUser(username) {
if (!confirm(`Are you sure you want to delete user "${username}"?`)) {
return;
}
try {
const response = await fetch(`/admin/delete_user/${username}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok && result.status === 'success') {
alert('User deleted successfully');
location.reload();
} else {
alert(result.detail || 'Failed to delete user');
}
} catch (error) {
console.error(`Error deleting user ${username}:`, error);
alert('Network error occurred while deleting user');
}
}
function toggleHistory(username) {
const historyDiv = document.getElementById(`history-${username}`);
const toggleButton = document.getElementById(`history-btn-${username}`);
if (historyDiv.classList.contains('show')) {
historyDiv.classList.remove('show');
toggleButton.textContent = 'Show History';
} else {
historyDiv.classList.add('show');
toggleButton.textContent = 'Hide History';
}
}
function toggleUserActions(username) {
const userActions = document.querySelector(`li[data-username="${username}"] .user-actions`);
const manageButton = document.querySelector(`li[data-username="${username}"] .manage-user-btn`);
if (userActions.classList.contains('show')) {
userActions.classList.remove('show');
manageButton.textContent = 'Manage User';
} else {
userActions.classList.add('show');
manageButton.textContent = 'Hide Actions';
}
}
</script>
</body>
</html>