package handlers import ( "encoding/json" "fmt" "net/http" "net/url" "strconv" "strings" "instance-manager/internal/auth" "instance-manager/internal/db" "instance-manager/internal/instance" "instance-manager/internal/proxy" "github.com/go-chi/chi/v5" ) type Handler struct { db *db.Database auth *auth.Manager manager *instance.Manager proxy *proxy.Proxy templates *TemplateRenderer } func New(db *db.Database, authMgr *auth.Manager, manager *instance.Manager, proxy *proxy.Proxy, templates *TemplateRenderer) *Handler { return &Handler{ db: db, auth: authMgr, manager: manager, proxy: proxy, templates: templates, } } func (h *Handler) RegisterRoutes(r *chi.Mux) { r.Get("/", h.handleHome) r.Get("/login", h.handleLogin) r.Post("/login", h.handleLoginPost) r.Get("/signup", h.handleSignup) r.Post("/signup", h.handleSignupPost) r.Post("/logout", h.handleLogout) r.Get("/dashboard", h.requireAuth(h.handleDashboard)) r.Post("/api/instance/start", h.requireAuth(h.handleStartInstance)) r.Post("/api/instance/stop", h.requireAuth(h.handleStopInstance)) r.Get("/change-password", h.requireAuth(h.handleChangePassword)) r.Post("/change-password", h.requireAuth(h.handleChangePasswordPost)) r.Get("/app/{id}", h.requireAuth(h.handleAppProxy)) r.Get("/app/{id}/*", h.requireAuth(h.handleAppProxy)) r.Get("/app/public/{id}", h.handlePublicAppProxy) r.Get("/app/public/{id}/*", h.handlePublicAppProxy) admin := r.With(h.requireAdminMiddleware()) admin.Get("/admin", h.handleAdminUsers) admin.Post("/admin/users/{id}/toggle", h.handleToggleUser) admin.Post("/admin/users/{id}/restart", h.handleRestartInstance) admin.Get("/admin/dashboard", h.handleAdminDashboard) } func (h *Handler) requireAuth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sess, err := h.auth.GetSession(r) if err != nil || sess == nil { http.Redirect(w, r, "/login?redirect="+url.QueryEscape(r.URL.Path), http.StatusFound) return } next(w, r) } } func (h *Handler) requireAdminMiddleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sess, err := h.auth.GetSession(r) if err != nil || sess == nil || sess.Role != "admin" { http.Redirect(w, r, "/login", http.StatusFound) return } next.ServeHTTP(w, r) }) } } func getSession(r *http.Request, authMgr *auth.Manager) *auth.SessionData { sess, _ := authMgr.GetSession(r) return sess } func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) { sess, _ := h.auth.GetSession(r) if sess != nil { http.Redirect(w, r, "/dashboard", http.StatusFound) return } h.templates.Render(w, r, "index", map[string]interface{}{ "Title": "Instance Manager", "LoggedIn": false, }) } func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) { sess, _ := h.auth.GetSession(r) if sess != nil { redirect := r.URL.Query().Get("redirect") if redirect == "" || redirect == "/login" { redirect = "/dashboard" } http.Redirect(w, r, redirect, http.StatusFound) return } h.templates.Render(w, r, "login", map[string]interface{}{ "Title": "Login", "LoggedIn": false, "Redirect": r.URL.Query().Get("redirect"), }) } func (h *Handler) handleLoginPost(w http.ResponseWriter, r *http.Request) { username := r.FormValue("username") password := r.FormValue("password") user, err := h.db.GetUserByUsername(username) if err != nil || user == nil { h.templates.Render(w, r, "login", map[string]interface{}{ "Title": "Login", "Error": "Invalid username or password", "Username": username, "LoggedIn": false, }) return } if !db.CheckPasswordHash(password, user.PasswordHash) { h.templates.Render(w, r, "login", map[string]interface{}{ "Title": "Login", "Error": "Invalid username or password", "Username": username, "LoggedIn": false, }) return } if !user.IsActive { h.templates.Render(w, r, "login", map[string]interface{}{ "Title": "Login", "Error": "Your account is not active. Please contact an administrator.", "Username": username, "LoggedIn": false, }) return } if err := h.auth.CreateSession(w, r, user); err != nil { http.Error(w, "Session error", http.StatusInternalServerError) return } if user.IsActive { if err := h.manager.Start(user); err != nil { // Don't fail login, just log the error // In production this could trigger async start } } redirect := r.FormValue("redirect") if redirect == "" { redirect = "/dashboard" } http.Redirect(w, r, redirect, http.StatusFound) } func (h *Handler) handleSignup(w http.ResponseWriter, r *http.Request) { sess, _ := h.auth.GetSession(r) if sess != nil { http.Redirect(w, r, "/dashboard", http.StatusFound) return } h.templates.Render(w, r, "signup", map[string]interface{}{ "Title": "Sign Up", "LoggedIn": false, }) } func (h *Handler) handleSignupPost(w http.ResponseWriter, r *http.Request) { username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") confirmPassword := r.FormValue("confirm_password") var errors []string if len(username) < 3 { errors = append(errors, "Username must be at least 3 characters") } if len(password) < 6 { errors = append(errors, "Password must be at least 6 characters") } if password != confirmPassword { errors = append(errors, "Passwords do not match") } if existing, _ := h.db.GetUserByUsername(username); existing != nil { errors = append(errors, "Username already taken") } if len(errors) > 0 { h.templates.Render(w, r, "signup", map[string]interface{}{ "Title": "Sign Up", "Errors": errors, "Username": username, "Password": password, "LoggedIn": false, }) return } passwordHash, err := db.HashPassword(password) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } user, err := h.db.CreateUser(username, passwordHash) if err != nil { h.templates.Render(w, r, "signup", map[string]interface{}{ "Title": "Sign Up", "Error": "Failed to create account", "Username": username, "Password": password, "LoggedIn": false, }) return } if err := h.auth.CreateSession(w, r, user); err != nil { http.Error(w, "Session error", http.StatusInternalServerError) return } go func() { if err := h.manager.Start(user); err != nil { fmt.Printf("Failed to start instance for new user %s: %v\n", user.Username, err) } }() http.Redirect(w, r, "/dashboard", http.StatusFound) } func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) { sess, _ := h.auth.GetSession(r) if sess != nil { user, _ := h.db.GetUserByID(sess.UserID) if user != nil && user.IsActive { h.manager.Stop(user) } h.auth.DestroySession(w, r) } http.Redirect(w, r, "/login", http.StatusFound) } func (h *Handler) handleDashboard(w http.ResponseWriter, r *http.Request) { sess, _ := h.auth.GetSession(r) if sess == nil { http.Redirect(w, r, "/login", http.StatusFound) return } user, err := h.db.GetUserByID(sess.UserID) if err != nil || user == nil { h.auth.DestroySession(w, r) http.Redirect(w, r, "/login", http.StatusFound) return } inst, _ := h.manager.GetInstance(user) scheme := "http" if r.TLS != nil { scheme = "https" } var instanceURL string if inst != nil { instanceURL = fmt.Sprintf("%s://%s.%s", scheme, user.Username, h.proxy.BaseDomain()) } data := map[string]interface{}{ "Title": "Dashboard", "LoggedIn": true, "User": user, "Instance": inst, "InstanceURL": instanceURL, "IsAdmin": user.Role == "admin", } h.templates.Render(w, r, "dashboard", data) } func (h *Handler) handleChangePassword(w http.ResponseWriter, r *http.Request) { sess, _ := h.auth.GetSession(r) if sess == nil { http.Redirect(w, r, "/login", http.StatusFound) return } h.templates.Render(w, r, "change_password", map[string]interface{}{ "Title": "Change Password", "LoggedIn": true, "User": getSession(r, h.auth), }) } func (h *Handler) handleChangePasswordPost(w http.ResponseWriter, r *http.Request) { sess, _ := h.auth.GetSession(r) if sess == nil { http.Redirect(w, r, "/login", http.StatusFound) return } currentPass := r.FormValue("current_password") newPass := r.FormValue("new_password") confirmPass := r.FormValue("confirm_password") user, _ := h.db.GetUserByID(sess.UserID) if !db.CheckPasswordHash(currentPass, user.PasswordHash) { h.templates.Render(w, r, "change_password", map[string]interface{}{ "Title": "Change Password", "Error": "Current password is incorrect", "LoggedIn": true, "User": getSession(r, h.auth), }) return } if len(newPass) < 6 { h.templates.Render(w, r, "change_password", map[string]interface{}{ "Title": "Change Password", "Error": "New password must be at least 6 characters", "LoggedIn": true, "User": getSession(r, h.auth), }) return } if newPass != confirmPass { h.templates.Render(w, r, "change_password", map[string]interface{}{ "Title": "Change Password", "Error": "Passwords do not match", "LoggedIn": true, "User": getSession(r, h.auth), }) return } passwordHash, err := db.HashPassword(newPass) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } if err := h.db.UpdateUserPassword(user.ID, passwordHash); err != nil { http.Error(w, "Failed to update password", http.StatusInternalServerError) return } h.templates.Render(w, r, "change_password", map[string]interface{}{ "Title": "Change Password", "Success": "Password changed successfully", "LoggedIn": true, "User": getSession(r, h.auth), }) } func (h *Handler) handleAppProxy(w http.ResponseWriter, r *http.Request) { sess, _ := h.auth.GetSession(r) if sess == nil { http.Redirect(w, r, "/login", http.StatusFound) return } idStr := chi.URLParam(r, "id") userID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } user, _ := h.db.GetUserByID(sess.UserID) if user == nil { http.Redirect(w, r, "/login", http.StatusFound) return } pathPrefix := "/app/" + idStr remainingPath := strings.TrimPrefix(r.URL.Path, pathPrefix) if h.proxy.IsPrivateEndpoint(remainingPath) { if userID != sess.UserID { http.Error(w, "Unauthorized", http.StatusForbidden) return } } h.proxy.ProxyHandler(userID, pathPrefix).ServeHTTP(w, r) } func (h *Handler) handlePublicAppProxy(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") userID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } user, _ := h.db.GetUserByID(userID) if user == nil { http.Error(w, "User not found", http.StatusNotFound) return } pathPrefix := "/app/public/" + idStr remainingPath := strings.TrimPrefix(r.URL.Path, pathPrefix) if h.proxy.IsPrivateEndpoint(remainingPath) { http.Error(w, "Unauthorized", http.StatusForbidden) return } h.proxy.ProxyHandler(userID, pathPrefix).ServeHTTP(w, r) } func (h *Handler) handleAdminDashboard(w http.ResponseWriter, r *http.Request) { users, _ := h.db.GetUsers() h.templates.Render(w, r, "admin_dashboard", map[string]interface{}{ "Title": "Admin Dashboard", "LoggedIn": true, "User": getSession(r, h.auth), "Users": users, }) } func (h *Handler) handleAdminUsers(w http.ResponseWriter, r *http.Request) { users, _ := h.db.GetUsers() scheme := "http" if r.TLS != nil { scheme = "https" } type userWithInstance struct { Username string Role string IsActive bool ID int64 Instance interface{} InstanceURL string } var enriched []userWithInstance for _, u := range users { inst, _ := h.manager.GetInstance(&u) ui := userWithInstance{ Username: u.Username, Role: u.Role, IsActive: u.IsActive, ID: u.ID, Instance: inst, } if inst != nil { ui.InstanceURL = fmt.Sprintf("%s://%s.%s", scheme, u.Username, h.proxy.BaseDomain()) } enriched = append(enriched, ui) } h.templates.Render(w, r, "admin_users", map[string]interface{}{ "Title": "Manage Users", "LoggedIn": true, "User": getSession(r, h.auth), "Users": enriched, }) } func (h *Handler) handleToggleUser(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") userID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } user, err := h.db.GetUserByID(userID) if err != nil || user == nil { http.Error(w, "User not found", http.StatusNotFound) return } newActive := !user.IsActive if err := h.db.UpdateUserActive(userID, newActive); err != nil { http.Error(w, "Failed to update user", http.StatusInternalServerError) return } if !newActive { h.manager.Stop(user) } else { h.manager.Start(user) } if r.Header.Get("X-Requested-With") == "XMLHttpRequest" { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "active": newActive, }) } else { http.Redirect(w, r, "/admin", http.StatusFound) } } func (h *Handler) handleRestartInstance(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") userID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } user, err := h.db.GetUserByID(userID) if err != nil || user == nil { http.Error(w, "User not found", http.StatusNotFound) return } h.manager.Stop(user) if user.IsActive { h.manager.Start(user) } http.Redirect(w, r, "/admin", http.StatusFound) } func (h *Handler) handleStartInstance(w http.ResponseWriter, r *http.Request) { sess, _ := h.auth.GetSession(r) if sess == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } user, err := h.db.GetUserByID(sess.UserID) if err != nil || user == nil { http.Error(w, "User not found", http.StatusNotFound) return } if err := h.manager.Start(user); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": err.Error(), }) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, }) } func (h *Handler) handleStopInstance(w http.ResponseWriter, r *http.Request) { sess, _ := h.auth.GetSession(r) if sess == nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } user, err := h.db.GetUserByID(sess.UserID) if err != nil || user == nil { http.Error(w, "User not found", http.StatusNotFound) return } if err := h.manager.Stop(user); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "error": err.Error(), }) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, }) }