@@ -17,7 +17,7 @@ internal sealed class UserApplicationService : IUserApplicationService
1717 private readonly IUserProfileStore _profileStore ;
1818 private readonly IUserIdentifierStore _identifierStore ;
1919 private readonly IEnumerable < IUserLifecycleIntegration > _integrations ;
20- private readonly UAuthUserIdentifierOptions _identifierOptions ;
20+ private readonly UAuthServerOptions _options ;
2121 private readonly IClock _clock ;
2222
2323 public UserApplicationService (
@@ -34,7 +34,7 @@ public UserApplicationService(
3434 _profileStore = profileStore ;
3535 _identifierStore = identifierStore ;
3636 _integrations = integrations ;
37- _identifierOptions = options . Value . UserIdentifiers ;
37+ _options = options . Value ;
3838 _clock = clock ;
3939 }
4040
@@ -229,6 +229,17 @@ public async Task AddUserIdentifierAsync(AccessContext context, AddUserIdentifie
229229 EnsureVerificationRequirements ( request . Type , isVerified : false ) ;
230230 }
231231
232+ var mustBeUnique = _options . LoginIdentifiers . EnforceGlobalUniquenessForAllIdentifiers ||
233+ ( request . IsPrimary && _options . LoginIdentifiers . AllowedTypes . Contains ( request . Type ) ) ;
234+
235+ if ( mustBeUnique )
236+ {
237+ var exists = await _identifierStore . ExistsAsync ( context . ResourceTenant , request . Type , request . Value , innerCt ) ;
238+
239+ if ( exists )
240+ throw new UAuthIdentifierConflictException ( "identifier_already_exists" ) ;
241+ }
242+
232243 await _identifierStore . CreateAsync ( context . ResourceTenant ,
233244 new UserIdentifier
234245 {
@@ -256,14 +267,25 @@ public async Task UpdateUserIdentifierAsync(AccessContext context, UpdateUserIde
256267
257268 EnsureOverrideAllowed ( context ) ;
258269
259- if ( identifier . Type == UserIdentifierType . Username && ! _identifierOptions . AllowUsernameChange )
270+ if ( identifier . Type == UserIdentifierType . Username && ! _options . UserIdentifiers . AllowUsernameChange )
260271 {
261272 throw new UAuthIdentifierValidationException ( "username_change_not_allowed" ) ;
262273 }
263274
264275 if ( string . Equals ( identifier . Value , request . NewValue , StringComparison . Ordinal ) )
265276 throw new UAuthIdentifierValidationException ( "identifier_value_unchanged" ) ;
266277
278+ var mustBeUnique = _options . LoginIdentifiers . EnforceGlobalUniquenessForAllIdentifiers ||
279+ ( identifier . IsPrimary && _options . LoginIdentifiers . AllowedTypes . Contains ( identifier . Type ) ) ;
280+
281+ if ( mustBeUnique )
282+ {
283+ var existing = await _identifierStore . GetAsync ( identifier . Tenant , identifier . Type , request . NewValue , innerCt ) ;
284+
285+ if ( existing is not null && existing . Id != identifier . Id && ! existing . IsDeleted )
286+ throw new UAuthIdentifierConflictException ( "identifier_already_exists" ) ;
287+ }
288+
267289 await _identifierStore . UpdateValueAsync ( identifier . Id , request . NewValue , _clock . UtcNow , innerCt ) ;
268290 } ) ;
269291
@@ -282,6 +304,20 @@ public async Task SetPrimaryUserIdentifierAsync(AccessContext context, SetPrimar
282304
283305 EnsureVerificationRequirements ( identifier . Type , identifier . IsVerified ) ;
284306
307+ var identifiers = await _identifierStore . GetByUserAsync ( identifier . Tenant , identifier . UserKey , innerCt ) ;
308+ var activeIdentifiers = identifiers . Where ( i => ! i . IsDeleted ) . ToList ( ) ;
309+
310+ if ( identifier . IsPrimary )
311+ throw new UAuthIdentifierValidationException ( "identifier_already_primary" ) ;
312+
313+ if ( _options . LoginIdentifiers . EnforceGlobalUniquenessForAllIdentifiers )
314+ {
315+ var exists = await _identifierStore . ExistsAsync ( identifier . Tenant , identifier . Type , identifier . Value , innerCt ) ;
316+
317+ if ( exists )
318+ throw new UAuthIdentifierConflictException ( "identifier_already_exists" ) ;
319+ }
320+
285321 await _identifierStore . SetPrimaryAsync ( request . IdentifierId , innerCt ) ;
286322 } ) ;
287323
@@ -303,14 +339,18 @@ public async Task UnsetPrimaryUserIdentifierAsync(AccessContext context, UnsetPr
303339
304340 var identifiers = await _identifierStore . GetByUserAsync ( identifier . Tenant , identifier . UserKey , innerCt ) ;
305341
306- var otherLoginIdentifiers = identifiers
307- . Where ( i => ! i . IsDeleted &&
308- IsLoginIdentifier ( i . Type ) &&
309- i . Id != identifier . Id )
310- . ToList ( ) ;
342+ var activeIdentifiers = identifiers . Where ( i => ! i . IsDeleted ) . ToList ( ) ;
343+
344+ var primaryLoginIdentifiers = activeIdentifiers
345+ . Where ( i =>
346+ i . IsPrimary &&
347+ _options . LoginIdentifiers . AllowedTypes . Contains ( i . Type ) )
348+ . ToList ( ) ;
311349
312- if ( otherLoginIdentifiers . Count == 0 )
313- throw new UAuthIdentifierConflictException ( "cannot_unset_last_primary_login_identifier" ) ;
350+ if ( primaryLoginIdentifiers . Count == 1 && primaryLoginIdentifiers [ 0 ] . Id == identifier . Id )
351+ {
352+ throw new UAuthIdentifierConflictException ( "cannot_unset_last_login_identifier" ) ;
353+ }
314354
315355 await _identifierStore . UnsetPrimaryAsync ( request . IdentifierId , innerCt ) ;
316356 } ) ;
@@ -345,8 +385,7 @@ public async Task DeleteUserIdentifierAsync(AccessContext context, DeleteUserIde
345385 if ( identifier . IsPrimary )
346386 throw new UAuthIdentifierValidationException ( "cannot_delete_primary_identifier" ) ;
347387
348- if ( _identifierOptions . RequireUsernameIdentifier &&
349- identifier . Type == UserIdentifierType . Username )
388+ if ( _options . UserIdentifiers . RequireUsernameIdentifier && identifier . Type == UserIdentifierType . Username )
350389 {
351390 var activeUsernames = identifiers
352391 . Where ( i => ! i . IsDeleted && i . Type == UserIdentifierType . Username )
@@ -419,35 +458,35 @@ private void EnsureMultipleIdentifierAllowed(UserIdentifierType type, IReadOnlyL
419458 if ( ! hasSameType )
420459 return ;
421460
422- if ( type == UserIdentifierType . Username && ! _identifierOptions . AllowMultipleUsernames )
461+ if ( type == UserIdentifierType . Username && ! _options . UserIdentifiers . AllowMultipleUsernames )
423462 throw new InvalidOperationException ( "multiple_usernames_not_allowed" ) ;
424463
425- if ( type == UserIdentifierType . Email && ! _identifierOptions . AllowMultipleEmail )
464+ if ( type == UserIdentifierType . Email && ! _options . UserIdentifiers . AllowMultipleEmail )
426465 throw new InvalidOperationException ( "multiple_emails_not_allowed" ) ;
427466
428- if ( type == UserIdentifierType . Phone && ! _identifierOptions . AllowMultiplePhone )
467+ if ( type == UserIdentifierType . Phone && ! _options . UserIdentifiers . AllowMultiplePhone )
429468 throw new InvalidOperationException ( "multiple_phones_not_allowed" ) ;
430469 }
431470
432471 private void EnsureVerificationRequirements ( UserIdentifierType type , bool isVerified )
433472 {
434- if ( type == UserIdentifierType . Email && _identifierOptions . RequireEmailVerification && ! isVerified )
473+ if ( type == UserIdentifierType . Email && _options . UserIdentifiers . RequireEmailVerification && ! isVerified )
435474 {
436475 throw new InvalidOperationException ( "email_verification_required" ) ;
437476 }
438477
439- if ( type == UserIdentifierType . Phone && _identifierOptions . RequirePhoneVerification && ! isVerified )
478+ if ( type == UserIdentifierType . Phone && _options . UserIdentifiers . RequirePhoneVerification && ! isVerified )
440479 {
441480 throw new InvalidOperationException ( "phone_verification_required" ) ;
442481 }
443482 }
444483
445484 private void EnsureOverrideAllowed ( AccessContext context )
446485 {
447- if ( context . IsSelfAction && ! _identifierOptions . AllowUserOverride )
486+ if ( context . IsSelfAction && ! _options . UserIdentifiers . AllowUserOverride )
448487 throw new InvalidOperationException ( "user_override_not_allowed" ) ;
449488
450- if ( ! context . IsSelfAction && ! _identifierOptions . AllowAdminOverride )
489+ if ( ! context . IsSelfAction && ! _options . UserIdentifiers . AllowAdminOverride )
451490 throw new InvalidOperationException ( "admin_override_not_allowed" ) ;
452491 }
453492
0 commit comments