feat: connected accounts + Gemini API key in account settings UI
Settings page gains two new sections: - Connected Accounts: shows linked Google email (read-only) - Gemini API Key: paste personal key from aistudio.google.com, shows masked hint of saved key, remove link to revert to server key POST /settings/gemini-key saves/clears gemini_api_key in auth.json. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ import jwt
|
|||||||
from fastapi import APIRouter, Form, Request
|
from fastapi import APIRouter, Form, Request
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
|
|
||||||
from auth_utils import COOKIE_NAME, decode_token, check_credentials, set_password
|
from auth_utils import COOKIE_NAME, decode_token, check_credentials, set_password, _read_auth, _write_auth
|
||||||
from persona import list_user_personas
|
from persona import list_user_personas
|
||||||
from config import settings as app_settings
|
from config import settings as app_settings
|
||||||
|
|
||||||
@@ -41,6 +41,20 @@ def _get_session_user(request: Request) -> str | None:
|
|||||||
def _settings_page(username: str, personas: list[str], success: str = "", error: str = "") -> str:
|
def _settings_page(username: str, personas: list[str], success: str = "", error: str = "") -> str:
|
||||||
html = (_STATIC / "settings.html").read_text()
|
html = (_STATIC / "settings.html").read_text()
|
||||||
html = html.replace("{{ username }}", username)
|
html = html.replace("{{ username }}", username)
|
||||||
|
|
||||||
|
# Connected Google account
|
||||||
|
auth_data = _read_auth(username)
|
||||||
|
google_email = auth_data.get("google_email") or ""
|
||||||
|
html = html.replace("{{ google_email }}", google_email)
|
||||||
|
|
||||||
|
# Gemini API key — show masked hint only, never the full key
|
||||||
|
gemini_key = auth_data.get("gemini_api_key") or ""
|
||||||
|
if gemini_key:
|
||||||
|
hint = f"Saved (…{gemini_key[-4:]})"
|
||||||
|
else:
|
||||||
|
hint = "Using server key"
|
||||||
|
html = html.replace("{{ gemini_key_hint }}", hint)
|
||||||
|
html = html.replace("{{ gemini_key_set }}", "true" if gemini_key else "false")
|
||||||
persona_items = "\n".join(
|
persona_items = "\n".join(
|
||||||
f'''<li>
|
f'''<li>
|
||||||
<a href="/{username}/{p}" class="persona-link">{p}</a>
|
<a href="/{username}/{p}" class="persona-link">{p}</a>
|
||||||
@@ -139,6 +153,30 @@ async def rename_username(
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/settings/gemini-key", include_in_schema=False)
|
||||||
|
async def save_gemini_key(
|
||||||
|
request: Request,
|
||||||
|
gemini_api_key: str = Form(...),
|
||||||
|
):
|
||||||
|
username = _get_session_user(request)
|
||||||
|
if not username:
|
||||||
|
return RedirectResponse("/login", status_code=302)
|
||||||
|
|
||||||
|
personas = list_user_personas(username)
|
||||||
|
gemini_api_key = gemini_api_key.strip()
|
||||||
|
|
||||||
|
data = _read_auth(username)
|
||||||
|
if gemini_api_key:
|
||||||
|
data["gemini_api_key"] = gemini_api_key
|
||||||
|
msg = "Gemini API key saved."
|
||||||
|
else:
|
||||||
|
data.pop("gemini_api_key", None)
|
||||||
|
msg = "Gemini API key removed — using server key."
|
||||||
|
_write_auth(username, data)
|
||||||
|
logger.info("gemini key updated: %s", username)
|
||||||
|
return HTMLResponse(_settings_page(username, personas, success=msg))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/settings/persona/rename", include_in_schema=False)
|
@router.post("/settings/persona/rename", include_in_schema=False)
|
||||||
async def rename_persona(
|
async def rename_persona(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -232,6 +232,45 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Connected accounts -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Connected Accounts</h2>
|
||||||
|
<div class="field">
|
||||||
|
<label>Google Account</label>
|
||||||
|
<input type="text" value="{{ google_email }}" readonly
|
||||||
|
placeholder="No Google account linked"
|
||||||
|
style="{{ google_email == '' and 'color:#475569' or '' }}">
|
||||||
|
</div>
|
||||||
|
<p style="font-size:0.75rem; color:#94a3b8; margin-top:-0.5rem;">
|
||||||
|
To link or change your Google account, contact Scott.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gemini API key -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Gemini API Key</h2>
|
||||||
|
<p style="font-size:0.8rem; color:#94a3b8; margin-bottom:0.85rem; line-height:1.55;">
|
||||||
|
Paste your personal key from
|
||||||
|
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener"
|
||||||
|
style="color:#a78bfa;">aistudio.google.com/apikey</a>
|
||||||
|
to use your own Gemini quota. Leave blank to use the shared server key.
|
||||||
|
</p>
|
||||||
|
<form method="POST" action="/settings/gemini-key">
|
||||||
|
<div class="field">
|
||||||
|
<label for="gemini_api_key">API Key</label>
|
||||||
|
<input type="password" id="gemini_api_key" name="gemini_api_key"
|
||||||
|
placeholder="{{ gemini_key_hint }}" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<button type="submit">Save Key</button>
|
||||||
|
</form>
|
||||||
|
<p id="gemini-key-status" style="font-size:0.75rem; color:#94a3b8; margin-top:0.5rem;">
|
||||||
|
Current: {{ gemini_key_hint }}
|
||||||
|
<span id="gemini-remove-wrap" style="{{ gemini_key_set == 'false' and 'display:none' or '' }}">
|
||||||
|
— <a href="#" id="gemini-remove-link" style="color:#f87171;">remove</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Change password -->
|
<!-- Change password -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Change Password</h2>
|
<h2>Change Password</h2>
|
||||||
@@ -287,6 +326,16 @@
|
|||||||
document.getElementById('show-rename-user').style.display = '';
|
document.getElementById('show-rename-user').style.display = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Gemini key — "remove" link clears the input and submits the form
|
||||||
|
const geminiRemove = document.getElementById('gemini-remove-link');
|
||||||
|
if (geminiRemove) {
|
||||||
|
geminiRemove.addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('gemini_api_key').value = '';
|
||||||
|
document.querySelector('form[action="/settings/gemini-key"]').submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Persona rename toggle
|
// Persona rename toggle
|
||||||
document.querySelectorAll('.persona-rename-toggle').forEach(btn => {
|
document.querySelectorAll('.persona-rename-toggle').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user