diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index ebae66d..4452bcf 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -160,6 +160,45 @@
+
+
Two-Factor Authentication
+
+
+
+
Scan this with your authenticator app:
+
{{ totpSecret }}
+
URI: {{ totpUri }}
+
+
+
+
+
+ Git Remotes
+
+
+ {{ r.name }}
+ {{ r.url }}
+
+
No remotes configured
+
+
+
+
+
+
@@ -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) {