902 lines
38 KiB
Python
902 lines
38 KiB
Python
"""
|
|
Admin-Templates für User-Verwaltung
|
|
"""
|
|
|
|
ADMIN_ANALYTICS_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Usage Analytics</title>
|
|
<link rel="stylesheet" href="/static/styles.css">
|
|
<style>
|
|
.analytics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.analytics-card {
|
|
background: var(--bg-secondary);
|
|
border: 2px solid var(--border-main);
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
}
|
|
.analytics-card h3 {
|
|
margin-top: 0;
|
|
color: var(--primary);
|
|
font-size: 1.1rem;
|
|
}
|
|
.analytics-card .metric {
|
|
font-size: 2.5rem;
|
|
font-weight: bold;
|
|
color: var(--text-primary);
|
|
margin: 10px 0;
|
|
}
|
|
.analytics-card .label {
|
|
color: var(--text-secondary);
|
|
font-size: 0.9rem;
|
|
}
|
|
.client-section {
|
|
background: var(--bg-secondary);
|
|
border: 2px solid var(--border-main);
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.client-section h3 {
|
|
margin-top: 0;
|
|
color: var(--primary);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.user-badge {
|
|
display: inline-block;
|
|
background: var(--primary);
|
|
color: white;
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 0.85rem;
|
|
margin: 4px;
|
|
}
|
|
.session-item {
|
|
padding: 12px;
|
|
border-bottom: 1px solid var(--border-main);
|
|
}
|
|
.session-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.session-info {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.session-meta {
|
|
color: var(--text-secondary);
|
|
font-size: 0.85rem;
|
|
margin-top: 4px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
|
</svg>
|
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<h1>Usage Analytics</h1>
|
|
<p>Active sessions and client usage statistics</p>
|
|
</header>
|
|
|
|
<div class="controls">
|
|
<a href="/admin/users" style="text-decoration: none;">
|
|
<button>Manage Users</button>
|
|
</a>
|
|
<a href="/admin/clients" style="text-decoration: none;">
|
|
<button>Manage Clients</button>
|
|
</a>
|
|
<a href="/admin/logout" style="text-decoration: none;">
|
|
<button class="danger">Logout</button>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Summary Stats -->
|
|
<div class="analytics-grid">
|
|
<div class="analytics-card">
|
|
<h3>Active Users</h3>
|
|
<div class="metric">{{ summary.total_active_users }}</div>
|
|
<div class="label">Users with active sessions</div>
|
|
</div>
|
|
<div class="analytics-card">
|
|
<h3>Active Tokens</h3>
|
|
<div class="metric">{{ summary.total_active_tokens }}</div>
|
|
<div class="label">Total valid access tokens</div>
|
|
</div>
|
|
<div class="analytics-card">
|
|
<h3>Clients in Use</h3>
|
|
<div class="metric">{{ summary.total_clients_in_use }}</div>
|
|
<div class="label">Applications being accessed</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Client Usage Summary -->
|
|
<h2 style="margin-top: 40px; margin-bottom: 20px;">Usage by Client</h2>
|
|
{% if by_client %}
|
|
{% for client in by_client %}
|
|
<div class="client-section">
|
|
<h3>
|
|
<span>{{ client.client_name }}</span>
|
|
<span class="status-badge status-available">{{ client.active_users_count }} active users</span>
|
|
</h3>
|
|
<div style="color: var(--text-secondary); margin-top: 8px;">
|
|
<strong>Client ID:</strong> <code>{{ client.client_id }}</code><br>
|
|
<strong>Total Tokens:</strong> {{ client.total_tokens }}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="import-results" style="background: var(--bg-secondary);">
|
|
No active sessions found.
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Detailed Sessions -->
|
|
<h2 style="margin-top: 40px; margin-bottom: 20px;">Active Sessions Detail</h2>
|
|
|
|
{% if detailed_sessions %}
|
|
{% set current_client = namespace(value='') %}
|
|
{% for session in detailed_sessions %}
|
|
{% if session.client_name != current_client.value %}
|
|
{% set current_client.value = session.client_name %}
|
|
{% if not loop.first %}
|
|
</div>
|
|
{% endif %}
|
|
<div class="client-section">
|
|
<h3>{{ session.client_name }}</h3>
|
|
{% endif %}
|
|
|
|
<div class="session-item">
|
|
<div class="session-info">
|
|
<div>
|
|
<strong>{{ session.username }}</strong> ({{ session.email }})
|
|
</div>
|
|
<span class="status-badge status-available">Active</span>
|
|
</div>
|
|
<div class="session-meta">
|
|
Created: {{ session.created_at.strftime('%Y-%m-%d %H:%M:%S') }} |
|
|
Expires: {{ session.expires_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
|
</div>
|
|
</div>
|
|
|
|
{% if loop.last %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="import-results" style="background: var(--bg-secondary);">
|
|
No active sessions to display.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script>
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('dark-mode');
|
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
|
}
|
|
|
|
if (localStorage.getItem('darkMode') === 'true') {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
|
|
// Auto-refresh every 30 seconds
|
|
setTimeout(function() {
|
|
location.reload();
|
|
}, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
ADMIN_LOGIN_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin Login</title>
|
|
<link rel="stylesheet" href="/static/styles.css">
|
|
</head>
|
|
<body>
|
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
|
</svg>
|
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<h1>Admin Login</h1>
|
|
<p>User Administration Access</p>
|
|
</header>
|
|
|
|
<div class="modal-content" style="max-width: 450px; margin: 0 auto;">
|
|
{% if error %}
|
|
<div class="import-results error">
|
|
<strong>Error:</strong> {{ error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="POST" style="margin-top: 24px;">
|
|
<div class="form-group">
|
|
<label for="username">Admin Username</label>
|
|
<input type="text" id="username" name="username" placeholder="Enter admin username" required autofocus>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">Password</label>
|
|
<input type="password" id="password" name="password" placeholder="Enter password" required>
|
|
</div>
|
|
|
|
<button type="submit" style="width: 100%; margin-top: 8px;">Admin Login</button>
|
|
</form>
|
|
|
|
<div style="text-align: center; margin-top: 24px; padding-top: 24px; border-top: 2px solid var(--border-main);">
|
|
<a href="/" style="color: var(--primary); text-decoration: none; font-weight: 600;">← Back to Home</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('dark-mode');
|
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
|
}
|
|
|
|
if (localStorage.getItem('darkMode') === 'true') {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
ADMIN_DASHBOARD_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>User Administration</title>
|
|
<link rel="stylesheet" href="/static/styles.css">
|
|
</head>
|
|
<body>
|
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
|
</svg>
|
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<h1>User Administration</h1>
|
|
<p>Manage OIDC users - Logged in as: <strong>{{ admin_user.username }}</strong></p>
|
|
</header>
|
|
|
|
{% if message %}
|
|
<div class="import-results success" style="max-width: 100%; margin-bottom: 20px;">
|
|
{{ message }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<h3>Total Users</h3>
|
|
<div class="value">{{ total_users }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Active Users</h3>
|
|
<div class="value value.level-low">{{ active_users }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Admin Users</h3>
|
|
<div class="value">{{ admin_users }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>Inactive Users</h3>
|
|
<div class="value value.level-medium">{{ inactive_users }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<a href="/admin/user/create" style="text-decoration: none;">
|
|
<button class="secondary">Create New User</button>
|
|
</a>
|
|
<a href="/admin/logout" style="text-decoration: none;">
|
|
<button class="danger">Logout</button>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Username</th>
|
|
<th>Name</th>
|
|
<th>Email</th>
|
|
<th>Status</th>
|
|
<th>Role</th>
|
|
<th>Permissions</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in users %}
|
|
<tr>
|
|
<td><strong>{{ user.id }}</strong></td>
|
|
<td>{{ user.username }}</td>
|
|
<td>{{ user.name }}</td>
|
|
<td>{{ user.email }}</td>
|
|
<td>
|
|
{% if user.is_active %}
|
|
<span class="status-badge status-available">Active</span>
|
|
{% else %}
|
|
<span class="status-badge status-retired">Inactive</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if user.role == 'admin' %}
|
|
<span class="status-badge status-in_use">{{ user.role|capitalize }}</span>
|
|
{% elif user.role == 'moderator' %}
|
|
<span class="status-badge status-available">{{ user.role|capitalize }}</span>
|
|
{% elif user.role == 'readonly' %}
|
|
<span class="status-badge status-retired">{{ user.role|capitalize }}</span>
|
|
{% else %}
|
|
<span class="status-badge">{{ user.role|capitalize }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td style="font-size: 0.85rem;">
|
|
{% if user.get_permissions()|length > 0 %}
|
|
{{ user.get_permissions()|join(', ') }}
|
|
{% else %}
|
|
<em style="color: var(--text-secondary);">None</em>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<a href="/admin/user/{{ user.id }}/edit" style="text-decoration: none;">
|
|
<button type="button" style="padding: 7px 14px; font-size: 0.8rem;">Edit</button>
|
|
</a>
|
|
|
|
{% if user.is_active %}
|
|
<form method="POST" action="/admin/user/{{ user.id }}/deactivate" style="display: inline;">
|
|
<button type="submit" class="danger" style="padding: 7px 14px; font-size: 0.8rem;">Deactivate</button>
|
|
</form>
|
|
{% else %}
|
|
<form method="POST" action="/admin/user/{{ user.id }}/activate" style="display: inline;">
|
|
<button type="submit" class="secondary" style="padding: 7px 14px; font-size: 0.8rem;">Activate</button>
|
|
</form>
|
|
{% endif %}
|
|
|
|
{% if not user.is_admin or admin_count > 1 %}
|
|
<form method="POST" action="/admin/user/{{ user.id }}/delete" style="display: inline;" onsubmit="return confirm('Delete user {{ user.username }}?');">
|
|
<button type="submit" class="danger" style="padding: 7px 14px; font-size: 0.8rem;">Delete</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('dark-mode');
|
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
|
}
|
|
|
|
if (localStorage.getItem('darkMode') === 'true') {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
ADMIN_CREATE_USER_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Create New User</title>
|
|
<link rel="stylesheet" href="/static/styles.css">
|
|
</head>
|
|
<body>
|
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
|
</svg>
|
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<h1>Create New User</h1>
|
|
<p>Add a new user to the system</p>
|
|
</header>
|
|
|
|
<div class="modal-content" style="max-width: 600px; margin: 0 auto;">
|
|
{% if error %}
|
|
<div class="import-results error">
|
|
<strong>Error:</strong> {{ error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="POST" style="margin-top: 24px;">
|
|
<div class="form-group">
|
|
<label for="username">Username</label>
|
|
<input type="text" id="username" name="username" placeholder="Enter username" required autofocus>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="email">Email</label>
|
|
<input type="email" id="email" name="email" placeholder="user@example.com" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="name">Full Name</label>
|
|
<input type="text" id="name" name="name" placeholder="John Doe" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="password">Password</label>
|
|
<input type="password" id="password" name="password" placeholder="Enter password" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="role">Role</label>
|
|
<select id="role" name="role" required>
|
|
<option value="user" selected>User</option>
|
|
<option value="admin">Admin</option>
|
|
<option value="moderator">Moderator</option>
|
|
<option value="readonly">Read-Only</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="permissions">Permissions (comma-separated)</label>
|
|
<input type="text" id="permissions" name="permissions" placeholder="e.g. read:data, write:data">
|
|
<small style="color: var(--text-secondary); display: block; margin-top: 8px;">
|
|
Common permissions: read:data, write:data, manage:users, manage:settings
|
|
</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>
|
|
<input type="checkbox" name="is_admin">
|
|
Admin User
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>
|
|
<input type="checkbox" name="is_active" checked>
|
|
Account Active
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<a href="/admin/users">
|
|
<button type="button" class="danger">Cancel</button>
|
|
</a>
|
|
<button type="submit" class="secondary">Create User</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('dark-mode');
|
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
|
}
|
|
|
|
if (localStorage.getItem('darkMode') === 'true') {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
ADMIN_EDIT_USER_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Edit User - {{ user.username }}</title>
|
|
<link rel="stylesheet" href="/static/styles.css">
|
|
</head>
|
|
<body>
|
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
|
</svg>
|
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<h1>Edit User</h1>
|
|
<p>Modify user details for: <strong>{{ user.username }}</strong></p>
|
|
</header>
|
|
|
|
<div class="modal-content" style="max-width: 600px; margin: 0 auto;">
|
|
{% if error %}
|
|
<div class="import-results error">
|
|
<strong>Error:</strong> {{ error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="POST" style="margin-top: 24px;">
|
|
<div class="form-group">
|
|
<label for="username">Username</label>
|
|
<input type="text" id="username" name="username" value="{{ user.username }}" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="email">Email</label>
|
|
<input type="email" id="email" name="email" value="{{ user.email }}" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="name">Full Name</label>
|
|
<input type="text" id="name" name="name" value="{{ user.name }}" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>
|
|
<input type="checkbox" name="is_admin" {% if user.is_admin %}checked{% endif %}>
|
|
Admin User
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>
|
|
<input type="checkbox" name="is_active" {% if user.is_active %}checked{% endif %}>
|
|
Account Active
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="role">Role</label>
|
|
<select id="role" name="role" required>
|
|
<option value="user" {% if user.role == 'user' %}selected{% endif %}>User</option>
|
|
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>Admin</option>
|
|
<option value="moderator" {% if user.role == 'moderator' %}selected{% endif %}>Moderator</option>
|
|
<option value="readonly" {% if user.role == 'readonly' %}selected{% endif %}>Read-Only</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="permissions">Permissions (comma-separated)</label>
|
|
<input type="text" id="permissions" name="permissions" value="{{ user.get_permissions()|join(', ') }}" placeholder="e.g. read:data, write:data, manage:users">
|
|
<small style="color: var(--text-secondary); display: block; margin-top: 8px;">
|
|
Common permissions: read:data, write:data, manage:users, manage:settings
|
|
</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="new_password">New Password (leave empty to keep current)</label>
|
|
<input type="password" id="new_password" name="new_password" placeholder="Optional: Set new password">
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<a href="/admin/users">
|
|
<button type="button" class="danger">Cancel</button>
|
|
</a>
|
|
<button type="submit" class="secondary">Save Changes</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('dark-mode');
|
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
|
}
|
|
|
|
if (localStorage.getItem('darkMode') === 'true') {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
ADMIN_CLIENTS_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Client Administration</title>
|
|
<link rel="stylesheet" href="/static/styles.css">
|
|
</head>
|
|
<body>
|
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
|
</svg>
|
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
|
</svg>
|
|
</button>
|
|
<div class="container">
|
|
<header>
|
|
<h1>OIDC Clients</h1>
|
|
<p>Manage OIDC clients - Logged in as: <strong>{{ admin_user.username }}</strong></p>
|
|
</header>
|
|
{% if message %}
|
|
<div class="import-results success" style="max-width: 100%; margin-bottom: 20px;">
|
|
{{ message }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="controls">
|
|
<a href="/admin/client/create" style="text-decoration: none;">
|
|
<button class="secondary">Create New Client</button>
|
|
</a>
|
|
<a href="/admin/users" style="text-decoration: none;">
|
|
<button>Manage Users</button>
|
|
</a>
|
|
<a href="/admin/logout" style="text-decoration: none;">
|
|
<button class="danger">Logout</button>
|
|
</a>
|
|
</div>
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Client ID</th>
|
|
<th>Client Name</th>
|
|
<th>Redirect URIs</th>
|
|
<th>Allowed Scopes</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for client in clients %}
|
|
<tr>
|
|
<td><strong>{{ client.id }}</strong></td>
|
|
<td><code>{{ client.client_id }}</code></td>
|
|
<td>{{ client.client_name }}</td>
|
|
<td>
|
|
<ul>
|
|
{% for uri in client.get_redirect_uris() %}
|
|
<li>{{ uri }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</td>
|
|
<td>{{ client.get_allowed_scopes()|join(', ') }}</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<a href="/admin/client/{{ client.id }}/edit" style="text-decoration: none;">
|
|
<button type="button" style="padding: 7px 14px; font-size: 0.8rem;">Edit</button>
|
|
</a>
|
|
<form method="POST" action="/admin/client/{{ client.id }}/delete" style="display: inline;" onsubmit="return confirm('Delete client {{ client.client_name }}?');">
|
|
<button type="submit" class="danger" style="padding: 7px 14px; font-size: 0.8rem;">Delete</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('dark-mode');
|
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
|
}
|
|
if (localStorage.getItem('darkMode') === 'true') {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
ADMIN_CREATE_CLIENT_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Create New Client</title>
|
|
<link rel="stylesheet" href="/static/styles.css">
|
|
</head>
|
|
<body>
|
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
|
</svg>
|
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<h1>Create New OIDC Client</h1>
|
|
<p>Add a new client application to the system</p>
|
|
</header>
|
|
|
|
<div class="modal-content" style="max-width: 600px; margin: 0 auto;">
|
|
{% if error %}
|
|
<div class="import-results error">
|
|
<strong>Error:</strong> {{ error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="POST" style="margin-top: 24px;">
|
|
<div class="form-group">
|
|
<label for="client_name">Client Name</label>
|
|
<input type="text" id="client_name" name="client_name" placeholder="My Awesome App" required autofocus>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="client_id">Client ID</label>
|
|
<input type="text" id="client_id" name="client_id" placeholder="leave blank to auto-generate" >
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="client_secret">Client Secret</label>
|
|
<input type="text" id="client_secret" name="client_secret" placeholder="leave blank to auto-generate">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="redirect_uris">Redirect URIs (one per line)</label>
|
|
<textarea id="redirect_uris" name="redirect_uris" rows="3" placeholder="https://app.example.com/callback" required></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="allowed_scopes">Allowed Scopes (comma-separated)</label>
|
|
<input type="text" id="allowed_scopes" name="allowed_scopes" value="openid, profile, email" placeholder="e.g. openid, profile, email">
|
|
</div>
|
|
|
|
|
|
<div class="form-actions">
|
|
<a href="/admin/clients">
|
|
<button type="button" class="danger">Cancel</button>
|
|
</a>
|
|
<button type="submit" class="secondary">Create Client</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('dark-mode');
|
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
|
}
|
|
|
|
if (localStorage.getItem('darkMode') === 'true') {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
ADMIN_EDIT_CLIENT_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Edit Client - {{ client.client_name }}</title>
|
|
<link rel="stylesheet" href="/static/styles.css">
|
|
</head>
|
|
<body>
|
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
|
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
|
</svg>
|
|
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div class="container">
|
|
<header>
|
|
<h1>Edit OIDC Client</h1>
|
|
<p>Modify details for client: <strong>{{ client.client_name }}</strong></p>
|
|
</header>
|
|
|
|
<div class="modal-content" style="max-width: 600px; margin: 0 auto;">
|
|
{% if error %}
|
|
<div class="import-results error">
|
|
<strong>Error:</strong> {{ error }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="POST" style="margin-top: 24px;">
|
|
<div class="form-group">
|
|
<label for="client_name">Client Name</label>
|
|
<input type="text" id="client_name" name="client_name" value="{{ client.client_name }}" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="client_id">Client ID</label>
|
|
<input type="text" id="client_id" name="client_id" value="{{ client.client_id }}" readonly>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="new_client_secret">New Client Secret (leave empty to keep current)</label>
|
|
<input type="text" id="new_client_secret" name="new_client_secret" placeholder="Optional: Set new secret">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="redirect_uris">Redirect URIs (one per line)</label>
|
|
<textarea id="redirect_uris" name="redirect_uris" rows="3" required>{{ client.get_redirect_uris()|join('\n') }}</textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="allowed_scopes">Allowed Scopes (comma-separated)</label>
|
|
<input type="text" id="allowed_scopes" name="allowed_scopes" value="{{ client.get_allowed_scopes()|join(', ') }}" placeholder="e.g. openid, profile, email">
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<a href="/admin/clients">
|
|
<button type="button" class="danger">Cancel</button>
|
|
</a>
|
|
<button type="submit" class="secondary">Save Changes</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('dark-mode');
|
|
localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
|
|
}
|
|
|
|
if (localStorage.getItem('darkMode') === 'true') {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
"""
|