repos
/ orchard main

orchard

mirror

Every 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

3.1 KB · 98 lines · Go Raw History
 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}