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