package admin import ( "context" "fmt" "net/http" "os" "path/filepath" "strings" "time" "arimelody-web/controller" "arimelody-web/global" "arimelody-web/model" "github.com/jmoiron/sqlx" "golang.org/x/crypto/bcrypt" ) type TemplateData struct { Account *model.Account Token string } func Handler() http.Handler { mux := http.NewServeMux() mux.Handle("/login", LoginHandler()) mux.Handle("/create-account", createAccountHandler()) mux.Handle("/logout", RequireAccount(global.DB, LogoutHandler())) mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) mux.Handle("/release/", RequireAccount(global.DB, http.StripPrefix("/release", serveRelease()))) mux.Handle("/artist/", RequireAccount(global.DB, http.StripPrefix("/artist", serveArtist()))) mux.Handle("/track/", RequireAccount(global.DB, http.StripPrefix("/track", serveTrack()))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } account, err := controller.GetAccountByRequest(global.DB, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err) } if account == nil { http.Redirect(w, r, "/admin/login", http.StatusFound) return } releases, err := controller.GetAllReleases(global.DB, false, 0, true) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } artists, err := controller.GetAllArtists(global.DB) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } tracks, err := controller.GetOrphanTracks(global.DB) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } type IndexData struct { Account *model.Account Releases []*model.Release Artists []*model.Artist Tracks []*model.Track } err = pages["index"].Execute(w, IndexData{ Account: account, Releases: releases, Artists: artists, Tracks: tracks, }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } })) return mux } func RequireAccount(db *sqlx.DB, next http.Handler) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { account, err := controller.GetAccountByRequest(db, r) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err.Error()) return } if account == nil { // TODO: include context in redirect http.Redirect(w, r, "/admin/login", http.StatusFound) return } ctx := context.WithValue(r.Context(), "account", account) next.ServeHTTP(w, r.WithContext(ctx)) }) } func LoginHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { account, err := controller.GetAccountByRequest(global.DB, r) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err.Error()) return } if account != nil { http.Redirect(w, r, "/admin", http.StatusFound) return } err = pages["login"].Execute(w, TemplateData{}) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } return } if r.Method != http.MethodPost { http.NotFound(w, r); return } err := r.ParseForm() if err != nil { fmt.Fprintf(os.Stderr, "WARN: Error logging in: %s\n", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` TOTP string `json:"totp"` } data := LoginRequest{ Username: r.Form.Get("username"), Password: r.Form.Get("password"), TOTP: r.Form.Get("totp"), } account, err := controller.GetAccount(global.DB, data.Username) if err != nil { http.Error(w, "No account exists with this username and password.", http.StatusBadRequest) return } err = bcrypt.CompareHashAndPassword(account.Password, []byte(data.Password)) if err != nil { http.Error(w, "No account exists with this username and password.", http.StatusBadRequest) return } // TODO: check TOTP // login success! token, err := controller.CreateToken(global.DB, account.ID, r.UserAgent()) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to create token: %s\n", err.Error()) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } cookie := http.Cookie{} cookie.Name = global.COOKIE_TOKEN cookie.Value = token.Token cookie.Expires = token.ExpiresAt if strings.HasPrefix(global.Config.BaseUrl, "https") { cookie.Secure = true } cookie.HttpOnly = true cookie.Path = "/" http.SetCookie(w, &cookie) err = pages["login"].Execute(w, TemplateData{ Account: account, Token: token.Token, }) if err != nil { fmt.Printf("Error rendering admin login page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } }) } func LogoutHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.NotFound(w, r) return } token_str := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if token_str == "" { cookie, err := r.Cookie(global.COOKIE_TOKEN) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Error fetching token cookie: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if cookie != nil { token_str = cookie.Value } } if len(token_str) > 0 { err := controller.DeleteToken(global.DB, token_str) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to revoke token: %s\n", err.Error()) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } cookie := http.Cookie{} cookie.Name = global.COOKIE_TOKEN cookie.Value = "" cookie.Expires = time.Now() if strings.HasPrefix(global.Config.BaseUrl, "https") { cookie.Secure = true } cookie.HttpOnly = true cookie.Path = "/" http.SetCookie(w, &cookie) http.Redirect(w, r, "/admin/login", http.StatusFound) }) } func createAccountHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := pages["create-account"].Execute(w, TemplateData{}) if err != nil { fmt.Printf("Error rendering create account page: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } }) } func staticHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { info, err := os.Stat(filepath.Join("admin", "static", filepath.Clean(r.URL.Path))) // does the file exist? if err != nil { if os.IsNotExist(err) { http.NotFound(w, r) return } } // is thjs a directory? (forbidden) if info.IsDir() { http.NotFound(w, r) return } http.FileServer(http.Dir(filepath.Join("admin", "static"))).ServeHTTP(w, r) }) }