This commit is contained in:
2026-01-25 14:36:01 +01:00
commit fdf1e0e8ed
22 changed files with 1269 additions and 0 deletions

150
frontend/index.html Normal file
View File

@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASF SSO - Admin Portal</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<!-- Login Page -->
<div id="login-page" class="flex items-center justify-center" style="height: 100vh;">
<div class="card text-center" style="width: 100%; max-width: 400px;">
<img src="/static/img/logo.png" alt="ASF Logo" class="logo" style="height: 80px; margin-bottom: 2rem;">
<h2 class="text-2xl mb-4">Admin Login</h2>
<form id="login-form">
<input type="text" id="username" placeholder="Username" required>
<input type="password" id="password" placeholder="Password" required>
<button type="submit" class="btn btn-primary w-full">Login</button>
</form>
<p id="login-error" class="text-accent mt-4 hidden"></p>
</div>
</div>
<!-- Dashboard Layout -->
<div id="dashboard-layout" class="hidden">
<header class="flex items-center justify-between">
<div class="flex items-center">
<img src="/static/img/logo.png" alt="ASF Logo" class="logo">
<h1 class="text-xl">ASF SSO Admin</h1>
</div>
<button id="logout-btn" class="btn btn-secondary">Logout</button>
</header>
<main class="p-8">
<!-- Tabs -->
<div class="flex gap-4 mb-8">
<button class="btn btn-primary" onclick="showSection('users')">Users</button>
<button class="btn btn-secondary" onclick="showSection('apps')">Applications</button>
</div>
<!-- Users Section -->
<section id="users-section">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl">Users</h2>
<button class="btn btn-primary" onclick="openModal('user-modal')">Add User</button>
</div>
<div class="card">
<table id="users-table">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Admin</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Users will be populated here -->
</tbody>
</table>
</div>
</section>
<!-- Applications Section -->
<section id="apps-section" class="hidden">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl">Applications</h2>
<button class="btn btn-primary" onclick="openModal('app-modal')">Add Application</button>
</div>
<div class="card">
<table id="apps-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>URL</th>
<th>API Key</th>
</tr>
</thead>
<tbody>
<!-- Apps will be populated here -->
</tbody>
</table>
</div>
</section>
</main>
</div>
<!-- User Modal -->
<div id="user-modal" class="modal-overlay hidden">
<div class="card modal">
<h3 class="text-xl mb-4">Add/Edit User</h3>
<form id="user-form">
<input type="hidden" id="user-id">
<input type="text" id="user-username" placeholder="Username" required>
<input type="email" id="user-email" placeholder="Email" required>
<input type="password" id="user-password" placeholder="Password (leave blank to keep current)">
<div class="flex items-center gap-4 mb-4">
<label class="flex items-center gap-2">
<input type="checkbox" id="user-is-admin" style="width: auto; margin: 0;"> Admin
</label>
<label class="flex items-center gap-2">
<input type="checkbox" id="user-is-active" style="width: auto; margin: 0;" checked> Active
</label>
</div>
<div class="flex justify-end gap-4">
<button type="button" class="btn btn-secondary" onclick="closeModal('user-modal')">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- App Modal -->
<div id="app-modal" class="modal-overlay hidden">
<div class="card modal">
<h3 class="text-xl mb-4">Add Application</h3>
<form id="app-form">
<input type="text" id="app-name" placeholder="Application Name" required>
<input type="url" id="app-url" placeholder="Application URL" required>
<div class="flex justify-end gap-4">
<button type="button" class="btn btn-secondary" onclick="closeModal('app-modal')">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- Assign App Modal -->
<div id="assign-modal" class="modal-overlay hidden">
<div class="card modal">
<h3 class="text-xl mb-4">Assign Application</h3>
<form id="assign-form">
<input type="hidden" id="assign-user-id">
<select id="assign-app-select" required>
<option value="">Select Application</option>
</select>
<div class="flex justify-end gap-4">
<button type="button" class="btn btn-secondary" onclick="closeModal('assign-modal')">Cancel</button>
<button type="submit" class="btn btn-primary">Assign</button>
</div>
</form>
</div>
</div>
<script src="/static/js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,152 @@
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--background-color: #0f172a; /* Dark Blue */
--surface-color: #1e293b; /* Lighter Dark Blue */
--text-color: #f8fafc;
--text-muted: #94a3b8;
--accent-color: #38bdf8;
--glass-bg: rgba(30, 41, 59, 0.7);
--glass-border: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: var(--background-color);
color: var(--text-color);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Utilities */
.hidden { display: none !important; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.gap-4 { gap: 1rem; }
.p-4 { padding: 1rem; }
.p-8 { padding: 2rem; }
.m-4 { margin: 1rem; }
.mt-4 { margin-top: 1rem; }
.w-full { width: 100%; }
.text-center { text-align: center; }
.text-xl { font-size: 1.25rem; font-weight: bold; }
.text-2xl { font-size: 1.5rem; font-weight: bold; }
.text-accent { color: var(--accent-color); }
/* Glassmorphism Card */
.card {
background: var(--glass-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 1rem;
padding: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
/* Buttons */
.btn {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
border: none;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: var(--surface-color);
color: var(--text-color);
border: 1px solid var(--glass-border);
}
.btn-secondary:hover {
background-color: #334155;
}
/* Inputs */
input, select {
width: 100%;
padding: 0.75rem;
margin-bottom: 1rem;
border-radius: 0.5rem;
border: 1px solid var(--glass-border);
background-color: rgba(0, 0, 0, 0.2);
color: white;
outline: none;
}
input:focus {
border-color: var(--accent-color);
}
/* Layout */
header {
background: var(--surface-color);
border-bottom: 1px solid var(--glass-border);
padding: 1rem 2rem;
}
.logo {
height: 40px;
margin-right: 1rem;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid var(--glass-border);
}
th {
color: var(--text-muted);
font-weight: 600;
}
tr:hover {
background-color: rgba(255, 255, 255, 0.05);
}
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
width: 90%;
max-width: 500px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

257
frontend/static/js/app.js Normal file
View File

@@ -0,0 +1,257 @@
const API_URL = ""; // Relative path since served by same origin
// State
let token = localStorage.getItem("token");
let currentUser = null;
// DOM Elements
const loginPage = document.getElementById("login-page");
const dashboardLayout = document.getElementById("dashboard-layout");
const loginForm = document.getElementById("login-form");
const loginError = document.getElementById("login-error");
const logoutBtn = document.getElementById("logout-btn");
const usersSection = document.getElementById("users-section");
const appsSection = document.getElementById("apps-section");
const usersTableBody = document.querySelector("#users-table tbody");
const appsTableBody = document.querySelector("#apps-table tbody");
// Init
function init() {
if (token) {
showDashboard();
} else {
showLogin();
}
}
// Navigation
function showLogin() {
loginPage.classList.remove("hidden");
dashboardLayout.classList.add("hidden");
}
function showDashboard() {
loginPage.classList.add("hidden");
dashboardLayout.classList.remove("hidden");
loadUsers();
}
function showSection(section) {
if (section === 'users') {
usersSection.classList.remove("hidden");
appsSection.classList.add("hidden");
loadUsers();
} else {
usersSection.classList.add("hidden");
appsSection.classList.remove("hidden");
loadApps();
}
}
// Auth
loginForm.addEventListener("submit", async (e) => {
e.preventDefault();
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
try {
const formData = new FormData();
formData.append("username", username);
formData.append("password", password);
const res = await fetch(`${API_URL}/token`, {
method: "POST",
body: formData
});
if (!res.ok) throw new Error("Invalid credentials");
const data = await res.json();
token = data.access_token;
localStorage.setItem("token", token);
loginError.classList.add("hidden");
showDashboard();
} catch (err) {
loginError.textContent = err.message;
loginError.classList.remove("hidden");
}
});
logoutBtn.addEventListener("click", () => {
token = null;
localStorage.removeItem("token");
showLogin();
});
// API Helpers
async function authFetch(url, options = {}) {
const headers = {
...options.headers,
"Authorization": `Bearer ${token}`
};
const res = await fetch(url, { ...options, headers });
if (res.status === 401) {
logoutBtn.click();
throw new Error("Unauthorized");
}
return res;
}
// Users
async function loadUsers() {
const res = await authFetch(`${API_URL}/users/`);
const users = await res.json();
usersTableBody.innerHTML = users.map(user => `
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>${user.is_admin ? "Yes" : "No"}</td>
<td>${user.is_active ? "Yes" : "No"}</td>
<td>
<button class="btn btn-secondary" onclick='editUser(${JSON.stringify(user)})'>Edit</button>
<button class="btn btn-primary" onclick='assignAppModal(${user.id})'>Assign App</button>
</td>
</tr>
`).join("");
}
// Apps
async function loadApps() {
const res = await authFetch(`${API_URL}/apps/`);
const apps = await res.json();
appsTableBody.innerHTML = apps.map(app => `
<tr>
<td>${app.id}</td>
<td>${app.name}</td>
<td>${app.url}</td>
<td><code>${app.api_key}</code></td>
</tr>
`).join("");
}
// Modals
function openModal(id) {
document.getElementById(id).classList.remove("hidden");
}
function closeModal(id) {
document.getElementById(id).classList.add("hidden");
}
// User Form
const userForm = document.getElementById("user-form");
userForm.addEventListener("submit", async (e) => {
e.preventDefault();
const id = document.getElementById("user-id").value;
const username = document.getElementById("user-username").value;
const email = document.getElementById("user-email").value;
const password = document.getElementById("user-password").value;
const isAdmin = document.getElementById("user-is-admin").checked;
const isActive = document.getElementById("user-is-active").checked;
const data = { username, email, is_admin: isAdmin, is_active: isActive };
if (password) data.password = password;
try {
let res;
if (id) {
res = await authFetch(`${API_URL}/users/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
} else {
if (!password) return alert("Password required for new user");
res = await authFetch(`${API_URL}/users/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
}
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail);
}
closeModal("user-modal");
loadUsers();
} catch (err) {
alert(err.message);
}
});
window.editUser = (user) => {
document.getElementById("user-id").value = user.id;
document.getElementById("user-username").value = user.username;
document.getElementById("user-email").value = user.email;
document.getElementById("user-password").value = "";
document.getElementById("user-is-admin").checked = user.is_admin;
document.getElementById("user-is-active").checked = user.is_active;
openModal("user-modal");
};
// App Form
const appForm = document.getElementById("app-form");
appForm.addEventListener("submit", async (e) => {
e.preventDefault();
const name = document.getElementById("app-name").value;
const url = document.getElementById("app-url").value;
try {
const res = await authFetch(`${API_URL}/apps/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, url })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail);
}
closeModal("app-modal");
loadApps();
} catch (err) {
alert(err.message);
}
});
// Assign App
window.assignAppModal = async (userId) => {
document.getElementById("assign-user-id").value = userId;
// Load apps for select
const res = await authFetch(`${API_URL}/apps/`);
const apps = await res.json();
const select = document.getElementById("assign-app-select");
select.innerHTML = '<option value="">Select Application</option>' +
apps.map(app => `<option value="${app.id}">${app.name}</option>`).join("");
openModal("assign-modal");
};
const assignForm = document.getElementById("assign-form");
assignForm.addEventListener("submit", async (e) => {
e.preventDefault();
const userId = document.getElementById("assign-user-id").value;
const appId = document.getElementById("assign-app-select").value;
try {
const res = await authFetch(`${API_URL}/users/${userId}/assign/${appId}`, {
method: "POST"
});
if (!res.ok) throw new Error("Failed to assign");
closeModal("assign-modal");
alert("Assigned successfully");
} catch (err) {
alert(err.message);
}
});
// Start
init();