feat(admin): bot management (list, verify, create, delete)
- Add a Bots admin tab: list/search bots with a dedicated read query (users.is_bot, excluded from the accounts list), showing owner and system-vs-user type - Create system bots from the admin via a new bot.create command that reuses the existing bot provisioning flow; the token is shown once - Delete user-created bots via a new bot.delete command backed by a dedicated Postgres DeleteBotAccount (revokes sessions, purges private state, releases username, drops the bots row, tombstones the user); system service bots are rejected - Verified badge toggling reuses the existing set-verified command - All write paths go through the dry-run/confirm + audit command pipeline - Rebuild dist bundle
This commit is contained in:
parent
ad9d535edc
commit
9e45da69ef
22 changed files with 985 additions and 12 deletions
|
|
@ -448,6 +448,42 @@ func (s *Service) ListOwnedBots(ctx context.Context, ownerUserID int64) ([]domai
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// botAccountDeleter is the optional store capability used to permanently delete
|
||||
// a user-created bot. Only the Postgres store implements it, so the memory store
|
||||
// and other BotStore mocks are unaffected.
|
||||
type botAccountDeleter interface {
|
||||
DeleteBotAccount(ctx context.Context, botUserID int64) (domain.User, error)
|
||||
}
|
||||
|
||||
// DeleteBot permanently removes a user-created bot. System service bots are
|
||||
// rejected. Live sessions are dropped and the bot's caches are invalidated so
|
||||
// the deletion is visible immediately. Returns the tombstoned user.
|
||||
func (s *Service) DeleteBot(ctx context.Context, botUserID int64) (domain.User, error) {
|
||||
if s == nil || s.bots == nil || botUserID == 0 {
|
||||
return domain.User{}, domain.ErrBotNotFound
|
||||
}
|
||||
if domain.IsSystemUserID(botUserID) {
|
||||
return domain.User{}, domain.ErrBotNotFound
|
||||
}
|
||||
deleter, ok := s.bots.(botAccountDeleter)
|
||||
if !ok {
|
||||
return domain.User{}, fmt.Errorf("bot deletion is not supported by the configured store")
|
||||
}
|
||||
// Drop live sessions up front so the token stops working even if a caller
|
||||
// races the tombstone; DeleteBotAccount also revokes the authorization rows.
|
||||
if s.hooks != nil {
|
||||
if err := s.hooks.RevokeBotSessions(ctx, botUserID); err != nil {
|
||||
s.log.Warn("revoke bot sessions before delete", zap.Int64("bot_user_id", botUserID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
u, err := deleter.DeleteBotAccount(ctx, botUserID)
|
||||
if err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
s.invalidateBotReadCaches(ctx, botUserID)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// ExportBotToken 返回 bot token;revoke=true 时先轮换 secret 并撤销已登录 session。
|
||||
func (s *Service) ExportBotToken(ctx context.Context, ownerUserID, botUserID int64, revoke bool) (string, error) {
|
||||
if revoke {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue