Add admin-create registration + live email-verified flag
Registration now goes through POST /api/auth/register, which admin-creates the Kratos identity (unverified email, NO auto-sent code). Kratos self-service registration never returns the verification flow id, so the client could never submit the user's code to the right flow; admin creation lets the client own a single verification flow instead. Also surface the live Kratos verified flag and fix Apple audience + team IDs. - kratos.Client.CreateIdentity via admin API; ErrIdentityExists / ErrInvalidCredentials - AuthService.Register + AuthHandler.Register + public POST /api/auth/register/ - CurrentUser overrides stale user_profile.verified with the live Kratos flag; UserRepository.MarkVerified mirrors it back - configmap: additional_id_token_audiences allows the .dev bundle id_token - fix Apple/APNs team id V3PF3M6B6U -> X86BR9WTLD in .env.example + dev init Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,33 @@ func (s *AuthService) SetKratosClient(k *kratos.Client) {
|
||||
s.kratos = k
|
||||
}
|
||||
|
||||
// Register admin-creates a Kratos identity for a new password account with an
|
||||
// unverified email and no auto-sent verification email (see
|
||||
// kratos.Client.CreateIdentity for the rationale). The client logs in
|
||||
// immediately afterward to obtain a session, then drives email verification
|
||||
// explicitly. No local auth_user row is created here — the KratosAuth
|
||||
// middleware lazily provisions it on the first authenticated request.
|
||||
func (s *AuthService) Register(ctx context.Context, req *requests.RegisterRequest) error {
|
||||
if s.kratos == nil {
|
||||
return apperrors.Internal(errors.New("identity service unavailable"))
|
||||
}
|
||||
_, err := s.kratos.CreateIdentity(ctx, req.Email, req.FirstName, req.LastName, req.Password)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, kratos.ErrIdentityExists):
|
||||
return apperrors.Conflict("error.email_already_taken")
|
||||
default:
|
||||
var invalid *kratos.ErrInvalidCredentials
|
||||
if errors.As(err, &invalid) {
|
||||
return apperrors.BadRequest("error.password_complexity")
|
||||
}
|
||||
log.Error().Err(err).Msg("Kratos identity creation failed")
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentUser returns the current authenticated user with profile.
|
||||
func (s *AuthService) GetCurrentUser(ctx context.Context, userID uint) (*responses.CurrentUserResponse, error) {
|
||||
user, err := s.userRepo.WithContext(ctx).FindByIDWithProfile(userID)
|
||||
@@ -65,6 +92,20 @@ func (s *AuthService) GetCurrentUser(ctx context.Context, userID uint) (*respons
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// MarkUserVerified flips the local user_profile.verified and auth_user.verified
|
||||
// mirrors to true. Called opportunistically from CurrentUser when the live
|
||||
// Kratos email-verified flag is true but the local mirror is stale (the user
|
||||
// completed verification after the local row was provisioned). Best-effort:
|
||||
// the canonical truth lives in Kratos's verifiable_addresses, so a failure
|
||||
// here only means a brief stale read on background-only queries.
|
||||
func (s *AuthService) MarkUserVerified(ctx context.Context, userID uint) error {
|
||||
if err := s.userRepo.WithContext(ctx).MarkVerified(userID); err != nil {
|
||||
log.Warn().Err(err).Uint("user_id", userID).Msg("Failed to mirror verified flag from Kratos")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProfile updates a user's profile fields.
|
||||
func (s *AuthService) UpdateProfile(ctx context.Context, userID uint, req *requests.UpdateProfileRequest) (*responses.CurrentUserResponse, error) {
|
||||
user, err := s.userRepo.WithContext(ctx).FindByID(userID)
|
||||
|
||||
Reference in New Issue
Block a user