1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5package ws
6
7import (
8 "database/sql"
9 "embed"
10 "fmt"
11 "io"
12 "net/http"
13 "net/url"
14 "strings"
15 "sync"
16 "text/template"
17 "time"
18
19 "git.sr.ht/~amolith/willow/users"
20
21 "git.sr.ht/~amolith/willow/project"
22 "github.com/microcosm-cc/bluemonday"
23)
24
25type Handler struct {
26 DbConn *sql.DB
27 Mutex *sync.Mutex
28 Req *chan struct{}
29 ManualRefresh *chan struct{}
30 Res *chan []project.Project
31}
32
33//go:embed static
34var fs embed.FS
35
36// bmUGC = bluemonday.UGCPolicy()
37var bmStrict = bluemonday.StrictPolicy()
38
39func (h Handler) RootHandler(w http.ResponseWriter, r *http.Request) {
40 if !h.isAuthorised(r) {
41 http.Redirect(w, r, "/login", http.StatusSeeOther)
42 return
43 }
44 *h.Req <- struct{}{}
45 data := <-*h.Res
46 tmpl := template.Must(template.ParseFS(fs, "static/home.html"))
47 if err := tmpl.Execute(w, data); err != nil {
48 fmt.Println(err)
49 }
50}
51
52func (h Handler) NewHandler(w http.ResponseWriter, r *http.Request) {
53 if !h.isAuthorised(r) {
54 http.Redirect(w, r, "/login", http.StatusSeeOther)
55 return
56 }
57 params := r.URL.Query()
58 action := bmStrict.Sanitize(params.Get("action"))
59 if r.Method == http.MethodGet {
60 if action == "" {
61 tmpl := template.Must(template.ParseFS(fs, "static/new.html"))
62 if err := tmpl.Execute(w, nil); err != nil {
63 fmt.Println(err)
64 }
65 } else if action != "delete" {
66 submittedURL := bmStrict.Sanitize(params.Get("url"))
67 if submittedURL == "" {
68 w.WriteHeader(http.StatusBadRequest)
69 _, err := w.Write([]byte("No URL provided"))
70 if err != nil {
71 fmt.Println(err)
72 }
73 return
74 }
75
76 forge := bmStrict.Sanitize(params.Get("forge"))
77 if forge == "" {
78 w.WriteHeader(http.StatusBadRequest)
79 _, err := w.Write([]byte("No forge provided"))
80 if err != nil {
81 fmt.Println(err)
82 }
83 return
84 }
85
86 name := bmStrict.Sanitize(params.Get("name"))
87 if name == "" {
88 w.WriteHeader(http.StatusBadRequest)
89 _, err := w.Write([]byte("No name provided"))
90 if err != nil {
91 fmt.Println(err)
92 }
93 return
94 }
95
96 proj := project.Project{
97 URL: submittedURL,
98 Name: name,
99 Forge: forge,
100 }
101
102 proj.URL = strings.TrimSuffix(proj.URL, ".git")
103
104 proj, err := project.GetReleases(h.DbConn, proj)
105 if err != nil {
106 w.WriteHeader(http.StatusBadRequest)
107 _, err := w.Write([]byte(fmt.Sprintf("Error getting releases: %s", err)))
108 if err != nil {
109 fmt.Println(err)
110 }
111 return
112 }
113 tmpl := template.Must(template.ParseFS(fs, "static/select-release.html"))
114 if err := tmpl.Execute(w, proj); err != nil {
115 fmt.Println(err)
116 }
117 } else if action == "delete" {
118 submittedURL := params.Get("url")
119 if submittedURL == "" {
120 w.WriteHeader(http.StatusBadRequest)
121 _, err := w.Write([]byte("No URL provided"))
122 if err != nil {
123 fmt.Println(err)
124 }
125 return
126 }
127
128 project.Untrack(h.DbConn, h.ManualRefresh, submittedURL)
129 http.Redirect(w, r, "/", http.StatusSeeOther)
130 }
131 }
132
133 if r.Method == http.MethodPost {
134 err := r.ParseForm()
135 if err != nil {
136 fmt.Println(err)
137 }
138 nameValue := bmStrict.Sanitize(r.FormValue("name"))
139 urlValue := bmStrict.Sanitize(r.FormValue("url"))
140 forgeValue := bmStrict.Sanitize(r.FormValue("forge"))
141 releaseValue := bmStrict.Sanitize(r.FormValue("release"))
142
143 if nameValue != "" && urlValue != "" && forgeValue != "" && releaseValue != "" {
144 project.Track(h.DbConn, h.ManualRefresh, nameValue, urlValue, forgeValue, releaseValue)
145 http.Redirect(w, r, "/", http.StatusSeeOther)
146 return
147 }
148
149 if nameValue != "" && urlValue != "" && forgeValue != "" && releaseValue == "" {
150 http.Redirect(w, r, "/new?action=yoink&name="+url.QueryEscape(nameValue)+"&url="+url.QueryEscape(urlValue)+"&forge="+url.QueryEscape(forgeValue), http.StatusSeeOther)
151 return
152 }
153
154 if nameValue == "" && urlValue == "" && forgeValue == "" && releaseValue == "" {
155 w.WriteHeader(http.StatusBadRequest)
156 _, err := w.Write([]byte("No data provided"))
157 if err != nil {
158 fmt.Println(err)
159 }
160 }
161 }
162}
163
164func (h Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
165 if r.Method == http.MethodGet {
166 if h.isAuthorised(r) {
167 http.Redirect(w, r, "/", http.StatusSeeOther)
168 return
169 }
170
171 login, err := fs.ReadFile("static/login.html")
172 if err != nil {
173 fmt.Println("Error reading login.html:", err)
174 }
175
176 if _, err := io.WriteString(w, string(login)); err != nil {
177 fmt.Println(err)
178 }
179 }
180
181 if r.Method == http.MethodPost {
182 err := r.ParseForm()
183 if err != nil {
184 fmt.Println(err)
185 }
186 username := bmStrict.Sanitize(r.FormValue("username"))
187 password := bmStrict.Sanitize(r.FormValue("password"))
188
189 if username == "" || password == "" {
190 w.WriteHeader(http.StatusBadRequest)
191 _, err := w.Write([]byte("No data provided"))
192 if err != nil {
193 fmt.Println(err)
194 }
195 return
196 }
197
198 authorised, err := users.UserAuthorised(h.DbConn, username, password)
199 if err != nil {
200 w.WriteHeader(http.StatusBadRequest)
201 _, err := w.Write([]byte(fmt.Sprintf("Error logging in: %s", err)))
202 if err != nil {
203 fmt.Println(err)
204 }
205 return
206 }
207
208 if !authorised {
209 w.WriteHeader(http.StatusUnauthorized)
210 _, err := w.Write([]byte("Incorrect username or password"))
211 if err != nil {
212 fmt.Println(err)
213 }
214 return
215 }
216
217 session, expiry, err := users.CreateSession(h.DbConn, username)
218 if err != nil {
219 w.WriteHeader(http.StatusBadRequest)
220 _, err := w.Write([]byte(fmt.Sprintf("Error creating session: %s", err)))
221 if err != nil {
222 fmt.Println(err)
223 }
224 return
225 }
226
227 maxAge := int(time.Until(expiry))
228
229 cookie := http.Cookie{
230 Name: "id",
231 Value: session,
232 MaxAge: maxAge,
233 HttpOnly: true,
234 SameSite: http.SameSiteStrictMode,
235 Secure: true,
236 }
237
238 http.SetCookie(w, &cookie)
239 http.Redirect(w, r, "/", http.StatusSeeOther)
240 }
241}
242
243func (h Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
244 cookie, err := r.Cookie("id")
245 if err != nil {
246 fmt.Println(err)
247 }
248
249 err = users.InvalidateSession(h.DbConn, cookie.Value)
250 if err != nil {
251 fmt.Println(err)
252 _, err = w.Write([]byte(fmt.Sprintf("Error logging out: %s", err)))
253 if err != nil {
254 fmt.Println(err)
255 }
256 return
257 }
258 cookie.MaxAge = -1
259 http.SetCookie(w, cookie)
260 http.Redirect(w, r, "/login", http.StatusSeeOther)
261}
262
263// isAuthorised makes a database request to the sessions table to see if the
264// user has a valid session cookie.
265func (h Handler) isAuthorised(r *http.Request) bool {
266 cookie, err := r.Cookie("id")
267 if err != nil {
268 return false
269 }
270
271 authorised, err := users.SessionAuthorised(h.DbConn, cookie.Value)
272 if err != nil {
273 fmt.Println("Error checking session:", err)
274 return false
275 }
276
277 return authorised
278}
279
280func StaticHandler(writer http.ResponseWriter, request *http.Request) {
281 resource := strings.TrimPrefix(request.URL.Path, "/")
282 if strings.HasSuffix(resource, ".css") {
283 writer.Header().Set("Content-Type", "text/css")
284 } else if strings.HasSuffix(resource, ".js") {
285 writer.Header().Set("Content-Type", "text/javascript")
286 }
287 home, err := fs.ReadFile(resource)
288 if err != nil {
289 fmt.Println(err)
290 }
291 if _, err = io.WriteString(writer, string(home)); err != nil {
292 fmt.Println(err)
293 }
294}