feat: initial cfdav project with webdav+r2+d1 and pages admin docs

This commit is contained in:
OpenClaw Agent
2026-03-20 16:35:51 +08:00
commit 334bb75672
15 changed files with 2124 additions and 0 deletions

117
web/app.js Normal file
View 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
View 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
View 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}