package admin import ( "context" "fmt" "net/http" "os" "path/filepath" "strings" "time" "arimelody-web/discord" "arimelody-web/global" "arimelody-web/controller" "arimelody-web/model" ) type loginData struct { DiscordURI string Token string } func Handler() http.Handler { mux := http.NewServeMux() mux.Handle("/login", LoginHandler()) mux.Handle("/logout", MustAuthorise(LogoutHandler())) mux.Handle("/static/", http.StripPrefix("/static", staticHandler())) mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease()))) mux.Handle("/artist/", MustAuthorise(http.StripPrefix("/artist", serveArtist()))) mux.Handle("/track/", MustAuthorise(http.StripPrefix("/track", serveTrack()))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } session := GetSession(r) if session == nil { http.Redirect(w, r, "/admin/login", http.StatusFound) return } releases, err := controller.GetAllReleases(global.DB, false, 0, true) if err != nil { fmt.Printf("FATAL: 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.Printf("FATAL: 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.Printf("FATAL: Failed to pull orphan tracks: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } type IndexData struct { Releases []*model.Release Artists []*model.Artist Tracks []*model.Track } err = pages["index"].Execute(w, IndexData{ Releases: releases, Artists: artists, Tracks: tracks, }) if err != nil { fmt.Printf("Error executing template: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } })) return mux } func MustAuthorise(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session := GetSession(r) if session == nil { http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } ctx := context.WithValue(r.Context(), "session", session) next.ServeHTTP(w, r.WithContext(ctx)) }) } func GetSession(r *http.Request) *Session { if ADMIN_BYPASS { return &Session{} } var token = "" // is the session token in context? var ctx_session = r.Context().Value("session") if ctx_session != nil { token = ctx_session.(*Session).Token } // okay, is it in the auth header? if token == "" { if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { token = r.Header.Get("Authorization")[7:] } } // finally, is it in the cookie? if token == "" { cookie, err := r.Cookie("token") if err != nil { return nil } token = cookie.Value } var session *Session = nil for _, s := range sessions { if s.Expires.Before(time.Now()) { // expired session. remove it from the list! new_sessions := []*Session{} for _, ns := range sessions { if ns.Token == s.Token { continue } new_sessions = append(new_sessions, ns) } sessions = new_sessions continue } if s.Token == token { session = s break } } return session } func LoginHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !discord.CREDENTIALS_PROVIDED || ADMIN_ID_DISCORD == "" { http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) return } fmt.Println(discord.CLIENT_ID) fmt.Println(discord.API_ENDPOINT) fmt.Println(discord.REDIRECT_URI) code := r.URL.Query().Get("code") if code == "" { pages["login"].Execute(w, loginData{DiscordURI: discord.REDIRECT_URI}) return } auth_token, err := discord.GetOAuthTokenFromCode(code) if err != nil { fmt.Printf("Failed to retrieve discord access token: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } discord_user, err := discord.GetDiscordUserFromAuth(auth_token) if err != nil { fmt.Printf("Failed to retrieve discord user information: %s\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if discord_user.ID != ADMIN_ID_DISCORD { // TODO: unauthorized user; revoke the token fmt.Printf("Unauthorized login attempted: %s\n", discord_user.ID) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } // login success! session := createSession(discord_user.Username, time.Now().Add(24 * time.Hour)) sessions = append(sessions, &session) cookie := http.Cookie{} cookie.Name = "token" cookie.Value = session.Token cookie.Expires = time.Now().Add(24 * time.Hour) if strings.HasPrefix(global.HTTP_DOMAIN, "https") { cookie.Secure = true } cookie.HttpOnly = true cookie.Path = "/" http.SetCookie(w, &cookie) err = pages["login"].Execute(w, loginData{Token: session.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 } session := GetSession(r) // remove this session from the list sessions = func (token string) []*Session { new_sessions := []*Session{} for _, session := range sessions { if session.Token != token { new_sessions = append(new_sessions, session) } } return new_sessions }(session.Token) err := pages["logout"].Execute(w, nil) if err != nil { fmt.Printf("Error rendering admin logout 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) }) }