home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

logs in use; new audit log panel!

+417 -74
+9 -11
admin/accounthttp.go
··· 6 6 "net/http" 7 7 "net/url" 8 8 "os" 9 - "time" 10 9 11 10 "arimelody-web/controller" 11 + "arimelody-web/log" 12 12 "arimelody-web/model" 13 13 14 14 "golang.org/x/crypto/bcrypt" ··· 115 115 return 116 116 } 117 117 118 + app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" changed password by user request. (%s)", session.Account.Username, controller.ResolveIP(r)) 119 + 118 120 controller.SetSessionError(app.DB, session, "") 119 121 controller.SetSessionMessage(app.DB, session, "Password updated successfully.") 120 122 http.Redirect(w, r, "/admin/account", http.StatusFound) ··· 143 145 144 146 // check password 145 147 if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(r.Form.Get("password"))); err != nil { 146 - fmt.Printf( 147 - "[%s] WARN: Account \"%s\" attempted account deletion with incorrect password.\n", 148 - time.Now().Format(time.UnixDate), 149 - session.Account.Username, 150 - ) 148 + app.Log.Warn(log.TYPE_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(r)) 151 149 controller.SetSessionError(app.DB, session, "Incorrect password.") 152 150 http.Redirect(w, r, "/admin/account", http.StatusFound) 153 151 return ··· 161 159 return 162 160 } 163 161 164 - fmt.Printf( 165 - "[%s] INFO: Account \"%s\" deleted by user request.\n", 166 - time.Now().Format(time.UnixDate), 167 - session.Account.Username, 168 - ) 162 + app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" deleted by user request. (%s)", session.Account.Username, controller.ResolveIP(r)) 169 163 170 164 controller.SetSessionAccount(app.DB, session, nil) 171 165 controller.SetSessionError(app.DB, session, "") ··· 324 318 return 325 319 } 326 320 321 + app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" created TOTP method \"%s\".", session.Account.Username, totp.Name) 322 + 327 323 controller.SetSessionError(app.DB, session, "") 328 324 controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" created successfully.", totp.Name)) 329 325 http.Redirect(w, r, "/admin/account", http.StatusFound) ··· 364 360 http.Redirect(w, r, "/admin/account", http.StatusFound) 365 361 return 366 362 } 363 + 364 + app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" deleted TOTP method \"%s\".", session.Account.Username, totp.Name) 367 365 368 366 controller.SetSessionError(app.DB, session, "") 369 367 controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" deleted successfully.", totp.Name))
+14 -25
admin/http.go
··· 11 11 "time" 12 12 13 13 "arimelody-web/controller" 14 + "arimelody-web/log" 14 15 "arimelody-web/model" 15 16 16 17 "golang.org/x/crypto/bcrypt" ··· 38 39 39 40 mux.Handle("/account", requireAccount(accountIndexHandler(app))) 40 41 mux.Handle("/account/", requireAccount(http.StripPrefix("/account", accountHandler(app)))) 42 + 43 + mux.Handle("/logs", requireAccount(logsHandler(app))) 41 44 42 45 mux.Handle("/release/", requireAccount(http.StripPrefix("/release", serveRelease(app)))) 43 46 mux.Handle("/artist/", requireAccount(http.StripPrefix("/artist", serveArtist(app)))) ··· 198 201 return 199 202 } 200 203 201 - fmt.Printf( 202 - "[%s]: Account registered: %s (%s)\n", 203 - time.Now().Format(time.UnixDate), 204 - account.Username, 205 - account.ID, 206 - ) 204 + app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" (%s) created using invite \"%s\". (%s)", account.Username, account.ID, invite.Code, controller.ResolveIP(r)) 207 205 208 206 err = controller.DeleteInvite(app.DB, invite.Code) 209 - if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } 207 + if err != nil { 208 + app.Log.Warn(log.TYPE_ACCOUNT, "Failed to delete expired invite \"%s\": %v", invite.Code, err) 209 + } 210 210 211 211 // registration success! 212 212 controller.SetSessionAccount(app.DB, session, &account) ··· 277 277 278 278 err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password)) 279 279 if err != nil { 280 - fmt.Printf( 281 - "[%s] INFO: Account \"%s\" attempted login with incorrect password.\n", 282 - time.Now().Format(time.UnixDate), 283 - account.Username, 284 - ) 280 + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(r)) 285 281 controller.SetSessionError(app.DB, session, "Invalid username or password.") 286 282 render() 287 283 return ··· 307 303 return 308 304 } 309 305 310 - fmt.Printf( 311 - "[%s] INFO: Account \"%s\" logged in\n", 312 - time.Now().Format(time.UnixDate), 313 - account.Username, 314 - ) 315 - 306 + // login success! 316 307 // TODO: log login activity to user 308 + app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in. (%s)", account.Username, controller.ResolveIP(r)) 309 + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" does not have any TOTP methods assigned.", account.Username) 317 310 318 - // login success! 319 311 err = controller.SetSessionAccount(app.DB, session, account) 320 312 if err != nil { 321 313 fmt.Fprintf(os.Stderr, "WARN: Failed to set session account: %v\n", err) ··· 371 363 totpCode := r.FormValue("totp") 372 364 373 365 if len(totpCode) != controller.TOTP_CODE_LENGTH { 366 + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(r)) 374 367 controller.SetSessionError(app.DB, session, "Invalid TOTP.") 375 368 render() 376 369 return ··· 384 377 return 385 378 } 386 379 if totpMethod == nil { 380 + app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(r)) 387 381 controller.SetSessionError(app.DB, session, "Invalid TOTP.") 388 382 render() 389 383 return 390 384 } 391 385 392 - fmt.Printf( 393 - "[%s] INFO: Account \"%s\" logged in with method \"%s\"\n", 394 - time.Now().Format(time.UnixDate), 395 - session.AttemptAccount.Username, 396 - totpMethod.Name, 397 - ) 386 + app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in with TOTP method \"%s\". (%s)", session.AttemptAccount.Username, totpMethod.Name, controller.ResolveIP(r)) 398 387 399 388 err = controller.SetSessionAccount(app.DB, session, session.AttemptAccount) 400 389 if err != nil {
+67
admin/logshttp.go
··· 1 + package admin 2 + 3 + import ( 4 + "arimelody-web/log" 5 + "arimelody-web/model" 6 + "fmt" 7 + "net/http" 8 + "os" 9 + "strings" 10 + ) 11 + 12 + func logsHandler(app *model.AppState) http.Handler { 13 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 + if r.Method != http.MethodGet { 15 + http.NotFound(w, r) 16 + return 17 + } 18 + 19 + session := r.Context().Value("session").(*model.Session) 20 + 21 + levelFilter := []log.LogLevel{} 22 + typeFilter := []string{} 23 + 24 + query := r.URL.Query().Get("q") 25 + 26 + for key, value := range r.URL.Query() { 27 + if strings.HasPrefix(key, "level-") && value[0] == "on" { 28 + m := map[string]log.LogLevel{ 29 + "info": log.LEVEL_INFO, 30 + "warn": log.LEVEL_WARN, 31 + } 32 + level, ok := m[strings.TrimPrefix(key, "level-")] 33 + if ok { 34 + levelFilter = append(levelFilter, level) 35 + } 36 + continue 37 + } 38 + 39 + if strings.HasPrefix(key, "type-") && value[0] == "on" { 40 + typeFilter = append(typeFilter, string(strings.TrimPrefix(key, "type-"))) 41 + continue 42 + } 43 + } 44 + 45 + logs, err := app.Log.Search(levelFilter, typeFilter, query, 100, 0) 46 + if err != nil { 47 + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch audit logs: %v\n", err) 48 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 49 + return 50 + } 51 + 52 + type LogsResponse struct { 53 + Session *model.Session 54 + Logs []*log.Log 55 + } 56 + 57 + err = logsTemplate.Execute(w, LogsResponse{ 58 + Session: session, 59 + Logs: logs, 60 + }) 61 + if err != nil { 62 + fmt.Fprintf(os.Stderr, "WARN: Failed to render audit logs page: %v\n", err) 63 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 64 + return 65 + } 66 + }) 67 + }
+85
admin/static/logs.css
··· 1 + main { 2 + width: min(1080px, calc(100% - 2em))!important 3 + } 4 + 5 + form { 6 + margin: 1em 0; 7 + } 8 + 9 + div#search { 10 + display: flex; 11 + } 12 + 13 + #search input { 14 + margin: 0; 15 + flex-grow: 1; 16 + 17 + border-right: none; 18 + border-top-right-radius: 0; 19 + border-bottom-right-radius: 0; 20 + } 21 + 22 + #search button { 23 + padding: 0 .5em; 24 + 25 + border-top-left-radius: 0; 26 + border-bottom-left-radius: 0; 27 + } 28 + 29 + form #filters p { 30 + margin: .5em 0 0 0; 31 + } 32 + form #filters label { 33 + display: inline; 34 + } 35 + form #filters input { 36 + margin-right: 1em; 37 + display: inline; 38 + } 39 + 40 + #logs { 41 + width: 100%; 42 + border-collapse: collapse; 43 + } 44 + 45 + #logs tr { 46 + } 47 + 48 + #logs tr td { 49 + border-bottom: 1px solid #8888; 50 + } 51 + 52 + #logs tr td:nth-child(even) { 53 + background: #00000004; 54 + } 55 + 56 + #logs th, #logs td { 57 + padding: .4em .8em; 58 + } 59 + 60 + td, th { 61 + width: 1%; 62 + text-align: left; 63 + white-space: nowrap; 64 + } 65 + td.log-level, 66 + th.log-level, 67 + td.log-type, 68 + th.log-type { 69 + text-align: center; 70 + } 71 + td.log-content, 72 + td.log-content { 73 + width: 100%; 74 + } 75 + 76 + .log:hover { 77 + background: #fff8; 78 + } 79 + 80 + .log.warn { 81 + background: #ffe86a; 82 + } 83 + .log.warn:hover { 84 + background: #ffec81; 85 + }
+37 -2
admin/templates.go
··· 1 1 package admin 2 2 3 3 import ( 4 - "html/template" 5 - "path/filepath" 4 + "arimelody-web/log" 5 + "fmt" 6 + "html/template" 7 + "path/filepath" 8 + "strings" 9 + "time" 6 10 ) 7 11 8 12 var indexTemplate = template.Must(template.ParseFiles( ··· 46 50 filepath.Join("admin", "views", "layout.html"), 47 51 filepath.Join("views", "prideflag.html"), 48 52 filepath.Join("admin", "views", "totp-confirm.html"), 53 + )) 54 + 55 + var logsTemplate = template.Must(template.New("layout.html").Funcs(template.FuncMap{ 56 + "parseLevel": func(level log.LogLevel) string { 57 + switch level { 58 + case log.LEVEL_INFO: 59 + return "INFO" 60 + case log.LEVEL_WARN: 61 + return "WARN" 62 + } 63 + return fmt.Sprintf("%d?", level) 64 + }, 65 + "titleCase": func(logType string) string { 66 + runes := []rune(logType) 67 + for i, r := range runes { 68 + if (i == 0 || runes[i - 1] == ' ') && r >= 'a' && r <= 'z' { 69 + runes[i] = r + ('A' - 'a') 70 + } 71 + } 72 + return string(runes) 73 + }, 74 + "lower": func(str string) string { return strings.ToLower(str) }, 75 + "prettyTime": func(t time.Time) string { 76 + // return t.Format("2006-01-02 15:04:05") 77 + // return t.Format("15:04:05, 2 Jan 2006") 78 + return t.Format("02 Jan 2006, 15:04:05") 79 + }, 80 + }).ParseFiles( 81 + filepath.Join("admin", "views", "layout.html"), 82 + filepath.Join("views", "prideflag.html"), 83 + filepath.Join("admin", "views", "logs.html"), 49 84 )) 50 85 51 86 var releaseTemplate = template.Must(template.ParseFiles(
+7
admin/views/layout.html
··· 23 23 <div class="nav-item"> 24 24 <a href="/admin">home</a> 25 25 </div> 26 + {{if .Session.Account}} 27 + <div class="nav-item"> 28 + <a href="/admin/logs">logs</a> 29 + </div> 30 + {{end}} 31 + 26 32 <div class="flex-fill"></div> 33 + 27 34 {{if .Session.Account}} 28 35 <div class="nav-item"> 29 36 <a href="/admin/account">account ({{.Session.Account.Username}})</a>
+68
admin/views/logs.html
··· 1 + {{define "head"}} 2 + <title>Audit Logs - ari melody 💫</title> 3 + <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 + <link rel="stylesheet" href="/admin/static/admin.css"> 5 + <link rel="stylesheet" href="/admin/static/logs.css"> 6 + {{end}} 7 + 8 + {{define "content"}} 9 + <main> 10 + <h1>Audit Logs</h1> 11 + 12 + <form action="/admin/logs" method="GET"> 13 + <div id="search"> 14 + <input type="text" name="q" value="" placeholder="Filter by message..."> 15 + <button type="submit" class="save">Search</button> 16 + </div> 17 + <div id="filters"> 18 + <div> 19 + <p>Level:</p> 20 + <label for="level-info">Info</label> 21 + <input type="checkbox" name="level-info" id="level-info"> 22 + <label for="level-warn">Warning</label> 23 + <input type="checkbox" name="level-warn" id="level-warn"> 24 + </div> 25 + <div> 26 + <p>Type:</p> 27 + <label for="type-account">Account</label> 28 + <input type="checkbox" name="type-account" id="type-account"> 29 + <label for="type-music">Music</label> 30 + <input type="checkbox" name="type-music" id="type-music"> 31 + <label for="type-artist">Artist</label> 32 + <input type="checkbox" name="type-artist" id="type-artist"> 33 + <label for="type-blog">Blog</label> 34 + <input type="checkbox" name="type-blog" id="type-blog"> 35 + <label for="type-artwork">Artwork</label> 36 + <input type="checkbox" name="type-artwork" id="type-artwork"> 37 + <label for="type-files">Files</label> 38 + <input type="checkbox" name="type-files" id="type-files"> 39 + <label for="type-misc">Misc</label> 40 + <input type="checkbox" name="type-misc" id="type-misc"> 41 + </div> 42 + </div> 43 + </form> 44 + 45 + <hr> 46 + 47 + <table id="logs"> 48 + <thead> 49 + <tr> 50 + <th class="log-time">Time</th> 51 + <th class="log-level">Level</th> 52 + <th class="log-type">Type</th> 53 + <th class="log-content">Message</th> 54 + </tr> 55 + </thead> 56 + <tbody> 57 + {{range .Logs}} 58 + <tr class="log {{lower (parseLevel .Level)}}"> 59 + <td class="log-time">{{prettyTime .CreatedAt}}</td> 60 + <td class="log-level">{{parseLevel .Level}}</td> 61 + <td class="log-type">{{titleCase .Type}}</td> 62 + <td class="log-content">{{.Content}}</td> 63 + </tr> 64 + {{end}} 65 + </tbody> 66 + </table> 67 + </main> 68 + {{end}}
+13
api/artist.go
··· 11 11 "time" 12 12 13 13 "arimelody-web/controller" 14 + "arimelody-web/log" 14 15 "arimelody-web/model" 15 16 ) 16 17 ··· 88 89 89 90 func CreateArtist(app *model.AppState) http.Handler { 90 91 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 92 + session := r.Context().Value("session").(*model.Session) 93 + 91 94 var artist model.Artist 92 95 err := json.NewDecoder(r.Body).Decode(&artist) 93 96 if err != nil { ··· 112 115 return 113 116 } 114 117 118 + app.Log.Info(log.TYPE_ARTIST, "Artist \"%s\" created by \"%s\".", artist.Name, session.Account.Username) 119 + 115 120 w.WriteHeader(http.StatusCreated) 116 121 }) 117 122 } 118 123 119 124 func UpdateArtist(app *model.AppState, artist *model.Artist) http.Handler { 120 125 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 126 + session := r.Context().Value("session").(*model.Session) 127 + 121 128 err := json.NewDecoder(r.Body).Decode(&artist) 122 129 if err != nil { 123 130 fmt.Printf("WARN: Failed to update artist: %s\n", err) ··· 158 165 fmt.Printf("WARN: Failed to update artist %s: %s\n", artist.ID, err) 159 166 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 160 167 } 168 + 169 + app.Log.Info(log.TYPE_ARTIST, "Artist \"%s\" updated by \"%s\".", artist.Name, session.Account.Username) 161 170 }) 162 171 } 163 172 164 173 func DeleteArtist(app *model.AppState, artist *model.Artist) http.Handler { 165 174 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 175 + session := r.Context().Value("session").(*model.Session) 176 + 166 177 err := controller.DeleteArtist(app.DB, artist.ID) 167 178 if err != nil { 168 179 if strings.Contains(err.Error(), "no rows") { ··· 172 183 fmt.Printf("WARN: Failed to delete artist %s: %s\n", artist.ID, err) 173 184 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 174 185 } 186 + 187 + app.Log.Info(log.TYPE_ARTIST, "Artist \"%s\" deleted by \"%s\".", artist.Name, session.Account.Username) 175 188 }) 176 189 }
+23 -8
api/release.go
··· 11 11 "time" 12 12 13 13 "arimelody-web/controller" 14 + "arimelody-web/log" 14 15 "arimelody-web/model" 15 16 ) 16 17 ··· 189 190 190 191 func CreateRelease(app *model.AppState) http.Handler { 191 192 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 192 - if r.Method != http.MethodPost { 193 - http.NotFound(w, r) 194 - return 195 - } 193 + session := r.Context().Value("session").(*model.Session) 196 194 197 195 var release model.Release 198 196 err := json.NewDecoder(r.Body).Decode(&release) ··· 226 224 return 227 225 } 228 226 227 + app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" created by \"%s\".", release.ID, session.Account.Username) 228 + 229 229 w.Header().Add("Content-Type", "application/json") 230 230 w.WriteHeader(http.StatusCreated) 231 231 encoder := json.NewEncoder(w) ··· 240 240 241 241 func UpdateRelease(app *model.AppState, release *model.Release) http.Handler { 242 242 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 243 + session := r.Context().Value("session").(*model.Session) 244 + 243 245 if r.URL.Path == "/" { 244 246 http.NotFound(w, r) 245 247 return ··· 304 306 fmt.Printf("WARN: Failed to update release %s: %s\n", release.ID, err) 305 307 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 306 308 } 309 + 310 + app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) 307 311 }) 308 312 } 309 313 310 314 func UpdateReleaseTracks(app *model.AppState, release *model.Release) http.Handler { 311 315 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 316 + session := r.Context().Value("session").(*model.Session) 317 + 312 318 var trackIDs = []string{} 313 319 err := json.NewDecoder(r.Body).Decode(&trackIDs) 314 320 if err != nil { ··· 325 331 fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err) 326 332 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 327 333 } 334 + 335 + app.Log.Info(log.TYPE_MUSIC, "Tracklist for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) 328 336 }) 329 337 } 330 338 331 339 func UpdateReleaseCredits(app *model.AppState, release *model.Release) http.Handler { 332 340 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 341 + session := r.Context().Value("session").(*model.Session) 342 + 333 343 type creditJSON struct { 334 344 Artist string 335 345 Role string ··· 366 376 fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err) 367 377 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 368 378 } 379 + 380 + app.Log.Info(log.TYPE_MUSIC, "Credits for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) 369 381 }) 370 382 } 371 383 372 384 func UpdateReleaseLinks(app *model.AppState, release *model.Release) http.Handler { 373 385 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 374 - if r.Method != http.MethodPut { 375 - http.NotFound(w, r) 376 - return 377 - } 386 + session := r.Context().Value("session").(*model.Session) 378 387 379 388 var links = []*model.Link{} 380 389 err := json.NewDecoder(r.Body).Decode(&links) ··· 392 401 fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err) 393 402 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 394 403 } 404 + 405 + app.Log.Info(log.TYPE_MUSIC, "Links for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username) 395 406 }) 396 407 } 397 408 398 409 func DeleteRelease(app *model.AppState, release *model.Release) http.Handler { 399 410 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 411 + session := r.Context().Value("session").(*model.Session) 412 + 400 413 err := controller.DeleteRelease(app.DB, release.ID) 401 414 if err != nil { 402 415 if strings.Contains(err.Error(), "no rows") { ··· 406 419 fmt.Printf("WARN: Failed to delete release %s: %s\n", release.ID, err) 407 420 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 408 421 } 422 + 423 + app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" deleted by \"%s\".", release.ID, session.Account.Username) 409 424 }) 410 425 }
+14 -6
api/track.go
··· 6 6 "net/http" 7 7 8 8 "arimelody-web/controller" 9 + "arimelody-web/log" 9 10 "arimelody-web/model" 10 11 ) 11 12 ··· 75 76 76 77 func CreateTrack(app *model.AppState) http.Handler { 77 78 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 78 - if r.Method != http.MethodPost { 79 - http.NotFound(w, r) 80 - return 81 - } 79 + session := r.Context().Value("session").(*model.Session) 82 80 83 81 var track model.Track 84 82 err := json.NewDecoder(r.Body).Decode(&track) ··· 99 97 return 100 98 } 101 99 100 + app.Log.Info(log.TYPE_MUSIC, "Track \"%s\" (%s) created by \"%s\".", track.Title, track.ID, session.Account.Username) 101 + 102 102 w.Header().Add("Content-Type", "text/plain") 103 103 w.WriteHeader(http.StatusCreated) 104 104 w.Write([]byte(id)) ··· 107 107 108 108 func UpdateTrack(app *model.AppState, track *model.Track) http.Handler { 109 109 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 110 - if r.Method != http.MethodPut || r.URL.Path == "/" { 110 + if r.URL.Path == "/" { 111 111 http.NotFound(w, r) 112 112 return 113 113 } 114 114 115 + session := r.Context().Value("session").(*model.Session) 116 + 115 117 err := json.NewDecoder(r.Body).Decode(&track) 116 118 if err != nil { 117 119 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) ··· 130 132 return 131 133 } 132 134 135 + app.Log.Info(log.TYPE_MUSIC, "Track \"%s\" (%s) updated by \"%s\".", track.Title, track.ID, session.Account.Username) 136 + 133 137 w.Header().Add("Content-Type", "application/json") 134 138 encoder := json.NewEncoder(w) 135 139 encoder.SetIndent("", "\t") ··· 142 146 143 147 func DeleteTrack(app *model.AppState, track *model.Track) http.Handler { 144 148 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 145 - if r.Method != http.MethodDelete || r.URL.Path == "/" { 149 + if r.URL.Path == "/" { 146 150 http.NotFound(w, r) 147 151 return 148 152 } 153 + 154 + session := r.Context().Value("session").(*model.Session) 149 155 150 156 var trackID = r.URL.Path[1:] 151 157 err := controller.DeleteTrack(app.DB, trackID) ··· 153 159 fmt.Printf("WARN: Failed to delete track %s: %s\n", trackID, err) 154 160 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 155 161 } 162 + 163 + app.Log.Info(log.TYPE_MUSIC, "Track \"%s\" (%s) deleted by \"%s\".", track.Title, track.ID, session.Account.Username) 156 164 }) 157 165 }
+3
api/uploads.go
··· 1 1 package api 2 2 3 3 import ( 4 + "arimelody-web/log" 4 5 "arimelody-web/model" 5 6 "bufio" 6 7 "encoding/base64" ··· 48 49 if err := buffer.Flush(); err != nil { 49 50 return "", nil 50 51 } 52 + 53 + app.Log.Info(log.TYPE_FILES, "\"%s/%s.%s\" created.", directory, filename, ext) 51 54 52 55 return filename, nil 53 56 }
+19
controller/ip.go
··· 1 + package controller 2 + 3 + import ( 4 + "net/http" 5 + "slices" 6 + ) 7 + 8 + // Returns the request's original IP address, resolving the `x-forwarded-for` 9 + // header if the request originates from a trusted proxy. 10 + func ResolveIP(r *http.Request) string { 11 + trustedProxies := []string{ "10.4.20.69" } 12 + if slices.Contains(trustedProxies, r.RemoteAddr) { 13 + forwardedFor := r.Header.Get("x-forwarded-for") 14 + if len(forwardedFor) > 0 { 15 + return forwardedFor 16 + } 17 + } 18 + return r.RemoteAddr 19 + }
+25 -12
log/log.go
··· 25 25 const ( 26 26 TYPE_ACCOUNT string = "account" 27 27 TYPE_MUSIC string = "music" 28 + TYPE_ARTIST string = "artist" 28 29 TYPE_BLOG string = "blog" 29 30 TYPE_ARTWORK string = "artwork" 31 + TYPE_FILES string = "files" 30 32 TYPE_MISC string = "misc" 31 33 ) 32 34 ··· 40 42 41 43 func (self *Logger) Info(logType string, format string, args ...any) { 42 44 logString := fmt.Sprintf(format, args...) 43 - fmt.Printf("[%s] INFO: %s", logType, logString) 45 + fmt.Printf("[%s] [%s] INFO: %s\n", time.Now().Format(time.UnixDate), logType, logString) 44 46 err := createLog(self.DB, LEVEL_INFO, logType, logString) 45 47 if err != nil { 46 48 fmt.Fprintf(os.Stderr, "WARN: Failed to push log to database: %v\n", err) ··· 49 51 50 52 func (self *Logger) Warn(logType string, format string, args ...any) { 51 53 logString := fmt.Sprintf(format, args...) 52 - fmt.Fprintf(os.Stderr, "[%s] WARN: %s", logType, logString) 54 + fmt.Fprintf(os.Stderr, "[%s] [%s] WARN: %s\n", time.Now().Format(time.UnixDate), logType, logString) 53 55 err := createLog(self.DB, LEVEL_WARN, logType, logString) 54 56 if err != nil { 55 57 fmt.Fprintf(os.Stderr, "WARN: Failed to push log to database: %v\n", err) 56 58 } 57 59 } 58 60 59 - func (self *Logger) Fatal(logType string, format string, args ...any) { 60 - fmt.Fprintf(os.Stderr, fmt.Sprintf("[%s] FATAL: %s", logType, format), args...) 61 - // we won't need to push fatal logs to DB, as these usually precede a panic or crash 62 - } 63 - 64 61 func (self *Logger) Fetch(id string) (*Log, error) { 65 62 log := Log{} 66 63 err := self.DB.Get(&log, "SELECT * FROM auditlog WHERE id=$1", id) 67 64 return &log, err 68 65 } 69 66 70 - func (self *Logger) Search(levelFilters []LogLevel, typeFilters []string, content string, offset int, limit int) ([]*Log, error) { 67 + func (self *Logger) Search(levelFilters []LogLevel, typeFilters []string, content string, limit int, offset int) ([]*Log, error) { 71 68 logs := []*Log{} 72 69 73 70 params := []any{ limit, offset } ··· 80 77 } 81 78 82 79 if len(levelFilters) > 0 { 83 - conditions += " AND level IN (" 80 + if len(conditions) > 0 { 81 + conditions += " AND level IN (" 82 + } else { 83 + conditions += " WHERE level IN (" 84 + } 84 85 for i := range levelFilters { 85 86 conditions += fmt.Sprintf("$%d", len(params) + 1) 86 87 if i < len(levelFilters) - 1 { ··· 92 93 } 93 94 94 95 if len(typeFilters) > 0 { 95 - conditions += " AND type IN (" 96 + if len(conditions) > 0 { 97 + conditions += " AND type IN (" 98 + } else { 99 + conditions += " WHERE type IN (" 100 + } 96 101 for i := range typeFilters { 97 102 conditions += fmt.Sprintf("$%d", len(params) + 1) 98 103 if i < len(typeFilters) - 1 { ··· 108 113 conditions, 109 114 ) 110 115 111 - // TODO: remove after testing 112 - fmt.Println(query) 116 + /* 117 + fmt.Printf("%s (", query) 118 + for i, param := range params { 119 + fmt.Print(param) 120 + if i < len(params) - 1 { 121 + fmt.Print(", ") 122 + } 123 + } 124 + fmt.Print(")\n") 125 + */ 113 126 114 127 err := self.DB.Select(&logs, query, params...) 115 128 if err != nil {
+26 -9
main.go
··· 19 19 "arimelody-web/controller" 20 20 "arimelody-web/model" 21 21 "arimelody-web/templates" 22 + "arimelody-web/log" 22 23 "arimelody-web/view" 23 - "arimelody-web/log" 24 24 25 25 "github.com/jmoiron/sqlx" 26 26 _ "github.com/lib/pq" ··· 78 78 app.DB.SetMaxIdleConns(10) 79 79 defer app.DB.Close() 80 80 81 + app.Log = log.Logger{ DB: app.DB } 82 + 81 83 // handle command arguments 82 84 if len(os.Args) > 1 { 83 85 arg := os.Args[1] ··· 119 121 os.Exit(1) 120 122 } 121 123 124 + app.Log.Info(log.TYPE_ACCOUNT, "TOTP method \"%s\" for \"%s\" created via config utility.", totp.Name, account.Username) 122 125 url := controller.GenerateTOTPURI(account.Username, totp.Secret) 123 126 fmt.Printf("%s\n", url) 124 127 return ··· 148 151 os.Exit(1) 149 152 } 150 153 154 + app.Log.Info(log.TYPE_ACCOUNT, "TOTP method \"%s\" for \"%s\" deleted via config utility.", totpName, account.Username) 151 155 fmt.Printf("TOTP method \"%s\" deleted.\n", totpName) 152 156 return 153 157 ··· 223 227 fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up TOTP methods: %v\n", err) 224 228 os.Exit(1) 225 229 } 230 + app.Log.Info(log.TYPE_ACCOUNT, "TOTP methods pruned via config utility.") 226 231 fmt.Printf("Cleaned up dangling TOTP methods successfully.\n") 227 232 return 228 233 ··· 234 239 os.Exit(1) 235 240 } 236 241 242 + app.Log.Info(log.TYPE_ACCOUNT, "Invite generted via config utility (%s).", invite.Code) 237 243 fmt.Printf( 238 244 "Here you go! This code expires in %d hours: %s\n", 239 245 int(math.Ceil(invite.ExpiresAt.Sub(invite.CreatedAt).Hours())), ··· 249 255 os.Exit(1) 250 256 } 251 257 258 + app.Log.Info(log.TYPE_ACCOUNT, "Invites purged via config utility.") 252 259 fmt.Printf("Invites deleted successfully.\n") 253 260 return 254 261 ··· 301 308 account.Password = string(hashedPassword) 302 309 err = controller.UpdateAccount(app.DB, account) 303 310 if err != nil { 304 - fmt.Fprintf(os.Stderr, "FATAL: Failed to delete account: %v\n", err) 311 + fmt.Fprintf(os.Stderr, "FATAL: Failed to update password: %v\n", err) 305 312 os.Exit(1) 306 313 } 307 314 308 - fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username) 315 + app.Log.Info(log.TYPE_ACCOUNT, "Password for '%s' updated via config utility.", account.Username) 316 + fmt.Printf("Password for \"%s\" updated successfully.\n", account.Username) 309 317 return 310 318 311 319 case "deleteAccount": ··· 340 348 os.Exit(1) 341 349 } 342 350 351 + app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' deleted via config utility.", account.Username) 343 352 fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username) 344 353 return 345 354 346 - case "testLogSearch": 347 - // TODO: rename to "logs"; add parameters 348 - logger := log.Logger { DB: app.DB } 349 - logs, err := logger.Search([]log.LogLevel{ log.LEVEL_INFO, log.LEVEL_WARN }, []string{ log.TYPE_ACCOUNT, log.TYPE_MUSIC }, "ari", 0, 100) 355 + case "logs": 356 + // TODO: add log search parameters 357 + logs, err := app.Log.Search([]log.LogLevel{}, []string{}, "", 100, 0) 350 358 if err != nil { 351 359 fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch logs: %v\n", err) 352 360 os.Exit(1) 353 361 } 354 - for _, log := range(logs) { 355 - fmt.Printf("[%s] [%s] [%d] [%s] %s\n", log.CreatedAt.Format(time.UnixDate), log.ID, log.Level, log.Type, log.Content) 362 + for _, item := range(logs) { 363 + levelStr := "" 364 + switch item.Level { 365 + case log.LEVEL_INFO: 366 + levelStr = "INFO" 367 + case log.LEVEL_WARN: 368 + levelStr = "WARN" 369 + default: 370 + levelStr = fmt.Sprintf("? (%d)", item.Level) 371 + } 372 + fmt.Printf("[%s] %s:\n\t[%s] %s: %s\n", item.CreatedAt.Format(time.UnixDate), item.ID, item.Type, levelStr, item.Content) 356 373 } 357 374 return 358 375 }
+6 -1
model/appstate.go
··· 1 1 package model 2 2 3 - import "github.com/jmoiron/sqlx" 3 + import ( 4 + "github.com/jmoiron/sqlx" 5 + 6 + "arimelody-web/log" 7 + ) 4 8 5 9 type ( 6 10 DBConfig struct { ··· 29 33 AppState struct { 30 34 DB *sqlx.DB 31 35 Config Config 36 + Log log.Logger 32 37 } 33 38 )
+1
model/release.go
··· 24 24 Tracks []*Track `json:"tracks"` 25 25 Credits []*Credit `json:"credits"` 26 26 Links []*Link `json:"links"` 27 + CreatedAt time.Time `json:"-" db:"created_at"` 27 28 } 28 29 ) 29 30