This commit is contained in:
2026-08-24 04:03:44 +02:00
parent 8a690a0464
commit 7bafd7a322
9 changed files with 391 additions and 53 deletions
+52 -45
View File
@@ -59,6 +59,8 @@ type LoginResult struct {
User User `json:"user"`
CompanyID uuid.UUID `json:"company_id"`
Companies []Company `json:"companies"`
// ProvisionCompanyIDs are newly created tenants that still need Free-plan billing setup.
ProvisionCompanyIDs []uuid.UUID `json:"-"`
}
func (s *Service) Register(ctx context.Context, in RegisterInput) (LoginResult, error) {
@@ -100,31 +102,12 @@ func (s *Service) Register(ctx context.Context, in RegisterInput) (LoginResult,
return LoginResult{}, err
}
var companyID uuid.UUID
err = tx.QueryRow(ctx, `
INSERT INTO companies (name, owner_user_id) VALUES ($1, $2) RETURNING id`,
strings.TrimSpace(in.CompanyName), userID).Scan(&companyID)
companyID, created, err := ensureOwnedCompanyTx(ctx, tx, userID, strings.TrimSpace(in.CompanyName))
if err != nil {
return LoginResult{}, err
}
_, err = tx.Exec(ctx, `
INSERT INTO memberships (company_id, user_id, role, status)
VALUES ($1, $2, 'admin', 'active')`, companyID, userID)
if err != nil {
return LoginResult{}, err
}
_, err = tx.Exec(ctx, `
INSERT INTO company_settings (company_id) VALUES ($1)
ON CONFLICT DO NOTHING`, companyID)
if err != nil {
return LoginResult{}, err
}
_, err = tx.Exec(ctx, `
INSERT INTO credit_balances (company_id) VALUES ($1)
ON CONFLICT DO NOTHING`, companyID)
if err != nil {
return LoginResult{}, err
if !created {
return LoginResult{}, errors.New("owned company unexpectedly already existed during register")
}
if err := tx.Commit(ctx); err != nil {
@@ -136,9 +119,10 @@ func (s *Service) Register(ctx context.Context, in RegisterInput) (LoginResult,
return LoginResult{}, err
}
return LoginResult{
User: user,
CompanyID: companyID,
Companies: []Company{{ID: companyID, Name: strings.TrimSpace(in.CompanyName)}},
User: user,
CompanyID: companyID,
Companies: []Company{{ID: companyID, Name: strings.TrimSpace(in.CompanyName)}},
ProvisionCompanyIDs: []uuid.UUID{companyID},
}, nil
}
@@ -228,20 +212,20 @@ func (s *Service) AcceptInvite(ctx context.Context, token, password, name string
defer tx.Rollback(ctx)
var userID uuid.UUID
var existingHash string
var existingHash *string
var mustSet bool
err = tx.QueryRow(ctx, `
SELECT id, password_hash, must_set_password FROM users WHERE email = $1`,
strings.ToLower(email)).Scan(&userID, &existingHash, &mustSet)
displayName := strings.TrimSpace(name)
if errors.Is(err, pgx.ErrNoRows) {
hash, herr := HashPassword(password)
if herr != nil {
return LoginResult{}, herr
}
var n *string
if strings.TrimSpace(name) != "" {
nn := strings.TrimSpace(name)
n = &nn
if displayName != "" {
n = &displayName
}
err = tx.QueryRow(ctx, `
INSERT INTO users (email, name, password_hash, must_set_password)
@@ -251,26 +235,45 @@ func (s *Service) AcceptInvite(ctx context.Context, token, password, name string
}
} else if err != nil {
return LoginResult{}, err
} else if mustSet {
// Migration / first-password invites may set a password once.
hash, herr := HashPassword(password)
if herr != nil {
return LoginResult{}, herr
}
_, err = tx.Exec(ctx, `
UPDATE users SET password_hash = $2, must_set_password = false, updated_at = now()
WHERE id = $1 AND must_set_password = true`, userID, hash)
if err != nil {
} else {
var activeMemberships int64
if err := tx.QueryRow(ctx, `
SELECT count(*) FROM memberships
WHERE user_id = $1 AND status = 'active'`, userID).Scan(&activeMemberships); err != nil {
return LoginResult{}, err
}
} else {
// Existing accounts keep their password; invitee must prove ownership.
ok, verr := VerifyPassword(existingHash, password)
if verr != nil || !ok {
return LoginResult{}, ErrInvalidCredentials
// New password when must_set, or orphan recovery (removed from all companies; no SMTP reset).
if mustSet || activeMemberships == 0 {
hash, herr := HashPassword(password)
if herr != nil {
return LoginResult{}, herr
}
_, err = tx.Exec(ctx, `
UPDATE users SET password_hash = $2, must_set_password = false, updated_at = now()
WHERE id = $1`, userID, hash)
if err != nil {
return LoginResult{}, err
}
} else {
// Active elsewhere: keep password; invitee must prove ownership.
hash := ""
if existingHash != nil {
hash = *existingHash
}
ok, verr := VerifyPassword(hash, password)
if verr != nil || !ok {
return LoginResult{}, ErrInvalidCredentials
}
}
}
// Always ensure a personal owned workspace; invite also joins the inviting company.
ownedName := defaultOwnedCompanyName(email, displayName)
ownedID, ownedCreated, err := ensureOwnedCompanyTx(ctx, tx, userID, ownedName)
if err != nil {
return LoginResult{}, err
}
_, err = tx.Exec(ctx, `
INSERT INTO memberships (company_id, user_id, role, status)
VALUES ($1, $2, $3, 'active')
@@ -305,7 +308,11 @@ func (s *Service) AcceptInvite(ctx context.Context, token, password, name string
if err != nil {
return LoginResult{}, err
}
return LoginResult{User: user, CompanyID: companyID, Companies: companies}, nil
res := LoginResult{User: user, CompanyID: companyID, Companies: companies}
if ownedCreated {
res.ProvisionCompanyIDs = []uuid.UUID{ownedID}
}
return res, nil
}
func (s *Service) SetPassword(ctx context.Context, userID uuid.UUID, password string) error {