From 3a7d82a396fbbf9ce3efdaf97c9ecf2edea49011 Mon Sep 17 00:00:00 2001 From: Julien Riou Date: Sat, 20 Sep 2025 06:47:31 +0200 Subject: [PATCH] refactor: Move handlers into their own files Signed-off-by: Julien Riou --- src/server/handlers_api.go | 149 +++++++++++++++ src/server/handlers_web.go | 234 ++++++++++++++++++++++++ src/server/server.go | 358 ------------------------------------- 3 files changed, 383 insertions(+), 358 deletions(-) create mode 100644 src/server/handlers_api.go create mode 100644 src/server/handlers_web.go diff --git a/src/server/handlers_api.go b/src/server/handlers_api.go new file mode 100644 index 0000000..f260328 --- /dev/null +++ b/src/server/handlers_api.go @@ -0,0 +1,149 @@ +package server + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" + + "github.com/gorilla/mux" + + "git.riou.xyz/jriou/coller/internal" +) + +func HealthHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "OK") +} + +type CreateNoteHandler struct { + logger *slog.Logger + db *Database + maxUploadSize int64 +} + +type CreateNotePayload struct { + Content string `json:"content"` + Password string `json:"password"` + Encrypted bool `json:"encrypted"` + Expiration int `json:"expiration"` + DeleteAfterRead bool `json:"delete_after_read"` + Language string `json:"language"` +} + +type CreateNoteResponse struct { + ID string `json:"id"` +} + +func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + bodyReader := http.MaxBytesReader(w, r.Body, h.maxUploadSize) + defer r.Body.Close() + + var body CreateNotePayload + err := json.NewDecoder(bodyReader).Decode(&body) + if err != nil { + WriteError(w, "could not decode payload to create note", err) + return + } + + content, err := internal.Decode(body.Content) + + if err != nil { + WriteError(w, "could not decode content", err) + return + } + + note, err := h.db.Create(content, body.Password, body.Encrypted, body.Expiration, body.DeleteAfterRead, body.Language) + if err != nil { + WriteError(w, "could not create note", err) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(CreateNoteResponse{ID: note.ID}) +} + +type GetNoteHandler struct { + logger *slog.Logger + db *Database +} + +func (h *GetNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + + id := mux.Vars(r)["id"] + + note, err := h.db.Get(id) + + if err != nil { + WriteError(w, "could not get note", err) + } else if note == nil { + w.WriteHeader(http.StatusNotFound) + } else { + if note.Encrypted { + w.Header().Set("Content-Type", "application/octet-stream") + } + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, string(note.Content)) + } +} + +type GetProtectedNoteHandler struct { + logger *slog.Logger + db *Database +} + +func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + + vars := mux.Vars(r) + id := vars["id"] + password := vars["password"] + + note, err := h.db.Get(id) + + if err != nil { + WriteError(w, "could not get note", err) + return + } else if note == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + if password != "" && note.Encrypted { + note.Content, err = internal.Decrypt(note.Content, password) + if err != nil { + WriteError(w, "could not decrypt note", err) + return + } + } + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, string(note.Content)) +} + +type ClientHandler struct { + logger *slog.Logger + version string +} + +func (h *ClientHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.logger.Debug("rendering client redirection") + vars := mux.Vars(r) + os := vars["os"] + arch := vars["arch"] + clientName := vars["clientName"] + + if !internal.InSlice(supportedOSes, os) || !internal.InSlice(supportedArches, arch) || !internal.InSlice(supportedClients, clientName) { + w.WriteHeader(http.StatusNotFound) + return + } + + version := h.version + if version == "" { + version = "latest" + } + + http.Redirect(w, r, fmt.Sprintf("https://git.riou.xyz/jriou/%s/releases/download/%s/%s-%s-%s", clientName, version, clientName, os, arch), http.StatusMovedPermanently) +} diff --git a/src/server/handlers_web.go b/src/server/handlers_web.go new file mode 100644 index 0000000..d059d7a --- /dev/null +++ b/src/server/handlers_web.go @@ -0,0 +1,234 @@ +package server + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "io" + "log/slog" + "net/http" + "strconv" + "strings" + + "github.com/gorilla/mux" + + "git.riou.xyz/jriou/coller/internal" +) + +type PageData struct { + Title string + Version string + Expirations []int + Expiration int + Languages []string + Err error + URL string + Note *Note + EnableUploadFileButton bool + BootstrapDirectory string +} + +type HomeHandler struct { + Templates *template.Template + PageData PageData +} + +func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.Templates.ExecuteTemplate(w, "index", h.PageData) +} + +type CreateNoteWithFormHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger + db *Database + maxUploadSize int64 +} + +func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.PageData.Err = nil + templateName := "create" + + h.logger.Debug("parsing multipart form") + err := r.ParseMultipartForm(h.maxUploadSize) + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + h.logger.Debug("parsing content") + content := []byte(r.FormValue("content")) + + h.logger.Debug("parsing file") + file, handler, err := r.FormFile("file") + if err != nil && !errors.Is(err, http.ErrMissingFile) { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if !errors.Is(err, http.ErrMissingFile) { + defer file.Close() + + h.logger.Debug("checking file size") + if handler.Size > h.maxUploadSize { + h.PageData.Err = fmt.Errorf("file too large (%d > %d)", handler.Size, h.maxUploadSize) + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + h.logger.Debug("checking file content type") + if !strings.HasPrefix(handler.Header.Get("Content-Type"), "text/") { + h.PageData.Err = fmt.Errorf("text file expected (got %s)", handler.Header.Get("Content-Type")) + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + h.logger.Debug("reading uploaded file") + var fileContent bytes.Buffer + n, err := io.Copy(&fileContent, file) + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + h.logger.Debug("file uploaded", slog.Any("bytes", n)) + if n != 0 { + content = fileContent.Bytes() + } + } + + h.logger.Debug("checking content") + if content == nil || len(content) == 0 { + h.PageData.Err = fmt.Errorf("empty note") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + h.logger.Debug("checking inputs") + noPassword := r.FormValue("no-password") + password := r.FormValue("password") + expiration := r.FormValue("expiration") + deleteAfterRead := r.FormValue("delete-after-read") + language := r.FormValue("language") + + if password == "" && noPassword == "" { + h.logger.Debug("generating password") + password = internal.GenerateChars(passwordLength) + } + + h.logger.Debug("computing expiration") + var expirationInt int + if expiration == "Expiration" { + expirationInt = 0 + } else { + expirationInt, _ = strconv.Atoi(expiration) + } + + h.logger.Debug("saving note to the database") + note, err := h.db.Create(content, password, password != "", expirationInt, deleteAfterRead != "", language) + if err != nil { + h.PageData.Err = err + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + h.logger.Debug("building note url") + var scheme = "http://" + if r.TLS != nil { + scheme = "https://" + } + + h.PageData.URL = fmt.Sprintf("%s%s/%s", scheme, r.Host, note.ID) + if password != "" { + h.PageData.URL += "/" + password + } + + h.logger.Debug("rendering page") + h.Templates.ExecuteTemplate(w, "create", h.PageData) +} + +type GetWebNoteHandler struct { + Templates *template.Template + PageData PageData + logger *slog.Logger + db *Database +} + +func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.PageData.Err = nil + templateName := "note" + + vars := mux.Vars(r) + id := vars["id"] + + note, err := h.db.Get(id) + + if err != nil { + h.PageData.Err = fmt.Errorf("could not find note: %v", err) + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note == nil { + h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note.Encrypted { + h.PageData.Err = fmt.Errorf("note is encrypted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + h.PageData.Note = note + + h.logger.Debug("rendering note web page") + h.Templates.ExecuteTemplate(w, "note", h.PageData) +} + +func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.PageData.Err = nil + templateName := "note" + + vars := mux.Vars(r) + id := vars["id"] + password := vars["password"] + + note, err := h.db.Get(id) + + if err != nil { + h.PageData.Err = fmt.Errorf("could not find note: %v", err) + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if note == nil { + h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + + if password != "" && note.Encrypted { + note.Content, err = internal.Decrypt(note.Content, password) + if err != nil { + h.PageData.Err = fmt.Errorf("could not decrypt note: %v", err) + h.Templates.ExecuteTemplate(w, templateName, h.PageData) + return + } + } + + h.PageData.Note = note + + h.logger.Debug("rendering protected note web page") + h.Templates.ExecuteTemplate(w, "note", h.PageData) +} + +func (h *ClientsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.logger.Debug("rendering clients web page") + h.Templates.ExecuteTemplate(w, "clients", h.PageData) +} diff --git a/src/server/server.go b/src/server/server.go index 4cef956..0c9bae5 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -1,16 +1,12 @@ package server import ( - "bytes" "embed" "encoding/json" - "errors" "fmt" "html/template" - "io" "log/slog" "net/http" - "strconv" "strings" "github.com/gorilla/mux" @@ -79,293 +75,6 @@ func WriteError(w http.ResponseWriter, message string, err error) { }.ToJSON()) } -func HealthHandler(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "OK") -} - -type CreateNoteHandler struct { - logger *slog.Logger - db *Database - maxUploadSize int64 -} - -type CreateNotePayload struct { - Content string `json:"content"` - Password string `json:"password"` - Encrypted bool `json:"encrypted"` - Expiration int `json:"expiration"` - DeleteAfterRead bool `json:"delete_after_read"` - Language string `json:"language"` -} - -type CreateNoteResponse struct { - ID string `json:"id"` -} - -func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - bodyReader := http.MaxBytesReader(w, r.Body, h.maxUploadSize) - defer r.Body.Close() - - var body CreateNotePayload - err := json.NewDecoder(bodyReader).Decode(&body) - if err != nil { - WriteError(w, "could not decode payload to create note", err) - return - } - - content, err := internal.Decode(body.Content) - - if err != nil { - WriteError(w, "could not decode content", err) - return - } - - note, err := h.db.Create(content, body.Password, body.Encrypted, body.Expiration, body.DeleteAfterRead, body.Language) - if err != nil { - WriteError(w, "could not create note", err) - return - } - - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(CreateNoteResponse{ID: note.ID}) -} - -type GetNoteHandler struct { - logger *slog.Logger - db *Database -} - -func (h *GetNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - - id := mux.Vars(r)["id"] - - note, err := h.db.Get(id) - - if err != nil { - WriteError(w, "could not get note", err) - } else if note == nil { - w.WriteHeader(http.StatusNotFound) - } else { - if note.Encrypted { - w.Header().Set("Content-Type", "application/octet-stream") - } - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, string(note.Content)) - } -} - -type GetProtectedNoteHandler struct { - logger *slog.Logger - db *Database -} - -func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - - vars := mux.Vars(r) - id := vars["id"] - password := vars["password"] - - note, err := h.db.Get(id) - - if err != nil { - WriteError(w, "could not get note", err) - return - } else if note == nil { - w.WriteHeader(http.StatusNotFound) - return - } - - if password != "" && note.Encrypted { - note.Content, err = internal.Decrypt(note.Content, password) - if err != nil { - WriteError(w, "could not decrypt note", err) - return - } - } - - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, string(note.Content)) -} - -type PageData struct { - Title string - Version string - Expirations []int - Expiration int - Languages []string - Err error - URL string - Note *Note - EnableUploadFileButton bool - BootstrapDirectory string -} - -type HomeHandler struct { - Templates *template.Template - PageData PageData -} - -func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.Templates.ExecuteTemplate(w, "index", h.PageData) -} - -type CreateNoteWithFormHandler struct { - Templates *template.Template - PageData PageData - logger *slog.Logger - db *Database - maxUploadSize int64 -} - -func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.PageData.Err = nil - templateName := "create" - - h.logger.Debug("parsing multipart form") - err := r.ParseMultipartForm(h.maxUploadSize) - if err != nil { - h.PageData.Err = err - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - h.logger.Debug("parsing content") - content := []byte(r.FormValue("content")) - - h.logger.Debug("parsing file") - file, handler, err := r.FormFile("file") - if err != nil && !errors.Is(err, http.ErrMissingFile) { - h.PageData.Err = err - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if !errors.Is(err, http.ErrMissingFile) { - defer file.Close() - - h.logger.Debug("checking file size") - if handler.Size > h.maxUploadSize { - h.PageData.Err = fmt.Errorf("file too large (%d > %d)", handler.Size, h.maxUploadSize) - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - h.logger.Debug("checking file content type") - if !strings.HasPrefix(handler.Header.Get("Content-Type"), "text/") { - h.PageData.Err = fmt.Errorf("text file expected (got %s)", handler.Header.Get("Content-Type")) - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - h.logger.Debug("reading uploaded file") - var fileContent bytes.Buffer - n, err := io.Copy(&fileContent, file) - if err != nil { - h.PageData.Err = err - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - h.logger.Debug("file uploaded", slog.Any("bytes", n)) - if n != 0 { - content = fileContent.Bytes() - } - } - - h.logger.Debug("checking content") - if content == nil || len(content) == 0 { - h.PageData.Err = fmt.Errorf("empty note") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - h.logger.Debug("checking inputs") - noPassword := r.FormValue("no-password") - password := r.FormValue("password") - expiration := r.FormValue("expiration") - deleteAfterRead := r.FormValue("delete-after-read") - language := r.FormValue("language") - - if password == "" && noPassword == "" { - h.logger.Debug("generating password") - password = internal.GenerateChars(passwordLength) - } - - h.logger.Debug("computing expiration") - var expirationInt int - if expiration == "Expiration" { - expirationInt = 0 - } else { - expirationInt, _ = strconv.Atoi(expiration) - } - - h.logger.Debug("saving note to the database") - note, err := h.db.Create(content, password, password != "", expirationInt, deleteAfterRead != "", language) - if err != nil { - h.PageData.Err = err - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - h.logger.Debug("building note url") - var scheme = "http://" - if r.TLS != nil { - scheme = "https://" - } - - h.PageData.URL = fmt.Sprintf("%s%s/%s", scheme, r.Host, note.ID) - if password != "" { - h.PageData.URL += "/" + password - } - - h.logger.Debug("rendering page") - h.Templates.ExecuteTemplate(w, "create", h.PageData) -} - -type GetWebNoteHandler struct { - Templates *template.Template - PageData PageData - logger *slog.Logger - db *Database -} - -func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.PageData.Err = nil - templateName := "note" - - vars := mux.Vars(r) - id := vars["id"] - - note, err := h.db.Get(id) - - if err != nil { - h.PageData.Err = fmt.Errorf("could not find note: %v", err) - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if note == nil { - h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if note.Encrypted { - h.PageData.Err = fmt.Errorf("note is encrypted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - h.PageData.Note = note - - h.logger.Debug("rendering note web page") - h.Templates.ExecuteTemplate(w, "note", h.PageData) -} - type GetProtectedWebNoteHandler struct { Templates *template.Template PageData PageData @@ -373,79 +82,12 @@ type GetProtectedWebNoteHandler struct { db *Database } -func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.PageData.Err = nil - templateName := "note" - - vars := mux.Vars(r) - id := vars["id"] - password := vars["password"] - - note, err := h.db.Get(id) - - if err != nil { - h.PageData.Err = fmt.Errorf("could not find note: %v", err) - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if note == nil { - h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted") - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - - if password != "" && note.Encrypted { - note.Content, err = internal.Decrypt(note.Content, password) - if err != nil { - h.PageData.Err = fmt.Errorf("could not decrypt note: %v", err) - h.Templates.ExecuteTemplate(w, templateName, h.PageData) - return - } - } - - h.PageData.Note = note - - h.logger.Debug("rendering protected note web page") - h.Templates.ExecuteTemplate(w, "note", h.PageData) -} - type ClientsHandler struct { Templates *template.Template PageData PageData logger *slog.Logger } -func (h *ClientsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.logger.Debug("rendering clients web page") - h.Templates.ExecuteTemplate(w, "clients", h.PageData) -} - -type ClientHandler struct { - logger *slog.Logger - version string -} - -func (h *ClientHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.logger.Debug("rendering client redirection") - vars := mux.Vars(r) - os := vars["os"] - arch := vars["arch"] - clientName := vars["clientName"] - - if !internal.InSlice(supportedOSes, os) || !internal.InSlice(supportedArches, arch) || !internal.InSlice(supportedClients, clientName) { - w.WriteHeader(http.StatusNotFound) - return - } - - version := h.version - if version == "" { - version = "latest" - } - - http.Redirect(w, r, fmt.Sprintf("https://git.riou.xyz/jriou/%s/releases/download/%s/%s-%s-%s", clientName, version, clientName, os, arch), http.StatusMovedPermanently) -} - //go:embed templates/* var templatesFS embed.FS