merged main, dev, and i guess got accounts working??

i am so good at commit messages :3
This commit is contained in:
ari melody 2025-01-20 15:08:01 +00:00
commit 5566a795da
Signed by: ari
GPG key ID: CF99829C92678188
53 changed files with 1366 additions and 398 deletions

View file

@ -9,14 +9,16 @@ import (
"strings"
"time"
"arimelody-web/discord"
"arimelody-web/global"
"arimelody-web/controller"
"arimelody-web/global"
"arimelody-web/model"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
)
type loginData struct {
DiscordURI string
type TemplateData struct {
Account *model.Account
Token string
}
@ -25,57 +27,62 @@ func Handler() http.Handler {
mux.Handle("/login", LoginHandler())
mux.Handle("/create-account", createAccountHandler())
mux.Handle("/logout", MustAuthorise(LogoutHandler()))
mux.Handle("/logout", RequireAccount(global.DB, 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("/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
}
session := GetSession(r)
if session == nil {
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.Printf("FATAL: Failed to pull releases: %s\n", err)
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.Printf("FATAL: Failed to pull artists: %s\n", err)
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.Printf("FATAL: Failed to pull orphan tracks: %s\n", err)
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.Printf("Error executing template: %s\n", err)
fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
@ -84,125 +91,109 @@ func Handler() http.Handler {
return mux
}
func MustAuthorise(next http.Handler) http.Handler {
func RequireAccount(db *sqlx.DB, next http.Handler) http.HandlerFunc {
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)
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(), "session", session)
ctx := context.WithValue(r.Context(), "account", account)
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)
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
}
code := r.URL.Query().Get("code")
if code == "" {
pages["login"].Execute(w, loginData{DiscordURI: discord.REDIRECT_URI})
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
}
auth_token, err := discord.GetOAuthTokenFromCode(code)
if r.Method != http.MethodPost {
http.NotFound(w, r);
return
}
err := r.ParseForm()
if err != nil {
fmt.Printf("Failed to retrieve discord access token: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
fmt.Fprintf(os.Stderr, "WARN: Error logging in: %s\n", err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
discord_user, err := discord.GetDiscordUserFromAuth(auth_token)
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 {
fmt.Printf("Failed to retrieve discord user information: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
http.Error(w, "No account exists with this username and password.", http.StatusBadRequest)
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)
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!
session := createSession(discord_user.Username, time.Now().Add(24 * time.Hour))
sessions = append(sessions, &session)
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 = "token"
cookie.Value = session.Token
cookie.Expires = time.Now().Add(24 * time.Hour)
if strings.HasPrefix(global.HTTP_DOMAIN, "https") {
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, loginData{Token: session.Token})
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)
@ -218,33 +209,48 @@ func LogoutHandler() http.Handler {
return
}
session := GetSession(r)
token_str := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
// 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)
}
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
}
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
}
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, nil)
err := pages["create-account"].Execute(w, TemplateData{})
if err != nil {
fmt.Printf("Error rendering admin crearte account page: %s\n", err)
fmt.Printf("Error rendering create account page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}