From 1f1fd908907fd633ea42ee74c6058cdd704214c9 Mon Sep 17 00:00:00 2001 From: domrichardson <100129001+domrichardson@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:11:39 +0000 Subject: [PATCH] feat: Updated admin panel styles --- backend/cmd/server/main.go | 5 + .../application/services/admin_service.go | 114 ++++++ .../interfaces/handlers/admin_handler.go | 60 +++ frontend/src/components/AdminGroupModal.vue | 125 ++++++ frontend/src/components/AdminSpaceModal.vue | 33 +- frontend/src/components/AdminUserModal.vue | 111 ++++++ frontend/src/pages/Admin.vue | 364 ++++++++++++------ frontend/src/utils/markdown.js | 19 +- 8 files changed, 700 insertions(+), 131 deletions(-) create mode 100644 frontend/src/components/AdminGroupModal.vue create mode 100644 frontend/src/components/AdminUserModal.vue diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index b355431..ea97bd7 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -117,6 +117,8 @@ func main() { adminService := services.NewAdminService( db.UserRepo, db.GroupRepo, + db.ProviderRepo, + db.LinkRepo, db.SpaceRepo, db.MembershipRepo, db.NoteRepo, @@ -255,10 +257,12 @@ func main() { }) }) admin.HandleFunc("/users", adminHandler.ListUsers).Methods("GET") + admin.HandleFunc("/users/{userId}", adminHandler.DeleteUser).Methods("DELETE") admin.HandleFunc("/users/{userId}/groups", adminHandler.UpdateUserGroups).Methods("PUT") admin.HandleFunc("/groups", adminHandler.ListGroups).Methods("GET") admin.HandleFunc("/groups", adminHandler.CreateGroup).Methods("POST") admin.HandleFunc("/groups/{groupId}", adminHandler.UpdateGroup).Methods("PUT") + admin.HandleFunc("/groups/{groupId}", adminHandler.DeleteGroup).Methods("DELETE") admin.HandleFunc("/spaces", adminHandler.ListAllSpaces).Methods("GET") admin.HandleFunc("/spaces/{spaceId}", adminHandler.UpdateSpace).Methods("PUT") admin.HandleFunc("/spaces/{spaceId}", adminHandler.DeleteSpace).Methods("DELETE") @@ -270,6 +274,7 @@ func main() { admin.HandleFunc("/feature-flags", adminHandler.UpdateFeatureFlags).Methods("PUT") // manage identity providers — admin-only admin.HandleFunc("/auth/providers", authHandler.CreateProvider).Methods("POST") + admin.HandleFunc("/auth/providers/{providerId}", adminHandler.DeleteProvider).Methods("DELETE") // Serve static files (frontend) for all other routes // This must be after all API route handlers to allow API routes to take precedence diff --git a/backend/internal/application/services/admin_service.go b/backend/internal/application/services/admin_service.go index c78b713..dabbd02 100644 --- a/backend/internal/application/services/admin_service.go +++ b/backend/internal/application/services/admin_service.go @@ -17,6 +17,8 @@ import ( type AdminService struct { userRepo repositories.UserRepository groupRepo repositories.GroupRepository + providerRepo repositories.AuthProviderRepository + linkRepo repositories.UserProviderLinkRepository spaceRepo repositories.SpaceRepository membershipRepo repositories.MembershipRepository noteRepo repositories.NoteRepository @@ -30,6 +32,8 @@ type AdminService struct { func NewAdminService( userRepo repositories.UserRepository, groupRepo repositories.GroupRepository, + providerRepo repositories.AuthProviderRepository, + linkRepo repositories.UserProviderLinkRepository, spaceRepo repositories.SpaceRepository, membershipRepo repositories.MembershipRepository, noteRepo repositories.NoteRepository, @@ -41,6 +45,8 @@ func NewAdminService( return &AdminService{ userRepo: userRepo, groupRepo: groupRepo, + providerRepo: providerRepo, + linkRepo: linkRepo, spaceRepo: spaceRepo, membershipRepo: membershipRepo, noteRepo: noteRepo, @@ -51,6 +57,114 @@ func NewAdminService( } } +// DeleteUser deletes a user and related memberships/provider links. +func (s *AdminService) DeleteUser(ctx context.Context, currentUserID, targetUserID bson.ObjectID) error { + if currentUserID == targetUserID { + return errors.New("you cannot delete your own account") + } + + spaces, err := s.spaceRepo.GetAllSpaces(ctx) + if err != nil { + return err + } + for _, space := range spaces { + if space.OwnerID == targetUserID { + return errors.New("cannot delete user that owns spaces; transfer or delete spaces first") + } + } + + memberships, err := s.membershipRepo.GetUserMemberships(ctx, targetUserID) + if err == nil { + for _, membership := range memberships { + if err := s.membershipRepo.DeleteMembership(ctx, membership.ID); err != nil { + return err + } + } + } + + if s.linkRepo != nil { + links, err := s.linkRepo.GetUserLinks(ctx, targetUserID) + if err == nil { + for _, link := range links { + if err := s.linkRepo.DeleteLink(ctx, link.ID); err != nil { + return err + } + } + } + } + + return s.userRepo.DeleteUser(ctx, targetUserID) +} + +// DeleteGroup deletes a non-system group and removes it from users. +func (s *AdminService) DeleteGroup(ctx context.Context, groupID bson.ObjectID) error { + group, err := s.groupRepo.GetGroupByID(ctx, groupID) + if err != nil { + return err + } + if group.IsSystem { + return errors.New("system groups cannot be deleted") + } + + users, err := s.userRepo.ListAllUsers(ctx) + if err != nil { + return err + } + for _, user := range users { + filtered := make([]bson.ObjectID, 0, len(user.GroupIDs)) + changed := false + for _, assignedGroupID := range user.GroupIDs { + if assignedGroupID == groupID { + changed = true + continue + } + filtered = append(filtered, assignedGroupID) + } + if !changed { + continue + } + user.GroupIDs = filtered + if err := s.userRepo.UpdateUser(ctx, user); err != nil { + return err + } + } + + if err := s.groupRepo.DeleteGroup(ctx, groupID); err != nil { + return err + } + + return s.refreshAllUserPermissions(ctx) +} + +// DeleteProvider deletes an auth provider and all user-provider links connected to it. +func (s *AdminService) DeleteProvider(ctx context.Context, providerID bson.ObjectID) error { + if s.providerRepo == nil { + return errors.New("provider repository unavailable") + } + + if s.linkRepo != nil { + users, err := s.userRepo.ListAllUsers(ctx) + if err != nil { + return err + } + for _, user := range users { + links, err := s.linkRepo.GetUserLinks(ctx, user.ID) + if err != nil { + continue + } + for _, link := range links { + if link.ProviderID == providerID { + if err := s.linkRepo.DeleteLink(ctx, link.ID); err != nil { + return err + } + } + } + } + } + + return s.providerRepo.DeleteProvider(ctx, providerID) +} + // ListUsers returns all users as admin DTOs func (s *AdminService) ListUsers(ctx context.Context) ([]*dto.AdminUserDTO, error) { users, err := s.userRepo.ListAllUsers(ctx) diff --git a/backend/internal/interfaces/handlers/admin_handler.go b/backend/internal/interfaces/handlers/admin_handler.go index e0a990e..1f8e986 100644 --- a/backend/internal/interfaces/handlers/admin_handler.go +++ b/backend/internal/interfaces/handlers/admin_handler.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/noteapp/backend/internal/interfaces/middleware" "go.mongodb.org/mongo-driver/v2/bson" "github.com/noteapp/backend/internal/application/dto" @@ -32,6 +33,33 @@ func (h *AdminHandler) ListUsers(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]interface{}{"users": users}) } +// DeleteUser handles DELETE /admin/users/{userId} +func (h *AdminHandler) DeleteUser(w http.ResponseWriter, r *http.Request) { + targetUserID, err := bson.ObjectIDFromHex(mux.Vars(r)["userId"]) + if err != nil { + http.Error(w, "invalid user id", http.StatusBadRequest) + return + } + + currentUserIDHex, err := middleware.GetUserIDFromContext(r.Context()) + if err != nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + currentUserID, err := bson.ObjectIDFromHex(currentUserIDHex) + if err != nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + if err := h.adminService.DeleteUser(r.Context(), currentUserID, targetUserID); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNoContent) +} + // UpdateUserGroups handles PUT /admin/users/{userId}/groups func (h *AdminHandler) UpdateUserGroups(w http.ResponseWriter, r *http.Request) { userID, err := bson.ObjectIDFromHex(mux.Vars(r)["userId"]) @@ -66,6 +94,22 @@ func (h *AdminHandler) UpdateUserGroups(w http.ResponseWriter, r *http.Request) json.NewEncoder(w).Encode(user) } +// DeleteGroup handles DELETE /admin/groups/{groupId} +func (h *AdminHandler) DeleteGroup(w http.ResponseWriter, r *http.Request) { + groupID, err := bson.ObjectIDFromHex(mux.Vars(r)["groupId"]) + if err != nil { + http.Error(w, "invalid group id", http.StatusBadRequest) + return + } + + if err := h.adminService.DeleteGroup(r.Context(), groupID); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNoContent) +} + // ListGroups handles GET /admin/groups func (h *AdminHandler) ListGroups(w http.ResponseWriter, r *http.Request) { groups, err := h.adminService.ListGroups(r.Context()) @@ -292,3 +336,19 @@ func (h *AdminHandler) UpdateFeatureFlags(w http.ResponseWriter, r *http.Request w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(flags) } + +// DeleteProvider handles DELETE /admin/auth/providers/{providerId} +func (h *AdminHandler) DeleteProvider(w http.ResponseWriter, r *http.Request) { + providerID, err := bson.ObjectIDFromHex(mux.Vars(r)["providerId"]) + if err != nil { + http.Error(w, "invalid provider id", http.StatusBadRequest) + return + } + + if err := h.adminService.DeleteProvider(r.Context(), providerID); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/frontend/src/components/AdminGroupModal.vue b/frontend/src/components/AdminGroupModal.vue new file mode 100644 index 0000000..557b658 --- /dev/null +++ b/frontend/src/components/AdminGroupModal.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/frontend/src/components/AdminSpaceModal.vue b/frontend/src/components/AdminSpaceModal.vue index 2ff9820..d8864f7 100644 --- a/frontend/src/components/AdminSpaceModal.vue +++ b/frontend/src/components/AdminSpaceModal.vue @@ -1,7 +1,7 @@ @@ -252,3 +252,30 @@ const deleteSpace = async () => { } }; + + diff --git a/frontend/src/components/AdminUserModal.vue b/frontend/src/components/AdminUserModal.vue new file mode 100644 index 0000000..2da7d8c --- /dev/null +++ b/frontend/src/components/AdminUserModal.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/frontend/src/pages/Admin.vue b/frontend/src/pages/Admin.vue index 9200c81..a53f5ab 100644 --- a/frontend/src/pages/Admin.vue +++ b/frontend/src/pages/Admin.vue @@ -38,48 +38,39 @@
Loading users...
No users found.
-
- - - - - - - - - - - - - - - - - - - -
UsernameEmailGroupsStatusJoined
{{ u.username }}{{ u.email }} - -
Ctrl/Cmd+Click for multiple groups
-
- - {{ u.is_active ? "Active" : "Inactive" }} - - {{ formatDate(u.created_at) }}
+
+
+
+
+ {{ u.username }} + + {{ u.is_active ? "Active" : "Inactive" }} + +
+ +
+
+
Email
+
{{ u.email }}
+
+
+
Joined
+
{{ formatDate(u.created_at) }}
+
+
+
Groups
+
{{ getUserGroupSummary(u) }}
+
+
+
+ +
+ +
+
@@ -106,7 +97,10 @@
{{ group.description || "No description" }}
{{ (group.permissions || []).length }} permission{{ (group.permissions || []).length === 1 ? "" : "s" }}
- +
+ + +
@@ -154,9 +148,12 @@
{{ provider.type.toUpperCase() }} · {{ provider.scopes.join(", ") }}
Callback: {{ buildCallbackUrl(provider.id) }}
- - {{ provider.is_active ? "Active" : "Disabled" }} - +
+ + {{ provider.is_active ? "Active" : "Disabled" }} + + +
@@ -309,49 +306,17 @@ - - - - +