deleçao de contas web
This commit is contained in:
@@ -6,6 +6,10 @@
|
|||||||
# Desenvolvimento local: 127.0.0.1
|
# Desenvolvimento local: 127.0.0.1
|
||||||
BP_HOST_LAN_IP=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
|
# debug | info | warn | error
|
||||||
BP_LOG_LEVEL=info
|
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
|
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
|
return err
|
||||||
}
|
}
|
||||||
_, err = s.pool.Exec(ctx, `
|
_, err = s.pool.Exec(ctx, `
|
||||||
INSERT INTO audit_logs (source, actor_user_id, actor_account, target_user_id, event_type, success, details)
|
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, $4, $5, $6, $7::text::jsonb)`,
|
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))
|
string(e.Source), e.ActorUserID, e.ActorAccount, e.TargetUserID, e.EventType, e.Success, string(raw))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ func (s *Store) ListAudit(ctx context.Context, f AuditFilter) ([]domain.AuditLog
|
|||||||
f.Limit = 50
|
f.Limit = 50
|
||||||
}
|
}
|
||||||
rows, err := s.pool.Query(ctx, `
|
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
|
a.target_user_id, tu.display_name, a.event_type, a.success, a.details::text
|
||||||
FROM audit_logs a
|
FROM audit_logs a
|
||||||
LEFT JOIN users au ON au.id = a.actor_user_id
|
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))
|
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) {
|
func (s *Store) CountActiveOwners(ctx context.Context) (int, error) {
|
||||||
var n int
|
var n int
|
||||||
err := s.pool.QueryRow(ctx, `SELECT count(*) FROM web_accounts WHERE role = 'owner' AND disabled_at IS NULL`).Scan(&n)
|
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
|
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.
|
// 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 {
|
func (s *Service) ResetPassword(ctx context.Context, actor Actor, id int64, password string) error {
|
||||||
if err := crypto.ValidatePassword(password); err != nil {
|
if err := crypto.ValidatePassword(password); err != nil {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const (
|
|||||||
EventWebLogout = "web_logout"
|
EventWebLogout = "web_logout"
|
||||||
EventWebAccountCreated = "web_account_created"
|
EventWebAccountCreated = "web_account_created"
|
||||||
EventWebAccountChanged = "web_account_changed"
|
EventWebAccountChanged = "web_account_changed"
|
||||||
|
EventWebAccountDeleted = "web_account_deleted"
|
||||||
EventWebPasswordReset = "web_password_reset"
|
EventWebPasswordReset = "web_password_reset"
|
||||||
EventRetentionApplied = "retention_applied"
|
EventRetentionApplied = "retention_applied"
|
||||||
EventEncryptionKeyRotated = "encryption_key_rotated"
|
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("GET /api/v1/accounts", a.auth(domain.RoleOwner, a.listAccounts))
|
||||||
mux.Handle("POST /api/v1/accounts", a.auth(domain.RoleOwner, a.createAccount))
|
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("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.Handle("POST /api/v1/accounts/{id}/reset-password", a.auth(domain.RoleOwner, a.resetPassword))
|
||||||
|
|
||||||
mux.HandleFunc("/", notFound)
|
mux.HandleFunc("/", notFound)
|
||||||
|
|||||||
@@ -340,6 +340,22 @@ func (a *API) updateAccount(w http.ResponseWriter, r *http.Request) {
|
|||||||
httpx.WriteJSON(w, http.StatusOK, toAccount(account))
|
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) {
|
func (a *API) resetPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
id, ok := pathID(w, r)
|
id, ok := pathID(w, r)
|
||||||
if !ok {
|
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:
|
# 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
|
# 2. Copiar para /etc/systemd/system/botpontos-firewall.service
|
||||||
# 3. systemctl daemon-reload && systemctl enable --now botpontos-firewall
|
# 3. systemctl daemon-reload && systemctl enable --now botpontos-firewall
|
||||||
#
|
#
|
||||||
@@ -8,7 +9,7 @@
|
|||||||
# vai na cadeia DOCKER-USER, aplicada depois que o Docker sobe.
|
# vai na cadeia DOCKER-USER, aplicada depois que o Docker sobe.
|
||||||
|
|
||||||
[Unit]
|
[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
|
After=docker.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ Requires=docker.service
|
|||||||
Type=oneshot
|
Type=oneshot
|
||||||
RemainAfterExit=yes
|
RemainAfterExit=yes
|
||||||
Environment=NPM_IP=IP_DO_NPM
|
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]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
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
|
# 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.
|
# (deploy/botpontos-firewall.service). Em desenvolvimento, use 127.0.0.1.
|
||||||
ports:
|
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
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /tmp
|
- /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.`}
|
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>}
|
{error && <span className="error-text">{error}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export default async function AccountsPage() {
|
|||||||
<p className="muted">
|
<p className="muted">
|
||||||
<strong>owner</strong> gerencia contas; <strong>admin</strong> altera usuários, lembretes, feriados e
|
<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
|
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>
|
</p>
|
||||||
|
|
||||||
<div className="table-wrap">
|
<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