ws.go

  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			proj, err := project.GetProject(h.DbConn, submittedURL)
 77			if err != nil {
 78				w.WriteHeader(http.StatusBadRequest)
 79				_, err := w.Write([]byte(fmt.Sprintf("Error getting project: %s", err)))
 80				if err != nil {
 81					fmt.Println(err)
 82				}
 83				return
 84			}
 85
 86			if proj.Running == "" {
 87				forge := bmStrict.Sanitize(params.Get("forge"))
 88				if forge == "" {
 89					w.WriteHeader(http.StatusBadRequest)
 90					_, err := w.Write([]byte("No forge provided"))
 91					if err != nil {
 92						fmt.Println(err)
 93					}
 94					return
 95				}
 96
 97				name := bmStrict.Sanitize(params.Get("name"))
 98				if name == "" {
 99					w.WriteHeader(http.StatusBadRequest)
100					_, err := w.Write([]byte("No name provided"))
101					if err != nil {
102						fmt.Println(err)
103					}
104					return
105				}
106
107				proj = project.Project{
108					URL:   submittedURL,
109					Name:  name,
110					Forge: forge,
111				}
112
113				proj.URL = strings.TrimSuffix(proj.URL, ".git")
114
115			}
116
117			proj, err = project.GetReleases(h.DbConn, proj)
118			if err != nil {
119				w.WriteHeader(http.StatusBadRequest)
120				_, err := w.Write([]byte(fmt.Sprintf("Error getting releases: %s", err)))
121				if err != nil {
122					fmt.Println(err)
123				}
124				return
125			}
126
127			tmpl := template.Must(template.ParseFS(fs, "static/select-release.html"))
128			if err := tmpl.Execute(w, proj); err != nil {
129				fmt.Println(err)
130			}
131		} else if action == "delete" {
132			submittedURL := params.Get("url")
133			if submittedURL == "" {
134				w.WriteHeader(http.StatusBadRequest)
135				_, err := w.Write([]byte("No URL provided"))
136				if err != nil {
137					fmt.Println(err)
138				}
139				return
140			}
141
142			project.Untrack(h.DbConn, h.ManualRefresh, submittedURL)
143			http.Redirect(w, r, "/", http.StatusSeeOther)
144		}
145	}
146
147	if r.Method == http.MethodPost {
148		err := r.ParseForm()
149		if err != nil {
150			fmt.Println(err)
151		}
152		nameValue := bmStrict.Sanitize(r.FormValue("name"))
153		urlValue := bmStrict.Sanitize(r.FormValue("url"))
154		forgeValue := bmStrict.Sanitize(r.FormValue("forge"))
155		releaseValue := bmStrict.Sanitize(r.FormValue("release"))
156
157		if nameValue != "" && urlValue != "" && forgeValue != "" && releaseValue != "" {
158			project.Track(h.DbConn, h.ManualRefresh, nameValue, urlValue, forgeValue, releaseValue)
159			http.Redirect(w, r, "/", http.StatusSeeOther)
160			return
161		}
162
163		if nameValue != "" && urlValue != "" && forgeValue != "" && releaseValue == "" {
164			http.Redirect(w, r, "/new?action=yoink&name="+url.QueryEscape(nameValue)+"&url="+url.QueryEscape(urlValue)+"&forge="+url.QueryEscape(forgeValue), http.StatusSeeOther)
165			return
166		}
167
168		if nameValue == "" && urlValue == "" && forgeValue == "" && releaseValue == "" {
169			w.WriteHeader(http.StatusBadRequest)
170			_, err := w.Write([]byte("No data provided"))
171			if err != nil {
172				fmt.Println(err)
173			}
174		}
175	}
176}
177
178func (h Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
179	if r.Method == http.MethodGet {
180		if h.isAuthorised(r) {
181			http.Redirect(w, r, "/", http.StatusSeeOther)
182			return
183		}
184
185		login, err := fs.ReadFile("static/login.html")
186		if err != nil {
187			fmt.Println("Error reading login.html:", err)
188		}
189
190		if _, err := io.WriteString(w, string(login)); err != nil {
191			fmt.Println(err)
192		}
193	}
194
195	if r.Method == http.MethodPost {
196		err := r.ParseForm()
197		if err != nil {
198			fmt.Println(err)
199		}
200		username := bmStrict.Sanitize(r.FormValue("username"))
201		password := bmStrict.Sanitize(r.FormValue("password"))
202
203		if username == "" || password == "" {
204			w.WriteHeader(http.StatusBadRequest)
205			_, err := w.Write([]byte("No data provided"))
206			if err != nil {
207				fmt.Println(err)
208			}
209			return
210		}
211
212		authorised, err := users.UserAuthorised(h.DbConn, username, password)
213		if err != nil {
214			w.WriteHeader(http.StatusBadRequest)
215			_, err := w.Write([]byte(fmt.Sprintf("Error logging in: %s", err)))
216			if err != nil {
217				fmt.Println(err)
218			}
219			return
220		}
221
222		if !authorised {
223			w.WriteHeader(http.StatusUnauthorized)
224			_, err := w.Write([]byte("Incorrect username or password"))
225			if err != nil {
226				fmt.Println(err)
227			}
228			return
229		}
230
231		session, expiry, err := users.CreateSession(h.DbConn, username)
232		if err != nil {
233			w.WriteHeader(http.StatusBadRequest)
234			_, err := w.Write([]byte(fmt.Sprintf("Error creating session: %s", err)))
235			if err != nil {
236				fmt.Println(err)
237			}
238			return
239		}
240
241		maxAge := int(time.Until(expiry))
242
243		cookie := http.Cookie{
244			Name:     "id",
245			Value:    session,
246			MaxAge:   maxAge,
247			HttpOnly: true,
248			SameSite: http.SameSiteStrictMode,
249			Secure:   true,
250		}
251
252		http.SetCookie(w, &cookie)
253		http.Redirect(w, r, "/", http.StatusSeeOther)
254	}
255}
256
257func (h Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
258	cookie, err := r.Cookie("id")
259	if err != nil {
260		fmt.Println(err)
261	}
262
263	err = users.InvalidateSession(h.DbConn, cookie.Value)
264	if err != nil {
265		fmt.Println(err)
266		_, err = w.Write([]byte(fmt.Sprintf("Error logging out: %s", err)))
267		if err != nil {
268			fmt.Println(err)
269		}
270		return
271	}
272	cookie.MaxAge = -1
273	http.SetCookie(w, cookie)
274	http.Redirect(w, r, "/login", http.StatusSeeOther)
275}
276
277// isAuthorised makes a database request to the sessions table to see if the
278// user has a valid session cookie.
279func (h Handler) isAuthorised(r *http.Request) bool {
280	cookie, err := r.Cookie("id")
281	if err != nil {
282		return false
283	}
284
285	authorised, err := users.SessionAuthorised(h.DbConn, cookie.Value)
286	if err != nil {
287		fmt.Println("Error checking session:", err)
288		return false
289	}
290
291	return authorised
292}
293
294func StaticHandler(writer http.ResponseWriter, request *http.Request) {
295	resource := strings.TrimPrefix(request.URL.Path, "/")
296	if strings.HasSuffix(resource, ".css") {
297		writer.Header().Set("Content-Type", "text/css")
298	} else if strings.HasSuffix(resource, ".js") {
299		writer.Header().Set("Content-Type", "text/javascript")
300	}
301	home, err := fs.ReadFile(resource)
302	if err != nil {
303		fmt.Println(err)
304	}
305	if _, err = io.WriteString(writer, string(home)); err != nil {
306		fmt.Println(err)
307	}
308}