Update frame work and workspace codebase
This commit is contained in:
258
internal/http/admin_handlers.go
Normal file
258
internal/http/admin_handlers.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/example/storage-appliance/internal/auth"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// UsersHandler shows the users management page
|
||||
func (a *App) UsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := templateData(r, map[string]interface{}{
|
||||
"Title": "User Management",
|
||||
})
|
||||
if err := templates.ExecuteTemplate(w, "users", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// HXUsersHandler returns HTMX partial for users list
|
||||
func (a *App) HXUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||
rbacStore := auth.NewRBACStore(a.DB)
|
||||
|
||||
// Get all users (simplified - in production, you'd want pagination)
|
||||
rows, err := a.DB.QueryContext(r.Context(), `SELECT id, username, created_at FROM users ORDER BY username`)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type UserWithRoles struct {
|
||||
ID string
|
||||
Username string
|
||||
CreatedAt string
|
||||
Roles []auth.Role
|
||||
}
|
||||
|
||||
var users []UserWithRoles
|
||||
for rows.Next() {
|
||||
var u UserWithRoles
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.CreatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
roles, _ := rbacStore.GetUserRoles(r.Context(), u.ID)
|
||||
u.Roles = roles
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Users": users,
|
||||
}
|
||||
if err := templates.ExecuteTemplate(w, "hx_users", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUserHandler creates a new user
|
||||
func (a *App) CreateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
if username == "" || password == "" {
|
||||
http.Error(w, "username and password required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userStore := auth.NewUserStore(a.DB)
|
||||
_, err := userStore.CreateUser(r.Context(), username, password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Return HTMX partial or redirect
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
http.Redirect(w, r, "/admin/users", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteUserHandler deletes a user
|
||||
func (a *App) DeleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "id")
|
||||
|
||||
_, err := a.DB.ExecContext(r.Context(), `DELETE FROM users WHERE id = ?`, userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
http.Redirect(w, r, "/admin/users", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateUserRolesHandler updates roles for a user
|
||||
func (a *App) UpdateUserRolesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
userID := chi.URLParam(r, "id")
|
||||
|
||||
var req struct {
|
||||
RoleIDs []string `json:"role_ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rbacStore := auth.NewRBACStore(a.DB)
|
||||
|
||||
// Get current roles
|
||||
currentRoles, _ := rbacStore.GetUserRoles(r.Context(), userID)
|
||||
|
||||
// Remove all current roles
|
||||
for _, role := range currentRoles {
|
||||
rbacStore.RemoveRoleFromUser(r.Context(), userID, role.ID)
|
||||
}
|
||||
|
||||
// Add new roles
|
||||
for _, roleID := range req.RoleIDs {
|
||||
rbacStore.AssignRoleToUser(r.Context(), userID, roleID)
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
http.Redirect(w, r, "/admin/users", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
// RolesHandler shows the roles management page
|
||||
func (a *App) RolesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := templateData(r, map[string]interface{}{
|
||||
"Title": "Role Management",
|
||||
})
|
||||
if err := templates.ExecuteTemplate(w, "roles", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// HXRolesHandler returns HTMX partial for roles list
|
||||
func (a *App) HXRolesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
rbacStore := auth.NewRBACStore(a.DB)
|
||||
|
||||
roles, err := rbacStore.GetAllRoles(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
type RoleWithPermissions struct {
|
||||
auth.Role
|
||||
Permissions []auth.Permission
|
||||
}
|
||||
|
||||
var rolesWithPerms []RoleWithPermissions
|
||||
for _, role := range roles {
|
||||
rwp := RoleWithPermissions{Role: role}
|
||||
perms, _ := rbacStore.GetRolePermissions(r.Context(), role.ID)
|
||||
rwp.Permissions = perms
|
||||
rolesWithPerms = append(rolesWithPerms, rwp)
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Roles": rolesWithPerms,
|
||||
}
|
||||
if err := templates.ExecuteTemplate(w, "hx_roles", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateRoleHandler creates a new role
|
||||
func (a *App) CreateRoleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.FormValue("name")
|
||||
description := r.FormValue("description")
|
||||
|
||||
if name == "" {
|
||||
http.Error(w, "name required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
roleID := name // Using name as ID for simplicity
|
||||
_, err := a.DB.ExecContext(r.Context(),
|
||||
`INSERT INTO roles (id, name, description) VALUES (?, ?, ?)`,
|
||||
roleID, name, description)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
http.Redirect(w, r, "/admin/roles", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteRoleHandler deletes a role
|
||||
func (a *App) DeleteRoleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
roleID := chi.URLParam(r, "id")
|
||||
|
||||
_, err := a.DB.ExecContext(r.Context(), `DELETE FROM roles WHERE id = ?`, roleID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
http.Redirect(w, r, "/admin/roles", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateRolePermissionsHandler updates permissions for a role
|
||||
func (a *App) UpdateRolePermissionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
roleID := chi.URLParam(r, "id")
|
||||
|
||||
var req struct {
|
||||
PermissionIDs []string `json:"permission_ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rbacStore := auth.NewRBACStore(a.DB)
|
||||
|
||||
// Get current permissions
|
||||
currentPerms, _ := rbacStore.GetRolePermissions(r.Context(), roleID)
|
||||
|
||||
// Remove all current permissions
|
||||
for _, perm := range currentPerms {
|
||||
rbacStore.RemovePermissionFromRole(r.Context(), roleID, perm.ID)
|
||||
}
|
||||
|
||||
// Add new permissions
|
||||
for _, permID := range req.PermissionIDs {
|
||||
rbacStore.AssignPermissionToRole(r.Context(), roleID, permID)
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
http.Redirect(w, r, "/admin/roles", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
125
internal/http/auth_handlers.go
Normal file
125
internal/http/auth_handlers.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/example/storage-appliance/internal/auth"
|
||||
)
|
||||
|
||||
// LoginHandler handles user login
|
||||
func (a *App) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
// Show login page
|
||||
data := map[string]interface{}{
|
||||
"Title": "Login",
|
||||
}
|
||||
if err := templates.ExecuteTemplate(w, "login", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle POST login
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
req.Username = r.FormValue("username")
|
||||
req.Password = r.FormValue("password")
|
||||
}
|
||||
|
||||
// Authenticate user
|
||||
userStore := auth.NewUserStore(a.DB)
|
||||
user, err := userStore.Authenticate(r.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`<div class="text-red-600">Invalid username or password</div>`))
|
||||
} else {
|
||||
http.Error(w, "invalid credentials", http.StatusUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Create session
|
||||
sessionStore := auth.NewSessionStore(a.DB)
|
||||
session, err := sessionStore.CreateSession(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to create session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Set session cookie
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: auth.SessionCookieName,
|
||||
Value: session.Token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false, // Set to true in production with HTTPS
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
MaxAge: int(auth.SessionDuration.Seconds()),
|
||||
})
|
||||
|
||||
// Set CSRF token cookie
|
||||
csrfToken := generateCSRFToken()
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "csrf_token",
|
||||
Value: csrfToken,
|
||||
Path: "/",
|
||||
HttpOnly: false, // Needed for HTMX to read it
|
||||
Secure: false,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
MaxAge: int(auth.SessionDuration.Seconds()),
|
||||
})
|
||||
|
||||
// Redirect or return success
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Redirect", "/dashboard")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
// LogoutHandler handles user logout
|
||||
func (a *App) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Get session token from cookie
|
||||
cookie, err := r.Cookie(auth.SessionCookieName)
|
||||
if err == nil {
|
||||
// Delete session
|
||||
sessionStore := auth.NewSessionStore(a.DB)
|
||||
sessionStore.DeleteSession(r.Context(), cookie.Value)
|
||||
}
|
||||
|
||||
// Clear cookies
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: auth.SessionCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: -1,
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "csrf_token",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: false,
|
||||
MaxAge: -1,
|
||||
})
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Redirect", "/login")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
}
|
||||
121
internal/http/monitoring_handlers.go
Normal file
121
internal/http/monitoring_handlers.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/example/storage-appliance/internal/monitoring"
|
||||
)
|
||||
|
||||
// MetricsHandler serves Prometheus metrics
|
||||
func (a *App) MetricsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Create collectors
|
||||
collectors := []monitoring.Collector{
|
||||
monitoring.NewZFSCollector(a.ZFSSvc, a.Runner),
|
||||
monitoring.NewSMARTCollector(a.Runner),
|
||||
monitoring.NewServiceCollector(a.Runner),
|
||||
monitoring.NewHostCollector(),
|
||||
}
|
||||
|
||||
// Export metrics
|
||||
exporter := monitoring.NewPrometheusExporter(collectors...)
|
||||
metrics := exporter.Export(ctx)
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(metrics))
|
||||
}
|
||||
|
||||
// MonitoringHandler shows the monitoring dashboard
|
||||
func (a *App) MonitoringHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := templateData(r, map[string]interface{}{
|
||||
"Title": "Monitoring",
|
||||
})
|
||||
if err := templates.ExecuteTemplate(w, "monitoring", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// HXMonitoringHandler returns HTMX partial for monitoring metrics
|
||||
func (a *App) HXMonitoringHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Create collectors
|
||||
collectors := []monitoring.Collector{
|
||||
monitoring.NewZFSCollector(a.ZFSSvc, a.Runner),
|
||||
monitoring.NewSMARTCollector(a.Runner),
|
||||
monitoring.NewServiceCollector(a.Runner),
|
||||
monitoring.NewHostCollector(),
|
||||
}
|
||||
|
||||
// Export for UI
|
||||
exporter := monitoring.NewUIExporter(collectors...)
|
||||
groups := exporter.Export(ctx)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Groups": groups,
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(w, "hx_monitoring", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// HXMonitoringGroupHandler returns HTMX partial for a specific metric group
|
||||
func (a *App) HXMonitoringGroupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
groupName := r.URL.Query().Get("group")
|
||||
if groupName == "" {
|
||||
http.Error(w, "group parameter required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
// Create the specific collector (normalize group name)
|
||||
var collector monitoring.Collector
|
||||
groupLower := strings.ToLower(groupName)
|
||||
switch groupLower {
|
||||
case "zfs":
|
||||
collector = monitoring.NewZFSCollector(a.ZFSSvc, a.Runner)
|
||||
case "smart":
|
||||
collector = monitoring.NewSMARTCollector(a.Runner)
|
||||
case "services", "service":
|
||||
collector = monitoring.NewServiceCollector(a.Runner)
|
||||
case "host":
|
||||
collector = monitoring.NewHostCollector()
|
||||
default:
|
||||
// Try to match by collector name
|
||||
if strings.Contains(groupLower, "zfs") {
|
||||
collector = monitoring.NewZFSCollector(a.ZFSSvc, a.Runner)
|
||||
} else if strings.Contains(groupLower, "smart") {
|
||||
collector = monitoring.NewSMARTCollector(a.Runner)
|
||||
} else if strings.Contains(groupLower, "service") {
|
||||
collector = monitoring.NewServiceCollector(a.Runner)
|
||||
} else if strings.Contains(groupLower, "host") {
|
||||
collector = monitoring.NewHostCollector()
|
||||
} else {
|
||||
http.Error(w, "unknown group", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Export for UI
|
||||
exporter := monitoring.NewUIExporter(collector)
|
||||
groups := exporter.Export(ctx)
|
||||
|
||||
if len(groups) == 0 {
|
||||
http.Error(w, "no data", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Group": groups[0],
|
||||
}
|
||||
|
||||
if err := templates.ExecuteTemplate(w, "hx_monitoring_group", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
20
internal/http/template_helpers.go
Normal file
20
internal/http/template_helpers.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// templateData adds CSRF token and other common data to template context
|
||||
func templateData(r *http.Request, data map[string]interface{}) map[string]interface{} {
|
||||
if data == nil {
|
||||
data = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Get CSRF token from cookie
|
||||
if cookie, err := r.Cookie("csrf_token"); err == nil {
|
||||
data["CSRFToken"] = cookie.Value
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user