mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-20 17:25:43 +03:00
feat: disable/enable users (#437)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
@@ -63,7 +63,7 @@ func initRouter(ctx context.Context, db *gorm.DB, appConfigService *service.AppC
|
|||||||
job.RegisterFileCleanupJobs(ctx, db)
|
job.RegisterFileCleanupJobs(ctx, db)
|
||||||
|
|
||||||
// Initialize middleware for specific routes
|
// Initialize middleware for specific routes
|
||||||
authMiddleware := middleware.NewAuthMiddleware(apiKeyService, jwtService)
|
authMiddleware := middleware.NewAuthMiddleware(apiKeyService, userService, jwtService)
|
||||||
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()
|
||||||
|
|
||||||
// Set up API routes
|
// Set up API routes
|
||||||
|
|||||||
@@ -82,11 +82,6 @@ type FileTypeNotSupportedError struct{}
|
|||||||
func (e *FileTypeNotSupportedError) Error() string { return "file type not supported" }
|
func (e *FileTypeNotSupportedError) Error() string { return "file type not supported" }
|
||||||
func (e *FileTypeNotSupportedError) HttpStatusCode() int { return 400 }
|
func (e *FileTypeNotSupportedError) HttpStatusCode() int { return 400 }
|
||||||
|
|
||||||
type InvalidCredentialsError struct{}
|
|
||||||
|
|
||||||
func (e *InvalidCredentialsError) Error() string { return "no user found with provided credentials" }
|
|
||||||
func (e *InvalidCredentialsError) HttpStatusCode() int { return 400 }
|
|
||||||
|
|
||||||
type FileTooLargeError struct {
|
type FileTooLargeError struct {
|
||||||
MaxSize string
|
MaxSize string
|
||||||
}
|
}
|
||||||
@@ -229,8 +224,7 @@ type InvalidUUIDError struct{}
|
|||||||
func (e *InvalidUUIDError) Error() string {
|
func (e *InvalidUUIDError) Error() string {
|
||||||
return "Invalid UUID"
|
return "Invalid UUID"
|
||||||
}
|
}
|
||||||
|
func (e *InvalidUUIDError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||||
type InvalidEmailError struct{}
|
|
||||||
|
|
||||||
type OneTimeAccessDisabledError struct{}
|
type OneTimeAccessDisabledError struct{}
|
||||||
|
|
||||||
@@ -244,31 +238,34 @@ type InvalidAPIKeyError struct{}
|
|||||||
func (e *InvalidAPIKeyError) Error() string {
|
func (e *InvalidAPIKeyError) Error() string {
|
||||||
return "Invalid Api Key"
|
return "Invalid Api Key"
|
||||||
}
|
}
|
||||||
|
func (e *InvalidAPIKeyError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||||
|
|
||||||
type NoAPIKeyProvidedError struct{}
|
type NoAPIKeyProvidedError struct{}
|
||||||
|
|
||||||
func (e *NoAPIKeyProvidedError) Error() string {
|
func (e *NoAPIKeyProvidedError) Error() string {
|
||||||
return "No API Key Provided"
|
return "No API Key Provided"
|
||||||
}
|
}
|
||||||
|
func (e *NoAPIKeyProvidedError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||||
|
|
||||||
type APIKeyNotFoundError struct{}
|
type APIKeyNotFoundError struct{}
|
||||||
|
|
||||||
func (e *APIKeyNotFoundError) Error() string {
|
func (e *APIKeyNotFoundError) Error() string {
|
||||||
return "API Key Not Found"
|
return "API Key Not Found"
|
||||||
}
|
}
|
||||||
|
func (e *APIKeyNotFoundError) HttpStatusCode() int { return http.StatusUnauthorized }
|
||||||
|
|
||||||
type APIKeyExpirationDateError struct{}
|
type APIKeyExpirationDateError struct{}
|
||||||
|
|
||||||
func (e *APIKeyExpirationDateError) Error() string {
|
func (e *APIKeyExpirationDateError) Error() string {
|
||||||
return "API Key expiration time must be in the future"
|
return "API Key expiration time must be in the future"
|
||||||
}
|
}
|
||||||
|
func (e *APIKeyExpirationDateError) HttpStatusCode() int { return http.StatusBadRequest }
|
||||||
|
|
||||||
type OidcInvalidRefreshTokenError struct{}
|
type OidcInvalidRefreshTokenError struct{}
|
||||||
|
|
||||||
func (e *OidcInvalidRefreshTokenError) Error() string {
|
func (e *OidcInvalidRefreshTokenError) Error() string {
|
||||||
return "refresh token is invalid or expired"
|
return "refresh token is invalid or expired"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *OidcInvalidRefreshTokenError) HttpStatusCode() int {
|
func (e *OidcInvalidRefreshTokenError) HttpStatusCode() int {
|
||||||
return http.StatusBadRequest
|
return http.StatusBadRequest
|
||||||
}
|
}
|
||||||
@@ -278,7 +275,6 @@ type OidcMissingRefreshTokenError struct{}
|
|||||||
func (e *OidcMissingRefreshTokenError) Error() string {
|
func (e *OidcMissingRefreshTokenError) Error() string {
|
||||||
return "refresh token is required"
|
return "refresh token is required"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *OidcMissingRefreshTokenError) HttpStatusCode() int {
|
func (e *OidcMissingRefreshTokenError) HttpStatusCode() int {
|
||||||
return http.StatusBadRequest
|
return http.StatusBadRequest
|
||||||
}
|
}
|
||||||
@@ -288,7 +284,15 @@ type OidcMissingAuthorizationCodeError struct{}
|
|||||||
func (e *OidcMissingAuthorizationCodeError) Error() string {
|
func (e *OidcMissingAuthorizationCodeError) Error() string {
|
||||||
return "authorization code is required"
|
return "authorization code is required"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *OidcMissingAuthorizationCodeError) HttpStatusCode() int {
|
func (e *OidcMissingAuthorizationCodeError) HttpStatusCode() int {
|
||||||
return http.StatusBadRequest
|
return http.StatusBadRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserDisabledError struct{}
|
||||||
|
|
||||||
|
func (e *UserDisabledError) Error() string {
|
||||||
|
return "User account is disabled"
|
||||||
|
}
|
||||||
|
func (e *UserDisabledError) HttpStatusCode() int {
|
||||||
|
return http.StatusForbidden
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type AppConfigUpdateDto struct {
|
|||||||
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
||||||
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
|
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
|
||||||
LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
|
LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
|
||||||
|
LdapSoftDeleteUsers string `json:"ldapSoftDeleteUsers"`
|
||||||
EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" binding:"required"`
|
EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" binding:"required"`
|
||||||
EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"`
|
EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type UserDto struct {
|
|||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
UserGroups []UserGroupDto `json:"userGroups"`
|
UserGroups []UserGroupDto `json:"userGroups"`
|
||||||
LdapID *string `json:"ldapId"`
|
LdapID *string `json:"ldapId"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserCreateDto struct {
|
type UserCreateDto struct {
|
||||||
@@ -22,6 +23,7 @@ type UserCreateDto struct {
|
|||||||
LastName string `json:"lastName" binding:"required,min=1,max=50"`
|
LastName string `json:"lastName" binding:"required,min=1,max=50"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
Locale *string `json:"locale"`
|
Locale *string `json:"locale"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
LdapID string `json:"-"`
|
LdapID string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ func (m *ApiKeyAuthMiddleware) Verify(c *gin.Context, adminRequired bool) (userI
|
|||||||
return "", false, &common.NotSignedInError{}
|
return "", false, &common.NotSignedInError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is an admin
|
if user.Disabled {
|
||||||
|
return "", false, &common.UserDisabledError{}
|
||||||
|
}
|
||||||
|
|
||||||
if adminRequired && !user.IsAdmin {
|
if adminRequired && !user.IsAdmin {
|
||||||
return "", false, &common.MissingPermissionError{}
|
return "", false, &common.MissingPermissionError{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ type AuthOptions struct {
|
|||||||
|
|
||||||
func NewAuthMiddleware(
|
func NewAuthMiddleware(
|
||||||
apiKeyService *service.ApiKeyService,
|
apiKeyService *service.ApiKeyService,
|
||||||
|
userService *service.UserService,
|
||||||
jwtService *service.JwtService,
|
jwtService *service.JwtService,
|
||||||
) *AuthMiddleware {
|
) *AuthMiddleware {
|
||||||
return &AuthMiddleware{
|
return &AuthMiddleware{
|
||||||
apiKeyMiddleware: NewApiKeyAuthMiddleware(apiKeyService, jwtService),
|
apiKeyMiddleware: NewApiKeyAuthMiddleware(apiKeyService, jwtService),
|
||||||
jwtMiddleware: NewJwtAuthMiddleware(jwtService),
|
jwtMiddleware: NewJwtAuthMiddleware(jwtService, userService),
|
||||||
options: AuthOptions{
|
options: AuthOptions{
|
||||||
AdminRequired: true,
|
AdminRequired: true,
|
||||||
SuccessOptional: false,
|
SuccessOptional: false,
|
||||||
@@ -57,12 +58,13 @@ func (m *AuthMiddleware) WithSuccessOptional() *AuthMiddleware {
|
|||||||
|
|
||||||
func (m *AuthMiddleware) Add() gin.HandlerFunc {
|
func (m *AuthMiddleware) Add() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// First try JWT auth
|
|
||||||
userID, isAdmin, err := m.jwtMiddleware.Verify(c, m.options.AdminRequired)
|
userID, isAdmin, err := m.jwtMiddleware.Verify(c, m.options.AdminRequired)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// JWT auth succeeded, continue with the request
|
|
||||||
c.Set("userID", userID)
|
c.Set("userID", userID)
|
||||||
c.Set("userIsAdmin", isAdmin)
|
c.Set("userIsAdmin", isAdmin)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -70,9 +72,11 @@ func (m *AuthMiddleware) Add() gin.HandlerFunc {
|
|||||||
// JWT auth failed, try API key auth
|
// JWT auth failed, try API key auth
|
||||||
userID, isAdmin, err = m.apiKeyMiddleware.Verify(c, m.options.AdminRequired)
|
userID, isAdmin, err = m.apiKeyMiddleware.Verify(c, m.options.AdminRequired)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// API key auth succeeded, continue with the request
|
|
||||||
c.Set("userID", userID)
|
c.Set("userID", userID)
|
||||||
c.Set("userIsAdmin", isAdmin)
|
c.Set("userIsAdmin", isAdmin)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type JwtAuthMiddleware struct {
|
type JwtAuthMiddleware struct {
|
||||||
jwtService *service.JwtService
|
userService *service.UserService
|
||||||
|
jwtService *service.JwtService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJwtAuthMiddleware(jwtService *service.JwtService) *JwtAuthMiddleware {
|
func NewJwtAuthMiddleware(jwtService *service.JwtService, userService *service.UserService) *JwtAuthMiddleware {
|
||||||
return &JwtAuthMiddleware{jwtService: jwtService}
|
return &JwtAuthMiddleware{jwtService: jwtService, userService: userService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *JwtAuthMiddleware) Add(adminRequired bool) gin.HandlerFunc {
|
func (m *JwtAuthMiddleware) Add(adminRequired bool) gin.HandlerFunc {
|
||||||
@@ -55,12 +56,16 @@ func (m *JwtAuthMiddleware) Verify(c *gin.Context, adminRequired bool) (subject
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user is an admin
|
user, err := m.userService.GetUser(c, subject)
|
||||||
isAdmin, err = service.GetIsAdmin(token)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, &common.TokenInvalidError{}
|
return "", false, &common.NotSignedInError{}
|
||||||
}
|
}
|
||||||
if adminRequired && !isAdmin {
|
|
||||||
|
if user.Disabled {
|
||||||
|
return "", false, &common.UserDisabledError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if adminRequired && !user.IsAdmin {
|
||||||
return "", false, &common.MissingPermissionError{}
|
return "", false, &common.MissingPermissionError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type AppConfig struct {
|
|||||||
LdapAttributeGroupUniqueIdentifier AppConfigVariable `key:"ldapAttributeGroupUniqueIdentifier"`
|
LdapAttributeGroupUniqueIdentifier AppConfigVariable `key:"ldapAttributeGroupUniqueIdentifier"`
|
||||||
LdapAttributeGroupName AppConfigVariable `key:"ldapAttributeGroupName"`
|
LdapAttributeGroupName AppConfigVariable `key:"ldapAttributeGroupName"`
|
||||||
LdapAttributeAdminGroup AppConfigVariable `key:"ldapAttributeAdminGroup"`
|
LdapAttributeAdminGroup AppConfigVariable `key:"ldapAttributeAdminGroup"`
|
||||||
|
LdapSoftDeleteUsers AppConfigVariable `key:"ldapSoftDeleteUsers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AppConfig) ToAppConfigVariableSlice(showAll bool) []AppConfigVariable {
|
func (c *AppConfig) ToAppConfigVariableSlice(showAll bool) []AppConfigVariable {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type User struct {
|
|||||||
IsAdmin bool `sortable:"true"`
|
IsAdmin bool `sortable:"true"`
|
||||||
Locale *string
|
Locale *string
|
||||||
LdapID *string
|
LdapID *string
|
||||||
|
Disabled bool `sortable:"true"`
|
||||||
|
|
||||||
CustomClaims []CustomClaim
|
CustomClaims []CustomClaim
|
||||||
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
|
|||||||
LdapAttributeGroupUniqueIdentifier: model.AppConfigVariable{},
|
LdapAttributeGroupUniqueIdentifier: model.AppConfigVariable{},
|
||||||
LdapAttributeGroupName: model.AppConfigVariable{},
|
LdapAttributeGroupName: model.AppConfigVariable{},
|
||||||
LdapAttributeAdminGroup: model.AppConfigVariable{},
|
LdapAttributeAdminGroup: model.AppConfigVariable{},
|
||||||
|
LdapSoftDeleteUsers: model.AppConfigVariable{Value: "true"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -279,6 +279,22 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
|||||||
Where("ldap_id = ?", ldapId).
|
Where("ldap_id = ?", ldapId).
|
||||||
First(&databaseUser).
|
First(&databaseUser).
|
||||||
Error
|
Error
|
||||||
|
|
||||||
|
// If a user is found (even if disabled), enable them since they're now back in LDAP
|
||||||
|
if databaseUser.ID != "" && databaseUser.Disabled {
|
||||||
|
// Use the transaction instead of the direct context
|
||||||
|
err = tx.
|
||||||
|
WithContext(ctx).
|
||||||
|
Model(&model.User{}).
|
||||||
|
Where("id = ?", databaseUser.ID).
|
||||||
|
Update("disabled", false).
|
||||||
|
Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to enable user %s: %v", databaseUser.Username, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
// This could error with ErrRecordNotFound and we want to ignore that here
|
// This could error with ErrRecordNotFound and we want to ignore that here
|
||||||
return fmt.Errorf("failed to query for LDAP user ID '%s': %w", ldapId, err)
|
return fmt.Errorf("failed to query for LDAP user ID '%s': %w", ldapId, err)
|
||||||
@@ -336,24 +352,32 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
|||||||
err = tx.
|
err = tx.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
Find(&ldapUsersInDb, "ldap_id IS NOT NULL").
|
Find(&ldapUsersInDb, "ldap_id IS NOT NULL").
|
||||||
Select("ldap_id").
|
Select("id, username, ldap_id, disabled").
|
||||||
Error
|
Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch users from database: %w", err)
|
return fmt.Errorf("failed to fetch users from database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete users that no longer exist in LDAP
|
// Mark users as disabled or delete users that no longer exist in LDAP
|
||||||
for _, user := range ldapUsersInDb {
|
for _, user := range ldapUsersInDb {
|
||||||
|
// Skip if the user ID exists in the fetched LDAP results
|
||||||
if _, exists := ldapUserIDs[*user.LdapID]; exists {
|
if _, exists := ldapUserIDs[*user.LdapID]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.userService.deleteUserInternal(ctx, user.ID, true, tx)
|
if dbConfig.LdapSoftDeleteUsers.IsTrue() {
|
||||||
if err != nil {
|
err = s.userService.DisableUser(ctx, user.ID, tx)
|
||||||
return fmt.Errorf("failed to delete user '%s': %w", user.Username, err)
|
if err != nil {
|
||||||
|
log.Printf("Failed to disable user %s: %v", user.Username, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = s.userService.DeleteUser(ctx, user.ID, true)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to delete user %s: %v", user.Username, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Deleted user '%s'", user.Username)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -38,14 +38,19 @@ func NewUserService(db *gorm.DB, jwtService *JwtService, auditLogService *AuditL
|
|||||||
|
|
||||||
func (s *UserService) ListUsers(ctx context.Context, searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
|
func (s *UserService) ListUsers(ctx context.Context, searchTerm string, sortedPaginationRequest utils.SortedPaginationRequest) ([]model.User, utils.PaginationResponse, error) {
|
||||||
var users []model.User
|
var users []model.User
|
||||||
query := s.db.WithContext(ctx).Model(&model.User{})
|
query := s.db.WithContext(ctx).
|
||||||
|
Model(&model.User{}).
|
||||||
|
Preload("UserGroups").
|
||||||
|
Preload("CustomClaims")
|
||||||
|
|
||||||
if searchTerm != "" {
|
if searchTerm != "" {
|
||||||
searchPattern := "%" + searchTerm + "%"
|
searchPattern := "%" + searchTerm + "%"
|
||||||
query = query.Where("email LIKE ? OR first_name LIKE ? OR username LIKE ?", searchPattern, searchPattern, searchPattern)
|
query = query.Where("email LIKE ? OR first_name LIKE ? OR last_name LIKE ? OR username LIKE ?",
|
||||||
|
searchPattern, searchPattern, searchPattern, searchPattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &users)
|
pagination, err := utils.PaginateAndSort(sortedPaginationRequest, query, &users)
|
||||||
|
|
||||||
return users, pagination, err
|
return users, pagination, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,9 +175,28 @@ func (s *UserService) UpdateProfilePicture(userID string, file io.Reader) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) DeleteUser(ctx context.Context, userID string, allowLdapDelete bool) error {
|
func (s *UserService) DeleteUser(ctx context.Context, userID string, allowLdapDelete bool) error {
|
||||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
tx := s.db.Begin()
|
||||||
return s.deleteUserInternal(ctx, userID, allowLdapDelete, tx)
|
|
||||||
})
|
var user model.User
|
||||||
|
if err := tx.WithContext(ctx).First(&user, "id = ?", userID).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only soft-delete if user is LDAP and soft-delete is enabled and not allowing hard delete
|
||||||
|
if user.LdapID != nil && s.appConfigService.GetDbConfig().LdapSoftDeleteUsers.IsTrue() && !allowLdapDelete {
|
||||||
|
if !user.Disabled {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("LDAP user must be disabled before deletion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, hard delete (local users or LDAP users when allowed)
|
||||||
|
if err := s.deleteUserInternal(ctx, userID, allowLdapDelete, tx); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit().Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) deleteUserInternal(ctx context.Context, userID string, allowLdapDelete bool, tx *gorm.DB) error {
|
func (s *UserService) deleteUserInternal(ctx context.Context, userID string, allowLdapDelete bool, tx *gorm.DB) error {
|
||||||
@@ -187,8 +211,8 @@ func (s *UserService) deleteUserInternal(ctx context.Context, userID string, all
|
|||||||
return fmt.Errorf("failed to load user to delete: %w", err)
|
return fmt.Errorf("failed to load user to delete: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disallow deleting the user if it is an LDAP user and LDAP is enabled
|
// Disallow deleting the user if it is an LDAP user, LDAP is enabled, and the user is not disabled
|
||||||
if !allowLdapDelete && user.LdapID != nil && s.appConfigService.GetDbConfig().LdapEnabled.IsTrue() {
|
if !allowLdapDelete && !user.Disabled && user.LdapID != nil && s.appConfigService.GetDbConfig().LdapEnabled.IsTrue() {
|
||||||
return &common.LdapUserUpdateError{}
|
return &common.LdapUserUpdateError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +323,7 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
|
|||||||
user.Locale = updatedUser.Locale
|
user.Locale = updatedUser.Locale
|
||||||
if !updateOwnUser {
|
if !updateOwnUser {
|
||||||
user.IsAdmin = updatedUser.IsAdmin
|
user.IsAdmin = updatedUser.IsAdmin
|
||||||
|
user.Disabled = updatedUser.Disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.
|
err = tx.
|
||||||
@@ -606,3 +631,11 @@ func (s *UserService) ResetProfilePicture(userID string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UserService) DisableUser(ctx context.Context, userID string, tx *gorm.DB) error {
|
||||||
|
return tx.WithContext(ctx).
|
||||||
|
Model(&model.User{}).
|
||||||
|
Where("id = ?", userID).
|
||||||
|
Update("disabled", true).
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -244,6 +244,10 @@ func (s *WebAuthnService) VerifyLogin(ctx context.Context, sessionID string, cre
|
|||||||
return model.User{}, "", err
|
return model.User{}, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Disabled {
|
||||||
|
return model.User{}, "", &common.UserDisabledError{}
|
||||||
|
}
|
||||||
|
|
||||||
token, err := s.jwtService.GenerateAccessToken(*user)
|
token, err := s.jwtService.GenerateAccessToken(*user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.User{}, "", err
|
return model.User{}, "", err
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
DROP INDEX idx_users_disabled;
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
DROP COLUMN disabled;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS disabled BOOLEAN DEFAULT FALSE NOT NULL;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
DROP INDEX idx_users_disabled;
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
DROP COLUMN disabled;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN disabled NUMERIC DEFAULT FALSE NOT NULL;
|
||||||
@@ -324,5 +324,14 @@
|
|||||||
"client_authorization": "Client Authorization",
|
"client_authorization": "Client Authorization",
|
||||||
"new_client_authorization": "New Client Authorization",
|
"new_client_authorization": "New Client Authorization",
|
||||||
"disable_animations": "Disable Animations",
|
"disable_animations": "Disable Animations",
|
||||||
"turn_off_all_animations_throughout_the_admin_ui": "Turn off all animations throughout the Admin UI"
|
"turn_off_all_animations_throughout_the_admin_ui": "Turn off all animations throughout the Admin UI.",
|
||||||
|
"user_disabled": "Account Disabled",
|
||||||
|
"disabled_users_cannot_log_in_or_use_services": "Disabled users cannot log in or use services.",
|
||||||
|
"user_disabled_successfully": "User has been disabled successfully.",
|
||||||
|
"user_enabled_successfully": "User has been enabled successfully.",
|
||||||
|
"status": "Status",
|
||||||
|
"disable_firstname_lastname": "Disable {firstName} {lastName}",
|
||||||
|
"are_you_sure_you_want_to_disable_this_user": "Are you sure you want to disable this user? They will not be able to log in or access any services.",
|
||||||
|
"ldap_soft_delete_users": "Keep disabled users from LDAP.",
|
||||||
|
"ldap_soft_delete_users_description": "When enabled, users removed from LDAP will be disabled rather than deleted from the system."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export type AllAppConfig = AppConfig & {
|
|||||||
ldapAttributeGroupUniqueIdentifier: string;
|
ldapAttributeGroupUniqueIdentifier: string;
|
||||||
ldapAttributeGroupName: string;
|
ldapAttributeGroupName: string;
|
||||||
ldapAttributeAdminGroup: string;
|
ldapAttributeAdminGroup: string;
|
||||||
|
ldapSoftDeleteUsers: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppConfigRawResponse = {
|
export type AppConfigRawResponse = {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type User = {
|
|||||||
customClaims: CustomClaim[];
|
customClaims: CustomClaim[];
|
||||||
locale?: Locale;
|
locale?: Locale;
|
||||||
ldapId?: string;
|
ldapId?: string;
|
||||||
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId' | 'userGroups'>;
|
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId' | 'userGroups'>;
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ export function getAxiosErrorMessage(
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function axiosErrorToast(e: unknown, defaultMessage: string = m.an_unknown_error_occurred()) {
|
export function axiosErrorToast(
|
||||||
|
e: unknown,
|
||||||
|
defaultMessage: string = m.an_unknown_error_occurred()
|
||||||
|
) {
|
||||||
const message = getAxiosErrorMessage(e, defaultMessage);
|
const message = getAxiosErrorMessage(e, defaultMessage);
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
}
|
}
|
||||||
@@ -29,7 +32,8 @@ export function getWebauthnErrorMessage(e: unknown) {
|
|||||||
m.authenticator_does_not_support_resident_keys(),
|
m.authenticator_does_not_support_resident_keys(),
|
||||||
ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: m.passkey_was_previously_registered(),
|
ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: m.passkey_was_previously_registered(),
|
||||||
ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG:
|
ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG:
|
||||||
m.authenticator_does_not_support_any_of_the_requested_algorithms()
|
m.authenticator_does_not_support_any_of_the_requested_algorithms(),
|
||||||
|
ERROR_USER_DISABLED_MSG: m.user_disabled()
|
||||||
};
|
};
|
||||||
|
|
||||||
let message = m.an_unknown_error_occurred();
|
let message = m.an_unknown_error_occurred();
|
||||||
|
|||||||
@@ -43,7 +43,8 @@
|
|||||||
ldapAttributeGroupMember: appConfig.ldapAttributeGroupMember,
|
ldapAttributeGroupMember: appConfig.ldapAttributeGroupMember,
|
||||||
ldapAttributeGroupUniqueIdentifier: appConfig.ldapAttributeGroupUniqueIdentifier,
|
ldapAttributeGroupUniqueIdentifier: appConfig.ldapAttributeGroupUniqueIdentifier,
|
||||||
ldapAttributeGroupName: appConfig.ldapAttributeGroupName,
|
ldapAttributeGroupName: appConfig.ldapAttributeGroupName,
|
||||||
ldapAttributeAdminGroup: appConfig.ldapAttributeAdminGroup
|
ldapAttributeAdminGroup: appConfig.ldapAttributeAdminGroup,
|
||||||
|
ldapSoftDeleteUsers: appConfig.ldapSoftDeleteUsers || true
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -63,7 +64,8 @@
|
|||||||
ldapAttributeGroupMember: z.string(),
|
ldapAttributeGroupMember: z.string(),
|
||||||
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
||||||
ldapAttributeGroupName: z.string().min(1),
|
ldapAttributeGroupName: z.string().min(1),
|
||||||
ldapAttributeAdminGroup: z.string()
|
ldapAttributeAdminGroup: z.string(),
|
||||||
|
ldapSoftDeleteUsers: z.boolean()
|
||||||
});
|
});
|
||||||
|
|
||||||
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
|
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
|
||||||
@@ -116,7 +118,11 @@
|
|||||||
placeholder="cn=people,dc=example,dc=com"
|
placeholder="cn=people,dc=example,dc=com"
|
||||||
bind:input={$inputs.ldapBindDn}
|
bind:input={$inputs.ldapBindDn}
|
||||||
/>
|
/>
|
||||||
<FormInput label={m.ldap_bind_password()} type="password" bind:input={$inputs.ldapBindPassword} />
|
<FormInput
|
||||||
|
label={m.ldap_bind_password()}
|
||||||
|
type="password"
|
||||||
|
bind:input={$inputs.ldapBindPassword}
|
||||||
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
label={m.ldap_base_dn()}
|
label={m.ldap_base_dn()}
|
||||||
placeholder="dc=example,dc=com"
|
placeholder="dc=example,dc=com"
|
||||||
@@ -140,6 +146,12 @@
|
|||||||
description={m.this_can_be_useful_for_selfsigned_certificates()}
|
description={m.this_can_be_useful_for_selfsigned_certificates()}
|
||||||
bind:checked={$inputs.ldapSkipCertVerify.value}
|
bind:checked={$inputs.ldapSkipCertVerify.value}
|
||||||
/>
|
/>
|
||||||
|
<CheckboxWithLabel
|
||||||
|
id="ldap-soft-delete-users"
|
||||||
|
label={m.ldap_soft_delete_users()}
|
||||||
|
description={m.ldap_soft_delete_users_description()}
|
||||||
|
bind:checked={$inputs.ldapSoftDeleteUsers.value}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h4 class="mt-10 text-lg font-semibold">{m.attribute_mapping()}</h4>
|
<h4 class="mt-10 text-lg font-semibold">{m.attribute_mapping()}</h4>
|
||||||
<div class="mt-4 grid grid-cols-1 items-end gap-5 md:grid-cols-2">
|
<div class="mt-4 grid grid-cols-1 items-end gap-5 md:grid-cols-2">
|
||||||
@@ -203,7 +215,9 @@
|
|||||||
|
|
||||||
<div class="mt-8 flex flex-wrap justify-end gap-3">
|
<div class="mt-8 flex flex-wrap justify-end gap-3">
|
||||||
{#if ldapEnabled}
|
{#if ldapEnabled}
|
||||||
<Button variant="secondary" onclick={onDisable} disabled={uiConfigDisabled}>{m.disable()}</Button>
|
<Button variant="secondary" onclick={onDisable} disabled={uiConfigDisabled}
|
||||||
|
>{m.disable()}</Button
|
||||||
|
>
|
||||||
<Button variant="secondary" onclick={syncLdap} isLoading={ldapSyncing}>{m.sync_now()}</Button>
|
<Button variant="secondary" onclick={syncLdap} isLoading={ldapSyncing}>{m.sync_now()}</Button>
|
||||||
<Button type="submit" disabled={uiConfigDisabled}>{m.save()}</Button>
|
<Button type="submit" disabled={uiConfigDisabled}>{m.save()}</Button>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
lastName: existingUser?.lastName || '',
|
lastName: existingUser?.lastName || '',
|
||||||
email: existingUser?.email || '',
|
email: existingUser?.email || '',
|
||||||
username: existingUser?.username || '',
|
username: existingUser?.username || '',
|
||||||
isAdmin: existingUser?.isAdmin || false
|
isAdmin: existingUser?.isAdmin || false,
|
||||||
|
disabled: existingUser?.disabled || false
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -34,12 +35,10 @@
|
|||||||
.string()
|
.string()
|
||||||
.min(2)
|
.min(2)
|
||||||
.max(30)
|
.max(30)
|
||||||
.regex(
|
.regex(/^[a-z0-9_@.-]+$/, m.username_can_only_contain()),
|
||||||
/^[a-z0-9_@.-]+$/,
|
|
||||||
m.username_can_only_contain()
|
|
||||||
),
|
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
isAdmin: z.boolean()
|
isAdmin: z.boolean(),
|
||||||
|
disabled: z.boolean()
|
||||||
});
|
});
|
||||||
type FormSchema = typeof formSchema;
|
type FormSchema = typeof formSchema;
|
||||||
|
|
||||||
@@ -68,6 +67,12 @@
|
|||||||
description={m.admins_have_full_access_to_the_admin_panel()}
|
description={m.admins_have_full_access_to_the_admin_panel()}
|
||||||
bind:checked={$inputs.isAdmin.value}
|
bind:checked={$inputs.isAdmin.value}
|
||||||
/>
|
/>
|
||||||
|
<CheckboxWithLabel
|
||||||
|
id="user-disabled"
|
||||||
|
label={m.user_disabled()}
|
||||||
|
description={m.disabled_users_cannot_log_in_or_use_services()}
|
||||||
|
bind:checked={$inputs.disabled.value}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 flex justify-end">
|
<div class="mt-5 flex justify-end">
|
||||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||||
|
|||||||
@@ -2,20 +2,26 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||||
|
import OneTimeLinkModal from '$lib/components/one-time-link-modal.svelte';
|
||||||
import { Badge } from '$lib/components/ui/badge/index';
|
import { Badge } from '$lib/components/ui/badge/index';
|
||||||
import { buttonVariants } from '$lib/components/ui/button';
|
import { buttonVariants } from '$lib/components/ui/button';
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
|
import { m } from '$lib/paraglide/messages';
|
||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||||
import type { User } from '$lib/types/user.type';
|
import type { User } from '$lib/types/user.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideLink, LucidePencil, LucideTrash } from 'lucide-svelte';
|
import {
|
||||||
|
LucideLink,
|
||||||
|
LucidePencil,
|
||||||
|
LucideTrash,
|
||||||
|
LucideUserCheck,
|
||||||
|
LucideUserX
|
||||||
|
} from 'lucide-svelte';
|
||||||
import Ellipsis from 'lucide-svelte/icons/ellipsis';
|
import Ellipsis from 'lucide-svelte/icons/ellipsis';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import OneTimeLinkModal from '$lib/components/one-time-link-modal.svelte';
|
|
||||||
import { m } from '$lib/paraglide/messages';
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
users = $bindable(),
|
users = $bindable(),
|
||||||
@@ -28,7 +34,7 @@
|
|||||||
|
|
||||||
async function deleteUser(user: User) {
|
async function deleteUser(user: User) {
|
||||||
openConfirmDialog({
|
openConfirmDialog({
|
||||||
title: m.delete_firstname_lastname({firstName: user.firstName, lastName: user.lastName}),
|
title: m.delete_firstname_lastname({ firstName: user.firstName, lastName: user.lastName }),
|
||||||
message: m.are_you_sure_you_want_to_delete_this_user(),
|
message: m.are_you_sure_you_want_to_delete_this_user(),
|
||||||
confirm: {
|
confirm: {
|
||||||
label: m.delete(),
|
label: m.delete(),
|
||||||
@@ -45,6 +51,42 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function enableUser(user: User) {
|
||||||
|
await userService
|
||||||
|
.update(user.id, {
|
||||||
|
...user,
|
||||||
|
disabled: false
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(m.user_enabled_successfully());
|
||||||
|
userService.list(requestOptions!).then((updatedUsers) => (users = updatedUsers));
|
||||||
|
})
|
||||||
|
.catch(axiosErrorToast);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disableUser(user: User) {
|
||||||
|
openConfirmDialog({
|
||||||
|
title: m.disable_firstname_lastname({ firstName: user.firstName, lastName: user.lastName }),
|
||||||
|
message: m.are_you_sure_you_want_to_disable_this_user(),
|
||||||
|
confirm: {
|
||||||
|
label: m.disable(),
|
||||||
|
destructive: true,
|
||||||
|
action: async () => {
|
||||||
|
try {
|
||||||
|
await userService.update(user.id, {
|
||||||
|
...user,
|
||||||
|
disabled: true
|
||||||
|
});
|
||||||
|
users = await userService.list(requestOptions!);
|
||||||
|
toast.success(m.user_disabled_successfully());
|
||||||
|
} catch (e) {
|
||||||
|
axiosErrorToast(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AdvancedTable
|
<AdvancedTable
|
||||||
@@ -57,7 +99,8 @@
|
|||||||
{ label: m.email(), sortColumn: 'email' },
|
{ label: m.email(), sortColumn: 'email' },
|
||||||
{ label: m.username(), sortColumn: 'username' },
|
{ label: m.username(), sortColumn: 'username' },
|
||||||
{ label: m.role(), sortColumn: 'isAdmin' },
|
{ label: m.role(), sortColumn: 'isAdmin' },
|
||||||
...($appConfigStore.ldapEnabled ? [{ label: m.source()}] : []),
|
{ label: m.status(), sortColumn: 'disabled' },
|
||||||
|
...($appConfigStore.ldapEnabled ? [{ label: m.source() }] : []),
|
||||||
{ label: m.actions(), hidden: true }
|
{ label: m.actions(), hidden: true }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -69,9 +112,15 @@
|
|||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Badge variant="outline">{item.isAdmin ? m.admin() : m.user()}</Badge>
|
<Badge variant="outline">{item.isAdmin ? m.admin() : m.user()}</Badge>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Badge variant={item.disabled ? 'destructive' : 'default'}>
|
||||||
|
{item.disabled ? m.disabled() : m.enabled()}
|
||||||
|
</Badge>
|
||||||
|
</Table.Cell>
|
||||||
{#if $appConfigStore.ldapEnabled}
|
{#if $appConfigStore.ldapEnabled}
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Badge variant={item.ldapId ? 'default' : 'outline'}>{item.ldapId ? m.ldap() : m.local()}</Badge
|
<Badge variant={item.ldapId ? 'default' : 'outline'}
|
||||||
|
>{item.ldapId ? m.ldap() : m.local()}</Badge
|
||||||
>
|
>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -89,6 +138,17 @@
|
|||||||
><LucidePencil class="mr-2 h-4 w-4" /> {m.edit()}</DropdownMenu.Item
|
><LucidePencil class="mr-2 h-4 w-4" /> {m.edit()}</DropdownMenu.Item
|
||||||
>
|
>
|
||||||
{#if !item.ldapId || !$appConfigStore.ldapEnabled}
|
{#if !item.ldapId || !$appConfigStore.ldapEnabled}
|
||||||
|
{#if item.disabled}
|
||||||
|
<DropdownMenu.Item onclick={() => enableUser(item)}
|
||||||
|
><LucideUserCheck class="mr-2 h-4 w-4" />{m.enable()}</DropdownMenu.Item
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<DropdownMenu.Item onclick={() => disableUser(item)}
|
||||||
|
><LucideUserX class="mr-2 h-4 w-4" />{m.disable()}</DropdownMenu.Item
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if !item.ldapId || (item.ldapId && item.disabled)}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="text-red-500 focus:!text-red-700"
|
class="text-red-500 focus:!text-red-700"
|
||||||
onclick={() => deleteUser(item)}
|
onclick={() => deleteUser(item)}
|
||||||
|
|||||||
Reference in New Issue
Block a user