mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
refactor(core): migrate channel identity and binding across app
Align channel identity and bind flow across backend and app-facing layers, including generated swagger artifacts and package lock updates while excluding docs content changes.
This commit is contained in:
+84
-19
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/memohai/memoh/internal/db/sqlc"
|
||||
)
|
||||
|
||||
// Service provides bot CRUD and membership management.
|
||||
type Service struct {
|
||||
queries *sqlc.Queries
|
||||
logger *slog.Logger
|
||||
@@ -23,14 +24,17 @@ type Service struct {
|
||||
}
|
||||
|
||||
var (
|
||||
ErrBotNotFound = errors.New("bot not found")
|
||||
ErrBotAccessDenied = errors.New("bot access denied")
|
||||
ErrBotNotFound = errors.New("bot not found")
|
||||
ErrBotAccessDenied = errors.New("bot access denied")
|
||||
ErrOwnerUserNotFound = errors.New("owner user not found")
|
||||
)
|
||||
|
||||
// AccessPolicy controls bot access behavior.
|
||||
type AccessPolicy struct {
|
||||
AllowPublicMember bool
|
||||
}
|
||||
|
||||
// NewService creates a new bot service.
|
||||
func NewService(log *slog.Logger, queries *sqlc.Queries) *Service {
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
@@ -46,7 +50,8 @@ func (s *Service) SetContainerLifecycle(lc ContainerLifecycle) {
|
||||
s.containerLifecycle = lc
|
||||
}
|
||||
|
||||
func (s *Service) AuthorizeAccess(ctx context.Context, actorID, botID string, isAdmin bool, policy AccessPolicy) (Bot, error) {
|
||||
// AuthorizeAccess checks whether userID may access the given bot.
|
||||
func (s *Service) AuthorizeAccess(ctx context.Context, userID, botID string, isAdmin bool, policy AccessPolicy) (Bot, error) {
|
||||
if s.queries == nil {
|
||||
return Bot{}, fmt.Errorf("bot queries not configured")
|
||||
}
|
||||
@@ -57,17 +62,18 @@ func (s *Service) AuthorizeAccess(ctx context.Context, actorID, botID string, is
|
||||
}
|
||||
return Bot{}, err
|
||||
}
|
||||
if isAdmin || bot.OwnerUserID == actorID {
|
||||
if isAdmin || bot.OwnerUserID == userID {
|
||||
return bot, nil
|
||||
}
|
||||
if policy.AllowPublicMember && bot.Type == BotTypePublic {
|
||||
if _, err := s.GetMember(ctx, botID, actorID); err == nil {
|
||||
if _, err := s.GetMember(ctx, botID, userID); err == nil {
|
||||
return bot, nil
|
||||
}
|
||||
}
|
||||
return Bot{}, ErrBotAccessDenied
|
||||
}
|
||||
|
||||
// Create creates a new bot owned by owner user.
|
||||
func (s *Service) Create(ctx context.Context, ownerUserID string, req CreateBotRequest) (Bot, error) {
|
||||
if s.queries == nil {
|
||||
return Bot{}, fmt.Errorf("bot queries not configured")
|
||||
@@ -80,6 +86,9 @@ func (s *Service) Create(ctx context.Context, ownerUserID string, req CreateBotR
|
||||
if err != nil {
|
||||
return Bot{}, err
|
||||
}
|
||||
if err := s.ensureUserExists(ctx, ownerUUID); err != nil {
|
||||
return Bot{}, err
|
||||
}
|
||||
normalizedType, err := normalizeBotType(req.Type)
|
||||
if err != nil {
|
||||
return Bot{}, err
|
||||
@@ -127,6 +136,7 @@ func (s *Service) Create(ctx context.Context, ownerUserID string, req CreateBotR
|
||||
return bot, nil
|
||||
}
|
||||
|
||||
// Get returns a bot by its ID.
|
||||
func (s *Service) Get(ctx context.Context, botID string) (Bot, error) {
|
||||
if s.queries == nil {
|
||||
return Bot{}, fmt.Errorf("bot queries not configured")
|
||||
@@ -142,6 +152,7 @@ func (s *Service) Get(ctx context.Context, botID string) (Bot, error) {
|
||||
return toBot(row)
|
||||
}
|
||||
|
||||
// ListByOwner returns bots owned by the given user.
|
||||
func (s *Service) ListByOwner(ctx context.Context, ownerUserID string) ([]Bot, error) {
|
||||
if s.queries == nil {
|
||||
return nil, fmt.Errorf("bot queries not configured")
|
||||
@@ -165,15 +176,16 @@ func (s *Service) ListByOwner(ctx context.Context, ownerUserID string) ([]Bot, e
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListByMember(ctx context.Context, userID string) ([]Bot, error) {
|
||||
// ListByMember returns bots where the user is a member.
|
||||
func (s *Service) ListByMember(ctx context.Context, channelIdentityID string) ([]Bot, error) {
|
||||
if s.queries == nil {
|
||||
return nil, fmt.Errorf("bot queries not configured")
|
||||
}
|
||||
userUUID, err := parseUUID(userID)
|
||||
memberUUID, err := parseUUID(channelIdentityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.queries.ListBotsByMember(ctx, userUUID)
|
||||
rows, err := s.queries.ListBotsByMember(ctx, memberUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -188,12 +200,13 @@ func (s *Service) ListByMember(ctx context.Context, userID string) ([]Bot, error
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListAccessible(ctx context.Context, userID string) ([]Bot, error) {
|
||||
owned, err := s.ListByOwner(ctx, userID)
|
||||
// ListAccessible returns all bots the user can access (owned or member).
|
||||
func (s *Service) ListAccessible(ctx context.Context, channelIdentityID string) ([]Bot, error) {
|
||||
owned, err := s.ListByOwner(ctx, channelIdentityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
members, err := s.ListByMember(ctx, userID)
|
||||
members, err := s.ListByMember(ctx, channelIdentityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -213,6 +226,7 @@ func (s *Service) ListAccessible(ctx context.Context, userID string) ([]Bot, err
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// Update updates bot profile fields.
|
||||
func (s *Service) Update(ctx context.Context, botID string, req UpdateBotRequest) (Bot, error) {
|
||||
if s.queries == nil {
|
||||
return Bot{}, fmt.Errorf("bot queries not configured")
|
||||
@@ -264,6 +278,7 @@ func (s *Service) Update(ctx context.Context, botID string, req UpdateBotRequest
|
||||
return toBot(row)
|
||||
}
|
||||
|
||||
// TransferOwner transfers bot ownership to another user.
|
||||
func (s *Service) TransferOwner(ctx context.Context, botID string, ownerUserID string) (Bot, error) {
|
||||
if s.queries == nil {
|
||||
return Bot{}, fmt.Errorf("bot queries not configured")
|
||||
@@ -276,6 +291,9 @@ func (s *Service) TransferOwner(ctx context.Context, botID string, ownerUserID s
|
||||
if err != nil {
|
||||
return Bot{}, err
|
||||
}
|
||||
if err := s.ensureUserExists(ctx, ownerUUID); err != nil {
|
||||
return Bot{}, err
|
||||
}
|
||||
row, err := s.queries.UpdateBotOwner(ctx, sqlc.UpdateBotOwnerParams{
|
||||
ID: botUUID,
|
||||
OwnerUserID: ownerUUID,
|
||||
@@ -286,6 +304,7 @@ func (s *Service) TransferOwner(ctx context.Context, botID string, ownerUserID s
|
||||
return toBot(row)
|
||||
}
|
||||
|
||||
// Delete removes a bot and its associated resources.
|
||||
func (s *Service) Delete(ctx context.Context, botID string) error {
|
||||
if s.queries == nil {
|
||||
return fmt.Errorf("bot queries not configured")
|
||||
@@ -298,16 +317,34 @@ func (s *Service) Delete(ctx context.Context, botID string) error {
|
||||
return err
|
||||
}
|
||||
if s.containerLifecycle != nil {
|
||||
s.logger.Info("cleaning up bot container before deletion", slog.String("bot_id", botID))
|
||||
if err := s.containerLifecycle.CleanupBotContainer(ctx, botID); err != nil {
|
||||
s.logger.Error("failed to cleanup bot container",
|
||||
slog.String("bot_id", botID),
|
||||
slog.Any("error", err),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
s.logger.Warn("container lifecycle not configured, skipping container cleanup", slog.String("bot_id", botID))
|
||||
}
|
||||
return s.queries.DeleteBotByID(ctx, botUUID)
|
||||
}
|
||||
|
||||
func (s *Service) ensureUserExists(ctx context.Context, userID pgtype.UUID) error {
|
||||
if s.queries == nil {
|
||||
return fmt.Errorf("bot queries not configured")
|
||||
}
|
||||
_, err := s.queries.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrOwnerUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpsertMember creates or updates a bot membership.
|
||||
func (s *Service) UpsertMember(ctx context.Context, botID string, req UpsertMemberRequest) (BotMember, error) {
|
||||
if s.queries == nil {
|
||||
return BotMember{}, fmt.Errorf("bot queries not configured")
|
||||
@@ -316,7 +353,7 @@ func (s *Service) UpsertMember(ctx context.Context, botID string, req UpsertMemb
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
userUUID, err := parseUUID(req.UserID)
|
||||
memberUUID, err := parseUUID(req.UserID)
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
@@ -326,7 +363,7 @@ func (s *Service) UpsertMember(ctx context.Context, botID string, req UpsertMemb
|
||||
}
|
||||
row, err := s.queries.UpsertBotMember(ctx, sqlc.UpsertBotMemberParams{
|
||||
BotID: botUUID,
|
||||
UserID: userUUID,
|
||||
UserID: memberUUID,
|
||||
Role: role,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -335,6 +372,7 @@ func (s *Service) UpsertMember(ctx context.Context, botID string, req UpsertMemb
|
||||
return toBotMember(row), nil
|
||||
}
|
||||
|
||||
// ListMembers returns all members of a bot.
|
||||
func (s *Service) ListMembers(ctx context.Context, botID string) ([]BotMember, error) {
|
||||
if s.queries == nil {
|
||||
return nil, fmt.Errorf("bot queries not configured")
|
||||
@@ -354,7 +392,8 @@ func (s *Service) ListMembers(ctx context.Context, botID string) ([]BotMember, e
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetMember(ctx context.Context, botID, userID string) (BotMember, error) {
|
||||
// GetMember returns a specific bot member.
|
||||
func (s *Service) GetMember(ctx context.Context, botID, channelIdentityID string) (BotMember, error) {
|
||||
if s.queries == nil {
|
||||
return BotMember{}, fmt.Errorf("bot queries not configured")
|
||||
}
|
||||
@@ -362,13 +401,13 @@ func (s *Service) GetMember(ctx context.Context, botID, userID string) (BotMembe
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
userUUID, err := parseUUID(userID)
|
||||
memberUUID, err := parseUUID(channelIdentityID)
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
row, err := s.queries.GetBotMember(ctx, sqlc.GetBotMemberParams{
|
||||
BotID: botUUID,
|
||||
UserID: userUUID,
|
||||
UserID: memberUUID,
|
||||
})
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
@@ -376,7 +415,8 @@ func (s *Service) GetMember(ctx context.Context, botID, userID string) (BotMembe
|
||||
return toBotMember(row), nil
|
||||
}
|
||||
|
||||
func (s *Service) DeleteMember(ctx context.Context, botID, userID string) error {
|
||||
// DeleteMember removes a member from a bot.
|
||||
func (s *Service) DeleteMember(ctx context.Context, botID, channelIdentityID string) error {
|
||||
if s.queries == nil {
|
||||
return fmt.Errorf("bot queries not configured")
|
||||
}
|
||||
@@ -384,18 +424,43 @@ func (s *Service) DeleteMember(ctx context.Context, botID, userID string) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userUUID, err := parseUUID(userID)
|
||||
memberUUID, err := parseUUID(channelIdentityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.queries.DeleteBotMember(ctx, sqlc.DeleteBotMemberParams{
|
||||
BotID: botUUID,
|
||||
UserID: userUUID,
|
||||
UserID: memberUUID,
|
||||
})
|
||||
}
|
||||
|
||||
// UpsertMemberSimple creates or updates a bot membership with a direct channel identity ID and role.
|
||||
// This satisfies the router.BotMemberService interface.
|
||||
func (s *Service) UpsertMemberSimple(ctx context.Context, botID, channelIdentityID, role string) error {
|
||||
_, err := s.UpsertMember(ctx, botID, UpsertMemberRequest{
|
||||
UserID: channelIdentityID,
|
||||
Role: role,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// IsMember checks if a user is a member of a bot.
|
||||
func (s *Service) IsMember(ctx context.Context, botID, channelIdentityID string) (bool, error) {
|
||||
_, err := s.GetMember(ctx, botID, channelIdentityID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func normalizeBotType(raw string) (string, error) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||
if normalized == "" {
|
||||
return BotTypePersonal, nil
|
||||
}
|
||||
switch normalized {
|
||||
case BotTypePersonal, BotTypePublic:
|
||||
return normalized, nil
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Bot represents a bot entity.
|
||||
type Bot struct {
|
||||
ID string `json:"id"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
@@ -17,6 +18,7 @@ type Bot struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BotMember represents a bot membership record.
|
||||
type BotMember struct {
|
||||
BotID string `json:"bot_id"`
|
||||
UserID string `json:"user_id"`
|
||||
@@ -24,6 +26,7 @@ type BotMember struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// CreateBotRequest is the input for creating a bot.
|
||||
type CreateBotRequest struct {
|
||||
Type string `json:"type"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
@@ -32,6 +35,7 @@ type CreateBotRequest struct {
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateBotRequest is the input for updating a bot.
|
||||
type UpdateBotRequest struct {
|
||||
DisplayName *string `json:"display_name,omitempty"`
|
||||
AvatarURL *string `json:"avatar_url,omitempty"`
|
||||
@@ -39,19 +43,23 @@ type UpdateBotRequest struct {
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// TransferBotRequest is the input for transferring bot ownership.
|
||||
type TransferBotRequest struct {
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
}
|
||||
|
||||
// UpsertMemberRequest is the input for upserting a bot member.
|
||||
type UpsertMemberRequest struct {
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}
|
||||
|
||||
// ListBotsResponse wraps a list of bots.
|
||||
type ListBotsResponse struct {
|
||||
Items []Bot `json:"items"`
|
||||
}
|
||||
|
||||
// ListMembersResponse wraps a list of bot members.
|
||||
type ListMembersResponse struct {
|
||||
Items []BotMember `json:"items"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user