From a6abc75bdb4250f4b0454ddec120867e04524991 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Fri, 5 Jul 2024 23:26:36 +0300 Subject: [PATCH 1/3] chore(nginx): separate media from other requests --- nginx/dankmuzikk.conf | 44 ++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/nginx/dankmuzikk.conf b/nginx/dankmuzikk.conf index 8924a16..5273966 100644 --- a/nginx/dankmuzikk.conf +++ b/nginx/dankmuzikk.conf @@ -1,23 +1,33 @@ server { - listen 80; - listen [::]:80; - server_name dankmuzikk.com; + listen 80; + listen [::]:80; + server_name dankmuzikk.com; - location ~ ^/(.*)$ { - set $upstream http://127.0.0.1:20251; - proxy_read_timeout 180; - proxy_connect_timeout 180; - proxy_send_timeout 180; + location ~ ^/muzikkx(.*)$ { + set $upstream http://127.0.0.1:20251/muzikkx; + proxy_read_timeout 1800; + proxy_connect_timeout 1800; + proxy_send_timeout 1800; # required headers for safari :) - proxy_set_header Connection "keep-alive"; - proxy_set_header Range "bytes=0-"; - proxy_set_header Icy-Metadata "0"; - proxy_pass_request_headers on; - proxy_pass $upstream/$1$is_args$args; - } + proxy_set_header Connection "keep-alive"; + proxy_set_header Range "bytes=0-"; + proxy_set_header Icy-Metadata "0"; + proxy_pass_request_headers on; + proxy_pass $upstream$1$is_args$args; + } - location / { - proxy_pass http://127.0.0.1:20251; - } + location ~ ^/(.*)$ { + set $upstream http://127.0.0.1:20251; + proxy_read_timeout 180; + proxy_connect_timeout 180; + proxy_send_timeout 180; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_request_headers on; + proxy_pass $upstream/$1$is_args$args; + } + + location / { + proxy_pass http://127.0.0.1:20251; + } } From a534fa44b09d98e8262cb4bb4a741d81c289e4e2 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 6 Jul 2024 02:48:56 +0300 Subject: [PATCH 2/3] feat(middlewares): add middlewares --- app/handlers/middlewares/auth/auth.go | 139 ++++++++++++++++++ app/handlers/middlewares/contenttype/html.go | 16 ++ app/handlers/middlewares/contenttype/json.go | 10 ++ app/handlers/middlewares/ismobile/ismobile.go | 20 +++ app/handlers/middlewares/keys.go | 16 ++ app/handlers/middlewares/theme/theme.go | 32 ++++ 6 files changed, 233 insertions(+) create mode 100644 app/handlers/middlewares/auth/auth.go create mode 100644 app/handlers/middlewares/contenttype/html.go create mode 100644 app/handlers/middlewares/contenttype/json.go create mode 100644 app/handlers/middlewares/ismobile/ismobile.go create mode 100644 app/handlers/middlewares/keys.go create mode 100644 app/handlers/middlewares/theme/theme.go diff --git a/app/handlers/middlewares/auth/auth.go b/app/handlers/middlewares/auth/auth.go new file mode 100644 index 0000000..b17f2a3 --- /dev/null +++ b/app/handlers/middlewares/auth/auth.go @@ -0,0 +1,139 @@ +package auth + +import ( + "context" + "dankmuzikk/config" + "dankmuzikk/db" + "dankmuzikk/entities" + "dankmuzikk/handlers/middlewares/contenttype" + "dankmuzikk/models" + "dankmuzikk/services/jwt" + "net/http" + "slices" +) + +// Cookie keys +const ( + VerificationTokenKey = "verification-token" + SessionTokenKey = "token" +) + +// Context keys +const ( + ProfileIdKey = "profile-id" + PlaylistPermission = "playlist-permission" +) + +var noAuthPaths = []string{"/login", "/signup"} + +type mw struct { + profileRepo db.GORMDBGetter + jwtUtil jwt.Decoder[jwt.Json] +} + +// New returns a new auth middle ware instance. +// Using a GORMDBGetter because this is supposed to be a light fetch, +// Where BaseDB doesn't provide column selection yet :( +func New( + accountRepo db.GORMDBGetter, + jwtUtil jwt.Decoder[jwt.Json], +) *mw { + return &mw{accountRepo, jwtUtil} +} + +// AuthPage authenticates a page's handler. +func (a *mw) AuthPage(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + htmxRedirect := contenttype.IsNoLayoutPage(r) + profile, err := a.authenticate(r) + authed := err == nil + ctx := context.WithValue(r.Context(), ProfileIdKey, profile.Id) + + switch { + case authed && slices.Contains(noAuthPaths, r.URL.Path): + http.Redirect(w, r, config.Env().Hostname, http.StatusTemporaryRedirect) + case !authed && slices.Contains(noAuthPaths, r.URL.Path): + h(w, r.WithContext(ctx)) + case !authed && htmxRedirect: + w.Header().Set("HX-Redirect", "/login") + case !authed && !htmxRedirect: + http.Redirect(w, r, config.Env().Hostname+"/login", http.StatusTemporaryRedirect) + default: + h(w, r.WithContext(ctx)) + } + } +} + +// OptionalAuthPage authenticates a page's handler optionally (without redirection). +func (a *mw) OptionalAuthPage(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + profile, err := a.authenticate(r) + if err != nil { + h(w, r) + return + } + ctx := context.WithValue(r.Context(), ProfileIdKey, profile.Id) + h(w, r.WithContext(ctx)) + } +} + +// AuthApi authenticates an API's handler. +func (a *mw) AuthApi(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + profile, err := a.authenticate(r) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + ctx := context.WithValue(r.Context(), ProfileIdKey, profile.Id) + h(w, r.WithContext(ctx)) + } +} + +// OptionalAuthApi authenticates a page's handler optionally (without 401). +func (a *mw) OptionalAuthApi(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + profile, err := a.authenticate(r) + if err != nil { + h(w, r) + return + } + ctx := context.WithValue(r.Context(), ProfileIdKey, profile.Id) + h(w, r.WithContext(ctx)) + } +} + +func (a *mw) authenticate(r *http.Request) (entities.Profile, error) { + sessionToken, err := r.Cookie(SessionTokenKey) + if err != nil { + return entities.Profile{}, err + } + theThing, err := a.jwtUtil.Decode(sessionToken.Value, jwt.SessionToken) + if err != nil { + return entities.Profile{}, err + } + username, validUsername := theThing.Payload["username"].(string) + if !validUsername || username == "" { + return entities.Profile{}, err + } + + var profile models.Profile + + err = a. + profileRepo. + GetDB(). + Model(&profile). + Select("id"). + Where("username = ?", username). + First(&profile). + Error + + if err != nil { + return entities.Profile{}, err + } + + return entities.Profile{ + Id: profile.Id, + Name: profile.Name, + }, nil +} diff --git a/app/handlers/middlewares/contenttype/html.go b/app/handlers/middlewares/contenttype/html.go new file mode 100644 index 0000000..c1903fb --- /dev/null +++ b/app/handlers/middlewares/contenttype/html.go @@ -0,0 +1,16 @@ +package contenttype + +import "net/http" + +func Html(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + h(w, r) + } +} + +// IsNoLayoutPage checks if the requested page requires a no reload or not. +func IsNoLayoutPage(r *http.Request) bool { + noReload, exists := r.URL.Query()["no_layout"] + return exists && noReload[0] == "true" +} diff --git a/app/handlers/middlewares/contenttype/json.go b/app/handlers/middlewares/contenttype/json.go new file mode 100644 index 0000000..b8a86ef --- /dev/null +++ b/app/handlers/middlewares/contenttype/json.go @@ -0,0 +1,10 @@ +package contenttype + +import "net/http" + +func Json(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + h(w, r) + } +} diff --git a/app/handlers/middlewares/ismobile/ismobile.go b/app/handlers/middlewares/ismobile/ismobile.go new file mode 100644 index 0000000..3c915c8 --- /dev/null +++ b/app/handlers/middlewares/ismobile/ismobile.go @@ -0,0 +1,20 @@ +package ismobile + +import ( + "context" + "net/http" + "strings" +) + +const IsMobileKey = "is-mobile" + +func Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), IsMobileKey, isMobile(r)) + h.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func isMobile(r *http.Request) bool { + return strings.Contains(strings.ToLower(r.Header.Get("User-Agent")), "mobile") +} diff --git a/app/handlers/middlewares/keys.go b/app/handlers/middlewares/keys.go new file mode 100644 index 0000000..25a8e48 --- /dev/null +++ b/app/handlers/middlewares/keys.go @@ -0,0 +1,16 @@ +package middlewares + +// Cookie keys +const ( + VerificationTokenKey = "verification-token" + SessionTokenKey = "token" + ThemeName = "theme-name" +) + +// Context keys +const ( + ProfileIdKey = "profile-id" + ThemeKey = "theme-name" + IsMobileKey = "is-mobile" + PlaylistPermission = "playlist-permission" +) diff --git a/app/handlers/middlewares/theme/theme.go b/app/handlers/middlewares/theme/theme.go new file mode 100644 index 0000000..77049cb --- /dev/null +++ b/app/handlers/middlewares/theme/theme.go @@ -0,0 +1,32 @@ +package theme + +import ( + "context" + "net/http" +) + +const ThemeKey = "theme-name" + +func Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), ThemeKey, getTheme(r)) + h.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getTheme(r *http.Request) string { + themeCookie, err := r.Cookie(ThemeKey) + if err != nil || themeCookie == nil || themeCookie.Value == "" { + return "black" + } + switch themeCookie.Value { + case "dank": + return "dank" + case "white": + return "white" + case "black": + fallthrough + default: + return "black" + } +} From a4a56a8d9cef2a2962fecda1e424e604bc28d8e4 Mon Sep 17 00:00:00 2001 From: Baraa Al-Masri Date: Sat, 6 Jul 2024 02:49:39 +0300 Subject: [PATCH 3/3] feat(middlewares): move from the magic spaghetti --- app/cmd/server/server.go | 61 ++++++----- app/handlers/apis/email_login.go | 10 +- app/handlers/apis/google_login.go | 4 +- app/handlers/apis/history.go | 4 +- app/handlers/apis/logout.go | 4 +- app/handlers/apis/playlist.go | 18 +-- app/handlers/apis/songs.go | 10 +- app/handlers/handler.go | 175 ------------------------------ app/handlers/keys.go | 16 --- app/handlers/pages/pages.go | 31 +++--- 10 files changed, 73 insertions(+), 260 deletions(-) delete mode 100644 app/handlers/handler.go delete mode 100644 app/handlers/keys.go diff --git a/app/cmd/server/server.go b/app/cmd/server/server.go index 7d40808..e7b847f 100644 --- a/app/cmd/server/server.go +++ b/app/cmd/server/server.go @@ -3,8 +3,11 @@ package server import ( "dankmuzikk/config" "dankmuzikk/db" - "dankmuzikk/handlers" "dankmuzikk/handlers/apis" + "dankmuzikk/handlers/middlewares/auth" + "dankmuzikk/handlers/middlewares/contenttype" + "dankmuzikk/handlers/middlewares/ismobile" + "dankmuzikk/handlers/middlewares/theme" "dankmuzikk/handlers/pages" "dankmuzikk/log" "dankmuzikk/models" @@ -53,7 +56,7 @@ func StartServer(staticFS embed.FS) error { jwtUtil := jwt.NewJWTImpl() - gHandler := handlers.NewHandler(profileRepo, jwtUtil) + authMw := auth.New(profileRepo, jwtUtil) ///////////// Pages and files ///////////// pagesHandler := http.NewServeMux() @@ -74,16 +77,16 @@ func StartServer(staticFS embed.FS) error { pagesHandler.Handle("/muzikkx/", http.StripPrefix("/muzikkx", http.FileServer(http.Dir(config.Env().YouTube.MusicDir)))) pagesRouter := pages.NewPagesHandler(profileRepo, playlistsService, jwtUtil, &search.ScraperSearch{}, downloadService, historyService, songsService) - pagesHandler.HandleFunc("/", gHandler.OptionalAuthPage(pagesRouter.HandleHomePage)) - pagesHandler.HandleFunc("GET /signup", gHandler.AuthPage(pagesRouter.HandleSignupPage)) - pagesHandler.HandleFunc("GET /login", gHandler.AuthPage(pagesRouter.HandleLoginPage)) - pagesHandler.HandleFunc("GET /profile", gHandler.AuthPage(pagesRouter.HandleProfilePage)) - pagesHandler.HandleFunc("GET /about", gHandler.NoAuthPage(pagesRouter.HandleAboutPage)) - pagesHandler.HandleFunc("GET /playlists", gHandler.AuthPage(pagesRouter.HandlePlaylistsPage)) - pagesHandler.HandleFunc("GET /playlist/{playlist_id}", gHandler.AuthPage(pagesRouter.HandleSinglePlaylistPage)) - pagesHandler.HandleFunc("GET /song/{song_id}", gHandler.OptionalAuthPage(pagesRouter.HandleSingleSongPage)) - pagesHandler.HandleFunc("GET /privacy", gHandler.NoAuthPage(pagesRouter.HandlePrivacyPage)) - pagesHandler.HandleFunc("GET /search", gHandler.OptionalAuthPage(pagesRouter.HandleSearchResultsPage)) + pagesHandler.HandleFunc("/", contenttype.Html(authMw.OptionalAuthPage(pagesRouter.HandleHomePage))) + pagesHandler.HandleFunc("GET /signup", contenttype.Html(authMw.AuthPage(pagesRouter.HandleSignupPage))) + pagesHandler.HandleFunc("GET /login", contenttype.Html(authMw.AuthPage(pagesRouter.HandleLoginPage))) + pagesHandler.HandleFunc("GET /profile", contenttype.Html(authMw.AuthPage(pagesRouter.HandleProfilePage))) + pagesHandler.HandleFunc("GET /about", contenttype.Html(pagesRouter.HandleAboutPage)) + pagesHandler.HandleFunc("GET /playlists", contenttype.Html(authMw.AuthPage(pagesRouter.HandlePlaylistsPage))) + pagesHandler.HandleFunc("GET /playlist/{playlist_id}", contenttype.Html(authMw.AuthPage(pagesRouter.HandleSinglePlaylistPage))) + pagesHandler.HandleFunc("GET /song/{song_id}", contenttype.Html(authMw.OptionalAuthPage(pagesRouter.HandleSingleSongPage))) + pagesHandler.HandleFunc("GET /privacy", contenttype.Html(pagesRouter.HandlePrivacyPage)) + pagesHandler.HandleFunc("GET /search", contenttype.Html(authMw.OptionalAuthPage(pagesRouter.HandleSearchResultsPage))) ///////////// APIs ///////////// @@ -102,25 +105,25 @@ func StartServer(staticFS embed.FS) error { apisHandler.HandleFunc("/login/google/callback", googleLoginApi.HandleGoogleOAuthLoginCallback) apisHandler.HandleFunc("GET /logout", apis.HandleLogout) apisHandler.HandleFunc("GET /search-suggestion", apis.HandleSearchSuggestions) - apisHandler.HandleFunc("GET /song", gHandler.OptionalAuthApi(songApi.HandlePlaySong)) - apisHandler.HandleFunc("GET /song/single", gHandler.OptionalAuthApi(songApi.HandleGetSong)) - apisHandler.HandleFunc("PUT /song/playlist", gHandler.AuthApi(playlistsApi.HandleToggleSongInPlaylist)) - apisHandler.HandleFunc("PUT /song/playlist/plays", gHandler.AuthApi(songApi.HandleIncrementSongPlaysInPlaylist)) - apisHandler.HandleFunc("PUT /song/playlist/upvote", gHandler.AuthApi(songApi.HandleUpvoteSongPlaysInPlaylist)) - apisHandler.HandleFunc("PUT /song/playlist/downvote", gHandler.AuthApi(songApi.HandleDownvoteSongPlaysInPlaylist)) - apisHandler.HandleFunc("GET /playlist/all", gHandler.AuthApi(playlistsApi.HandleGetPlaylistsForPopover)) - apisHandler.HandleFunc("GET /playlist", gHandler.AuthApi(playlistsApi.HandleGetPlaylist)) - apisHandler.HandleFunc("POST /playlist", gHandler.AuthApi(playlistsApi.HandleCreatePlaylist)) - apisHandler.HandleFunc("PUT /playlist/public", gHandler.AuthApi(playlistsApi.HandleTogglePublicPlaylist)) - apisHandler.HandleFunc("PUT /playlist/join", gHandler.AuthApi(playlistsApi.HandleToggleJoinPlaylist)) - apisHandler.HandleFunc("DELETE /playlist", gHandler.AuthApi(playlistsApi.HandleDeletePlaylist)) - apisHandler.HandleFunc("GET /playlist/zip", gHandler.AuthApi(playlistsApi.HandleDonwnloadPlaylist)) - apisHandler.HandleFunc("GET /history/{page}", gHandler.AuthApi(historyApi.HandleGetMoreHistoryItems)) + apisHandler.HandleFunc("GET /song", authMw.OptionalAuthApi(songApi.HandlePlaySong)) + apisHandler.HandleFunc("GET /song/single", authMw.OptionalAuthApi(songApi.HandleGetSong)) + apisHandler.HandleFunc("PUT /song/playlist", authMw.AuthApi(playlistsApi.HandleToggleSongInPlaylist)) + apisHandler.HandleFunc("PUT /song/playlist/plays", authMw.AuthApi(songApi.HandleIncrementSongPlaysInPlaylist)) + apisHandler.HandleFunc("PUT /song/playlist/upvote", authMw.AuthApi(songApi.HandleUpvoteSongPlaysInPlaylist)) + apisHandler.HandleFunc("PUT /song/playlist/downvote", authMw.AuthApi(songApi.HandleDownvoteSongPlaysInPlaylist)) + apisHandler.HandleFunc("GET /playlist/all", authMw.AuthApi(playlistsApi.HandleGetPlaylistsForPopover)) + apisHandler.HandleFunc("GET /playlist", authMw.AuthApi(playlistsApi.HandleGetPlaylist)) + apisHandler.HandleFunc("POST /playlist", authMw.AuthApi(playlistsApi.HandleCreatePlaylist)) + apisHandler.HandleFunc("PUT /playlist/public", authMw.AuthApi(playlistsApi.HandleTogglePublicPlaylist)) + apisHandler.HandleFunc("PUT /playlist/join", authMw.AuthApi(playlistsApi.HandleToggleJoinPlaylist)) + apisHandler.HandleFunc("DELETE /playlist", authMw.AuthApi(playlistsApi.HandleDeletePlaylist)) + apisHandler.HandleFunc("GET /playlist/zip", authMw.AuthApi(playlistsApi.HandleDonwnloadPlaylist)) + apisHandler.HandleFunc("GET /history/{page}", authMw.AuthApi(historyApi.HandleGetMoreHistoryItems)) applicationHandler := http.NewServeMux() - applicationHandler.Handle("/", pagesHandler) - applicationHandler.Handle("/api/", http.StripPrefix("/api", apisHandler)) + applicationHandler.Handle("/", ismobile.Handler(theme.Handler(pagesHandler))) + applicationHandler.Handle("/api/", ismobile.Handler(theme.Handler(http.StripPrefix("/api", apisHandler)))) log.Info("Starting http server at port " + config.Env().Port) - return http.ListenAndServe(":"+config.Env().Port, m.Middleware(applicationHandler)) + return http.ListenAndServe(":"+config.Env().Port, ismobile.Handler(theme.Handler(m.Middleware(applicationHandler)))) } diff --git a/app/handlers/apis/email_login.go b/app/handlers/apis/email_login.go index 6d0bc64..f72e521 100644 --- a/app/handlers/apis/email_login.go +++ b/app/handlers/apis/email_login.go @@ -4,7 +4,7 @@ import ( "context" "dankmuzikk/config" "dankmuzikk/entities" - "dankmuzikk/handlers" + "dankmuzikk/handlers/middlewares/auth" "dankmuzikk/log" "dankmuzikk/services/login" "dankmuzikk/views/components/otp" @@ -50,7 +50,7 @@ func (e *emailLoginApi) HandleEmailLogin(w http.ResponseWriter, r *http.Request) } http.SetCookie(w, &http.Cookie{ - Name: handlers.VerificationTokenKey, + Name: auth.VerificationTokenKey, Value: verificationToken, HttpOnly: true, Path: "/api/verify-otp", @@ -87,7 +87,7 @@ func (e *emailLoginApi) HandleEmailSignup(w http.ResponseWriter, r *http.Request } http.SetCookie(w, &http.Cookie{ - Name: handlers.VerificationTokenKey, + Name: auth.VerificationTokenKey, Value: verificationToken, HttpOnly: true, Path: "/api/verify-otp", @@ -98,7 +98,7 @@ func (e *emailLoginApi) HandleEmailSignup(w http.ResponseWriter, r *http.Request } func (e *emailLoginApi) HandleEmailOTPVerification(w http.ResponseWriter, r *http.Request) { - verificationToken, err := r.Cookie(handlers.VerificationTokenKey) + verificationToken, err := r.Cookie(auth.VerificationTokenKey) if err != nil { // w.Write([]byte("Invalid verification token")) status. @@ -148,7 +148,7 @@ func (e *emailLoginApi) HandleEmailOTPVerification(w http.ResponseWriter, r *htt } http.SetCookie(w, &http.Cookie{ - Name: handlers.SessionTokenKey, + Name: auth.SessionTokenKey, Value: sessionToken, HttpOnly: true, Path: "/", diff --git a/app/handlers/apis/google_login.go b/app/handlers/apis/google_login.go index 2a9a07f..38dd83f 100644 --- a/app/handlers/apis/google_login.go +++ b/app/handlers/apis/google_login.go @@ -3,7 +3,7 @@ package apis import ( "context" "dankmuzikk/config" - "dankmuzikk/handlers" + "dankmuzikk/handlers/middlewares/auth" "dankmuzikk/log" "dankmuzikk/services/login" "dankmuzikk/views/components/status" @@ -57,7 +57,7 @@ func (g *googleLoginApi) HandleGoogleOAuthLoginCallback(w http.ResponseWriter, r } http.SetCookie(w, &http.Cookie{ - Name: handlers.SessionTokenKey, + Name: auth.SessionTokenKey, Value: sessionToken, HttpOnly: true, Path: "/", diff --git a/app/handlers/apis/history.go b/app/handlers/apis/history.go index ab8da0b..ff1f4d6 100644 --- a/app/handlers/apis/history.go +++ b/app/handlers/apis/history.go @@ -3,7 +3,7 @@ package apis import ( "bytes" "dankmuzikk/entities" - "dankmuzikk/handlers" + "dankmuzikk/handlers/middlewares/auth" "dankmuzikk/log" "dankmuzikk/services/history" "dankmuzikk/views/components/playlist" @@ -24,7 +24,7 @@ func NewHistoryApi(service *history.Service) *historyApi { } func (h *historyApi) HandleGetMoreHistoryItems(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.WriteHeader(http.StatusUnauthorized) return diff --git a/app/handlers/apis/logout.go b/app/handlers/apis/logout.go index 6e1f926..2841691 100644 --- a/app/handlers/apis/logout.go +++ b/app/handlers/apis/logout.go @@ -2,13 +2,13 @@ package apis import ( "dankmuzikk/config" - "dankmuzikk/handlers" + "dankmuzikk/handlers/middlewares/auth" "net/http" ) func HandleLogout(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{ - Name: handlers.SessionTokenKey, + Name: auth.SessionTokenKey, Value: "", Path: "/", Domain: config.Env().Hostname, diff --git a/app/handlers/apis/playlist.go b/app/handlers/apis/playlist.go index 879c98b..15c3eec 100644 --- a/app/handlers/apis/playlist.go +++ b/app/handlers/apis/playlist.go @@ -3,7 +3,7 @@ package apis import ( "context" "dankmuzikk/entities" - "dankmuzikk/handlers" + "dankmuzikk/handlers/middlewares/auth" "dankmuzikk/log" "dankmuzikk/services/playlists" "dankmuzikk/services/playlists/songs" @@ -26,7 +26,7 @@ func NewPlaylistApi(service *playlists.Service, songService *songs.Service) *pla } func (p *playlistApi) HandleCreatePlaylist(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -57,7 +57,7 @@ func (p *playlistApi) HandleCreatePlaylist(w http.ResponseWriter, r *http.Reques } func (p *playlistApi) HandleToggleSongInPlaylist(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -89,7 +89,7 @@ func (p *playlistApi) HandleToggleSongInPlaylist(w http.ResponseWriter, r *http. } func (p *playlistApi) HandleTogglePublicPlaylist(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -116,7 +116,7 @@ func (p *playlistApi) HandleTogglePublicPlaylist(w http.ResponseWriter, r *http. } func (p *playlistApi) HandleToggleJoinPlaylist(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -145,7 +145,7 @@ func (p *playlistApi) HandleToggleJoinPlaylist(w http.ResponseWriter, r *http.Re } func (p *playlistApi) HandleGetPlaylist(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -167,7 +167,7 @@ func (p *playlistApi) HandleGetPlaylist(w http.ResponseWriter, r *http.Request) } func (p *playlistApi) HandleDeletePlaylist(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -190,7 +190,7 @@ func (p *playlistApi) HandleDeletePlaylist(w http.ResponseWriter, r *http.Reques } func (p *playlistApi) HandleGetPlaylistsForPopover(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -214,7 +214,7 @@ func (p *playlistApi) HandleGetPlaylistsForPopover(w http.ResponseWriter, r *htt } func (p *playlistApi) HandleDonwnloadPlaylist(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("🤷‍♂️")) diff --git a/app/handlers/apis/songs.go b/app/handlers/apis/songs.go index 2fbb301..a9e1fc2 100644 --- a/app/handlers/apis/songs.go +++ b/app/handlers/apis/songs.go @@ -1,7 +1,7 @@ package apis import ( - "dankmuzikk/handlers" + "dankmuzikk/handlers/middlewares/auth" "dankmuzikk/log" "dankmuzikk/services/history" "dankmuzikk/services/playlists/songs" @@ -30,7 +30,7 @@ func NewDownloadHandler( } func (s *songDownloadHandler) HandleIncrementSongPlaysInPlaylist(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -55,7 +55,7 @@ func (s *songDownloadHandler) HandleIncrementSongPlaysInPlaylist(w http.Response } func (s *songDownloadHandler) HandleUpvoteSongPlaysInPlaylist(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -82,7 +82,7 @@ func (s *songDownloadHandler) HandleUpvoteSongPlaysInPlaylist(w http.ResponseWri } func (s *songDownloadHandler) HandleDownvoteSongPlaysInPlaylist(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { w.Write([]byte("🤷‍♂️")) return @@ -116,7 +116,7 @@ func (s *songDownloadHandler) HandlePlaySong(w http.ResponseWriter, r *http.Requ return } - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if profileIdCorrect { err := s.historyService.AddSongToHistory(id, profileId) if err != nil { diff --git a/app/handlers/handler.go b/app/handlers/handler.go deleted file mode 100644 index 15ff944..0000000 --- a/app/handlers/handler.go +++ /dev/null @@ -1,175 +0,0 @@ -package handlers - -import ( - "context" - "dankmuzikk/config" - "dankmuzikk/db" - "dankmuzikk/entities" - "dankmuzikk/models" - "dankmuzikk/services/jwt" - "net/http" - "slices" - "strings" -) - -var noAuthPaths = []string{"/login", "/signup"} - -// Handler is handler for pages and APIs, where it wraps the common stuff in one place. -type Handler struct { - profileRepo db.GORMDBGetter - jwtUtil jwt.Decoder[jwt.Json] -} - -// NewHandler returns a new AuthHandler instance. -// Using a GORMDBGetter because this is supposed to be a light fetch, -// Where BaseDB doesn't provide column selection yet :( -func NewHandler( - accountRepo db.GORMDBGetter, - jwtUtil jwt.Decoder[jwt.Json], -) *Handler { - return &Handler{accountRepo, jwtUtil} -} - -// OptionalAuthPage authenticates a page's handler optionally (without redirection). -func (a *Handler) OptionalAuthPage(h http.HandlerFunc) http.HandlerFunc { - return a.NoAuthPage(func(w http.ResponseWriter, r *http.Request) { - profile, err := a.authenticate(r) - if err != nil { - h(w, r) - return - } - ctx := context.WithValue(r.Context(), ProfileIdKey, profile.Id) - h(w, r.WithContext(ctx)) - }) -} - -// AuthPage authenticates a page's handler. -func (a *Handler) AuthPage(h http.HandlerFunc) http.HandlerFunc { - return a.NoAuthPage(func(w http.ResponseWriter, r *http.Request) { - htmxRedirect := IsNoLayoutPage(r) - profile, err := a.authenticate(r) - authed := err == nil - ctx := context.WithValue(r.Context(), ProfileIdKey, profile.Id) - - switch { - case authed && slices.Contains(noAuthPaths, r.URL.Path): - http.Redirect(w, r, config.Env().Hostname, http.StatusTemporaryRedirect) - case !authed && slices.Contains(noAuthPaths, r.URL.Path): - h(w, r.WithContext(ctx)) - case !authed && htmxRedirect: - w.Header().Set("HX-Redirect", "/login") - case !authed && !htmxRedirect: - http.Redirect(w, r, config.Env().Hostname+"/login", http.StatusTemporaryRedirect) - default: - h(w, r.WithContext(ctx)) - } - }) -} - -// NoAuthPage returns a page's handler after setting Content-Type to text/html, and some context values. -func (a *Handler) NoAuthPage(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - ctx := context.WithValue(r.Context(), ThemeKey, getTheme(r)) - ctx = context.WithValue(ctx, IsMobileKey, isMobile(r)) - h(w, r.WithContext(ctx)) - } -} - -// OptionalAuthApi authenticates a page's handler optionally (without 401). -func (a *Handler) OptionalAuthApi(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - profile, err := a.authenticate(r) - if err != nil { - h(w, r) - return - } - ctx := context.WithValue(r.Context(), ProfileIdKey, profile.Id) - ctx = context.WithValue(ctx, IsMobileKey, isMobile(r)) - h(w, r.WithContext(ctx)) - } -} - -// AuthApi authenticates an API's handler. -func (a *Handler) AuthApi(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - profile, err := a.authenticate(r) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - ctx := context.WithValue(r.Context(), ProfileIdKey, profile.Id) - ctx = context.WithValue(ctx, IsMobileKey, isMobile(r)) - h(w, r.WithContext(ctx)) - } -} - -// NoAuthApi returns a page's handler after setting Content-Type to application/json. -func (a *Handler) NoAuthApi(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - h(w, r.WithContext(context.WithValue(r.Context(), IsMobileKey, isMobile(r)))) - } -} - -func (a *Handler) authenticate(r *http.Request) (entities.Profile, error) { - sessionToken, err := r.Cookie(SessionTokenKey) - if err != nil { - return entities.Profile{}, err - } - theThing, err := a.jwtUtil.Decode(sessionToken.Value, jwt.SessionToken) - if err != nil { - return entities.Profile{}, err - } - username, validUsername := theThing.Payload["username"].(string) - if !validUsername || username == "" { - return entities.Profile{}, err - } - - var profile models.Profile - - err = a. - profileRepo. - GetDB(). - Model(&profile). - Select("id"). - Where("username = ?", username). - First(&profile). - Error - - if err != nil { - return entities.Profile{}, err - } - - return entities.Profile{ - Id: profile.Id, - Name: profile.Name, - }, nil -} - -func isMobile(r *http.Request) bool { - return strings.Contains(strings.ToLower(r.Header.Get("User-Agent")), "mobile") -} - -func getTheme(r *http.Request) string { - themeCookie, err := r.Cookie(ThemeName) - if err != nil || themeCookie == nil || themeCookie.Value == "" { - return "black" - } - switch themeCookie.Value { - case "dank": - return "dank" - case "white": - return "white" - case "black": - fallthrough - default: - return "black" - } -} - -// IsNoLayoutPage checks if the requested page requires a no reload or not. -func IsNoLayoutPage(r *http.Request) bool { - noReload, exists := r.URL.Query()["no_layout"] - return exists && noReload[0] == "true" -} diff --git a/app/handlers/keys.go b/app/handlers/keys.go deleted file mode 100644 index affff75..0000000 --- a/app/handlers/keys.go +++ /dev/null @@ -1,16 +0,0 @@ -package handlers - -// Cookie keys -const ( - VerificationTokenKey = "verification-token" - SessionTokenKey = "token" - ThemeName = "theme-name" -) - -// Context keys -const ( - ProfileIdKey = "profile-id" - ThemeKey = "theme-name" - IsMobileKey = "is-mobile" - PlaylistPermission = "playlist-permission" -) diff --git a/app/handlers/pages/pages.go b/app/handlers/pages/pages.go index 825235b..267128f 100644 --- a/app/handlers/pages/pages.go +++ b/app/handlers/pages/pages.go @@ -5,7 +5,8 @@ import ( "dankmuzikk/config" "dankmuzikk/db" "dankmuzikk/entities" - "dankmuzikk/handlers" + "dankmuzikk/handlers/middlewares/auth" + "dankmuzikk/handlers/middlewares/contenttype" "dankmuzikk/log" "dankmuzikk/models" "dankmuzikk/services/history" @@ -56,7 +57,7 @@ func NewPagesHandler( func (p *pagesHandler) HandleHomePage(w http.ResponseWriter, r *http.Request) { var recentPlays []entities.Song var err error - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if profileIdCorrect { recentPlays, err = p.historyService.Get(profileId, 1) if err != nil { @@ -64,7 +65,7 @@ func (p *pagesHandler) HandleHomePage(w http.ResponseWriter, r *http.Request) { } } - if handlers.IsNoLayoutPage(r) { + if contenttype.IsNoLayoutPage(r) { w.Header().Set("HX-Title", "Home") w.Header().Set("HX-Push-Url", "/") pages.Index(recentPlays).Render(r.Context(), w) @@ -74,7 +75,7 @@ func (p *pagesHandler) HandleHomePage(w http.ResponseWriter, r *http.Request) { } func (p *pagesHandler) HandleAboutPage(w http.ResponseWriter, r *http.Request) { - if handlers.IsNoLayoutPage(r) { + if contenttype.IsNoLayoutPage(r) { w.Header().Set("HX-Title", "About") w.Header().Set("HX-Push-Url", "/about") pages.About().Render(r.Context(), w) @@ -88,7 +89,7 @@ func (p *pagesHandler) HandleLoginPage(w http.ResponseWriter, r *http.Request) { } func (p *pagesHandler) HandlePlaylistsPage(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { status. BugsBunnyError("I'm not sure what you're trying to do :)"). @@ -101,7 +102,7 @@ func (p *pagesHandler) HandlePlaylistsPage(w http.ResponseWriter, r *http.Reques playlists = make([]entities.Playlist, 0) } - if handlers.IsNoLayoutPage(r) { + if contenttype.IsNoLayoutPage(r) { w.Header().Set("HX-Title", "Playlists") w.Header().Set("HX-Push-Url", "/playlists") pages.Playlists(playlists).Render(r.Context(), w) @@ -111,7 +112,7 @@ func (p *pagesHandler) HandlePlaylistsPage(w http.ResponseWriter, r *http.Reques } func (p *pagesHandler) HandleSinglePlaylistPage(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { status. BugsBunnyError("I'm not sure what you're trying to do :)"). @@ -128,7 +129,7 @@ func (p *pagesHandler) HandleSinglePlaylistPage(w http.ResponseWriter, r *http.R } playlist, permission, err := p.playlistsService.Get(playlistPubId, profileId) - htmxReq := handlers.IsNoLayoutPage(r) + htmxReq := contenttype.IsNoLayoutPage(r) switch { case errors.Is(err, playlists.ErrUnauthorizedToSeePlaylist): log.Errorln(err) @@ -155,9 +156,9 @@ func (p *pagesHandler) HandleSinglePlaylistPage(w http.ResponseWriter, r *http.R Render(r.Context(), w) } } - ctx := context.WithValue(r.Context(), handlers.PlaylistPermission, permission) + ctx := context.WithValue(r.Context(), auth.PlaylistPermission, permission) - if handlers.IsNoLayoutPage(r) { + if contenttype.IsNoLayoutPage(r) { w.Header().Set("HX-Title", playlist.Title) w.Header().Set("HX-Push-Url", "/playlist/"+playlist.PublicId) pages.Playlist(playlist).Render(ctx, w) @@ -183,7 +184,7 @@ func (p *pagesHandler) HandleSingleSongPage(w http.ResponseWriter, r *http.Reque return } - if handlers.IsNoLayoutPage(r) { + if contenttype.IsNoLayoutPage(r) { w.Header().Set("HX-Title", song.Title) w.Header().Set("HX-Push-Url", "/song/"+song.YtId) pages.Song(song).Render(r.Context(), w) @@ -197,9 +198,9 @@ func (p *pagesHandler) HandlePrivacyPage(w http.ResponseWriter, r *http.Request) } func (p *pagesHandler) HandleProfilePage(w http.ResponseWriter, r *http.Request) { - profileId, profileIdCorrect := r.Context().Value(handlers.ProfileIdKey).(uint) + profileId, profileIdCorrect := r.Context().Value(auth.ProfileIdKey).(uint) if !profileIdCorrect { - if handlers.IsNoLayoutPage(r) { + if contenttype.IsNoLayoutPage(r) { w.Header().Set("HX-Redirect", "/") } else { http.Redirect(w, r, config.Env().Hostname, http.StatusTemporaryRedirect) @@ -213,7 +214,7 @@ func (p *pagesHandler) HandleProfilePage(w http.ResponseWriter, r *http.Request) PfpLink: dbProfile.PfpLink, Username: dbProfile.Username, } - if handlers.IsNoLayoutPage(r) { + if contenttype.IsNoLayoutPage(r) { w.Header().Set("HX-Title", "Profile") w.Header().Set("HX-Push-Url", "/profile") pages.Profile(profile).Render(r.Context(), w) @@ -238,7 +239,7 @@ func (p *pagesHandler) HandleSearchResultsPage(w http.ResponseWriter, r *http.Re _ = p.downloadService.DownloadYoutubeSongsMetadata(results) } - if handlers.IsNoLayoutPage(r) { + if contenttype.IsNoLayoutPage(r) { w.Header().Set("HX-Title", "Results for "+query) w.Header().Set("HX-Push-Url", "/search?query="+query) pages.SearchResults(results).Render(r.Context(), w)