feat: initial cfdav project with webdav+r2+d1 and pages admin docs
This commit is contained in:
117
web/app.js
Normal file
117
web/app.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
function log(msg) {
|
||||
const el = $('log');
|
||||
el.textContent = `[${new Date().toISOString()}] ${msg}\n` + el.textContent;
|
||||
}
|
||||
|
||||
function getAuthHeader() {
|
||||
const email = $('email').value.trim();
|
||||
const pass = $('password').value;
|
||||
const token = btoa(`${email}:${pass}`);
|
||||
return `Basic ${token}`;
|
||||
}
|
||||
|
||||
function apiBase() {
|
||||
const base = $('apiBase').value.trim();
|
||||
return base ? base.replace(/\/$/, '') : '';
|
||||
}
|
||||
|
||||
async function apiFetch(path, options = {}) {
|
||||
const url = apiBase() + path;
|
||||
const headers = options.headers || {};
|
||||
headers['Authorization'] = getAuthHeader();
|
||||
headers['Content-Type'] = 'application/json';
|
||||
const res = await fetch(url, { ...options, headers });
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`${res.status} ${res.statusText}: ${text}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
const data = await apiFetch('/api/admin/users');
|
||||
const list = data.data || [];
|
||||
const tbody = $('userList');
|
||||
tbody.innerHTML = '';
|
||||
list.forEach((u) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${u.email}</td>
|
||||
<td>${u.is_admin ? 'yes' : 'no'}</td>
|
||||
<td>${u.created_at}</td>
|
||||
<td><button data-id="${u.id}">Delete</button></td>
|
||||
`;
|
||||
tr.querySelector('button').addEventListener('click', () => deleteUser(u.id));
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
log('Loaded users');
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
const email = $('newEmail').value.trim();
|
||||
const password = $('newPassword').value;
|
||||
const isAdmin = $('newIsAdmin').checked;
|
||||
if (!email || !password) {
|
||||
log('Email and password required');
|
||||
return;
|
||||
}
|
||||
await apiFetch('/api/admin/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password, isAdmin })
|
||||
});
|
||||
$('newEmail').value = '';
|
||||
$('newPassword').value = '';
|
||||
$('newIsAdmin').checked = false;
|
||||
log('User created');
|
||||
await loadUsers();
|
||||
}
|
||||
|
||||
async function deleteUser(id) {
|
||||
if (!confirm('Delete this user?')) return;
|
||||
await apiFetch(`/api/admin/users/${id}`, { method: 'DELETE' });
|
||||
log('User deleted');
|
||||
await loadUsers();
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
localStorage.setItem('cfdav_api_base', $('apiBase').value.trim());
|
||||
localStorage.setItem('cfdav_email', $('email').value.trim());
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
$('apiBase').value = localStorage.getItem('cfdav_api_base') || '';
|
||||
$('email').value = localStorage.getItem('cfdav_email') || '';
|
||||
}
|
||||
|
||||
function setLoggedIn(state) {
|
||||
$('loginCard').classList.toggle('hidden', state);
|
||||
$('app').classList.toggle('hidden', !state);
|
||||
}
|
||||
|
||||
async function login() {
|
||||
try {
|
||||
saveSettings();
|
||||
await loadUsers();
|
||||
setLoggedIn(true);
|
||||
log('Login success');
|
||||
} catch (e) {
|
||||
setLoggedIn(false);
|
||||
log(`Login failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
$('password').value = '';
|
||||
setLoggedIn(false);
|
||||
log('Logged out');
|
||||
}
|
||||
|
||||
$('loginBtn').addEventListener('click', login);
|
||||
$('refreshBtn').addEventListener('click', () => loadUsers().catch((e) => log(e.message)));
|
||||
$('createBtn').addEventListener('click', () => createUser().catch((e) => log(e.message)));
|
||||
$('logoutBtn').addEventListener('click', logout);
|
||||
|
||||
loadSettings();
|
||||
setLoggedIn(false);
|
||||
77
web/index.html
Normal file
77
web/index.html
Normal file
@@ -0,0 +1,77 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>cfdav Admin</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<h1>cfdav Admin</h1>
|
||||
|
||||
<section id="loginCard" class="card">
|
||||
<h2>Login</h2>
|
||||
<div class="row">
|
||||
<label>API Base URL</label>
|
||||
<input id="apiBase" placeholder="https://cfdav.fnos.workers.dev" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Email</label>
|
||||
<input id="email" placeholder="admin@example.com" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label>
|
||||
<input id="password" type="password" placeholder="******" />
|
||||
</div>
|
||||
<button id="loginBtn">Login</button>
|
||||
<p class="hint">提示:API Base 留空则默认同域(/api/admin)。</p>
|
||||
</section>
|
||||
|
||||
<section id="app" class="hidden">
|
||||
<section class="card">
|
||||
<h2>Users</h2>
|
||||
<div class="toolbar">
|
||||
<button id="refreshBtn">Refresh</button>
|
||||
<button id="logoutBtn" class="ghost">Logout</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Admin</th>
|
||||
<th>Created</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userList"></tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Create User</h2>
|
||||
<div class="row">
|
||||
<label>Email</label>
|
||||
<input id="newEmail" placeholder="user@example.com" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Password</label>
|
||||
<input id="newPassword" type="password" placeholder="password" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Admin</label>
|
||||
<input id="newIsAdmin" type="checkbox" />
|
||||
</div>
|
||||
<button id="createBtn">Create</button>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Log</h2>
|
||||
<pre id="log"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
web/style.css
Normal file
1
web/style.css
Normal file
@@ -0,0 +1 @@
|
||||
*{box-sizing:border-box;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}body{margin:0;background:#0b0f14;color:#e6edf3}.container{max-width:960px;margin:40px auto;padding:0 16px}.card{background:#111827;border:1px solid #1f2937;border-radius:12px;padding:16px;margin-bottom:16px}h1,h2{margin:0 0 12px}label{display:block;margin-bottom:6px;color:#9ca3af}.row{margin-bottom:12px}input{width:100%;padding:8px;border-radius:8px;border:1px solid #374151;background:#0f172a;color:#e6edf3}button{padding:8px 14px;border:0;border-radius:8px;background:#2563eb;color:#fff;cursor:pointer}button:hover{background:#1d4ed8}.ghost{background:#374151}.ghost:hover{background:#4b5563}.toolbar{margin-bottom:8px;display:flex;gap:8px;align-items:center}table{width:100%;border-collapse:collapse}th,td{border-bottom:1px solid #1f2937;padding:8px;text-align:left}.hint{color:#9ca3af;font-size:12px}pre{background:#0f172a;border:1px solid #1f2937;border-radius:8px;padding:10px;white-space:pre-wrap}.hidden{display:none}
|
||||
Reference in New Issue
Block a user