mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-16 02:03:05 +03:00
feat: display groups on the account page (#296)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
@@ -27,9 +27,12 @@ func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.Jwt
|
|||||||
group.GET("/users/:id", jwtAuthMiddleware.Add(true), uc.getUserHandler)
|
group.GET("/users/:id", jwtAuthMiddleware.Add(true), uc.getUserHandler)
|
||||||
group.POST("/users", jwtAuthMiddleware.Add(true), uc.createUserHandler)
|
group.POST("/users", jwtAuthMiddleware.Add(true), uc.createUserHandler)
|
||||||
group.PUT("/users/:id", jwtAuthMiddleware.Add(true), uc.updateUserHandler)
|
group.PUT("/users/:id", jwtAuthMiddleware.Add(true), uc.updateUserHandler)
|
||||||
|
group.GET("/users/:id/groups", jwtAuthMiddleware.Add(true), uc.getUserGroupsHandler)
|
||||||
group.PUT("/users/me", jwtAuthMiddleware.Add(false), uc.updateCurrentUserHandler)
|
group.PUT("/users/me", jwtAuthMiddleware.Add(false), uc.updateCurrentUserHandler)
|
||||||
group.DELETE("/users/:id", jwtAuthMiddleware.Add(true), uc.deleteUserHandler)
|
group.DELETE("/users/:id", jwtAuthMiddleware.Add(true), uc.deleteUserHandler)
|
||||||
|
|
||||||
|
group.PUT("/users/:id/user-groups", jwtAuthMiddleware.Add(true), uc.updateUserGroups)
|
||||||
|
|
||||||
group.GET("/users/:id/profile-picture.png", uc.getUserProfilePictureHandler)
|
group.GET("/users/:id/profile-picture.png", uc.getUserProfilePictureHandler)
|
||||||
group.GET("/users/me/profile-picture.png", jwtAuthMiddleware.Add(false), uc.getCurrentUserProfilePictureHandler)
|
group.GET("/users/me/profile-picture.png", jwtAuthMiddleware.Add(false), uc.getCurrentUserProfilePictureHandler)
|
||||||
group.PUT("/users/:id/profile-picture", jwtAuthMiddleware.Add(true), uc.updateUserProfilePictureHandler)
|
group.PUT("/users/:id/profile-picture", jwtAuthMiddleware.Add(true), uc.updateUserProfilePictureHandler)
|
||||||
@@ -46,6 +49,23 @@ type UserController struct {
|
|||||||
appConfigService *service.AppConfigService
|
appConfigService *service.AppConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
|
||||||
|
userID := c.Param("id")
|
||||||
|
groups, err := uc.userService.GetUserGroups(userID)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupsDto []dto.UserGroupDtoWithUsers
|
||||||
|
if err := dto.MapStructList(groups, &groupsDto); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, groupsDto)
|
||||||
|
}
|
||||||
|
|
||||||
func (uc *UserController) listUsersHandler(c *gin.Context) {
|
func (uc *UserController) listUsersHandler(c *gin.Context) {
|
||||||
searchTerm := c.Query("search")
|
searchTerm := c.Query("search")
|
||||||
var sortedPaginationRequest utils.SortedPaginationRequest
|
var sortedPaginationRequest utils.SortedPaginationRequest
|
||||||
@@ -315,3 +335,25 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, userDto)
|
c.JSON(http.StatusOK, userDto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (uc *UserController) updateUserGroups(c *gin.Context) {
|
||||||
|
var input dto.UserUpdateUserGroupDto
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := uc.userService.UpdateUserGroups(c.Param("id"), input.UserGroupIds)
|
||||||
|
if err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var userDto dto.UserDto
|
||||||
|
if err := dto.MapStruct(user, &userDto); err != nil {
|
||||||
|
c.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, userDto)
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ func (ugc *UserGroupController) updateUsers(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
group, err := ugc.UserGroupService.UpdateUsers(c.Param("id"), input)
|
group, err := ugc.UserGroupService.UpdateUsers(c.Param("id"), input.UserIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Error(err)
|
c.Error(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type UserDto struct {
|
|||||||
LastName string `json:"lastName"`
|
LastName string `json:"lastName"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
|
UserGroups []UserGroupDto `json:"userGroups"`
|
||||||
LdapID *string `json:"ldapId"`
|
LdapID *string `json:"ldapId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,3 +32,7 @@ type OneTimeAccessEmailDto struct {
|
|||||||
Email string `json:"email" binding:"required,email"`
|
Email string `json:"email" binding:"required,email"`
|
||||||
RedirectPath string `json:"redirectPath"`
|
RedirectPath string `json:"redirectPath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserUpdateUserGroupDto struct {
|
||||||
|
UserGroupIds []string `json:"userGroupIds" binding:"required"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ import (
|
|||||||
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type UserGroupDto struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
FriendlyName string `json:"friendlyName"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
|
LdapID *string `json:"ldapId"`
|
||||||
|
CreatedAt datatype.DateTime `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserGroupDtoWithUsers struct {
|
type UserGroupDtoWithUsers struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
FriendlyName string `json:"friendlyName"`
|
FriendlyName string `json:"friendlyName"`
|
||||||
|
|||||||
@@ -132,22 +132,18 @@ func (s *LdapService) SyncGroups() error {
|
|||||||
LdapID: value.GetAttributeValue(uniqueIdentifierAttribute),
|
LdapID: value.GetAttributeValue(uniqueIdentifierAttribute),
|
||||||
}
|
}
|
||||||
|
|
||||||
usersToAddDto := dto.UserGroupUpdateUsersDto{
|
|
||||||
UserIDs: membersUserId,
|
|
||||||
}
|
|
||||||
|
|
||||||
if databaseGroup.ID == "" {
|
if databaseGroup.ID == "" {
|
||||||
newGroup, err := s.groupService.Create(syncGroup)
|
newGroup, err := s.groupService.Create(syncGroup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
||||||
} else {
|
} else {
|
||||||
if _, err = s.groupService.UpdateUsers(newGroup.ID, usersToAddDto); err != nil {
|
if _, err = s.groupService.UpdateUsers(newGroup.ID, membersUserId); err != nil {
|
||||||
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_, err = s.groupService.Update(databaseGroup.ID, syncGroup, true)
|
_, err = s.groupService.Update(databaseGroup.ID, syncGroup, true)
|
||||||
_, err = s.groupService.UpdateUsers(databaseGroup.ID, usersToAddDto)
|
_, err = s.groupService.UpdateUsers(databaseGroup.ID, membersUserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
log.Printf("Error syncing group %s: %s", syncGroup.Name, err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -103,16 +103,16 @@ func (s *UserGroupService) Update(id string, input dto.UserGroupCreateDto, allow
|
|||||||
return group, nil
|
return group, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserGroupService) UpdateUsers(id string, input dto.UserGroupUpdateUsersDto) (group model.UserGroup, err error) {
|
func (s *UserGroupService) UpdateUsers(id string, userIds []string) (group model.UserGroup, err error) {
|
||||||
group, err = s.Get(id)
|
group, err = s.Get(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.UserGroup{}, err
|
return model.UserGroup{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the users based on UserIDs in input
|
// Fetch the users based on the userIds
|
||||||
var users []model.User
|
var users []model.User
|
||||||
if len(input.UserIDs) > 0 {
|
if len(userIds) > 0 {
|
||||||
if err := s.db.Where("id IN (?)", input.UserIDs).Find(&users).Error; err != nil {
|
if err := s.db.Where("id IN (?)", userIds).Find(&users).Error; err != nil {
|
||||||
return model.UserGroup{}, err
|
return model.UserGroup{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/utils/image"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -12,6 +10,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
profilepicture "github.com/pocket-id/pocket-id/backend/internal/utils/image"
|
||||||
|
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||||
@@ -48,7 +49,7 @@ func (s *UserService) ListUsers(searchTerm string, sortedPaginationRequest utils
|
|||||||
|
|
||||||
func (s *UserService) GetUser(userID string) (model.User, error) {
|
func (s *UserService) GetUser(userID string) (model.User, error) {
|
||||||
var user model.User
|
var user model.User
|
||||||
err := s.db.Preload("CustomClaims").Where("id = ?", userID).First(&user).Error
|
err := s.db.Preload("UserGroups").Preload("CustomClaims").Where("id = ?", userID).First(&user).Error
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +84,14 @@ func (s *UserService) GetProfilePicture(userID string) (io.Reader, int64, error)
|
|||||||
return defaultPicture, int64(defaultPicture.Len()), nil
|
return defaultPicture, int64(defaultPicture.Len()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UserService) GetUserGroups(userID string) ([]model.UserGroup, error) {
|
||||||
|
var user model.User
|
||||||
|
if err := s.db.Preload("UserGroups").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user.UserGroups, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *UserService) UpdateProfilePicture(userID string, file io.Reader) error {
|
func (s *UserService) UpdateProfilePicture(userID string, file io.Reader) error {
|
||||||
// Validate the user ID to prevent directory traversal
|
// Validate the user ID to prevent directory traversal
|
||||||
if err := uuid.Validate(userID); err != nil {
|
if err := uuid.Validate(userID); err != nil {
|
||||||
@@ -269,6 +278,33 @@ func (s *UserService) ExchangeOneTimeAccessToken(token string, ipAddress, userAg
|
|||||||
return oneTimeAccessToken.User, accessToken, nil
|
return oneTimeAccessToken.User, accessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UserService) UpdateUserGroups(id string, userGroupIds []string) (user model.User, err error) {
|
||||||
|
user, err = s.GetUser(id)
|
||||||
|
if err != nil {
|
||||||
|
return model.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the groups based on userGroupIds
|
||||||
|
var groups []model.UserGroup
|
||||||
|
if len(userGroupIds) > 0 {
|
||||||
|
if err := s.db.Where("id IN (?)", userGroupIds).Find(&groups).Error; err != nil {
|
||||||
|
return model.User{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the current groups with the new set of groups
|
||||||
|
if err := s.db.Model(&user).Association("UserGroups").Replace(groups); err != nil {
|
||||||
|
return model.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated user
|
||||||
|
if err := s.db.Save(&user).Error; err != nil {
|
||||||
|
return model.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *UserService) SetupInitialAdmin() (model.User, string, error) {
|
func (s *UserService) SetupInitialAdmin() (model.User, string, error) {
|
||||||
var userCount int64
|
var userCount int64
|
||||||
if err := s.db.Model(&model.User{}).Count(&userCount).Error; err != nil {
|
if err := s.db.Model(&model.User{}).Count(&userCount).Error; err != nil {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
import UserGroupService from '$lib/services/user-group-service';
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
import type { OidcClient } from '$lib/types/oidc.type';
|
|
||||||
import type { Paginated } from '$lib/types/pagination.type';
|
import type { Paginated } from '$lib/types/pagination.type';
|
||||||
import type { UserGroup } from '$lib/types/user-group.type';
|
import type { UserGroup } from '$lib/types/user-group.type';
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||||
|
import type { UserGroup } from '$lib/types/user-group.type';
|
||||||
import type { User, UserCreate } from '$lib/types/user.type';
|
import type { User, UserCreate } from '$lib/types/user.type';
|
||||||
import APIService from './api-service';
|
import APIService from './api-service';
|
||||||
|
|
||||||
@@ -25,6 +26,11 @@ export default class UserService extends APIService {
|
|||||||
return res.data as User;
|
return res.data as User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserGroups(userId: string) {
|
||||||
|
const res = await this.api.get(`/users/${userId}/groups`);
|
||||||
|
return res.data as UserGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
async update(id: string, user: UserCreate) {
|
async update(id: string, user: UserCreate) {
|
||||||
const res = await this.api.put(`/users/${id}`, user);
|
const res = await this.api.put(`/users/${id}`, user);
|
||||||
return res.data as User;
|
return res.data as User;
|
||||||
@@ -69,4 +75,9 @@ export default class UserService extends APIService {
|
|||||||
async requestOneTimeAccessEmail(email: string, redirectPath?: string) {
|
async requestOneTimeAccessEmail(email: string, redirectPath?: string) {
|
||||||
await this.api.post('/one-time-access-email', { email, redirectPath });
|
await this.api.post('/one-time-access-email', { email, redirectPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateUserGroups(id: string, userGroupIds: string[]) {
|
||||||
|
const res = await this.api.put(`/users/${id}/user-groups`, { userGroupIds });
|
||||||
|
return res.data as User;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CustomClaim } from './custom-claim.type';
|
import type { CustomClaim } from './custom-claim.type';
|
||||||
|
import type { UserGroup } from './user-group.type';
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -7,6 +8,7 @@ export type User = {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
userGroups: UserGroup[];
|
||||||
customClaims: CustomClaim[];
|
customClaims: CustomClaim[];
|
||||||
ldapId?: string;
|
ldapId?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { beforeNavigate } from '$app/navigation';
|
import { beforeNavigate } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||||
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
|
import UserGroupSelection from '$lib/components/user-group-selection.svelte';
|
||||||
import OidcService from '$lib/services/oidc-service';
|
import OidcService from '$lib/services/oidc-service';
|
||||||
import UserGroupService from '$lib/services/user-group-service';
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
import clientSecretStore from '$lib/stores/client-secret-store';
|
import clientSecretStore from '$lib/stores/client-secret-store';
|
||||||
@@ -15,8 +17,6 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import OidcForm from '../oidc-client-form.svelte';
|
import OidcForm from '../oidc-client-form.svelte';
|
||||||
import UserGroupSelection from '../user-group-selection.svelte';
|
|
||||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let client = $state({
|
let client = $state({
|
||||||
|
|||||||
@@ -5,5 +5,8 @@ import type { PageServerLoad } from './$types';
|
|||||||
export const load: PageServerLoad = async ({ params, cookies }) => {
|
export const load: PageServerLoad = async ({ params, cookies }) => {
|
||||||
const userService = new UserService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
const userService = new UserService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
|
||||||
const user = await userService.get(params.id);
|
const user = await userService.get(params.id);
|
||||||
return user;
|
|
||||||
|
return {
|
||||||
|
user
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,18 +6,34 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||||
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { UserCreate } from '$lib/types/user.type';
|
import type { UserCreate } from '$lib/types/user.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideChevronLeft } from 'lucide-svelte';
|
import { LucideChevronLeft } from 'lucide-svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import UserGroupSelection from '$lib/components/user-group-selection.svelte';
|
||||||
import UserForm from '../user-form.svelte';
|
import UserForm from '../user-form.svelte';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let user = $state(data);
|
let user = $state({
|
||||||
|
...data.user,
|
||||||
|
userGroupIds: data.user.userGroups.map((g) => g.id)
|
||||||
|
});
|
||||||
|
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
const customClaimService = new CustomClaimService();
|
const customClaimService = new CustomClaimService();
|
||||||
|
const userGroupService = new UserGroupService();
|
||||||
|
|
||||||
|
async function updateUserGroups(userIds: string[]) {
|
||||||
|
await userService
|
||||||
|
.updateUserGroups(user.id, userIds)
|
||||||
|
.then(() => toast.success('User groups updated successfully'))
|
||||||
|
.catch((e) => {
|
||||||
|
axiosErrorToast(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function updateUser(updatedUser: UserCreate) {
|
async function updateUser(updatedUser: UserCreate) {
|
||||||
let success = true;
|
let success = true;
|
||||||
@@ -80,6 +96,28 @@
|
|||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
<CollapsibleCard
|
||||||
|
id="user-groups"
|
||||||
|
title="User Groups"
|
||||||
|
description="Manage which groups this user belongs to."
|
||||||
|
>
|
||||||
|
{#await userGroupService.list() then groups}
|
||||||
|
<UserGroupSelection
|
||||||
|
{groups}
|
||||||
|
bind:selectedGroupIds={user.userGroupIds}
|
||||||
|
selectionDisabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
|
||||||
|
<div class="mt-5 flex justify-end">
|
||||||
|
<Button
|
||||||
|
on:click={() => updateUserGroups(user.userGroupIds)}
|
||||||
|
disabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
||||||
|
type="submit">Save</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</CollapsibleCard>
|
||||||
|
|
||||||
<CollapsibleCard
|
<CollapsibleCard
|
||||||
id="user-custom-claims"
|
id="user-custom-claims"
|
||||||
title="Custom Claims"
|
title="Custom Claims"
|
||||||
@@ -87,6 +125,6 @@
|
|||||||
>
|
>
|
||||||
<CustomClaimsInput bind:customClaims={user.customClaims} />
|
<CustomClaimsInput bind:customClaims={user.customClaims} />
|
||||||
<div class="mt-5 flex justify-end">
|
<div class="mt-5 flex justify-end">
|
||||||
<Button onclick={updateCustomClaims} type="submit">Save</Button>
|
<Button on:click={updateCustomClaims} type="submit">Save</Button>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import test, { expect } from '@playwright/test';
|
import test, { expect } from '@playwright/test';
|
||||||
import { users } from './data';
|
import { users, userGroups } from './data';
|
||||||
import { cleanupBackend } from './utils/cleanup.util';
|
import { cleanupBackend } from './utils/cleanup.util';
|
||||||
|
|
||||||
test.beforeEach(cleanupBackend);
|
test.beforeEach(cleanupBackend);
|
||||||
@@ -142,7 +142,7 @@ test('Update user fails with already taken username', async ({ page }) => {
|
|||||||
test('Update user custom claims', async ({ page }) => {
|
test('Update user custom claims', async ({ page }) => {
|
||||||
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Expand card' }).click();
|
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
||||||
|
|
||||||
// Add two custom claims
|
// Add two custom claims
|
||||||
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
||||||
@@ -178,3 +178,63 @@ test('Update user custom claims', async ({ page }) => {
|
|||||||
await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim2');
|
await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim2');
|
||||||
await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim2_value');
|
await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim2_value');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Update user group assignments', async ({ page }) => {
|
||||||
|
const user = users.craig;
|
||||||
|
await page.goto(`/settings/admin/users/${user.id}`);
|
||||||
|
|
||||||
|
// Increase the test timeout since this test is complex
|
||||||
|
test.setTimeout(30000);
|
||||||
|
|
||||||
|
// Expand the user groups section if it's collapsed
|
||||||
|
const expandButton = page.getByRole('button', { name: 'Expand card' }).first();
|
||||||
|
if (await expandButton.isVisible()) {
|
||||||
|
await expandButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the user groups table to load
|
||||||
|
await page.waitForSelector('table');
|
||||||
|
|
||||||
|
// First, ensure we start with a clean state - uncheck any checked boxes
|
||||||
|
const developersCheckbox = page
|
||||||
|
.getByRole('row', { name: userGroups.developers.name })
|
||||||
|
.getByRole('checkbox');
|
||||||
|
const designersCheckbox = page
|
||||||
|
.getByRole('row', { name: userGroups.designers.name })
|
||||||
|
.getByRole('checkbox');
|
||||||
|
|
||||||
|
// Force click if needed to overcome element interception issues
|
||||||
|
if ((await developersCheckbox.getAttribute('data-state')) === 'checked') {
|
||||||
|
await developersCheckbox.click({ force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((await designersCheckbox.getAttribute('data-state')) === 'checked') {
|
||||||
|
await designersCheckbox.click({ force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the changes to reset state if needed
|
||||||
|
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||||
|
|
||||||
|
// Wait for toast message to appear and disappear
|
||||||
|
await expect(page.getByRole('status')).toHaveText('User groups updated successfully');
|
||||||
|
await page.waitForTimeout(1000); // Wait for any animations or state changes
|
||||||
|
|
||||||
|
// Now add both groups (using force: true to avoid interception problems)
|
||||||
|
await developersCheckbox.click({ force: true });
|
||||||
|
await designersCheckbox.click({ force: true });
|
||||||
|
|
||||||
|
// Save the changes
|
||||||
|
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||||
|
|
||||||
|
// Verify success message
|
||||||
|
await expect(page.getByRole('status')).toHaveText('User groups updated successfully');
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox')
|
||||||
|
).toHaveAttribute('data-state', 'checked', { timeout: 10000 });
|
||||||
|
await expect(
|
||||||
|
page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox')
|
||||||
|
).toHaveAttribute('data-state', 'checked', { timeout: 10000 });
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user