willow/ws/ws.go

309 lines
7.4 KiB
Go
Raw Normal View History

2023-09-24 20:57:56 +00:00
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: Apache-2.0
package ws
2023-09-21 18:03:21 +00:00
import (
"database/sql"
2023-09-21 18:03:21 +00:00
"embed"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
2023-09-21 18:03:21 +00:00
"text/template"
2023-10-26 00:16:36 +00:00
"time"
"git.sr.ht/~amolith/willow/users"
"git.sr.ht/~amolith/willow/project"
"github.com/microcosm-cc/bluemonday"
2023-09-21 18:03:21 +00:00
)
type Handler struct {
DbConn *sql.DB
Mutex *sync.Mutex
Req *chan struct{}
ManualRefresh *chan struct{}
Res *chan []project.Project
}
2023-09-21 18:03:21 +00:00
//go:embed static
var fs embed.FS
// bmUGC = bluemonday.UGCPolicy()
var bmStrict = bluemonday.StrictPolicy()
func (h Handler) RootHandler(w http.ResponseWriter, r *http.Request) {
if !h.isAuthorised(r) {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
*h.Req <- struct{}{}
data := <-*h.Res
2023-09-21 18:03:21 +00:00
tmpl := template.Must(template.ParseFS(fs, "static/home.html"))
if err := tmpl.Execute(w, data); err != nil {
fmt.Println(err)
}
}
func (h Handler) NewHandler(w http.ResponseWriter, r *http.Request) {
if !h.isAuthorised(r) {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
2023-09-21 18:03:21 +00:00
params := r.URL.Query()
action := bmStrict.Sanitize(params.Get("action"))
if r.Method == http.MethodGet {
if action == "" {
tmpl := template.Must(template.ParseFS(fs, "static/new.html"))
if err := tmpl.Execute(w, nil); err != nil {
fmt.Println(err)
}
} else if action != "delete" {
submittedURL := bmStrict.Sanitize(params.Get("url"))
if submittedURL == "" {
2023-09-21 18:03:21 +00:00
w.WriteHeader(http.StatusBadRequest)
2023-09-24 21:08:00 +00:00
_, err := w.Write([]byte("No URL provided"))
if err != nil {
fmt.Println(err)
}
2023-09-21 18:03:21 +00:00
return
}
proj, err := project.GetProject(h.DbConn, submittedURL)
if err != nil {
2023-09-21 18:03:21 +00:00
w.WriteHeader(http.StatusBadRequest)
_, err := w.Write([]byte(fmt.Sprintf("Error getting project: %s", err)))
2023-09-24 21:08:00 +00:00
if err != nil {
fmt.Println(err)
}
2023-09-21 18:03:21 +00:00
return
}
if proj.Running == "" {
forge := bmStrict.Sanitize(params.Get("forge"))
if forge == "" {
w.WriteHeader(http.StatusBadRequest)
_, err := w.Write([]byte("No forge provided"))
if err != nil {
fmt.Println(err)
}
return
2023-09-24 21:08:00 +00:00
}
2023-09-21 18:03:21 +00:00
name := bmStrict.Sanitize(params.Get("name"))
if name == "" {
w.WriteHeader(http.StatusBadRequest)
_, err := w.Write([]byte("No name provided"))
if err != nil {
fmt.Println(err)
}
return
}
proj = project.Project{
URL: submittedURL,
Name: name,
Forge: forge,
}
proj.URL = strings.TrimSuffix(proj.URL, ".git")
}
proj, err = project.GetReleases(h.DbConn, proj)
2023-09-21 18:03:21 +00:00
if err != nil {
w.WriteHeader(http.StatusBadRequest)
2023-09-24 21:08:00 +00:00
_, err := w.Write([]byte(fmt.Sprintf("Error getting releases: %s", err)))
if err != nil {
fmt.Println(err)
}
return
2023-09-21 18:03:21 +00:00
}
2023-09-21 18:03:21 +00:00
tmpl := template.Must(template.ParseFS(fs, "static/select-release.html"))
if err := tmpl.Execute(w, proj); err != nil {
fmt.Println(err)
}
} else if action == "delete" {
submittedURL := params.Get("url")
if submittedURL == "" {
2023-09-21 18:03:21 +00:00
w.WriteHeader(http.StatusBadRequest)
2023-09-24 21:08:00 +00:00
_, err := w.Write([]byte("No URL provided"))
if err != nil {
fmt.Println(err)
}
return
2023-09-21 18:03:21 +00:00
}
project.Untrack(h.DbConn, h.ManualRefresh, submittedURL)
2023-09-21 18:03:21 +00:00
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
if r.Method == http.MethodPost {
2023-09-24 21:08:00 +00:00
err := r.ParseForm()
if err != nil {
fmt.Println(err)
}
2023-09-21 18:03:21 +00:00
nameValue := bmStrict.Sanitize(r.FormValue("name"))
urlValue := bmStrict.Sanitize(r.FormValue("url"))
forgeValue := bmStrict.Sanitize(r.FormValue("forge"))
releaseValue := bmStrict.Sanitize(r.FormValue("release"))
if nameValue != "" && urlValue != "" && forgeValue != "" && releaseValue != "" {
project.Track(h.DbConn, h.ManualRefresh, nameValue, urlValue, forgeValue, releaseValue)
2023-09-21 18:03:21 +00:00
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
if nameValue != "" && urlValue != "" && forgeValue != "" && releaseValue == "" {
http.Redirect(w, r, "/new?action=yoink&name="+url.QueryEscape(nameValue)+"&url="+url.QueryEscape(urlValue)+"&forge="+url.QueryEscape(forgeValue), http.StatusSeeOther)
return
}
if nameValue == "" && urlValue == "" && forgeValue == "" && releaseValue == "" {
w.WriteHeader(http.StatusBadRequest)
2023-09-24 21:08:00 +00:00
_, err := w.Write([]byte("No data provided"))
if err != nil {
fmt.Println(err)
}
2023-09-21 18:03:21 +00:00
}
}
}
func (h Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
2023-10-26 00:16:36 +00:00
if r.Method == http.MethodGet {
if h.isAuthorised(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
login, err := fs.ReadFile("static/login.html")
if err != nil {
fmt.Println("Error reading login.html:", err)
}
if _, err := io.WriteString(w, string(login)); err != nil {
fmt.Println(err)
}
}
if r.Method == http.MethodPost {
err := r.ParseForm()
if err != nil {
fmt.Println(err)
}
username := bmStrict.Sanitize(r.FormValue("username"))
password := bmStrict.Sanitize(r.FormValue("password"))
if username == "" || password == "" {
w.WriteHeader(http.StatusBadRequest)
_, err := w.Write([]byte("No data provided"))
if err != nil {
fmt.Println(err)
}
return
}
authorised, err := users.UserAuthorised(h.DbConn, username, password)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, err := w.Write([]byte(fmt.Sprintf("Error logging in: %s", err)))
if err != nil {
fmt.Println(err)
}
return
}
if !authorised {
w.WriteHeader(http.StatusUnauthorized)
_, err := w.Write([]byte("Incorrect username or password"))
if err != nil {
fmt.Println(err)
}
return
}
session, expiry, err := users.CreateSession(h.DbConn, username)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, err := w.Write([]byte(fmt.Sprintf("Error creating session: %s", err)))
if err != nil {
fmt.Println(err)
}
return
}
2023-10-29 14:49:02 +00:00
maxAge := int(time.Until(expiry))
2023-10-26 00:16:36 +00:00
cookie := http.Cookie{
Name: "id",
Value: session,
MaxAge: maxAge,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Secure: true,
}
http.SetCookie(w, &cookie)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
2023-10-26 00:16:36 +00:00
func (h Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("id")
if err != nil {
fmt.Println(err)
}
err = users.InvalidateSession(h.DbConn, cookie.Value)
if err != nil {
fmt.Println(err)
_, err = w.Write([]byte(fmt.Sprintf("Error logging out: %s", err)))
if err != nil {
fmt.Println(err)
}
return
2023-10-26 00:16:36 +00:00
}
cookie.MaxAge = -1
http.SetCookie(w, cookie)
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
// isAuthorised makes a database request to the sessions table to see if the
// user has a valid session cookie.
func (h Handler) isAuthorised(r *http.Request) bool {
2023-10-26 00:16:36 +00:00
cookie, err := r.Cookie("id")
if err != nil {
return false
}
authorised, err := users.SessionAuthorised(h.DbConn, cookie.Value)
if err != nil {
fmt.Println("Error checking session:", err)
return false
}
return authorised
}
func StaticHandler(writer http.ResponseWriter, request *http.Request) {
2023-09-21 18:03:21 +00:00
resource := strings.TrimPrefix(request.URL.Path, "/")
if strings.HasSuffix(resource, ".css") {
writer.Header().Set("Content-Type", "text/css")
} else if strings.HasSuffix(resource, ".js") {
writer.Header().Set("Content-Type", "text/javascript")
}
home, err := fs.ReadFile(resource)
if err != nil {
fmt.Println(err)
}
if _, err = io.WriteString(writer, string(home)); err != nil {
fmt.Println(err)
}
}