summaryrefslogtreecommitdiffstats
path: root/web/handler
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.net>2026-01-07 23:39:53 +0000
committerLeonardo Bishop <me@leonardobishop.net>2026-01-07 23:39:53 +0000
commit03cd6bdfbd473dba3f3dc50a1b15e389aac5bc70 (patch)
tree5fea2b1840e298aaab953add749fb9226bd4a710 /web/handler
Initial commit
Diffstat (limited to 'web/handler')
-rw-r--r--web/handler/auth.go89
-rw-r--r--web/handler/deploy.go81
-rw-r--r--web/handler/index.go34
-rw-r--r--web/handler/instance.go95
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",
+ })
+ }
+}