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 }}

+
+ + +
+
+
+
+

✅ 2FA is enabled

+
+ + +
+
+ +

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) {