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