main pico / pkg / shared / router / api.go
Antonio Mika  ·  2026-10-10
  1package router
  2
  3import (
  4	"html/template"
  5	"log/slog"
  6	"net/http"
  7	"os"
  8	"path/filepath"
  9	"strings"
 10
 11	"github.com/picosh/pico/pkg/db"
 12	"github.com/picosh/pico/pkg/shared"
 13)
 14
 15type SubdomainProps struct {
 16	ProjectName string
 17	Username    string
 18}
 19
 20func GetProjectFromSubdomain(subdomain string) (*SubdomainProps, error) {
 21	props := &SubdomainProps{}
 22	strs := strings.SplitN(subdomain, "-", 2)
 23	props.Username = strs[0]
 24	if len(strs) == 2 {
 25		props.ProjectName = strs[1]
 26	} else {
 27		props.ProjectName = props.Username
 28	}
 29	return props, nil
 30}
 31
 32func CorsHeaders(headers http.Header) {
 33	headers.Add("Access-Control-Allow-Origin", "*")
 34	headers.Add("Vary", "Origin")
 35	headers.Add("Vary", "Access-Control-Request-Method")
 36	headers.Add("Vary", "Access-Control-Request-Headers")
 37	headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept")
 38	headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE")
 39}
 40
 41func UnauthorizedHandler(w http.ResponseWriter, r *http.Request) {
 42	http.Error(w, "You do not have access to this site", http.StatusUnauthorized)
 43}
 44
 45// CheckHandler reports whether a custom domain points at a user's site on
 46// domain, through its TXT record for txtPrefix.
 47func CheckHandler(domain, txtPrefix string, users interface {
 48	FindUserByName(name string) (*db.User, error)
 49}) http.HandlerFunc {
 50	return func(w http.ResponseWriter, r *http.Request) {
 51		hostDomain := r.URL.Query().Get("domain")
 52		appDomain := strings.Split(domain, ":")[0]
 53
 54		if !strings.Contains(hostDomain, appDomain) {
 55			subdomain := GetCustomDomain(hostDomain, txtPrefix)
 56			if subdomain != "" {
 57				u, err := users.FindUserByName(subdomain)
 58				if u != nil && err == nil {
 59					w.WriteHeader(http.StatusOK)
 60					return
 61				}
 62			}
 63		}
 64
 65		w.WriteHeader(http.StatusNotFound)
 66	}
 67}
 68
 69// GetUsernameFromRequest is the user whose site a request is for: the
 70// subdomain, or the path's {user} on the main domain.
 71func GetUsernameFromRequest(r *http.Request) string {
 72	if subdomain := GetSubdomain(r); subdomain != "" {
 73		return subdomain
 74	}
 75	return r.PathValue("user")
 76}
 77
 78func minus(a, b int) int {
 79	return a - b
 80}
 81
 82func intRange(start, end int) []int {
 83	n := end - start + 1
 84	result := make([]int, n)
 85	for i := 0; i < n; i++ {
 86		result[i] = start + i
 87	}
 88	return result
 89}
 90
 91var FuncMap = template.FuncMap{
 92	"minus":    minus,
 93	"intRange": intRange,
 94}
 95
 96// RenderTemplate parses templates with the layout and footers in an app's
 97// dir.
 98func RenderTemplate(dir string, templates []string) (*template.Template, error) {
 99	files := make([]string, len(templates))
100	copy(files, templates)
101	files = append(
102		files,
103		filepath.Join(dir, "html/footer.partial.tmpl"),
104		filepath.Join(dir, "html/marketing-footer.partial.tmpl"),
105		filepath.Join(dir, "html/base.layout.tmpl"),
106	)
107
108	ts, err := template.New("base").Funcs(FuncMap).ParseFiles(files...)
109	if err != nil {
110		return nil, err
111	}
112	return ts, nil
113}
114
115// Static serves an app's files from Dir/public and its pages from Dir/html.
116type Static struct {
117	Dir    string
118	Logger *slog.Logger
119	Site   shared.SitePageData
120}
121
122// File serves the file name in Dir/public.
123func (s Static) File(name string, contentType string) http.HandlerFunc {
124	return func(w http.ResponseWriter, r *http.Request) {
125		contents, err := os.ReadFile(filepath.Join(s.Dir, "public", name))
126		if err != nil {
127			s.Logger.Error("could not read file", "fname", name, "err", err.Error())
128			http.Error(w, "file not found", http.StatusNotFound)
129			return
130		}
131
132		w.Header().Add("Content-Type", contentType)
133
134		_, err = w.Write(contents)
135		if err != nil {
136			s.Logger.Error("could not write http response", "file", name, "err", err.Error())
137		}
138	}
139}
140
141// Page renders the template fname in Dir with the site's data.
142func (s Static) Page(fname string) http.HandlerFunc {
143	return func(w http.ResponseWriter, r *http.Request) {
144		ts, err := RenderTemplate(s.Dir, []string{filepath.Join(s.Dir, fname)})
145		if err != nil {
146			s.Logger.Error("could not render template", "fname", fname, "err", err.Error())
147			http.Error(w, err.Error(), http.StatusInternalServerError)
148			return
149		}
150
151		err = ts.Execute(w, shared.PageData{Site: s.Site})
152		if err != nil {
153			s.Logger.Error("could not execute template", "fname", fname, "err", err.Error())
154			http.Error(w, err.Error(), http.StatusInternalServerError)
155		}
156	}
157}