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