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}