fix: hamburger menu sur toutes les pages + cookies path="/" Chrome fix
- local_workspace.html: supprimé Alpine.data dupliqué + ajout hamburger - workspaces.html, workspace.html, settings.html, page_editor.html: +hamburger - auth.py: +path="/" sur tous les set_cookie de session (Chrome compat) - csrf.py: +path="/" sur cookie CSRF Root cause des bugs: 1. Chrome: cookie sans path="/" → non envoyé sur certaines routes 2. Firefox: les templates écrasaient le block topbar → pas de hamburger → sidebar inaccessible sur mobile (overlay + slide-in ne fonctionnaient pas)
This commit is contained in:
@@ -33,6 +33,7 @@ class CSRFMiddleware(BaseHTTPMiddleware):
|
|||||||
httponly=False, # Must be readable by JS
|
httponly=False, # Must be readable by JS
|
||||||
samesite="lax",
|
samesite="lax",
|
||||||
max_age=86400,
|
max_age=86400,
|
||||||
|
path="/",
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -100,7 +100,7 @@ async def login(request: Request, provider: str = Query("gitea")):
|
|||||||
user_data = dict(user)
|
user_data = dict(user)
|
||||||
session = SessionManager.create_session(user_data)
|
session = SessionManager.create_session(user_data)
|
||||||
response = RedirectResponse(url="/workspaces", status_code=302)
|
response = RedirectResponse(url="/workspaces", status_code=302)
|
||||||
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
|
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
state = secrets.token_hex(32)
|
state = secrets.token_hex(32)
|
||||||
@@ -146,7 +146,7 @@ async def register(request: Request):
|
|||||||
session = SessionManager.create_session(user_data)
|
session = SessionManager.create_session(user_data)
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
response = JSONResponse({"status": "ok", "user": {"login": email, "name": name}})
|
response = JSONResponse({"status": "ok", "user": {"login": email, "name": name}})
|
||||||
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
|
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ async def local_login(request: Request):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
session = SessionManager.create_session(ud)
|
session = SessionManager.create_session(ud)
|
||||||
response = JSONResponse({"status": "ok", "user": {"login": ud["login"], "name": ud["full_name"]}})
|
response = JSONResponse({"status": "ok", "user": {"login": ud["login"], "name": ud["full_name"]}})
|
||||||
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
|
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ async def callback(
|
|||||||
# Create session
|
# Create session
|
||||||
session = SessionManager.create_session(user_data)
|
session = SessionManager.create_session(user_data)
|
||||||
response = RedirectResponse(url="/workspaces", status_code=302)
|
response = RedirectResponse(url="/workspaces", status_code=302)
|
||||||
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
|
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,24 @@
|
|||||||
|
|
||||||
{% block topbar %}
|
{% block topbar %}
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
|
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
<div class="topbar-left">
|
<div class="topbar-left">
|
||||||
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Workspace</span>
|
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Workspace</span>
|
||||||
<span class="page-title-topbar">{{ workspace_name }}</span>
|
<span class="page-title-topbar">{{ workspace_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-right"></div>
|
<div class="topbar-right">
|
||||||
|
<a href="/workspaces" class="page-action-btn" title="All workspaces">🏠</a>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<style>
|
<style>
|
||||||
.ws-layout{max-width:800px;margin:0 auto;padding:40px 24px;}
|
.ws-layout{max-width:800px;margin:0 auto;padding:40px 24px;}
|
||||||
.ws-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;}
|
@media (max-width: 768px) { .ws-layout { padding: 20px 12px; } }
|
||||||
|
.ws-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;flex-wrap:wrap;gap:12px;}
|
||||||
.ws-header h1{font-size:24px;}
|
.ws-header h1{font-size:24px;}
|
||||||
.ws-toolbar{display:flex;gap:8px;}
|
.ws-toolbar{display:flex;gap:8px;}
|
||||||
|
|
||||||
@@ -55,79 +61,6 @@
|
|||||||
.delete-confirm{padding:12px;background:rgba(255,80,80,.08);border-radius:8px;margin-top:12px;font-size:13px;color:var(--text-secondary);}
|
.delete-confirm{padding:12px;background:rgba(255,80,80,.08);border-radius:8px;margin-top:12px;font-size:13px;color:var(--text-secondary);}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('alpine:init', function() {
|
|
||||||
Alpine.data('wsInit', function() {
|
|
||||||
return {
|
|
||||||
tree:[],
|
|
||||||
loaded:false,
|
|
||||||
showCreate:false, showRename:false, showDelete:false,
|
|
||||||
createType:'file', newName:'', parentFolder:null, target:null,
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
try {
|
|
||||||
var r = await fetch('/api/local-workspace/tree');
|
|
||||||
var d = await r.json();
|
|
||||||
this.tree = flatten(d.tree||[],0);
|
|
||||||
this.loaded = true;
|
|
||||||
} catch(e) { this.loaded = true; }
|
|
||||||
},
|
|
||||||
|
|
||||||
openModal(type, parent) {
|
|
||||||
this.createType = type; this.parentFolder = parent||null; this.newName = '';
|
|
||||||
this.showCreate = true;
|
|
||||||
var self = this;
|
|
||||||
this.$nextTick(function() {
|
|
||||||
var el = self.$refs.cinp;
|
|
||||||
if (el) setTimeout(function() { el.focus(); }, 100);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async doCreate() {
|
|
||||||
var n = this.newName.trim(); if (!n) return;
|
|
||||||
var body = { name:n, type:this.createType };
|
|
||||||
if (this.parentFolder) body.parent_id = this.parentFolder.db_id;
|
|
||||||
var r = await fetch('/api/local-workspace/items', {
|
|
||||||
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)
|
|
||||||
});
|
|
||||||
if (r.ok) {
|
|
||||||
this.showCreate = false; this.parentFolder = null; this.newName = '';
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
openPage(id) { window.location = '/pages/'+id; },
|
|
||||||
|
|
||||||
startRename(node) {
|
|
||||||
this.target = node; this.newName = node.name.trim(); this.showRename = true;
|
|
||||||
var self = this;
|
|
||||||
this.$nextTick(function() {
|
|
||||||
var el = self.$refs.rinp;
|
|
||||||
if (el) setTimeout(function() { el.focus(); }, 100);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async doRename() {
|
|
||||||
var n = this.newName.trim(); if (!n||!this.target) return;
|
|
||||||
await fetch('/api/local-workspace/items/'+this.target.db_id, {
|
|
||||||
method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:n})
|
|
||||||
});
|
|
||||||
this.showRename = false; this.target = null; this.newName = '';
|
|
||||||
await this.init();
|
|
||||||
},
|
|
||||||
|
|
||||||
startDelete(node) { this.target = node; this.showDelete = true; },
|
|
||||||
|
|
||||||
async doDelete() {
|
|
||||||
if (!this.target) return;
|
|
||||||
await fetch('/api/local-workspace/items/'+this.target.db_id, {method:'DELETE'});
|
|
||||||
this.showDelete = false; this.target = null;
|
|
||||||
await this.init();
|
|
||||||
}
|
|
||||||
};});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="ws-layout" x-data="wsInit()">
|
<div class="ws-layout" x-data="wsInit()">
|
||||||
|
|
||||||
<div class="ws-header">
|
<div class="ws-header">
|
||||||
@@ -214,6 +147,17 @@ document.addEventListener('alpine:init', function() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function flatten(nodes, depth) {
|
||||||
|
var result = [];
|
||||||
|
for (var i=0; i<nodes.length; i++) {
|
||||||
|
var n = nodes[i];
|
||||||
|
var isFolder = n.type==='folder';
|
||||||
|
result.push({db_id:n.id, name:n.name, icon:isFolder?'📁':'📄', is_folder:isFolder, depth:depth});
|
||||||
|
if (n.children) result = result.concat(flatten(n.children, depth+1));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('alpine:init', function() {
|
document.addEventListener('alpine:init', function() {
|
||||||
Alpine.data('wsInit', function() {
|
Alpine.data('wsInit', function() {
|
||||||
return {
|
return {
|
||||||
@@ -263,9 +207,6 @@ document.addEventListener('alpine:init', function() {
|
|||||||
this.parentFolder = null;
|
this.parentFolder = null;
|
||||||
this.newName = '';
|
this.newName = '';
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
|
||||||
var e = await r.json();
|
|
||||||
alert(e.error||'Failed');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -307,16 +248,5 @@ document.addEventListener('alpine:init', function() {
|
|||||||
}
|
}
|
||||||
};});
|
};});
|
||||||
});
|
});
|
||||||
|
|
||||||
function flatten(nodes, depth) {
|
|
||||||
var result = [];
|
|
||||||
for (var i=0; i<nodes.length; i++) {
|
|
||||||
var n = nodes[i];
|
|
||||||
var isFolder = n.type==='folder';
|
|
||||||
result.push({db_id:n.id, name:n.name, icon:isFolder?'📁':'📄', is_folder:isFolder, depth:depth});
|
|
||||||
if (n.children) result = result.concat(flatten(n.children, depth+1));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
{% extends "base.html" %} {% block page_icon %}📄{% endblock %} {% block
|
{% extends "base.html" %} {% block page_icon %}📄{% endblock %} {% block
|
||||||
page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||||
<!-- Page editor uses its own topbar -->
|
<header class="topbar" style="background:var(--bg-primary);">
|
||||||
|
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
<div class="topbar-left">
|
||||||
|
<span class="breadcrumb" style="color:rgba(255,255,255,.5);font-size:13px">{{ page.title }}</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
{% endblock %} {% block content %}
|
{% endblock %} {% block content %}
|
||||||
<div class="page-editor-wrapper" x-data="editorState()">
|
<div class="page-editor-wrapper" x-data="editorState()">
|
||||||
<!-- ═══════════ Top Action Bar ═══════════ -->
|
<!-- ═══════════ Top Action Bar ═══════════ -->
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
{% block topbar %}
|
{% block topbar %}
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
|
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
<div class="topbar-left">
|
<div class="topbar-left">
|
||||||
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">
|
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">
|
||||||
Settings
|
Settings
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
{% block topbar %}
|
{% block topbar %}
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
|
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
<div class="topbar-left">
|
<div class="topbar-left">
|
||||||
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Workspace</span>
|
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Workspace</span>
|
||||||
<span class="page-title-topbar">Projects</span>
|
<span class="page-title-topbar">Projects</span>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
{% block topbar %}
|
{% block topbar %}
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
|
<button class="hamburger-btn" @click="mobileSidebarOpen = true; sidebarCollapsed = false;" title="Menu">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
<div class="topbar-left">
|
<div class="topbar-left">
|
||||||
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Home</span>
|
<span class="breadcrumb" style="color:rgba(255,255,255,.5)">Home</span>
|
||||||
<span class="page-title-topbar">Workspaces</span>
|
<span class="page-title-topbar">Workspaces</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user