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