Files
kandev/apps/backend/internal/auth/service_setup.go
T
João Salavisa 481359ddb2 feat(auth): organization units, tenancy, roles and scopes for team access (#2919)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 13:40:43 +00:00

77 lines
2.5 KiB
Go

package auth
import (
"context"
"go.uber.org/zap"
"github.com/kandev/kandev/internal/auth/store"
usermodels "github.com/kandev/kandev/internal/user/models"
userstore "github.com/kandev/kandev/internal/user/store"
)
// Setup completes the first-run wizard: it promotes the pre-auth default-user
// row into the admin account (preserving all existing user settings), claims
// unowned workspaces/secrets for that admin, persists the enabled toggle, and
// mints the first session.
//
// Ordering is deliberate: backfills run first (harmless if repeated), the
// local identity insert is the commit point that flips ModeSetup → ModeEnabled,
// and a partial failure before it leaves the instance safely in setup mode.
func (s *Service) Setup(ctx context.Context, email, password, displayName, userAgent, ip string) (*usermodels.User, string, error) {
s.setupMu.Lock()
defer s.setupMu.Unlock()
if s.Mode() != ModeSetup {
return nil, "", ErrSetupNotAvailable
}
email = normalizeEmail(email)
if err := validateEmailPassword(email, password); err != nil {
return nil, "", err
}
adminID := userstore.DefaultUserID
for _, backfill := range s.backfills {
if err := backfill(ctx, adminID); err != nil {
return nil, "", err
}
}
user, err := s.users.UpdateUserProfile(ctx, adminID, email, displayName, usermodels.RoleAdmin)
if err != nil {
return nil, "", err
}
hash, err := HashPassword(password)
if err != nil {
return nil, "", err
}
// Tenant placement must complete before the identity commit point below.
// If it fails, setup remains retryable instead of returning an org-less,
// non-operator session that cannot repair itself.
if s.adminCreated != nil {
if err := s.adminCreated(ctx, adminID); err != nil {
return nil, "", err
}
}
identity := &store.LoginIdentity{
UserID: adminID,
Provider: store.ProviderLocal,
Subject: email,
PasswordHash: hash,
}
if err := s.store.CreateIdentity(ctx, identity); err != nil {
// A concurrent setup already created the admin identity.
return nil, "", ErrSetupNotAvailable
}
// An admin identity now exists, so refreshMode derives Enabled from the
// (still-on) features.auth flag — no separate toggle to persist.
if err := s.refreshMode(ctx); err != nil {
return nil, "", err
}
token, err := s.createSession(ctx, adminID, userAgent, ip)
if err != nil {
return nil, "", err
}
if s.log != nil {
s.log.Info("authentication setup completed", zap.String("admin_email", email))
}
return user, token, nil
}