HOLY REFACTOR GOOD GRIEF (also finally started some CRUD work)

Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
ari melody 2024-08-02 22:48:26 +01:00
parent 1c310c9101
commit 442889340c
80 changed files with 1571 additions and 1330 deletions

View file

@ -9,9 +9,9 @@ import (
type (
Session struct {
UserID string
Token string
Expires int64
Token string
UserID string
Expires time.Time
}
)
@ -28,11 +28,11 @@ var ADMIN_ID_DISCORD = func() string {
var sessions []*Session
func createSession(UserID string) Session {
func createSession(username string, expires time.Time) Session {
return Session{
UserID: UserID,
Token: string(generateToken()),
Expires: time.Now().Add(24 * time.Hour).Unix(),
UserID: username,
Expires: expires,
}
}

View file

@ -12,71 +12,106 @@ import (
"arimelody.me/arimelody.me/discord"
"arimelody.me/arimelody.me/global"
musicModel "arimelody.me/arimelody.me/music/model"
)
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("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("hello /admin!"))
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
session := GetSession(r)
if session == nil {
http.Redirect(w, r, "/admin/login", http.StatusFound)
return
}
type IndexData struct {
Releases []musicModel.Release
Artists []musicModel.Artist
}
serveTemplate("index.html", IndexData{
Releases: global.Releases,
Artists: global.Artists,
}).ServeHTTP(w, r)
}))
mux.Handle("/callback", global.HTTPLog(OAuthCallbackHandler()))
mux.Handle("/login", global.HTTPLog(LoginHandler()))
mux.Handle("/verify", global.HTTPLog(MustAuthorise(VerifyHandler())))
mux.Handle("/logout", global.HTTPLog(MustAuthorise(LogoutHandler())))
mux.Handle("/static", global.HTTPLog(MustAuthorise(staticHandler())))
return mux
}
func MustAuthorise(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
auth = auth[7:]
} else {
cookie, err := r.Cookie("token")
if err != nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
auth = cookie.Value
}
var session *Session
for _, s := range sessions {
if s.Expires < time.Now().Unix() {
// 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)
}
continue
}
if s.Token == auth {
session = s
break
}
}
session := GetSession(r)
if session == nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "role", "admin")
ctx := context.WithValue(r.Context(), "session", session)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func GetSession(r *http.Request) *Session {
// TODO: remove later- this bypasses auth!
return &Session{}
var token = ""
// is the session token in context?
var ctx_session = r.Context().Value("session")
if ctx_session != nil {
token = ctx_session.(string)
}
// 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 ADMIN_ID_DISCORD == "" {
if discord.CREDENTIALS_PROVIDED && ADMIN_ID_DISCORD == "" {
http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
return
}
@ -84,7 +119,7 @@ func LoginHandler() http.Handler {
code := r.URL.Query().Get("code")
if code == "" {
http.Redirect(w, r, discord.REDIRECT_URI, http.StatusTemporaryRedirect)
serveTemplate("login.html", discord.REDIRECT_URI).ServeHTTP(w, r)
return
}
@ -109,7 +144,7 @@ func LoginHandler() http.Handler {
}
// login success!
session := createSession(discord_user.Username)
session := createSession(discord_user.Username, time.Now().Add(24 * time.Hour))
sessions = append(sessions, &session)
cookie := http.Cookie{}
@ -122,12 +157,25 @@ func LoginHandler() http.Handler {
http.SetCookie(w, &cookie)
w.WriteHeader(http.StatusOK)
w.Write([]byte(session.Token))
w.Header().Add("Content-Type", "text/html")
w.Write([]byte(
"<!DOCTYPE html><html><head>"+
"<meta http-equiv=\"refresh\" content=\"5;url=/admin/\" />"+
"</head><body>"+
"Logged in successfully. "+
"You should be redirected to <a href=\"/admin/\">/admin/</a> in 5 seconds."+
"</body></html>"),
)
})
}
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 := r.Context().Value("token").(string)
if token == "" {
@ -145,31 +193,19 @@ func LogoutHandler() http.Handler {
}(token)
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
w.Write([]byte(
"<meta http-equiv=\"refresh\" content=\"5;url=/\" />"+
"Logged out successfully. "+
"You should be redirected to <a href=\"/\">/</a> in 5 seconds."),
)
})
}
func OAuthCallbackHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
})
}
func VerifyHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// this is an authorised endpoint, so you *must* supply a valid token
// before accessing this route.
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
}
func ServeTemplate(page string, data any) http.Handler {
func serveTemplate(page string, data any) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lp_layout := filepath.Join("views", "layout.html")
lp_header := filepath.Join("views", "header.html")
lp_footer := filepath.Join("views", "footer.html")
lp_layout := filepath.Join("views", "admin", "layout.html")
lp_prideflag := filepath.Join("views", "prideflag.html")
fp := filepath.Join("views", filepath.Clean(page))
fp := filepath.Join("views", "admin", filepath.Clean(page))
info, err := os.Stat(fp)
if err != nil {
@ -184,7 +220,7 @@ func ServeTemplate(page string, data any) http.Handler {
return
}
template, err := template.ParseFiles(lp_layout, lp_header, lp_footer, lp_prideflag, fp)
template, err := template.ParseFiles(lp_layout, lp_prideflag, fp)
if err != nil {
fmt.Printf("Error parsing template files: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

187
admin/static/admin.css Normal file
View file

@ -0,0 +1,187 @@
@import url("/style/prideflag.css");
@import url("/font/inter/inter.css");
body {
width: 100%;
height: 100vh;
margin: 0;
padding: 0;
font-family: "Inter", sans-serif;
font-size: 16px;
color: #303030;
background: #f0f0f0;
}
header {
width: min(720px, calc(100% - 2em));
height: 2em;
margin: 1em auto;
display: flex;
flex-direction: row;
justify-content: center;
background: #f8f8f8;
border-radius: .5em;
border: 1px solid #808080;
}
header .icon {
height: 100%;
margin-right: 1em;
}
header a {
height: 100%;
width: auto;
margin: 0px;
padding: 0 1em;
display: flex;
line-height: 2em;
text-decoration: none;
color: inherit;
}
header a:hover {
background: #00000010;
}
main {
width: min(720px, calc(100% - 2em));
margin: 0 auto;
padding: 1em;
}
a {
color: inherit;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.card h2 {
margin: 0 0 .5em 0;
}
.card h3,
.card p {
margin: 0;
}
.release {
padding: 1em;
display: flex;
flex-direction: row;
gap: 1em;
border-radius: .5em;
background: #f8f8f8f8;
border: 1px solid #808080;
}
.release-artwork {
width: 96px;
display: flex;
justify-content: center;
align-items: center;
}
.release-artwork img {
width: 100%;
aspect-ratio: 1;
}
.latest-release .release-info {
width: 300px;
flex-direction: column;
}
.release-title small {
opacity: .75;
}
.release-links {
margin: .5em 0;
padding: 0;
display: flex;
flex-direction: row;
list-style: none;
flex-wrap: wrap;
gap: .5em;
}
.release-links li {
flex-grow: 1;
}
.release-links a {
padding: .5em;
display: block;
border-radius: .5em;
text-decoration: none;
color: #f0f0f0;
background: #303030;
text-align: center;
transition: color .1s, background .1s;
}
.release-links a:hover {
color: #303030;
background: #f0f0f0;
}
.release-actions {
margin-top: .5em;
}
.release-actions a {
margin-right: .3em;
padding: .3em .5em;
display: inline-block;
border-radius: .3em;
background: #e0e0e0;
transition: color .1s, background .1s;
}
.release-actions a:hover {
color: #303030;
background: #f0f0f0;
text-decoration: none;
}
.artist {
padding: .5em;
display: flex;
flex-direction: row;
align-items: center;
gap: .5em;
border-radius: .5em;
background: #f8f8f8f8;
border: 1px solid #808080;
}
.artist:hover {
text-decoration: hover;
}
.artist-avatar {
width: 32px;
height: 32px;
object-fit: cover;
border-radius: 100%;
}

0
admin/static/admin.js Normal file
View file