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