deleçao de contas web

This commit is contained in:
2026-10-30 23:25:38 -03:00
parent 61fde19133
commit f0560b1be4
16 changed files with 1250 additions and 10 deletions

View File

@@ -18,8 +18,8 @@ func (s *Store) InsertAudit(ctx context.Context, e domain.AuditEntry) error {
return err
}
_, err = s.pool.Exec(ctx, `
INSERT INTO audit_logs (source, actor_user_id, actor_account, target_user_id, event_type, success, details)
VALUES ($1::text::audit_source, $2, $3, $4, $5, $6, $7::text::jsonb)`,
INSERT INTO audit_logs (source, actor_user_id, actor_account, actor_account_name, target_user_id, event_type, success, details)
VALUES ($1::text::audit_source, $2, $3, (SELECT username::text FROM web_accounts WHERE id = $3), $4, $5, $6, $7::text::jsonb)`,
string(e.Source), e.ActorUserID, e.ActorAccount, e.TargetUserID, e.EventType, e.Success, string(raw))
return err
}
@@ -41,7 +41,7 @@ func (s *Store) ListAudit(ctx context.Context, f AuditFilter) ([]domain.AuditLog
f.Limit = 50
}
rows, err := s.pool.Query(ctx, `
SELECT a.id, a.occurred_at, a.source, a.actor_user_id, au.display_name, a.actor_account, wa.username::text,
SELECT a.id, a.occurred_at, a.source, a.actor_user_id, au.display_name, a.actor_account, COALESCE(wa.username::text, a.actor_account_name),
a.target_user_id, tu.display_name, a.event_type, a.success, a.details::text
FROM audit_logs a
LEFT JOIN users au ON au.id = a.actor_user_id

View File

@@ -82,6 +82,11 @@ func (s *Store) SetWebAccountPassword(ctx context.Context, id int64, passwordHas
WHERE id = $1`, id, passwordHash))
}
// DeleteWebAccount apaga a conta; as sessões vão junto e a auditoria mantém o nome (migration 00003).
func (s *Store) DeleteWebAccount(ctx context.Context, id int64) error {
return expectOne(s.pool.Exec(ctx, `DELETE FROM web_accounts WHERE id = $1`, id))
}
func (s *Store) CountActiveOwners(ctx context.Context) (int, error) {
var n int
err := s.pool.QueryRow(ctx, `SELECT count(*) FROM web_accounts WHERE role = 'owner' AND disabled_at IS NULL`).Scan(&n)

View File

@@ -246,6 +246,29 @@ func (s *Service) UpdateAccount(ctx context.Context, actor Actor, id int64, role
return nil
}
// DeleteAccount exclui a conta do painel, mantendo sempre um owner ativo (9.6).
func (s *Service) DeleteAccount(ctx context.Context, actor Actor, id int64) error {
account, err := s.store.GetWebAccount(ctx, id)
if err != nil {
return err
}
if account.Role == domain.RoleOwner && !account.IsDisabled() {
owners, err := s.store.CountActiveOwners(ctx)
if err != nil {
return err
}
if owners <= 1 {
return domain.ErrLastOwner
}
}
if err := s.store.DeleteWebAccount(ctx, id); err != nil {
return err
}
s.audit(ctx, actor, account.UserID, domain.EventWebAccountDeleted, true,
map[string]any{"account_id": id, "username": account.Username, "role": string(account.Role)})
return nil
}
// ResetPassword troca a senha e encerra todas as sessões da conta.
func (s *Service) ResetPassword(ctx context.Context, actor Actor, id int64, password string) error {
if err := crypto.ValidatePassword(password); err != nil {

View File

@@ -49,6 +49,7 @@ const (
EventWebLogout = "web_logout"
EventWebAccountCreated = "web_account_created"
EventWebAccountChanged = "web_account_changed"
EventWebAccountDeleted = "web_account_deleted"
EventWebPasswordReset = "web_password_reset"
EventRetentionApplied = "retention_applied"
EventEncryptionKeyRotated = "encryption_key_rotated"

View File

@@ -71,6 +71,7 @@ func NewHandler(db Pinger, svc *app.Service, cfg config.Config, logger *slog.Log
mux.Handle("GET /api/v1/accounts", a.auth(domain.RoleOwner, a.listAccounts))
mux.Handle("POST /api/v1/accounts", a.auth(domain.RoleOwner, a.createAccount))
mux.Handle("PATCH /api/v1/accounts/{id}", a.auth(domain.RoleOwner, a.updateAccount))
mux.Handle("DELETE /api/v1/accounts/{id}", a.auth(domain.RoleOwner, a.deleteAccount))
mux.Handle("POST /api/v1/accounts/{id}/reset-password", a.auth(domain.RoleOwner, a.resetPassword))
mux.HandleFunc("/", notFound)

View File

@@ -340,6 +340,22 @@ func (a *API) updateAccount(w http.ResponseWriter, r *http.Request) {
httpx.WriteJSON(w, http.StatusOK, toAccount(account))
}
func (a *API) deleteAccount(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
if id == sessionFrom(r).AccountID {
a.writeError(w, domain.InvalidState("você não pode excluir a própria conta"))
return
}
if err := a.svc.DeleteAccount(r.Context(), actorOf(r), id); err != nil {
a.writeError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (a *API) resetPassword(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {