orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1package main
2
3import (
4 "fmt"
5 "log/slog"
6 "net/http"
7 "strings"
8
9 "github.com/google/uuid"
10)
11
12func (s *site) properties(w http.ResponseWriter, r *http.Request) {
13 query := strings.TrimSpace(r.URL.Query().Get("q"))
14
15 rows, err := listProperties(r.Context(), s.db, query)
16 if err != nil {
17 slog.Info(fmt.Sprintf("properties list: %v", err))
18 http.Error(w, "internal server error", http.StatusInternalServerError)
19 return
20 }
21
22 data := s.page(r, "Properties", "Manage your properties.")
23 data.Query = query
24
25 for _, p := range rows {
26 view, err := s.buildPropertyView(r.Context(), p)
27 if err != nil {
28 // One property failing to summarise must not blank the list.
29 slog.Info(fmt.Sprintf("properties list: building view for %s: %v", p.URL, err))
30 continue
31 }
32 data.Properties = append(data.Properties, view)
33 }
34
35 data.PageScript = s.propsScript
36 data.PageStyles = s.propsStyles
37 s.renderer.Render(w, http.StatusOK, "properties.html", data)
38}
39
40// propertyCreate adds a tracked URL. https:// is required: the checker speaks
41// HTTP/2 over TLS and nothing else, so an http:// property could never pass.
42func (s *site) propertyCreate(w http.ResponseWriter, r *http.Request) {
43 if err := r.ParseForm(); err != nil {
44 http.Error(w, "bad request", http.StatusBadRequest)
45 return
46 }
47
48 raw := strings.TrimSpace(r.PostFormValue("url"))
49 if raw == "" || !strings.HasPrefix(strings.ToLower(raw), "https://") {
50 http.Redirect(w, r, "/properties", http.StatusSeeOther)
51 return
52 }
53 // Parsed as well as prefix-checked, so "https://" on its own is refused.
54 if _, err := parseHTTPURL(raw); err != nil {
55 http.Redirect(w, r, "/properties", http.StatusSeeOther)
56 return
57 }
58
59 if _, err := createProperty(r.Context(), s.db, raw); err != nil {
60 slog.Info(fmt.Sprintf("property create %s: %v", raw, err))
61 }
62 http.Redirect(w, r, "/properties", http.StatusSeeOther)
63}
64
65func (s *site) propertyDelete(w http.ResponseWriter, r *http.Request) {
66 id, err := uuid.Parse(r.PathValue("id"))
67 if err != nil {
68 http.Redirect(w, r, "/properties", http.StatusSeeOther)
69 return
70 }
71 if err := deleteProperty(r.Context(), s.db, id); err != nil {
72 slog.Info(fmt.Sprintf("property delete %s: %v", id, err))
73 }
74 http.Redirect(w, r, "/properties", http.StatusSeeOther)
75}
76
77// propertyPublic flips whether a property has a shareable status page. Answers
78// JSON because the toggle is a fetch() that updates the switch in place.
79func (s *site) propertyPublic(w http.ResponseWriter, r *http.Request) {
80 id, err := uuid.Parse(r.PathValue("id"))
81 if err != nil {
82 writeJSON(w, http.StatusNotFound, map[string]any{"success": false, "error": "not_found"})
83 return
84 }
85
86 isPublic, found, err := togglePublic(r.Context(), s.db, id)
87 switch {
88 case err != nil:
89 slog.Info(fmt.Sprintf("property public toggle %s: %v", id, err))
90 writeJSON(w, http.StatusInternalServerError, map[string]any{"success": false})
91 case !found:
92 // A 404 rather than a success carrying a made-up value.
93 writeJSON(w, http.StatusNotFound, map[string]any{"success": false, "error": "not_found"})
94 default:
95 writeJSON(w, http.StatusOK, map[string]any{"success": true, "is_public": isPublic})
96 }
97}