package controllers
import (
"fmt"
"net/http"
"theskyscape.com/repo/skycode/models"
"theskyscape.com/repo/skykit"
)
func Admin() (string, skykit.Handler) {
return "admin", &AdminController{}
}
type AdminController struct {
skykit.Controller
}
func (c *AdminController) Setup(app *skykit.Application) {
c.Controller.Setup(app)
// Admin page - full HTML
http.Handle("GET /admin", c.Serve("admin.html", c.requireAdmin))
// HTMX partials - return HTML fragments
http.HandleFunc("GET /admin/users", c.Protect(c.listUsers, c.requireAdmin))
http.HandleFunc("GET /admin/users/{id}/edit", c.Protect(c.editUserForm, c.requireAdmin))
http.HandleFunc("PUT /admin/users/{id}", c.Protect(c.updateUser, c.requireAdmin))
http.HandleFunc("DELETE /admin/users/{id}", c.Protect(c.deleteUser, c.requireAdmin))
http.HandleFunc("GET /admin/files", c.Protect(c.listFiles, c.requireAdmin))
http.HandleFunc("DELETE /admin/files/{id}", c.Protect(c.deleteFile, c.requireAdmin))
http.HandleFunc("GET /admin/errors", c.Protect(c.listErrors, c.requireAdmin))
http.HandleFunc("DELETE /admin/errors/{id}", c.Protect(c.deleteError, c.requireAdmin))
http.HandleFunc("POST /admin/errors/clear", c.Protect(c.clearAllErrors, c.requireAdmin))
http.HandleFunc("POST /admin/users/cleanup-duplicates", c.Protect(c.cleanupDuplicates, c.requireAdmin))
}
func (c AdminController) Handle(r *http.Request) skykit.Handler {
c.Request = r
return &c
}
// requireAdmin checks if user is an admin (handle == "connor")
func (c *AdminController) requireAdmin(app *skykit.Application, w http.ResponseWriter, r *http.Request) bool {
user, err := app.Users.Authenticate(r)
if err != nil || user == nil {
http.Redirect(w, r, "/auth/signin", http.StatusSeeOther)
return false
}
if user.Handle != "connor" {
http.Error(w, "Forbidden", http.StatusForbidden)
return false
}
return true
}
// Template method - check if current user is admin
func (c *AdminController) IsAdmin() bool {
user, err := c.Users.Authenticate(c.Request)
if err != nil || user == nil {
return false
}
return user.Handle == "connor"
}
// GET /admin/users - list all users (returns HTML partial)
func (c *AdminController) listUsers(w http.ResponseWriter, r *http.Request) {
users, err := c.Users.Search("ORDER BY CreatedAt DESC")
if err != nil {
http.Error(w, "failed to list users", http.StatusInternalServerError)
return
}
c.Render(w, r, "admin-users.html", map[string]any{"Users": users})
}
// GET /admin/users/{id}/edit - return edit form (HTML partial)
func (c *AdminController) editUserForm(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
user, err := c.Users.Get(userID)
if err != nil || user == nil {
http.Error(w, "user not found", http.StatusNotFound)
return
}
c.Render(w, r, "admin-user-edit.html", user)
}
// GET /admin/files - list all files (returns HTML partial)
func (c *AdminController) listFiles(w http.ResponseWriter, r *http.Request) {
files, err := models.Files.Search("ORDER BY CreatedAt DESC LIMIT 500")
if err != nil {
http.Error(w, "failed to list files", http.StatusInternalServerError)
return
}
// Create file info with size and truncated owner ID
type fileInfo struct {
ID string
OwnerID string
OwnerIDShort string
Path string
Language string
Size string
CreatedAt string
}
out := make([]fileInfo, 0, len(files))
for _, f := range files {
ownerShort := f.OwnerID
if len(ownerShort) > 8 {
ownerShort = ownerShort[:8] + "..."
}
out = append(out, fileInfo{
ID: f.ID,
OwnerID: f.OwnerID,
OwnerIDShort: ownerShort,
Path: f.Path,
Language: f.Language,
Size: formatSize(len(f.Content)),
CreatedAt: f.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
c.Render(w, r, "admin-files.html", map[string]any{"Files": out})
}
// PUT /admin/users/{id} - update a user (returns refreshed users list)
func (c *AdminController) updateUser(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
r.ParseForm()
user, err := c.Users.Get(userID)
if err != nil || user == nil {
http.Error(w, "user not found", http.StatusNotFound)
return
}
// Update fields if provided
if h := r.FormValue("handle"); h != "" {
user.Handle = h
}
if n := r.FormValue("name"); n != "" {
user.Name = n
}
if e := r.FormValue("email"); e != "" {
user.Email = e
}
if err := c.Users.Update(user); err != nil {
http.Error(w, "failed to update user", http.StatusInternalServerError)
return
}
// Return updated users list
c.listUsers(w, r)
}
// DELETE /admin/users/{id} - delete a user and their files (returns refreshed users list)
func (c *AdminController) deleteUser(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
user, err := c.Users.Get(userID)
if err != nil || user == nil {
http.Error(w, "user not found", http.StatusNotFound)
return
}
// Prevent deleting yourself
currentUser, _ := c.Users.Authenticate(r)
if currentUser.ID == userID {
http.Error(w, "cannot delete yourself", http.StatusBadRequest)
return
}
// Delete all user's files first
files := models.ListFiles(userID)
for _, f := range files {
models.Files.Delete(f)
}
// Delete user settings
if settings := models.GetSettings(userID); settings != nil && settings.ID != "" {
models.Settings.Delete(settings)
}
// Delete user
if err := c.Users.Delete(user); err != nil {
http.Error(w, "failed to delete user", http.StatusInternalServerError)
return
}
// Return updated users list
c.listUsers(w, r)
}
// DELETE /admin/files/{id} - delete a file (returns refreshed files list)
func (c *AdminController) deleteFile(w http.ResponseWriter, r *http.Request) {
fileID := r.PathValue("id")
file, err := models.Files.Get(fileID)
if err != nil || file == nil {
http.Error(w, "file not found", http.StatusNotFound)
return
}
if err := models.Files.Delete(file); err != nil {
http.Error(w, "failed to delete file", http.StatusInternalServerError)
return
}
// Return updated files list
c.listFiles(w, r)
}
// POST /admin/users/cleanup-duplicates - remove duplicate users (returns refreshed users list)
func (c *AdminController) cleanupDuplicates(w http.ResponseWriter, r *http.Request) {
users, err := c.Users.Search("ORDER BY CreatedAt ASC")
if err != nil {
http.Error(w, "failed to list users", http.StatusInternalServerError)
return
}
// Track seen handles - keep the first (oldest) one
seenHandles := make(map[string]bool)
var duplicates []*skykit.User
var emptyHandles []*skykit.User
currentUser, _ := c.Users.Authenticate(r)
for _, u := range users {
// Skip current admin user
if u.ID == currentUser.ID {
seenHandles[u.Handle] = true
continue
}
if u.Handle == "" {
emptyHandles = append(emptyHandles, u)
} else if seenHandles[u.Handle] {
duplicates = append(duplicates, u)
} else {
seenHandles[u.Handle] = true
}
}
// Delete duplicates and empty handle users
toDelete := append(duplicates, emptyHandles...)
for _, u := range toDelete {
// Delete user's files
files := models.ListFiles(u.ID)
for _, f := range files {
models.Files.Delete(f)
}
// Delete settings
if settings := models.GetSettings(u.ID); settings != nil && settings.ID != "" {
models.Settings.Delete(settings)
}
// Delete user
c.Users.Delete(u)
}
// Sync database to ensure changes are pushed to remote
models.DB.Sync()
// Return updated users list
c.listUsers(w, r)
}
// formatSize returns human-readable file size
func formatSize(bytes int) string {
if bytes < 1024 {
return fmt.Sprintf("%d B", bytes)
}
if bytes < 1024*1024 {
return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
}
return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
}
// GET /admin/errors - list all error logs (returns HTML partial)
func (c *AdminController) listErrors(w http.ResponseWriter, r *http.Request) {
errors := models.ListErrors()
type errorInfo struct {
ID string
Name string
Message string
Count int
LastSeenAt string
}
out := make([]errorInfo, 0, len(errors))
for _, e := range errors {
out = append(out, errorInfo{
ID: e.ID,
Name: e.Name,
Message: e.Message,
Count: e.Count,
LastSeenAt: e.LastSeenAt.Format("2006-01-02 15:04:05"),
})
}
c.Render(w, r, "admin-errors.html", map[string]any{"Errors": out})
}
// DELETE /admin/errors/{id} - delete an error log
func (c *AdminController) deleteError(w http.ResponseWriter, r *http.Request) {
errorID := r.PathValue("id")
if err := models.ClearError(errorID); err != nil {
http.Error(w, "error not found", http.StatusNotFound)
return
}
c.listErrors(w, r)
}
// POST /admin/errors/clear - clear all error logs
func (c *AdminController) clearAllErrors(w http.ResponseWriter, r *http.Request) {
models.ClearAllErrors()
c.listErrors(w, r)
}