add htmx web frontend with templates and session auth

This commit is contained in:
2026-01-18 21:11:23 +01:00
parent 1bc112047d
commit 01a0ac68a4
11 changed files with 969 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
package web
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/omixlab/mosis-portal/internal/database"
)
// Handler handles web page requests
type Handler struct {
db *database.DB
templates *Templates
}
// NewHandler creates a new web handler
func NewHandler(db *database.DB) (*Handler, error) {
templates, err := NewTemplates()
if err != nil {
return nil, err
}
return &Handler{
db: db,
templates: templates,
}, nil
}
// PageData is the base data structure for all pages
type PageData struct {
Title string
ActiveNav string
Developer *database.Developer
Error string
}
// Login renders the login page
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
data := PageData{
Title: "Sign In",
}
// Check for error message
if err := r.URL.Query().Get("error"); err != "" {
data.Error = err
}
h.render(w, "login", data)
}
// Dashboard renders the dashboard page
func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
developer := getDeveloperFromContext(r)
if developer == nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Get stats
apps, total, _ := h.db.ListApps(r.Context(), developer.ID, "", 1, 100)
data := struct {
PageData
Stats struct {
TotalApps int
Downloads int64
ActiveUsers int64
}
Apps []*database.App
}{
PageData: PageData{
Title: "Dashboard",
ActiveNav: "dashboard",
Developer: developer,
},
Apps: apps,
}
data.Stats.TotalApps = total
data.Stats.Downloads = 0 // TODO: implement
data.Stats.ActiveUsers = 0 // TODO: implement
h.render(w, "dashboard", data)
}
// AppNew renders the new app form
func (h *Handler) AppNew(w http.ResponseWriter, r *http.Request) {
developer := getDeveloperFromContext(r)
if developer == nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
data := PageData{
Title: "Create New App",
ActiveNav: "apps",
Developer: developer,
}
h.render(w, "app_new", data)
}
// AppDetail renders the app detail page
func (h *Handler) AppDetail(w http.ResponseWriter, r *http.Request) {
developer := getDeveloperFromContext(r)
if developer == nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
appID := chi.URLParam(r, "appID")
app, err := h.db.GetApp(r.Context(), appID)
if err != nil {
http.Error(w, "App not found", http.StatusNotFound)
return
}
// Verify ownership
if app.DeveloperID != developer.ID {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// Get latest version
versions, totalVersions, _ := h.db.ListVersions(r.Context(), appID, "", 1, 1)
var latestVersion *database.AppVersion
if len(versions) > 0 {
latestVersion = versions[0]
}
data := struct {
PageData
App *database.App
Tab string
LatestVersion *database.AppVersion
TotalVersions int
}{
PageData: PageData{
Title: app.Name,
ActiveNav: "apps",
Developer: developer,
},
App: app,
Tab: "overview",
LatestVersion: latestVersion,
TotalVersions: totalVersions,
}
h.render(w, "app_detail", data)
}
// AppListPartial renders the app list partial for htmx
func (h *Handler) AppListPartial(w http.ResponseWriter, r *http.Request) {
developer := getDeveloperFromContext(r)
if developer == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
apps, _, err := h.db.ListApps(r.Context(), developer.ID, "", 1, 100)
if err != nil {
http.Error(w, "Failed to load apps", http.StatusInternalServerError)
return
}
data := struct {
Apps []*database.App
}{
Apps: apps,
}
h.renderPartial(w, "app_list", data)
}
// render renders a full page template
func (h *Handler) render(w http.ResponseWriter, name string, data interface{}) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.RenderPage(w, name, data); err != nil {
http.Error(w, "Template error: "+err.Error(), http.StatusInternalServerError)
}
}
// renderPartial renders a partial template
func (h *Handler) renderPartial(w http.ResponseWriter, name string, data interface{}) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.RenderPartial(w, name, data); err != nil {
http.Error(w, "Template error: "+err.Error(), http.StatusInternalServerError)
}
}
// getDeveloperFromContext retrieves the developer from the request context
// This will be set by the session middleware
func getDeveloperFromContext(r *http.Request) *database.Developer {
developer, _ := r.Context().Value("developer").(*database.Developer)
return developer
}