- Email verification: send link on register via SMTP2GO (noreply@pedshub.com), verify endpoint, beautiful HTML email - Unverified users see yellow banner in app with resend button; also shown on settings page - User settings page (/settings): personal Nextcloud credentials (override admin-wide config) - Admin and per-user Nextcloud config merged (user settings take priority) - Groq PlayAI TTS: 20 English voices + 2 Arabic voices via groq-playai-tts / groq-playai-tts-arabic - Added groq-playai-tts and groq-playai-tts-arabic to litellm config - Favicon SVG (purple play button) across all pages - Settings link in user dropdown menu - verify-ok.html and verify-fail.html result pages - DB migration: added email_verified column, verification_tokens and user_settings tables Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
204 lines
8.3 KiB
HTML
204 lines
8.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Settings — Speechify</title>
|
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
:root {
|
|
--bg:#0d0d0d; --bg2:#161616; --bg3:#212121; --bg4:#2a2a2a;
|
|
--accent:#7c3aed; --accent-h:#9d6fff;
|
|
--text:#e8e8e8; --text-m:#888; --border:#232323; --success:#4ade80;
|
|
}
|
|
body { font-family:'Inter',sans-serif; background:var(--bg); color:var(--text); min-height:100vh; }
|
|
header {
|
|
height:54px; background:var(--bg2); border-bottom:1px solid var(--border);
|
|
display:flex; align-items:center; justify-content:space-between; padding:0 24px;
|
|
}
|
|
.logo { display:flex; align-items:center; gap:10px; font-size:17px; font-weight:700; text-decoration:none; color:var(--text); }
|
|
.header-right { display:flex; align-items:center; gap:12px; }
|
|
.back-btn, .logout-btn {
|
|
background:var(--bg3); border:1px solid var(--border); color:var(--text-m);
|
|
border-radius:7px; padding:6px 14px; font-size:13px; font-family:inherit;
|
|
cursor:pointer; text-decoration:none; display:inline-block; transition:all .15s;
|
|
}
|
|
.back-btn:hover, .logout-btn:hover { border-color:var(--accent); color:var(--text); }
|
|
.container { max-width:640px; margin:0 auto; padding:40px 24px; }
|
|
h2 { font-size:20px; font-weight:700; margin-bottom:6px; }
|
|
.subtitle { font-size:13px; color:var(--text-m); margin-bottom:32px; }
|
|
h3 { font-size:11px; font-weight:600; color:var(--text-m); text-transform:uppercase;
|
|
letter-spacing:.5px; margin-bottom:16px; }
|
|
.card { background:var(--bg2); border:1px solid var(--border); border-radius:12px;
|
|
padding:24px; margin-bottom:20px; }
|
|
.form-row { display:flex; flex-direction:column; gap:6px; margin-bottom:16px; }
|
|
.form-row:last-child { margin-bottom:0; }
|
|
label { font-size:12px; font-weight:600; color:var(--text-m); text-transform:uppercase; letter-spacing:.5px; }
|
|
input {
|
|
background:var(--bg3); border:1px solid var(--border); border-radius:8px;
|
|
color:var(--text); font-family:inherit; font-size:14px; padding:9px 12px; outline:none; width:100%;
|
|
}
|
|
input:focus { border-color:var(--accent); }
|
|
.btn-primary {
|
|
background:var(--accent); color:#fff; border:none; border-radius:8px;
|
|
padding:10px 20px; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer;
|
|
}
|
|
.btn-primary:hover { background:var(--accent-h); }
|
|
.btn-secondary {
|
|
background:var(--bg3); color:var(--text-m); border:1px solid var(--border); border-radius:8px;
|
|
padding:10px 20px; font-family:inherit; font-size:14px; font-weight:500; cursor:pointer;
|
|
}
|
|
.btn-secondary:hover { border-color:var(--accent); color:var(--text); }
|
|
.msg { font-size:13px; margin-top:12px; min-height:18px; }
|
|
.msg.ok { color:var(--success); }
|
|
.msg.err { color:#f87171; }
|
|
.verify-banner {
|
|
display:none; background:rgba(245,158,11,.1); border:1px solid rgba(245,158,11,.25);
|
|
border-radius:10px; padding:14px 18px; margin-bottom:24px;
|
|
font-size:13px; color:#fbbf24; line-height:1.6;
|
|
}
|
|
.verify-banner.show { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
|
.verify-banner button {
|
|
background:rgba(245,158,11,.2); border:1px solid rgba(245,158,11,.3); color:#fbbf24;
|
|
border-radius:6px; padding:5px 12px; font-size:12px; font-family:inherit; cursor:pointer; white-space:nowrap;
|
|
}
|
|
.verified-badge {
|
|
display:inline-flex; align-items:center; gap:6px; background:rgba(74,222,128,.1);
|
|
border:1px solid rgba(74,222,128,.2); color:#4ade80; border-radius:6px;
|
|
padding:4px 10px; font-size:12px; font-weight:600; margin-top:8px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<a href="/" class="logo">
|
|
<svg width="28" height="28" viewBox="0 0 30 30" fill="none">
|
|
<rect width="30" height="30" rx="9" fill="#7c3aed"/>
|
|
<polygon points="11,8 23,15 11,22" fill="white"/>
|
|
</svg>
|
|
Speechify
|
|
</a>
|
|
<div class="header-right">
|
|
<a href="/" class="back-btn">← Back to App</a>
|
|
<button class="logout-btn" onclick="logout()">Sign Out</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="container">
|
|
<h2>Account Settings</h2>
|
|
<p class="subtitle" id="subtitle">Manage your personal preferences.</p>
|
|
|
|
<!-- Email verification banner -->
|
|
<div class="verify-banner" id="verify-banner">
|
|
<span>⚠️ Your email address is not verified. Check your inbox or resend the link.</span>
|
|
<button onclick="resendVerification()">Resend Email</button>
|
|
</div>
|
|
|
|
<!-- Email / account info -->
|
|
<div class="card">
|
|
<h3>Account</h3>
|
|
<div class="form-row">
|
|
<label>Username</label>
|
|
<input id="acc-username" type="text" readonly style="opacity:.5;cursor:default"/>
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Email</label>
|
|
<input id="acc-email" type="email" readonly style="opacity:.5;cursor:default"/>
|
|
<div id="email-status"></div>
|
|
</div>
|
|
<div class="msg" id="acc-msg"></div>
|
|
</div>
|
|
|
|
<!-- Nextcloud -->
|
|
<div class="card">
|
|
<h3>Nextcloud Integration</h3>
|
|
<p style="font-size:13px;color:var(--text-m);margin-bottom:16px;line-height:1.6;">
|
|
Connect your personal Nextcloud to browse and read PDFs from the Cloud tab.
|
|
Leave blank to use the server-wide default configured by the admin.
|
|
</p>
|
|
<div class="form-row">
|
|
<label>Nextcloud URL</label>
|
|
<input id="nc-url" type="url" placeholder="https://cloud.example.com"/>
|
|
</div>
|
|
<div class="form-row">
|
|
<label>Username</label>
|
|
<input id="nc-user" type="text"/>
|
|
</div>
|
|
<div class="form-row">
|
|
<label>App Password</label>
|
|
<input id="nc-pass" type="password" placeholder="Leave blank to keep existing"/>
|
|
</div>
|
|
<button class="btn-primary" onclick="saveNextcloud()">Save</button>
|
|
<div class="msg" id="nc-msg"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let me = null;
|
|
|
|
async function init() {
|
|
const resp = await fetch('/api/auth/me');
|
|
if (resp.status === 401) { window.location.href = '/login'; return; }
|
|
me = await resp.json();
|
|
|
|
document.getElementById('acc-username').value = me.username;
|
|
document.getElementById('acc-email').value = me.email;
|
|
document.getElementById('subtitle').textContent = `Signed in as ${me.username}`;
|
|
|
|
if (!me.email_verified) {
|
|
document.getElementById('verify-banner').classList.add('show');
|
|
document.getElementById('email-status').innerHTML =
|
|
'<span style="font-size:12px;color:#f87171;">Not verified</span>';
|
|
} else {
|
|
document.getElementById('email-status').innerHTML =
|
|
'<div class="verified-badge"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg> Verified</div>';
|
|
}
|
|
|
|
// Load Nextcloud settings
|
|
const s = await fetch('/api/user/settings').then(r => r.json());
|
|
document.getElementById('nc-url').value = s.nc_url || '';
|
|
document.getElementById('nc-user').value = s.nc_user || '';
|
|
}
|
|
|
|
async function resendVerification() {
|
|
const resp = await fetch('/api/auth/resend-verification', { method: 'POST' });
|
|
if (resp.ok) {
|
|
document.getElementById('verify-banner').innerHTML =
|
|
'<span>✓ Verification email sent — check your inbox.</span>';
|
|
}
|
|
}
|
|
|
|
async function saveNextcloud() {
|
|
const msg = document.getElementById('nc-msg');
|
|
const pass = document.getElementById('nc-pass').value;
|
|
const body = {
|
|
nc_url: document.getElementById('nc-url').value,
|
|
nc_user: document.getElementById('nc-user').value,
|
|
};
|
|
if (pass) body.nc_pass = pass;
|
|
|
|
const resp = await fetch('/api/user/settings', {
|
|
method: 'POST',
|
|
headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (resp.ok) {
|
|
msg.textContent = '✓ Saved'; msg.className = 'msg ok';
|
|
document.getElementById('nc-pass').value = '';
|
|
} else {
|
|
msg.textContent = '✗ Error saving'; msg.className = 'msg err';
|
|
}
|
|
setTimeout(() => msg.textContent = '', 3000);
|
|
}
|
|
|
|
function logout() {
|
|
fetch('/api/auth/logout', { method: 'POST' }).finally(() => window.location.href = '/login');
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|