sso
This commit is contained in:
152
frontend/static/css/style.css
Normal file
152
frontend/static/css/style.css
Normal 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;
|
||||
}
|
||||
BIN
frontend/static/img/logo.png
Normal file
BIN
frontend/static/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
257
frontend/static/js/app.js
Normal file
257
frontend/static/js/app.js
Normal 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();
|
||||
Reference in New Issue
Block a user