home to your local SPACEGIRL 💫 arimelody.space
1
fork

Configure Feed

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

more account settings page improvements, among others

+407 -484
+307
admin/accounthttp.go
··· 3 3 import ( 4 4 "fmt" 5 5 "net/http" 6 + "os" 7 + "strings" 8 + "time" 6 9 7 10 "arimelody-web/controller" 11 + "arimelody-web/global" 8 12 "arimelody-web/model" 9 13 10 14 "github.com/jmoiron/sqlx" 15 + "golang.org/x/crypto/bcrypt" 11 16 ) 17 + 18 + type TemplateData struct { 19 + Account *model.Account 20 + Message string 21 + Token string 22 + } 12 23 13 24 func AccountHandler(db *sqlx.DB) http.Handler { 14 25 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ··· 36 47 }) 37 48 } 38 49 50 + func LoginHandler(db *sqlx.DB) http.Handler { 51 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 + if r.Method == http.MethodGet { 53 + account, err := controller.GetAccountByRequest(db, r) 54 + if err != nil { 55 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 56 + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) 57 + return 58 + } 59 + if account != nil { 60 + http.Redirect(w, r, "/admin", http.StatusFound) 61 + return 62 + } 63 + 64 + err = pages["login"].Execute(w, TemplateData{}) 65 + if err != nil { 66 + fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) 67 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 68 + return 69 + } 70 + return 71 + } 72 + 73 + type LoginResponse struct { 74 + Account *model.Account 75 + Token string 76 + Message string 77 + } 78 + 79 + render := func(data LoginResponse) { 80 + err := pages["login"].Execute(w, data) 81 + if err != nil { 82 + fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) 83 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 84 + return 85 + } 86 + } 87 + 88 + if r.Method != http.MethodPost { 89 + http.NotFound(w, r); 90 + return 91 + } 92 + 93 + err := r.ParseForm() 94 + if err != nil { 95 + render(LoginResponse{ Message: "Malformed request." }) 96 + return 97 + } 98 + 99 + type LoginRequest struct { 100 + Username string `json:"username"` 101 + Password string `json:"password"` 102 + TOTP string `json:"totp"` 103 + } 104 + credentials := LoginRequest{ 105 + Username: r.Form.Get("username"), 106 + Password: r.Form.Get("password"), 107 + TOTP: r.Form.Get("totp"), 108 + } 109 + 110 + account, err := controller.GetAccount(db, credentials.Username) 111 + if err != nil { 112 + render(LoginResponse{ Message: "Invalid username or password" }) 113 + return 114 + } 115 + if account == nil { 116 + render(LoginResponse{ Message: "Invalid username or password" }) 117 + return 118 + } 119 + 120 + err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)) 121 + if err != nil { 122 + render(LoginResponse{ Message: "Invalid username or password" }) 123 + return 124 + } 125 + 126 + totps, err := controller.GetTOTPsForAccount(db, account.ID) 127 + if err != nil { 128 + fmt.Fprintf(os.Stderr, "WARN: Failed to fetch TOTPs: %v\n", err) 129 + render(LoginResponse{ Message: "Something went wrong. Please try again." }) 130 + return 131 + } 132 + if len(totps) > 0 { 133 + success := false 134 + for _, totp := range totps { 135 + check := controller.GenerateTOTP(totp.Secret, 0) 136 + if check == credentials.TOTP { 137 + success = true 138 + break 139 + } 140 + } 141 + if !success { 142 + render(LoginResponse{ Message: "Invalid TOTP" }) 143 + return 144 + } 145 + } else { 146 + // TODO: user should be prompted to add 2FA method 147 + } 148 + 149 + // login success! 150 + token, err := controller.CreateToken(db, account.ID, r.UserAgent()) 151 + if err != nil { 152 + fmt.Fprintf(os.Stderr, "WARN: Failed to create token: %v\n", err) 153 + render(LoginResponse{ Message: "Something went wrong. Please try again." }) 154 + return 155 + } 156 + 157 + cookie := http.Cookie{} 158 + cookie.Name = global.COOKIE_TOKEN 159 + cookie.Value = token.Token 160 + cookie.Expires = token.ExpiresAt 161 + if strings.HasPrefix(global.Config.BaseUrl, "https") { 162 + cookie.Secure = true 163 + } 164 + cookie.HttpOnly = true 165 + cookie.Path = "/" 166 + http.SetCookie(w, &cookie) 167 + 168 + render(LoginResponse{ Account: account, Token: token.Token }) 169 + }) 170 + } 171 + 172 + func LogoutHandler(db *sqlx.DB) http.Handler { 173 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 174 + if r.Method != http.MethodGet { 175 + http.NotFound(w, r) 176 + return 177 + } 178 + 179 + tokenStr := controller.GetTokenFromRequest(db, r) 180 + 181 + if len(tokenStr) > 0 { 182 + err := controller.DeleteToken(db, tokenStr) 183 + if err != nil { 184 + fmt.Fprintf(os.Stderr, "WARN: Failed to revoke token: %v\n", err) 185 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 186 + return 187 + } 188 + } 189 + 190 + cookie := http.Cookie{} 191 + cookie.Name = global.COOKIE_TOKEN 192 + cookie.Value = "" 193 + cookie.Expires = time.Now() 194 + if strings.HasPrefix(global.Config.BaseUrl, "https") { 195 + cookie.Secure = true 196 + } 197 + cookie.HttpOnly = true 198 + cookie.Path = "/" 199 + http.SetCookie(w, &cookie) 200 + http.Redirect(w, r, "/admin/login", http.StatusFound) 201 + }) 202 + } 203 + 204 + func createAccountHandler(db *sqlx.DB) http.Handler { 205 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 206 + checkAccount, err := controller.GetAccountByRequest(db, r) 207 + if err != nil { 208 + fmt.Printf("WARN: Failed to fetch account: %s\n", err) 209 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 210 + return 211 + } 212 + if checkAccount != nil { 213 + // user is already logged in 214 + http.Redirect(w, r, "/admin", http.StatusFound) 215 + return 216 + } 217 + 218 + type CreateAccountResponse struct { 219 + Account *model.Account 220 + Message string 221 + } 222 + 223 + render := func(data CreateAccountResponse) { 224 + err := pages["create-account"].Execute(w, data) 225 + if err != nil { 226 + fmt.Printf("WARN: Error rendering create account page: %s\n", err) 227 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 228 + } 229 + } 230 + 231 + if r.Method == http.MethodGet { 232 + render(CreateAccountResponse{}) 233 + return 234 + } 235 + 236 + if r.Method != http.MethodPost { 237 + http.NotFound(w, r) 238 + return 239 + } 240 + 241 + err = r.ParseForm() 242 + if err != nil { 243 + render(CreateAccountResponse{ 244 + Message: "Malformed data.", 245 + }) 246 + return 247 + } 248 + 249 + type RegisterRequest struct { 250 + Username string `json:"username"` 251 + Email string `json:"email"` 252 + Password string `json:"password"` 253 + Invite string `json:"invite"` 254 + } 255 + credentials := RegisterRequest{ 256 + Username: r.Form.Get("username"), 257 + Email: r.Form.Get("email"), 258 + Password: r.Form.Get("password"), 259 + Invite: r.Form.Get("invite"), 260 + } 261 + 262 + // make sure code exists in DB 263 + invite, err := controller.GetInvite(db, credentials.Invite) 264 + if err != nil { 265 + fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) 266 + render(CreateAccountResponse{ 267 + Message: "Something went wrong. Please try again.", 268 + }) 269 + return 270 + } 271 + if invite == nil || time.Now().After(invite.ExpiresAt) { 272 + if invite != nil { 273 + err := controller.DeleteInvite(db, invite.Code) 274 + if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } 275 + } 276 + render(CreateAccountResponse{ 277 + Message: "Invalid invite code.", 278 + }) 279 + return 280 + } 281 + 282 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost) 283 + if err != nil { 284 + fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err) 285 + render(CreateAccountResponse{ 286 + Message: "Something went wrong. Please try again.", 287 + }) 288 + return 289 + } 290 + 291 + account := model.Account{ 292 + Username: credentials.Username, 293 + Password: string(hashedPassword), 294 + Email: credentials.Email, 295 + AvatarURL: "/img/default-avatar.png", 296 + } 297 + err = controller.CreateAccount(db, &account) 298 + if err != nil { 299 + if strings.HasPrefix(err.Error(), "pq: duplicate key") { 300 + render(CreateAccountResponse{ 301 + Message: "An account with that username already exists.", 302 + }) 303 + return 304 + } 305 + fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err) 306 + render(CreateAccountResponse{ 307 + Message: "Something went wrong. Please try again.", 308 + }) 309 + return 310 + } 311 + 312 + err = controller.DeleteInvite(db, invite.Code) 313 + if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } 314 + 315 + // registration success! 316 + token, err := controller.CreateToken(db, account.ID, r.UserAgent()) 317 + if err != nil { 318 + fmt.Fprintf(os.Stderr, "WARN: Failed to create token: %v\n", err) 319 + // gracefully redirect user to login page 320 + http.Redirect(w, r, "/admin/login", http.StatusFound) 321 + return 322 + } 323 + 324 + cookie := http.Cookie{} 325 + cookie.Name = global.COOKIE_TOKEN 326 + cookie.Value = token.Token 327 + cookie.Expires = token.ExpiresAt 328 + if strings.HasPrefix(global.Config.BaseUrl, "https") { 329 + cookie.Secure = true 330 + } 331 + cookie.HttpOnly = true 332 + cookie.Path = "/" 333 + http.SetCookie(w, &cookie) 334 + 335 + err = pages["login"].Execute(w, TemplateData{ 336 + Account: &account, 337 + Token: token.Token, 338 + }) 339 + if err != nil { 340 + fmt.Fprintf(os.Stderr, "WARN: Failed to render login page: %v\n", err) 341 + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 342 + return 343 + } 344 + }) 345 + }
-278
admin/http.go
··· 6 6 "net/http" 7 7 "os" 8 8 "path/filepath" 9 - "strings" 10 - "time" 11 9 12 10 "arimelody-web/controller" 13 - "arimelody-web/global" 14 11 "arimelody-web/model" 15 12 16 13 "github.com/jmoiron/sqlx" 17 - "golang.org/x/crypto/bcrypt" 18 14 ) 19 - 20 - type TemplateData struct { 21 - Account *model.Account 22 - Token string 23 - } 24 15 25 16 func Handler(db *sqlx.DB) http.Handler { 26 17 mux := http.NewServeMux() ··· 109 100 ctx := context.WithValue(r.Context(), "account", account) 110 101 111 102 next.ServeHTTP(w, r.WithContext(ctx)) 112 - }) 113 - } 114 - 115 - func LoginHandler(db *sqlx.DB) http.Handler { 116 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 117 - if r.Method == http.MethodGet { 118 - account, err := controller.GetAccountByRequest(db, r) 119 - if err != nil { 120 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 121 - fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %v\n", err) 122 - return 123 - } 124 - if account != nil { 125 - http.Redirect(w, r, "/admin", http.StatusFound) 126 - return 127 - } 128 - 129 - err = pages["login"].Execute(w, TemplateData{}) 130 - if err != nil { 131 - fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) 132 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 133 - return 134 - } 135 - return 136 - } 137 - 138 - if r.Method != http.MethodPost { 139 - http.NotFound(w, r); 140 - return 141 - } 142 - 143 - err := r.ParseForm() 144 - if err != nil { 145 - http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 146 - return 147 - } 148 - 149 - type LoginRequest struct { 150 - Username string `json:"username"` 151 - Password string `json:"password"` 152 - TOTP string `json:"totp"` 153 - } 154 - credentials := LoginRequest{ 155 - Username: r.Form.Get("username"), 156 - Password: r.Form.Get("password"), 157 - TOTP: r.Form.Get("totp"), 158 - } 159 - 160 - account, err := controller.GetAccount(db, credentials.Username) 161 - if err != nil { 162 - http.Error(w, "Invalid username or password", http.StatusBadRequest) 163 - return 164 - } 165 - if account == nil { 166 - http.Error(w, "Invalid username or password", http.StatusBadRequest) 167 - return 168 - } 169 - 170 - err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(credentials.Password)) 171 - if err != nil { 172 - http.Error(w, "Invalid username or password", http.StatusBadRequest) 173 - return 174 - } 175 - 176 - // TODO: check TOTP 177 - 178 - // login success! 179 - token, err := controller.CreateToken(db, account.ID, r.UserAgent()) 180 - if err != nil { 181 - fmt.Fprintf(os.Stderr, "WARN: Failed to create token: %v\n", err) 182 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 183 - return 184 - } 185 - 186 - cookie := http.Cookie{} 187 - cookie.Name = global.COOKIE_TOKEN 188 - cookie.Value = token.Token 189 - cookie.Expires = token.ExpiresAt 190 - if strings.HasPrefix(global.Config.BaseUrl, "https") { 191 - cookie.Secure = true 192 - } 193 - cookie.HttpOnly = true 194 - cookie.Path = "/" 195 - http.SetCookie(w, &cookie) 196 - 197 - err = pages["login"].Execute(w, TemplateData{ 198 - Account: account, 199 - Token: token.Token, 200 - }) 201 - if err != nil { 202 - fmt.Printf("Error rendering admin login page: %s\n", err) 203 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 204 - return 205 - } 206 - }) 207 - } 208 - 209 - func LogoutHandler(db *sqlx.DB) http.Handler { 210 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 211 - if r.Method != http.MethodGet { 212 - http.NotFound(w, r) 213 - return 214 - } 215 - 216 - tokenStr := controller.GetTokenFromRequest(db, r) 217 - 218 - if len(tokenStr) > 0 { 219 - err := controller.DeleteToken(db, tokenStr) 220 - if err != nil { 221 - fmt.Fprintf(os.Stderr, "WARN: Failed to revoke token: %v\n", err) 222 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 223 - return 224 - } 225 - } 226 - 227 - cookie := http.Cookie{} 228 - cookie.Name = global.COOKIE_TOKEN 229 - cookie.Value = "" 230 - cookie.Expires = time.Now() 231 - if strings.HasPrefix(global.Config.BaseUrl, "https") { 232 - cookie.Secure = true 233 - } 234 - cookie.HttpOnly = true 235 - cookie.Path = "/" 236 - http.SetCookie(w, &cookie) 237 - http.Redirect(w, r, "/admin/login", http.StatusFound) 238 - }) 239 - } 240 - 241 - func createAccountHandler(db *sqlx.DB) http.Handler { 242 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 243 - checkAccount, err := controller.GetAccountByRequest(db, r) 244 - if err != nil { 245 - fmt.Printf("WARN: Failed to fetch account: %s\n", err) 246 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 247 - return 248 - } 249 - if checkAccount != nil { 250 - // user is already logged in 251 - http.Redirect(w, r, "/admin", http.StatusFound) 252 - return 253 - } 254 - 255 - type CreateAccountResponse struct { 256 - Account *model.Account 257 - Message string 258 - } 259 - 260 - render := func(data CreateAccountResponse) { 261 - err := pages["create-account"].Execute(w, data) 262 - if err != nil { 263 - fmt.Printf("WARN: Error rendering create account page: %s\n", err) 264 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 265 - } 266 - } 267 - 268 - if r.Method == http.MethodGet { 269 - render(CreateAccountResponse{}) 270 - return 271 - } 272 - 273 - if r.Method != http.MethodPost { 274 - http.NotFound(w, r) 275 - return 276 - } 277 - 278 - err = r.ParseForm() 279 - if err != nil { 280 - render(CreateAccountResponse{ 281 - Message: "Malformed data.", 282 - }) 283 - return 284 - } 285 - 286 - type RegisterRequest struct { 287 - Username string `json:"username"` 288 - Email string `json:"email"` 289 - Password string `json:"password"` 290 - Invite string `json:"invite"` 291 - } 292 - credentials := RegisterRequest{ 293 - Username: r.Form.Get("username"), 294 - Email: r.Form.Get("email"), 295 - Password: r.Form.Get("password"), 296 - Invite: r.Form.Get("invite"), 297 - } 298 - 299 - // make sure code exists in DB 300 - invite, err := controller.GetInvite(db, credentials.Invite) 301 - if err != nil { 302 - fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve invite: %v\n", err) 303 - render(CreateAccountResponse{ 304 - Message: "Something went wrong. Please try again.", 305 - }) 306 - return 307 - } 308 - if invite == nil || time.Now().After(invite.ExpiresAt) { 309 - if invite != nil { 310 - err := controller.DeleteInvite(db, invite.Code) 311 - if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } 312 - } 313 - render(CreateAccountResponse{ 314 - Message: "Invalid invite code.", 315 - }) 316 - return 317 - } 318 - 319 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(credentials.Password), bcrypt.DefaultCost) 320 - if err != nil { 321 - fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err) 322 - render(CreateAccountResponse{ 323 - Message: "Something went wrong. Please try again.", 324 - }) 325 - return 326 - } 327 - 328 - account := model.Account{ 329 - Username: credentials.Username, 330 - Password: string(hashedPassword), 331 - Email: credentials.Email, 332 - AvatarURL: "/img/default-avatar.png", 333 - } 334 - err = controller.CreateAccount(db, &account) 335 - if err != nil { 336 - if strings.HasPrefix(err.Error(), "pq: duplicate key") { 337 - render(CreateAccountResponse{ 338 - Message: "An account with that username already exists.", 339 - }) 340 - return 341 - } 342 - fmt.Fprintf(os.Stderr, "WARN: Failed to create account: %v\n", err) 343 - render(CreateAccountResponse{ 344 - Message: "Something went wrong. Please try again.", 345 - }) 346 - return 347 - } 348 - 349 - err = controller.DeleteInvite(db, invite.Code) 350 - if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to delete expired invite: %v\n", err) } 351 - 352 - // registration success! 353 - token, err := controller.CreateToken(db, account.ID, r.UserAgent()) 354 - if err != nil { 355 - fmt.Fprintf(os.Stderr, "WARN: Failed to create token: %v\n", err) 356 - // gracefully redirect user to login page 357 - http.Redirect(w, r, "/admin/login", http.StatusFound) 358 - return 359 - } 360 - 361 - cookie := http.Cookie{} 362 - cookie.Name = global.COOKIE_TOKEN 363 - cookie.Value = token.Token 364 - cookie.Expires = token.ExpiresAt 365 - if strings.HasPrefix(global.Config.BaseUrl, "https") { 366 - cookie.Secure = true 367 - } 368 - cookie.HttpOnly = true 369 - cookie.Path = "/" 370 - http.SetCookie(w, &cookie) 371 - 372 - err = pages["login"].Execute(w, TemplateData{ 373 - Account: &account, 374 - Token: token.Token, 375 - }) 376 - if err != nil { 377 - fmt.Fprintf(os.Stderr, "WARN: Failed to render login page: %v\n", err) 378 - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 379 - return 380 - } 381 103 }) 382 104 } 383 105
+62
admin/static/admin.css
··· 124 124 font-size: 12px; 125 125 } 126 126 } 127 + 128 + 129 + 130 + #error { 131 + background: #ffa9b8; 132 + border: 1px solid #dc5959; 133 + padding: 1em; 134 + border-radius: 4px; 135 + } 136 + 137 + 138 + 139 + button, .button { 140 + padding: .5em .8em; 141 + font-family: inherit; 142 + font-size: inherit; 143 + border-radius: .5em; 144 + border: 1px solid #a0a0a0; 145 + background: #f0f0f0; 146 + color: inherit; 147 + } 148 + button:hover, .button:hover { 149 + background: #fff; 150 + border-color: #d0d0d0; 151 + } 152 + button:active, .button:active { 153 + background: #d0d0d0; 154 + border-color: #808080; 155 + } 156 + 157 + button { 158 + color: inherit; 159 + } 160 + button.new { 161 + background: #c4ff6a; 162 + border-color: #84b141; 163 + } 164 + button.save { 165 + background: #6fd7ff; 166 + border-color: #6f9eb0; 167 + } 168 + button.delete { 169 + background: #ff7171; 170 + border-color: #7d3535; 171 + } 172 + button:hover { 173 + background: #fff; 174 + border-color: #d0d0d0; 175 + } 176 + button:active { 177 + background: #d0d0d0; 178 + border-color: #808080; 179 + } 180 + button[disabled] { 181 + background: #d0d0d0 !important; 182 + border-color: #808080 !important; 183 + opacity: .5; 184 + cursor: not-allowed !important; 185 + } 186 + a.delete { 187 + color: #d22828; 188 + }
+24
admin/static/edit-account.css
··· 39 39 padding: 1em; 40 40 border-radius: 4px; 41 41 } 42 + 43 + .mfa-device { 44 + padding: .75em; 45 + background: #f8f8f8f8; 46 + border: 1px solid #808080; 47 + border-radius: .5em; 48 + margin-bottom: .5em; 49 + display: flex; 50 + justify-content: space-between; 51 + } 52 + 53 + .mfa-device div { 54 + display: flex; 55 + flex-direction: column; 56 + justify-content: center; 57 + } 58 + 59 + .mfa-device p { 60 + margin: 0; 61 + } 62 + 63 + .mfa-device .mfa-device-name { 64 + font-weight: bold; 65 + }
admin/static/edit-account.js

This is a binary file and will not be displayed.

-48
admin/static/edit-artist.css
··· 66 66 border-color: #808080; 67 67 } 68 68 69 - button, .button { 70 - padding: .5em .8em; 71 - font-family: inherit; 72 - font-size: inherit; 73 - border-radius: .5em; 74 - border: 1px solid #a0a0a0; 75 - background: #f0f0f0; 76 - color: inherit; 77 - } 78 - button:hover, .button:hover { 79 - background: #fff; 80 - border-color: #d0d0d0; 81 - } 82 - button:active, .button:active { 83 - background: #d0d0d0; 84 - border-color: #808080; 85 - } 86 - 87 - button { 88 - color: inherit; 89 - } 90 - button.save { 91 - background: #6fd7ff; 92 - border-color: #6f9eb0; 93 - } 94 - button.delete { 95 - background: #ff7171; 96 - border-color: #7d3535; 97 - } 98 - button:hover { 99 - background: #fff; 100 - border-color: #d0d0d0; 101 - } 102 - button:active { 103 - background: #d0d0d0; 104 - border-color: #808080; 105 - } 106 - button[disabled] { 107 - background: #d0d0d0 !important; 108 - border-color: #808080 !important; 109 - opacity: .5; 110 - cursor: not-allowed !important; 111 - } 112 - 113 - a.delete { 114 - color: #d22828; 115 - } 116 - 117 69 .artist-actions { 118 70 margin-top: auto; 119 71 display: flex;
-52
admin/static/edit-release.css
··· 109 109 padding: 0; 110 110 } 111 111 112 - button, .button { 113 - padding: .5em .8em; 114 - font-family: inherit; 115 - font-size: inherit; 116 - border-radius: .5em; 117 - border: 1px solid #a0a0a0; 118 - background: #f0f0f0; 119 - color: inherit; 120 - } 121 - button:hover, .button:hover { 122 - background: #fff; 123 - border-color: #d0d0d0; 124 - } 125 - button:active, .button:active { 126 - background: #d0d0d0; 127 - border-color: #808080; 128 - } 129 - 130 - button { 131 - color: inherit; 132 - } 133 - button.new { 134 - background: #c4ff6a; 135 - border-color: #84b141; 136 - } 137 - button.save { 138 - background: #6fd7ff; 139 - border-color: #6f9eb0; 140 - } 141 - button.delete { 142 - background: #ff7171; 143 - border-color: #7d3535; 144 - } 145 - button:hover { 146 - background: #fff; 147 - border-color: #d0d0d0; 148 - } 149 - button:active { 150 - background: #d0d0d0; 151 - border-color: #808080; 152 - } 153 - button[disabled] { 154 - background: #d0d0d0 !important; 155 - border-color: #808080 !important; 156 - opacity: .5; 157 - cursor: not-allowed !important; 158 - } 159 - 160 - a.delete { 161 - color: #d22828; 162 - } 163 - 164 112 .release-actions { 165 113 margin-top: auto; 166 114 display: flex;
-48
admin/static/edit-track.css
··· 67 67 border-color: #808080; 68 68 } 69 69 70 - button, .button { 71 - padding: .5em .8em; 72 - font-family: inherit; 73 - font-size: inherit; 74 - border-radius: .5em; 75 - border: 1px solid #a0a0a0; 76 - background: #f0f0f0; 77 - color: inherit; 78 - } 79 - button:hover, .button:hover { 80 - background: #fff; 81 - border-color: #d0d0d0; 82 - } 83 - button:active, .button:active { 84 - background: #d0d0d0; 85 - border-color: #808080; 86 - } 87 - 88 - button { 89 - color: inherit; 90 - } 91 - button.save { 92 - background: #6fd7ff; 93 - border-color: #6f9eb0; 94 - } 95 - button.delete { 96 - background: #ff7171; 97 - border-color: #7d3535; 98 - } 99 - button:hover { 100 - background: #fff; 101 - border-color: #d0d0d0; 102 - } 103 - button:active { 104 - background: #d0d0d0; 105 - border-color: #808080; 106 - } 107 - button[disabled] { 108 - background: #d0d0d0 !important; 109 - border-color: #808080 !important; 110 - opacity: .5; 111 - cursor: not-allowed !important; 112 - } 113 - 114 - a.delete { 115 - color: #d22828; 116 - } 117 - 118 70 .track-actions { 119 71 margin-top: 1em; 120 72 display: flex;
-46
admin/static/index.css
··· 98 98 .track .empty { 99 99 opacity: 0.75; 100 100 } 101 - 102 - 103 - 104 - button, .button { 105 - padding: .5em .8em; 106 - font-family: inherit; 107 - font-size: inherit; 108 - border-radius: .5em; 109 - border: 1px solid #a0a0a0; 110 - background: #f0f0f0; 111 - color: inherit; 112 - } 113 - button:hover, .button:hover { 114 - background: #fff; 115 - border-color: #d0d0d0; 116 - } 117 - button:active, .button:active { 118 - background: #d0d0d0; 119 - border-color: #808080; 120 - } 121 - 122 - button { 123 - color: inherit; 124 - } 125 - button.save { 126 - background: #6fd7ff; 127 - border-color: #6f9eb0; 128 - } 129 - button.delete { 130 - background: #ff7171; 131 - border-color: #7d3535; 132 - } 133 - button:hover { 134 - background: #fff; 135 - border-color: #d0d0d0; 136 - } 137 - button:active { 138 - background: #d0d0d0; 139 - border-color: #808080; 140 - } 141 - button[disabled] { 142 - background: #d0d0d0 !important; 143 - border-color: #808080 !important; 144 - opacity: .5; 145 - cursor: not-allowed !important; 146 - }
+1 -8
admin/views/create-account.html
··· 1 1 {{define "head"}} 2 2 <title>Register - ari melody 💫</title> 3 3 <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 - <link rel="stylesheet" href="/admin/static/index.css"> 4 + <link rel="stylesheet" href="/admin/static/admin.css"> 5 5 <style> 6 6 p a { 7 7 color: #2a67c8; ··· 42 42 font-size: inherit; 43 43 font-family: inherit; 44 44 color: inherit; 45 - } 46 - 47 - #error { 48 - background: #ffa9b8; 49 - border: 1px solid #dc5959; 50 - padding: 1em; 51 - border-radius: 4px; 52 45 } 53 46 </style> 54 47 {{end}}
+8 -3
admin/views/edit-account.html
··· 35 35 {{if .TOTPs}} 36 36 {{range .TOTPs}} 37 37 <div class="mfa-device"> 38 - <h3 class="mfa-device-name">{{.Name}}</h3> 39 - <p class="mfa-device-date">{{.CreatedAt}}</p> 38 + <div> 39 + <p class="mfa-device-name">{{.Name}}</p> 40 + <p class="mfa-device-date">Added: {{.CreatedAt}}</p> 41 + </div> 42 + <div> 43 + <a class="delete">Delete</a> 44 + </div> 40 45 </div> 41 46 {{end}} 42 47 {{else}} 43 48 <p>You have no MFA devices.</p> 44 49 {{end}} 45 50 46 - <a class="create-btn" id="add-mfa-device">Add MFA Device</a> 51 + <button type="submit" class="new" id="add-mfa-device">Add MFA Device</button> 47 52 </div> 48 53 49 54 <div class="card-title">
+5 -1
admin/views/login.html
··· 1 1 {{define "head"}} 2 2 <title>Login - ari melody 💫</title> 3 3 <link rel="shortcut icon" href="/img/favicon.png" type="image/x-icon"> 4 - 4 + <link rel="stylesheet" href="/admin/static/admin.css"> 5 5 <style> 6 6 p a { 7 7 color: #2a67c8; ··· 52 52 53 53 {{define "content"}} 54 54 <main> 55 + {{if .Message}} 56 + <p id="error">{{.Message}}</p> 57 + {{end}} 58 + 55 59 {{if .Token}} 56 60 57 61 <meta http-equiv="refresh" content="0;url=/admin/" />