deleçao de contas web
This commit is contained in:
@@ -6,6 +6,10 @@
|
||||
# Desenvolvimento local: 127.0.0.1
|
||||
BP_HOST_LAN_IP=127.0.0.1
|
||||
|
||||
# Porta do painel no servidor (padrão 3000). Troque se outra aplicação já usa a 3000;
|
||||
# use a mesma no firewall (deploy/botpontos-firewall.service) e no Forward Port do NPM.
|
||||
BP_WEB_PORT=3000
|
||||
|
||||
# debug | info | warn | error
|
||||
BP_LOG_LEVEL=info
|
||||
|
||||
|
||||
35
backend/db/migrations/00003_web_account_delete.sql
Normal file
35
backend/db/migrations/00003_web_account_delete.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- Permite excluir contas do painel sem perder a auditoria.
|
||||
-- Sessões da conta são apagadas junto; auditoria e configurações perdem só o
|
||||
-- vínculo, e a auditoria guarda o nome da conta em actor_account_name.
|
||||
|
||||
-- +goose Up
|
||||
ALTER TABLE audit_logs ADD COLUMN actor_account_name text;
|
||||
UPDATE audit_logs a SET actor_account_name = wa.username::text
|
||||
FROM web_accounts wa WHERE wa.id = a.actor_account;
|
||||
|
||||
ALTER TABLE web_sessions DROP CONSTRAINT web_sessions_account_id_fkey;
|
||||
ALTER TABLE web_sessions ADD CONSTRAINT web_sessions_account_id_fkey
|
||||
FOREIGN KEY (account_id) REFERENCES web_accounts(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE audit_logs DROP CONSTRAINT audit_logs_actor_account_fkey;
|
||||
ALTER TABLE audit_logs ADD CONSTRAINT audit_logs_actor_account_fkey
|
||||
FOREIGN KEY (actor_account) REFERENCES web_accounts(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE app_settings DROP CONSTRAINT app_settings_updated_by_account_fkey;
|
||||
ALTER TABLE app_settings ADD CONSTRAINT app_settings_updated_by_account_fkey
|
||||
FOREIGN KEY (updated_by_account) REFERENCES web_accounts(id) ON DELETE SET NULL;
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE app_settings DROP CONSTRAINT app_settings_updated_by_account_fkey;
|
||||
ALTER TABLE app_settings ADD CONSTRAINT app_settings_updated_by_account_fkey
|
||||
FOREIGN KEY (updated_by_account) REFERENCES web_accounts(id);
|
||||
|
||||
ALTER TABLE audit_logs DROP CONSTRAINT audit_logs_actor_account_fkey;
|
||||
ALTER TABLE audit_logs ADD CONSTRAINT audit_logs_actor_account_fkey
|
||||
FOREIGN KEY (actor_account) REFERENCES web_accounts(id);
|
||||
|
||||
ALTER TABLE web_sessions DROP CONSTRAINT web_sessions_account_id_fkey;
|
||||
ALTER TABLE web_sessions ADD CONSTRAINT web_sessions_account_id_fkey
|
||||
FOREIGN KEY (account_id) REFERENCES web_accounts(id);
|
||||
|
||||
ALTER TABLE audit_logs DROP COLUMN actor_account_name;
|
||||
@@ -1,3 +1,22 @@
|
||||
module botpontos
|
||||
|
||||
go 1.25
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
github.com/pressly/goose/v3 v3.27.0
|
||||
golang.org/x/crypto v0.55.0
|
||||
golang.org/x/term v0.45.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.3 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/sethvargo/go-retry v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
)
|
||||
|
||||
60
backend/go.sum
Normal file
60
backend/go.sum
Normal file
@@ -0,0 +1,60 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
|
||||
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.3 h1:f8HoREISb3l5eO9coPgK7U3hKLhWGokOFVcThl5jrao=
|
||||
github.com/jackc/puddle/v2 v2.2.3/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM=
|
||||
github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/sethvargo/go-retry v0.5.0 h1:JYfFKmVJ8ER06GReN1efzc5cm52BrVsFVzmcHiKJwfY=
|
||||
github.com/sethvargo/go-retry v0.5.0/go.mod h1:tvsjdKG6xfiCx4LSiUZ06kcv38xvdVQwv8R6/VnnVWg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
|
||||
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Restringe a porta 3000 (painel Next.js) ao IP do Nginx Proxy Manager.
|
||||
# Restringe a porta do painel Next.js (3000 ou BP_WEB_PORT) ao IP do Nginx Proxy Manager.
|
||||
# ARQUITETURA.md, seção 11.4. Instalação no servidor Debian:
|
||||
# 1. Trocar IP_DO_NPM abaixo pelo IP real do NPM.
|
||||
# 1. Trocar IP_DO_NPM abaixo pelo IP real do NPM e, se BP_WEB_PORT no .env
|
||||
# não for 3000, trocar WEB_PORT pela mesma porta.
|
||||
# 2. Copiar para /etc/systemd/system/botpontos-firewall.service
|
||||
# 3. systemctl daemon-reload && systemctl enable --now botpontos-firewall
|
||||
#
|
||||
@@ -8,7 +9,7 @@
|
||||
# vai na cadeia DOCKER-USER, aplicada depois que o Docker sobe.
|
||||
|
||||
[Unit]
|
||||
Description=Restringe a porta 3000 do Bot de Pontos ao NPM
|
||||
Description=Restringe a porta do painel do Bot de Pontos ao NPM
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
@@ -16,7 +17,8 @@ Requires=docker.service
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
Environment=NPM_IP=IP_DO_NPM
|
||||
ExecStart=/bin/sh -c 'iptables -C DOCKER-USER -p tcp -m conntrack --ctorigdstport 3000 ! -s $NPM_IP -j DROP 2>/dev/null || iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport 3000 ! -s $NPM_IP -j DROP'
|
||||
Environment=WEB_PORT=3000
|
||||
ExecStart=/bin/sh -c 'iptables -C DOCKER-USER -p tcp -m conntrack --ctorigdstport $WEB_PORT ! -s $NPM_IP -j DROP 2>/dev/null || iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport $WEB_PORT ! -s $NPM_IP -j DROP'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -121,7 +121,7 @@ services:
|
||||
# máquina) aponta para cá, e o firewall libera a 3000 só para o IP do NPM
|
||||
# (deploy/botpontos-firewall.service). Em desenvolvimento, use 127.0.0.1.
|
||||
ports:
|
||||
- "${BP_HOST_LAN_IP:?defina BP_HOST_LAN_IP no arquivo .env}:3000:3000"
|
||||
- "${BP_HOST_LAN_IP:?defina BP_HOST_LAN_IP no arquivo .env}:${BP_WEB_PORT:-3000}:3000"
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
|
||||
@@ -158,6 +158,15 @@ export function AccountActions({ account, isSelf, users }: { account: Account; i
|
||||
confirm={`Desativar a conta ${account.username}? As sessões abertas serão encerradas.`}
|
||||
/>
|
||||
))}
|
||||
{!isSelf && (
|
||||
<ActionButton
|
||||
path={`/accounts/${account.id}`}
|
||||
method="DELETE"
|
||||
label="Excluir"
|
||||
variant="danger"
|
||||
confirm={`Excluir a conta ${account.username} de vez? Não dá para desfazer; a auditoria mantém o nome da conta.`}
|
||||
/>
|
||||
)}
|
||||
{error && <span className="error-text">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,8 @@ export default async function AccountsPage() {
|
||||
<p className="muted">
|
||||
<strong>owner</strong> gerencia contas; <strong>admin</strong> altera usuários, lembretes, feriados e
|
||||
configurações; <strong>leitura</strong> só consulta. Uma conta de leitura ligada a um usuário do Telegram enxerga e
|
||||
altera somente os dados desse usuário. Sempre existe pelo menos um owner ativo.
|
||||
altera somente os dados desse usuário. Sempre existe pelo menos um owner ativo. <strong>Desativar</strong> bloqueia o
|
||||
acesso e pode ser revertido; <strong>Excluir</strong> apaga a conta de vez.
|
||||
</p>
|
||||
|
||||
<div className="table-wrap">
|
||||
|
||||
1063
web/package-lock.json
generated
Normal file
1063
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
web/tsconfig.tsbuildinfo
Normal file
1
web/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user