Files
flowdeck/app/templates/settings.html
T
bruno 32c1f70c53
FlowDeck CI / test (push) Failing after 6s
FlowDeck CI / docker (push) Has been skipped
feat: multi-user auth — local accounts, settings page, OAuth prep
Phase 1 foundation:
- DB: users table extended (password_hash, is_active, login_attempts, locked_until)
- DB: user_oauth_tokens table (user_id, provider, access_token, refresh_token)
- password_utils.py: SHA-256+salt hashing, verify, rate-limit lock check
- auth.py: POST /auth/register + POST /auth/local-login + /auth/login?provider=local
- Login page: tabs Login/Register with Gitea OAuth button
- settings.html: profile (name/password), forges, API tokens, sessions
- ALTER TABLE migrations for existing DBs
2026-07-10 15:34:58 -04:00

190 lines
7.7 KiB
HTML

{% extends "base.html" %}
{% block page_title %}Account Settings{% endblock %}
{% block page_icon %}⚙️{% endblock %}
{% block topbar %}
<header class="topbar">
<div class="topbar-left">
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">
Settings
</span>
<span class="page-title-topbar">Account Settings</span>
</div>
<div class="topbar-right"></div>
</header>
{% endblock %}
{% block content %}
<style>
.settings-layout{max-width:700px;margin:0 auto;padding:40px 24px;}
.settings-section{margin-bottom:32px;}
.settings-section h2{font-size:14px;color:rgba(255,255,255,.5);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;font-weight:500;}
.settings-card{background:#222;border:1px solid rgba(255,255,255,.08);border-radius:12px;overflow:hidden;}
.settings-row{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid rgba(255,255,255,.06);}
.settings-row:last-child{border-bottom:none;}
.settings-row-left{flex:1;}
.settings-row-label{font-size:14px;color:#fff;}
.settings-row-desc{font-size:12px;color:rgba(255,255,255,.45);margin-top:2px;}
.settings-row-value{font-size:13px;color:rgba(255,255,255,.5);margin-right:12px;}
.settings-input{background:#2A2A2A;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:8px 12px;color:#fff;font-size:13px;width:220px;outline:none;}
.settings-input:focus{border-color:#2383E2;}
.settings-btn{padding:8px 16px;border-radius:8px;font-size:13px;cursor:pointer;border:none;font-weight:500;}
.settings-btn-primary{background:#2383E2;color:#fff;}
.settings-btn-primary:hover{background:#2C8CEB;}
.settings-btn-danger{background:rgba(255,80,80,.15);color:#ff5050;}
.settings-btn-danger:hover{background:rgba(255,80,80,.25);}
.settings-btn-secondary{background:#333;color:#fff;}
.settings-btn-secondary:hover{background:#444;}
.inline-form{display:flex;align-items:center;gap:8px;}
.forge-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;background:#2A2A2A;border-radius:6px;font-size:12px;}
.connected{color:#50ff50;}
.disconnected{color:rgba(255,255,255,.3);}
</style>
<div class="settings-layout" x-data="settingsPage()">
<!-- Profile -->
<div class="settings-section">
<h2>Profile</h2>
<div class="settings-card">
<div class="settings-row">
<div class="settings-row-left">
<div class="settings-row-label">Email</div>
<div class="settings-row-desc">Used for login and notifications</div>
</div>
<div class="settings-row-value">{{ user.email or user.login or '' }}</div>
</div>
<div class="settings-row">
<div class="settings-row-left">
<div class="settings-row-label">Name</div>
<div class="settings-row-desc">Display name</div>
</div>
<div class="inline-form">
<input class="settings-input" x-model="name" :value="name" placeholder="Your name">
<button class="settings-btn settings-btn-primary" @click="saveName">Save</button>
</div>
</div>
<div class="settings-row">
<div class="settings-row-left">
<div class="settings-row-label">Password</div>
<div class="settings-row-desc">Change your password</div>
</div>
<div class="inline-form">
<input class="settings-input" type="password" x-model="newPassword" placeholder="New password">
<button class="settings-btn settings-btn-primary" @click="savePassword">Update</button>
</div>
</div>
</div>
</div>
<!-- Connected Forges -->
<div class="settings-section">
<h2>Connected Forges</h2>
<div class="settings-card">
<div class="settings-row">
<div class="settings-row-left">
<div class="settings-row-label">🔗 Gitea</div>
<div class="settings-row-desc">Connect to your Gitea instance</div>
</div>
{% if gitea_connected %}
<span class="forge-badge connected">✅ Connected</span>
<button class="settings-btn settings-btn-danger" @click="disconnect('gitea')">Disconnect</button>
{% else %}
<button class="settings-btn settings-btn-primary" onclick="window.location='/auth/login?provider=gitea'">Connect</button>
{% endif %}
</div>
<div class="settings-row">
<div class="settings-row-left">
<div class="settings-row-label">🐙 GitHub</div>
<div class="settings-row-desc">Connect to GitHub</div>
</div>
{% if github_connected %}
<span class="forge-badge connected">✅ Connected</span>
<button class="settings-btn settings-btn-danger" @click="disconnect('github')">Disconnect</button>
{% else %}
<button class="settings-btn settings-btn-secondary">
Coming soon
</button>
{% endif %}
</div>
</div>
</div>
<!-- API Tokens -->
<div class="settings-section">
<h2>API Tokens</h2>
<div class="settings-card">
<div class="settings-row">
<div class="settings-row-left">
<div class="settings-row-label">Generate API Token</div>
<div class="settings-row-desc">For programmatic access to FlowDeck API</div>
</div>
<button class="settings-btn settings-btn-primary" @click="generateToken">Generate</button>
</div>
<template x-if="token">
<div class="settings-row" style="background:#1a2a1a">
<div class="settings-row-left">
<div class="settings-row-label" style="font-family:monospace;font-size:12px;word-break:break-all" x-text="token"></div>
<div class="settings-row-desc" style="color:#50ff50">Copy this token now — it won't be shown again</div>
</div>
<button class="settings-btn settings-btn-primary" @click="copyToken">Copy</button>
</div>
</template>
</div>
</div>
<!-- Active Sessions -->
<div class="settings-section">
<h2>Active Sessions</h2>
<div class="settings-card">
<div class="settings-row">
<div class="settings-row-left">
<div class="settings-row-label">Current session</div>
<div class="settings-row-desc">You are logged in</div>
</div>
<button class="settings-btn settings-btn-danger" onclick="window.location='/auth/logout'">Log out</button>
</div>
</div>
</div>
</div>
<script>
function settingsPage() {
return {
name: '{{ user.full_name or '' }}',
newPassword: '',
token: '',
async saveName() {
await fetch('/api/user/profile', {method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({full_name:this.name})});
alert('Name saved');
},
async savePassword() {
if (!this.newPassword || this.newPassword.length < 6) { alert('Password must be at least 6 characters'); return; }
await fetch('/api/user/password', {method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:this.newPassword})});
this.newPassword = '';
alert('Password updated');
},
async generateToken() {
const r = await fetch('/api/user/token', {method:'POST'});
const d = await r.json();
this.token = d.token;
},
copyToken() {
navigator.clipboard?.writeText(this.token);
const ta = document.createElement('textarea');
ta.value = this.token; ta.style.position='fixed'; ta.style.opacity='0';
document.body.appendChild(ta); ta.select(); document.execCommand('copy');
document.body.removeChild(ta);
alert('Token copied to clipboard');
},
async disconnect(provider) {
if (!confirm('Disconnect ' + provider + '?')) return;
await fetch('/api/user/forge/' + provider, {method:'DELETE'});
window.location.reload();
}
};
}
</script>
{% endblock %}