feat(auth): implement web authentication with Keycloak OAuth2
- Add auth routes: /auth/login, /auth/callback, /auth/logout - Add OAuth2 flow with Keycloak using HTTP-only cookies - Add web auth dependencies with role checking - Add profile page (read-only) at /web/profile - Update header with user menu (sign in/out, profile) - Filter posts based on user permissions (hide drafts from guests) - Conditionally show/hide create/edit/delete buttons - Add authorization rules documentation to AGENTS.md - Secure post editing/deletion endpoints with auth checks - Add can_edit, can_delete flags to templates
This commit is contained in:
@@ -27,9 +27,148 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<a href="/posts/new" class="btn btn-primary btn-sm" data-testid="btn-create-post">
|
||||
New Post
|
||||
{% if user %}
|
||||
<div class="user-menu" data-testid="user-menu">
|
||||
<button
|
||||
type="button"
|
||||
class="user-menu-toggle"
|
||||
data-testid="user-menu-toggle"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="avatar avatar-sm" data-testid="user-avatar">{{ user.username[0]|upper }}</span>
|
||||
<span class="user-name" data-testid="user-name">{{ user.username }}</span>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-left: 0.25rem;">
|
||||
<path d="M2 4L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="user-menu-dropdown" data-testid="user-menu-dropdown">
|
||||
<a href="/web/profile" class="user-menu-item" data-testid="user-menu-profile">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 0.5rem;">
|
||||
<circle cx="8" cy="6" r="3" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M2 14c0-3 3-5 6-5s6 2 6 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
Profile
|
||||
</a>
|
||||
{% if can_create %}
|
||||
<a href="/web/posts/new" class="user-menu-item" data-testid="user-menu-new-post">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 0.5rem;">
|
||||
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
New Post
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="user-menu-divider" data-testid="user-menu-divider"></div>
|
||||
<a href="/auth/logout" class="user-menu-item user-menu-item-danger" data-testid="user-menu-logout">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 0.5rem;">
|
||||
<path d="M10 12h2a2 2 0 002-2V6a2 2 0 00-2-2h-2M6 12l-3-3m0 0l3-3m-3 3h8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Sign Out
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="/auth/login" class="btn btn-primary btn-sm" data-testid="btn-login">
|
||||
Sign In
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.user-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-menu-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-menu-toggle:hover {
|
||||
background-color: var(--color-hover);
|
||||
border-color: var(--color-secondary-dark-2);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.user-menu-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 0.5rem;
|
||||
min-width: 180px;
|
||||
background-color: var(--color-box-body);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px var(--color-shadow);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.user-menu:hover .user-menu-dropdown,
|
||||
.user-menu-toggle:focus + .user-menu-dropdown,
|
||||
.user-menu-dropdown:hover {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.user-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.user-menu-item:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.user-menu-item:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.user-menu-item:hover {
|
||||
background-color: var(--color-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-menu-item-danger {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.user-menu-item-danger:hover {
|
||||
background-color: var(--color-error-bg);
|
||||
}
|
||||
|
||||
.user-menu-divider {
|
||||
height: 1px;
|
||||
background-color: var(--color-border);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.user-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user