From 83286fa913e5680a88e349507837d2408832f31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sat, 31 Oct 2026 21:46:27 +0300 Subject: [PATCH 1/3] feat(users): enforce profile field and metadata safety limits --- docs/content/plugin-domains/authorization.md | 2 +- docs/content/plugin-domains/credentials.md | 2 +- docs/content/plugin-domains/policies.md | 2 +- .../profile-validation-limits.md | 110 +++++++++++++ .../wwwroot/docs/docs-index.json | 13 +- .../docs/plugin-domains/authorization.json | 2 +- .../docs/plugin-domains/credentials.json | 2 +- .../wwwroot/docs/plugin-domains/policies.json | 2 +- .../profile-validation-limits.json | 44 +++++ .../Components/Dialogs/ProfileDialog.razor | 64 ++++---- .../uauth.db-shm | Bin 32768 -> 32768 bytes .../uauth.db-wal | Bin 881712 -> 885832 bytes .../Validator/UserProfileSafetyValidation.cs | 78 +++++++++ .../Validator/UserProfileValidator.cs | 6 +- .../Dtos/UserProfileLimits.cs | 24 +++ .../Data/UAuthUsersModelBuilder.cs | 11 +- .../Services/UserApplicationService.cs | 30 ++++ .../Users/UserApplicationServiceTests.cs | 152 +++++++++++++++++- .../Users/UserProfileSafetyValidationTests.cs | 135 ++++++++++++++++ 19 files changed, 636 insertions(+), 43 deletions(-) create mode 100644 docs/content/plugin-domains/profile-validation-limits.md create mode 100644 docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/profile-validation-limits.json create mode 100644 src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileSafetyValidation.cs create mode 100644 src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileLimits.cs create mode 100644 tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileSafetyValidationTests.cs diff --git a/docs/content/plugin-domains/authorization.md b/docs/content/plugin-domains/authorization.md index f5369dbb..8cb2dc89 100644 --- a/docs/content/plugin-domains/authorization.md +++ b/docs/content/plugin-domains/authorization.md @@ -1,6 +1,6 @@ --- title: Authorization -order: 4 +order: 5 group: plugin-domains --- diff --git a/docs/content/plugin-domains/credentials.md b/docs/content/plugin-domains/credentials.md index c9ad7f59..0858f034 100644 --- a/docs/content/plugin-domains/credentials.md +++ b/docs/content/plugin-domains/credentials.md @@ -1,6 +1,6 @@ --- title: Credentials -order: 3 +order: 4 group: plugin-domains --- diff --git a/docs/content/plugin-domains/policies.md b/docs/content/plugin-domains/policies.md index 63c660aa..47ceff9c 100644 --- a/docs/content/plugin-domains/policies.md +++ b/docs/content/plugin-domains/policies.md @@ -1,6 +1,6 @@ --- title: Policies & Access Control -order: 5 +order: 6 group: plugin-domains --- diff --git a/docs/content/plugin-domains/profile-validation-limits.md b/docs/content/plugin-domains/profile-validation-limits.md new file mode 100644 index 00000000..edaa3b70 --- /dev/null +++ b/docs/content/plugin-domains/profile-validation-limits.md @@ -0,0 +1,110 @@ +--- +title: Profile Validation & Limits +order: 3 +group: plugin-domains +--- + +# Profile Validation & Limits + +UltimateAuth enforces framework-level safety limits on standard user profile fields and metadata. + +These limits protect applications from excessively large profile values, unnecessary storage consumption, and resource exhaustion. + +They apply consistently to the reference Users domain, regardless of whether InMemory or Entity Framework Core persistence is used. + +## Profile Field Limits + +The following maximum lengths are enforced: + +| Field | Maximum characters | +|---|---:| +| FirstName | 128 | +| LastName | 128 | +| DisplayName | 256 | +| Gender | 128 | +| Bio | 4,096 | +| Language | 64 | +| TimeZone | 128 | +| Culture | 64 | + +All fields are optional unless an application introduces additional requirements. + +Limits are defined centrally in `UserProfileLimits` and cannot be increased through application configuration. + +## Metadata Limits + +User profiles support optional key-value metadata. + +To prevent unbounded metadata storage, UltimateAuth enforces the following limits: + +| Constraint | Maximum | +|---|---:| +| Metadata entries | 32 | +| Key length | 128 characters | +| Value length | 2,048 characters | +| Total metadata size | 8,192 UTF-8 bytes | + +The total size includes metadata keys and values. + +These limits apply to the complete metadata collection, not individually to each entry. + +## Validation Enforcement + +Profile safety limits are enforced during: + +- User creation. +- Additional profile creation. +- Profile updates. +- Profile cloning. + +Validation occurs before invalid data is persisted. + +Framework-level safety validation is independent of `IUserProfileValidator`, ensuring that replacing the application validator cannot bypass these limits. + +The same restrictions apply to both self-service and administrative operations. + +## Custom Validation + +Applications can implement `IUserProfileValidator` to introduce additional validation rules. + +For example, an application may require a display name, restrict accepted languages, or enforce a shorter biography. + +Custom validation can be stricter than the framework limits but cannot override the built-in safety boundaries. + +The recommended approach is to use `IUserProfileValidator` for application-specific rules while relying on UltimateAuth for baseline safety validation. + +## Validation Errors + +When a profile exceeds the supported limits, UltimateAuth rejects the operation with a validation error. + +Common error codes include: + +| Error code | Meaning | +|---|---| +| `profile_field_too_long` | A standard profile field exceeds its maximum length. | +| `profile_metadata_entries_exceeded` | The metadata collection contains too many entries. | +| `profile_metadata_key_too_long` | A metadata key exceeds its maximum length. | +| `profile_metadata_value_too_long` | A metadata value exceeds its maximum length. | +| `profile_metadata_too_large` | The combined metadata size exceeds the allowed limit. | + +Applications should use the error `Code` rather than relying on exception message text. + +## Persistence Considerations + +The reference Entity Framework Core implementation configures profile column lengths using the same framework limits. + +Existing databases may require a migration when upgrading to a version that introduces these constraints. + +Before applying restrictive database changes, review existing profile records for values exceeding the new limits. + +The InMemory implementation uses the same application-level validation rules. + +## Security Considerations + +Profile limits are a defense-in-depth measure. + +They do not replace HTTP request size limits, rate limiting, or application-specific validation. + +Applications should configure these additional protections according to their deployment requirements. + +For an overview of user identity and profile management, see [Users Domain](/docs/plugin-domains/users). \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json index a2d1d0fc..ab0acd2b 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/docs-index.json @@ -132,24 +132,31 @@ "Group": "plugin-domains", "GroupOrder": 5 }, + { + "Title": "Profile Validation \u0026 Limits", + "Slug": "plugin-domains/profile-validation-limits", + "Order": 3, + "Group": "plugin-domains", + "GroupOrder": 5 + }, { "Title": "Credentials", "Slug": "plugin-domains/credentials", - "Order": 3, + "Order": 4, "Group": "plugin-domains", "GroupOrder": 5 }, { "Title": "Authorization", "Slug": "plugin-domains/authorization", - "Order": 4, + "Order": 5, "Group": "plugin-domains", "GroupOrder": 5 }, { "Title": "Policies \u0026 Access Control", "Slug": "plugin-domains/policies", - "Order": 5, + "Order": 6, "Group": "plugin-domains", "GroupOrder": 5 }, diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/authorization.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/authorization.json index a8d97bf2..43d6f13d 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/authorization.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/authorization.json @@ -154,6 +154,6 @@ "Level": 0 } ], - "SourceHash": "E8E9478440BD9758F067833BE9E8597AEE9A25F0870CB9416A4390F8674A8D07", + "SourceHash": "D39724293797A9792171FDEB55D849FA5D3845BF2E1B2413F10EC86A39B92123", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/credentials.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/credentials.json index 6e4d4195..5b33614a 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/credentials.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/credentials.json @@ -109,6 +109,6 @@ "Level": 0 } ], - "SourceHash": "F0E1E773A693F7405C202D30CDB37AF1154B0EA97BCE385786157DF1B6E20D24", + "SourceHash": "8618A9ED0DF478D9B489CBC7C51420145259EB1DBA1956617ECF3600D163BF2F", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json index 0251d021..02ccc7b1 100644 --- a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/policies.json @@ -159,6 +159,6 @@ "Level": 0 } ], - "SourceHash": "05FA98F9ED56849BAB9A4450CA6B85F273091D8A4F1422F7238421375E5DCA62", + "SourceHash": "DFAC894A519ADC862CC8E8B5450A17DC934DAAAB2004C631BFD9594CC7AA37E4", "BuilderVersion": "1" } \ No newline at end of file diff --git a/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/profile-validation-limits.json b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/profile-validation-limits.json new file mode 100644 index 00000000..3750624d --- /dev/null +++ b/docs/website/CodeBeam.UltimateAuth.Docs.Wasm/CodeBeam.UltimateAuth.Docs.Wasm.Client/wwwroot/docs/plugin-domains/profile-validation-limits.json @@ -0,0 +1,44 @@ +{ + "Slug": "plugin-domains/profile-validation-limits", + "Title": "Profile Validation \u0026 Limits", + "Html": "\n\u003Cp\u003EUltimateAuth enforces framework-level safety limits on standard user profile fields and metadata.\u003C/p\u003E\n\u003Cp\u003EThese limits protect applications from excessively large profile values, unnecessary storage consumption, and resource exhaustion.\u003C/p\u003E\n\u003Cp\u003EThey apply consistently to the reference Users domain, regardless of whether InMemory or Entity Framework Core persistence is used.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022profile-field-limits\u0022\u003EProfile Field Limits\u003C/h2\u003E\n\u003Cp\u003EThe following maximum lengths are enforced:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EField\u003C/th\u003E\n\u003Cth style=\u0022text-align: right;\u0022\u003EMaximum characters\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EFirstName\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E128\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ELastName\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E128\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EDisplayName\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E256\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EGender\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E128\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EBio\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E4,096\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ELanguage\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E64\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETimeZone\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E128\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECulture\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E64\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EAll fields are optional unless an application introduces additional requirements.\u003C/p\u003E\n\u003Cp\u003ELimits are defined centrally in \u003Ccode\u003EUserProfileLimits\u003C/code\u003E and cannot be increased through application configuration.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022metadata-limits\u0022\u003EMetadata Limits\u003C/h2\u003E\n\u003Cp\u003EUser profiles support optional key-value metadata.\u003C/p\u003E\n\u003Cp\u003ETo prevent unbounded metadata storage, UltimateAuth enforces the following limits:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EConstraint\u003C/th\u003E\n\u003Cth style=\u0022text-align: right;\u0022\u003EMaximum\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMetadata entries\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E32\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EKey length\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E128 characters\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EValue length\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E2,048 characters\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ETotal metadata size\u003C/td\u003E\n\u003Ctd style=\u0022text-align: right;\u0022\u003E8,192 UTF-8 bytes\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EThe total size includes metadata keys and values.\u003C/p\u003E\n\u003Cp\u003EThese limits apply to the complete metadata collection, not individually to each entry.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-enforcement\u0022\u003EValidation Enforcement\u003C/h2\u003E\n\u003Cp\u003EProfile safety limits are enforced during:\u003C/p\u003E\n\u003Cul\u003E\n\u003Cli\u003EUser creation.\u003C/li\u003E\n\u003Cli\u003EAdditional profile creation.\u003C/li\u003E\n\u003Cli\u003EProfile updates.\u003C/li\u003E\n\u003Cli\u003EProfile cloning.\u003C/li\u003E\n\u003C/ul\u003E\n\u003Cp\u003EValidation occurs before invalid data is persisted.\u003C/p\u003E\n\u003Cp\u003EFramework-level safety validation is independent of \u003Ccode\u003EIUserProfileValidator\u003C/code\u003E, ensuring that replacing the application validator cannot bypass these limits.\u003C/p\u003E\n\u003Cp\u003EThe same restrictions apply to both self-service and administrative operations.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022custom-validation\u0022\u003ECustom Validation\u003C/h2\u003E\n\u003Cp\u003EApplications can implement \u003Ccode\u003EIUserProfileValidator\u003C/code\u003E to introduce additional validation rules.\u003C/p\u003E\n\u003Cp\u003EFor example, an application may require a display name, restrict accepted languages, or enforce a shorter biography.\u003C/p\u003E\n\u003Cp\u003ECustom validation can be stricter than the framework limits but cannot override the built-in safety boundaries.\u003C/p\u003E\n\u003Cp\u003EThe recommended approach is to use \u003Ccode\u003EIUserProfileValidator\u003C/code\u003E for application-specific rules while relying on UltimateAuth for baseline safety validation.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022validation-errors\u0022\u003EValidation Errors\u003C/h2\u003E\n\u003Cp\u003EWhen a profile exceeds the supported limits, UltimateAuth rejects the operation with a validation error.\u003C/p\u003E\n\u003Cp\u003ECommon error codes include:\u003C/p\u003E\n\u003Cdiv class=\u0022docs-table-wrapper\u0022\u003E\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003EError code\u003C/th\u003E\n\u003Cth\u003EMeaning\u003C/th\u003E\n\u003C/tr\u003E\n\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eprofile_field_too_long\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA standard profile field exceeds its maximum length.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eprofile_metadata_entries_exceeded\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe metadata collection contains too many entries.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eprofile_metadata_key_too_long\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA metadata key exceeds its maximum length.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eprofile_metadata_value_too_long\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EA metadata value exceeds its maximum length.\u003C/td\u003E\n\u003C/tr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E\u003Ccode\u003Eprofile_metadata_too_large\u003C/code\u003E\u003C/td\u003E\n\u003Ctd\u003EThe combined metadata size exceeds the allowed limit.\u003C/td\u003E\n\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\u003C/div\u003E\n\u003Cp\u003EApplications should use the error \u003Ccode\u003ECode\u003C/code\u003E rather than relying on exception message text.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022persistence-considerations\u0022\u003EPersistence Considerations\u003C/h2\u003E\n\u003Cp\u003EThe reference Entity Framework Core implementation configures profile column lengths using the same framework limits.\u003C/p\u003E\n\u003Cp\u003EExisting databases may require a migration when upgrading to a version that introduces these constraints.\u003C/p\u003E\n\u003Cp\u003EBefore applying restrictive database changes, review existing profile records for values exceeding the new limits.\u003C/p\u003E\n\u003Cp\u003EThe InMemory implementation uses the same application-level validation rules.\u003C/p\u003E\n\u003Ch2 class=\u0022mud-scrollspy-section\u0022 id=\u0022security-considerations\u0022\u003ESecurity Considerations\u003C/h2\u003E\n\u003Cp\u003EProfile limits are a defense-in-depth measure.\u003C/p\u003E\n\u003Cp\u003EThey do not replace HTTP request size limits, rate limiting, or application-specific validation.\u003C/p\u003E\n\u003Cp\u003EApplications should configure these additional protections according to their deployment requirements.\u003C/p\u003E\n\u003Cp\u003EFor an overview of user identity and profile management, see \u003Ca href=\u0022/docs/plugin-domains/users\u0022\u003EUsers Domain\u003C/a\u003E.\u003C/p\u003E\n", + "Headings": [ + { + "Id": "profile-field-limits", + "Text": "Profile Field Limits", + "Level": 0 + }, + { + "Id": "metadata-limits", + "Text": "Metadata Limits", + "Level": 0 + }, + { + "Id": "validation-enforcement", + "Text": "Validation Enforcement", + "Level": 0 + }, + { + "Id": "custom-validation", + "Text": "Custom Validation", + "Level": 0 + }, + { + "Id": "validation-errors", + "Text": "Validation Errors", + "Level": 0 + }, + { + "Id": "persistence-considerations", + "Text": "Persistence Considerations", + "Level": 0 + }, + { + "Id": "security-considerations", + "Text": "Security Considerations", + "Level": 0 + } + ], + "SourceHash": "5F102B8246218017DC2F9A89EB40C588773A804D661CE3C97625DA31C0461590", + "BuilderVersion": "1" +} \ No newline at end of file diff --git a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ProfileDialog.razor b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ProfileDialog.razor index 140615ad..87dbc04d 100644 --- a/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ProfileDialog.razor +++ b/samples/CodeBeam.UltimateAuth.Sample.Shared/Components/Dialogs/ProfileDialog.razor @@ -1,4 +1,5 @@ -@inject IUAuthClient UAuthClient +@using CodeBeam.UltimateAuth.Users.Contracts +@inject IUAuthClient UAuthClient @inject ISnackbar Snackbar @@ -11,9 +12,7 @@
Edit Profile - - Update identity details and personal preferences. - + Update identity details and personal preferences.
@@ -21,8 +20,7 @@ @if (_isLoading) { - + } else if (_loadError is not null) { @@ -36,9 +34,7 @@
@if (PreviewMode) { - + Preview mode is active. Profile changes cannot be saved. } @@ -51,9 +47,7 @@
- +
Basic Information How this identity appears throughout the application. @@ -65,12 +59,18 @@ Label="First Name" Variant="Variant.Outlined" Margin="Margin.Dense" + MaxLength="@UserProfileLimits.FirstName" + Counter="@UserProfileLimits.FirstName" + Immediate="true" Disabled="@_isSaving" />
@@ -78,6 +78,9 @@ Label="Display Name" Variant="Variant.Outlined" Margin="Margin.Dense" + MaxLength="@UserProfileLimits.DisplayName" + Counter="@UserProfileLimits.DisplayName" + Immediate="true" Disabled="@_isSaving" />
@@ -87,9 +90,7 @@
- +
Personal Details Optional information associated with this profile. @@ -107,6 +108,9 @@ Label="Gender" Variant="Variant.Outlined" Margin="Margin.Dense" + MaxLength="@UserProfileLimits.Gender" + Counter="@UserProfileLimits.Gender" + Immediate="true" Disabled="@_isSaving" />
@@ -115,6 +119,9 @@ Lines="3" Variant="Variant.Outlined" Margin="Margin.Dense" + MaxLength="@UserProfileLimits.Bio" + Counter="@UserProfileLimits.Bio" + Immediate="true" Disabled="@_isSaving" />
@@ -124,9 +131,7 @@
- +
Localization Language, time zone and culture preferences. @@ -139,6 +144,9 @@ Placeholder="e.g. en" Variant="Variant.Outlined" Margin="Margin.Dense" + MaxLength="@UserProfileLimits.Language" + Counter="@UserProfileLimits.Language" + Immediate="true" Disabled="@_isSaving" />
@@ -155,6 +166,9 @@ Variant="Variant.Outlined" Margin="Margin.Dense" SearchBox="true" + MaxLength="@UserProfileLimits.TimeZone" + Counter="@UserProfileLimits.TimeZone" + Immediate="true" Disabled="@_isSaving"> @foreach (var tz in _timeZones) { @@ -173,20 +187,12 @@
- + Cancel - + Save Changes
diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/uauth.db-shm index cec5825e651ba80dc3a2fdb07e9fa5898a5bfb32..38ab800e0f4e6a3d1180e764cbbbe8e2ac02de41 100644 GIT binary patch delta 156 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 J3)5Zoe*+covpfI* delta 12548 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 p%)_6l&lmNH3 Validate(UserProfileInfo profile) + { + ArgumentNullException.ThrowIfNull(profile); + + var errors = new List(); + + Check(profile.FirstName, UserProfileLimits.FirstName, "first_name"); + Check(profile.LastName, UserProfileLimits.LastName, "last_name"); + Check(profile.DisplayName, UserProfileLimits.DisplayName, "display_name"); + Check(profile.Gender, UserProfileLimits.Gender, "gender"); + Check(profile.Bio, UserProfileLimits.Bio, "bio"); + Check(profile.Language, UserProfileLimits.Language, "language"); + Check(profile.TimeZone, UserProfileLimits.TimeZone, "time_zone"); + Check(profile.Culture, UserProfileLimits.Culture, "culture"); + + if (profile.Metadata is { } metadata) + { + if (metadata.Count > UserProfileLimits.MetadataEntries) + { + errors.Add(new UAuthValidationError("profile_metadata_entries_exceeded", "metadata")); + } + + long totalBytes = 0; + + foreach (var (key, value) in metadata) + { + if (key.Length > UserProfileLimits.MetadataKey) + { + errors.Add(new UAuthValidationError("profile_metadata_key_too_long" ,"metadata")); + } + + if (value is null || value.Length > UserProfileLimits.MetadataValue) + { + errors.Add(new UAuthValidationError("profile_metadata_value_too_long", "metadata")); + } + + totalBytes += Encoding.UTF8.GetByteCount(key); + + if (value is not null) + totalBytes += Encoding.UTF8.GetByteCount(value); + } + + if (totalBytes > UserProfileLimits.MetadataTotalUtf8Bytes) + { + errors.Add(new UAuthValidationError("profile_metadata_too_large", "metadata")); + } + } + + return errors; + + void Check(string? value, int maxLength, string field) + { + if (value is not null && value.Length > maxLength) + { + errors.Add(new UAuthValidationError("profile_field_too_long", field)); + } + } + } + + public static void EnsureValid(UserProfileInfo profile) + { + var errors = Validate(profile); + + if (errors.Count > 0) + { + throw new UAuthValidationException(string.Join(", ", errors)); + } + } +} diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs index 7d9ac809..22d948e7 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileValidator.cs @@ -9,6 +9,10 @@ public Task ValidateAsync(AccessContext context, Us { ct.ThrowIfCancellationRequested(); - return Task.FromResult(UserProfileValidationResult.Success()); + var errors = UserProfileSafetyValidation.Validate(profile); + + return Task.FromResult(errors.Count == 0 + ? UserProfileValidationResult.Success() + : UserProfileValidationResult.Failed(errors)); } } diff --git a/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileLimits.cs b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileLimits.cs new file mode 100644 index 00000000..237530d9 --- /dev/null +++ b/src/users/CodeBeam.UltimateAuth.Users.Contracts/Dtos/UserProfileLimits.cs @@ -0,0 +1,24 @@ +namespace CodeBeam.UltimateAuth.Users.Contracts; + +/// +/// Framework-level safety limits for user profile content. +/// Applications may enforce stricter limits but cannot increase these limits. +/// +public static class UserProfileLimits +{ + public const int FirstName = 128; + public const int LastName = 128; + public const int DisplayName = 256; + + public const int Gender = 128; + public const int Bio = 4096; + + public const int Language = 64; + public const int TimeZone = 128; + public const int Culture = 64; + + public const int MetadataEntries = 32; + public const int MetadataKey = 128; + public const int MetadataValue = 2048; + public const int MetadataTotalUtf8Bytes = 8192; +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs index f011d9ce..bd2c0fa5 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.EntityFrameworkCore/Data/UAuthUsersModelBuilder.cs @@ -117,6 +117,15 @@ private static void ConfigureProfiles(ModelBuilder b) .HasMaxLength(64) .IsRequired(); + e.Property(x => x.FirstName).HasMaxLength(UserProfileLimits.FirstName); + e.Property(x => x.LastName).HasMaxLength(UserProfileLimits.LastName); + e.Property(x => x.DisplayName).HasMaxLength(UserProfileLimits.DisplayName); + e.Property(x => x.Gender).HasMaxLength(UserProfileLimits.Gender); + e.Property(x => x.Bio).HasMaxLength(UserProfileLimits.Bio); + e.Property(x => x.Language).HasMaxLength(UserProfileLimits.Language); + e.Property(x => x.TimeZone).HasMaxLength(UserProfileLimits.TimeZone); + e.Property(x => x.Culture).HasMaxLength(UserProfileLimits.Culture); + e.Property(x => x.Metadata) .HasConversion(new NullableJsonValueConverter>()) .Metadata.SetValueComparer(JsonValueComparers.Create>()); @@ -128,4 +137,4 @@ private static void ConfigureProfiles(ModelBuilder b) e.HasIndex(x => new { x.Tenant, x.UserKey, x.ProfileKey }).IsUnique(); }); } -} \ No newline at end of file +} diff --git a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs index 41ac9a69..cf5e6bb1 100644 --- a/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs +++ b/src/users/CodeBeam.UltimateAuth.Users.Reference/Services/UserApplicationService.cs @@ -76,6 +76,20 @@ public async Task CreateUserAsync(AccessContext context, Creat throw new UAuthValidationException(string.Join(", ", validationResult.Errors)); } + UserProfileSafetyValidation.EnsureValid(new UserProfileInfo + { + FirstName = request.FirstName, + LastName = request.LastName, + DisplayName = request.DisplayName ?? request.UserName ?? request.Email ?? request.Phone, + BirthDate = request.BirthDate, + Gender = request.Gender, + Bio = request.Bio, + Language = request.Language, + TimeZone = request.TimeZone, + Culture = request.Culture, + Metadata = request.Metadata + }); + EnsureCreateUserIdentifierLimits(request); return await _atomicExecutor.ExecuteAsync( @@ -466,6 +480,7 @@ public async Task CreateProfileAsync(AccessContext context, CreateProfileRequest metadata: request.Metadata); } + UserProfileSafetyValidation.EnsureValid(profile.ToDto()); await store.AddAsync(profile, innerCt); }); @@ -491,6 +506,21 @@ public async Task UpdateUserProfileAsync(AccessContext context, UpdateProfileReq if (!_options.UserProfile.EnableMultiProfile && profileKey != ProfileKey.Default) throw new UAuthConflictException("multi_profile_disabled"); + UserProfileSafetyValidation.EnsureValid(new UserProfileInfo + { + ProfileKey = profileKey, + FirstName = request.FirstName, + LastName = request.LastName, + DisplayName = request.DisplayName, + BirthDate = request.BirthDate, + Gender = request.Gender, + Bio = request.Bio, + Language = request.Language, + TimeZone = request.TimeZone, + Culture = request.Culture, + Metadata = request.Metadata + }); + var expectedVersion = profile.Version; profile diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs index e34d512c..11b12a45 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs @@ -20,8 +20,7 @@ namespace CodeBeam.UltimateAuth.Tests.Unit.Users; public sealed class UserApplicationServiceTests { - private static readonly DateTimeOffset Now = - new(2026, 9, 21, 12, 0, 0, TimeSpan.Zero); + private static readonly DateTimeOffset Now = new(2026, 9, 21, 12, 0, 0, TimeSpan.Zero); // ============================================================ // Create @@ -2210,6 +2209,153 @@ public async Task CreateUserAsync_WithDefaultLimit_AllowsThreeInitialIdentifiers Times.Exactly(3)); } + [Fact] + public async Task CreateUserAsync_WhenProfileFieldExceedsLimit_DoesNotPersist() + { + var f = CreateFixture(); + var context = CreateContext(); + + var request = new CreateUserRequest + { + UserName = "alice", + FirstName = new string('x', UserProfileLimits.FirstName + 1) + }; + + // Simulate an application validator that accepts the request. + SetupValidCreate(f, context, request); + + var act = () => f.Sut.CreateUserAsync(context, request); + + var exception = await act.Should().ThrowAsync(); + exception.Which.Code.Contains("profile_field_too_long"); + + f.LifecycleStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + + f.ProfileStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + + f.IdentifierStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + + f.Atomic.Verify( + x => x.ExecuteAsync( + It.IsAny>>(), + It.IsAny()), + Times.Never); + } + + [Fact] + public async Task CreateProfileAsync_WhenMetadataExceedsLimit_DoesNotPersist() + { + var f = CreateFixtureWithOptions(options => + { + options.UserProfile.EnableMultiProfile = true; + }); + + var userKey = UserKey.New(); + + var context = CreateContext( + actorUserKey: userKey, + targetUserKey: userKey); + + var profileKey = ProfileKey.Parse("secondary", null); + + var request = new CreateProfileRequest + { + ProfileKey = profileKey, + Metadata = Enumerable + .Range(0, UserProfileLimits.MetadataEntries + 1) + .ToDictionary( + x => $"key{x}", + _ => "value") + }; + + f.ProfileStore + .Setup(x => x.ExistsAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(false); + + var act = () => f.Sut.CreateProfileAsync(context, request); + + var exception = await act.Should().ThrowAsync(); + exception.Which.Code.Contains("profile_metadata_entries_exceeded"); + + f.ProfileStore.Verify( + x => x.AddAsync( + It.IsAny(), + It.IsAny()), + Times.Never); + } + + [Fact] + public async Task UpdateUserProfileAsync_WhenFieldExceedsLimit_DoesNotMutateOrPersist() + { + var f = CreateFixture(); + + var userKey = UserKey.New(); + + var context = CreateContext( + actorUserKey: userKey, + targetUserKey: userKey); + + var profile = UserProfile.Create( + Guid.NewGuid(), + context.ResourceTenant, + userKey, + ProfileKey.Default, + Now.AddDays(-1), + firstName: "Alice", + lastName: "Smith", + displayName: "Alice Smith"); + + var originalVersion = profile.Version; + var originalUpdatedAt = profile.UpdatedAt; + + f.ProfileStore + .Setup(x => x.GetAsync( + It.IsAny(), + It.IsAny())) + .ReturnsAsync(profile); + + var request = new UpdateProfileRequest + { + FirstName = new string('x', UserProfileLimits.FirstName + 1), + LastName = "Changed", + DisplayName = "Changed Name" + }; + + var act = () => f.Sut.UpdateUserProfileAsync(context, request); + + var exception = await act.Should().ThrowAsync(); + exception.Which.Code.Contains("profile_field_too_long"); + + // The existing tracked entity must remain unchanged. + profile.FirstName.Should().Be("Alice"); + profile.LastName.Should().Be("Smith"); + profile.DisplayName.Should().Be("Alice Smith"); + + profile.Version.Should().Be(originalVersion); + profile.UpdatedAt.Should().Be(originalUpdatedAt); + + f.ProfileStore.Verify( + x => x.SaveAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + // ============================================================ // Helpers // ============================================================ @@ -2588,4 +2734,4 @@ private static UserIdentifier CreateIdentifier( deletedAt: null, version: version); } -} \ No newline at end of file +} diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileSafetyValidationTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileSafetyValidationTests.cs new file mode 100644 index 00000000..2d359ad8 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserProfileSafetyValidationTests.cs @@ -0,0 +1,135 @@ +using AwesomeAssertions; +using CodeBeam.UltimateAuth.Server.Infrastructure; +using CodeBeam.UltimateAuth.Users.Contracts; + +namespace CodeBeam.UltimateAuth.Tests.Unit; + +public sealed class UserProfileSafetyValidationTests +{ + [Theory] + [InlineData(nameof(UserProfileInfo.FirstName), UserProfileLimits.FirstName)] + [InlineData(nameof(UserProfileInfo.LastName), UserProfileLimits.LastName)] + [InlineData(nameof(UserProfileInfo.DisplayName), UserProfileLimits.DisplayName)] + [InlineData(nameof(UserProfileInfo.Gender), UserProfileLimits.Gender)] + [InlineData(nameof(UserProfileInfo.Bio), UserProfileLimits.Bio)] + [InlineData(nameof(UserProfileInfo.Language), UserProfileLimits.Language)] + [InlineData(nameof(UserProfileInfo.TimeZone), UserProfileLimits.TimeZone)] + [InlineData(nameof(UserProfileInfo.Culture), UserProfileLimits.Culture)] + public void ProfileFields_AtLimit_AreAccepted(string property, int maxLength) + { + var profile = new UserProfileInfo(); + + typeof(UserProfileInfo) + .GetProperty(property)! + .SetValue(profile, new string('x', maxLength)); + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().BeEmpty(); + } + + [Theory] + [InlineData(nameof(UserProfileInfo.FirstName), UserProfileLimits.FirstName)] + [InlineData(nameof(UserProfileInfo.LastName), UserProfileLimits.LastName)] + [InlineData(nameof(UserProfileInfo.DisplayName), UserProfileLimits.DisplayName)] + [InlineData(nameof(UserProfileInfo.Gender), UserProfileLimits.Gender)] + [InlineData(nameof(UserProfileInfo.Bio), UserProfileLimits.Bio)] + [InlineData(nameof(UserProfileInfo.Language), UserProfileLimits.Language)] + [InlineData(nameof(UserProfileInfo.TimeZone), UserProfileLimits.TimeZone)] + [InlineData(nameof(UserProfileInfo.Culture), UserProfileLimits.Culture)] + public void ProfileFields_OverLimit_AreRejected(string property, int maxLength) + { + var profile = new UserProfileInfo(); + + typeof(UserProfileInfo) + .GetProperty(property)! + .SetValue(profile, new string('x', maxLength + 1)); + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().ContainSingle() + .Which.Code.Should().Be("profile_field_too_long"); + } + + [Fact] + public void Metadata_TooManyEntries_IsRejected() + { + var metadata = Enumerable + .Range(0, UserProfileLimits.MetadataEntries + 1) + .ToDictionary(x => $"key{x}", _ => "value"); + + var profile = new UserProfileInfo + { + Metadata = metadata + }; + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().Contain( + x => x.Code == "profile_metadata_entries_exceeded"); + } + + [Fact] + public void Metadata_KeyTooLong_IsRejected() + { + var profile = new UserProfileInfo + { + Metadata = new Dictionary + { + [new string('k', UserProfileLimits.MetadataKey + 1)] = "value" + } + }; + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().Contain( + x => x.Code == "profile_metadata_key_too_long"); + } + + [Fact] + public void Metadata_ValueTooLong_IsRejected() + { + var profile = new UserProfileInfo + { + Metadata = new Dictionary + { + ["key"] = new string( + 'v', + UserProfileLimits.MetadataValue + 1) + } + }; + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().Contain( + x => x.Code == "profile_metadata_value_too_long"); + } + + [Fact] + public void Metadata_TotalUtf8SizeExceeded_IsRejected() + { + var metadata = Enumerable.Range(0, 5) + .ToDictionary( + x => $"key{x}", + _ => new string('é', 1000)); + + var profile = new UserProfileInfo + { + Metadata = metadata + }; + + var errors = UserProfileSafetyValidation.Validate(profile); + + errors.Should().Contain( + x => x.Code == "profile_metadata_too_large"); + } + + [Fact] + public void EmptyProfile_IsValid() + { + var errors = UserProfileSafetyValidation.Validate( + new UserProfileInfo()); + + errors.Should().BeEmpty(); + } +} From 12d9c598d94434f8c81e51f49a77e9426bc801b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sat, 31 Oct 2026 21:56:15 +0300 Subject: [PATCH 2/3] Add Migrations --- ...31185511_AddProfileFieldLimits.Designer.cs | 742 ++++++++++++++++++ .../20261031185511_AddProfileFieldLimits.cs | 178 +++++ .../Migrations/UAuthDbContextModelSnapshot.cs | 24 +- ...31185108_AddProfileFieldLimits.Designer.cs | 729 +++++++++++++++++ .../20261031185108_AddProfileFieldLimits.cs | 22 + .../Migrations/UAuthDbContextModelSnapshot.cs | 8 + ...31184917_AddProfileFieldLimits.Designer.cs | 729 +++++++++++++++++ .../20261031184917_AddProfileFieldLimits.cs | 22 + .../Migrations/UAuthDbContextModelSnapshot.cs | 8 + .../MultiTenancy/UAuthTenantResolver.cs | 3 - 10 files changed, 2454 insertions(+), 11 deletions(-) create mode 100644 apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.Designer.cs create mode 100644 apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.Designer.cs create mode 100644 samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.cs create mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.Designer.cs create mode 100644 samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.cs diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.Designer.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.Designer.cs new file mode 100644 index 00000000..0a45ba86 --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.Designer.cs @@ -0,0 +1,742 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CodeBeam.UltimateAuth.IdentityLab.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20261031185511_AddProfileFieldLimits")] + partial class AddProfileFieldLimits + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CredentialType") + .HasColumnType("integer"); + + b.Property("FailedAttempts") + .HasColumnType("integer"); + + b.Property("LastFailedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LockedUntil") + .HasColumnType("timestamp with time zone"); + + b.Property("RequiresReauthentication") + .HasColumnType("boolean"); + + b.Property("ResetAttempts") + .HasColumnType("integer"); + + b.Property("ResetConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetRequestedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Scope") + .HasColumnType("integer"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastUsedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("SecurityStamp") + .HasColumnType("uuid"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ActiveSessionId") + .HasColumnType("text"); + + b.Property("ChainId") + .HasColumnType("uuid"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Device") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastSeenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RootId") + .HasColumnType("uuid"); + + b.Property("RotationCount") + .HasColumnType("integer"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("bigint"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TouchCount") + .HasColumnType("integer"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChainId") + .HasColumnType("uuid"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Device") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("bigint"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("uuid"); + + b.Property("SecurityVersion") + .HasColumnType("bigint"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChainId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TokenId") + .HasColumnType("uuid"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityVersion") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.HasIndex("Tenant", "DeletedAt", "CreatedAt", "Id"); + + b.HasIndex("Tenant", "Status", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("FirstName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Gender") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Language") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("TimeZone") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.cs new file mode 100644 index 00000000..67d4fada --- /dev/null +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/20261031185511_AddProfileFieldLimits.cs @@ -0,0 +1,178 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.IdentityLab.Migrations +{ + /// + public partial class AddProfileFieldLimits : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TimeZone", + table: "UAuth_UserProfiles", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "UAuth_UserProfiles", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Language", + table: "UAuth_UserProfiles", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Gender", + table: "UAuth_UserProfiles", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "FirstName", + table: "UAuth_UserProfiles", + type: "character varying(128)", + maxLength: 128, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DisplayName", + table: "UAuth_UserProfiles", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Culture", + table: "UAuth_UserProfiles", + type: "character varying(64)", + maxLength: 64, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Bio", + table: "UAuth_UserProfiles", + type: "character varying(4096)", + maxLength: 4096, + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TimeZone", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "LastName", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Language", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Gender", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "FirstName", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(128)", + oldMaxLength: 128, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DisplayName", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Culture", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Bio", + table: "UAuth_UserProfiles", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(4096)", + oldMaxLength: 4096, + oldNullable: true); + } + } +} diff --git a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs index 7b6b4d47..381996b7 100644 --- a/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs +++ b/apps/identity-lab/CodeBeam.UltimateAuth.IdentityLab/Migrations/UAuthDbContextModelSnapshot.cs @@ -641,7 +641,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("Bio") - .HasColumnType("text"); + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); b.Property("BirthDate") .HasColumnType("date"); @@ -650,25 +651,31 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("Culture") - .HasColumnType("text"); + .HasMaxLength(64) + .HasColumnType("character varying(64)"); b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); b.Property("DisplayName") - .HasColumnType("text"); + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.Property("FirstName") - .HasColumnType("text"); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Gender") - .HasColumnType("text"); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Language") - .HasColumnType("text"); + .HasMaxLength(64) + .HasColumnType("character varying(64)"); b.Property("LastName") - .HasColumnType("text"); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("Metadata") .HasColumnType("text"); @@ -684,7 +691,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(128)"); b.Property("TimeZone") - .HasColumnType("text"); + .HasMaxLength(128) + .HasColumnType("character varying(128)"); b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.Designer.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.Designer.cs new file mode 100644 index 00000000..d3d07c21 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.Designer.cs @@ -0,0 +1,729 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20261031185108_AddProfileFieldLimits")] + partial class AddProfileFieldLimits + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.HasIndex("Tenant", "DeletedAt", "CreatedAt", "Id"); + + b.HasIndex("Tenant", "Status", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Language") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.cs new file mode 100644 index 00000000..64065857 --- /dev/null +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/20261031185108_AddProfileFieldLimits.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore.Migrations +{ + /// + public partial class AddProfileFieldLimits : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs index de4aec39..5e2dd6f8 100644 --- a/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs +++ b/samples/UAuthHub/CodeBeam.UltimateAuth.Sample.UAuthHub.EFCore/Migrations/UAuthDbContextModelSnapshot.cs @@ -628,6 +628,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Bio") + .HasMaxLength(4096) .HasColumnType("TEXT"); b.Property("BirthDate") @@ -637,24 +638,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Culture") + .HasMaxLength(64) .HasColumnType("TEXT"); b.Property("DeletedAt") .HasColumnType("TEXT"); b.Property("DisplayName") + .HasMaxLength(256) .HasColumnType("TEXT"); b.Property("FirstName") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Gender") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Language") + .HasMaxLength(64) .HasColumnType("TEXT"); b.Property("LastName") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Metadata") @@ -671,6 +678,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("TimeZone") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("UpdatedAt") diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.Designer.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.Designer.cs new file mode 100644 index 00000000..c81fe3d6 --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.Designer.cs @@ -0,0 +1,729 @@ +// +using System; +using CodeBeam.UltimateAuth.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + [DbContext(typeof(UAuthDbContext))] + [Migration("20261031184917_AddProfileFieldLimits")] + partial class AddProfileFieldLimits + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authentication.EntityFrameworkCore.AuthenticationSecurityStateProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CredentialType") + .HasColumnType("INTEGER"); + + b.Property("FailedAttempts") + .HasColumnType("INTEGER"); + + b.Property("LastFailedAt") + .HasColumnType("TEXT"); + + b.Property("LockedUntil") + .HasColumnType("TEXT"); + + b.Property("RequiresReauthentication") + .HasColumnType("INTEGER"); + + b.Property("ResetAttempts") + .HasColumnType("INTEGER"); + + b.Property("ResetConsumedAt") + .HasColumnType("TEXT"); + + b.Property("ResetExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ResetRequestedAt") + .HasColumnType("TEXT"); + + b.Property("ResetTokenHash") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Scope") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "LockedUntil"); + + b.HasIndex("Tenant", "ResetRequestedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "Scope"); + + b.HasIndex("Tenant", "UserKey", "Scope", "CredentialType") + .IsUnique(); + + b.ToTable("UAuth_Authentication", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RolePermissionProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("Permission") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "RoleId", "Permission"); + + b.HasIndex("Tenant", "Permission"); + + b.HasIndex("Tenant", "RoleId"); + + b.ToTable("UAuth_RolePermissions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.RoleProjection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "NormalizedName") + .IsUnique(); + + b.ToTable("UAuth_Roles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Authorization.EntityFrameworkCore.UserRoleProjection", b => + { + b.Property("Tenant") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("AssignedAt") + .HasColumnType("TEXT"); + + b.HasKey("Tenant", "UserKey", "RoleId"); + + b.HasIndex("Tenant", "RoleId"); + + b.HasIndex("Tenant", "UserKey"); + + b.ToTable("UAuth_UserRoles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Credentials.EntityFrameworkCore.PasswordCredentialProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecretHash") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "Id") + .IsUnique(); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeletedAt"); + + b.ToTable("UAuth_PasswordCredentials", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AbsoluteExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ActiveSessionId") + .HasColumnType("TEXT"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("ClaimsSnapshot") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasColumnType("TEXT"); + + b.Property("RotationCount") + .HasColumnType("INTEGER"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TouchCount") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId") + .IsUnique(); + + b.HasIndex("Tenant", "RootId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "DeviceId"); + + b.ToTable("UAuth_SessionChains", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("Claims") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Device") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Metadata") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersionAtCreation") + .HasColumnType("INTEGER"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "RevokedAt"); + + b.HasIndex("Tenant", "SessionId") + .IsUnique(); + + b.HasIndex("Tenant", "ChainId", "RevokedAt"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_Sessions", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("RootId") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "RootId") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "UserKey", "RevokedAt"); + + b.ToTable("UAuth_SessionRoots", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Tokens.EntityFrameworkCore.RefreshTokenProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChainId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("ReplacedByTokenHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("RevokedAt") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TokenId") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "ChainId"); + + b.HasIndex("Tenant", "ExpiresAt"); + + b.HasIndex("Tenant", "ReplacedByTokenHash"); + + b.HasIndex("Tenant", "SessionId"); + + b.HasIndex("Tenant", "TokenHash") + .IsUnique(); + + b.HasIndex("Tenant", "TokenId"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "ExpiresAt", "RevokedAt"); + + b.HasIndex("Tenant", "TokenHash", "RevokedAt"); + + b.ToTable("UAuth_RefreshTokens", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserIdentifierProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER"); + + b.Property("NormalizedValue") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("VerifiedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "NormalizedValue"); + + b.HasIndex("Tenant", "UserKey"); + + b.HasIndex("Tenant", "Type", "NormalizedValue") + .IsUnique(); + + b.HasIndex("Tenant", "UserKey", "IsPrimary"); + + b.HasIndex("Tenant", "UserKey", "Type", "IsPrimary", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserIdentifiers", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserLifecycleProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey") + .IsUnique(); + + b.HasIndex("Tenant", "DeletedAt", "CreatedAt", "Id"); + + b.HasIndex("Tenant", "Status", "DeletedAt", "CreatedAt", "Id"); + + b.ToTable("UAuth_UserLifecycles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Users.EntityFrameworkCore.UserProfileProjection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasMaxLength(4096) + .HasColumnType("TEXT"); + + b.Property("BirthDate") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Culture") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Gender") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Language") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastName") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Metadata") + .HasColumnType("TEXT"); + + b.Property("ProfileKey") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("Tenant") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("TimeZone") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Tenant", "UserKey", "ProfileKey") + .IsUnique(); + + b.ToTable("UAuth_UserProfiles", (string)null); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionRootProjection", null) + .WithMany() + .HasForeignKey("Tenant", "RootId") + .HasPrincipalKey("Tenant", "RootId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionProjection", b => + { + b.HasOne("CodeBeam.UltimateAuth.Sessions.EntityFrameworkCore.SessionChainProjection", null) + .WithMany() + .HasForeignKey("Tenant", "ChainId") + .HasPrincipalKey("Tenant", "ChainId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.cs new file mode 100644 index 00000000..31574eaa --- /dev/null +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/20261031184917_AddProfileFieldLimits.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore.Migrations +{ + /// + public partial class AddProfileFieldLimits : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs index 3e5045d5..ff7c7c41 100644 --- a/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs +++ b/samples/blazor-server/CodeBeam.UltimateAuth.Sample.BlazorServer.EFCore/Migrations/UAuthDbContextModelSnapshot.cs @@ -628,6 +628,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Bio") + .HasMaxLength(4096) .HasColumnType("TEXT"); b.Property("BirthDate") @@ -637,24 +638,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Culture") + .HasMaxLength(64) .HasColumnType("TEXT"); b.Property("DeletedAt") .HasColumnType("TEXT"); b.Property("DisplayName") + .HasMaxLength(256) .HasColumnType("TEXT"); b.Property("FirstName") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Gender") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Language") + .HasMaxLength(64) .HasColumnType("TEXT"); b.Property("LastName") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("Metadata") @@ -671,6 +678,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("TimeZone") + .HasMaxLength(128) .HasColumnType("TEXT"); b.Property("UpdatedAt") diff --git a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs index 934d33a0..967b1055 100644 --- a/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs +++ b/src/CodeBeam.UltimateAuth.Server/MultiTenancy/UAuthTenantResolver.cs @@ -1,9 +1,6 @@ using CodeBeam.UltimateAuth.Core.Abstractions; using CodeBeam.UltimateAuth.Core.MultiTenancy; -using CodeBeam.UltimateAuth.Core.Options; -using CodeBeam.UltimateAuth.Server.Options; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; namespace CodeBeam.UltimateAuth.Server.MultiTenancy; From 8f9bae688859b744774e3016c9767c3bd69fa69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sat, 31 Oct 2026 22:08:31 +0300 Subject: [PATCH 3/3] Fix Tests --- .../Infrastructure/Validator/UserProfileSafetyValidation.cs | 4 +--- .../Users/UserApplicationServiceTests.cs | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileSafetyValidation.cs b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileSafetyValidation.cs index f1cc32f7..1a6b4e48 100644 --- a/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileSafetyValidation.cs +++ b/src/CodeBeam.UltimateAuth.Server/Infrastructure/Validator/UserProfileSafetyValidation.cs @@ -71,8 +71,6 @@ public static void EnsureValid(UserProfileInfo profile) var errors = Validate(profile); if (errors.Count > 0) - { - throw new UAuthValidationException(string.Join(", ", errors)); - } + throw new UAuthValidationException(errors[0].Code); } } diff --git a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs index 11b12a45..05dc040f 100644 --- a/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs +++ b/tests/CodeBeam.UltimateAuth.Tests.Unit/Users/UserApplicationServiceTests.cs @@ -2227,7 +2227,7 @@ public async Task CreateUserAsync_WhenProfileFieldExceedsLimit_DoesNotPersist() var act = () => f.Sut.CreateUserAsync(context, request); var exception = await act.Should().ThrowAsync(); - exception.Which.Code.Contains("profile_field_too_long"); + exception.Which.Code.Should().Be("profile_field_too_long"); f.LifecycleStore.Verify( x => x.AddAsync( @@ -2289,7 +2289,7 @@ public async Task CreateProfileAsync_WhenMetadataExceedsLimit_DoesNotPersist() var act = () => f.Sut.CreateProfileAsync(context, request); var exception = await act.Should().ThrowAsync(); - exception.Which.Code.Contains("profile_metadata_entries_exceeded"); + exception.Which.Code.Should().Be("profile_metadata_entries_exceeded"); f.ProfileStore.Verify( x => x.AddAsync( @@ -2338,7 +2338,7 @@ public async Task UpdateUserProfileAsync_WhenFieldExceedsLimit_DoesNotMutateOrPe var act = () => f.Sut.UpdateUserProfileAsync(context, request); var exception = await act.Should().ThrowAsync(); - exception.Which.Code.Contains("profile_field_too_long"); + exception.Which.Code.Should().Be("profile_field_too_long"); // The existing tracked entity must remain unchanged. profile.FirstName.Should().Be("Alice");