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