feat(PROJ-53): Konfigurierbare Listenanzahl pro Seite
- users.list_page_size (Default 25), PATCH /api/auth/preferences, Whitelist 25/50/100/200, Wert in login/me-Response - Settings-UI mit Select, /search nutzt gespeicherte Seitengröße - /api/search page_size serverseitig auf max. 500 gecappt fix(PROJ-46): login_attempts-Migration nutzte s.db statt s.pool (Backend kompilierte nicht) feat(PROJ-50): DSGVO-Löschersuchen Backend (dsgvo_requests, Handler, cc_addr/bcc_addr Indexerweiterung) — noch nicht QA'd/deployed
This commit is contained in:
@@ -24,17 +24,18 @@ const (
|
||||
|
||||
// User represents a user account in the system.
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Source string `json:"source"` // "local" or "ldap"
|
||||
Active bool `json:"active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
TenantID *int64 `json:"tenant_id,omitempty"`
|
||||
TOTPEnabled bool `json:"totp_enabled"`
|
||||
TOTPResetAt *time.Time `json:"totp_reset_at,omitempty"`
|
||||
TOTPResetBy *string `json:"totp_reset_by,omitempty"`
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Source string `json:"source"` // "local" or "ldap"
|
||||
Active bool `json:"active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
TenantID *int64 `json:"tenant_id,omitempty"`
|
||||
TOTPEnabled bool `json:"totp_enabled"`
|
||||
TOTPResetAt *time.Time `json:"totp_reset_at,omitempty"`
|
||||
TOTPResetBy *string `json:"totp_reset_by,omitempty"`
|
||||
ListPageSize int `json:"list_page_size"`
|
||||
}
|
||||
|
||||
// CreateUserRequest holds parameters for creating a new user.
|
||||
@@ -106,6 +107,14 @@ func (s *Store) initSchema(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// PROJ-46: login_attempts.username auf VARCHAR(255) erweitern (passend zu users.email),
|
||||
// damit lange E-Mail-Adressen als Login-Identifier nicht abgeschnitten werden.
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
ALTER TABLE login_attempts ALTER COLUMN username TYPE VARCHAR(255);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// PROJ-24: TOTP 2FA columns
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_secret BYTEA;
|
||||
@@ -113,6 +122,13 @@ func (s *Store) initSchema(ctx context.Context) error {
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_reset_at TIMESTAMPTZ;
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS totp_reset_by TEXT;
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// PROJ-53: konfigurierbare Listenanzahl pro Seite (25/50/100/200, Default 25)
|
||||
_, err = s.pool.Exec(ctx, `
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS list_page_size INT NOT NULL DEFAULT 25;
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -180,7 +196,7 @@ func (s *Store) Activate(ctx context.Context, id int64) error {
|
||||
// GetByEmail retrieves a user by email address.
|
||||
func (s *Store) GetByEmail(ctx context.Context, email string) (*User, error) {
|
||||
row := s.pool.QueryRow(ctx,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by FROM users WHERE email = $1`, email,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by, list_page_size FROM users WHERE email = $1`, email,
|
||||
)
|
||||
return scanUser(row)
|
||||
}
|
||||
@@ -199,7 +215,7 @@ func (s *Store) SetPassword(ctx context.Context, id int64, newPassword string) e
|
||||
func (s *Store) GetByID(id int64) (*User, error) {
|
||||
ctx := context.Background()
|
||||
row := s.pool.QueryRow(ctx,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by FROM users WHERE id = $1`, id,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by, list_page_size FROM users WHERE id = $1`, id,
|
||||
)
|
||||
return scanUser(row)
|
||||
}
|
||||
@@ -208,7 +224,7 @@ func (s *Store) GetByID(id int64) (*User, error) {
|
||||
func (s *Store) GetByUsername(username string) (*User, error) {
|
||||
ctx := context.Background()
|
||||
row := s.pool.QueryRow(ctx,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by FROM users WHERE username = $1`, username,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by, list_page_size FROM users WHERE username = $1`, username,
|
||||
)
|
||||
return scanUser(row)
|
||||
}
|
||||
@@ -218,13 +234,13 @@ func (s *Store) GetByUsername(username string) (*User, error) {
|
||||
func (s *Store) VerifyPassword(username, password string) (*User, error) {
|
||||
ctx := context.Background()
|
||||
row := s.pool.QueryRow(ctx,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by, password_hash FROM users WHERE username = $1`,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by, list_page_size, password_hash FROM users WHERE username = $1`,
|
||||
username,
|
||||
)
|
||||
|
||||
var u User
|
||||
var hash string
|
||||
err := row.Scan(&u.ID, &u.Username, &u.Email, &u.Role, &u.Source, &u.Active, &u.CreatedAt, &u.TenantID, &u.TOTPEnabled, &u.TOTPResetAt, &u.TOTPResetBy, &hash)
|
||||
err := row.Scan(&u.ID, &u.Username, &u.Email, &u.Role, &u.Source, &u.Active, &u.CreatedAt, &u.TenantID, &u.TOTPEnabled, &u.TOTPResetAt, &u.TOTPResetBy, &u.ListPageSize, &hash)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errors.New("userstore: user not found")
|
||||
}
|
||||
@@ -296,10 +312,10 @@ func (s *Store) List(role string) ([]*User, error) {
|
||||
|
||||
if role == "" {
|
||||
rows, err = s.pool.Query(ctx,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by FROM users ORDER BY id`)
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by, list_page_size FROM users ORDER BY id`)
|
||||
} else {
|
||||
rows, err = s.pool.Query(ctx,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by FROM users WHERE role = $1 ORDER BY id`, role)
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by, list_page_size FROM users WHERE role = $1 ORDER BY id`, role)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("userstore: list: %w", err)
|
||||
@@ -320,7 +336,7 @@ func (s *Store) List(role string) ([]*User, error) {
|
||||
// ListByTenant returns all users belonging to a specific tenant.
|
||||
func (s *Store) ListByTenant(ctx context.Context, tenantID int64) ([]*User, error) {
|
||||
rows, err := s.pool.Query(ctx,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by FROM users WHERE tenant_id = $1 ORDER BY id`,
|
||||
`SELECT id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by, list_page_size FROM users WHERE tenant_id = $1 ORDER BY id`,
|
||||
tenantID,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -438,10 +454,10 @@ func (s *Store) UpsertLDAPUser(username, email, role string, tenantID *int64) (*
|
||||
active = true,
|
||||
tenant_id = COALESCE($3, tenant_id)
|
||||
WHERE email = $4
|
||||
RETURNING id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by
|
||||
RETURNING id, username, email, role, source, active, created_at, tenant_id, totp_enabled, totp_reset_at, totp_reset_by, list_page_size
|
||||
`, username, role, tenantID, email).Scan(
|
||||
&u.ID, &u.Username, &u.Email, &u.Role, &u.Source, &u.Active,
|
||||
&u.CreatedAt, &u.TenantID, &u.TOTPEnabled, &u.TOTPResetAt, &u.TOTPResetBy,
|
||||
&u.CreatedAt, &u.TenantID, &u.TOTPEnabled, &u.TOTPResetAt, &u.TOTPResetBy, &u.ListPageSize,
|
||||
)
|
||||
if err == nil {
|
||||
return &u, nil
|
||||
@@ -466,7 +482,7 @@ func (s *Store) UpsertLDAPUser(username, email, role string, tenantID *int64) (*
|
||||
|
||||
func scanUser(row pgx.Row) (*User, error) {
|
||||
var u User
|
||||
err := row.Scan(&u.ID, &u.Username, &u.Email, &u.Role, &u.Source, &u.Active, &u.CreatedAt, &u.TenantID, &u.TOTPEnabled, &u.TOTPResetAt, &u.TOTPResetBy)
|
||||
err := row.Scan(&u.ID, &u.Username, &u.Email, &u.Role, &u.Source, &u.Active, &u.CreatedAt, &u.TenantID, &u.TOTPEnabled, &u.TOTPResetAt, &u.TOTPResetBy, &u.ListPageSize)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, fmt.Errorf("userstore: not found")
|
||||
}
|
||||
@@ -478,7 +494,7 @@ func scanUser(row pgx.Row) (*User, error) {
|
||||
|
||||
func scanUserRow(rows pgx.Rows) (*User, error) {
|
||||
var u User
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.Role, &u.Source, &u.Active, &u.CreatedAt, &u.TenantID, &u.TOTPEnabled, &u.TOTPResetAt, &u.TOTPResetBy); err != nil {
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.Role, &u.Source, &u.Active, &u.CreatedAt, &u.TenantID, &u.TOTPEnabled, &u.TOTPResetAt, &u.TOTPResetBy, &u.ListPageSize); err != nil {
|
||||
return nil, fmt.Errorf("userstore: scan row: %w", err)
|
||||
}
|
||||
return &u, nil
|
||||
@@ -555,6 +571,16 @@ func (s *Store) UpdateEmail(ctx context.Context, userID int64, email string) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateListPageSize sets the number of list entries per page for the given user.
|
||||
// Validation of allowed values (25/50/100/200) happens in the API handler.
|
||||
func (s *Store) UpdateListPageSize(ctx context.Context, userID int64, pageSize int) error {
|
||||
_, err := s.pool.Exec(ctx, `UPDATE users SET list_page_size = $1 WHERE id = $2`, pageSize, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("userstore: update list page size: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTOTPSecret returns the encrypted TOTP secret and enabled status for a user.
|
||||
func (s *Store) GetTOTPSecret(ctx context.Context, userID int64) (secret []byte, enabled bool, err error) {
|
||||
err = s.pool.QueryRow(ctx,
|
||||
|
||||
Reference in New Issue
Block a user