Ajout : - e2e/csp_preview.spec.js — apercu CSP strict SANS deployer : le build officiel @alpinejs/csp (e2e/fixtures/alpine.csp.js, 0 eval) est servi a la place de alpine.min.js par interception Playwright ; toute expression que le parseur maison ne digere pas = pageerror (filet). Premiere surface VERTE : library (composant lie, icones SVG via Alpine.effect, recherche ouverte + focalisee, 0 erreur). Changed : - 16 composants x-data="fn()" enregistres via Alpine.data (registre = seule resolution du build CSP, probe « Undefined variable » ; scripts classiques executes pendant le parsing => alpine:init toujours joint) : appState, libraryPage, workspacesPage, editorState, board x4, settings/import/table_view/team_load/trash/workspace/welcome/accounts/ card_detail. - base.html (shell) migre : x-effect document.* -> syncSidebarClass(), $nextTick(arrow) -> initSidebarSort(), window.FlowDeck.* -> fdCreatePage/fdCreateFolder/fdGwRefresh, Object.keys/Math.min/ window.innerWidth dans x-for et :style -> sidebarSections()/ sectionMenuPos() — tout = simple appel de methode. - x-html restants du shell -> x-init + Alpine.effect : icone agent, carte projet, library x3 ; recherche library -> toggleSearch() (Alpine.nextTick) ; openMoveSelected() pour Object.keys en expression. - eslint : 70 warnings -> 0/0 (globals getCsrf depuis A38 ph1, /* exported openCardDetail */ + /* global owner, repo */, 3 ;; residuels). suite **1093/1093** · ruff OK · eslint 0/0 · E2E 3/3 (csp_preview + smoke x2) · docs a jour (ROADMAP ph3 LOT 1, CHANGELOG, WORKLOAD, VERSION)
138 lines
5.6 KiB
HTML
138 lines
5.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block page_icon %}{{ fd_icon("user",18) }}{% endblock %}
|
|
{% block page_title %}Accounts{% endblock %}
|
|
{% block title_prefix %}Accounts{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-title-area">
|
|
<div class="page-title">
|
|
<span class="page-icon-lg">{{ fd_icon("user",24) }}</span>
|
|
<h1>Account Management</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="padding: 0 24px; max-width: 900px;" x-data="accountsData()">
|
|
<!-- Current user profile -->
|
|
<div style="background:var(--bg-secondary); border-radius:8px; padding:20px; margin-bottom:24px;">
|
|
<h3 style="margin-bottom:16px;">Your Profile</h3>
|
|
<div style="display:flex; gap:16px; align-items:flex-start;">
|
|
<div class="workspace-avatar" style="width:48px; height:48px; font-size:20px;">
|
|
{{ user.login[0] if user else 'B' }}
|
|
</div>
|
|
<div style="flex:1;">
|
|
<div style="display:flex; gap:12px; margin-bottom:12px;">
|
|
<div style="flex:1;">
|
|
<label style="font-size:12px; color:var(--text-dim);">Username</label>
|
|
<input type="text" value="{{ user.login }}" disabled
|
|
style="width:100%; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:6px;
|
|
padding:8px; color:var(--text-dim); font-size:14px;">
|
|
</div>
|
|
<div style="flex:1;">
|
|
<label style="font-size:12px; color:var(--text-dim);">Display name</label>
|
|
<input type="text" x-model="profile.full_name"
|
|
style="width:100%; background:var(--bg-secondary); border:1px solid var(--border); border-radius:6px;
|
|
padding:8px; color:var(--text-primary); font-size:14px; outline:none;">
|
|
</div>
|
|
</div>
|
|
<div style="margin-bottom:12px;">
|
|
<label style="font-size:12px; color:var(--text-dim);">Email</label>
|
|
<input type="text" x-model="profile.email"
|
|
style="width:100%; background:var(--bg-secondary); border:1px solid var(--border); border-radius:6px;
|
|
padding:8px; color:var(--text-primary); font-size:14px; outline:none;">
|
|
</div>
|
|
<button class="btn-new" @click="saveProfile()">Save Profile</button>
|
|
<span x-show="saved" style="color:var(--green); font-size:13px; margin-left:8px;">✓ Saved</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Connected accounts -->
|
|
<div style="background:var(--bg-secondary); border-radius:8px; padding:20px; margin-bottom:24px;">
|
|
<h3 style="margin-bottom:12px;">Connected Accounts</h3>
|
|
<div style="display:flex; align-items:center; gap:12px; padding:12px; border:1px solid var(--border); border-radius:6px;">
|
|
<span style="font-size:20px;">{{ fd_icon("link",20) }}</span>
|
|
<div style="flex:1;">
|
|
<div style="font-weight:600;">Gitea</div>
|
|
<div style="font-size:12px; color:var(--text-dim);">
|
|
{% if user %}Connected as {{ user.login }}{% else %}Not connected{% endif %}
|
|
</div>
|
|
</div>
|
|
{% if user %}
|
|
<a href="/auth/logout" class="toolbar-btn" style="color:var(--red);">Disconnect</a>
|
|
{% else %}
|
|
<a href="/auth/login" class="btn-new">Connect Gitea</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Users in workspace -->
|
|
<div style="background:var(--bg-secondary); border-radius:8px; padding:20px;">
|
|
<h3 style="margin-bottom:12px;">Workspace Members</h3>
|
|
<div style="font-size:13px; color:var(--text-dim); margin-bottom:12px;">
|
|
Members are managed through Gitea. New users connect via OAuth and appear here automatically.
|
|
</div>
|
|
<table class="data-table" style="width:100%;">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Email</th>
|
|
<th>Joined</th>
|
|
<th>Role</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for u in users %}
|
|
<tr>
|
|
<td>
|
|
<div style="display:flex; align-items:center; gap:8px;">
|
|
<div class="card-avatar" style="width:24px; height:24px; font-size:11px;">{{ u.login[0] }}</div>
|
|
<span>{{ u.login }}</span>
|
|
</div>
|
|
</td>
|
|
<td>{{ u.email or '—' }}</td>
|
|
<td>{{ u.created_at[:10] if u.created_at else '—' }}</td>
|
|
<td>{% if u.is_admin %}Admin{% else %}Member{% endif %}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% if not users %}
|
|
<tr><td colspan="4" style="text-align:center; color:var(--text-dim); padding:24px;">No members yet</td></tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script data-cfasync="false" nonce="{{ csp_nonce() }}">
|
|
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
|
|
|
// globales window — probe « Undefined variable: accountsData »).
|
|
|
|
document.addEventListener('alpine:init', function () { Alpine.data('accountsData', accountsData); });
|
|
function accountsData() {
|
|
return {
|
|
profile: { full_name: '', email: '' },
|
|
saved: false,
|
|
async init() {
|
|
try {
|
|
const r = await fetch('/api/users/me');
|
|
const data = await r.json();
|
|
this.profile = { full_name: data.full_name || '', email: data.email || '' };
|
|
} catch(e) {}
|
|
},
|
|
saveProfile() {
|
|
const csrf = getCsrf();;
|
|
const token = csrf;
|
|
fetch(`/api/users/me?full_name=${encodeURIComponent(this.profile.full_name)}&email=${encodeURIComponent(this.profile.email)}`, {
|
|
method: 'PUT', headers: { 'X-CSRF-Token': token }
|
|
}).then(() => {
|
|
this.saved = true;
|
|
setTimeout(() => this.saved = false, 3000);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|