feat: Gitea — register, settings connect, tabs org, search
1. Pas de Gitea sans config: _require_gitea() → 401 si pas de token OAuth 2. Register Gitea: boutons "Login/Register with Gitea" dynamiques selon l'onglet 3. Settings → Integrations: ✅ Active / 🔗 Connect / Disconnect + API status/disconnect 4. Workspaces: tabs par org (All | org1 | org2) + barre recherche 🔍 filtrage live 5. API: GET /api/gitea/status, DELETE /api/gitea/disconnect
This commit is contained in:
+4
-3
@@ -70,13 +70,14 @@ body{background:#191919;color:#fff;font-family:-apple-system,BlinkMacSystemFont,
|
|||||||
<button type="submit" class="btn btn-primary" id="submit-btn">Login</button>
|
<button type="submit" class="btn btn-primary" id="submit-btn">Login</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="oauth-section">
|
<div class="oauth-section">
|
||||||
<button class="oauth-btn" onclick="window.location='/auth/login?provider=gitea'" style="margin-bottom:8px">🔗 Login with Gitea</button>
|
<p style="font-size:13px;color:rgba(255,255,255,.4);margin-bottom:8px;">Or continue with</p>
|
||||||
<button class="oauth-btn" onclick="window.location='/auth/login?provider=github'">🐙 Login with GitHub</button>
|
<button class="oauth-btn" onclick="window.location='/auth/login?provider=gitea'" style="margin-bottom:8px">🔗 <span id="gitea-btn-label">Login</span> with Gitea</button>
|
||||||
|
<button class="oauth-btn" onclick="window.location='/auth/login?provider=github'">🐙 <span id="github-btn-label">Login</span> with GitHub</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
let mode='login';
|
let mode='login';
|
||||||
function switchTab(t){mode=t;document.querySelectorAll('.tab').forEach(el=>el.classList.remove('active'));document.getElementById('tab-'+t).classList.add('active');document.getElementById('name-group').style.display=t==='register'?'block':'none';document.getElementById('submit-btn').textContent=t==='register'?'Register':'Login';document.getElementById('error-msg').style.display='none';document.getElementById('success-msg').style.display='none';}
|
function switchTab(t){mode=t;document.querySelectorAll('.tab').forEach(el=>el.classList.remove('active'));document.getElementById('tab-'+t).classList.add('active');document.getElementById('name-group').style.display=t==='register'?'block':'none';document.getElementById('submit-btn').textContent=t==='register'?'Register':'Login';document.getElementById('error-msg').style.display='none';document.getElementById('success-msg').style.display='none';var lbl=t==='register'?'Register':'Login';var e1=document.getElementById('gitea-btn-label');var e2=document.getElementById('github-btn-label');if(e1)e1.textContent=lbl;if(e2)e2.textContent=lbl;}
|
||||||
function togglePassword(){var pw=document.getElementById('password');var btn=pw.parentElement.querySelector('.pw-toggle');if(pw.type==='password'){pw.type='text';btn.textContent='🙈';}else{pw.type='password';btn.textContent='👁';}}
|
function togglePassword(){var pw=document.getElementById('password');var btn=pw.parentElement.querySelector('.pw-toggle');if(pw.type==='password'){pw.type='text';btn.textContent='🙈';}else{pw.type='password';btn.textContent='👁';}}
|
||||||
async function handleLogin(e){e.preventDefault();const email=document.getElementById('email').value;const password=document.getElementById('password').value;const name=document.getElementById('name').value;const url=mode==='register'?'/auth/register':'/auth/local-login';const body={email,password};if(mode==='register')body.name=name;try{const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});const d=await r.json();if(!r.ok){document.getElementById('error-msg').textContent=d.error||'Error';document.getElementById('error-msg').style.display='block';return;}if(d.status==='ok'){window.location='/';}}catch(err){document.getElementById('error-msg').textContent='Network error';document.getElementById('error-msg').style.display='block';}}
|
async function handleLogin(e){e.preventDefault();const email=document.getElementById('email').value;const password=document.getElementById('password').value;const name=document.getElementById('name').value;const url=mode==='register'?'/auth/register':'/auth/local-login';const body={email,password};if(mode==='register')body.name=name;try{const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});const d=await r.json();if(!r.ok){document.getElementById('error-msg').textContent=d.error||'Error';document.getElementById('error-msg').style.display='block';return;}if(d.status==='ok'){window.location='/';}}catch(err){document.getElementById('error-msg').textContent='Network error';document.getElementById('error-msg').style.display='block';}}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+27
-4
@@ -6,12 +6,11 @@ router = APIRouter(tags=["gitea"], prefix="/api/gitea")
|
|||||||
|
|
||||||
|
|
||||||
def _require_gitea(request: Request):
|
def _require_gitea(request: Request):
|
||||||
"""Return a per-user GiteaClient or fallback to admin token."""
|
"""Return a per-user GiteaClient or raise 401."""
|
||||||
from app.services.gitea_client import get_user_gitea_client, GiteaClient
|
from app.services.gitea_client import get_user_gitea_client
|
||||||
client = get_user_gitea_client(request)
|
client = get_user_gitea_client(request)
|
||||||
if not client:
|
if not client:
|
||||||
# Fallback to server admin token
|
raise HTTPException(status_code=401, detail="Gitea not connected. Go to Settings → Integrations or register with Gitea.")
|
||||||
client = GiteaClient()
|
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
@@ -129,3 +128,27 @@ async def get_labels(request: Request, owner: str, repo: str):
|
|||||||
]}
|
]}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse({"error": str(e)}, status_code=502)
|
return JSONResponse({"error": str(e)}, status_code=502)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Account linking ──
|
||||||
|
|
||||||
|
@router.get("/status")
|
||||||
|
async def gitea_status(request: Request):
|
||||||
|
"""Check if the current user has Gitea linked."""
|
||||||
|
from app.services.gitea_client import get_user_gitea_client
|
||||||
|
client = get_user_gitea_client(request)
|
||||||
|
return {"linked": client is not None}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/disconnect")
|
||||||
|
async def disconnect_gitea(request: Request):
|
||||||
|
"""Remove all Gitea OAuth tokens for the current user."""
|
||||||
|
from app.auth.session import SessionManager
|
||||||
|
from app.db import get_conn
|
||||||
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||||
|
if not user:
|
||||||
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||||
|
with get_conn() as conn:
|
||||||
|
conn.execute("DELETE FROM user_oauth_tokens WHERE user_id=? AND provider='gitea'", (user["id"],))
|
||||||
|
conn.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|||||||
@@ -318,9 +318,16 @@
|
|||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div>
|
<div>
|
||||||
<div class="setting-label">Gitea</div>
|
<div class="setting-label">Gitea</div>
|
||||||
<div class="setting-desc">Connected to git.dracodev.net</div>
|
<div class="setting-desc">
|
||||||
|
<span x-show="giteaLinked">Connected to git.dracodev.net</span>
|
||||||
|
<span x-show="!giteaLinked">Connect your Gitea account to access projects</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<span x-show="giteaLinked" style="font-size:12px;color:#0F7B6C;">✅ Active</span>
|
||||||
|
<button x-show="giteaLinked" class="btn-sm" style="color:var(--danger);" @click="disconnectGitea()">Disconnect</button>
|
||||||
|
<a x-show="!giteaLinked" class="btn-sm" href="/auth/login?provider=gitea" style="text-decoration:none;color:var(--accent);">🔗 Connect</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-control"><span style="font-size:12px;color:#0F7B6C;">✅ Active</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-row">
|
<div class="setting-row">
|
||||||
<div>
|
<div>
|
||||||
@@ -502,6 +509,8 @@ document.addEventListener('alpine:init', function() {
|
|||||||
defaultView: localStorage.getItem('fd_default_view') || 'tree',
|
defaultView: localStorage.getItem('fd_default_view') || 'tree',
|
||||||
userInfo: {full_name: '{{ user.full_name or "" }}', login: '{{ user.login or "" }}', email: '{{ user.email or "" }}'},
|
userInfo: {full_name: '{{ user.full_name or "" }}', login: '{{ user.login or "" }}', email: '{{ user.email or "" }}'},
|
||||||
userIsAdmin: {{ 'true' if user.get('is_admin') else 'false' }},
|
userIsAdmin: {{ 'true' if user.get('is_admin') else 'false' }},
|
||||||
|
// Gitea
|
||||||
|
giteaLinked: false,
|
||||||
// Account form
|
// Account form
|
||||||
accountForm: {login:'{{ user.login or "" }}', full_name:'{{ user.full_name or "" }}', email:'{{ user.email or "" }}', password:''},
|
accountForm: {login:'{{ user.login or "" }}', full_name:'{{ user.full_name or "" }}', email:'{{ user.email or "" }}', password:''},
|
||||||
accountSaving: false,
|
accountSaving: false,
|
||||||
@@ -518,6 +527,7 @@ document.addEventListener('alpine:init', function() {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.loadTags();
|
await this.loadTags();
|
||||||
|
await this.loadGiteaStatus();
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadTags() {
|
async loadTags() {
|
||||||
@@ -716,6 +726,21 @@ document.addEventListener('alpine:init', function() {
|
|||||||
saveDefaultView() {
|
saveDefaultView() {
|
||||||
localStorage.setItem('fd_default_view', this.defaultView);
|
localStorage.setItem('fd_default_view', this.defaultView);
|
||||||
}
|
}
|
||||||
|
async loadGiteaStatus() {
|
||||||
|
try {
|
||||||
|
var r = await fetch('/api/gitea/status');
|
||||||
|
var d = await r.json();
|
||||||
|
this.giteaLinked = d.linked || false;
|
||||||
|
} catch(e) { this.giteaLinked = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
async disconnectGitea() {
|
||||||
|
if (!confirm('Disconnect Gitea? You can reconnect anytime.')) return;
|
||||||
|
try {
|
||||||
|
var r = await fetch('/api/gitea/disconnect', { method: 'DELETE', headers: {'X-CSRF-Token': this.getCsrfToken()} });
|
||||||
|
if (r.ok) { this.giteaLinked = false; }
|
||||||
|
} catch(e) {}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,6 +63,12 @@
|
|||||||
.btn-new:hover{background:var(--bg-tertiary);color:var(--text);}
|
.btn-new:hover{background:var(--bg-tertiary);color:var(--text);}
|
||||||
.btn-sm{padding:4px 8px;font-size:11px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-dim);cursor:pointer;}
|
.btn-sm{padding:4px 8px;font-size:11px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-dim);cursor:pointer;}
|
||||||
.btn-sm:hover{background:var(--bg-tertiary);color:var(--text);}
|
.btn-sm:hover{background:var(--bg-tertiary);color:var(--text);}
|
||||||
|
/* Org tabs */
|
||||||
|
.org-tab{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;font-size:13px;color:var(--text-dim);background:transparent;border:1px solid var(--border);cursor:pointer;white-space:nowrap;transition:all 100ms;}
|
||||||
|
.org-tab:hover{background:var(--bg-tertiary);color:var(--text);}
|
||||||
|
.org-tab.active{background:var(--accent);color:#fff;border-color:var(--accent);}
|
||||||
|
.org-tab-badge{font-size:10px;padding:0 5px;border-radius:8px;background:rgba(255,255,255,.15);}
|
||||||
|
.search-input:focus{border-color:var(--accent);}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="ws-page" x-data="workspacesPage()">
|
<div class="ws-page" x-data="workspacesPage()">
|
||||||
@@ -98,7 +104,9 @@
|
|||||||
<!-- Not linked -->
|
<!-- Not linked -->
|
||||||
<div class="gitea-empty" x-show="giteaStatus==='not_linked'">
|
<div class="gitea-empty" x-show="giteaStatus==='not_linked'">
|
||||||
<div>Connect your Gitea account to see your projects.</div>
|
<div>Connect your Gitea account to see your projects.</div>
|
||||||
<a class="gitea-connect" href="/auth/login?provider=gitea">🔗 Login with Gitea</a>
|
<a class="gitea-connect" href="/auth/login?provider=gitea">🔗 Connect Gitea</a>
|
||||||
|
<span style="margin:0 8px;color:var(--text-dim);">or</span>
|
||||||
|
<a class="gitea-connect" href="/auth/login?provider=gitea">📝 Register with Gitea</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading -->
|
<!-- Loading -->
|
||||||
@@ -110,35 +118,57 @@
|
|||||||
<button class="gitea-connect" @click="loadGitea()">Retry</button>
|
<button class="gitea-connect" @click="loadGitea()">Retry</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Projects (grouped by org) -->
|
<!-- Projects (tabs by org + search) -->
|
||||||
<template x-for="group in giteaGroups" :key="group.org">
|
<div x-show="giteaStatus==='ok' && giteaGroups.length > 0">
|
||||||
<div class="gitea-org">
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap;">
|
||||||
<div class="gitea-org-header">
|
<!-- Org tabs -->
|
||||||
<img x-show="group.avatar" :src="group.avatar" class="gitea-org-avatar" alt="">
|
<div style="display:flex;gap:4px;overflow-x:auto;flex:1;min-width:0;">
|
||||||
<span class="gitea-org-avatar" x-show="!group.avatar" x-text="group.org.charAt(0).toUpperCase()"></span>
|
<button class="org-tab" :class="{active:activeOrg==='all'}" @click="activeOrg='all'">
|
||||||
<span x-text="group.org"></span>
|
All <span class="org-tab-badge" x-text="giteaTotal"></span>
|
||||||
</div>
|
</button>
|
||||||
<div class="ws-grid">
|
<template x-for="g in giteaGroups" :key="g.org">
|
||||||
<template x-for="proj in group.projects" :key="proj.id">
|
<button class="org-tab" :class="{active:activeOrg===g.org}" @click="activeOrg=g.org">
|
||||||
<div class="ws-card" @click="openGitea(proj)">
|
<span x-text="g.org"></span>
|
||||||
<div class="ws-card-name" x-text="proj.name"></div>
|
<span class="org-tab-badge" x-text="g.projects.length"></span>
|
||||||
<div class="ws-card-sub">
|
</button>
|
||||||
<span x-text="proj.owner?.login || group.org"></span>
|
|
||||||
<span style="margin:0 4px">/</span>
|
|
||||||
<span x-text="proj.name"></span>
|
|
||||||
</div>
|
|
||||||
<div class="ws-card-count">
|
|
||||||
<span x-show="proj.private" style="margin-right:6px">🔒</span>
|
|
||||||
<span x-text="proj.description || ''" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Search -->
|
||||||
|
<input type="text" class="search-input" x-model="giteaSearch" placeholder="🔍 Filter projects…" style="width:200px;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none;">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- No projects in orgs -->
|
<!-- Project cards (filtered) -->
|
||||||
<div class="gitea-empty" x-show="giteaStatus==='ok' && giteaTotal==0">
|
<template x-for="group in filteredGroups" :key="group.org">
|
||||||
|
<div class="gitea-org">
|
||||||
|
<div class="gitea-org-header">
|
||||||
|
<span class="gitea-org-avatar" x-text="group.org.charAt(0).toUpperCase()"></span>
|
||||||
|
<span x-text="group.org"></span>
|
||||||
|
</div>
|
||||||
|
<div class="ws-grid">
|
||||||
|
<template x-for="proj in group.projects" :key="proj.id">
|
||||||
|
<div class="ws-card" @click="openGitea(proj)">
|
||||||
|
<div class="ws-card-name" x-text="proj.name"></div>
|
||||||
|
<div class="ws-card-sub">
|
||||||
|
<span x-text="group.org"></span>
|
||||||
|
<span style="margin:0 4px">/</span>
|
||||||
|
<span x-text="proj.name"></span>
|
||||||
|
</div>
|
||||||
|
<div class="ws-card-count">
|
||||||
|
<span x-show="proj.private" style="margin-right:6px">🔒</span>
|
||||||
|
<span x-text="proj.description || proj.language || ''" style="display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px;"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="gitea-empty" x-show="filteredGroups.length===0" style="margin-top:12px;">
|
||||||
|
No projects match your search.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No projects at all -->
|
||||||
|
<div class="gitea-empty" x-show="giteaStatus==='ok' && giteaTotal==0 && giteaGroups.length===0">
|
||||||
No Gitea projects found.
|
No Gitea projects found.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,6 +201,20 @@ function workspacesPage() {
|
|||||||
giteaStatus: 'loading', // loading|ok|not_linked|error
|
giteaStatus: 'loading', // loading|ok|not_linked|error
|
||||||
giteaGroups: [],
|
giteaGroups: [],
|
||||||
giteaTotal: 0,
|
giteaTotal: 0,
|
||||||
|
activeOrg: 'all',
|
||||||
|
giteaSearch: '',
|
||||||
|
|
||||||
|
get filteredGroups() {
|
||||||
|
var self = this;
|
||||||
|
var q = (this.giteaSearch || '').toLowerCase();
|
||||||
|
var groups = this.activeOrg === 'all' ? this.giteaGroups : this.giteaGroups.filter(function(g) { return g.org === self.activeOrg; });
|
||||||
|
if (!q) return groups;
|
||||||
|
return groups.map(function(g) {
|
||||||
|
return {org: g.org, avatar: g.avatar, projects: g.projects.filter(function(p) {
|
||||||
|
return p.name.toLowerCase().includes(q) || (p.description||'').toLowerCase().includes(q);
|
||||||
|
})};
|
||||||
|
}).filter(function(g) { return g.projects.length > 0; });
|
||||||
|
},
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.load();
|
await this.load();
|
||||||
|
|||||||
Reference in New Issue
Block a user