Connor McCutcheon
/ SkyCode
admin.go
go
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)
}
No comments yet.