fix
This commit is contained in:
@@ -34,7 +34,11 @@ func ClientError(err error) (msg string, ok bool) {
|
||||
errors.Is(err, ErrEmailRequired),
|
||||
errors.Is(err, ErrSyntheticEmail),
|
||||
errors.Is(err, ErrNotEligibleSetPassword),
|
||||
errors.Is(err, ErrEmailMismatch):
|
||||
errors.Is(err, ErrEmailMismatch),
|
||||
errors.Is(err, ErrNotCompanyOwner),
|
||||
errors.Is(err, ErrCannotRemoveOwner),
|
||||
errors.Is(err, ErrTransferSelf),
|
||||
errors.Is(err, ErrOwnerRequired):
|
||||
return err.Error(), true
|
||||
default:
|
||||
return "", false
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotCompanyOwner = errors.New("company owner required")
|
||||
ErrCannotRemoveOwner = errors.New("transfer ownership before removing the company owner")
|
||||
ErrTransferSelf = errors.New("user is already the company owner")
|
||||
ErrOwnerRequired = errors.New("new owner must be an active company member")
|
||||
)
|
||||
|
||||
// CompanyOwnerID returns the billing representative for the company, if set.
|
||||
func (s *Service) CompanyOwnerID(ctx context.Context, companyID uuid.UUID) (uuid.UUID, bool, error) {
|
||||
var owner *uuid.UUID
|
||||
err := s.Pool.QueryRow(ctx, `SELECT owner_user_id FROM companies WHERE id = $1`, companyID).Scan(&owner)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return uuid.Nil, false, ErrCompanyNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return uuid.Nil, false, err
|
||||
}
|
||||
if owner == nil || *owner == uuid.Nil {
|
||||
return uuid.Nil, false, nil
|
||||
}
|
||||
return *owner, true, nil
|
||||
}
|
||||
|
||||
// IsCompanyOwner reports whether userID is the company's owner_user_id.
|
||||
func (s *Service) IsCompanyOwner(ctx context.Context, companyID, userID uuid.UUID) (bool, error) {
|
||||
ownerID, ok, err := s.CompanyOwnerID(ctx, companyID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ok && ownerID == userID, nil
|
||||
}
|
||||
|
||||
// TransferOwnership sets a new company owner. The target must be an active member.
|
||||
// The new owner is promoted to membership admin so they retain team powers.
|
||||
func (s *Service) TransferOwnership(ctx context.Context, companyID, newOwnerID uuid.UUID) error {
|
||||
tx, err := s.Pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var current *uuid.UUID
|
||||
err = tx.QueryRow(ctx, `SELECT owner_user_id FROM companies WHERE id = $1 FOR UPDATE`, companyID).Scan(¤t)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrCompanyNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current != nil && *current == newOwnerID {
|
||||
return ErrTransferSelf
|
||||
}
|
||||
|
||||
var status string
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT status FROM memberships
|
||||
WHERE company_id = $1 AND user_id = $2`, companyID, newOwnerID).Scan(&status)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrOwnerRequired
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != "active" {
|
||||
return ErrOwnerRequired
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, `
|
||||
UPDATE memberships
|
||||
SET role = 'admin', status = 'active', updated_at = now()
|
||||
WHERE company_id = $1 AND user_id = $2`, companyID, newOwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tag, err := tx.Exec(ctx, `
|
||||
UPDATE companies SET owner_user_id = $2, updated_at = now() WHERE id = $1`, companyID, newOwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return ErrCompanyNotFound
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOwnershipErrorClientFacing(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, err := range []error{
|
||||
ErrNotCompanyOwner,
|
||||
ErrCannotRemoveOwner,
|
||||
ErrTransferSelf,
|
||||
ErrOwnerRequired,
|
||||
} {
|
||||
msg, ok := ClientError(err)
|
||||
if !ok || msg == "" {
|
||||
t.Fatalf("expected client error for %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,8 @@ func (s *Service) Register(ctx context.Context, in RegisterInput) (LoginResult,
|
||||
|
||||
var companyID uuid.UUID
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO companies (name) VALUES ($1) RETURNING id`, strings.TrimSpace(in.CompanyName)).Scan(&companyID)
|
||||
INSERT INTO companies (name, owner_user_id) VALUES ($1, $2) RETURNING id`,
|
||||
strings.TrimSpace(in.CompanyName), userID).Scan(&companyID)
|
||||
if err != nil {
|
||||
return LoginResult{}, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user