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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user