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