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}