diff options
| author | Leonardo Bishop <me@leonardobishop.net> | 2026-01-07 23:39:53 +0000 |
|---|---|---|
| committer | Leonardo Bishop <me@leonardobishop.net> | 2026-01-07 23:39:53 +0000 |
| commit | 03cd6bdfbd473dba3f3dc50a1b15e389aac5bc70 (patch) | |
| tree | 5fea2b1840e298aaab953add749fb9226bd4a710 /web/handler | |
Initial commit
Diffstat (limited to 'web/handler')
| -rw-r--r-- | web/handler/auth.go | 89 | ||||
| -rw-r--r-- | web/handler/deploy.go | 81 | ||||
| -rw-r--r-- | web/handler/index.go | 34 | ||||
| -rw-r--r-- | web/handler/instance.go | 95 |
4 files changed, 299 insertions, 0 deletions
diff --git a/web/handler/auth.go b/web/handler/auth.go new file mode 100644 index 0000000..38b87b6 --- /dev/null +++ b/web/handler/auth.go @@ -0,0 +1,89 @@ +package handler + +import ( + "html/template" + "log/slog" + "net/http" + "strconv" + "time" + + "git.leonardobishop.net/instancer/pkg/session" +) + +func GetAuth(tmpl *template.Template) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + tmpl.ExecuteTemplate(w, "auth.html", nil) + } +} + +func PostAuth(tmpl *template.Template, session *session.MemoryStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { + Message string + }{ + Message: "Invalid form data", + }) + return + } + + team := r.FormValue("team") + if team == "" { + tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { + Message string + }{ + Message: "No team entered", + }) + return + } + + if _, err := strconv.Atoi(team); err != nil { + tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { + Message string + }{ + Message: "Team ID must be number", + }) + return + } + + session, err := session.Create(team) + if err != nil { + slog.Error("could not create session", "cause", err) + tmpl.ExecuteTemplate(w, "f_auth_error.html", struct { + Message string + }{ + Message: "Could not create session", + }) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: session.Token, + + Path: "/", + Secure: true, + SameSite: http.SameSiteStrictMode, + HttpOnly: true, + }) + w.Header().Add("HX-Redirect", "/") + } +} + +func GetLogout(session *session.MemoryStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + //TODO expire session here + + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: "", + Expires: time.Unix(0, 0), + + Path: "/", + Secure: true, + SameSite: http.SameSiteStrictMode, + HttpOnly: true, + }) + http.Redirect(w, r, "/auth", http.StatusFound) + } +} diff --git a/web/handler/deploy.go b/web/handler/deploy.go new file mode 100644 index 0000000..24e0f95 --- /dev/null +++ b/web/handler/deploy.go @@ -0,0 +1,81 @@ +package handler + +import ( + "fmt" + "html/template" + "net/http" + + "git.leonardobishop.net/instancer/pkg/deployer" + "git.leonardobishop.net/instancer/pkg/registry" + "git.leonardobishop.net/instancer/pkg/session" +) + +func PostDeploy(tmpl *template.Template, dockerDeployer *deployer.DockerDeployer) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Invalid form data", http.StatusBadRequest) + return + } + + challenge := r.FormValue("challenge") + if challenge == "" { + http.Error(w, "No challenge selected", http.StatusBadRequest) + return + } + + session := r.Context().Value("session").(*session.UserSession) + deployKey := dockerDeployer.StartDeploy(challenge, session.Team) + + tmpl.ExecuteTemplate(w, "f_deploy.html", struct { + DeployKey string + Challenge string + }{ + DeployKey: deployKey, + Challenge: challenge, + }) + } +} + +func DeploySSE(tmpl *template.Template, registryClient *registry.RegistryClient, dockerDeployer *deployer.DockerDeployer) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + return + } + + deployKey := r.URL.Query().Get("deploy") + if deployKey == "" { + http.Error(w, "No deployment specified", http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + clientGone := r.Context().Done() + job := dockerDeployer.GetJob(deployKey) + + if job == nil || job.DeployChan == nil { + fmt.Fprintf(w, "event: %s\ndata: %s\n\n", "close", "Invalid deployment key") + flusher.Flush() + return + } + + for { + select { + case <-clientGone: + return + case update, ok := <-job.DeployChan: + if !ok { + return + } + + fmt.Fprintf(w, "event: %s\ndata: %s\n\n", update.Status, update.Message) + flusher.Flush() + } + } + + } +} diff --git a/web/handler/index.go b/web/handler/index.go new file mode 100644 index 0000000..5cf44cf --- /dev/null +++ b/web/handler/index.go @@ -0,0 +1,34 @@ +package handler + +import ( + "html/template" + "log" + "net/http" + + "git.leonardobishop.net/instancer/pkg/registry" + "git.leonardobishop.net/instancer/pkg/session" +) + +func GetIndex(tmpl *template.Template, registryClient *registry.RegistryClient) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + challenges, err := registryClient.ListRepositories() + if err != nil { + log.Printf("Could not list repositories: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + session := r.Context().Value("session").(*session.UserSession) + + if err := tmpl.ExecuteTemplate(w, "index.html", struct { + Challenges []string + Team string + }{ + Challenges: challenges, + Team: session.Team, + }); err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + } +} diff --git a/web/handler/instance.go b/web/handler/instance.go new file mode 100644 index 0000000..0222a42 --- /dev/null +++ b/web/handler/instance.go @@ -0,0 +1,95 @@ +package handler + +import ( + "fmt" + "html/template" + "log/slog" + "net/http" + "time" + + "git.leonardobishop.net/instancer/pkg/deployer" + "git.leonardobishop.net/instancer/pkg/session" +) + +func GetInstances(tmpl *template.Template, dockerDeployer *deployer.DockerDeployer) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*session.UserSession) + + instances, err := dockerDeployer.GetTeamInstances(r.Context(), session.Team) + if err != nil { + slog.Error("could not get team instances", "team", session.Team, "error", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + type instanceView struct { + ChallengeName string + DeployKey string + Address template.HTML + ExpiresIn string + } + var instanceViews []instanceView + for _, instance := range instances { + var expiresIn string + timeLeft := time.Until(instance.ExpiresAt) + if timeLeft <= 0 { + expiresIn = "now" + } else { + expiresIn = fmt.Sprintf("%dm %ds", int(timeLeft.Minutes()), int(timeLeft.Seconds())%60) + } + + var address string + + if instance.AddressFormat == "http" || instance.AddressFormat == "https" { + address = `<a target="_blank" href="` + instance.AddressFormat + "://" + instance.Address + `">` + instance.Address + `</a>` + } else { + address = `<code>` + instance.Address + `</code>` + } + + instanceViews = append(instanceViews, instanceView{ + ChallengeName: instance.ChallengeName, + DeployKey: instance.DeployKey, + Address: template.HTML(address), + ExpiresIn: expiresIn, + }) + } + + if err := tmpl.ExecuteTemplate(w, "f_instance.html", struct { + Instances []instanceView + }{ + Instances: instanceViews, + }); err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + } +} + +func PostStopInstance(tmpl *template.Template, dockerDeployer *deployer.DockerDeployer) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*session.UserSession) + deployKey := r.PathValue("deployKey") + + err := dockerDeployer.StopInstance(r.Context(), deployKey, session.Team) + if err != nil { + tmpl.ExecuteTemplate(w, "f_instance_result.html", struct { + State string + Message string + }{ + State: "danger", + Message: "Could not stop instance" + err.Error(), + }) + return + } + + w.Header().Set("HX-Trigger", "poll-instances-now") + + tmpl.ExecuteTemplate(w, "f_deploy_result.html", struct { + State string + Message string + }{ + State: "success", + Message: "Instance " + deployKey + " stopped", + }) + } +} |
