Allow non-admin users to access dashboard

- Change dashboard route from adminGet to authed middleware
- Show account counts and audit events only for admin users
- Show welcome message for non-admin authenticated users

Security: non-admin users cannot access account lists or audit
events; admin-only data is gated by claims.HasRole("admin") in
the handler, not just at the route level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 23:40:21 -07:00
parent 4e544665d2
commit 5a1f4f5837
3 changed files with 36 additions and 23 deletions

View File

@@ -7,7 +7,8 @@ import (
"git.wntrmute.dev/kyle/mcias/internal/model"
)
// handleDashboard renders the main dashboard page with account counts and recent events.
// handleDashboard renders the main dashboard page. Admin users see account
// counts and recent audit events; non-admin users see a welcome page.
func (u *UIServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
csrfToken, err := u.setCSRFCookies(w)
if err != nil {
@@ -16,30 +17,35 @@ func (u *UIServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
return
}
accounts, err := u.db.ListAccounts()
if err != nil {
u.renderError(w, r, http.StatusInternalServerError, "failed to load accounts")
return
claims := claimsFromContext(r.Context())
isAdmin := claims != nil && claims.HasRole("admin")
data := DashboardData{
PageData: PageData{CSRFToken: csrfToken, ActorName: u.actorName(r)},
IsAdmin: isAdmin,
}
var total, active int
for _, a := range accounts {
total++
if a.Status == model.AccountStatusActive {
active++
if isAdmin {
accounts, err := u.db.ListAccounts()
if err != nil {
u.renderError(w, r, http.StatusInternalServerError, "failed to load accounts")
return
}
for _, a := range accounts {
data.TotalAccounts++
if a.Status == model.AccountStatusActive {
data.ActiveAccounts++
}
}
events, _, err := u.db.ListAuditEventsPaged(db.AuditQueryParams{Limit: 10, Offset: 0})
if err != nil {
u.logger.Warn("load recent audit events", "error", err)
events = nil
}
data.RecentEvents = events
}
events, _, err := u.db.ListAuditEventsPaged(db.AuditQueryParams{Limit: 10, Offset: 0})
if err != nil {
u.logger.Warn("load recent audit events", "error", err)
events = nil
}
u.render(w, "dashboard", DashboardData{
PageData: PageData{CSRFToken: csrfToken, ActorName: u.actorName(r)},
TotalAccounts: total,
ActiveAccounts: active,
RecentEvents: events,
})
u.render(w, "dashboard", data)
}

View File

@@ -314,7 +314,7 @@ func (u *UIServer) Register(mux *http.ServeMux) {
return authed(u.requireAdminRole(http.HandlerFunc(h)))
}
uiMux.Handle("GET /dashboard", adminGet(u.handleDashboard))
uiMux.Handle("GET /dashboard", authed(http.HandlerFunc(u.handleDashboard)))
uiMux.Handle("GET /accounts", adminGet(u.handleAccountsList))
uiMux.Handle("POST /accounts", admin(u.handleCreateAccount))
uiMux.Handle("GET /accounts/{id}", adminGet(u.handleAccountDetail))
@@ -609,6 +609,7 @@ type LoginData struct {
// DashboardData is the view model for the dashboard page.
type DashboardData struct {
PageData
IsAdmin bool
RecentEvents []*db.AuditEventView
TotalAccounts int
ActiveAccounts int

View File

@@ -4,6 +4,7 @@
<div class="page-header">
<h1>Dashboard</h1>
</div>
{{if .IsAdmin}}
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;margin-bottom:1.5rem">
<div class="card" style="text-align:center">
<div style="font-size:2rem;font-weight:700;color:#2563eb">{{.TotalAccounts}}</div>
@@ -33,4 +34,9 @@
</div>
</div>
{{end}}
{{else}}
<div class="card">
<p>Welcome, <strong>{{.ActorName}}</strong>. Use the navigation above to access your profile and credentials.</p>
</div>
{{end}}
{{end}}