Skip to content

Commit 026469d

Browse files
committed
Sample Improvement (Part 2/2)
1 parent edfcb21 commit 026469d

File tree

5 files changed

+68
-25
lines changed

5 files changed

+68
-25
lines changed

samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using MudExtensions.Services;
1818
using Scalar.AspNetCore;
1919
using CodeBeam.UltimateAuth.Sample.BlazorServer.Infrastructure;
20+
using CodeBeam.UltimateAuth.Users.Contracts;
2021

2122
var builder = WebApplication.CreateBuilder(args);
2223

@@ -46,6 +47,7 @@
4647
o.Login.MaxFailedAttempts = 2;
4748
o.Login.LockoutDuration = TimeSpan.FromSeconds(10);
4849
o.UserIdentifiers.AllowMultipleUsernames = true;
50+
o.LoginIdentifiers.AllowedTypes = new HashSet<UserIdentifierType>() { UserIdentifierType.Username, UserIdentifierType.Email };
4951
})
5052
.AddUltimateAuthUsersInMemory()
5153
.AddUltimateAuthUsersReference()

src/CodeBeam.UltimateAuth.Server/Extensions/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ private static IServiceCollection AddUltimateAuthServerInternal(this IServiceCol
252252

253253
services.Configure<UAuthLoginIdentifierOptions>(opt =>
254254
{
255-
opt.AllowedBuiltIns = new HashSet<UserIdentifierType>
255+
opt.AllowedTypes = new HashSet<UserIdentifierType>
256256
{
257257
UserIdentifierType.Username,
258258
UserIdentifierType.Email

src/CodeBeam.UltimateAuth.Server/Options/UAuthLoginIdentifierOptions.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace CodeBeam.UltimateAuth.Server.Options;
44
public sealed class UAuthLoginIdentifierOptions
55
{
6-
public ISet<UserIdentifierType> AllowedBuiltIns { get; set; } =
6+
public ISet<UserIdentifierType> AllowedTypes { get; set; } =
77
new HashSet<UserIdentifierType>
88
{
99
UserIdentifierType.Username,
@@ -17,12 +17,15 @@ public sealed class UAuthLoginIdentifierOptions
1717
public bool EnableCustomResolvers { get; set; } = true;
1818
public bool CustomResolversFirst { get; set; } = true;
1919

20+
public bool EnforceGlobalUniquenessForAllIdentifiers { get; set; } = false;
21+
2022
internal UAuthLoginIdentifierOptions Clone() => new()
2123
{
22-
AllowedBuiltIns = new HashSet<UserIdentifierType>(AllowedBuiltIns),
24+
AllowedTypes = new HashSet<UserIdentifierType>(AllowedTypes),
2325
RequireVerificationForEmail = RequireVerificationForEmail,
2426
RequireVerificationForPhone = RequireVerificationForPhone,
2527
EnableCustomResolvers = EnableCustomResolvers,
2628
CustomResolversFirst = CustomResolversFirst,
29+
EnforceGlobalUniquenessForAllIdentifiers = EnforceGlobalUniquenessForAllIdentifiers
2730
};
2831
}

src/users/CodeBeam.UltimateAuth.Users.Reference/Infrastructure/LoginIdentifierResolver.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public LoginIdentifierResolver(
3939

4040
var builtInType = DetectBuiltInType(normalized);
4141

42-
if (!_options.AllowedBuiltIns.Contains(builtInType))
42+
if (!_options.AllowedTypes.Contains(builtInType))
4343
{
4444
if (_options.EnableCustomResolvers && !_options.CustomResolversFirst)
4545
return await TryCustomAsync(tenant, normalized, ct);
@@ -112,8 +112,7 @@ public LoginIdentifierResolver(
112112
return null;
113113
}
114114

115-
private static string Normalize(string identifier)
116-
=> identifier.Trim();
115+
private static string Normalize(string identifier) => identifier.Trim();
117116

118117
private static UserIdentifierType DetectBuiltInType(string normalized)
119118
{

src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)