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