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:
@@ -9,9 +9,14 @@
|
||||
<h1 class="page-title" data-testid="page-title-home">Latest Posts</h1>
|
||||
<p class="page-subtitle" data-testid="page-subtitle-home">Discover stories, thinking, and expertise from writers on any topic.</p>
|
||||
</div>
|
||||
<a href="/posts/new" class="btn btn-primary" data-testid="btn-create-post-header">
|
||||
{% if can_create %}
|
||||
<a href="/web/posts/new" class="btn btn-primary" data-testid="btn-create-post-header">
|
||||
<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>
|
||||
Write a Post
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -45,16 +45,28 @@
|
||||
Back to posts
|
||||
</a>
|
||||
|
||||
{% if can_edit or can_delete %}
|
||||
<div class="flex gap-2" data-testid="post-detail-edit-actions">
|
||||
{% if can_edit %}
|
||||
<a href="/posts/{{ post.id }}/edit" class="btn" data-testid="btn-edit-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="M11 2L14 5M2 14L3 10L12 1L15 4L6 13L2 14Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_delete %}
|
||||
<form action="/posts/{{ post.id }}/delete" method="POST" style="display: inline;" data-testid="form-delete-post">
|
||||
<button type="submit" class="btn btn-danger" data-testid="btn-delete-post" onclick="return confirm('Are you sure you want to delete this 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="M2 4h12M6 4V2a2 2 0 012-2h0a2 2 0 012 2v2m3 0v10a2 2 0 01-2 2H5a2 2 0 01-2-2V4h9z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
125
app/presentation/templates/pages/profile.html
Normal file
125
app/presentation/templates/pages/profile.html
Normal file
@@ -0,0 +1,125 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Profile - {{ user.username }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header" data-testid="page-header-profile">
|
||||
<h1 class="page-title" data-testid="page-title-profile">User Profile</h1>
|
||||
</div>
|
||||
|
||||
<div class="card" data-testid="profile-card">
|
||||
<div class="card-body" data-testid="profile-card-body">
|
||||
<div class="profile-header" data-testid="profile-header">
|
||||
<div class="avatar avatar-lg" data-testid="profile-avatar">
|
||||
{{ user.username[0]|upper }}
|
||||
</div>
|
||||
<div class="profile-info" data-testid="profile-info">
|
||||
<h2 class="profile-username" data-testid="profile-username">{{ user.username }}</h2>
|
||||
<span class="badge {% if user_role == 'admin' %}badge-primary{% else %}badge-success{% endif %}" data-testid="profile-role">
|
||||
{{ user_role|upper }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider" data-testid="profile-divider"></div>
|
||||
|
||||
<div class="profile-details" data-testid="profile-details">
|
||||
<div class="profile-field" data-testid="profile-field-email">
|
||||
<span class="profile-label" data-testid="profile-label-email">Email:</span>
|
||||
<span class="profile-value" data-testid="profile-value-email">{{ user.email or 'Not provided' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="profile-field" data-testid="profile-field-userid">
|
||||
<span class="profile-label" data-testid="profile-label-userid">User ID:</span>
|
||||
<span class="profile-value" data-testid="profile-value-userid">{{ user.user_id }}</span>
|
||||
</div>
|
||||
|
||||
{% if user.first_name or user.last_name %}
|
||||
<div class="profile-field" data-testid="profile-field-name">
|
||||
<span class="profile-label" data-testid="profile-label-name">Name:</span>
|
||||
<span class="profile-value" data-testid="profile-value-name">
|
||||
{{ user.first_name or '' }} {{ user.last_name or '' }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer" data-testid="profile-card-footer">
|
||||
<div class="flex justify-between items-center" data-testid="profile-actions">
|
||||
<a href="/web/" class="btn" data-testid="btn-back-home">
|
||||
<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 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
Back to Home
|
||||
</a>
|
||||
|
||||
{% if can_create %}
|
||||
<a href="/web/posts/new" class="btn btn-primary" data-testid="btn-create-post-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;">
|
||||
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
New Post
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-username {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.profile-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.profile-field {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-label {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-light);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.profile-value {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-field {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.profile-label {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user