131 lines
4.7 KiB
Go
131 lines
4.7 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.arnef.de/arnef/nidus/internal/db"
|
|
"git.arnef.de/arnef/nidus/internal/web/templates"
|
|
)
|
|
|
|
// icsURLValid does a light sanity check on a subscription URL: it must be
|
|
// http(s):// (fetched directly) or webcal:// (rewritten to https:// by
|
|
// internal/icssub before fetching), and within a reasonable length.
|
|
func icsURLValid(u string) bool {
|
|
if len(u) == 0 || len(u) > 2048 {
|
|
return false
|
|
}
|
|
return strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "webcal://")
|
|
}
|
|
|
|
// handleICSResource handles POST (create) and DELETE (remove) for the
|
|
// current user's ICS/webcal calendar subscriptions, mounted at
|
|
// /resources/ics.
|
|
func (s *Server) handleICSResource(w http.ResponseWriter, r *http.Request) {
|
|
username := userFromContext(r.Context())
|
|
|
|
// htmx v2 sends DELETE request parameters as URL query parameters, not
|
|
// a request body (see internal/web/resources.go's handleResource).
|
|
if r.Method == http.MethodDelete {
|
|
r.PostForm = r.URL.Query()
|
|
} else if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "invalid form", http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := strings.TrimSpace(r.PostForm.Get("name"))
|
|
if !resourceNameRe.MatchString(name) {
|
|
http.Error(w, "name must be 1-64 letters, digits, '-' or '_'", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodPost:
|
|
url := strings.TrimSpace(r.PostForm.Get("url"))
|
|
if !icsURLValid(url) {
|
|
http.Error(w, "url must be a http://, https:// or webcal:// address", http.StatusBadRequest)
|
|
return
|
|
}
|
|
color := strings.TrimSpace(r.PostForm.Get("color"))
|
|
if color != "" && !hexColorRe.MatchString(color) {
|
|
http.Error(w, "color must be a hex value like #3b82f6", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.dbase.CreateICSSubscription(username, name, url, color); err != nil {
|
|
if err == db.ErrResourceExists {
|
|
http.Error(w, "already exists", http.StatusConflict)
|
|
return
|
|
}
|
|
if err == db.ErrReservedName {
|
|
http.Error(w, "this name is reserved for a computed calendar", http.StatusConflict)
|
|
return
|
|
}
|
|
s.logger.Error("creating ics subscription", "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
case http.MethodDelete:
|
|
if err := s.dbase.DeleteICSSubscription(username, name); err != nil && err != db.ErrResourceNotFound {
|
|
s.logger.Error("deleting ics subscription", "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
default:
|
|
w.Header().Set("Allow", "POST, DELETE")
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// The set of cards changed (one added/removed), so re-render the
|
|
// whole #resources list rather than a single card.
|
|
resources, err := s.resourceCards(username)
|
|
if err != nil {
|
|
s.logger.Error("listing resources", "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_ = templates.ResourceList(resources).Render(context.Background(), w)
|
|
}
|
|
|
|
// handleICSColor updates the color of one of the current user's ICS
|
|
// subscriptions, mounted at /resources/ics/color. It re-renders just that
|
|
// card (not the whole list), since the set of cards doesn't change.
|
|
func (s *Server) handleICSColor(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
w.Header().Set("Allow", "POST")
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
username := userFromContext(r.Context())
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "invalid form", http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := strings.TrimSpace(r.PostForm.Get("name"))
|
|
color := strings.TrimSpace(r.PostForm.Get("color"))
|
|
if !resourceNameRe.MatchString(name) {
|
|
http.Error(w, "name must be 1-64 letters, digits, '-' or '_'", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if color != "" && !hexColorRe.MatchString(color) {
|
|
http.Error(w, "color must be a hex value like #3b82f6", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.dbase.SetICSSubscriptionColor(username, name, color); err != nil && err != db.ErrResourceNotFound {
|
|
s.logger.Error("setting ics subscription color", "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
sub, err := s.dbase.GetICSSubscription(username, name)
|
|
if err != nil {
|
|
s.logger.Error("loading ics subscription", "error", err)
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
card := templates.ResourceCard{Kind: "ics", Name: sub.Name, Color: sub.Color, URL: sub.URL}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
_ = templates.ResourceCardView(card).Render(r.Context(), w)
|
|
}
|