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>
|
<option value="light">Light</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</main>
|
||||||
<!-- Admin Panel -->
|
<!-- Admin Panel -->
|
||||||
<main class="panel" v-if="view === 'admin' && isAdmin">
|
<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 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']
|
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
|
// Admin
|
||||||
const users = ref([])
|
const users = ref([])
|
||||||
const newUser = ref({ username: '', email: '', password: '', isAdmin: false })
|
const newUser = ref({ username: '', email: '', password: '', isAdmin: false })
|
||||||
@@ -268,6 +317,7 @@ async function login() {
|
|||||||
loginError.value = ''
|
loginError.value = ''
|
||||||
loadFiles()
|
loadFiles()
|
||||||
loadShared()
|
loadShared()
|
||||||
|
loadRemotes()
|
||||||
if (isAdmin.value) loadUsers()
|
if (isAdmin.value) loadUsers()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loginError.value = 'Invalid credentials'
|
loginError.value = 'Invalid credentials'
|
||||||
@@ -430,6 +480,78 @@ function applyThemeFromPrefs() {
|
|||||||
savePrefs()
|
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 ───────────────────────────────────────────────────────────────────
|
// ─── Admin ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function loadUsers() {
|
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-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; }
|
.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 ──────────────────────────────────────────────────────────── */
|
/* ─── Responsive ──────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user