225 lines
6.0 KiB
Go
225 lines
6.0 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/query"
|
|
"github.com/ultisuite/ulti-backend/internal/securityaudit"
|
|
)
|
|
|
|
var ErrNotFound = errors.New("not found")
|
|
|
|
type Service struct {
|
|
db *pgxpool.Pool
|
|
audit *securityaudit.Logger
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewService(db *pgxpool.Pool, audit *securityaudit.Logger) *Service {
|
|
return &Service{
|
|
db: db,
|
|
audit: audit,
|
|
logger: slog.Default().With("component", "admin-service"),
|
|
}
|
|
}
|
|
|
|
type UsersList struct {
|
|
Users []map[string]any `json:"users"`
|
|
Pagination query.PaginationMeta `json:"pagination,omitempty"`
|
|
}
|
|
|
|
func (s *Service) ListUsers(ctx context.Context, params query.ListParams) (UsersList, error) {
|
|
var total int64
|
|
if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM users`).Scan(&total); err != nil {
|
|
return UsersList{}, err
|
|
}
|
|
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, external_id, email, name, created_at FROM users
|
|
ORDER BY created_at DESC
|
|
LIMIT $1 OFFSET $2
|
|
`, params.Limit(), params.Offset())
|
|
if err != nil {
|
|
return UsersList{}, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
users := make([]map[string]any, 0)
|
|
for rows.Next() {
|
|
var id, extID, email, name string
|
|
var createdAt time.Time
|
|
if err := rows.Scan(&id, &extID, &email, &name, &createdAt); err != nil {
|
|
return UsersList{}, err
|
|
}
|
|
users = append(users, map[string]any{
|
|
"id": id, "external_id": extID, "email": email, "name": name, "created_at": createdAt,
|
|
})
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return UsersList{}, err
|
|
}
|
|
|
|
return UsersList{
|
|
Users: users,
|
|
Pagination: params.Meta(&total),
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) GetUser(ctx context.Context, userID string) (map[string]any, error) {
|
|
var id, extID, email, name string
|
|
var createdAt time.Time
|
|
err := s.db.QueryRow(ctx, `
|
|
SELECT id, external_id, email, name, created_at FROM users WHERE id = $1
|
|
`, userID).Scan(&id, &extID, &email, &name, &createdAt)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var mailCount int64
|
|
if err := s.db.QueryRow(ctx, `
|
|
SELECT COALESCE(COUNT(*), 0) FROM messages m
|
|
JOIN mail_accounts ma ON m.account_id = ma.id WHERE ma.user_id = $1
|
|
`, userID).Scan(&mailCount); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var maxStorage int64
|
|
if err := s.db.QueryRow(ctx, `
|
|
SELECT COALESCE((preferences->>'max_storage')::bigint, 5368709120)
|
|
FROM settings WHERE user_id = $1
|
|
`, userID).Scan(&maxStorage); err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]any{
|
|
"id": id, "external_id": extID, "email": email, "name": name,
|
|
"created_at": createdAt,
|
|
"quota": map[string]any{
|
|
"mail_count": mailCount,
|
|
"storage_used_bytes": int64(0),
|
|
"max_storage_bytes": maxStorage,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) SetQuota(ctx context.Context, actorSub, userID string, maxStorageBytes int64) error {
|
|
_, err := s.db.Exec(ctx, `
|
|
INSERT INTO settings (user_id, preferences)
|
|
VALUES ($1, jsonb_build_object('max_storage', $2::text))
|
|
ON CONFLICT (user_id) DO UPDATE
|
|
SET preferences = settings.preferences || jsonb_build_object('max_storage', $2::text),
|
|
updated_at = NOW()
|
|
`, userID, maxStorageBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.logAudit(ctx, actorSub, "set_quota", map[string]any{
|
|
"target_user": userID, "max_storage_bytes": maxStorageBytes,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) DeleteUser(ctx context.Context, actorSub, userID string) error {
|
|
result, err := s.db.Exec(ctx, `DELETE FROM users WHERE id = $1`, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if result.RowsAffected() == 0 {
|
|
return ErrNotFound
|
|
}
|
|
s.logAudit(ctx, actorSub, "delete_user", map[string]any{"target_user": userID})
|
|
return nil
|
|
}
|
|
|
|
type AuditLogsList struct {
|
|
Logs []map[string]any `json:"logs"`
|
|
Pagination query.PaginationMeta `json:"pagination,omitempty"`
|
|
}
|
|
|
|
func (s *Service) ListAuditLogs(ctx context.Context, params query.ListParams) (AuditLogsList, error) {
|
|
var total int64
|
|
if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM audit_logs`).Scan(&total); err != nil {
|
|
return AuditLogsList{}, err
|
|
}
|
|
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT id, actor, action, details, created_at
|
|
FROM audit_logs ORDER BY created_at DESC
|
|
LIMIT $1 OFFSET $2
|
|
`, params.Limit(), params.Offset())
|
|
if err != nil {
|
|
return AuditLogsList{}, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
logs := make([]map[string]any, 0)
|
|
for rows.Next() {
|
|
var id, actor, action string
|
|
var details []byte
|
|
var createdAt time.Time
|
|
if err := rows.Scan(&id, &actor, &action, &details, &createdAt); err != nil {
|
|
return AuditLogsList{}, err
|
|
}
|
|
logs = append(logs, map[string]any{
|
|
"id": id, "actor": actor, "action": action,
|
|
"details": json.RawMessage(details), "created_at": createdAt,
|
|
})
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return AuditLogsList{}, err
|
|
}
|
|
|
|
return AuditLogsList{
|
|
Logs: logs,
|
|
Pagination: params.Meta(&total),
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) GetStats(ctx context.Context) (map[string]any, error) {
|
|
stats := map[string]any{}
|
|
|
|
var userCount, mailCount, accountCount int64
|
|
if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM users`).Scan(&userCount); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM messages`).Scan(&mailCount); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM mail_accounts`).Scan(&accountCount); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats["total_users"] = userCount
|
|
stats["total_messages"] = mailCount
|
|
stats["total_accounts"] = accountCount
|
|
return stats, nil
|
|
}
|
|
|
|
func (s *Service) logAudit(ctx context.Context, actor, action string, details map[string]any) {
|
|
if s.audit == nil {
|
|
return
|
|
}
|
|
if action == "delete_user" {
|
|
s.audit.Log(ctx, actor, securityaudit.ActionCriticalDeletion, map[string]any{
|
|
"target": "user",
|
|
"target_user": details["target_user"],
|
|
"admin_action": true,
|
|
})
|
|
return
|
|
}
|
|
s.audit.Log(ctx, actor, securityaudit.ActionAdminAction, map[string]any{
|
|
"action": action,
|
|
"details": details,
|
|
})
|
|
}
|