diff --git a/api/admin/handler.go b/api/admin/handler.go deleted file mode 100644 index d987796..0000000 --- a/api/admin/handler.go +++ /dev/null @@ -1,229 +0,0 @@ -package admin - -import ( - "database/sql" - "html" - "log" - "net/http" - "strings" - - "golang.org/x/crypto/bcrypt" - - "mal/api/auth" - "mal/internal/db" - "mal/internal/middleware" - webcontext "mal/web/context" - "mal/web/templates" -) - -type Handler struct { - db database.Querier - authService *auth.Service -} - -type contextKey string - -func NewHandler(db database.Querier, authService *auth.Service) *Handler { - return &Handler{db: db, authService: authService} -} - -func (h *Handler) HandleAdminPage(w http.ResponseWriter, r *http.Request) { - users, err := h.db.ListUsers(r.Context()) - if err != nil { - log.Printf("list users error: %v", err) - http.Error(w, "Failed to load users", http.StatusInternalServerError) - return - } - - if err := templates.AdminPage(users).Render(r.Context(), w); err != nil { - log.Printf("render error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - } -} - -func (h *Handler) HandleAddUserForm(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - username := r.FormValue("username") - password := r.FormValue("password") - - if username == "" || password == "" { - writeInlineError(w, "Username and password are required") - return - } - - hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) - if err != nil { - log.Printf("bcrypt error: %v", err) - writeInlineError(w, "Failed to create user") - return - } - - _, err = h.db.CreateUser(r.Context(), database.CreateUserParams{ - ID: generateUserID(), - Username: username, - PasswordHash: string(hash), - }) - if err != nil { - log.Printf("create user error: %v", err) - writeInlineError(w, "Failed to create user (may already exist)") - return - } - - users, err := h.db.ListUsers(r.Context()) - if err != nil { - log.Printf("list users error: %v", err) - writeInlineError(w, "User created but failed to refresh list") - return - } - - if err := templates.AdminUsersList(users).Render(r.Context(), w); err != nil { - log.Printf("render error: %v", err) - writeInlineError(w, "User created but failed to render list") - } -} - -func (h *Handler) HandleDeleteUserRouter(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - parts := strings.Split(strings.TrimSuffix(path, "/delete"), "/") - userID := parts[len(parts)-1] - - if userID == "" { - writeInlineError(w, "Invalid user ID") - return - } - - currentUser, ok := r.Context().Value(webcontext.UserKey).(*database.User) - if !ok || currentUser == nil { - writeInlineError(w, "Not authenticated") - return - } - if userID == currentUser.ID { - writeInlineError(w, "Cannot delete your own account") - return - } - - err := h.db.DeleteUser(r.Context(), userID) - if err != nil { - log.Printf("delete user error: %v", err) - writeInlineError(w, "Failed to delete user") - return - } - - users, err := h.db.ListUsers(r.Context()) - if err != nil { - log.Printf("list users error: %v", err) - writeInlineError(w, "User deleted but failed to refresh list") - return - } - - if err := templates.AdminUsersList(users).Render(r.Context(), w); err != nil { - log.Printf("render error: %v", err) - writeInlineError(w, "User deleted but failed to render list") - } -} - -func (h *Handler) HandleUserRouter(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - switch { - case strings.HasSuffix(path, "/delete"): - h.HandleDeleteUserRouter(w, r) - case strings.HasSuffix(path, "/watchlist"): - h.HandleUserWatchlist(w, r) - case strings.HasSuffix(path, "/continue-watching"): - h.HandleUserContinueWatching(w, r) - default: - h.HandleImpersonateUser(w, r) - } -} - -func (h *Handler) HandleImpersonateUser(w http.ResponseWriter, r *http.Request) { - userID := r.URL.Path[len("/admin/users/"):] - if userID == "" { - http.Error(w, "Invalid user ID", http.StatusBadRequest) - return - } - - targetUser, err := h.db.GetUser(r.Context(), userID) - if err != nil { - if err == sql.ErrNoRows { - http.Error(w, "User not found", http.StatusNotFound) - return - } - log.Printf("get user error: %v", err) - http.Error(w, "Failed to load user", http.StatusInternalServerError) - return - } - - if err := templates.AdminImpersonatePage(targetUser).Render(r.Context(), w); err != nil { - log.Printf("render error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - } -} - -func (h *Handler) HandleUserWatchlist(w http.ResponseWriter, r *http.Request) { - userID := r.URL.Path[len("/admin/users/"):] - userID = userID[:len(userID)-len("/watchlist")] - - entries, err := h.db.GetUserWatchList(r.Context(), userID) - if err != nil { - log.Printf("get user watchlist error: %v", err) - http.Error(w, "Failed to load watchlist", http.StatusInternalServerError) - return - } - - if err := templates.AdminUserWatchlist(entries).Render(r.Context(), w); err != nil { - log.Printf("render error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - } -} - -func (h *Handler) HandleUserContinueWatching(w http.ResponseWriter, r *http.Request) { - userID := r.URL.Path[len("/admin/users/"):] - userID = userID[:len(userID)-len("/continue-watching")] - - entries, err := h.db.GetContinueWatchingEntries(r.Context(), userID) - if err != nil { - log.Printf("get continue watching error: %v", err) - http.Error(w, "Failed to load continue watching", http.StatusInternalServerError) - return - } - - if err := templates.AdminUserContinueWatching(entries).Render(r.Context(), w); err != nil { - log.Printf("render error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - } -} - -func writeInlineError(w http.ResponseWriter, message string) { - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(`

` + html.EscapeString(message) + `

`)) -} - -func generateUserID() string { - b := make([]byte, 16) - for i := range b { - b[i] = byte('a' + (i % 26)) - } - return string(b) -} - -const bcryptCost = 12 - -func GetImpersonatedUserID(r *http.Request) string { - impersonateID := r.URL.Query().Get("as_user") - if impersonateID == "" { - return "" - } - - user, ok := r.Context().Value(webcontext.UserKey).(*database.User) - if !ok || user == nil || !middleware.IsAdmin(user) { - return "" - } - - return impersonateID -} diff --git a/internal/middleware/admin.go b/internal/middleware/admin.go deleted file mode 100644 index 6556f54..0000000 --- a/internal/middleware/admin.go +++ /dev/null @@ -1,30 +0,0 @@ -package middleware - -import ( - "net/http" - - "mal/internal/db" - webcontext "mal/web/context" - "mal/web/shared/admin" -) - -func IsAdmin(user *database.User) bool { - return admin.IsAdmin(user) -} - -func RequireAdmin(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user, ok := r.Context().Value(webcontext.UserKey).(*database.User) - if !ok || user == nil { - http.Redirect(w, r, "/login", http.StatusFound) - return - } - - if !admin.IsAdmin(user) { - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - - next.ServeHTTP(w, r) - }) -} diff --git a/web/shared/admin/admin.go b/web/shared/admin/admin.go deleted file mode 100644 index 0d65df3..0000000 --- a/web/shared/admin/admin.go +++ /dev/null @@ -1,22 +0,0 @@ -package admin - -import ( - "mal/internal/db" -) - -const AdminEmail = "mikkelelvers@outlook.com" - -func IsAdmin(user *database.User) bool { - if user == nil { - return false - } - return user.Username == AdminEmail -} - -func IsAdminFromContext(ctx interface { - Value(key interface{}) interface{} -}) bool { - const userKey = "mal:user" - user, _ := ctx.Value(userKey).(*database.User) - return IsAdmin(user) -} diff --git a/web/shared/layout/layout.templ b/web/shared/layout/layout.templ index bc9cbbf..ac7da24 100644 --- a/web/shared/layout/layout.templ +++ b/web/shared/layout/layout.templ @@ -2,7 +2,6 @@ package layout import ( "mal/web/components/icons" - "mal/web/shared/admin" ) templ Layout(title string, showHeader bool) { @@ -63,14 +62,6 @@ templ Layout(title string, showHeader bool) { > Watchlist - if admin.IsAdminFromContext(ctx) { - - Admin - - }
-

Admin Panel

- - -
-

Add New User

-
- - - -
-
- - -
-

Users

- @AdminUsersList(users) -
-
- } -} - -templ AdminUsersList(users []database.User) { -
- for _, user := range users { -
-
-
{ user.Username }
-
ID: { user.ID }
-
Created: { user.CreatedAt.Format("2006-01-02") }
-
-
- - View - - -
-
- } -
-} - -templ AdminImpersonatePage(user database.User) { - @layout.Layout("mal - admin - user view", true) { -
-
-
-

Viewing User

-

{ user.Username }

-
- - ← Back to Admin - -
- -
- - -

Watchlist

-

View user's anime watchlist

-
- - - -

Continue Watching

-

View user's watch progress

-
-
- -
- Read-only mode: You are viewing this user's data. Changes cannot be made from this view. -
-
- } -} - -templ AdminUserWatchlist(entries []database.GetUserWatchListRow) { - @layout.Layout("mal - admin - watchlist", true) { -
-
-

User Watchlist

- - ← Back to Admin - -
- - if len(entries) == 0 { -
- No watchlist entries -
- } else { -
- for _, entry := range entries { -
-
- if entry.ImageUrl != "" { - { - } else { -
No image
- } -
-
{ entry.TitleOriginal }
-
Status: { entry.Status }
- if entry.CurrentEpisode.Valid { -
Episode: { fmt.Sprintf("%d", entry.CurrentEpisode.Int64) }
- } -
- } -
- } - -
- Read-only mode: You are viewing this user's data. Changes cannot be made from this view. -
-
- } -} - -templ AdminUserContinueWatching(entries []database.GetContinueWatchingEntriesRow) { - @layout.Layout("mal - admin - continue watching", true) { -
-
-

Continue Watching

- - ← Back to Admin - -
- - if len(entries) == 0 { -
- No continue watching entries -
- } else { -
- for _, entry := range entries { -
-
- if entry.ImageUrl != "" { - { - } else { -
No image
- } -
-
{ entry.TitleOriginal }
- if entry.CurrentEpisode.Valid { -
Episode: { fmt.Sprintf("%d", entry.CurrentEpisode.Int64) }
- } - if entry.CurrentTimeSeconds > 0 { -
Time: { fmt.Sprintf("%.0fs", entry.CurrentTimeSeconds) }
- } -
- } -
- } - -
- Read-only mode: You are viewing this user's data. Changes cannot be made from this view. -
-
- } -}