From 532ba9bc9dbf56761a6998ff4eec849d14c0678a Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 19 Jan 2021 12:51:43 +0100 Subject: [PATCH] Refactoring/users (#623) * Simplified user service. * Refactor users and deleting of users. * Fix tests * Auth fix. * Should revert fixes. * Stability improvements. * Build fix. * Tests fixed. * Tests * Tests simplified. --- backend/i18n/frontend_en.json | 9 +- backend/i18n/frontend_it.json | 9 +- backend/i18n/frontend_nl.json | 9 +- backend/i18n/source/backend_en.json | 2 +- backend/i18n/source/backend_it.json | 1 - backend/i18n/source/backend_nl.json | 1 - backend/i18n/source/frontend_en.json | 9 +- backend/i18n/source/frontend_it.json | 4 +- backend/i18n/source/frontend_nl.json | 3 +- .../PredefinedPatternsFormatter.cs | 6 +- .../Scripting/JintUser.cs | 2 +- .../Extensions/UserFluidExtension.cs | 3 +- .../Contents/Queries/GeoQueryTransformer.cs | 1 - .../Squidex.Domain.Apps.Entities/Context.cs | 2 +- .../History/NotifoService.cs | 4 +- .../Notifications/NotificationEmailSender.cs | 7 +- .../DefaultUserResolver.cs | 60 +- .../DefaultUserService.cs | 408 ++++++++++++ .../Squidex.Domain.Users/IUserEventHandler.cs | 26 - .../src/Squidex.Domain.Users/IUserEvents.cs | 16 +- .../src/Squidex.Domain.Users/IUserService.cs | 55 ++ .../src/Squidex.Domain.Users/UserEvents.cs | 49 -- .../UserManagerExtensions.cs | 295 +++------ .../src/Squidex.Domain.Users/UserValues.cs | 109 ---- .../Squidex.Domain.Users/UserWithClaims.cs | 20 +- .../Validation/LocalizedCompareAttribute.cs | 2 +- .../LocalizedEmailAddressAttribute.cs | 2 +- .../Validation/LocalizedRangeAttribute.cs | 2 +- .../LocalizedRegularExpressionAttribute.cs | 2 +- .../Validation/LocalizedRequiredAttribute.cs | 2 +- .../LocalizedStringLengthAttribute.cs | 2 +- .../Identity/ClaimsPrincipalExtensions.cs | 47 -- .../Identity/SquidexClaimTypes.cs | 20 +- .../Identity/SquidexClaimsExtensions.cs | 159 +++++ backend/src/Squidex.Shared/Texts.it.resx | 6 +- backend/src/Squidex.Shared/Texts.nl.resx | 6 +- backend/src/Squidex.Shared/Texts.resx | 6 +- .../src/Squidex.Shared/Users/ClientUser.cs | 28 +- backend/src/Squidex.Shared/Users/IUser.cs | 4 +- .../Squidex.Shared/Users/UserExtensions.cs | 121 ---- .../Controllers/Apps/AppClientsController.cs | 7 +- .../Apps/AppContributorsController.cs | 7 +- .../Apps/AppLanguagesController.cs | 7 +- .../Controllers/Apps/AppPatternsController.cs | 7 +- .../Controllers/Apps/AppRolesController.cs | 9 +- .../Apps/AppWorkflowsController.cs | 9 +- .../Api/Controllers/Apps/AppsController.cs | 12 +- .../Controllers/Apps/Models/ContributorDto.cs | 3 +- .../Assets/AssetContentController.cs | 5 +- .../Assets/AssetFoldersController.cs | 7 +- .../Controllers/Assets/AssetsController.cs | 14 +- .../Backups/BackupContentController.cs | 3 +- .../Controllers/Backups/BackupsController.cs | 7 +- .../Controllers/Backups/RestoreController.cs | 3 +- .../Comments/CommentsController.cs | 3 +- .../UserNotificationsController.cs | 3 +- .../Contents/ContentsController.cs | 31 +- .../EventConsumersController.cs | 9 +- .../Controllers/History/HistoryController.cs | 3 +- .../Languages/LanguagesController.cs | 3 +- .../Api/Controllers/News/NewsController.cs | 3 +- .../Api/Controllers/Ping/PingController.cs | 3 +- .../Controllers/Plans/AppPlansController.cs | 5 +- .../Api/Controllers/Rules/RulesController.cs | 17 +- .../Schemas/SchemaFieldsController.cs | 35 +- .../Controllers/Schemas/SchemasController.cs | 21 +- .../Controllers/Search/SearchController.cs | 3 +- .../Statistics/UsagesController.cs | 9 +- .../Translations/TranslationsController.cs | 3 +- .../Areas/Api/Controllers/UI/UIController.cs | 7 +- .../Api/Controllers/Users/Models/UserDto.cs | 7 +- .../Api/Controllers/Users/Models/UsersDto.cs | 4 +- .../Users/UserManagementController.cs | 51 +- .../Api/Controllers/Users/UsersController.cs | 16 +- .../Config/CreateAdminInitializer.cs | 24 +- .../Config/IdentityServerExtensions.cs | 57 -- .../Config/IdentityServerServices.cs | 3 + .../IdentityServer/Config/LazyClientStore.cs | 20 +- .../Controllers/Account/AccountController.cs | 153 ++--- .../Controllers/Profile/ProfileController.cs | 107 ++- .../Squidex/Areas/IdentityServer/Startup.cs | 2 +- ...rleansDashboardAuthenticationMiddleware.cs | 4 +- .../Config/Authentication/GithubHandler.cs | 2 +- .../Config/Authentication/GoogleHandler.cs | 4 +- .../Config/Authentication/MicrosoftHandler.cs | 5 +- .../Squidex/Config/Domain/HistoryServices.cs | 2 +- .../Config/Domain/SubscriptionServices.cs | 4 - .../DefaultUserResolverTests.cs | 207 ++---- .../DefaultUserServiceTests.cs | 607 ++++++++++++++++++ .../TestSuite/TestSuite.ApiTests/AppTests.cs | 4 +- .../pages/users/user.component.html | 11 +- .../pages/users/user.component.ts | 4 + .../pages/users/users-page.component.html | 2 +- .../services/users.service.spec.ts | 19 + .../administration/services/users.service.ts | 15 +- .../administration/state/users.state.spec.ts | 10 + .../administration/state/users.state.ts | 12 + .../pages/content/content-page.component.html | 6 +- .../content-references.component.html | 4 +- .../pages/schema/schema-page.component.html | 10 +- .../schema/ui/schema-ui-form.component.html | 4 +- .../components/schema-category.component.html | 2 +- frontend/app/shared/state/contents.forms.ts | 2 +- .../shared/state/contents.forms.visitors.ts | 2 +- 104 files changed, 1903 insertions(+), 1260 deletions(-) create mode 100644 backend/src/Squidex.Domain.Users/DefaultUserService.cs delete mode 100644 backend/src/Squidex.Domain.Users/IUserEventHandler.cs create mode 100644 backend/src/Squidex.Domain.Users/IUserService.cs delete mode 100644 backend/src/Squidex.Domain.Users/UserEvents.cs delete mode 100644 backend/src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs create mode 100644 backend/src/Squidex.Shared/Identity/SquidexClaimsExtensions.cs delete mode 100644 backend/src/Squidex.Shared/Users/UserExtensions.cs create mode 100644 backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 774af780b..b3f7b0219 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -352,6 +352,7 @@ "contents.arrayNoFields": "Add a nested field first to add items.", "contents.assetsUpload": "Drop files or click", "contents.autotranslate": "Autotranslate from master language", + "contents.bulkFailed": "Failed to delete or update content. Please reload.", "contents.changeStatusTo": "Change content item(s) to {action}", "contents.changeStatusToImmediately": "Set to {action} immediately.", "contents.changeStatusToLater": "Set to {action} at a later point date and time.", @@ -370,7 +371,6 @@ "contents.currentStatusLabel": "Current Version", "contents.deleteConfirmText": "Do you really want to delete the content?", "contents.deleteConfirmTitle": "Delete content", - "contents.deleteFailed": "Failed to delete content. Please reload.", "contents.deleteManyConfirmText": "Do you really want to delete the selected content items?", "contents.deleteReferrerConfirmText": "The content is referenced by another content item.\n\nDo you really want to delete this content?", "contents.deleteReferrerConfirmTitle": "Delete content", @@ -379,7 +379,6 @@ "contents.draftNew": "New Draft", "contents.draftStatus": "New Version", "contents.editPageTitle": "Edit Content", - "contents.editTitle": "Edit Content", "contents.invariantFieldDescription": "The '{fieldName}' field of the content item.", "contents.languageModeAll": "All Languages", "contents.languageModeSingle": "Single Language", @@ -644,7 +643,6 @@ "rules.wizard.selectAction": "Select Action", "rules.wizard.selectTrigger": "Select Trigger", "rules.wizard.triggerHint": "The selection of the trigger type cannot be changed later.", - "schema.fields.localConfirmText": "Lock field", "schemas.addField": "Add Field", "schemas.addFieldAndClose": "Create and close", "schemas.addFieldAndCreate": "Create and add field", @@ -883,17 +881,22 @@ "users.createPageTitle": "Create User", "users.createTitle": "New User", "users.createTooltip": "New User (CTRL + N)", + "users.deleteConfirmText": "Do you really want to delete this user?", + "users.deleteConfirmTitle": "Delete user", + "users.deleteFailed": "Failed to delete user. Please reload.", "users.editPageTitle": "Edit User", "users.editTitle": "Edit User", "users.listPageTitle": "User Management", "users.listTitle": "Users", "users.loadFailed": "Failed to load users. Please reload.", "users.loadUserFailed": "Failed to load user. Please reload.", + "users.lockFailed": "Failed to lock user. Please reload.", "users.lockTooltip": "Lock User", "users.passwordConfirmValidationMessage": "Passwords must be the same.", "users.refreshTooltip": "Refresh Users (CTRL + SHIFT + R)", "users.reloaded": "Users reloaded.", "users.search": "Search for user", + "users.unlockFailed": "Failed to unlock user. Please reload.", "users.unlockTooltip": "Unlock User", "users.updateFailed": "Failed to update user. Please reload.", "validation.between": "{field} must be between '{min}' and '{max}'.", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index a22e9430b..3b9a5e4b2 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -352,6 +352,7 @@ "contents.arrayNoFields": "Aggiungi un primo campo annidato agli elementi.", "contents.assetsUpload": "Trascina i file o clicca", "contents.autotranslate": "Traduci in automatico dalla lingua principale", + "contents.bulkFailed": "Non è stato possibile eliminare il contenuto. Per favore ricarica.", "contents.changeStatusTo": "Cambia l'elemeto(i) del contenuti in {action}", "contents.changeStatusToImmediately": "Imposta {action} immediatamente.", "contents.changeStatusToLater": "Imposta {action} ad una data e ora successiva.", @@ -370,7 +371,6 @@ "contents.currentStatusLabel": "Versione corrente", "contents.deleteConfirmText": "Sei sicuro di voler eliminare il contenuto?", "contents.deleteConfirmTitle": "Elimina il contenuto", - "contents.deleteFailed": "Non è stato possibile eliminare il contenuto. Per favore ricarica.", "contents.deleteManyConfirmText": "Sei sicuro di voler eliminare gli elementi del contenuto selezionati?", "contents.deleteReferrerConfirmText": "Il contenuto è collegato a un altro contenuto.\n\nSei sicuro di volerlo cancellare?", "contents.deleteReferrerConfirmTitle": "Contenuto cancellato", @@ -379,7 +379,6 @@ "contents.draftNew": "Nuova bozza", "contents.draftStatus": "Nuova versione", "contents.editPageTitle": "Modifica contenuto", - "contents.editTitle": "Modifica il contenuto", "contents.invariantFieldDescription": "Il campo '{fieldName}' del contenuto.", "contents.languageModeAll": "Tutte le lingue", "contents.languageModeSingle": "Una sola lingua", @@ -644,7 +643,6 @@ "rules.wizard.selectAction": "Seleziona l'Azione", "rules.wizard.selectTrigger": "Seleziona l'Attivazione", "rules.wizard.triggerHint": "La selezione del tipo di attivazione non potrà essere modificata successivamente.", - "schema.fields.localConfirmText": "Blocca il campo", "schemas.addField": "Aggiungi un Campo", "schemas.addFieldAndClose": "Crea e chiudi", "schemas.addFieldAndCreate": "Crea e aggiungi il campo", @@ -883,17 +881,22 @@ "users.createPageTitle": "Crea un utente", "users.createTitle": "Nuovo utente", "users.createTooltip": "Nuovo utente (CTRL + N)", + "users.deleteConfirmText": "Do you really want to delete this user?", + "users.deleteConfirmTitle": "Delete user", + "users.deleteFailed": "Failed to delete user. Please reload.", "users.editPageTitle": "Modifica l'utente", "users.editTitle": "Modifica l'utente", "users.listPageTitle": "Gestione Utente", "users.listTitle": "Utenti", "users.loadFailed": "Non è stato possibile caricare gli utenti. Per favore ricarica.", "users.loadUserFailed": "Non è stato possibile caricare l'utente. Per favore ricarica.", + "users.lockFailed": "Failed to lock user. Please reload.", "users.lockTooltip": "Utente bloccato", "users.passwordConfirmValidationMessage": "Le password devono essere uguali.", "users.refreshTooltip": "Aggiorna gli Utenti (CTRL + SHIFT + R)", "users.reloaded": "Utenti ricaricati.", "users.search": "Cerca l'utente", + "users.unlockFailed": "Failed to unlock user. Please reload.", "users.unlockTooltip": "Sblocca l'utente", "users.updateFailed": "Non è stato possibile aggiornare l'utente. Per favore ricarica.", "validation.between": "{field} deve essere tra '{min}' e '{max}'.", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index dcdc9e828..b608d6b5d 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -352,6 +352,7 @@ "contents.arrayNoFields": "Voeg eerst een genest veld toe om items toe te voegen.", "contents.assetsUpload": "Zet bestanden neer of klik", "contents.autotranslate": "Automatisch vertalen vanuit de hoofdtaal", + "contents.bulkFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.", "contents.changeStatusTo": "Verander inhoud item(s) in {action}", "contents.changeStatusToImmediately": "Zet onmiddellijk op {action}.", "contents.changeStatusToLater": "Zet op {action} op een latere datum en tijd.", @@ -370,7 +371,6 @@ "contents.currentStatusLabel": "Huidige versie", "contents.deleteConfirmText": "Wilt je de inhoud echt verwijderen?", "contents.deleteConfirmTitle": "Inhoud verwijderen", - "contents.deleteFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.", "contents.deleteManyConfirmText": "Weet je zeker dat je de geselecteerde inhoudsitems wilt verwijderen?", "contents.deleteReferrerConfirmText": "Er wordt naar de inhoud verwezen door een ander inhoudsitem. \n \n Wil je de inhoud echt verwijderen?", "contents.deleteReferrerConfirmTitle": "Inhoud verwijderen", @@ -379,7 +379,6 @@ "contents.draftNew": "Nieuw concept", "contents.draftStatus": "Nieuwe versie", "contents.editPageTitle": "Inhoud bewerken", - "contents.editTitle": "Inhoud bewerken", "contents.invariantFieldDescription": "Het veld '{fieldName}' van het inhoudsitem.", "contents.languageModeAll": "Alle talen", "contents.languageModeSingle": "Enkele taal", @@ -644,7 +643,6 @@ "rules.wizard.selectAction": "Selecteer actie", "rules.wizard.selectTrigger": "Selecteer Trigger", "rules.wizard.triggerHint": "De selectie van het triggertype kan later niet worden gewijzigd.", - "schema.fields.localConfirmText": "Lock field", "schemas.addField": "Veld toevoegen", "schemas.addFieldAndClose": "Maken en sluiten", "schemas.addFieldAndCreate": "Maak en voeg veld toe", @@ -883,17 +881,22 @@ "users.createPageTitle": "Gebruiker aanmaken", "users.createTitle": "Nieuwe gebruiker", "users.createTooltip": "Nieuwe gebruiker (CTRL + N)", + "users.deleteConfirmText": "Do you really want to delete this user?", + "users.deleteConfirmTitle": "Delete user", + "users.deleteFailed": "Failed to delete user. Please reload.", "users.editPageTitle": "Gebruiker bewerken", "users.editTitle": "Gebruiker bewerken", "users.listPageTitle": "Gebruikersbeheer", "users.listTitle": "Gebruikers", "users.loadFailed": "Laden van gebruikers mislukt. Laad opnieuw.", "users.loadUserFailed": "Kan gebruiker niet laden. Laad opnieuw.", + "users.lockFailed": "Failed to lock user. Please reload.", "users.lockTooltip": "Gebruiker vergrendelen", "users.passwordConfirmValidationMessage": "Wachtwoorden moeten hetzelfde zijn.", "users.refreshTooltip": "Ververs gebruikers (CTRL + SHIFT + R)", "users.reloaded": "Gebruikers herladen.", "users.search": "Zoeken naar gebruiker", + "users.unlockFailed": "Failed to unlock user. Please reload.", "users.unlockTooltip": "Gebruiker ontgrendelen", "users.updateFailed": "Update gebruiker mislukt. Laad opnieuw.", "validation.between": "{field} moet tussen '{min}' en '{max}' liggen.", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 2b43774e0..44c66589e 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -126,7 +126,6 @@ "contents.invalidGeolocation": "Invalid json type, expected latitude/longitude object.", "contents.invalidGeolocationLatitude": "Latitude must be between -90 and 90.", "contents.invalidGeolocationLongitude": "Longitude must be between -180 and 180.", - "contents.invalidGeolocationMoreProperties": "Geolocation can only have latitude and longitude property.", "contents.invalidNumber": "Invalid json type, expected number.", "contents.invalidString": "Invalid json type, expected string.", "contents.listReferences": "{count} Reference(s)", @@ -282,6 +281,7 @@ "users.consent.piiHeadline": "Personal Information", "users.consent.piiText": "I understand and agree that Squidex collects the following private information that are retrieved from external authentication providers such as Google, Microsoft or Github. ", "users.consent.title": "Consent", + "users.deleteYourselfError": "You cannot delete yourself.", "users.error.headline": "Operation failed", "users.error.text": "We are really sorry that something went wrong.", "users.error.title": "Error", diff --git a/backend/i18n/source/backend_it.json b/backend/i18n/source/backend_it.json index b1d10caeb..7c1b506b7 100644 --- a/backend/i18n/source/backend_it.json +++ b/backend/i18n/source/backend_it.json @@ -126,7 +126,6 @@ "contents.invalidGeolocation": "Errore nel json, atteso un object latitudine/longitudine.", "contents.invalidGeolocationLatitude": "La latitudine deve essere tra -90 and 90.", "contents.invalidGeolocationLongitude": "La longitude deve essere tra -180 and 180.", - "contents.invalidGeolocationMoreProperties": "E' possibile impostare la geolocalizzazione solo impostando latitudine e longitudine.", "contents.invalidNumber": "Errore nel json,, atteso un number.", "contents.invalidString": "Errore nel json, atteso una string.", "contents.listReferences": "{count} Collegamenti(s)", diff --git a/backend/i18n/source/backend_nl.json b/backend/i18n/source/backend_nl.json index 09103ae63..35ec0f2e0 100644 --- a/backend/i18n/source/backend_nl.json +++ b/backend/i18n/source/backend_nl.json @@ -126,7 +126,6 @@ "contents.invalidGeolocation": "Ongeldig json-type, verwacht object voor lengte- / breedtegraad.", "contents.invalidGeolocationLatitude": "Breedtegraad moet tussen -90 en 90 zijn.", "contents.invalidGeolocationLongitude": "Lengtegraad moet tussen -180 en 180 liggen.", - "contents.invalidGeolocationMoreProperties": "Geolocatie kan alleen de eigenschap lengte- en breedtegraad hebben.", "contents.invalidNumber": "Ongeldig json-type, verwacht aantal.", "contents.invalidString": "Ongeldig json-type, verwachte string.", "contents.listReferences": "{count} referentie (s)", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 774af780b..b3f7b0219 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -352,6 +352,7 @@ "contents.arrayNoFields": "Add a nested field first to add items.", "contents.assetsUpload": "Drop files or click", "contents.autotranslate": "Autotranslate from master language", + "contents.bulkFailed": "Failed to delete or update content. Please reload.", "contents.changeStatusTo": "Change content item(s) to {action}", "contents.changeStatusToImmediately": "Set to {action} immediately.", "contents.changeStatusToLater": "Set to {action} at a later point date and time.", @@ -370,7 +371,6 @@ "contents.currentStatusLabel": "Current Version", "contents.deleteConfirmText": "Do you really want to delete the content?", "contents.deleteConfirmTitle": "Delete content", - "contents.deleteFailed": "Failed to delete content. Please reload.", "contents.deleteManyConfirmText": "Do you really want to delete the selected content items?", "contents.deleteReferrerConfirmText": "The content is referenced by another content item.\n\nDo you really want to delete this content?", "contents.deleteReferrerConfirmTitle": "Delete content", @@ -379,7 +379,6 @@ "contents.draftNew": "New Draft", "contents.draftStatus": "New Version", "contents.editPageTitle": "Edit Content", - "contents.editTitle": "Edit Content", "contents.invariantFieldDescription": "The '{fieldName}' field of the content item.", "contents.languageModeAll": "All Languages", "contents.languageModeSingle": "Single Language", @@ -644,7 +643,6 @@ "rules.wizard.selectAction": "Select Action", "rules.wizard.selectTrigger": "Select Trigger", "rules.wizard.triggerHint": "The selection of the trigger type cannot be changed later.", - "schema.fields.localConfirmText": "Lock field", "schemas.addField": "Add Field", "schemas.addFieldAndClose": "Create and close", "schemas.addFieldAndCreate": "Create and add field", @@ -883,17 +881,22 @@ "users.createPageTitle": "Create User", "users.createTitle": "New User", "users.createTooltip": "New User (CTRL + N)", + "users.deleteConfirmText": "Do you really want to delete this user?", + "users.deleteConfirmTitle": "Delete user", + "users.deleteFailed": "Failed to delete user. Please reload.", "users.editPageTitle": "Edit User", "users.editTitle": "Edit User", "users.listPageTitle": "User Management", "users.listTitle": "Users", "users.loadFailed": "Failed to load users. Please reload.", "users.loadUserFailed": "Failed to load user. Please reload.", + "users.lockFailed": "Failed to lock user. Please reload.", "users.lockTooltip": "Lock User", "users.passwordConfirmValidationMessage": "Passwords must be the same.", "users.refreshTooltip": "Refresh Users (CTRL + SHIFT + R)", "users.reloaded": "Users reloaded.", "users.search": "Search for user", + "users.unlockFailed": "Failed to unlock user. Please reload.", "users.unlockTooltip": "Unlock User", "users.updateFailed": "Failed to update user. Please reload.", "validation.between": "{field} must be between '{min}' and '{max}'.", diff --git a/backend/i18n/source/frontend_it.json b/backend/i18n/source/frontend_it.json index fbc295343..450344dfc 100644 --- a/backend/i18n/source/frontend_it.json +++ b/backend/i18n/source/frontend_it.json @@ -347,6 +347,7 @@ "contents.arrayNoFields": "Aggiungi un primo campo annidato agli elementi.", "contents.assetsUpload": "Trascina i file o clicca", "contents.autotranslate": "Traduci in automatico dalla lingua principale", + "contents.bulkFailed": "Non è stato possibile eliminare il contenuto. Per favore ricarica.", "contents.changeStatusTo": "Cambia l'elemeto(i) del contenuti in {action}", "contents.changeStatusToImmediately": "Imposta {action} immediatamente.", "contents.changeStatusToLater": "Imposta {action} ad una data e ora successiva.", @@ -362,7 +363,6 @@ "contents.currentStatusLabel": "Versione corrente", "contents.deleteConfirmText": "Sei sicuro di voler eliminare il contenuto?", "contents.deleteConfirmTitle": "Elimina il contenuto", - "contents.deleteFailed": "Non è stato possibile eliminare il contenuto. Per favore ricarica.", "contents.deleteManyConfirmText": "Sei sicuro di voler eliminare gli elementi del contenuto selezionati?", "contents.deleteReferrerConfirmText": "Il contenuto è collegato a un altro contenuto.\n\nSei sicuro di volerlo cancellare?", "contents.deleteReferrerConfirmTitle": "Contenuto cancellato", @@ -371,7 +371,6 @@ "contents.draftNew": "Nuova bozza", "contents.draftStatus": "Nuova versione", "contents.editPageTitle": "Modifica contenuto", - "contents.editTitle": "Modifica il contenuto", "contents.invariantFieldDescription": "Il campo '{fieldName}' del contenuto.", "contents.languageModeAll": "Tutte le lingue", "contents.languageModeSingle": "Una sola lingua", @@ -626,7 +625,6 @@ "rules.wizard.selectAction": "Seleziona l'Azione", "rules.wizard.selectTrigger": "Seleziona l'Attivazione", "rules.wizard.triggerHint": "La selezione del tipo di attivazione non potrà essere modificata successivamente.", - "schema.fields.localConfirmText": "Blocca il campo", "schemas.addField": "Aggiungi un Campo", "schemas.addFieldAndClose": "Crea e chiudi", "schemas.addFieldAndCreate": "Crea e aggiungi il campo", diff --git a/backend/i18n/source/frontend_nl.json b/backend/i18n/source/frontend_nl.json index 5ab379cad..6ba22de24 100644 --- a/backend/i18n/source/frontend_nl.json +++ b/backend/i18n/source/frontend_nl.json @@ -340,6 +340,7 @@ "contents.arrayNoFields": "Voeg eerst een genest veld toe om items toe te voegen.", "contents.assetsUpload": "Zet bestanden neer of klik", "contents.autotranslate": "Automatisch vertalen vanuit de hoofdtaal", + "contents.bulkFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.", "contents.changeStatusTo": "Verander inhoud item(s) in {action}", "contents.changeStatusToImmediately": "Zet onmiddellijk op {action}.", "contents.changeStatusToLater": "Zet op {action} op een latere datum en tijd.", @@ -355,7 +356,6 @@ "contents.currentStatusLabel": "Huidige versie", "contents.deleteConfirmText": "Wilt je de inhoud echt verwijderen?", "contents.deleteConfirmTitle": "Inhoud verwijderen", - "contents.deleteFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.", "contents.deleteManyConfirmText": "Weet je zeker dat je de geselecteerde inhoudsitems wilt verwijderen?", "contents.deleteReferrerConfirmText": "Er wordt naar de inhoud verwezen door een ander inhoudsitem. \n \n Wil je de inhoud echt verwijderen?", "contents.deleteReferrerConfirmTitle": "Inhoud verwijderen", @@ -364,7 +364,6 @@ "contents.draftNew": "Nieuw concept", "contents.draftStatus": "Nieuwe versie", "contents.editPageTitle": "Inhoud bewerken", - "contents.editTitle": "Inhoud bewerken", "contents.invariantFieldDescription": "Het veld '{fieldName}' van het inhoudsitem.", "contents.languageModeAll": "Alle talen", "contents.languageModeSingle": "Enkele taal", diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs index 4f472a1cf..62a739c3a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs @@ -10,7 +10,7 @@ using System.Collections.Generic; using System.Globalization; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Infrastructure; -using Squidex.Shared.Users; +using Squidex.Shared.Identity; using Squidex.Text; namespace Squidex.Domain.Apps.Core.HandleRules @@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules { if (@event is EnrichedUserEventBase userEvent) { - return userEvent.User?.DisplayName(); + return userEvent.User?.Claims.DisplayName(); } return null; @@ -191,7 +191,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules { if (@event is EnrichedCommentEvent commentEvent) { - return commentEvent.MentionedUser.DisplayName(); + return commentEvent.MentionedUser.Claims.DisplayName(); } return null; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs index a0725cbe5..aca73b166 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Scripting var isClient = !string.IsNullOrWhiteSpace(clientId); - return CreateUser(engine, user.Id, isClient, user.Email, user.DisplayName(), user.Claims); + return CreateUser(engine, user.Id, isClient, user.Email, user.Claims.DisplayName(), user.Claims); } public static ObjectWrapper Create(Engine engine, ClaimsPrincipal principal) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidExtension.cs index f74b05589..a246dcff2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/UserFluidExtension.cs @@ -9,6 +9,7 @@ using System; using System.Linq; using Fluid; using Fluid.Values; +using Squidex.Shared.Identity; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Core.Templates.Extensions @@ -26,7 +27,7 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions case "email": return new StringValue(user.Email); case "name": - return new StringValue(user.DisplayName()); + return new StringValue(user.Claims.DisplayName()); default: { var claim = user.Claims.FirstOrDefault(x => string.Equals(name, x.Type, StringComparison.OrdinalIgnoreCase)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs index 741d87d8b..5b98ddeb8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/GeoQueryTransformer.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Contents.Text; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Context.cs b/backend/src/Squidex.Domain.Apps.Entities/Context.cs index 335250bff..e35874636 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Context.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Context.cs @@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities User = user; - Permissions = User.Permissions(); + Permissions = User.Claims.Permissions(); IsFrontendClient = User.IsInClient(DefaultClients.Frontend); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs index 55652f5b6..84b82eddd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs @@ -25,7 +25,7 @@ using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.History { - public class NotifoService : IUserEventHandler + public class NotifoService : IUserEvents { private static readonly Duration MaxAge = Duration.FromHours(12); private readonly NotifoOptions options; @@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.History var userRequest = new UpsertUserDto { Id = user.Id, - FullName = user.DisplayName(), + FullName = user.Claims.DisplayName(), PreferredLanguage = "en", PreferredTimezone = null, Settings = settings, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs index bf22c8b94..4a43b9c8b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs @@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Core; using Squidex.Infrastructure; using Squidex.Infrastructure.Email; using Squidex.Log; +using Squidex.Shared.Identity; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Notifications @@ -86,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Notifications var vars = new TemplatesVars { Assigner = assigner, AppName = appName }; - if (user.HasConsent()) + if (user.Claims.HasConsent()) { return SendEmailAsync("ExistingUser", texts.ExistingUserSubject, @@ -152,13 +153,13 @@ namespace Squidex.Domain.Apps.Entities.Notifications if (vars.Assigner != null) { text = text.Replace("$ASSIGNER_EMAIL", vars.Assigner.Email); - text = text.Replace("$ASSIGNER_NAME", vars.Assigner.DisplayName()); + text = text.Replace("$ASSIGNER_NAME", vars.Assigner.Claims.DisplayName()); } if (vars.User != null) { text = text.Replace("$USER_EMAIL", vars.User.Email); - text = text.Replace("$USER_NAME", vars.User.DisplayName()); + text = text.Replace("$USER_NAME", vars.User.Claims.DisplayName()); } if (vars.ApiCallsLimit != null) diff --git a/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs index abacbaa8e..fcfb27540 100644 --- a/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs +++ b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure; using Squidex.Shared.Users; @@ -34,35 +33,26 @@ namespace Squidex.Domain.Users { Guard.NotNullOrEmpty(email, nameof(email)); - var created = false; - using (var scope = serviceProvider.CreateScope()) { - var userFactory = scope.ServiceProvider.GetRequiredService(); - var userManager = scope.ServiceProvider.GetRequiredService>(); + var userService = scope.ServiceProvider.GetRequiredService(); try { - var user = userFactory.Create(email); - - var result = await userManager.CreateAsync(user); - - if (result.Succeeded) + var user = await userService.CreateAsync(email, new UserValues { - created = true; + Invited = invited + }); - var values = new UserValues { DisplayName = email, Invited = invited }; - - await userManager.UpdateAsync(user, values); - } + return (user, true); } catch { } - var found = await userManager.FindByEmailWithClaimsAsync(email); + var found = await FindByIdOrEmailAsync(email); - return (found, created); + return (found, false); } } @@ -74,7 +64,7 @@ namespace Squidex.Domain.Users using (var scope = serviceProvider.CreateScope()) { - var userManager = scope.ServiceProvider.GetRequiredService>(); + var userService = scope.ServiceProvider.GetRequiredService(); var values = new UserValues { @@ -84,7 +74,7 @@ namespace Squidex.Domain.Users } }; - await userManager.UpdateAsync(id, values); + await userService.UpdateAsync(id, values); } } @@ -94,9 +84,9 @@ namespace Squidex.Domain.Users using (var scope = serviceProvider.CreateScope()) { - var userManager = scope.ServiceProvider.GetRequiredService>(); + var userService = scope.ServiceProvider.GetRequiredService(); - return await userManager.FindByIdWithClaimsAsync(id); + return await userService.FindByIdAsync(id); } } @@ -106,16 +96,15 @@ namespace Squidex.Domain.Users using (var scope = serviceProvider.CreateScope()) { - var userFactory = scope.ServiceProvider.GetRequiredService(); - var userManager = scope.ServiceProvider.GetRequiredService>(); + var userService = scope.ServiceProvider.GetRequiredService(); - if (userFactory.IsId(idOrEmail)) + if (idOrEmail.Contains("@")) { - return await userManager.FindByIdWithClaimsAsync(idOrEmail); + return await userService.FindByEmailAsync(idOrEmail); } else { - return await userManager.FindByEmailWithClaimsAsync(idOrEmail); + return await userService.FindByIdAsync(idOrEmail); } } } @@ -124,11 +113,11 @@ namespace Squidex.Domain.Users { using (var scope = serviceProvider.CreateScope()) { - var userManager = scope.ServiceProvider.GetRequiredService>(); + var userService = scope.ServiceProvider.GetRequiredService(); - var result = await userManager.QueryByEmailAsync(null); + var result = await userService.QueryAsync(take: int.MaxValue); - return result.OfType().ToList(); + return result.ToList(); } } @@ -138,11 +127,11 @@ namespace Squidex.Domain.Users using (var scope = serviceProvider.CreateScope()) { - var userManager = scope.ServiceProvider.GetRequiredService>(); + var userService = scope.ServiceProvider.GetRequiredService(); - var result = await userManager.QueryByEmailAsync(email); + var result = await userService.QueryAsync(email); - return result.OfType().ToList(); + return result.ToList(); } } @@ -152,12 +141,9 @@ namespace Squidex.Domain.Users using (var scope = serviceProvider.CreateScope()) { - var userManager = scope.ServiceProvider.GetRequiredService>(); - var userFactory = scope.ServiceProvider.GetRequiredService(); - - ids = ids.Where(x => userFactory.IsId(x)).ToArray(); + var userService = scope.ServiceProvider.GetRequiredService(); - var result = await userManager.QueryByIdsAync(ids); + var result = await userService.QueryAsync(ids); return result.OfType().ToDictionary(x => x.Id); } diff --git a/backend/src/Squidex.Domain.Users/DefaultUserService.cs b/backend/src/Squidex.Domain.Users/DefaultUserService.cs new file mode 100644 index 000000000..15cb93954 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/DefaultUserService.cs @@ -0,0 +1,408 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Squidex.Log; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users +{ + public sealed class DefaultUserService : IUserService + { + private readonly UserManager userManager; + private readonly IUserFactory userFactory; + private readonly IEnumerable userEvents; + private readonly ISemanticLog log; + + public DefaultUserService(UserManager userManager, IUserFactory userFactory, + IEnumerable userEvents, ISemanticLog log) + { + Guard.NotNull(userManager, nameof(userManager)); + Guard.NotNull(userFactory, nameof(userFactory)); + Guard.NotNull(userEvents, nameof(userEvents)); + Guard.NotNull(log, nameof(log)); + + this.userManager = userManager; + this.userFactory = userFactory; + this.userEvents = userEvents; + + this.log = log; + } + + public async Task IsEmptyAsync() + { + var result = await QueryAsync(null, 0, 0); + + return result.Total == 0; + } + + public string GetUserId(ClaimsPrincipal user) + { + Guard.NotNull(user, nameof(user)); + + return userManager.GetUserId(user); + } + + public async Task> QueryAsync(IEnumerable ids) + { + Guard.NotNull(ids, nameof(ids)); + + ids = ids.Where(userFactory.IsId); + + if (!ids.Any()) + { + return ResultList.CreateFrom(0); + } + + var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); + + var resolved = await ResolveAsync(users); + + return ResultList.Create(users.Count, resolved); + } + + public async Task> QueryAsync(string? query, int take, int skip) + { + IQueryable QueryUsers(string? email = null) + { + var result = userManager.Users; + + if (!string.IsNullOrWhiteSpace(email)) + { + var normalizedEmail = userManager.NormalizeEmail(email); + + result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); + } + + return result; + } + + var userItems = QueryUsers(query).Take(take).Skip(skip).ToList(); + var userTotal = QueryUsers(query).LongCount(); + + var resolved = await ResolveAsync(userItems); + + return ResultList.Create(userTotal, resolved); + } + + public Task> GetLoginsAsync(IUser user) + { + Guard.NotNull(user, nameof(user)); + + return userManager.GetLoginsAsync((IdentityUser)user.Identity); + } + + public Task HasPasswordAsync(IUser user) + { + Guard.NotNull(user, nameof(user)); + + return userManager.HasPasswordAsync((IdentityUser)user.Identity); + } + + public async Task FindByLoginAsync(string provider, string key) + { + Guard.NotNullOrEmpty(provider, nameof(provider)); + + var user = await userManager.FindByLoginAsync(provider, key); + + return await ResolveOptionalAsync(user); + } + + public async Task FindByEmailAsync(string email) + { + Guard.NotNullOrEmpty(email, nameof(email)); + + var user = await userManager.FindByEmailAsync(email); + + return await ResolveOptionalAsync(user); + } + + public async Task GetAsync(ClaimsPrincipal principal) + { + Guard.NotNull(principal, nameof(principal)); + + var user = await userManager.GetUserAsync(principal); + + return await ResolveOptionalAsync(user); + } + + public async Task FindByIdAsync(string id) + { + if (!userFactory.IsId(id)) + { + return null; + } + + var user = await userManager.FindByIdAsync(id); + + return await ResolveOptionalAsync(user); + } + + public async Task CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false) + { + Guard.NotNullOrEmpty(email, nameof(email)); + + var isFirst = !userManager.Users.Any(); + + var user = userFactory.Create(email); + + try + { + await userManager.CreateAsync(user).Throw(log); + + values ??= new UserValues(); + + if (string.IsNullOrWhiteSpace(values.DisplayName)) + { + values.DisplayName = email; + } + + if (isFirst) + { + var permissions = values.Permissions?.ToIds().ToList() ?? new List(); + + permissions.Add(Permissions.Admin); + + values.Permissions = new PermissionSet(permissions); + } + + await userManager.SyncClaims(user, values).Throw(log); + + if (!string.IsNullOrWhiteSpace(values.Password)) + { + await userManager.AddPasswordAsync(user, values.Password).Throw(log); + } + + if (!isFirst && lockAutomatically) + { + await userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log); + } + } + catch (Exception) + { + try + { + if (userFactory.IsId(user.Id)) + { + await userManager.DeleteAsync(user); + } + } + catch (Exception ex2) + { + log.LogError(ex2, w => w + .WriteProperty("action", "CleanupUser") + .WriteProperty("status", "Failed")); + } + + throw; + } + + var resolved = await ResolveAsync(user); + + foreach (var @events in userEvents) + { + @events.OnUserRegistered(resolved); + } + + if (HasConsentGiven(values, null!)) + { + foreach (var @events in userEvents) + { + @events.OnConsentGiven(resolved); + } + } + + return resolved; + } + + public Task SetPasswordAsync(string id, string password, string? oldPassword) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + return ForUserAsync(id, async user => + { + if (await userManager.HasPasswordAsync(user)) + { + await userManager.ChangePasswordAsync(user, oldPassword!, password).Throw(log); + } + else + { + await userManager.AddPasswordAsync(user, password).Throw(log); + } + }); + } + + public async Task UpdateAsync(string id, UserValues values) + { + Guard.NotNullOrEmpty(id, nameof(id)); + Guard.NotNull(values, nameof(values)); + + var user = await GetUserAsync(id); + + var oldUser = await ResolveAsync(user); + + if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) + { + await userManager.SetEmailAsync(user, values.Email).Throw(log); + await userManager.SetUserNameAsync(user, values.Email).Throw(log); + } + + await userManager.SyncClaims(user, values).Throw(log); + + if (!string.IsNullOrWhiteSpace(values.Password)) + { + if (await userManager.HasPasswordAsync(user)) + { + await userManager.RemovePasswordAsync(user).Throw(log); + } + + await userManager.AddPasswordAsync(user, values.Password).Throw(log); + } + + var resolved = await ResolveAsync(user); + + foreach (var @events in userEvents) + { + @events.OnUserUpdated(resolved); + } + + if (HasConsentGiven(values, oldUser)) + { + foreach (var @events in userEvents) + { + @events.OnConsentGiven(resolved); + } + } + + return resolved; + } + + public Task LockAsync(string id) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log)); + } + + public Task UnlockAsync(string id) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, null).Throw(log)); + } + + public Task AddLoginAsync(string id, ExternalLoginInfo externalLogin) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + return ForUserAsync(id, user => userManager.AddLoginAsync(user, externalLogin).Throw(log)); + } + + public Task RemoveLoginAsync(string id, string loginProvider, string providerKey) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + return ForUserAsync(id, user => userManager.RemoveLoginAsync(user, loginProvider, providerKey).Throw(log)); + } + + public async Task DeleteAsync(string id) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + var user = await GetUserAsync(id); + + var resolved = await ResolveAsync(user); + + await userManager.DeleteAsync(user).Throw(log); + + foreach (var @events in userEvents) + { + @events.OnUserDeleted(resolved); + } + } + + private async Task ForUserAsync(string id, Func action) + { + var user = await GetUserAsync(id); + + await action(user); + + return await ResolveAsync(user); + } + + private async Task GetUserAsync(string id) + { + if (!userFactory.IsId(id)) + { + throw new DomainObjectNotFoundException(id); + } + + var user = await userManager.FindByIdAsync(id); + + if (user == null) + { + throw new DomainObjectNotFoundException(id); + } + + return user; + } + + private Task ResolveAsync(IEnumerable users) + { + return Task.WhenAll(users.Select(async user => + { + return await ResolveAsync(user); + })); + } + + private async Task ResolveAsync(IdentityUser user) + { + var claims = await userManager.GetClaimsAsync(user); + + if (!claims.Any(x => string.Equals(x.Type, SquidexClaimTypes.DisplayName, StringComparison.OrdinalIgnoreCase))) + { + claims.Add(new Claim(SquidexClaimTypes.DisplayName, user.Email)); + } + + return new UserWithClaims(user, claims.ToList()); + } + + private async Task ResolveOptionalAsync(IdentityUser? user) + { + if (user == null) + { + return null; + } + + return await ResolveAsync(user); + } + + private static bool HasConsentGiven(UserValues values, IUser? oldUser) + { + if (values.Consent == true && oldUser?.Claims.HasConsent() != true) + { + return true; + } + + return values.ConsentForEmails == true && oldUser?.Claims.HasConsentForEmails() != true; + } + + private static DateTimeOffset LockoutDate() + { + return DateTimeOffset.UtcNow.AddYears(100); + } + } +} diff --git a/backend/src/Squidex.Domain.Users/IUserEventHandler.cs b/backend/src/Squidex.Domain.Users/IUserEventHandler.cs deleted file mode 100644 index 0bf9f8a48..000000000 --- a/backend/src/Squidex.Domain.Users/IUserEventHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Shared.Users; - -namespace Squidex.Domain.Users -{ - public interface IUserEventHandler - { - void OnUserRegistered(IUser user) - { - } - - void OnUserUpdated(IUser user) - { - } - - void OnConsentGiven(IUser user) - { - } - } -} diff --git a/backend/src/Squidex.Domain.Users/IUserEvents.cs b/backend/src/Squidex.Domain.Users/IUserEvents.cs index 8383b7770..26f51ea75 100644 --- a/backend/src/Squidex.Domain.Users/IUserEvents.cs +++ b/backend/src/Squidex.Domain.Users/IUserEvents.cs @@ -11,10 +11,20 @@ namespace Squidex.Domain.Users { public interface IUserEvents { - void OnUserRegistered(IUser user); + void OnUserRegistered(IUser user) + { + } - void OnUserUpdated(IUser user); + void OnUserUpdated(IUser user) + { + } - void OnConsentGiven(IUser user); + void OnUserDeleted(IUser user) + { + } + + void OnConsentGiven(IUser user) + { + } } } diff --git a/backend/src/Squidex.Domain.Users/IUserService.cs b/backend/src/Squidex.Domain.Users/IUserService.cs new file mode 100644 index 000000000..d70dc8cc8 --- /dev/null +++ b/backend/src/Squidex.Domain.Users/IUserService.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users +{ + public interface IUserService + { + Task> QueryAsync(IEnumerable ids); + + Task> QueryAsync(string? query = null, int take = 10, int skip = 0); + + string GetUserId(ClaimsPrincipal user); + + Task> GetLoginsAsync(IUser user); + + Task HasPasswordAsync(IUser user); + + Task IsEmptyAsync(); + + Task CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false); + + Task GetAsync(ClaimsPrincipal principal); + + Task FindByEmailAsync(string email); + + Task FindByIdAsync(string id); + + Task FindByLoginAsync(string provider, string key); + + Task SetPasswordAsync(string id, string password, string? oldPassword = null); + + Task AddLoginAsync(string id, ExternalLoginInfo externalLogin); + + Task RemoveLoginAsync(string id, string loginProvider, string providerKey); + + Task LockAsync(string id); + + Task UnlockAsync(string id); + + Task UpdateAsync(string id, UserValues values); + + Task DeleteAsync(string id); + } +} diff --git a/backend/src/Squidex.Domain.Users/UserEvents.cs b/backend/src/Squidex.Domain.Users/UserEvents.cs deleted file mode 100644 index 4bf14785a..000000000 --- a/backend/src/Squidex.Domain.Users/UserEvents.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Infrastructure; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Users -{ - public sealed class UserEvents : IUserEvents - { - private readonly IEnumerable userEventHandlers; - - public UserEvents(IEnumerable userEventHandlers) - { - Guard.NotNull(userEventHandlers, nameof(userEventHandlers)); - - this.userEventHandlers = userEventHandlers; - } - - public void OnUserRegistered(IUser user) - { - foreach (var handler in userEventHandlers) - { - handler.OnUserRegistered(user); - } - } - - public void OnUserUpdated(IUser user) - { - foreach (var handler in userEventHandlers) - { - handler.OnUserUpdated(user); - } - } - - public void OnConsentGiven(IUser user) - { - foreach (var handler in userEventHandlers) - { - handler.OnConsentGiven(user); - } - } - } -} diff --git a/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs b/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs index c4f39e65e..d30be87c5 100644 --- a/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ b/backend/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -9,263 +9,158 @@ using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; +using Squidex.Log; +using Squidex.Shared.Identity; namespace Squidex.Domain.Users { - public static class UserManagerExtensions + internal static class UserManagerExtensions { - public static async Task GetUserWithClaimsAsync(this UserManager userManager, ClaimsPrincipal principal) + public static async Task Throw(this Task task, ISemanticLog log) { - if (principal == null) - { - return null; - } + var result = await task; - var user = await userManager.FindByIdWithClaimsAsync(userManager.GetUserId(principal)); - - return user; - } - - public static async Task ResolveUserAsync(this UserManager userManager, IdentityUser user) - { - if (user == null) + static string Localize(IdentityError error) { - return null; + if (!string.IsNullOrWhiteSpace(error.Code)) + { + return T.Get($"dotnet_identity_{error.Code}", error.Description); + } + else + { + return error.Description; + } } - var claims = await userManager.GetClaimsAsync(user); - - return new UserWithClaims(user, claims); - } - - public static async Task FindByIdWithClaimsAsync(this UserManager userManager, string id) - { - if (id == null) + if (!result.Succeeded) { - return null; - } + var errorMessageBuilder = new StringBuilder(); - var user = await userManager.FindByIdAsync(id); - - return await userManager.ResolveUserAsync(user); - } - - public static async Task FindByEmailWithClaimsAsync(this UserManager userManager, string email) - { - if (email == null) - { - return null; - } + foreach (var error in result.Errors) + { + errorMessageBuilder.Append(error.Code); + errorMessageBuilder.Append(": "); + errorMessageBuilder.AppendLine(error.Description); + } - var user = await userManager.FindByEmailAsync(email); + var errorMessage = errorMessageBuilder.ToString(); - return await userManager.ResolveUserAsync(user); - } + log.LogError(errorMessage, (ctx, w) => w + .WriteProperty("action", "IdentityOperation") + .WriteProperty("status", "Failed") + .WriteProperty("message", ctx)); - public static async Task FindByLoginWithClaimsAsync(this UserManager userManager, string loginProvider, string providerKey) - { - if (loginProvider == null || providerKey == null) - { - return null; + throw new ValidationException(result.Errors.Select(x => new ValidationError(Localize(x))).ToList()); } - - var user = await userManager.FindByLoginAsync(loginProvider, providerKey); - - return await userManager.ResolveUserAsync(user); - } - - public static Task CountByEmailAsync(this UserManager userManager, string? email = null) - { - var count = QueryUsers(userManager, email).LongCount(); - - return Task.FromResult(count); - } - - public static async Task> QueryByIdsAync(this UserManager userManager, string[] ids) - { - var users = userManager.Users.Where(x => ids.Contains(x.Id)).ToList(); - - var result = await userManager.ResolveUsersAsync(users); - - return result.ToList(); } - public static async Task> QueryByEmailAsync(this UserManager userManager, string? email = null, int take = 10, int skip = 0) + public static async Task SyncClaims(this UserManager userManager, IdentityUser user, UserValues values) { - var users = QueryUsers(userManager, email).Skip(skip).Take(take).ToList(); + var current = await userManager.GetClaimsAsync(user); - var result = await userManager.ResolveUsersAsync(users); + var claimsToRemove = new List(); + var claimsToAdd = new List(); - return result.ToList(); - } - - public static Task ResolveUsersAsync(this UserManager userManager, IEnumerable users) - { - return Task.WhenAll(users.Select(async user => + void RemoveClaims(Func predicate) { - return (await userManager.ResolveUserAsync(user))!; - })); - } - - public static IQueryable QueryUsers(UserManager userManager, string? email = null) - { - var result = userManager.Users; + claimsToAdd.RemoveAll(x => predicate(x)); + claimsToRemove.AddRange(current.Where(predicate)); + } - if (!string.IsNullOrWhiteSpace(email)) + void AddClaim(string type, string value) { - var normalizedEmail = userManager.NormalizeEmail(email); - - result = result.Where(x => x.NormalizedEmail.Contains(normalizedEmail)); + claimsToAdd.Add(new Claim(type, value)); } - return result; - } - - public static async Task CreateAsync(this UserManager userManager, IUserFactory factory, UserValues values) - { - var user = factory.Create(values.Email); - - try + void SyncString(string type, string? value) { - await DoChecked(() => userManager.CreateAsync(user)); - await DoChecked(() => values.SyncClaims(userManager, user)); - - if (!string.IsNullOrWhiteSpace(values.Password)) + if (value != null) { - await DoChecked(() => userManager.AddPasswordAsync(user, values.Password)); - } - } - catch - { - await userManager.DeleteAsync(user); + RemoveClaims(x => x.Type == type); - throw; + if (!string.IsNullOrWhiteSpace(value)) + { + AddClaim(type, value); + } + } } - return (await userManager.ResolveUserAsync(user))!; - } - - public static async Task UpdateAsync(this UserManager userManager, string id, UserValues values) - { - var user = await userManager.FindByIdAsync(id); - - if (user == null) + void SyncBoolean(string type, bool? value) { - throw new DomainObjectNotFoundException(id); + if (value != null) + { + RemoveClaims(x => x.Type == type); + + if (value == true) + { + AddClaim(type, value.ToString()!); + } + } } - await UpdateAsync(userManager, user, values); + SyncString(SquidexClaimTypes.ClientSecret, values.ClientSecret); + SyncString(SquidexClaimTypes.DisplayName, values.DisplayName); + SyncString(SquidexClaimTypes.PictureUrl, values.PictureUrl); - return (await userManager.ResolveUserAsync(user))!; - } + SyncBoolean(SquidexClaimTypes.Hidden, values.Hidden); + SyncBoolean(SquidexClaimTypes.Invited, values.Invited); + SyncBoolean(SquidexClaimTypes.Consent, values.Consent); + SyncBoolean(SquidexClaimTypes.ConsentForEmails, values.ConsentForEmails); - public static Task GenerateClientSecretAsync(this UserManager userManager, IdentityUser user) - { - var update = new UserValues + if (values.Permissions != null) { - ClientSecret = RandomHash.New() - }; + RemoveClaims(x => x.Type == SquidexClaimTypes.Permissions); - return update.SyncClaims(userManager, user); - } + foreach (var permission in values.Permissions) + { + AddClaim(SquidexClaimTypes.Permissions, permission.Id); + } + } - public static async Task UpdateSafeAsync(this UserManager userManager, IdentityUser user, UserValues values) - { - try + if (values.Properties != null) { - await userManager.UpdateAsync(user, values); + RemoveClaims(x => x.Type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase)); - return IdentityResult.Success; - } - catch (ValidationException ex) - { - return IdentityResult.Failed(ex.Errors.Select(x => new IdentityError { Description = x.Message }).ToArray()); + foreach (var (name, value) in values.Properties) + { + AddClaim($"{SquidexClaimTypes.CustomPrefix}:{name}", value); + } } - } - public static async Task UpdateAsync(this UserManager userManager, IdentityUser user, UserValues values) - { - Guard.NotNull(user, nameof(user)); - Guard.NotNull(values, nameof(values)); - - if (!string.IsNullOrWhiteSpace(values.Email) && values.Email != user.Email) + if (values.CustomClaims != null) { - await DoChecked(() => userManager.SetEmailAsync(user, values.Email)); - await DoChecked(() => userManager.SetUserNameAsync(user, values.Email)); - } - - await DoChecked(() => values.SyncClaims(userManager, user)); + foreach (var group in values.CustomClaims.GroupBy(x => x.Type)) + { + RemoveClaims(x => x.Type == group.Key); - if (!string.IsNullOrWhiteSpace(values.Password)) - { - await DoChecked(() => userManager.RemovePasswordAsync(user)); - await DoChecked(() => userManager.AddPasswordAsync(user, values.Password)); + foreach (var claim in group) + { + AddClaim(claim.Type, claim.Value); + } + } } - } - - public static async Task LockAsync(this UserManager userManager, string id) - { - var user = await userManager.FindByIdAsync(id); - if (user == null) + if (claimsToRemove.Count > 0) { - throw new DomainObjectNotFoundException(id); - } - - await DoChecked(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100))); - - return (await userManager.ResolveUserAsync(user))!; - } + var result = await userManager.RemoveClaimsAsync(user, claimsToRemove); - public static async Task UnlockAsync(this UserManager userManager, string id) - { - var user = await userManager.FindByIdAsync(id); - - if (user == null) - { - throw new DomainObjectNotFoundException(id); + if (!result.Succeeded) + { + return result; + } } - await DoChecked(() => userManager.SetLockoutEndDateAsync(user, null)); - - return (await userManager.ResolveUserAsync(user))!; - } - - private static async Task DoChecked(Func> action) - { - var result = await action(); - - if (!result.Succeeded) + if (claimsToAdd.Count > 0) { - throw new ValidationException(result.Errors.Select(x => new ValidationError(x.Localize())).ToList()); + return await userManager.AddClaimsAsync(user, claimsToAdd); } - } - public static Task SyncClaims(this UserManager userManager, IdentityUser user, UserValues values) - { - return values.SyncClaims(userManager, user); - } - - public static string Localize(this IdentityResult result) - { - return string.Join(". ", result.Errors.Select(x => x.Localize())); - } - - public static string Localize(this IdentityError error) - { - if (!string.IsNullOrWhiteSpace(error.Code)) - { - return T.Get($"dotnet_identity_{error.Code}", error.Description); - } - else - { - return error.Description; - } + return IdentityResult.Success; } } } diff --git a/backend/src/Squidex.Domain.Users/UserValues.cs b/backend/src/Squidex.Domain.Users/UserValues.cs index f1bd1a763..5726d02d8 100644 --- a/backend/src/Squidex.Domain.Users/UserValues.cs +++ b/backend/src/Squidex.Domain.Users/UserValues.cs @@ -5,14 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; -using System.Linq; using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; using Squidex.Infrastructure.Security; -using Squidex.Shared.Identity; namespace Squidex.Domain.Users { @@ -41,109 +36,5 @@ namespace Squidex.Domain.Users public List? CustomClaims { get; set; } public List<(string Name, string Value)>? Properties { get; set; } - - internal async Task SyncClaims(UserManager userManager, IdentityUser user) - { - var current = await userManager.GetClaimsAsync(user); - - var claimsToRemove = new List(); - var claimsToAdd = new List(); - - void RemoveClaims(Func predicate) - { - claimsToAdd.RemoveAll(x => predicate(x)); - claimsToRemove.AddRange(current.Where(predicate)); - } - - void AddClaim(string type, string value) - { - claimsToAdd.Add(new Claim(type, value)); - } - - void SyncString(string type, string? value) - { - if (value != null) - { - RemoveClaims(x => x.Type == type); - - if (!string.IsNullOrWhiteSpace(value)) - { - AddClaim(type, value); - } - } - } - - void SyncBoolean(string type, bool? value) - { - if (value != null) - { - RemoveClaims(x => x.Type == type); - - if (value == true) - { - AddClaim(type, value.ToString()!); - } - } - } - - SyncString(SquidexClaimTypes.ClientSecret, ClientSecret); - SyncString(SquidexClaimTypes.DisplayName, DisplayName); - SyncString(SquidexClaimTypes.PictureUrl, PictureUrl); - - SyncBoolean(SquidexClaimTypes.Hidden, Hidden); - SyncBoolean(SquidexClaimTypes.Invited, Invited); - SyncBoolean(SquidexClaimTypes.Consent, Consent); - SyncBoolean(SquidexClaimTypes.ConsentForEmails, ConsentForEmails); - - if (Permissions != null) - { - RemoveClaims(x => x.Type == SquidexClaimTypes.Permissions); - - foreach (var permission in Permissions) - { - AddClaim(SquidexClaimTypes.Permissions, permission.Id); - } - } - - if (Properties != null) - { - RemoveClaims(x => x.Type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase)); - - foreach (var (name, value) in Properties) - { - AddClaim($"{SquidexClaimTypes.CustomPrefix}:{name}", value); - } - } - - if (CustomClaims != null) - { - foreach (var group in CustomClaims.GroupBy(x => x.Type)) - { - RemoveClaims(x => x.Type == group.Key); - - foreach (var claim in group) - { - AddClaim(claim.Type, claim.Value); - } - } - } - - if (claimsToRemove.Count > 0) - { - var result = await userManager.RemoveClaimsAsync(user, claimsToRemove); - - if (!result.Succeeded) - { - return result; - } - } - - if (claimsToAdd.Count > 0) - { - return await userManager.AddClaimsAsync(user, claimsToAdd); - } - - return IdentityResult.Success; - } } } diff --git a/backend/src/Squidex.Domain.Users/UserWithClaims.cs b/backend/src/Squidex.Domain.Users/UserWithClaims.cs index 541c42cd1..b376b1c11 100644 --- a/backend/src/Squidex.Domain.Users/UserWithClaims.cs +++ b/backend/src/Squidex.Domain.Users/UserWithClaims.cs @@ -7,20 +7,16 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Security.Claims; using Microsoft.AspNetCore.Identity; -using Squidex.Infrastructure; using Squidex.Shared.Users; namespace Squidex.Domain.Users { - public sealed class UserWithClaims : IUser + internal sealed class UserWithClaims : IUser { public IdentityUser Identity { get; } - public List Claims { get; } - public string Id { get { return Identity.Id; } @@ -36,19 +32,15 @@ namespace Squidex.Domain.Users get { return Identity.LockoutEnd > DateTime.UtcNow; } } - IReadOnlyList IUser.Claims - { - get { return Claims; } - } + public IReadOnlyList Claims { get; } - public UserWithClaims(IdentityUser user, IEnumerable claims) - { - Guard.NotNull(user, nameof(user)); - Guard.NotNull(claims, nameof(claims)); + object IUser.Identity => Identity; + public UserWithClaims(IdentityUser user, IReadOnlyList claims) + { Identity = user; - Claims = claims.ToList(); + Claims = claims; } } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedCompareAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedCompareAttribute.cs index 766b11e31..13e3fdfde 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedCompareAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedCompareAttribute.cs @@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Validation var other = T.Get($"common.{OtherProperty.ToCamelCase()}", OtherProperty); - return T.Get("annotations_Compare", base.FormatErrorMessage(name), new { property, other }); + return T.Get("annotations_Compare", base.FormatErrorMessage(name), new { name = property, other }); } } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedEmailAddressAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedEmailAddressAttribute.cs index 5c6e37e0b..15bc81a59 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedEmailAddressAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedEmailAddressAttribute.cs @@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Validation { var property = T.Get($"common.{name.ToCamelCase()}", name); - return T.Get("annotations_EmailAddress", base.FormatErrorMessage(name), new { property }); + return T.Get("annotations_EmailAddress", base.FormatErrorMessage(name), new { name = property }); } } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedRangeAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedRangeAttribute.cs index ca3e41fd6..5e3b7a483 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedRangeAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedRangeAttribute.cs @@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.Validation var min = Minimum; var max = Maximum; - return T.Get("annotations_Range", base.FormatErrorMessage(name), new { property, min, max }); + return T.Get("annotations_Range", base.FormatErrorMessage(name), new { name = property, min, max }); } } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedRegularExpressionAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedRegularExpressionAttribute.cs index 174ef3ced..cef55b31c 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedRegularExpressionAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedRegularExpressionAttribute.cs @@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Validation { var property = T.Get($"common.{name.ToCamelCase()}", name); - return T.Get("annotations_RegularExpression", base.FormatErrorMessage(name), new { property }); + return T.Get("annotations_RegularExpression", base.FormatErrorMessage(name), new { name = property }); } } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedRequiredAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedRequiredAttribute.cs index 50b0c334b..5fa2d84da 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedRequiredAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedRequiredAttribute.cs @@ -19,7 +19,7 @@ namespace Squidex.Infrastructure.Validation { var property = T.Get($"common.{name.ToCamelCase()}", name); - return T.Get("annotations_Required", base.FormatErrorMessage(name), new { property }); + return T.Get("annotations_Required", base.FormatErrorMessage(name), new { name = property }); } } } diff --git a/backend/src/Squidex.Infrastructure/Validation/LocalizedStringLengthAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/LocalizedStringLengthAttribute.cs index 951cf1c3a..f610dda35 100644 --- a/backend/src/Squidex.Infrastructure/Validation/LocalizedStringLengthAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/LocalizedStringLengthAttribute.cs @@ -25,7 +25,7 @@ namespace Squidex.Infrastructure.Validation var min = MinimumLength; var max = MaximumLength; - var args = new { property, min, max }; + var args = new { name = property, min, max }; if (min > 0) { diff --git a/backend/src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs b/backend/src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs deleted file mode 100644 index d4f8ee4d8..000000000 --- a/backend/src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Squidex.Infrastructure.Security; - -namespace Squidex.Shared.Identity -{ - public static class ClaimsPrincipalExtensions - { - public static void SetDisplayName(this ClaimsIdentity identity, string displayName) - { - identity.AddClaim(new Claim(SquidexClaimTypes.DisplayName, displayName)); - } - - public static void SetPictureUrl(this ClaimsIdentity identity, string pictureUrl) - { - identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl)); - } - - public static PermissionSet Permissions(this ClaimsPrincipal principal) - { - return new PermissionSet(principal.Claims - .Where(x => - (x.Type == SquidexClaimTypes.Permissions || - x.Type == SquidexClaimTypes.PermissionsClient) && - !string.IsNullOrWhiteSpace(x.Value)) - .Select(x => new Permission(x.Value))); - } - - public static IEnumerable GetSquidexClaims(this ClaimsPrincipal principal) - { - return principal.Claims - .Where(x => - (x.Type.StartsWith(SquidexClaimTypes.Prefix, StringComparison.Ordinal) || - x.Type.StartsWith(SquidexClaimTypes.PrefixClient, StringComparison.Ordinal)) && - !string.IsNullOrWhiteSpace(x.Value)); - } - } -} diff --git a/backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs b/backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs index c2594c52e..38d4ea32a 100644 --- a/backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs +++ b/backend/src/Squidex.Shared/Identity/SquidexClaimTypes.cs @@ -9,31 +9,25 @@ namespace Squidex.Shared.Identity { public static class SquidexClaimTypes { - public static readonly string DisplayName = "urn:squidex:name"; - - public static readonly string PictureUrl = "urn:squidex:picture"; - - public static readonly string NotifoKey = "urn:squidex:notifo"; + public static readonly string ClientSecret = "urn:squidex:clientSecret"; public static readonly string Consent = "urn:squidex:consent"; public static readonly string ConsentForEmails = "urn:squidex:consent:emails"; + public static readonly string CustomPrefix = "urn:squidex:custom"; + + public static readonly string DisplayName = "urn:squidex:name"; + public static readonly string Hidden = "urn:squidex:hidden"; public static readonly string Invited = "urn:squidex:invited"; - public static readonly string CustomPrefix = "urn:squidex:custom"; + public static readonly string NotifoKey = "urn:squidex:notifo"; public static readonly string Permissions = "urn:squidex:permissions"; - public static readonly string PermissionsClient = "client_urn:squidex:permissions"; - - public static readonly string ClientSecret = "urn:squidex:clientSecret"; - - public static readonly string Prefix = "urn:squidex:"; - - public static readonly string PrefixClient = "client_urn:squidex:"; + public static readonly string PictureUrl = "urn:squidex:picture"; public static readonly string PictureUrlStore = "store"; } diff --git a/backend/src/Squidex.Shared/Identity/SquidexClaimsExtensions.cs b/backend/src/Squidex.Shared/Identity/SquidexClaimsExtensions.cs new file mode 100644 index 000000000..c2dbbd48e --- /dev/null +++ b/backend/src/Squidex.Shared/Identity/SquidexClaimsExtensions.cs @@ -0,0 +1,159 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Squidex.Infrastructure.Security; + +namespace Squidex.Shared.Identity +{ + public static class SquidexClaimsExtensions + { + private const string ClientPrefix = "client_"; + + public static PermissionSet Permissions(this IEnumerable user) + { + return new PermissionSet(user.GetClaims(SquidexClaimTypes.Permissions).Select(x => new Permission(x.Value))); + } + + public static bool IsHidden(this IEnumerable user) + { + return user.HasClaimValue(SquidexClaimTypes.Hidden, "true"); + } + + public static bool HasConsent(this IEnumerable user) + { + return user.HasClaimValue(SquidexClaimTypes.Consent, "true"); + } + + public static bool HasConsentForEmails(this IEnumerable user) + { + return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true"); + } + + public static bool HasDisplayName(this IEnumerable user) + { + return user.HasClaim(SquidexClaimTypes.DisplayName); + } + + public static bool HasPictureUrl(this IEnumerable user) + { + return user.HasClaim(SquidexClaimTypes.PictureUrl); + } + + public static bool IsPictureUrlStored(this IEnumerable user) + { + return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); + } + + public static string? ClientSecret(this IEnumerable user) + { + return user.GetClaimValue(SquidexClaimTypes.ClientSecret); + } + + public static string? PictureUrl(this IEnumerable user) + { + return user.GetClaimValue(SquidexClaimTypes.PictureUrl); + } + + public static string? DisplayName(this IEnumerable user) + { + return user.GetClaimValue(SquidexClaimTypes.DisplayName); + } + + public static bool HasClaim(this IEnumerable user, string type) + { + return user.GetClaims(type).Any(); + } + + public static bool HasClaimValue(this IEnumerable user, string type, string value) + { + return user.GetClaims(type).Any(x => string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase)); + } + + public static IEnumerable GetSquidexClaims(this IEnumerable user) + { + const string prefix = "urn:squidex:"; + + foreach (var claim in user) + { + var type = GetType(claim); + + if (type.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + yield return claim; + } + } + } + + public static IEnumerable<(string Name, string Value)> GetCustomProperties(this IEnumerable user) + { + foreach (var claim in user) + { + var type = GetType(claim); + + if (type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase)) + { + var name = type[(SquidexClaimTypes.CustomPrefix.Length + 1)..].ToString(); + + yield return (name, claim.Value); + } + } + } + + public static string? PictureNormalizedUrl(this IEnumerable user) + { + var url = user.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; + + if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) + { + if (url.Contains("?")) + { + url += "&d=404"; + } + else + { + url += "?d=404"; + } + } + + return url; + } + + private static string? GetClaimValue(this IEnumerable user, string type) + { + return user.GetClaims(type).FirstOrDefault()?.Value; + } + + private static IEnumerable GetClaims(this IEnumerable user, string request) + { + foreach (var claim in user) + { + var type = GetType(claim); + + if (type.Equals(request, StringComparison.OrdinalIgnoreCase)) + { + yield return claim; + } + } + } + + private static ReadOnlySpan GetType(Claim claim) + { + var type = claim.Type.AsSpan(); + + if (type.StartsWith(ClientPrefix, StringComparison.OrdinalIgnoreCase)) + { + type = type[ClientPrefix.Length..]; + } + + return type; + } + } +} diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index e93b41b0e..f51d4b34e 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -463,9 +463,6 @@ La longitude deve essere tra -180 and 180. - - E' possibile impostare la geolocalizzazione solo impostando latitudine e longitudine. - Errore nel json,, atteso un number. @@ -931,6 +928,9 @@ Acconsento + + You cannot delete yourself. + Operazione non riuscita diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index d1b2c183b..edee5068b 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -463,9 +463,6 @@ Lengtegraad moet tussen -180 en 180 liggen. - - Geolocatie kan alleen de eigenschap lengte- en breedtegraad hebben. - Ongeldig json-type, verwacht aantal. @@ -931,6 +928,9 @@ Toestemming + + You cannot delete yourself. + Bewerking mislukt diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index 65dc55e89..9507a3795 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -463,9 +463,6 @@ Longitude must be between -180 and 180. - - Geolocation can only have latitude and longitude property. - Invalid json type, expected number. @@ -931,6 +928,9 @@ Consent + + You cannot delete yourself. + Operation failed diff --git a/backend/src/Squidex.Shared/Users/ClientUser.cs b/backend/src/Squidex.Shared/Users/ClientUser.cs index 88f840eff..52419312e 100644 --- a/backend/src/Squidex.Shared/Users/ClientUser.cs +++ b/backend/src/Squidex.Shared/Users/ClientUser.cs @@ -18,19 +18,6 @@ namespace Squidex.Shared.Users private readonly RefToken token; private readonly List claims; - public ClientUser(RefToken token) - { - Guard.NotNull(token, nameof(token)); - - this.token = token; - - claims = new List - { - new Claim(OpenIdClaims.ClientId, token.Identifier), - new Claim(SquidexClaimTypes.DisplayName, token.ToString()) - }; - } - public string Id { get { return token.Identifier; } @@ -50,5 +37,20 @@ namespace Squidex.Shared.Users { get { return claims; } } + + public object Identity => throw new System.NotImplementedException(); + + public ClientUser(RefToken token) + { + Guard.NotNull(token, nameof(token)); + + this.token = token; + + claims = new List + { + new Claim(OpenIdClaims.ClientId, token.Identifier), + new Claim(SquidexClaimTypes.DisplayName, token.ToString()) + }; + } } } diff --git a/backend/src/Squidex.Shared/Users/IUser.cs b/backend/src/Squidex.Shared/Users/IUser.cs index 2c0534095..8e9253fb5 100644 --- a/backend/src/Squidex.Shared/Users/IUser.cs +++ b/backend/src/Squidex.Shared/Users/IUser.cs @@ -12,11 +12,13 @@ namespace Squidex.Shared.Users { public interface IUser { + bool IsLocked { get; } + string Id { get; } string Email { get; } - bool IsLocked { get; } + object Identity { get; } IReadOnlyList Claims { get; } } diff --git a/backend/src/Squidex.Shared/Users/UserExtensions.cs b/backend/src/Squidex.Shared/Users/UserExtensions.cs deleted file mode 100644 index 89f9e7dbe..000000000 --- a/backend/src/Squidex.Shared/Users/UserExtensions.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Infrastructure.Security; -using Squidex.Shared.Identity; - -namespace Squidex.Shared.Users -{ - public static class UserExtensions - { - public static PermissionSet Permissions(this IUser user) - { - return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x))); - } - - public static bool IsInvited(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.Invited, "true"); - } - - public static bool IsHidden(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.Hidden, "true"); - } - - public static bool HasConsent(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.Consent, "true"); - } - - public static bool HasConsentForEmails(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true"); - } - - public static bool HasDisplayName(this IUser user) - { - return user.HasClaim(SquidexClaimTypes.DisplayName); - } - - public static bool HasPictureUrl(this IUser user) - { - return user.HasClaim(SquidexClaimTypes.PictureUrl); - } - - public static bool IsPictureUrlStored(this IUser user) - { - return user.HasClaimValue(SquidexClaimTypes.PictureUrl, SquidexClaimTypes.PictureUrlStore); - } - - public static string? ClientSecret(this IUser user) - { - return user.GetClaimValue(SquidexClaimTypes.ClientSecret); - } - - public static string? PictureUrl(this IUser user) - { - return user.GetClaimValue(SquidexClaimTypes.PictureUrl); - } - - public static string? DisplayName(this IUser user) - { - return user.GetClaimValue(SquidexClaimTypes.DisplayName); - } - - public static string? GetClaimValue(this IUser user, string type) - { - return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value; - } - - public static string[] GetClaimValues(this IUser user, string type) - { - return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Value).ToArray(); - } - - public static List<(string Name, string Value)> GetCustomProperties(this IUser user) - { - return user.Claims.Where(x => x.Type.StartsWith(SquidexClaimTypes.CustomPrefix, StringComparison.OrdinalIgnoreCase)) - .Select(x => (x.Type[(SquidexClaimTypes.CustomPrefix.Length + 1)..], x.Value)).ToList(); - } - - public static bool HasClaim(this IUser user, string type) - { - return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); - } - - public static bool HasClaimValue(this IUser user, string type, string value) - { - return user.Claims.Any(x => - string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase) && - string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase)); - } - - public static string? PictureNormalizedUrl(this IUser user) - { - var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; - - if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) - { - if (url.Contains("?")) - { - url += "&d=404"; - } - else - { - url += "?d=404"; - } - } - - return url; - } - } -} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 24ec24e27..c4dee1df7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; @@ -41,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}/clients/")] - [ProducesResponseType(typeof(ClientsDto), 200)] + [ProducesResponseType(typeof(ClientsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppClientsRead)] [ApiCosts(0)] public IActionResult GetClients(string app) @@ -100,7 +101,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPut] [Route("apps/{app}/clients/{id}/")] - [ProducesResponseType(typeof(ClientsDto), 200)] + [ProducesResponseType(typeof(ClientsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppClientsUpdate)] [ApiCosts(1)] public async Task PutClient(string app, string id, [FromBody] UpdateClientDto request) @@ -126,7 +127,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/clients/{id}/")] - [ProducesResponseType(typeof(ClientsDto), 200)] + [ProducesResponseType(typeof(ClientsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppClientsDelete)] [ApiCosts(1)] public async Task DeleteClient(string app, string id) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index 4e5ad1c62..cf78feb99 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; @@ -50,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}/contributors/")] - [ProducesResponseType(typeof(ContributorsDto), 200)] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContributorsRead)] [ApiCosts(0)] public IActionResult GetContributors(string app) @@ -99,7 +100,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/contributors/me/")] - [ProducesResponseType(typeof(ContributorsDto), 200)] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] [ApiPermission] [ApiCosts(1)] public async Task DeleteMyself(string app) @@ -122,7 +123,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/contributors/{id}/")] - [ProducesResponseType(typeof(ContributorsDto), 200)] + [ProducesResponseType(typeof(ContributorsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContributorsRevoke)] [ApiCosts(1)] public async Task DeleteContributor(string app, string id) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index 8f07b66bf..1acc33bce 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -7,6 +7,7 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; @@ -40,7 +41,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}/languages/")] - [ProducesResponseType(typeof(AppLanguagesDto), 200)] + [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppLanguagesRead)] [ApiCosts(0)] public IActionResult GetLanguages(string app) @@ -92,7 +93,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPut] [Route("apps/{app}/languages/{language}/")] - [ProducesResponseType(typeof(AppLanguagesDto), 200)] + [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppLanguagesUpdate)] [ApiCosts(1)] public async Task PutLanguage(string app, string language, [FromBody] UpdateLanguageDto request) @@ -116,7 +117,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/languages/{language}/")] - [ProducesResponseType(typeof(AppLanguagesDto), 200)] + [ProducesResponseType(typeof(AppLanguagesDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppLanguagesDelete)] [ApiCosts(1)] public async Task DeleteLanguage(string app, string language) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs index 3f0f76f95..d39075d11 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; @@ -42,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}/patterns/")] - [ProducesResponseType(typeof(PatternsDto), 200)] + [ProducesResponseType(typeof(PatternsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppPatternsRead)] [ApiCosts(0)] public IActionResult GetPatterns(string app) @@ -94,7 +95,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPut] [Route("apps/{app}/patterns/{id}/")] - [ProducesResponseType(typeof(PatternsDto), 200)] + [ProducesResponseType(typeof(PatternsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppPatternsUpdate)] [ApiCosts(1)] public async Task PutPattern(string app, DomainId id, [FromBody] UpdatePatternDto request) @@ -120,7 +121,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/patterns/{id}/")] - [ProducesResponseType(typeof(PatternsDto), 200)] + [ProducesResponseType(typeof(PatternsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppPatternsDelete)] [ApiCosts(1)] public async Task DeletePattern(string app, DomainId id) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs index b8d018d09..a0ddc72fa 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; @@ -42,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}/roles/")] - [ProducesResponseType(typeof(RolesDto), 200)] + [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRolesRead)] [ApiCosts(0)] public IActionResult GetRoles(string app) @@ -67,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}/roles/permissions")] - [ProducesResponseType(typeof(string[]), 200)] + [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRolesRead)] [ApiCosts(0)] public IActionResult GetPermissions(string app) @@ -119,7 +120,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPut] [Route("apps/{app}/roles/{roleName}/")] - [ProducesResponseType(typeof(RolesDto), 200)] + [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRolesUpdate)] [ApiCosts(1)] public async Task PutRole(string app, string roleName, [FromBody] UpdateRoleDto request) @@ -143,7 +144,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/roles/{roleName}/")] - [ProducesResponseType(typeof(RolesDto), 200)] + [ProducesResponseType(typeof(RolesDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRolesDelete)] [ApiCosts(1)] public async Task DeleteRole(string app, string roleName) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs index 8645038e2..abd9f8c8f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Apps.Models; @@ -43,7 +44,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}/workflows/")] - [ProducesResponseType(typeof(WorkflowsDto), 200)] + [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppWorkflowsRead)] [ApiCosts(0)] public IActionResult GetWorkflows(string app) @@ -70,7 +71,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPost] [Route("apps/{app}/workflows/")] - [ProducesResponseType(typeof(WorkflowsDto), 200)] + [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppWorkflowsUpdate)] [ApiCosts(1)] public async Task PostWorkflow(string app, [FromBody] AddWorkflowDto request) @@ -95,7 +96,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPut] [Route("apps/{app}/workflows/{id}")] - [ProducesResponseType(typeof(WorkflowsDto), 200)] + [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppWorkflowsUpdate)] [ApiCosts(1)] public async Task PutWorkflow(string app, DomainId id, [FromBody] UpdateWorkflowDto request) @@ -118,7 +119,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/workflows/{id}")] - [ProducesResponseType(typeof(WorkflowsDto), 200)] + [ProducesResponseType(typeof(WorkflowsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppWorkflowsUpdate)] [ApiCosts(1)] public async Task DeleteWorkflow(string app, DomainId id) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 2b04ceb89..65437462f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -70,7 +70,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/")] - [ProducesResponseType(typeof(AppDto[]), 200)] + [ProducesResponseType(typeof(AppDto[]), StatusCodes.Status200OK)] [ApiPermission] [ApiCosts(0)] public async Task GetApps() @@ -102,7 +102,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}")] - [ProducesResponseType(typeof(AppDto), 200)] + [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ApiPermission] [ApiCosts(0)] public IActionResult GetApp(string app) @@ -158,7 +158,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPut] [Route("apps/{app}/")] - [ProducesResponseType(typeof(AppDto), 200)] + [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppUpdate)] [ApiCosts(0)] public async Task UpdateApp(string app, [FromBody] UpdateAppDto request) @@ -180,7 +180,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPost] [Route("apps/{app}/image")] - [ProducesResponseType(typeof(AppDto), 200)] + [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppUpdateImage)] [ApiCosts(0)] public async Task UploadImage(string app, IFormFile file) @@ -200,7 +200,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpGet] [Route("apps/{app}/image")] - [ProducesResponseType(typeof(FileResult), 200)] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [AllowAnonymous] [ApiCosts(0)] public IActionResult GetImage(string app) @@ -271,7 +271,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/image")] - [ProducesResponseType(typeof(AppDto), 200)] + [ProducesResponseType(typeof(AppDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppUpdateImage)] [ApiCosts(0)] public async Task DeleteImage(string app) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs index 594953d95..bb1667286 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; +using Squidex.Shared.Identity; using Squidex.Shared.Users; using Squidex.Web; @@ -49,7 +50,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models { if (users.TryGetValue(ContributorId, out var user)) { - ContributorName = user.DisplayName()!; + ContributorName = user.Claims.DisplayName()!; ContributorEmail = user.Email; } else diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 5f51dd0bc..b89a34082 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -10,6 +10,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Assets.Models; @@ -67,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpGet] [Route("assets/{app}/{idOrSlug}/{*more}")] - [ProducesResponseType(typeof(FileResult), 200)] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ApiPermission] [ApiCosts(0.5)] [AllowAnonymous] @@ -94,7 +95,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpGet] [Route("assets/{id}/")] - [ProducesResponseType(typeof(FileResult), 200)] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ApiPermission] [ApiCosts(0.5)] [AllowAnonymous] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs index c59190e78..595798419 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Assets.Models; @@ -47,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpGet] [Route("apps/{app}/assets/folders", Order = -1)] - [ProducesResponseType(typeof(AssetsDto), 200)] + [ProducesResponseType(typeof(AssetsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAssetFolders(string app, [FromQuery] DomainId parentId) @@ -106,7 +107,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpPut] [Route("apps/{app}/assets/folders/{id}/", Order = -1)] - [ProducesResponseType(typeof(AssetDto), 200)] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] [AssetRequestSizeLimit] [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] @@ -132,7 +133,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpPut] [Route("apps/{app}/assets/folders/{id}/parent", Order = -1)] - [ProducesResponseType(typeof(AssetDto), 200)] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] [AssetRequestSizeLimit] [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index c2907f4d0..f19b63a2b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -64,7 +64,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpGet] [Route("apps/{app}/assets/tags")] - [ProducesResponseType(typeof(Dictionary), 200)] + [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetTags(string app) @@ -92,7 +92,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpGet] [Route("apps/{app}/assets/")] - [ProducesResponseType(typeof(AssetsDto), 200)] + [ProducesResponseType(typeof(AssetsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAssets(string app, [FromQuery] DomainId? parentId, [FromQuery] string? ids = null, [FromQuery] string? q = null) @@ -121,7 +121,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpPost] [Route("apps/{app}/assets/query")] - [ProducesResponseType(typeof(AssetsDto), 200)] + [ProducesResponseType(typeof(AssetsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAssetsPost(string app, [FromBody] QueryDto query) @@ -147,7 +147,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpGet] [Route("apps/{app}/assets/{id}/")] - [ProducesResponseType(typeof(AssetDto), 200)] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAsset(string app, DomainId id) @@ -223,7 +223,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpPut] [Route("apps/{app}/assets/{id}/content/")] - [ProducesResponseType(typeof(AssetDto), 200)] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppAssetsUpload)] [ApiCosts(1)] public async Task PutAssetContent(string app, DomainId id, IFormFile file) @@ -250,7 +250,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpPut] [Route("apps/{app}/assets/{id}/")] - [ProducesResponseType(typeof(AssetDto), 200)] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] [AssetRequestSizeLimit] [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] @@ -276,7 +276,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpPut] [Route("apps/{app}/assets/{id}/parent")] - [ProducesResponseType(typeof(AssetDto), 200)] + [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)] [AssetRequestSizeLimit] [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs index c0868e44f..30c50716d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Domain.Apps.Entities.Backup; using Squidex.Infrastructure; @@ -45,7 +46,7 @@ namespace Squidex.Areas.Api.Controllers.Backups [HttpGet] [Route("apps/{app}/backups/{id}")] [ResponseCache(Duration = 3600 * 24 * 30)] - [ProducesResponseType(typeof(FileResult), 200)] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ApiCosts(0)] [AllowAnonymous] public async Task GetBackupContent(string app, DomainId id) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs index 10941e581..c217b3054 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Backups.Models; using Squidex.Domain.Apps.Entities.Backup; @@ -43,7 +44,7 @@ namespace Squidex.Areas.Api.Controllers.Backups /// [HttpGet] [Route("apps/{app}/backups/")] - [ProducesResponseType(typeof(BackupJobsDto), 200)] + [ProducesResponseType(typeof(BackupJobsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppBackupsRead)] [ApiCosts(0)] public async Task GetBackups(string app) @@ -66,7 +67,7 @@ namespace Squidex.Areas.Api.Controllers.Backups /// [HttpPost] [Route("apps/{app}/backups/")] - [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppBackupsCreate)] [ApiCosts(0)] public IActionResult PostBackup(string app) @@ -87,7 +88,7 @@ namespace Squidex.Areas.Api.Controllers.Backups /// [HttpDelete] [Route("apps/{app}/backups/{id}")] - [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppBackupsDelete)] [ApiCosts(0)] public async Task DeleteBackup(string app, DomainId id) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs index 5f71b3125..75b369af6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Backups.Models; using Squidex.Domain.Apps.Entities.Backup; @@ -38,7 +39,7 @@ namespace Squidex.Areas.Api.Controllers.Backups /// [HttpGet] [Route("apps/restore/")] - [ProducesResponseType(typeof(RestoreJobDto), 200)] + [ProducesResponseType(typeof(RestoreJobDto), StatusCodes.Status200OK)] [ApiPermission(Permissions.AdminRestore)] public async Task GetRestoreJob() { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs index 0c1945ab2..f3f71616b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Comments.Models; @@ -47,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Comments /// [HttpGet] [Route("apps/{app}/comments/{commentsId}")] - [ProducesResponseType(typeof(CommentsDto), 200)] + [ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppCommentsRead)] [ApiCosts(0)] public async Task GetComments(string app, DomainId commentsId, [FromQuery] long version = EtagVersion.Any) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs index 455a4ecc7..6244c397b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Comments.Models; @@ -47,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Comments.Notifications /// [HttpGet] [Route("users/{userId}/notifications")] - [ProducesResponseType(typeof(CommentsDto), 200)] + [ProducesResponseType(typeof(CommentsDto), StatusCodes.Status200OK)] [ApiPermission] public async Task GetNotifications(DomainId userId, [FromQuery] long version = EtagVersion.Any) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 75f200616..451ab462a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Domain.Apps.Core.Contents; @@ -149,7 +150,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpGet] [Route("content/{app}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetAllContents(string app, [FromQuery] string ids) @@ -178,7 +179,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpPost] [Route("content/{app}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetAllContentsPost(string app, [FromBody] ContentsIdsQueryDto query) @@ -209,7 +210,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpGet] [Route("content/{app}/{name}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContents(string app, string name, [FromQuery] string? ids = null, [FromQuery] string? q = null) @@ -241,7 +242,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpPost] [Route("content/{app}/{name}/query")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContentsPost(string app, string name, [FromBody] QueryDto query) @@ -273,7 +274,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpGet] [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentDto), 200)] + [ProducesResponseType(typeof(ContentDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetContent(string app, string name, DomainId id) @@ -333,7 +334,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpGet] [Route("content/{app}/{name}/{id}/references")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetReferences(string app, string name, DomainId id, [FromQuery] string? q = null) @@ -364,7 +365,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpGet] [Route("content/{app}/{name}/{id}/referencing")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(1)] public async Task GetReferencing(string app, string name, DomainId id, [FromQuery] string? q = null) @@ -462,7 +463,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpPost] [Route("content/{app}/{name}/import")] - [ProducesResponseType(typeof(BulkResultDto[]), 200)] + [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsCreate)] [ApiCosts(5)] public async Task PostContents(string app, string name, [FromBody] ImportContentsDto request) @@ -493,7 +494,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpPost] [Route("content/{app}/{name}/bulk")] - [ProducesResponseType(typeof(BulkResultDto[]), 200)] + [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContents)] [ApiCosts(5)] public async Task BulkContents(string app, string name, [FromBody] BulkUpdateDto request) @@ -526,7 +527,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpPost] [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpsert)] [ApiCosts(1)] public async Task PostContent(string app, string name, DomainId id, [FromBody] NamedContentData request, [FromQuery] bool publish = false) @@ -555,7 +556,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpPut] [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PutContent(string app, string name, DomainId id, [FromBody] NamedContentData request) @@ -584,7 +585,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpPatch] [Route("content/{app}/{name}/{id}/")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PatchContent(string app, string name, DomainId id, [FromBody] NamedContentData request) @@ -613,7 +614,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpPut] [Route("content/{app}/{name}/{id}/status/")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PutContentStatus(string app, string name, DomainId id, ChangeStatusDto request) @@ -640,7 +641,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpPost] [Route("content/{app}/{name}/{id}/draft/")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsVersionCreate)] [ApiCosts(1)] public async Task CreateDraft(string app, string name, DomainId id) @@ -667,7 +668,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpDelete] [Route("content/{app}/{name}/{id}/draft/")] - [ProducesResponseType(typeof(ContentsDto), 200)] + [ProducesResponseType(typeof(ContentsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppContentsDelete)] [ApiCosts(1)] public async Task DeleteVersion(string app, string name, DomainId id) diff --git a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs index c276af340..622ee0dff 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Orleans; using Squidex.Areas.Api.Controllers.EventConsumers.Models; @@ -29,7 +30,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [HttpGet] [Route("event-consumers/")] - [ProducesResponseType(typeof(EventConsumersDto), 200)] + [ProducesResponseType(typeof(EventConsumersDto), StatusCodes.Status200OK)] [ApiPermission(Permissions.AdminEventsRead)] public async Task GetEventConsumers() { @@ -42,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [HttpPut] [Route("event-consumers/{consumerName}/start/")] - [ProducesResponseType(typeof(EventConsumerDto), 200)] + [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] [ApiPermission(Permissions.AdminEventsManage)] public async Task StartEventConsumer(string consumerName) { @@ -55,7 +56,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [HttpPut] [Route("event-consumers/{consumerName}/stop/")] - [ProducesResponseType(typeof(EventConsumerDto), 200)] + [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] [ApiPermission(Permissions.AdminEventsManage)] public async Task StopEventConsumer(string consumerName) { @@ -68,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [HttpPut] [Route("event-consumers/{consumerName}/reset/")] - [ProducesResponseType(typeof(EventConsumerDto), 200)] + [ProducesResponseType(typeof(EventConsumerDto), StatusCodes.Status200OK)] [ApiPermission(Permissions.AdminEventsManage)] public async Task ResetEventConsumer(string consumerName) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index 9f7b3e16a..820200c79 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.History.Models; using Squidex.Domain.Apps.Entities.History; @@ -41,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.History /// [HttpGet] [Route("apps/{app}/history/")] - [ProducesResponseType(typeof(HistoryEventDto), 200)] + [ProducesResponseType(typeof(HistoryEventDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppHistory)] [ApiCosts(0.1)] public async Task GetHistory(string app, string channel) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs index 5c9a74e67..9a023ed72 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Linq; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Infrastructure; @@ -36,7 +37,7 @@ namespace Squidex.Areas.Api.Controllers.Languages /// [HttpGet] [Route("languages/")] - [ProducesResponseType(typeof(LanguageDto[]), 200)] + [ProducesResponseType(typeof(LanguageDto[]), StatusCodes.Status200OK)] [ApiPermission] public IActionResult GetLanguages() { diff --git a/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs b/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs index 50e28fbfd..be965fc2e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.News.Models; using Squidex.Areas.Api.Controllers.News.Service; @@ -37,7 +38,7 @@ namespace Squidex.Areas.Api.Controllers.News /// [HttpGet] [Route("news/features/")] - [ProducesResponseType(typeof(FeaturesDto), 200)] + [ProducesResponseType(typeof(FeaturesDto), StatusCodes.Status200OK)] [ApiPermission] public async Task GetNews([FromQuery] int version = 0) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs index 1e1881131..9165f7b93 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Infrastructure.Commands; using Squidex.Shared; @@ -33,7 +34,7 @@ namespace Squidex.Areas.Api.Controllers.Ping /// 200 => Infos returned. /// [HttpGet] - [ProducesResponseType(typeof(ExposedValues), 200)] + [ProducesResponseType(typeof(ExposedValues), StatusCodes.Status200OK)] [Route("info/")] public IActionResult GetInfo() { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index 0b81d48e5..39e502087 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Plans.Models; @@ -44,7 +45,7 @@ namespace Squidex.Areas.Api.Controllers.Plans /// [HttpGet] [Route("apps/{app}/plans/")] - [ProducesResponseType(typeof(AppPlansDto), 200)] + [ProducesResponseType(typeof(AppPlansDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppPlansRead)] [ApiCosts(0)] public IActionResult GetPlans(string app) @@ -73,7 +74,7 @@ namespace Squidex.Areas.Api.Controllers.Plans /// [HttpPut] [Route("apps/{app}/plan/")] - [ProducesResponseType(typeof(PlanChangedDto), 200)] + [ProducesResponseType(typeof(PlanChangedDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppPlansChange)] [ApiCosts(0)] public async Task PutPlan(string app, [FromBody] ChangePlanDto request) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index be4b445fe..0138d4aca 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using NodaTime; @@ -56,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpGet] [Route("rules/actions/")] - [ProducesResponseType(typeof(Dictionary), 200)] + [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] [ApiPermission] [ApiCosts(0)] public IActionResult GetActions() @@ -83,7 +84,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpGet] [Route("apps/{app}/rules/")] - [ProducesResponseType(typeof(RulesDto), 200)] + [ProducesResponseType(typeof(RulesDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesRead)] [ApiCosts(1)] public async Task GetRules(string app) @@ -131,7 +132,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpDelete] [Route("apps/{app}/rules/run")] - [ProducesResponseType(204)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ApiPermissionOrAnonymous(Permissions.AppRulesEvents)] [ApiCosts(1)] public async Task DeleteRuleRun(string app) @@ -154,7 +155,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpPut] [Route("apps/{app}/rules/{id}/")] - [ProducesResponseType(typeof(RuleDto), 200)] + [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesUpdate)] [ApiCosts(1)] public async Task PutRule(string app, DomainId id, [FromBody] UpdateRuleDto request) @@ -177,7 +178,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpPut] [Route("apps/{app}/rules/{id}/enable/")] - [ProducesResponseType(typeof(RuleDto), 200)] + [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesDisable)] [ApiCosts(1)] public async Task EnableRule(string app, DomainId id) @@ -200,7 +201,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpPut] [Route("apps/{app}/rules/{id}/disable/")] - [ProducesResponseType(typeof(RuleDto), 200)] + [ProducesResponseType(typeof(RuleDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesDisable)] [ApiCosts(1)] public async Task DisableRule(string app, DomainId id) @@ -245,7 +246,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpPut] [Route("apps/{app}/rules/{id}/run")] - [ProducesResponseType(204)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ApiPermissionOrAnonymous(Permissions.AppRulesEvents)] [ApiCosts(1)] public async Task PutRuleRun(string app, DomainId id, [FromQuery] bool fromSnapshots = false) @@ -288,7 +289,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpGet] [Route("apps/{app}/rules/events/")] - [ProducesResponseType(typeof(RuleEventsDto), 200)] + [ProducesResponseType(typeof(RuleEventsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppRulesRead)] [ApiCosts(0)] public async Task GetEvents(string app, [FromQuery] DomainId? ruleId = null, [FromQuery] int skip = 0, [FromQuery] int take = 20) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index e199ba5e1..33cceda86 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Domain.Apps.Entities.Schemas; @@ -93,7 +94,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/ui/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutSchemaUIFields(string app, string name, [FromBody] ConfigureUIFieldsDto request) @@ -118,7 +119,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/ordering/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutSchemaFieldOrdering(string app, string name, [FromBody] ReorderFieldsDto request) @@ -144,7 +145,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/ordering/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutNestedFieldOrdering(string app, string name, long parentId, [FromBody] ReorderFieldsDto request) @@ -170,7 +171,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutField(string app, string name, long id, [FromBody] UpdateFieldDto request) @@ -197,7 +198,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutNestedField(string app, string name, long parentId, long id, [FromBody] UpdateFieldDto request) @@ -225,7 +226,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/lock/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task LockField(string app, string name, long id) @@ -254,7 +255,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/lock/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task LockNestedField(string app, string name, long parentId, long id) @@ -282,7 +283,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/hide/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task HideField(string app, string name, long id) @@ -311,7 +312,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/hide/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task HideNestedField(string app, string name, long parentId, long id) @@ -339,7 +340,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/show/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task ShowField(string app, string name, long id) @@ -368,7 +369,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/show/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task ShowNestedField(string app, string name, long parentId, long id) @@ -396,7 +397,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/enable/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task EnableField(string app, string name, long id) @@ -425,7 +426,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/enable/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task EnableNestedField(string app, string name, long parentId, long id) @@ -453,7 +454,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/disable/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task DisableField(string app, string name, long id) @@ -482,7 +483,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/disable/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task DisableNestedField(string app, string name, long parentId, long id) @@ -507,7 +508,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpDelete] [Route("apps/{app}/schemas/{name}/fields/{id:long}/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task DeleteField(string app, string name, long id) @@ -533,7 +534,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpDelete] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task DeleteNestedField(string app, string name, long parentId, long id) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 52437870f..375506fbd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -7,6 +7,7 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Schemas.Models; @@ -44,7 +45,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpGet] [Route("apps/{app}/schemas/")] - [ProducesResponseType(typeof(SchemasDto), 200)] + [ProducesResponseType(typeof(SchemasDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasRead)] [ApiCosts(0)] public async Task GetSchemas(string app) @@ -72,7 +73,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpGet] [Route("apps/{app}/schemas/{name}/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasRead)] [ApiCosts(0)] public async Task GetSchema(string app, string name) @@ -142,7 +143,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutSchema(string app, string name, [FromBody] UpdateSchemaDto request) @@ -167,7 +168,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/sync")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutSchemaSync(string app, string name, [FromBody] SynchronizeSchemaDto request) @@ -192,7 +193,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/category")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutCategory(string app, string name, [FromBody] ChangeCategoryDto request) @@ -217,7 +218,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/preview-urls")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutPreviewUrls(string app, string name, [FromBody] ConfigurePreviewUrlsDto request) @@ -242,7 +243,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/scripts/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasScripts)] [ApiCosts(1)] public async Task PutScripts(string app, string name, [FromBody] SchemaScriptsDto request) @@ -267,7 +268,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/rules/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutRules(string app, string name, [FromBody] ConfigureFieldRulesDto request) @@ -290,7 +291,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/publish/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasPublish)] [ApiCosts(1)] public async Task PublishSchema(string app, string name) @@ -313,7 +314,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpPut] [Route("apps/{app}/schemas/{name}/unpublish/")] - [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ProducesResponseType(typeof(SchemaDetailsDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSchemasPublish)] [ApiCosts(1)] public async Task UnpublishSchema(string app, string name) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs b/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs index ac9230937..10d41b551 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Search.Models; using Squidex.Domain.Apps.Entities.Search; @@ -41,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Search /// [HttpGet] [Route("apps/{app}/search/")] - [ProducesResponseType(typeof(SearchResultDto[]), 200)] + [ProducesResponseType(typeof(SearchResultDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppSearch)] [ApiCosts(0)] public async Task GetSearchResults(string app, [FromQuery] string? query = null) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 5a6e51163..98c2f9352 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -9,6 +9,7 @@ using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Statistics.Models; using Squidex.Domain.Apps.Entities.Apps; @@ -66,7 +67,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics /// [HttpGet] [Route("apps/{app}/usages/log/")] - [ProducesResponseType(typeof(LogDownloadDto), 200)] + [ProducesResponseType(typeof(LogDownloadDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppUsage)] [ApiCosts(0)] public IActionResult GetLog(string app) @@ -93,7 +94,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics /// [HttpGet] [Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")] - [ProducesResponseType(typeof(CallsUsageDtoDto), 200)] + [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppUsage)] [ApiCosts(0)] public async Task GetUsages(string app, DateTime fromDate, DateTime toDate) @@ -122,7 +123,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics /// [HttpGet] [Route("apps/{app}/usages/storage/today/")] - [ProducesResponseType(typeof(CurrentStorageDto), 200)] + [ProducesResponseType(typeof(CurrentStorageDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppUsage)] [ApiCosts(0)] public async Task GetCurrentStorageSize(string app) @@ -149,7 +150,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics /// [HttpGet] [Route("apps/{app}/usages/storage/{fromDate}/{toDate}/")] - [ProducesResponseType(typeof(StorageUsagePerDateDto[]), 200)] + [ProducesResponseType(typeof(StorageUsagePerDateDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppUsage)] [ApiCosts(0)] public async Task GetStorageSizes(string app, DateTime fromDate, DateTime toDate) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs index ea9b74897..539982cea 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Translations.Models; using Squidex.Infrastructure.Commands; @@ -39,7 +40,7 @@ namespace Squidex.Areas.Api.Controllers.Translations /// [HttpPost] [Route("apps/{app}/translations/")] - [ProducesResponseType(typeof(TranslationDto), 200)] + [ProducesResponseType(typeof(TranslationDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppTranslate)] [ApiCosts(0)] public async Task PostTranslation(string app, [FromBody] TranslateDto request) diff --git a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index 8a4b825e3..ba161ad2c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Squidex.Areas.Api.Controllers.UI.Models; @@ -42,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.UI /// [HttpGet] [Route("ui/settings/")] - [ProducesResponseType(typeof(UISettingsDto), 200)] + [ProducesResponseType(typeof(UISettingsDto), StatusCodes.Status200OK)] [ApiPermission] public IActionResult GetSettings() { @@ -64,7 +65,7 @@ namespace Squidex.Areas.Api.Controllers.UI /// [HttpGet] [Route("apps/{app}/ui/settings/")] - [ProducesResponseType(typeof(Dictionary), 200)] + [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] [ApiPermission] public async Task GetSettings(string app) { @@ -83,7 +84,7 @@ namespace Squidex.Areas.Api.Controllers.UI /// [HttpGet] [Route("apps/{app}/ui/settings/me")] - [ProducesResponseType(typeof(Dictionary), 200)] + [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] [ApiPermission] public async Task GetUserSettings(string app) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs index 6c51fe683..a80e143f5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Validation; +using Squidex.Shared.Identity; using Squidex.Shared.Users; using Squidex.Web; @@ -47,8 +48,8 @@ namespace Squidex.Areas.Api.Controllers.Users.Models public static UserDto FromUser(IUser user, Resources resources) { - var userPermssions = user.Permissions().ToIds(); - var userName = user.DisplayName()!; + var userPermssions = user.Claims.Permissions().ToIds(); + var userName = user.Claims.DisplayName()!; var result = SimpleMapper.Map(user, new UserDto { DisplayName = userName, Permissions = userPermssions }); @@ -79,6 +80,8 @@ namespace Squidex.Areas.Api.Controllers.Users.Models { AddPutLink("unlock", resources.Url(c => nameof(c.UnlockUser), values)); } + + AddDeleteLink("delete", resources.Url(x => nameof(x.DeleteUser), values)); } if (resources.CanUpdateUser) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs index de247f7a2..68c44ef5d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/UsersDto.cs @@ -7,8 +7,8 @@ using System.Collections.Generic; using System.Linq; -using Squidex.Domain.Users; using Squidex.Infrastructure.Validation; +using Squidex.Shared.Users; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Users.Models @@ -26,7 +26,7 @@ namespace Squidex.Areas.Api.Controllers.Users.Models [LocalizedRequired] public UserDto[] Items { get; set; } - public static UsersDto FromResults(IEnumerable items, long total, Resources resources) + public static UsersDto FromResults(IEnumerable items, long total, Resources resources) { var result = new UsersDto { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs index 76c00fd50..9ef90613d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs @@ -6,13 +6,12 @@ // ========================================================================== using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Users.Models; using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Translations; using Squidex.Shared; using Squidex.Web; @@ -22,29 +21,23 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiModelValidation(true)] public sealed class UserManagementController : ApiController { - private readonly UserManager userManager; - private readonly IUserFactory userFactory; - private readonly IUserEvents userEvents; + private readonly IUserService userService; - public UserManagementController(ICommandBus commandBus, UserManager userManager, IUserFactory userFactory, IUserEvents userEvents) + public UserManagementController(ICommandBus commandBus, IUserService userService) : base(commandBus) { - this.userManager = userManager; - this.userFactory = userFactory; - this.userEvents = userEvents; + this.userService = userService; } [HttpGet] [Route("user-management/")] - [ProducesResponseType(typeof(UsersDto), 200)] + [ProducesResponseType(typeof(UsersDto), StatusCodes.Status200OK)] [ApiPermission(Permissions.AdminUsersRead)] public async Task GetUsers([FromQuery] string? query = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) { - var (items, total) = await AsyncHelper.WhenAll( - userManager.QueryByEmailAsync(query, take, skip), - userManager.CountByEmailAsync(query)); + var users = await userService.QueryAsync(query, take, skip); - var response = UsersDto.FromResults(items, total, Resources); + var response = UsersDto.FromResults(users, users.Total, Resources); return Ok(response); } @@ -55,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiPermission(Permissions.AdminUsersRead)] public async Task GetUser(string id) { - var user = await userManager.FindByIdWithClaimsAsync(id); + var user = await userService.FindByIdAsync(id); if (user == null) { @@ -73,9 +66,7 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiPermission(Permissions.AdminUsersCreate)] public async Task PostUser([FromBody] CreateUserDto request) { - var user = await userManager.CreateAsync(userFactory, request.ToValues()); - - userEvents.OnUserRegistered(user); + var user = await userService.CreateAsync(request.Email, request.ToValues()); var response = UserDto.FromUser(user, Resources); @@ -88,9 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Users [ApiPermission(Permissions.AdminUsersUpdate)] public async Task PutUser(string id, [FromBody] UpdateUserDto request) { - var user = await userManager.UpdateAsync(id, request.ToValues()); - - userEvents.OnUserUpdated(user); + var user = await userService.UpdateAsync(id, request.ToValues()); var response = UserDto.FromUser(user, Resources); @@ -108,7 +97,7 @@ namespace Squidex.Areas.Api.Controllers.Users throw new DomainForbiddenException(T.Get("users.lockYourselfError")); } - var user = await userManager.LockAsync(id); + var user = await userService.LockAsync(id); var response = UserDto.FromUser(user, Resources); @@ -126,11 +115,27 @@ namespace Squidex.Areas.Api.Controllers.Users throw new DomainForbiddenException(T.Get("users.unlockYourselfError")); } - var user = await userManager.UnlockAsync(id); + var user = await userService.UnlockAsync(id); var response = UserDto.FromUser(user, Resources); return Ok(response); } + + [HttpDelete] + [Route("user-management/{id}/")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ApiPermission(Permissions.AdminUsersUnlock)] + public async Task DeleteUser(string id) + { + if (this.IsUser(id)) + { + throw new DomainForbiddenException(T.Get("users.deleteYourselfError")); + } + + await userService.DeleteAsync(id); + + return NoContent(); + } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index 98e6fe384..8c49c2b67 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -10,12 +10,14 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Squidex.Areas.Api.Controllers.Users.Models; using Squidex.Domain.Users; using Squidex.Infrastructure.Commands; using Squidex.Log; +using Squidex.Shared.Identity; using Squidex.Shared.Users; using Squidex.Web; @@ -68,7 +70,7 @@ namespace Squidex.Areas.Api.Controllers.Users /// [HttpGet] [Route("/")] - [ProducesResponseType(typeof(ResourcesDto), 200)] + [ProducesResponseType(typeof(ResourcesDto), StatusCodes.Status200OK)] [ApiPermission] public IActionResult GetUserResources() { @@ -89,7 +91,7 @@ namespace Squidex.Areas.Api.Controllers.Users /// [HttpGet] [Route("users/")] - [ProducesResponseType(typeof(UserDto[]), 200)] + [ProducesResponseType(typeof(UserDto[]), StatusCodes.Status200OK)] [ApiPermission] public async Task GetUsers(string query) { @@ -97,7 +99,7 @@ namespace Squidex.Areas.Api.Controllers.Users { var users = await userResolver.QueryByEmailAsync(query); - var response = users.Where(x => !x.IsHidden()).Select(x => UserDto.FromUser(x, Resources)).ToArray(); + var response = users.Where(x => !x.Claims.IsHidden()).Select(x => UserDto.FromUser(x, Resources)).ToArray(); return Ok(response); } @@ -121,7 +123,7 @@ namespace Squidex.Areas.Api.Controllers.Users /// [HttpGet] [Route("users/{id}/")] - [ProducesResponseType(typeof(UserDto), 200)] + [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] [ApiPermission] public async Task GetUser(string id) { @@ -156,7 +158,7 @@ namespace Squidex.Areas.Api.Controllers.Users /// [HttpGet] [Route("users/{id}/picture/")] - [ProducesResponseType(typeof(FileResult), 200)] + [ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)] [ResponseCache(Duration = 300)] public async Task GetUserPicture(string id) { @@ -166,7 +168,7 @@ namespace Squidex.Areas.Api.Controllers.Users if (entity != null) { - if (entity.IsPictureUrlStored()) + if (entity.Claims.IsPictureUrlStored()) { var callback = new FileCallback(async (body, range, ct) => { @@ -185,7 +187,7 @@ namespace Squidex.Areas.Api.Controllers.Users using (var client = httpClientFactory.CreateClient()) { - var url = entity.PictureNormalizedUrl(); + var url = entity.Claims.PictureNormalizedUrl(); if (!string.IsNullOrWhiteSpace(url)) { diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs b/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs index 728470dc7..234546bfb 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs @@ -6,10 +6,8 @@ // ========================================================================== using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Logging; @@ -20,7 +18,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Security; using Squidex.Log; using Squidex.Shared; -using Squidex.Shared.Users; +using Squidex.Shared.Identity; namespace Squidex.Areas.IdentityServer.Config { @@ -46,25 +44,24 @@ namespace Squidex.Areas.IdentityServer.Config { using (var scope = serviceProvider.CreateScope()) { - var userManager = scope.ServiceProvider.GetRequiredService>(); - var userFactory = scope.ServiceProvider.GetRequiredService(); + var userService = scope.ServiceProvider.GetRequiredService(); var adminEmail = identityOptions.AdminEmail; var adminPass = identityOptions.AdminPassword; - var isEmpty = IsEmpty(userManager); + var isEmpty = await IsEmptyAsync(userService); if (isEmpty || identityOptions.AdminRecreate) { try { - var user = await userManager.FindByEmailWithClaimsAsync(adminEmail); + var user = await userService.FindByIdAsync(adminEmail); if (user != null) { if (identityOptions.AdminRecreate) { - var permissions = CreatePermissions(user.Permissions()); + var permissions = CreatePermissions(user.Claims.Permissions()); var values = new UserValues { @@ -72,7 +69,7 @@ namespace Squidex.Areas.IdentityServer.Config Permissions = permissions }; - await userManager.UpdateAsync(user.Identity, values); + await userService.UpdateAsync(user.Id, values); } } else @@ -81,13 +78,12 @@ namespace Squidex.Areas.IdentityServer.Config var values = new UserValues { - Email = adminEmail, Password = adminPass, Permissions = permissions, DisplayName = adminEmail }; - await userManager.CreateAsync(userFactory, values); + await userService.CreateAsync(adminEmail, values); } } catch (Exception ex) @@ -115,9 +111,11 @@ namespace Squidex.Areas.IdentityServer.Config return permissions; } - private static bool IsEmpty(UserManager userManager) + private static async Task IsEmptyAsync(IUserService userService) { - return userManager.SupportsQueryableUsers && !userManager.Users.Any(); + var users = await userService.QueryAsync(take: 0); + + return users.Total == 0; } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs index 029d686f6..e4b2a091b 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs @@ -5,19 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Linq; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Logging; -using Squidex.Config; -using Squidex.Domain.Users; -using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Tasks; -using Squidex.Log; -using Squidex.Shared; namespace Squidex.Areas.IdentityServer.Config { @@ -29,50 +17,5 @@ namespace Squidex.Areas.IdentityServer.Config return app; } - - public static IServiceProvider UseSquidexAdmin(this IServiceProvider services) - { - var options = services.GetRequiredService>().Value; - - IdentityModelEventSource.ShowPII = options.ShowPII; - - var userManager = services.GetRequiredService>(); - var userFactory = services.GetRequiredService(); - - var log = services.GetRequiredService(); - - if (options.IsAdminConfigured()) - { - var adminEmail = options.AdminEmail; - var adminPass = options.AdminPassword; - - AsyncHelper.Sync(async () => - { - if (userManager.SupportsQueryableUsers && !userManager.Users.Any()) - { - try - { - var values = new UserValues - { - Email = adminEmail, - Password = adminPass, - Permissions = new PermissionSet(Permissions.Admin), - DisplayName = adminEmail - }; - - await userManager.CreateAsync(userFactory, values); - } - catch (Exception ex) - { - log.LogError(ex, w => w - .WriteProperty("action", "createAdmin") - .WriteProperty("status", "failed")); - } - } - }); - } - - return services; - } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs index ec060c9c2..8057f4b90 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -43,6 +43,9 @@ namespace Squidex.Areas.IdentityServer.Config services.AddSingletonAs() .As().As(); + services.AddScopedAs() + .As(); + services.AddSingletonAs() .As>(); diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs index 4e3ae48a1..352d22c00 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using IdentityServer4; using IdentityServer4.Models; using IdentityServer4.Stores; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Squidex.Config; @@ -69,20 +68,27 @@ namespace Squidex.Areas.IdentityServer.Config using (var scope = serviceProvider.CreateScope()) { - var userManager = scope.ServiceProvider.GetRequiredService>(); + var userService = scope.ServiceProvider.GetRequiredService(); - var user = await userManager.FindByIdWithClaimsAsync(clientId); + var user = await userService.FindByIdAsync(clientId); - if (!string.IsNullOrWhiteSpace(user?.ClientSecret())) + if (user == null) { - return CreateClientFromUser(user); + return null; + } + + var secret = user.Claims.ClientSecret(); + + if (!string.IsNullOrWhiteSpace(secret)) + { + return CreateClientFromUser(user, secret); } } return null; } - private static Client CreateClientFromUser(UserWithClaims user) + private static Client CreateClientFromUser(IUser user, string secret) { return new Client { @@ -91,7 +97,7 @@ namespace Squidex.Areas.IdentityServer.Config ClientClaimsPrefix = null, ClientSecrets = new List { - new Secret(user.ClientSecret().Sha256()) + new Secret(secret.Sha256()) }, AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, AllowedGrantTypes = GrantTypes.ClientCredentials, diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs index 8cbbacad2..c47ecdcc4 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -6,11 +6,7 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; -using System.Security.Claims; -using System.Text; using System.Threading.Tasks; using IdentityModel; using IdentityServer4; @@ -27,21 +23,16 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Translations; using Squidex.Log; -using Squidex.Shared; using Squidex.Shared.Identity; using Squidex.Shared.Users; using Squidex.Web; -#pragma warning disable CA1827 // Do not use Count() or LongCount() when Any() can be used - namespace Squidex.Areas.IdentityServer.Controllers.Account { public sealed class AccountController : IdentityServerController { private readonly SignInManager signInManager; - private readonly UserManager userManager; - private readonly IUserFactory userFactory; - private readonly IUserEvents userEvents; + private readonly IUserService userService; private readonly IUrlGenerator urlGenerator; private readonly MyIdentityOptions identityOptions; private readonly ISemanticLog log; @@ -49,9 +40,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account public AccountController( SignInManager signInManager, - UserManager userManager, - IUserFactory userFactory, - IUserEvents userEvents, + IUserService userService, IUrlGenerator urlGenerator, IOptions identityOptions, ISemanticLog log, @@ -61,9 +50,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account this.interactions = interactions; this.signInManager = signInManager; this.urlGenerator = urlGenerator; - this.userEvents = userEvents; - this.userFactory = userFactory; - this.userManager = userManager; + this.userService = userService; this.log = log; } @@ -130,7 +117,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account return View(vm); } - var user = await userManager.GetUserWithClaimsAsync(User); + var user = await userService.GetAsync(User); if (user == null) { @@ -143,9 +130,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account ConsentForEmails = model.ConsentToAutomatedEmails }; - await userManager.UpdateAsync(user.Id, update); - - userEvents.OnConsentGiven(user); + await userService.UpdateAsync(user.Id, update); return RedirectToReturnUrl(returnUrl); } @@ -286,11 +271,11 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account var isLoggedIn = result.Succeeded; - UserWithClaims? user; + IUser? user; if (isLoggedIn) { - user = await userManager.FindByLoginWithClaimsAsync(externalLogin.LoginProvider, externalLogin.ProviderKey); + user = await userService.FindByLoginAsync(externalLogin.LoginProvider, externalLogin.ProviderKey); } else { @@ -301,42 +286,38 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account throw new DomainException("User has no exposed email address."); } - user = await userManager.FindByEmailWithClaimsAsync(email); + user = await userService.FindByEmailAsync(email); if (user != null) { - isLoggedIn = - await AddLoginAsync(user, externalLogin) && - await AddClaimsAsync(user, externalLogin, email) && - await LoginAsync(externalLogin); + var update = CreateUserValues(externalLogin, email, user: user); + + await userService.UpdateAsync(user.Id, update); } else { - user = new UserWithClaims(userFactory.Create(email), new List()); + var update = CreateUserValues(externalLogin, email); - var isFirst = userManager.Users.LongCount() == 0; + user = await userService.CreateAsync(email, update, identityOptions.LockAutomatically); + } - isLoggedIn = - await AddUserAsync(user) && - await AddLoginAsync(user, externalLogin) && - await AddClaimsAsync(user, externalLogin, email, isFirst) && - await LockAsync(user, isFirst) && - await LoginAsync(externalLogin); + await userService.AddLoginAsync(user.Id, externalLogin); - userEvents.OnUserRegistered(user); + var (success, locked) = await LoginAsync(externalLogin); - if (await userManager.IsLockedOutAsync(user.Identity)) - { - return View(nameof(LockedOut)); - } + if (locked) + { + return View(nameof(LockedOut)); } + + isLoggedIn = success; } if (!isLoggedIn) { return RedirectToAction(nameof(Login)); } - else if (user != null && !user.HasConsent() && !identityOptions.NoConsent) + else if (user != null && !user.Claims.HasConsent() && !identityOptions.NoConsent) { return RedirectToAction(nameof(Consent), new { returnUrl }); } @@ -346,56 +327,31 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account } } - private Task AddLoginAsync(UserWithClaims user, UserLoginInfo externalLogin) - { - return MakeIdentityOperation(() => userManager.AddLoginAsync(user.Identity, externalLogin)); - } - - private Task AddUserAsync(UserWithClaims user) + private static UserValues CreateUserValues(ExternalLoginInfo externalLogin, string email, IUser? user = null) { - return MakeIdentityOperation(() => userManager.CreateAsync(user.Identity)); - } - - private async Task LoginAsync(UserLoginInfo externalLogin) - { - var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); - - return result.Succeeded; - } - - private Task LockAsync(UserWithClaims user, bool isFirst) - { - if (isFirst || !identityOptions.LockAutomatically) + var values = new UserValues { - return Task.FromResult(true); - } - - return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user.Identity, DateTimeOffset.UtcNow.AddYears(100))); - } - - private async Task AddClaimsAsync(UserWithClaims user, ExternalLoginInfo externalLogin, string email, bool isFirst = false) - { - var update = new UserValues - { - CustomClaims = externalLogin.Principal.GetSquidexClaims().ToList() + CustomClaims = externalLogin.Principal.Claims.GetSquidexClaims().ToList() }; - if (!user.HasPictureUrl()) + if (user != null && !user.Claims.HasPictureUrl()) { - update.PictureUrl = GravatarHelper.CreatePictureUrl(email); + values.PictureUrl = GravatarHelper.CreatePictureUrl(email); } - if (!user.HasDisplayName()) + if (user != null && !user.Claims.HasDisplayName()) { - update.DisplayName = email; + values.DisplayName = email; } - if (isFirst) - { - update.Permissions = new PermissionSet(Permissions.Admin); - } + return values; + } + + private async Task<(bool Success, bool Locked)> LoginAsync(UserLoginInfo externalLogin) + { + var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true); - return await MakeIdentityOperation(() => userManager.SyncClaims(user.Identity, update)); + return (result.Succeeded, result.IsLockedOut); } private IActionResult RedirectToLogoutUrl(LogoutRequest context) @@ -421,42 +377,5 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account return Redirect("~/../"); } } - - private async Task MakeIdentityOperation(Func> action, [CallerMemberName] string? operationName = null) - { - try - { - var result = await action(); - - if (!result.Succeeded) - { - var errorMessageBuilder = new StringBuilder(); - - foreach (var error in result.Errors) - { - errorMessageBuilder.Append(error.Code); - errorMessageBuilder.Append(": "); - errorMessageBuilder.AppendLine(error.Description); - } - - var errorMessage = errorMessageBuilder.ToString(); - - log.LogError((operationName, errorMessage), (ctx, w) => w - .WriteProperty("action", ctx.operationName) - .WriteProperty("status", "Failed") - .WriteProperty("message", ctx.errorMessage)); - } - - return result.Succeeded; - } - catch (Exception ex) - { - log.LogError(ex, operationName, (logOperationName, w) => w - .WriteProperty("action", logOperationName) - .WriteProperty("status", "Failed")); - - return false; - } - } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index 0ff58eab0..02ec61b7e 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -23,6 +23,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Translations; +using Squidex.Infrastructure.Validation; using Squidex.Shared.Identity; using Squidex.Shared.Users; @@ -33,25 +34,22 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile { private static readonly ResizeOptions ResizeOptions = new ResizeOptions { Width = 128, Height = 128, Mode = ResizeMode.Crop }; private readonly SignInManager signInManager; - private readonly UserManager userManager; private readonly IUserPictureStore userPictureStore; - private readonly IUserEvents userEvents; + private readonly IUserService userService; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly MyIdentityOptions identityOptions; public ProfileController( SignInManager signInManager, - UserManager userManager, IUserPictureStore userPictureStore, - IUserEvents userEvents, + IUserService userService, IAssetThumbnailGenerator assetThumbnailGenerator, IOptions identityOptions) { this.signInManager = signInManager; this.identityOptions = identityOptions.Value; - this.userManager = userManager; this.userPictureStore = userPictureStore; - this.userEvents = userEvents; + this.userService = userService; this.assetThumbnailGenerator = assetThumbnailGenerator; } @@ -59,7 +57,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/")] public async Task Profile(string? successMessage = null) { - var user = await userManager.GetUserWithClaimsAsync(User); + var user = await userService.GetAsync(User); return View(await GetProfileVM(user, successMessage: successMessage)); } @@ -72,7 +70,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, - Url.Action(nameof(AddLoginCallback)), userManager.GetUserId(User)); + Url.Action(nameof(AddLoginCallback)), userService.GetUserId(User)); return Challenge(properties, provider); } @@ -82,14 +80,14 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile public Task AddLoginCallback() { return MakeChangeAsync(u => AddLoginAsync(u), - T.Get("users.profile.addLoginDone")); + T.Get("users.profile.addLoginDone"), None.Value); } [HttpPost] [Route("/account/profile/update/")] public Task UpdateProfile(ChangeProfileModel model) { - return MakeChangeAsync(u => UpdateAsync(u, model.ToValues()), + return MakeChangeAsync(id => userService.UpdateAsync(id, model.ToValues()), T.Get("users.profile.updateProfileDone"), model); } @@ -97,7 +95,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/properties/")] public Task UpdateProperties(ChangePropertiesModel model) { - return MakeChangeAsync(u => UpdateAsync(u, model.ToValues()), + return MakeChangeAsync(id => userService.UpdateAsync(id, model.ToValues()), T.Get("users.profile.updatePropertiesDone"), model); } @@ -105,7 +103,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/login-remove/")] public Task RemoveLogin(RemoveLoginModel model) { - return MakeChangeAsync(u => userManager.RemoveLoginAsync(u, model.LoginProvider, model.ProviderKey), + return MakeChangeAsync(id => userService.RemoveLoginAsync(id, model.LoginProvider, model.ProviderKey), T.Get("users.profile.removeLoginDone"), model); } @@ -113,7 +111,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/password-set/")] public Task SetPassword(SetPasswordModel model) { - return MakeChangeAsync(u => userManager.AddPasswordAsync(u, model.Password), + return MakeChangeAsync(id => userService.SetPasswordAsync(id, model.Password), T.Get("users.profile.setPasswordDone"), model); } @@ -121,7 +119,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/password-change/")] public Task ChangePassword(ChangePasswordModel model) { - return MakeChangeAsync(u => userManager.ChangePasswordAsync(u, model.OldPassword, model.Password), + return MakeChangeAsync(id => userService.SetPasswordAsync(id, model.Password, model.OldPassword), T.Get("users.profile.changePasswordDone"), model); } @@ -129,8 +127,8 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile [Route("/account/profile/generate-client-secret/")] public Task GenerateClientSecret() { - return MakeChangeAsync(user => userManager.GenerateClientSecretAsync(user), - T.Get("users.profile.generateClientDone")); + return MakeChangeAsync(id => GenerateClientSecretAsync(id), + T.Get("users.profile.generateClientDone"), None.Value); } [HttpPost] @@ -138,40 +136,28 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile public Task UploadPicture(List file) { return MakeChangeAsync(user => UpdatePictureAsync(file, user), - T.Get("users.profile.uploadPictureDone")); + T.Get("users.profile.uploadPictureDone"), None.Value); } - private async Task AddLoginAsync(IdentityUser user) + private async Task GenerateClientSecretAsync(string id) { - var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(userManager.GetUserId(User)); + var update = new UserValues { ClientSecret = RandomHash.New() }; - return await userManager.AddLoginAsync(user, externalLogin); + await userService.UpdateAsync(id, update); } - private async Task UpdateAsync(IdentityUser user, UserValues values) + private async Task AddLoginAsync(string id) { - var result = await userManager.UpdateSafeAsync(user, values); + var externalLogin = await signInManager.GetExternalLoginInfoWithDisplayNameAsync(id); - if (result.Succeeded) - { - var resolved = await userManager.ResolveUserAsync(user); - - if (resolved != null) - { - userEvents.OnUserUpdated(resolved); - } - } - - return result; + await userService.AddLoginAsync(id, externalLogin); } - private async Task UpdatePictureAsync(List file, IdentityUser user) + private async Task UpdatePictureAsync(List file, string id) { if (file.Count != 1) { - var description = T.Get("validation.onlyOneFile"); - - return IdentityResult.Failed(new IdentityError { Code = "PictureNotOneFile", Description = description }); + throw new ValidationException(T.Get("validation.onlyOneFile")); } using (var thumbnailStream = new MemoryStream()) @@ -184,24 +170,24 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile } catch { - var description = T.Get("validation.notAnImage"); - - return IdentityResult.Failed(new IdentityError { Code = "PictureNotAnImage", Description = description }); + throw new ValidationException(T.Get("validation.notAnImage")); } - await userPictureStore.UploadAsync(user.Id, thumbnailStream); + await userPictureStore.UploadAsync(id, thumbnailStream); } - return await userManager.UpdateSafeAsync(user, new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore }); + var update = new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore }; + + await userService.UpdateAsync(id, update); } - private async Task MakeChangeAsync(Func> action, string successMessage, TModel? model = null) where TModel : class + private async Task MakeChangeAsync(Func action, string successMessage, TModel? model = null) where TModel : class { - var user = await userManager.GetUserWithClaimsAsync(User); + var user = await userService.GetAsync(User); if (user == null) { - throw new DomainException(T.Get("users.userNotFound")); + return NotFound(); } if (!ModelState.IsValid) @@ -212,18 +198,17 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile string errorMessage; try { - var result = await action(user.Identity); - - if (result.Succeeded) - { - await signInManager.SignInAsync(user.Identity, true); + await action(user.Id); - return RedirectToAction(nameof(Profile), new { successMessage }); - } + await signInManager.SignInAsync((IdentityUser)user.Identity, true); - errorMessage = result.Localize(); + return RedirectToAction(nameof(Profile), new { successMessage }); + } + catch (ValidationException ex) + { + errorMessage = ex.Message; } - catch + catch (Exception) { errorMessage = T.Get("users.errorHappened"); } @@ -231,7 +216,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile return View(nameof(Profile), await GetProfileVM(user, model, errorMessage)); } - private async Task GetProfileVM(UserWithClaims? user, TModel? model = null, string? errorMessage = null, string? successMessage = null) where TModel : class + private async Task GetProfileVM(IUser? user, TModel? model = null, string? errorMessage = null, string? successMessage = null) where TModel : class { if (user == null) { @@ -240,21 +225,21 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile var (providers, hasPassword, logins) = await AsyncHelper.WhenAll( signInManager.GetExternalProvidersAsync(), - userManager.HasPasswordAsync(user.Identity), - userManager.GetLoginsAsync(user.Identity)); + userService.HasPasswordAsync(user), + userService.GetLoginsAsync(user)); var result = new ProfileVM { Id = user.Id, - ClientSecret = user.ClientSecret()!, + ClientSecret = user.Claims.ClientSecret()!, Email = user.Email, ErrorMessage = errorMessage, ExternalLogins = logins, ExternalProviders = providers, - DisplayName = user.DisplayName()!, + DisplayName = user.Claims.DisplayName()!, HasPassword = hasPassword, HasPasswordAuth = identityOptions.AllowPasswordAuth, - IsHidden = user.IsHidden(), + IsHidden = user.Claims.IsHidden(), SuccessMessage = successMessage }; @@ -263,7 +248,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile SimpleMapper.Map(model, result); } - result.Properties ??= user.GetCustomProperties().Select(UserProperty.FromTuple).ToList(); + result.Properties ??= user.Claims.GetCustomProperties().Select(UserProperty.FromTuple).ToList(); return result; } diff --git a/backend/src/Squidex/Areas/IdentityServer/Startup.cs b/backend/src/Squidex/Areas/IdentityServer/Startup.cs index 39b45fb7a..75a89085e 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Startup.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Startup.cs @@ -22,7 +22,7 @@ namespace Squidex.Areas.IdentityServer app.Map(Constants.IdentityServerPrefix, identityApp => { - if (environment.IsDevelopment()) + if (!environment.IsDevelopment()) { identityApp.UseDeveloperExceptionPage(); } diff --git a/backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs b/backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs index 78b6f269d..5a871405f 100644 --- a/backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs +++ b/backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs @@ -33,7 +33,9 @@ namespace Squidex.Areas.OrleansDashboard.Middlewares if (authentication.Succeeded) { - if (authentication.Principal?.Permissions().Allows(OrleansPermissions) == true) + var permisisons = authentication.Principal?.Claims.Permissions(); + + if (permisisons?.Allows(OrleansPermissions) == true) { await next(context); } diff --git a/backend/src/Squidex/Config/Authentication/GithubHandler.cs b/backend/src/Squidex/Config/Authentication/GithubHandler.cs index 85b3233c4..5bfa5a9e9 100644 --- a/backend/src/Squidex/Config/Authentication/GithubHandler.cs +++ b/backend/src/Squidex/Config/Authentication/GithubHandler.cs @@ -22,7 +22,7 @@ namespace Squidex.Config.Authentication if (!string.IsNullOrWhiteSpace(nameClaim)) { - context.Identity.SetDisplayName(nameClaim); + context.Identity.AddClaim(new Claim(SquidexClaimTypes.DisplayName, nameClaim)); } if (string.IsNullOrWhiteSpace(context.Identity.FindFirst(ClaimTypes.Email)?.Value)) diff --git a/backend/src/Squidex/Config/Authentication/GoogleHandler.cs b/backend/src/Squidex/Config/Authentication/GoogleHandler.cs index d09b3f12c..c33b5176c 100644 --- a/backend/src/Squidex/Config/Authentication/GoogleHandler.cs +++ b/backend/src/Squidex/Config/Authentication/GoogleHandler.cs @@ -29,7 +29,7 @@ namespace Squidex.Config.Authentication if (!string.IsNullOrWhiteSpace(nameClaim)) { - context.Identity.SetDisplayName(nameClaim); + context.Identity.AddClaim(new Claim(SquidexClaimTypes.DisplayName, nameClaim)); } string? pictureUrl = null; @@ -54,7 +54,7 @@ namespace Squidex.Config.Authentication if (!string.IsNullOrWhiteSpace(pictureUrl)) { - context.Identity.SetPictureUrl(pictureUrl); + context.Identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl)); } return base.CreatingTicket(context); diff --git a/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs b/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs index 718924871..babe4df4e 100644 --- a/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs +++ b/backend/src/Squidex/Config/Authentication/MicrosoftHandler.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OAuth; @@ -25,7 +26,7 @@ namespace Squidex.Config.Authentication if (!string.IsNullOrEmpty(displayName)) { - context.Identity.SetDisplayName(displayName); + context.Identity.AddClaim(new Claim(SquidexClaimTypes.DisplayName, displayName)); } string? id = null; @@ -39,7 +40,7 @@ namespace Squidex.Config.Authentication { var pictureUrl = $"https://apis.live.net/v5.0/{id}/picture"; - context.Identity.SetPictureUrl(pictureUrl); + context.Identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl)); } return base.CreatingTicket(context); diff --git a/backend/src/Squidex/Config/Domain/HistoryServices.cs b/backend/src/Squidex/Config/Domain/HistoryServices.cs index 74bf8951d..88248a928 100644 --- a/backend/src/Squidex/Config/Domain/HistoryServices.cs +++ b/backend/src/Squidex/Config/Domain/HistoryServices.cs @@ -21,7 +21,7 @@ namespace Squidex.Config.Domain config.GetSection("notifo")); services.AddSingletonAs() - .AsSelf().As(); + .AsSelf().As(); services.AddSingletonAs() .As().As(); diff --git a/backend/src/Squidex/Config/Domain/SubscriptionServices.cs b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs index 37ef1e815..b939a6a32 100644 --- a/backend/src/Squidex/Config/Domain/SubscriptionServices.cs +++ b/backend/src/Squidex/Config/Domain/SubscriptionServices.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Entities.Apps.Plans; -using Squidex.Domain.Users; using Squidex.Infrastructure; using Squidex.Web; @@ -29,9 +28,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); - - services.AddSingletonAs() - .AsOptional(); } } } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs index 08047aa33..99f58a01d 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs @@ -6,31 +6,23 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Linq; -using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure; +using Squidex.Shared.Users; using Xunit; namespace Squidex.Domain.Users { public class DefaultUserResolverTests { - private readonly IUserFactory userFactory = A.Fake(); - private readonly UserManager userManager = A.Fake>(); + private readonly IUserService userService = A.Fake(); private readonly DefaultUserResolver sut; public DefaultUserResolverTests() { - A.CallTo(() => userFactory.IsId(A.That.StartsWith("id"))) - .Returns(true); - - A.CallTo(() => userManager.NormalizeEmail(A._)) - .ReturnsLazily(c => c.GetArgument(0)!.ToUpperInvariant()); - var serviceProvider = A.Fake(); var scope = A.Fake(); @@ -46,11 +38,8 @@ namespace Squidex.Domain.Users A.CallTo(() => serviceProvider.GetService(typeof(IServiceScopeFactory))) .Returns(scopeFactory); - A.CallTo(() => serviceProvider.GetService(typeof(IUserFactory))) - .Returns(userFactory); - - A.CallTo(() => serviceProvider.GetService(typeof(UserManager))) - .Returns(userManager); + A.CallTo(() => serviceProvider.GetService(typeof(IUserService))) + .Returns(userService); sut = new DefaultUserResolver(serviceProvider); } @@ -58,202 +47,134 @@ namespace Squidex.Domain.Users [Fact] public async Task Should_create_user_and_return_true_when_created() { - var (user, claims) = GenerateUser("id1"); - - A.CallTo(() => userFactory.Create(user.Email)) - .Returns(user); - - A.CallTo(() => userManager.CreateAsync(user)) - .Returns(IdentityResult.Success); + var email = "123@email.com"; - SetupUser(user, claims); + var user = A.Fake(); - var (result, created) = await sut.CreateUserIfNotExistsAsync(user.Email, false); - - Assert.Equal(user.Id, result!.Id); - Assert.Equal(user.Email, result!.Email); - - Assert.True(created); - } - - [Fact] - public async Task Should_create_user_and_return_false_when_already_exists() - { - var (user, claims) = GenerateUser("id1"); - - A.CallTo(() => userFactory.Create(user.Email)) + A.CallTo(() => userService.CreateAsync(email, A.That.Matches(x => x.Invited == true), false)) .Returns(user); - A.CallTo(() => userManager.CreateAsync(user)) - .Returns(IdentityResult.Failed()); - - SetupUser(user, claims); - - var (result, created) = await sut.CreateUserIfNotExistsAsync(user.Email, false); + var result = await sut.CreateUserIfNotExistsAsync(email, true); - Assert.Equal(user.Id, result!.Id); - Assert.Equal(user.Email, result!.Email); - - Assert.False(created); + Assert.Equal((user, true), result); } [Fact] public async Task Should_create_user_and_return_false_when_exception_thrown() { - var (user, claims) = GenerateUser("id1"); - - A.CallTo(() => userFactory.Create(user.Email)) - .Throws(new InvalidOperationException()); + var email = "123@email.com"; - A.CallTo(() => userManager.CreateAsync(user)) - .Returns(IdentityResult.Failed()); + var user = A.Fake(); - SetupUser(user, claims); + A.CallTo(() => userService.CreateAsync(email, A._, false)) + .Throws(new InvalidOperationException()); - var (result, created) = await sut.CreateUserIfNotExistsAsync(user.Email, false); + A.CallTo(() => userService.FindByEmailAsync(email)) + .Returns(user); - Assert.Equal(user.Id, result!.Id); - Assert.Equal(user.Email, result!.Email); + var result = await sut.CreateUserIfNotExistsAsync(email, true); - Assert.False(created); + Assert.Equal((user, false), result); } [Fact] public async Task Should_add_claim_when_not_added_yet() { - var (user, claims) = GenerateUser("id2"); - - A.CallTo(() => userManager.AddClaimsAsync(user, A>._)) - .Returns(IdentityResult.Success); + var id = "123"; - SetupUser(user, claims); + await sut.SetClaimAsync(id, "my-claim", "my-value"); - await sut.SetClaimAsync("id2", "my-claim", "new-value"); - - A.CallTo(() => userManager.AddClaimsAsync(user, - A>.That.Matches(x => x.Any(y => y.Type == "my-claim" && y.Value == "new-value")))) + A.CallTo(() => userService.UpdateAsync(id, + A.That.Matches(x => x.CustomClaims!.Any(y => y.Type == "my-claim" && y.Value == "my-value")))) .MustHaveHappened(); } [Fact] - public async Task Should_remove_previous_claim() + public async Task Should_resolve_user_by_email() { - var (user, claims) = GenerateUser("id2"); + var id = "123@email.com"; - claims.Add(new Claim("my-claim", "old-value")); + var user = A.Fake(); - A.CallTo(() => userManager.AddClaimsAsync(user, A>._)) - .Returns(IdentityResult.Success); - - A.CallTo(() => userManager.RemoveClaimsAsync(user, A>._)) - .Returns(IdentityResult.Success); - - SetupUser(user, claims); - - await sut.SetClaimAsync("id2", "my-claim", "new-value"); + A.CallTo(() => userService.FindByEmailAsync(id)) + .Returns(user); - A.CallTo(() => userManager.AddClaimsAsync(user, - A>.That.Matches(x => x.Any(y => y.Type == "my-claim" && y.Value == "new-value")))) - .MustHaveHappened(); + var result = await sut.FindByIdOrEmailAsync(id); - A.CallTo(() => userManager.RemoveClaimsAsync(user, - A>.That.Matches(x => x.Any(y => y.Type == "my-claim" && y.Value == "old-value")))) - .MustHaveHappened(); + Assert.Equal(user, result); } [Fact] - public async Task Should_resolve_user_by_email() + public async Task Should_resolve_user_by_id() { - var (user, claims) = GenerateUser("id1"); + var id = "123"; - SetupUser(user, claims); + var user = A.Fake(); - var result = await sut.FindByIdOrEmailAsync(user.Email); + A.CallTo(() => userService.FindByIdAsync(id)) + .Returns(user); - Assert.Equal(user.Id, result!.Id); - Assert.Equal(user.Email, result!.Email); + var result = await sut.FindByIdOrEmailAsync(id); - Assert.Equal(claims, result!.Claims); + Assert.Equal(user, result); } [Fact] - public async Task Should_resolve_user_by_id() + public async Task Should_resolve_user_by_id_only() { - var (user, claims) = GenerateUser("id2"); + var id = "123"; - SetupUser(user, claims); + var user = A.Fake(); - var result = await sut.FindByIdOrEmailAsync(user.Id)!; + A.CallTo(() => userService.FindByIdAsync(id)) + .Returns(user); - Assert.Equal(user.Id, result!.Id); - Assert.Equal(user.Email, result!.Email); + var result = await sut.FindByIdOrEmailAsync(id); - Assert.Equal(claims, result!.Claims); + Assert.Equal(user, result); } [Fact] - public async Task Should_resolve_user_by_id_only() + public async Task Should_query_many_by_email() { - var (user, claims) = GenerateUser("id2"); + var email = "hello@squidex.io"; - SetupUser(user, claims); + var users = ResultList.CreateFrom(0, A.Fake()); - var result = await sut.FindByIdAsync(user.Id)!; + A.CallTo(() => userService.QueryAsync(email, 10, 0)) + .Returns(users); - Assert.Equal(user.Id, result!.Id); - Assert.Equal(user.Email, result!.Email); + var result = await sut.QueryByEmailAsync(email); - Assert.Equal(claims, result!.Claims); + Assert.Single(result); } [Fact] - public async Task Should_query_many_by_email_async() + public async Task Should_query_by_ids() { - var (user1, claims1) = GenerateUser("id1"); - var (user2, claims2) = GenerateUser("id2"); - - var list = new List { user1, user2 }; - - A.CallTo(() => userManager.Users) - .Returns(list.AsQueryable()); + var ids = new[] { "1", "2" }; - A.CallTo(() => userManager.GetClaimsAsync(user2)) - .Returns(claims2); + var users = ResultList.CreateFrom(0, A.Fake()); - var result = await sut.QueryByEmailAsync("2"); + A.CallTo(() => userService.QueryAsync(ids)) + .Returns(users); - Assert.Equal(user2.Id, result[0].Id); - Assert.Equal(user2.Email, result[0].Email); + var result = await sut.QueryManyAsync(ids); - Assert.Equal(claims2, result[0].Claims); - - A.CallTo(() => userManager.GetClaimsAsync(user1)) - .MustNotHaveHappened(); + Assert.Single(result); } - private static (IdentityUser, List) GenerateUser(string id) + [Fact] + public async Task Should_query_all() { - var user = new IdentityUser { Id = id, Email = $"email_{id}", NormalizedEmail = $"EMAIL_{id}" }; + var users = ResultList.CreateFrom(0, A.Fake()); - var claims = new List - { - new Claim($"{id}_a", "1"), - new Claim($"{id}_b", "2") - }; + A.CallTo(() => userService.QueryAsync(null, int.MaxValue, 0)) + .Returns(users); - return (user, claims); - } - - private void SetupUser(IdentityUser user, List claims) - { - A.CallTo(() => userManager.FindByEmailAsync(user.Email)) - .Returns(user); - - A.CallTo(() => userManager.FindByIdAsync(user.Id)) - .Returns(user); + var result = await sut.QueryAllAsync(); - A.CallTo(() => userManager.GetClaimsAsync(user)) - .Returns(claims); + Assert.Single(result); } } } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs new file mode 100644 index 000000000..18d7eb33d --- /dev/null +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs @@ -0,0 +1,607 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Identity; +using Squidex.Infrastructure; +using Squidex.Log; +using Squidex.Shared; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Users +{ + public class DefaultUserServiceTests + { + private readonly UserManager userManager = A.Fake>(); + private readonly IUserFactory userFactory = A.Fake(); + private readonly IUserEvents userEvents = A.Fake(); + private readonly DefaultUserService sut; + + public DefaultUserServiceTests() + { + A.CallTo(() => userFactory.IsId(A._)) + .Returns(true); + + A.CallTo(userManager).WithReturnType>() + .Returns(IdentityResult.Success); + + sut = new DefaultUserService(userManager, userFactory, Enumerable.Repeat(userEvents, 1), A.Fake()); + } + + [Fact] + public async Task Should_not_resolve_identity_if_id_not_valid() + { + var invalidId = "__"; + + A.CallTo(() => userFactory.IsId(invalidId)) + .Returns(false); + + var result = await sut.FindByIdAsync(invalidId); + + Assert.Null(result); + + A.CallTo(() => userManager.FindByIdAsync(invalidId)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_return_identity_by_id_if_found() + { + var identity = CreateIdentity(found: true); + + var result = await sut.FindByIdAsync(identity.Id); + + Assert.Same(identity, result?.Identity); + } + + [Fact] + public async Task Should_return_null_if_identity_by_id_not_found() + { + var identity = CreateIdentity(found: false); + + var result = await sut.FindByIdAsync(identity.Id); + + Assert.Null(result); + } + + [Fact] + public async Task Should_return_identity_by_email_if_found() + { + var identity = CreateIdentity(found: true); + + var result = await sut.FindByEmailAsync(identity.Email); + + Assert.Same(identity, result?.Identity); + } + + [Fact] + public async Task Should_return_null_if_identity_by_email_not_found() + { + var identity = CreateIdentity(found: false); + + var result = await sut.FindByEmailAsync(identity.Email); + + Assert.Null(result); + } + + [Fact] + public async Task Should_return_identity_by_login_if_found() + { + var provider = "my-provider"; + var providerKey = "key"; + + var identity = CreateIdentity(found: true); + + A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) + .Returns(identity); + + var result = await sut.FindByLoginAsync(provider, providerKey); + + Assert.Same(identity, result?.Identity); + } + + [Fact] + public async Task Should_return_null_if_identity_by_login_not_found() + { + var provider = "my-provider"; + var providerKey = "key"; + + var identity = CreateIdentity(found: false); + + A.CallTo(() => userManager.FindByLoginAsync(provider, providerKey)) + .Returns(Task.FromResult(null!)); + + var result = await sut.FindByLoginAsync(provider, providerKey); + + Assert.Null(result); + } + + [Fact] + public async Task Should_provide_password_existence() + { + var identity = CreateIdentity(found: true); + + var user = A.Fake(); + + A.CallTo(() => user.Identity) + .Returns(identity); + + A.CallTo(() => userManager.HasPasswordAsync(identity)) + .Returns(true); + + var result = await sut.HasPasswordAsync(user); + + Assert.True(result); + } + + [Fact] + public async Task Should_provide_logins() + { + var logins = new List(); + + var identity = CreateIdentity(found: true); + + var user = A.Fake(); + + A.CallTo(() => user.Identity) + .Returns(identity); + + A.CallTo(() => userManager.GetLoginsAsync(identity)) + .Returns(logins); + + var result = await sut.GetLoginsAsync(user); + + Assert.Same(logins, result); + } + + [Fact] + public async Task Create_should_add_user() + { + var identity = CreateIdentity(found: false); + + var values = new UserValues + { + Email = identity.Email + }; + + SetupCreation(identity, values, 1); + + await sut.CreateAsync(values.Email, values); + + A.CallTo(() => userEvents.OnUserRegistered(A.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); + + A.CallTo(() => userEvents.OnConsentGiven(A.That.Matches(x => x.Identity == identity))) + .MustNotHaveHappened(); + + A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions))) + .MustNotHaveHappened(); + + A.CallTo(() => userManager.AddPasswordAsync(identity, A._)) + .MustNotHaveHappened(); + + A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Create_should_raise_event_if_consent_given() + { + var identity = CreateIdentity(found: false); + + var values = new UserValues + { + Consent = true, + }; + + SetupCreation(identity, values, 1); + + await sut.CreateAsync(identity.Email, values); + + A.CallTo(() => userEvents.OnConsentGiven(A.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); + } + + [Fact] + public async Task Create_should_set_admin_if_first_user() + { + var identity = CreateIdentity(found: false); + + var values = new UserValues + { + Consent = true, + }; + + SetupCreation(identity, values, 0); + + await sut.CreateAsync(identity.Email, values); + + A.CallTo(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Permissions, Permissions.Admin))) + .MustHaveHappened(); + } + + [Fact] + public async Task Create_should_not_lock_first_user() + { + var identity = CreateIdentity(found: false); + + var values = new UserValues + { + Consent = true, + }; + + SetupCreation(identity, values, 0); + + await sut.CreateAsync(identity.Email, values, true); + + A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Create_should_lock_second_user() + { + var identity = CreateIdentity(found: false); + + var values = new UserValues + { + Consent = true, + }; + + SetupCreation(identity, values, 1); + + await sut.CreateAsync(identity.Email, values, true); + + A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) + .MustHaveHappened(); + } + + [Fact] + public async Task Create_should_add_password() + { + var identity = CreateIdentity(found: false); + + var values = new UserValues + { + Password = "password" + }; + + SetupCreation(identity, values, 1); + + await sut.CreateAsync(identity.Email, values, false); + + A.CallTo(() => userManager.AddPasswordAsync(identity, values.Password)) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_throw_exception_if_not_found() + { + var update = new UserValues + { + Email = "new@email.com" + }; + + var identity = CreateIdentity(found: false); + + await Assert.ThrowsAsync(() => sut.UpdateAsync(identity.Id, update)); + } + + [Fact] + public async Task Update_should_do_nothing_for_new_update() + { + var update = new UserValues(); + + var identity = CreateIdentity(found: true); + + await sut.UpdateAsync(identity.Id, update); + + A.CallTo(() => userEvents.OnUserUpdated(A.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_change_password_if_changed() + { + var update = new UserValues + { + Password = "password" + }; + + var identity = CreateIdentity(found: true); + + A.CallTo(() => userManager.HasPasswordAsync(identity)) + .Returns(true); + + await sut.UpdateAsync(identity.Id, update); + + A.CallTo(() => userManager.RemovePasswordAsync(identity)) + .MustHaveHappened(); + + A.CallTo(() => userManager.AddPasswordAsync(identity, update.Password)) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_change_email_if_changed() + { + var update = new UserValues + { + Email = "new@email.com" + }; + + var identity = CreateIdentity(found: true); + + await sut.UpdateAsync(identity.Id, update); + + A.CallTo(() => userManager.SetEmailAsync(identity, update.Email)) + .MustHaveHappened(); + + A.CallTo(() => userManager.SetUserNameAsync(identity, update.Email)) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_set_claim_if_consent_given() + { + var update = new UserValues + { + Consent = true + }; + + var identity = CreateIdentity(found: true); + + await sut.UpdateAsync(identity.Id, update); + + A.CallTo>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.Consent))) + .MustHaveHappened(); + + A.CallTo(() => userEvents.OnConsentGiven(A.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); + } + + [Fact] + public async Task Update_should_set_claim_if_email_consent_given() + { + var update = new UserValues + { + ConsentForEmails = true + }; + + var identity = CreateIdentity(found: true); + + await sut.UpdateAsync(identity.Id, update); + + A.CallTo>(() => userManager.AddClaimsAsync(identity, HasClaim(SquidexClaimTypes.ConsentForEmails))) + .MustHaveHappened(); + + A.CallTo(() => userEvents.OnConsentGiven(A.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); + } + + [Fact] + public async Task SetPassword_should_throw_exception_if_not_found() + { + var password = "password"; + + var identity = CreateIdentity(found: false); + + await Assert.ThrowsAsync(() => sut.SetPasswordAsync(identity.Id, password, null)); + } + + [Fact] + public async Task SetPassword_should_succeed_if_found() + { + var password = "password"; + + var identity = CreateIdentity(found: true);; + + await sut.SetPasswordAsync(identity.Id, password, null); + + A.CallTo(() => userManager.AddPasswordAsync(identity, password)) + .MustHaveHappened(); + } + + [Fact] + public async Task SetPassword_should_change_password_if_identity_has_password() + { + var password = "password"; + + var identity = CreateIdentity(found: true); + + A.CallTo(() => userManager.HasPasswordAsync(identity)) + .Returns(true); + + await sut.SetPasswordAsync(identity.Id, password, "old"); + + A.CallTo(() => userManager.ChangePasswordAsync(identity, "old", password)) + .MustHaveHappened(); + } + + [Fact] + public async Task AddLogin_should_throw_exception_if_not_found() + { + var login = A.Fake(); + + var identity = CreateIdentity(found: false); + + await Assert.ThrowsAsync(() => sut.AddLoginAsync(identity.Id, login)); + } + + [Fact] + public async Task AddLogin_should_succeed_if_found() + { + var login = A.Fake(); + + var identity = CreateIdentity(found: true); + + await sut.AddLoginAsync(identity.Id, login); + + A.CallTo(() => userManager.AddLoginAsync(identity, login)) + .MustHaveHappened(); + } + + [Fact] + public async Task RemoveLogin_should_throw_exception_if_not_found() + { + var provider = "my-provider"; + var providerKey = "key"; + + var identity = CreateIdentity(found: false); + + await Assert.ThrowsAsync(() => sut.RemoveLoginAsync(identity.Id, provider, providerKey)); + } + + [Fact] + public async Task RemoveLogin_should_succeed_if_found() + { + var provider = "my-provider"; + var providerKey = "key"; + + var identity = CreateIdentity(found: true); + + await sut.RemoveLoginAsync(identity.Id, provider, providerKey); + + A.CallTo(() => userManager.RemoveLoginAsync(identity, provider, providerKey)) + .MustHaveHappened(); + } + + [Fact] + public async Task Lock_should_throw_exception_if_not_found() + { + var identity = CreateIdentity(found: false); + + await Assert.ThrowsAsync(() => sut.LockAsync(identity.Id)); + } + + [Fact] + public async Task Lock_should_succeed_if_found() + { + var identity = CreateIdentity(found: true); + + await sut.LockAsync(identity.Id); + + A.CallTo>(() => userManager.SetLockoutEndDateAsync(identity, InFuture())) + .MustHaveHappened(); + } + + [Fact] + public async Task Unlock_should_throw_exception_if_not_found() + { + var identity = CreateIdentity(found: false); + + await Assert.ThrowsAsync(() => sut.UnlockAsync(identity.Id)); + } + + [Fact] + public async Task Unlock_should_succeeed_if_found() + { + var identity = CreateIdentity(found: true); + + await sut.UnlockAsync(identity.Id); + + A.CallTo(() => userManager.SetLockoutEndDateAsync(identity, null)) + .MustHaveHappened(); + } + + [Fact] + public async Task Delete_should_throw_exception_if_not_found() + { + var identity = CreateIdentity(found: false); + + await Assert.ThrowsAsync(() => sut.DeleteAsync(identity.Id)); + + A.CallTo(() => userEvents.OnUserDeleted(A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Delete_should_succeed_if_found() + { + var identity = CreateIdentity(found: true); + + await sut.DeleteAsync(identity.Id); + + A.CallTo(() => userManager.DeleteAsync(identity)) + .MustHaveHappened(); + + A.CallTo(() => userEvents.OnUserDeleted(A.That.Matches(x => x.Identity == identity))) + .MustHaveHappened(); + } + + private IdentityUser CreateIdentity(bool found, string id = "123") + { + var identity = CreatePendingUser(id); + + if (found) + { + A.CallTo(() => userManager.FindByIdAsync(identity.Id)) + .Returns(identity); + + A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) + .Returns(identity); + } + else + { + A.CallTo(() => userManager.FindByIdAsync(identity.Id)) + .Returns(Task.FromResult(null!)); + + A.CallTo(() => userManager.FindByEmailAsync(identity.Email)) + .Returns(Task.FromResult(null!)); + } + + return identity; + } + + private void SetupCreation(IdentityUser identity, UserValues values, int numCurrentUsers) + { + var users = new List(); + + for (var i = 0; i < numCurrentUsers; i++) + { + users.Add(CreatePendingUser(i.ToString())); + } + + A.CallTo(() => userManager.Users) + .Returns(users.AsQueryable()); + + A.CallTo(() => userFactory.Create(identity.Email)) + .Returns(identity); + } + + private static IEnumerable HasClaim(string claim) + { + return A>.That.Matches(x => x.Any(y => y.Type == claim)); + } + + private static IEnumerable HasClaim(string claim, string value) + { + return A>.That.Matches(x => x.Any(y => y.Type == claim && y.Value == value)); + } + + private static DateTimeOffset InFuture() + { + return A.That.Matches(x => x >= DateTimeOffset.UtcNow.AddYears(1)); + } + + private static IdentityUser CreatePendingUser(string id = "123") + { + return new IdentityUser + { + Id = id, + Email = $"{id}@email.com" + }; + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs index ec215d2ac..3a9417d26 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs @@ -162,7 +162,7 @@ namespace TestSuite.ApiTests var contributor_1 = contributors_1.Items.FirstOrDefault(x => x.ContributorName == contributorEmail); // Should return contributor with correct email. - Assert.Equal(contributorRole1, contributor_1.Role); + Assert.Equal(contributorRole1, contributor_1?.Role); // STEP 2: Update contributor role. @@ -172,7 +172,7 @@ namespace TestSuite.ApiTests var contributor_2 = contributors_2.Items.FirstOrDefault(x => x.ContributorId == contributor_1.ContributorId); // Should return contributor with correct role. - Assert.Equal(contributorRole2, contributor_2.Role); + Assert.Equal(contributorRole2, contributor_2?.Role); // STEP 3: Remove contributor. diff --git a/frontend/app/features/administration/pages/users/user.component.html b/frontend/app/features/administration/pages/users/user.component.html index 5e3ad64dd..823b61a32 100644 --- a/frontend/app/features/administration/pages/users/user.component.html +++ b/frontend/app/features/administration/pages/users/user.component.html @@ -8,13 +8,22 @@ {{user.email}} - + + + \ No newline at end of file diff --git a/frontend/app/features/administration/pages/users/user.component.ts b/frontend/app/features/administration/pages/users/user.component.ts index d2e436c8e..de9779295 100644 --- a/frontend/app/features/administration/pages/users/user.component.ts +++ b/frontend/app/features/administration/pages/users/user.component.ts @@ -32,4 +32,8 @@ export class UserComponent { public unlock() { this.usersState.unlock(this.user); } + + public delete() { + this.usersState.delete(this.user); + } } \ No newline at end of file diff --git a/frontend/app/features/administration/pages/users/users-page.component.html b/frontend/app/features/administration/pages/users/users-page.component.html index 310c64065..7cac6f278 100644 --- a/frontend/app/features/administration/pages/users/users-page.component.html +++ b/frontend/app/features/administration/pages/users/users-page.component.html @@ -41,7 +41,7 @@ {{ 'common.email' | sqxTranslate }} - + {{ 'common.actions' | sqxTranslate }} diff --git a/frontend/app/features/administration/services/users.service.spec.ts b/frontend/app/features/administration/services/users.service.spec.ts index 6b1366a12..32a916e77 100644 --- a/frontend/app/features/administration/services/users.service.spec.ts +++ b/frontend/app/features/administration/services/users.service.spec.ts @@ -202,6 +202,25 @@ describe('UsersService', () => { expect(user!).toEqual(createUser(12)); })); + it('should make delete request to delete user', + inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { + + const resource: Resource = { + _links: { + delete: { method: 'DELETE', href: 'api/user-management/123' } + } + }; + + userManagementService.deleteUser(resource).subscribe(); + + const req = httpMock.expectOne('http://service/p/api/user-management/123'); + + expect(req.request.method).toEqual('DELETE'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush({}); + })); + function userResponse(id: number) { return { id: `${id}`, diff --git a/frontend/app/features/administration/services/users.service.ts b/frontend/app/features/administration/services/users.service.ts index b6c32af22..f1f73ce37 100644 --- a/frontend/app/features/administration/services/users.service.ts +++ b/frontend/app/features/administration/services/users.service.ts @@ -23,6 +23,7 @@ export class UserDto { public readonly canLock: boolean; public readonly canUnlock: boolean; public readonly canUpdate: boolean; + public readonly canDelete: boolean; constructor(links: ResourceLinks, public readonly id: string, @@ -36,6 +37,7 @@ export class UserDto { this.canLock = hasAnyLink(links, 'lock'); this.canUnlock = hasAnyLink(links, 'unlock'); this.canUpdate = hasAnyLink(links, 'update'); + this.canDelete = hasAnyLink(links, 'delete'); } } @@ -114,7 +116,7 @@ export class UsersService { map(body => { return parseUser(body); }), - pretifyError('i18n:users.loadFailed')); + pretifyError('i18n:users.lockFailed')); } public unlockUser(user: Resource): Observable { @@ -126,7 +128,16 @@ export class UsersService { map(body => { return parseUser(body); }), - pretifyError('i18n:users.loadFailed')); + pretifyError('i18n:users.unlockFailed')); + } + + public deleteUser(user: Resource): Observable { + const link = user._links['delete']; + + const url = this.apiUrl.buildUrl(link.href); + + return this.http.request(link.method, url).pipe( + pretifyError('i18n:users.deleteFailed')); } } diff --git a/frontend/app/features/administration/state/users.state.spec.ts b/frontend/app/features/administration/state/users.state.spec.ts index d45ec152b..5be60d7f8 100644 --- a/frontend/app/features/administration/state/users.state.spec.ts +++ b/frontend/app/features/administration/state/users.state.spec.ts @@ -225,6 +225,16 @@ describe('UsersState', () => { expect(usersState.snapshot.total).toBe(201); }); + it('should remove user from snapshot when delete', () => { + usersService.setup(x => x.deleteUser(user1)) + .returns(() => of(newUser)).verifiable(); + + usersState.delete(user1).subscribe(); + + expect(usersState.snapshot.users).toEqual([user2]); + expect(usersState.snapshot.total).toBe(199); + }); + it('should truncate users when page size reached', () => { const request = { ...newUser, password: 'password' }; diff --git a/frontend/app/features/administration/state/users.state.ts b/frontend/app/features/administration/state/users.state.ts index cd5c2673e..6041d457e 100644 --- a/frontend/app/features/administration/state/users.state.ts +++ b/frontend/app/features/administration/state/users.state.ts @@ -164,6 +164,18 @@ export class UsersState extends State { shareSubscribed(this.dialogs)); } + public delete(user: UserDto) { + return this.usersService.deleteUser(user).pipe( + tap(updated => { + this.next(s => { + const users = s.users.filter(x => x.id !== user.id); + + return { ...s, users, total: s.total - 1 }; + }, 'Delete'); + }), + shareSubscribed(this.dialogs)); + } + public search(query: string) { if (!this.next({ query, page: 0 }, 'Loading Search')) { return EMPTY; diff --git a/frontend/app/features/content/pages/content/content-page.component.html b/frontend/app/features/content/pages/content/content-page.component.html index eadfec435..a7453452b 100644 --- a/frontend/app/features/content/pages/content/content-page.component.html +++ b/frontend/app/features/content/pages/content/content-page.component.html @@ -21,17 +21,17 @@ diff --git a/frontend/app/features/content/pages/content/references/content-references.component.html b/frontend/app/features/content/pages/content/references/content-references.component.html index ab44e2ac2..4c3843eed 100644 --- a/frontend/app/features/content/pages/content/references/content-references.component.html +++ b/frontend/app/features/content/pages/content/references/content-references.component.html @@ -15,10 +15,10 @@ - {{ 'i18n:contents.noReferences' | sqxTranslate }} + {{ 'contents.noReferences' | sqxTranslate }} - {{ 'i18n:contents.noReferencing' | sqxTranslate }} + {{ 'contents.noReferencing' | sqxTranslate }} diff --git a/frontend/app/features/schemas/pages/schema/schema-page.component.html b/frontend/app/features/schemas/pages/schema/schema-page.component.html index 4fdb92506..1c2c14679 100644 --- a/frontend/app/features/schemas/pages/schema/schema-page.component.html +++ b/frontend/app/features/schemas/pages/schema/schema-page.component.html @@ -5,27 +5,27 @@ diff --git a/frontend/app/features/schemas/pages/schema/ui/schema-ui-form.component.html b/frontend/app/features/schemas/pages/schema/ui/schema-ui-form.component.html index 78d219268..d2b983be5 100644 --- a/frontend/app/features/schemas/pages/schema/ui/schema-ui-form.component.html +++ b/frontend/app/features/schemas/pages/schema/ui/schema-ui-form.component.html @@ -3,11 +3,11 @@ diff --git a/frontend/app/shared/components/schema-category.component.html b/frontend/app/shared/components/schema-category.component.html index a0a916c86..6ab316365 100644 --- a/frontend/app/shared/components/schema-category.component.html +++ b/frontend/app/shared/components/schema-category.component.html @@ -17,7 +17,7 @@ {{schemaCategory.name}} - {{ 'i18n:common.schemas' | sqxTranslate }} + {{ 'common.schemas' | sqxTranslate }} diff --git a/frontend/app/shared/state/contents.forms.ts b/frontend/app/shared/state/contents.forms.ts index e7c6da015..ed534bb32 100644 --- a/frontend/app/shared/state/contents.forms.ts +++ b/frontend/app/shared/state/contents.forms.ts @@ -310,7 +310,7 @@ export class FieldValueForm extends AbstractContentForm { } public visitJson(_: JsonFieldPropertiesDto): any { - return undefined; + return null; } public visitNumber(properties: NumberFieldPropertiesDto): any {