CLI tool (mdsync), 2FA setup UI, git remotes UI

- mdsync: login, pull, push, status, list, flag commands
- Preferences: 2FA enable/disable with TOTP code verification
- Preferences: git remotes add/list, push/pull buttons
- Load remotes on login
This commit is contained in:
2026-05-22 23:25:29 +02:00
parent 1433890a4c
commit 62ab0fb796
+136
View File
@@ -160,6 +160,45 @@
<option value="light">Light</option>
</select>
</div>
<h3>Two-Factor Authentication</h3>
<div class="panel-section" v-if="!totpEnabled">
<button @click="setupTOTP" class="action-btn">Enable 2FA</button>
<div v-if="totpUri" class="totp-setup">
<p>Scan this with your authenticator app:</p>
<code class="totp-secret">{{ totpSecret }}</code>
<p style="margin-top:8px">URI: <code style="font-size:11px;word-break:break-all">{{ totpUri }}</code></p>
<form @submit.prevent="verifyTOTP" class="totp-verify">
<input v-model="totpCode" placeholder="Enter 6-digit code" maxlength="6" />
<button type="submit">Verify & Enable</button>
</form>
</div>
</div>
<div class="panel-section" v-else>
<p>✅ 2FA is enabled</p>
<form @submit.prevent="disableTOTP" class="totp-verify">
<input v-model="totpCode" placeholder="Enter code to disable" maxlength="6" />
<button type="submit" style="background:var(--danger)">Disable 2FA</button>
</form>
</div>
<h3>Git Remotes</h3>
<div class="panel-section">
<div v-for="r in gitRemotes" :key="r.name" class="remote-item">
<span>{{ r.name }}</span>
<code>{{ r.url }}</code>
</div>
<p v-if="!gitRemotes.length" style="color:var(--text-muted)">No remotes configured</p>
<form @submit.prevent="addRemote" class="remote-form">
<input v-model="newRemote.name" placeholder="Name (e.g. gitea)" required />
<input v-model="newRemote.url" placeholder="https://git.example.com/user/repo.git" required />
<button type="submit">Add Remote</button>
</form>
<div v-if="gitRemotes.length" class="remote-actions">
<button @click="gitPush" class="action-btn">Push</button>
<button @click="gitPull" class="action-btn">Pull</button>
</div>
</div>
</main>
<!-- Admin Panel -->
<main class="panel" v-if="view === 'admin' && isAdmin">
@@ -242,6 +281,16 @@ const trashItems = ref([])
const prefs = ref({ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, defaultMode: 'split', theme: 'dark' })
const timezones = Intl.supportedValuesOf ? Intl.supportedValuesOf('timeZone') : ['UTC', 'Europe/Stockholm', 'America/New_York', 'Asia/Tokyo']
// TOTP
const totpEnabled = ref(false)
const totpUri = ref('')
const totpSecret = ref('')
const totpCode = ref('')
// Git remotes
const gitRemotes = ref([])
const newRemote = ref({ name: '', url: '' })
// Admin
const users = ref([])
const newUser = ref({ username: '', email: '', password: '', isAdmin: false })
@@ -268,6 +317,7 @@ async function login() {
loginError.value = ''
loadFiles()
loadShared()
loadRemotes()
if (isAdmin.value) loadUsers()
} catch (e) {
loginError.value = 'Invalid credentials'
@@ -430,6 +480,78 @@ function applyThemeFromPrefs() {
savePrefs()
}
// ─── TOTP ────────────────────────────────────────────────────────────────────
async function setupTOTP() {
try {
const res = await api('/api/auth/totp/setup', {})
totpUri.value = res.uri
totpSecret.value = res.secret
} catch (e) {
alert('Failed to setup 2FA')
}
}
async function verifyTOTP() {
try {
await api('/api/auth/totp/verify', { code: totpCode.value })
totpEnabled.value = true
totpUri.value = ''
totpSecret.value = ''
totpCode.value = ''
alert('2FA enabled!')
} catch (e) {
alert('Invalid code')
}
}
async function disableTOTP() {
try {
await api('/api/auth/totp/disable', { code: totpCode.value })
totpEnabled.value = false
totpCode.value = ''
alert('2FA disabled')
} catch (e) {
alert('Invalid code')
}
}
// ─── Git Remotes ─────────────────────────────────────────────────────────────
async function loadRemotes() {
try {
gitRemotes.value = await api('/api/git/remote/list', {})
} catch { gitRemotes.value = [] }
}
async function addRemote() {
if (!newRemote.value.name || !newRemote.value.url) return
await api('/api/git/remote/add', newRemote.value)
newRemote.value = { name: '', url: '' }
loadRemotes()
}
async function gitPush() {
try {
await api('/api/git/push', { remote: gitRemotes.value[0]?.name || 'origin' })
alert('Pushed successfully')
checkGitStatus()
} catch (e) {
alert('Push failed')
}
}
async function gitPull() {
try {
await api('/api/git/pull', { remote: gitRemotes.value[0]?.name || 'origin' })
alert('Pulled successfully')
loadFiles()
checkGitStatus()
} catch (e) {
alert('Pull failed')
}
}
// ─── Admin ───────────────────────────────────────────────────────────────────
async function loadUsers() {
@@ -1082,6 +1204,20 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
.trash-item button { background: none; border: none; cursor: pointer; font-size: 14px; }
.trash-empty { color: var(--text-muted); font-size: 13px; text-align: center; padding: 20px; }
.action-btn { background: var(--accent); color: var(--bg-primary); border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 13px; }
.totp-setup { margin-top: 12px; }
.totp-secret { display: block; margin: 8px 0; padding: 8px; background: var(--code-bg); border-radius: 4px; font-size: 14px; letter-spacing: 2px; }
.totp-verify { display: flex; gap: 8px; margin-top: 8px; align-items: center; }
.totp-verify input { padding: 6px 10px; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font-size: 14px; width: 140px; }
.totp-verify button { padding: 6px 12px; background: var(--accent); color: var(--bg-primary); border: none; border-radius: 4px; cursor: pointer; }
.remote-item { display: flex; gap: 12px; align-items: center; padding: 6px 0; font-size: 13px; }
.remote-item code { color: var(--text-muted); font-size: 12px; }
.remote-form { display: flex; gap: 8px; margin-top: 12px; align-items: center; }
.remote-form input { padding: 6px 10px; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font-size: 13px; }
.remote-form button { padding: 6px 12px; background: var(--accent); color: var(--bg-primary); border: none; border-radius: 4px; cursor: pointer; }
.remote-actions { display: flex; gap: 8px; margin-top: 12px; }
/* ─── Responsive ──────────────────────────────────────────────────────────── */
@media (max-width: 768px) {