From cf309cccbad81494b84d1ca8328f3b3096629737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=C4=83ng=20Nguy=E1=BB=85n?= Date: Wed, 10 Sep 2025 10:48:39 +0700 Subject: [PATCH] Init project --- .gitignore | 1 + RobotApp.Client/Layout/MainLayout.razor | 23 ++ RobotApp.Client/Layout/MainLayout.razor.css | 98 ++++++ RobotApp.Client/Layout/NavMenu.razor | 92 ++++++ RobotApp.Client/Layout/NavMenu.razor.css | 125 ++++++++ RobotApp.Client/Pages/Auth.razor | 13 + RobotApp.Client/Pages/Counter.razor | 18 ++ RobotApp.Client/Pages/Home.razor | 7 + RobotApp.Client/Pages/Weather.razor | 63 ++++ RobotApp.Client/Program.cs | 9 + RobotApp.Client/RedirectToLogin.razor | 8 + RobotApp.Client/RobotApp.Client.csproj | 16 + RobotApp.Client/Routes.razor | 10 + RobotApp.Client/_Imports.razor | 10 + RobotApp.Client/libman.json | 15 + .../wwwroot/appsettings.Development.json | 8 + RobotApp.Client/wwwroot/appsettings.json | 8 + RobotApp.VDA5050/Connection/ConnectionMsg.cs | 28 ++ .../Factsheet/ActionParameters.cs | 25 ++ RobotApp.VDA5050/Factsheet/AgvActions.cs | 23 ++ RobotApp.VDA5050/Factsheet/AgvGeometry.cs | 12 + .../Factsheet/BoundingBoxReference.cs | 15 + RobotApp.VDA5050/Factsheet/Envelopes2d.cs | 21 ++ RobotApp.VDA5050/Factsheet/Envelopes3d.cs | 16 + RobotApp.VDA5050/Factsheet/FactSheetMsg.cs | 30 ++ RobotApp.VDA5050/Factsheet/LoadDimensions.cs | 13 + RobotApp.VDA5050/Factsheet/LoadSets.cs | 26 ++ .../Factsheet/LoadSpecification.cs | 9 + .../Factsheet/LocalizationParameter.cs | 6 + RobotApp.VDA5050/Factsheet/MaxArrayLens.cs | 21 ++ RobotApp.VDA5050/Factsheet/MaxStringLens.cs | 12 + .../Factsheet/OptionalParameters.cs | 19 ++ .../Factsheet/PhysicalParameters.cs | 22 ++ .../Factsheet/ProtocolFeatures.cs | 13 + RobotApp.VDA5050/Factsheet/ProtocolLimits.cs | 14 + RobotApp.VDA5050/Factsheet/Timing.cs | 13 + .../Factsheet/TypeSpecification.cs | 50 ++++ .../Factsheet/WheelDefinitions.cs | 39 +++ RobotApp.VDA5050/FactsheetExtend/Battery.cs | 9 + .../FactsheetExtend/BatteryThreshold.cs | 11 + .../FactsheetExtend/CameraSafety.cs | 31 ++ .../FactsheetExtend/ChargerParam.cs | 9 + .../FactsheetExtend/FactsheetExtendMsg.cs | 19 ++ .../FactsheetExtend/ForkSafety.cs | 9 + RobotApp.VDA5050/FactsheetExtend/Initpose.cs | 9 + .../FactsheetExtend/LineSegment.cs | 10 + .../FactsheetExtend/Localization.cs | 14 + RobotApp.VDA5050/FactsheetExtend/Motor.cs | 9 + .../FactsheetExtend/Navigation.cs | 11 + RobotApp.VDA5050/FactsheetExtend/PPA.cs | 7 + RobotApp.VDA5050/FactsheetExtend/PTA.cs | 15 + .../FactsheetExtend/RobotParam.cs | 12 + RobotApp.VDA5050/FactsheetExtend/Rotate.cs | 9 + RobotApp.VDA5050/FactsheetExtend/Safety.cs | 8 + .../FactsheetExtend/ServerParam.cs | 14 + RobotApp.VDA5050/FactsheetExtend/VlMarker.cs | 21 ++ RobotApp.VDA5050/FactsheetExtend/Xloc.cs | 16 + .../InstantAction/ActionParameter.cs | 12 + RobotApp.VDA5050/InstantAction/Actions.cs | 23 ++ .../InstantAction/InstantActionsMsg.cs | 13 + RobotApp.VDA5050/Order/Corridor.cs | 18 ++ RobotApp.VDA5050/Order/Edge.cs | 33 +++ RobotApp.VDA5050/Order/EdgeLog.cs | 18 ++ RobotApp.VDA5050/Order/Node.cs | 19 ++ RobotApp.VDA5050/Order/NodeLog.cs | 19 ++ RobotApp.VDA5050/Order/NodePosition.cs | 19 ++ RobotApp.VDA5050/Order/OrderLog.cs | 21 ++ RobotApp.VDA5050/Order/OrderMsg.cs | 29 ++ RobotApp.VDA5050/Order/Trajectory.cs | 23 ++ RobotApp.VDA5050/RobotApp.VDA5050.csproj | 9 + RobotApp.VDA5050/State/ActionState.cs | 25 ++ RobotApp.VDA5050/State/BatteryState.cs | 15 + RobotApp.VDA5050/State/EdgeState.cs | 19 ++ RobotApp.VDA5050/State/Error.cs | 29 ++ RobotApp.VDA5050/State/Information.cs | 27 ++ RobotApp.VDA5050/State/Load.cs | 16 + RobotApp.VDA5050/State/Map.cs | 20 ++ RobotApp.VDA5050/State/NodeState.cs | 30 ++ RobotApp.VDA5050/State/SafetyState.cs | 20 ++ RobotApp.VDA5050/State/StateMsg.cs | 61 ++++ RobotApp.VDA5050/Type/ActionType.cs | 35 +++ RobotApp.VDA5050/VDA5050Helper.cs | 29 ++ RobotApp.VDA5050/VDA5050Setting.cs | 19 ++ RobotApp.VDA5050/VDA5050Topic.cs | 12 + RobotApp.VDA5050/Visualization/AgvPosition.cs | 21 ++ RobotApp.VDA5050/Visualization/Velocity.cs | 8 + .../Visualization/Visualizationmsg.cs | 16 + RobotApp.sln | 34 +++ ...omponentsEndpointRouteBuilderExtensions.cs | 113 +++++++ .../Account/IdentityNoOpEmailSender.cs | 21 ++ .../Account/IdentityRedirectManager.cs | 59 ++++ ...RevalidatingAuthenticationStateProvider.cs | 48 +++ .../Account/IdentityUserAccessor.cs | 20 ++ .../Account/Pages/AccessDenied.razor | 8 + .../Account/Pages/ConfirmEmail.razor | 48 +++ .../Account/Pages/ConfirmEmailChange.razor | 68 +++++ .../Account/Pages/ExternalLogin.razor | 205 +++++++++++++ .../Account/Pages/ForgotPassword.razor | 68 +++++ .../Pages/ForgotPasswordConfirmation.razor | 8 + .../Account/Pages/InvalidPasswordReset.razor | 8 + .../Account/Pages/InvalidUser.razor | 7 + .../Components/Account/Pages/Lockout.razor | 8 + RobotApp/Components/Account/Pages/Login.razor | 128 ++++++++ .../Account/Pages/LoginWith2fa.razor | 101 +++++++ .../Account/Pages/LoginWithRecoveryCode.razor | 85 ++++++ .../Account/Pages/Manage/ChangePassword.razor | 96 ++++++ .../Pages/Manage/DeletePersonalData.razor | 86 ++++++ .../Account/Pages/Manage/Disable2fa.razor | 64 ++++ .../Account/Pages/Manage/Email.razor | 123 ++++++++ .../Pages/Manage/EnableAuthenticator.razor | 172 +++++++++++ .../Account/Pages/Manage/ExternalLogins.razor | 140 +++++++++ .../Pages/Manage/GenerateRecoveryCodes.razor | 68 +++++ .../Account/Pages/Manage/Index.razor | 77 +++++ .../Account/Pages/Manage/PersonalData.razor | 34 +++ .../Pages/Manage/ResetAuthenticator.razor | 52 ++++ .../Account/Pages/Manage/SetPassword.razor | 87 ++++++ .../Manage/TwoFactorAuthentication.razor | 101 +++++++ .../Account/Pages/Manage/_Imports.razor | 2 + .../Components/Account/Pages/Register.razor | 145 +++++++++ .../Account/Pages/RegisterConfirmation.razor | 68 +++++ .../Pages/ResendEmailConfirmation.razor | 68 +++++ .../Account/Pages/ResetPassword.razor | 103 +++++++ .../Pages/ResetPasswordConfirmation.razor | 7 + .../Components/Account/Pages/_Imports.razor | 2 + .../Account/Shared/ExternalLoginPicker.razor | 43 +++ .../Account/Shared/ManageLayout.razor | 17 ++ .../Account/Shared/ManageNavMenu.razor | 37 +++ .../Account/Shared/ShowRecoveryCodes.razor | 28 ++ .../Account/Shared/StatusMessage.razor | 29 ++ RobotApp/Components/App.razor | 29 ++ RobotApp/Components/Pages/Error.razor | 36 +++ RobotApp/Components/_Imports.razor | 12 + RobotApp/Data/ApplicationDbContext.cs | 9 + RobotApp/Data/ApplicationUser.cs | 10 + ...000000000_CreateIdentitySchema.Designer.cs | 279 ++++++++++++++++++ .../00000000000000_CreateIdentitySchema.cs | 224 ++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 276 +++++++++++++++++ RobotApp/Program.cs | 70 +++++ RobotApp/Properties/launchSettings.json | 25 ++ RobotApp/Properties/serviceDependencies.json | 8 + .../Properties/serviceDependencies.local.json | 8 + RobotApp/RobotApp.csproj | 23 ++ RobotApp/appsettings.Development.json | 8 + RobotApp/appsettings.json | 12 + RobotApp/wwwroot/app.css | 60 ++++ RobotApp/wwwroot/favicon.png | Bin 0 -> 1148 bytes 146 files changed, 5627 insertions(+) create mode 100644 RobotApp.Client/Layout/MainLayout.razor create mode 100644 RobotApp.Client/Layout/MainLayout.razor.css create mode 100644 RobotApp.Client/Layout/NavMenu.razor create mode 100644 RobotApp.Client/Layout/NavMenu.razor.css create mode 100644 RobotApp.Client/Pages/Auth.razor create mode 100644 RobotApp.Client/Pages/Counter.razor create mode 100644 RobotApp.Client/Pages/Home.razor create mode 100644 RobotApp.Client/Pages/Weather.razor create mode 100644 RobotApp.Client/Program.cs create mode 100644 RobotApp.Client/RedirectToLogin.razor create mode 100644 RobotApp.Client/RobotApp.Client.csproj create mode 100644 RobotApp.Client/Routes.razor create mode 100644 RobotApp.Client/_Imports.razor create mode 100644 RobotApp.Client/libman.json create mode 100644 RobotApp.Client/wwwroot/appsettings.Development.json create mode 100644 RobotApp.Client/wwwroot/appsettings.json create mode 100644 RobotApp.VDA5050/Connection/ConnectionMsg.cs create mode 100644 RobotApp.VDA5050/Factsheet/ActionParameters.cs create mode 100644 RobotApp.VDA5050/Factsheet/AgvActions.cs create mode 100644 RobotApp.VDA5050/Factsheet/AgvGeometry.cs create mode 100644 RobotApp.VDA5050/Factsheet/BoundingBoxReference.cs create mode 100644 RobotApp.VDA5050/Factsheet/Envelopes2d.cs create mode 100644 RobotApp.VDA5050/Factsheet/Envelopes3d.cs create mode 100644 RobotApp.VDA5050/Factsheet/FactSheetMsg.cs create mode 100644 RobotApp.VDA5050/Factsheet/LoadDimensions.cs create mode 100644 RobotApp.VDA5050/Factsheet/LoadSets.cs create mode 100644 RobotApp.VDA5050/Factsheet/LoadSpecification.cs create mode 100644 RobotApp.VDA5050/Factsheet/LocalizationParameter.cs create mode 100644 RobotApp.VDA5050/Factsheet/MaxArrayLens.cs create mode 100644 RobotApp.VDA5050/Factsheet/MaxStringLens.cs create mode 100644 RobotApp.VDA5050/Factsheet/OptionalParameters.cs create mode 100644 RobotApp.VDA5050/Factsheet/PhysicalParameters.cs create mode 100644 RobotApp.VDA5050/Factsheet/ProtocolFeatures.cs create mode 100644 RobotApp.VDA5050/Factsheet/ProtocolLimits.cs create mode 100644 RobotApp.VDA5050/Factsheet/Timing.cs create mode 100644 RobotApp.VDA5050/Factsheet/TypeSpecification.cs create mode 100644 RobotApp.VDA5050/Factsheet/WheelDefinitions.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/Battery.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/BatteryThreshold.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/CameraSafety.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/ChargerParam.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/FactsheetExtendMsg.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/ForkSafety.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/Initpose.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/LineSegment.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/Localization.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/Motor.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/Navigation.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/PPA.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/PTA.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/RobotParam.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/Rotate.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/Safety.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/ServerParam.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/VlMarker.cs create mode 100644 RobotApp.VDA5050/FactsheetExtend/Xloc.cs create mode 100644 RobotApp.VDA5050/InstantAction/ActionParameter.cs create mode 100644 RobotApp.VDA5050/InstantAction/Actions.cs create mode 100644 RobotApp.VDA5050/InstantAction/InstantActionsMsg.cs create mode 100644 RobotApp.VDA5050/Order/Corridor.cs create mode 100644 RobotApp.VDA5050/Order/Edge.cs create mode 100644 RobotApp.VDA5050/Order/EdgeLog.cs create mode 100644 RobotApp.VDA5050/Order/Node.cs create mode 100644 RobotApp.VDA5050/Order/NodeLog.cs create mode 100644 RobotApp.VDA5050/Order/NodePosition.cs create mode 100644 RobotApp.VDA5050/Order/OrderLog.cs create mode 100644 RobotApp.VDA5050/Order/OrderMsg.cs create mode 100644 RobotApp.VDA5050/Order/Trajectory.cs create mode 100644 RobotApp.VDA5050/RobotApp.VDA5050.csproj create mode 100644 RobotApp.VDA5050/State/ActionState.cs create mode 100644 RobotApp.VDA5050/State/BatteryState.cs create mode 100644 RobotApp.VDA5050/State/EdgeState.cs create mode 100644 RobotApp.VDA5050/State/Error.cs create mode 100644 RobotApp.VDA5050/State/Information.cs create mode 100644 RobotApp.VDA5050/State/Load.cs create mode 100644 RobotApp.VDA5050/State/Map.cs create mode 100644 RobotApp.VDA5050/State/NodeState.cs create mode 100644 RobotApp.VDA5050/State/SafetyState.cs create mode 100644 RobotApp.VDA5050/State/StateMsg.cs create mode 100644 RobotApp.VDA5050/Type/ActionType.cs create mode 100644 RobotApp.VDA5050/VDA5050Helper.cs create mode 100644 RobotApp.VDA5050/VDA5050Setting.cs create mode 100644 RobotApp.VDA5050/VDA5050Topic.cs create mode 100644 RobotApp.VDA5050/Visualization/AgvPosition.cs create mode 100644 RobotApp.VDA5050/Visualization/Velocity.cs create mode 100644 RobotApp.VDA5050/Visualization/Visualizationmsg.cs create mode 100644 RobotApp.sln create mode 100644 RobotApp/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs create mode 100644 RobotApp/Components/Account/IdentityNoOpEmailSender.cs create mode 100644 RobotApp/Components/Account/IdentityRedirectManager.cs create mode 100644 RobotApp/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs create mode 100644 RobotApp/Components/Account/IdentityUserAccessor.cs create mode 100644 RobotApp/Components/Account/Pages/AccessDenied.razor create mode 100644 RobotApp/Components/Account/Pages/ConfirmEmail.razor create mode 100644 RobotApp/Components/Account/Pages/ConfirmEmailChange.razor create mode 100644 RobotApp/Components/Account/Pages/ExternalLogin.razor create mode 100644 RobotApp/Components/Account/Pages/ForgotPassword.razor create mode 100644 RobotApp/Components/Account/Pages/ForgotPasswordConfirmation.razor create mode 100644 RobotApp/Components/Account/Pages/InvalidPasswordReset.razor create mode 100644 RobotApp/Components/Account/Pages/InvalidUser.razor create mode 100644 RobotApp/Components/Account/Pages/Lockout.razor create mode 100644 RobotApp/Components/Account/Pages/Login.razor create mode 100644 RobotApp/Components/Account/Pages/LoginWith2fa.razor create mode 100644 RobotApp/Components/Account/Pages/LoginWithRecoveryCode.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/ChangePassword.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/DeletePersonalData.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/Disable2fa.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/Email.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/EnableAuthenticator.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/ExternalLogins.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/Index.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/PersonalData.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/ResetAuthenticator.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/SetPassword.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/TwoFactorAuthentication.razor create mode 100644 RobotApp/Components/Account/Pages/Manage/_Imports.razor create mode 100644 RobotApp/Components/Account/Pages/Register.razor create mode 100644 RobotApp/Components/Account/Pages/RegisterConfirmation.razor create mode 100644 RobotApp/Components/Account/Pages/ResendEmailConfirmation.razor create mode 100644 RobotApp/Components/Account/Pages/ResetPassword.razor create mode 100644 RobotApp/Components/Account/Pages/ResetPasswordConfirmation.razor create mode 100644 RobotApp/Components/Account/Pages/_Imports.razor create mode 100644 RobotApp/Components/Account/Shared/ExternalLoginPicker.razor create mode 100644 RobotApp/Components/Account/Shared/ManageLayout.razor create mode 100644 RobotApp/Components/Account/Shared/ManageNavMenu.razor create mode 100644 RobotApp/Components/Account/Shared/ShowRecoveryCodes.razor create mode 100644 RobotApp/Components/Account/Shared/StatusMessage.razor create mode 100644 RobotApp/Components/App.razor create mode 100644 RobotApp/Components/Pages/Error.razor create mode 100644 RobotApp/Components/_Imports.razor create mode 100644 RobotApp/Data/ApplicationDbContext.cs create mode 100644 RobotApp/Data/ApplicationUser.cs create mode 100644 RobotApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs create mode 100644 RobotApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs create mode 100644 RobotApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 RobotApp/Program.cs create mode 100644 RobotApp/Properties/launchSettings.json create mode 100644 RobotApp/Properties/serviceDependencies.json create mode 100644 RobotApp/Properties/serviceDependencies.local.json create mode 100644 RobotApp/RobotApp.csproj create mode 100644 RobotApp/appsettings.Development.json create mode 100644 RobotApp/appsettings.json create mode 100644 RobotApp/wwwroot/app.css create mode 100644 RobotApp/wwwroot/favicon.png diff --git a/.gitignore b/.gitignore index d5dca0c..66e2cac 100644 --- a/.gitignore +++ b/.gitignore @@ -412,3 +412,4 @@ FodyWeavers.xsd # Built Visual Studio Code Extensions *.vsix +*/wwwroot/lib \ No newline at end of file diff --git a/RobotApp.Client/Layout/MainLayout.razor b/RobotApp.Client/Layout/MainLayout.razor new file mode 100644 index 0000000..78624f3 --- /dev/null +++ b/RobotApp.Client/Layout/MainLayout.razor @@ -0,0 +1,23 @@ +@inherits LayoutComponentBase + +
+ + +
+
+ About +
+ +
+ @Body +
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/RobotApp.Client/Layout/MainLayout.razor.css b/RobotApp.Client/Layout/MainLayout.razor.css new file mode 100644 index 0000000..38d1f25 --- /dev/null +++ b/RobotApp.Client/Layout/MainLayout.razor.css @@ -0,0 +1,98 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/RobotApp.Client/Layout/NavMenu.razor b/RobotApp.Client/Layout/NavMenu.razor new file mode 100644 index 0000000..1a00e42 --- /dev/null +++ b/RobotApp.Client/Layout/NavMenu.razor @@ -0,0 +1,92 @@ +@implements IDisposable + +@inject NavigationManager NavigationManager + + + + + + + +@code { + private string? currentUrl; + + protected override void OnInitialized() + { + currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); + NavigationManager.LocationChanged += OnLocationChanged; + } + + private void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + currentUrl = NavigationManager.ToBaseRelativePath(e.Location); + StateHasChanged(); + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + } +} + diff --git a/RobotApp.Client/Layout/NavMenu.razor.css b/RobotApp.Client/Layout/NavMenu.razor.css new file mode 100644 index 0000000..0145d9d --- /dev/null +++ b/RobotApp.Client/Layout/NavMenu.razor.css @@ -0,0 +1,125 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + min-height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.bi-lock-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E"); +} + +.bi-person-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z'/%3E%3C/svg%3E"); +} + +.bi-person-badge-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E"); +} + +.bi-person-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-fill' viewBox='0 0 16 16'%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E"); +} + +.bi-arrow-bar-left-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/RobotApp.Client/Pages/Auth.razor b/RobotApp.Client/Pages/Auth.razor new file mode 100644 index 0000000..b7bbe6e --- /dev/null +++ b/RobotApp.Client/Pages/Auth.razor @@ -0,0 +1,13 @@ +@page "/auth" + +@using Microsoft.AspNetCore.Authorization + +@attribute [Authorize] + +Auth + +

You are authenticated

+ + + Hello @context.User.Identity?.Name! + diff --git a/RobotApp.Client/Pages/Counter.razor b/RobotApp.Client/Pages/Counter.razor new file mode 100644 index 0000000..ef23cb3 --- /dev/null +++ b/RobotApp.Client/Pages/Counter.razor @@ -0,0 +1,18 @@ +@page "/counter" + +Counter + +

Counter

+ +

Current count: @currentCount

+ + + +@code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } +} diff --git a/RobotApp.Client/Pages/Home.razor b/RobotApp.Client/Pages/Home.razor new file mode 100644 index 0000000..9001e0b --- /dev/null +++ b/RobotApp.Client/Pages/Home.razor @@ -0,0 +1,7 @@ +@page "/" + +Home + +

Hello, world!

+ +Welcome to your new app. diff --git a/RobotApp.Client/Pages/Weather.razor b/RobotApp.Client/Pages/Weather.razor new file mode 100644 index 0000000..dd36b18 --- /dev/null +++ b/RobotApp.Client/Pages/Weather.razor @@ -0,0 +1,63 @@ +@page "/weather" + +Weather + +

Weather

+ +

This component demonstrates showing data.

+ +@if (forecasts == null) +{ +

Loading...

+} +else +{ + + + + + + + + + + + @foreach (var forecast in forecasts) + { + + + + + + + } + +
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+} + +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() + { + // Simulate asynchronous loading to demonstrate a loading indicator + await Task.Delay(500); + + var startDate = DateOnly.FromDateTime(DateTime.Now); + var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; + forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = startDate.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }).ToArray(); + } + + private class WeatherForecast + { + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} diff --git a/RobotApp.Client/Program.cs b/RobotApp.Client/Program.cs new file mode 100644 index 0000000..21e4b6a --- /dev/null +++ b/RobotApp.Client/Program.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +builder.Services.AddAuthorizationCore(); +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddAuthenticationStateDeserialization(); + +await builder.Build().RunAsync(); diff --git a/RobotApp.Client/RedirectToLogin.razor b/RobotApp.Client/RedirectToLogin.razor new file mode 100644 index 0000000..c8b8eff --- /dev/null +++ b/RobotApp.Client/RedirectToLogin.razor @@ -0,0 +1,8 @@ +@inject NavigationManager NavigationManager + +@code { + protected override void OnInitialized() + { + NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true); + } +} diff --git a/RobotApp.Client/RobotApp.Client.csproj b/RobotApp.Client/RobotApp.Client.csproj new file mode 100644 index 0000000..65c1b8a --- /dev/null +++ b/RobotApp.Client/RobotApp.Client.csproj @@ -0,0 +1,16 @@ + + + + net9.0 + enable + enable + true + Default + + + + + + + + diff --git a/RobotApp.Client/Routes.razor b/RobotApp.Client/Routes.razor new file mode 100644 index 0000000..16f0026 --- /dev/null +++ b/RobotApp.Client/Routes.razor @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/RobotApp.Client/_Imports.razor b/RobotApp.Client/_Imports.razor new file mode 100644 index 0000000..11e67c1 --- /dev/null +++ b/RobotApp.Client/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using RobotApp.Client diff --git a/RobotApp.Client/libman.json b/RobotApp.Client/libman.json new file mode 100644 index 0000000..08e2c3c --- /dev/null +++ b/RobotApp.Client/libman.json @@ -0,0 +1,15 @@ +{ + "version": "3.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "bootstrap@5.3.7", + "destination": "wwwroot/lib/bootstrap/" + }, + { + "provider": "jsdelivr", + "library": "@mdi/font@7.4.47", + "destination": "wwwroot/lib/mdi/font/" + } + ] +} \ No newline at end of file diff --git a/RobotApp.Client/wwwroot/appsettings.Development.json b/RobotApp.Client/wwwroot/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/RobotApp.Client/wwwroot/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/RobotApp.Client/wwwroot/appsettings.json b/RobotApp.Client/wwwroot/appsettings.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/RobotApp.Client/wwwroot/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/RobotApp.VDA5050/Connection/ConnectionMsg.cs b/RobotApp.VDA5050/Connection/ConnectionMsg.cs new file mode 100644 index 0000000..2c5168d --- /dev/null +++ b/RobotApp.VDA5050/Connection/ConnectionMsg.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Connection; + +#nullable disable + +public enum ConnectionState +{ + ONLINE, + OFFLINE, + CONNECTIONBROKEN +} + +public class ConnectionMsg +{ + [Required] + public uint HeaderId { get; set; } + [Required] + public string Timestamp { get; set; } = ""; + [Required] + public string Version { get; set; } = ""; + [Required] + public string Manufacturer { get; set; } = ""; + [Required] + public string SerialNumber { get; set; } = ""; + [Required] + public string ConnectionState { get; set; } = ""; +} diff --git a/RobotApp.VDA5050/Factsheet/ActionParameters.cs b/RobotApp.VDA5050/Factsheet/ActionParameters.cs new file mode 100644 index 0000000..9202786 --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/ActionParameters.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public enum ValueDataType +{ + BOOL, + NUMBER, + INTEGER, + FLOAT, + STRING, + OBJECT, + ARRAY, +} +public class ActionParameters +{ + [Required] + public string Key { get; set; } + [Required] + public string ValueDataType { get; set; } + public string Description { get; set; } + public bool IsOptional { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/AgvActions.cs b/RobotApp.VDA5050/Factsheet/AgvActions.cs new file mode 100644 index 0000000..f0a87a5 --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/AgvActions.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public enum ActionScopes +{ + INSTANT, + NODE, + EDGE, +} +public class AgvActions +{ + [Required] + public string ActionType { get; set; } + public string ActionDescription { get; set; } + [Required] + public string[] ActionScopes { get; set; } + public ActionParameters[] ActionParameters { get; set; } + public string ResultDescription { get; set; } + public string[] BlockingTypes { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/AgvGeometry.cs b/RobotApp.VDA5050/Factsheet/AgvGeometry.cs new file mode 100644 index 0000000..2852120 --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/AgvGeometry.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public class AgvGeometry +{ + public WheelDefinitions[] WheelDefinitions { get; set; } + public Envelopes2d[] Envelopes2d { get; set; } + public Envelopes3d[] Envelopes3d { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/BoundingBoxReference.cs b/RobotApp.VDA5050/Factsheet/BoundingBoxReference.cs new file mode 100644 index 0000000..931931d --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/BoundingBoxReference.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +public class BoundingBoxReference +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + [Required] + public double Z { get; set; } + public double Theta { get; set; } + +} diff --git a/RobotApp.VDA5050/Factsheet/Envelopes2d.cs b/RobotApp.VDA5050/Factsheet/Envelopes2d.cs new file mode 100644 index 0000000..01fad43 --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/Envelopes2d.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public class PolygonPoints +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } +} +public class Envelopes2d +{ + [Required] + public string Set { get; set; } + [Required] + public PolygonPoints[] PolygonPoints { get; set; } + public string Description { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/Envelopes3d.cs b/RobotApp.VDA5050/Factsheet/Envelopes3d.cs new file mode 100644 index 0000000..276d8bf --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/Envelopes3d.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public class Envelopes3d +{ + [Required] + public string Set { get; set; } + [Required] + public string Format { get; set; } + public object Data { get; set; } + public string Url { get; set; } + public string Description { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/FactSheetMsg.cs b/RobotApp.VDA5050/Factsheet/FactSheetMsg.cs new file mode 100644 index 0000000..41cef4d --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/FactSheetMsg.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public class FactSheetMsg +{ + public uint HeaderId { get; set; } + public string Timestamp { get; set; } + [Required] + public string Version { get; set; } + [Required] + public string Manufacturer { get; set; } + [Required] + public string SerialNumber { get; set; } + [Required] + public TypeSpecification TypeSpecification { get; set; } + [Required] + public PhysicalParameters PhysicalParameters { get; set; } + [Required] + public ProtocolLimits ProtocolLimits { get; set; } + [Required] + public ProtocolFeatures ProtocolFeatures { get; set; } + [Required] + public AgvGeometry AgvGeometry { get; set; } + [Required] + public LoadSpecification LoadSpecification { get; set; } + //public LocalizationParameter LocalizationParameters { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/LoadDimensions.cs b/RobotApp.VDA5050/Factsheet/LoadDimensions.cs new file mode 100644 index 0000000..dc3f101 --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/LoadDimensions.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +public class LoadDimensions +{ + [Required] + public double Length { get; set; } + [Required] + public double Width { get; set; } + public double Height { get; set; } + +} diff --git a/RobotApp.VDA5050/Factsheet/LoadSets.cs b/RobotApp.VDA5050/Factsheet/LoadSets.cs new file mode 100644 index 0000000..ce9fb7f --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/LoadSets.cs @@ -0,0 +1,26 @@ +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public class LoadSets +{ + public string SetName { get; set; } + public string LoadType { get; set; } + public string[] LoadPositions { get; set; } + public BoundingBoxReference BoundingBoxReference { get; set; } + public LoadDimensions LoadDimensions { get; set; } + public double MaxWeigth { get; set; } + public double MinLoadhandlingHeight { get; set; } + public double MaxLoadhandlingHeight { get; set; } + public double MinLoadhandlingDepth { get; set; } + public double MaxLoadhandlingDepth { get; set; } + public double MinLoadhandlingTilt { get; set; } + public double MaxLoadhandlingTilt { get; set; } + public double AgvSpeedLimit { get; set; } + public double AgvAccelerationLimit { get; set; } + public double AgvDecelerationLimit { get; set; } + public double PickTime { get; set; } + public double DropTime { get; set; } + public string Description { get; set; } + +} diff --git a/RobotApp.VDA5050/Factsheet/LoadSpecification.cs b/RobotApp.VDA5050/Factsheet/LoadSpecification.cs new file mode 100644 index 0000000..7102e9c --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/LoadSpecification.cs @@ -0,0 +1,9 @@ +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public class LoadSpecification +{ + public string[] LoadPositions { get; set; } + public LoadSets[] LoadSets { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/LocalizationParameter.cs b/RobotApp.VDA5050/Factsheet/LocalizationParameter.cs new file mode 100644 index 0000000..c3ba206 --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/LocalizationParameter.cs @@ -0,0 +1,6 @@ +namespace RobotApp.VDA5050.Factsheet; + +public class LocalizationParameter +{ + public double LocalizationParameters { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/MaxArrayLens.cs b/RobotApp.VDA5050/Factsheet/MaxArrayLens.cs new file mode 100644 index 0000000..411d8b9 --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/MaxArrayLens.cs @@ -0,0 +1,21 @@ +namespace RobotApp.VDA5050.Factsheet; + +public class MaxArrayLens +{ + public int OrderNodes { get; set; } + public int OrderEdges { get; set; } + public int NodeActions { get; set; } + public int EdgeActions { get; set; } + public int ActionsActionsParameters { get; set; } + public int InstantActions { get; set; } + public int TrajectoryKnotVector { get; set; } + public int TrajectoryControlPoints { get; set; } + public int StateNodeStates { get; set; } + public int StateEdgeStates { get; set; } + public int StateLoads { get; set; } + public int StateActionStates { get; set; } + public int StateErrors { get; set; } + public int StateInformation { get; set; } + public int ErrorErrorReferences { get; set; } + public int InformationInfoReferences { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/MaxStringLens.cs b/RobotApp.VDA5050/Factsheet/MaxStringLens.cs new file mode 100644 index 0000000..0221420 --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/MaxStringLens.cs @@ -0,0 +1,12 @@ +namespace RobotApp.VDA5050.Factsheet; + +public class MaxStringLens +{ + public int MsgLen { get; set; } + public int TopicSerialLen { get; set; } + public int TopicElemLen { get; set; } + public int IdLen { get; set; } + public bool IdNumericalOnly { get; set; } + public int EnumLen { get; set; } + public int LoadIdLen { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/OptionalParameters.cs b/RobotApp.VDA5050/Factsheet/OptionalParameters.cs new file mode 100644 index 0000000..ed7ecaa --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/OptionalParameters.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public enum Support +{ + SUPPORTED, + REQUIRED, +} +public class OptionalParameters +{ + [Required] + public string Parameter { get; set; } + [Required] + public string Support { get; set; } + public string Description { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/PhysicalParameters.cs b/RobotApp.VDA5050/Factsheet/PhysicalParameters.cs new file mode 100644 index 0000000..0470a8e --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/PhysicalParameters.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +public class PhysicalParameters +{ + [Required] + public double SpeedMin { get; set; } + [Required] + public double SpeedMax { get; set; } + [Required] + public double AccelerationMax { get; set; } + [Required] + public double DecelerationMax { get; set; } + public double HeightMin { get; set; } + [Required] + public double HeightMax { get; set; } + [Required] + public double Width { get; set; } + [Required] + public double Length { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/ProtocolFeatures.cs b/RobotApp.VDA5050/Factsheet/ProtocolFeatures.cs new file mode 100644 index 0000000..d876148 --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/ProtocolFeatures.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public class ProtocolFeatures +{ + [Required] + public OptionalParameters[] OptionalParameters { get; set; } + [Required] + public AgvActions[] AgvActions { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/ProtocolLimits.cs b/RobotApp.VDA5050/Factsheet/ProtocolLimits.cs new file mode 100644 index 0000000..4f58e8d --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/ProtocolLimits.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable +public class ProtocolLimits +{ + [Required] + public MaxStringLens MaxStringLens { get; set; } + [Required] + public MaxArrayLens MaxArrayLens { get; set; } + [Required] + public Timing Timing { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/Timing.cs b/RobotApp.VDA5050/Factsheet/Timing.cs new file mode 100644 index 0000000..dc6552b --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/Timing.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +public class Timing +{ + [Required] + public double MinOrderInterval { get; set; } + [Required] + public double MinStateInterval { get; set; } + public double DefaultStateInterval { get; set; } + public double VisualizationInterval { get; set; } +} diff --git a/RobotApp.VDA5050/Factsheet/TypeSpecification.cs b/RobotApp.VDA5050/Factsheet/TypeSpecification.cs new file mode 100644 index 0000000..c3d2e9a --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/TypeSpecification.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public enum AgvKinematic +{ + DIFF, + OMNI, + THREEWHEEL +} +public enum AgvClass +{ + FORKLIFT, + CONVEYOR, + TUGGER, + CARRIER +} +public enum LocalizationTypes +{ + NATURAL, + REFLECTOR, + RFID, + DMC, + SPOT, + GRID, +} +public enum NavigationTypes +{ + PHYSICAL_LINDE_GUIDED, + VIRTUAL_LINE_GUIDED, + AUTONOMOUS, +} +public class TypeSpecification +{ + [Required] + public string SeriesName { get; set; } + public string SeriesDescription { get; set; } + [Required] + public string AgvKinematic { get; set; } + [Required] + public string AgvClass { get; set; } + [Required] + public double MaxLoadMass { get; set; } + [Required] + public string[] LocalizationTypes { get; set; } = []; + [Required] + public string[] NavigationTypes { get; set; } = []; +} diff --git a/RobotApp.VDA5050/Factsheet/WheelDefinitions.cs b/RobotApp.VDA5050/Factsheet/WheelDefinitions.cs new file mode 100644 index 0000000..1b86945 --- /dev/null +++ b/RobotApp.VDA5050/Factsheet/WheelDefinitions.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Factsheet; + +#nullable disable + +public enum WheelDefinitionsType +{ + DRIVE, + CASTER, + FIXED, + MECANUM, +} +public class WheelDefinitionsPosition +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + public double Theta { get; set; } +} + +public class WheelDefinitions +{ + [Required] + public string Type { get; set; } + [Required] + public bool IsActiveDriven { get; set; } + [Required] + public bool IsActiveSteered { get; set; } + [Required] + public WheelDefinitionsPosition Position { get; set; } + [Required] + public double Diameter { get; set; } + [Required] + public double Width { get; set; } + public double CenterDisplacement { get; set; } + public string Constraints { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/Battery.cs b/RobotApp.VDA5050/FactsheetExtend/Battery.cs new file mode 100644 index 0000000..b38594d --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/Battery.cs @@ -0,0 +1,9 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class Battery +{ + public uint Battery_low { get; set; } + public uint Battery_normal { get; set; } + public uint Battery_good { get; set; } + public uint Battery_full { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/BatteryThreshold.cs b/RobotApp.VDA5050/FactsheetExtend/BatteryThreshold.cs new file mode 100644 index 0000000..970ba3e --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/BatteryThreshold.cs @@ -0,0 +1,11 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public enum BatteryThreshold +{ + LOW, + NORMAL, + MIDDLE, + GOOD, + FULL, + NONE +} diff --git a/RobotApp.VDA5050/FactsheetExtend/CameraSafety.cs b/RobotApp.VDA5050/FactsheetExtend/CameraSafety.cs new file mode 100644 index 0000000..5a503b6 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/CameraSafety.cs @@ -0,0 +1,31 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class CameraSafety +{ + public double Pass_through_x_min { get; set; } + public double Pass_through_x_max { get; set; } + public double Pass_through_y_min { get; set; } + public double Pass_through_y_max { get; set; } + public double Pass_through_z_min { get; set; } + public double Pass_through_z_max { get; set; } + public uint Ground_seg_max_iterations { get; set; } + public double Ground_seg_distance_threshold { get; set; } + public double Warn_z1 { get; set; } + public double Protect_z1 { get; set; } + public double Warn_z2 { get; set; } + public double Protect_z2 { get; set; } + public double Warn_z3 { get; set; } + public double Protect_z3 { get; set; } + public double Warn_z4 { get; set; } + public double Protect_z4 { get; set; } + public double Warn_z5 { get; set; } + public double Protect_z5 { get; set; } + public double Warn_z6 { get; set; } + public double Protect_z6 { get; set; } + public uint Min_cluster_warn_size { get; set; } + public uint Min_cluster_protect_size { get; set; } + public uint Min_cluster_detect_size { get; set; } + public uint Min_consecutive_warn_count { get; set; } + public uint Min_consecutive_protect_count { get; set; } + public uint Min_consecutive_detect_count { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/ChargerParam.cs b/RobotApp.VDA5050/FactsheetExtend/ChargerParam.cs new file mode 100644 index 0000000..0f38c76 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/ChargerParam.cs @@ -0,0 +1,9 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +#nullable disable + +public class ChargerParam +{ + public string Charger_ip { get; set; } + public uint Charger_port { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/FactsheetExtendMsg.cs b/RobotApp.VDA5050/FactsheetExtend/FactsheetExtendMsg.cs new file mode 100644 index 0000000..c794d8d --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/FactsheetExtendMsg.cs @@ -0,0 +1,19 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +#nullable disable + +public class FactsheetExtendMsg +{ + public uint HeaderId { get; set; } + public DateTime Timestamp { get; set; } + public string Version { get; set; } + public string Manufacturer { get; set; } + public string SerialNumber { get; set; } + + public ServerParam Server_param { get; set; } + public RobotParam Robot_param { get; set; } + public Localization Localization { get; set; } + public Navigation Navigation { get; set; } + public Safety Safety { get; set; } + public ChargerParam Charger_param { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/ForkSafety.cs b/RobotApp.VDA5050/FactsheetExtend/ForkSafety.cs new file mode 100644 index 0000000..eaa0a99 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/ForkSafety.cs @@ -0,0 +1,9 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class ForkSafety +{ + public double Muted_field_size { get; set; } + public double Protected_field_size { get; set; } + public double Warning_field_size { get; set; } + public double Detect_field_size { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/Initpose.cs b/RobotApp.VDA5050/FactsheetExtend/Initpose.cs new file mode 100644 index 0000000..45917dd --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/Initpose.cs @@ -0,0 +1,9 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class Initpose +{ + public bool Use_manual_initpose { get; set; } + public double Initpose_x { get; set; } + public double Initpose_y { get; set; } + public double Initpose_yaw { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/LineSegment.cs b/RobotApp.VDA5050/FactsheetExtend/LineSegment.cs new file mode 100644 index 0000000..cbb7d06 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/LineSegment.cs @@ -0,0 +1,10 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class LineSegment +{ + public double Least_thresh { get; set; } + public double Min_line_length { get; set; } + public double Predict_distance { get; set; } + public uint Seed_line_points { get; set; } + public uint Min_line_points { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/Localization.cs b/RobotApp.VDA5050/FactsheetExtend/Localization.cs new file mode 100644 index 0000000..51fdfec --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/Localization.cs @@ -0,0 +1,14 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +#nullable disable + +public class Localization +{ + public uint Threshold_quality_loc { get; set; } + public bool Use_localization_marker { get; set; } + public bool Use_pallet_detection { get; set; } + public Initpose Initpose { get; set; } + public Xloc Xloc { get; set; } + public VlMarker Vl_marker { get; set; } + +} diff --git a/RobotApp.VDA5050/FactsheetExtend/Motor.cs b/RobotApp.VDA5050/FactsheetExtend/Motor.cs new file mode 100644 index 0000000..0302320 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/Motor.cs @@ -0,0 +1,9 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class Motor +{ + public double OdomEncSteeringAngleOffset { get; set; } + public double Steering_fix_wheel_distance_x { get; set; } + public double Steering_fix_wheel_distance_y { get; set; } + public double WheelAcceleration { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/Navigation.cs b/RobotApp.VDA5050/FactsheetExtend/Navigation.cs new file mode 100644 index 0000000..8ece0e9 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/Navigation.cs @@ -0,0 +1,11 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +#nullable disable +public class Navigation +{ + public bool Using_control_safety { get; set; } + public uint Control_rate { get; set; } + public Rotate Rotate { get; set; } + public PTA Pta { get; set; } + public PPA Ppa { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/PPA.cs b/RobotApp.VDA5050/FactsheetExtend/PPA.cs new file mode 100644 index 0000000..83f045f --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/PPA.cs @@ -0,0 +1,7 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class PPA +{ + public double Ppa_accuracy_goal { get; set; } + public double Ppa_distance_reduce { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/PTA.cs b/RobotApp.VDA5050/FactsheetExtend/PTA.cs new file mode 100644 index 0000000..b97e588 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/PTA.cs @@ -0,0 +1,15 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class PTA +{ + public double Pta_linear_vel_max { get; set; } + public double Pta_linear_vel_min { get; set; } + public double Pta_accuracy_goal { get; set; } + public double Pta_distance_reduce { get; set; } + public double Pta_acceleration { get; set; } + public double Pta_lm_front { get; set; } + public double Pta_lm_back { get; set; } + public double Pta_phi_max { get; set; } + public double Pta_amplitude_max { get; set; } + public uint Pta_w_option { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/RobotParam.cs b/RobotApp.VDA5050/FactsheetExtend/RobotParam.cs new file mode 100644 index 0000000..73c4d59 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/RobotParam.cs @@ -0,0 +1,12 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class RobotParam +{ + public bool Use_dynamic_parameter { get; set; } // (Default: True) Declare whether to use dynamic parameters or not + public string? Ethernet_name { get; set; } // The name of Ethernet port which connects to wifi client (e.g eno1, lo, enp3s0) + public double Speed_max_backward { get; set; } // (Default: True) Declare whether to use dynamic parameters or not + public uint Num_day_logger { get; set; } // The name of Ethernet port which connects to wifi client (e.g eno1, lo, enp3s0) + + public Motor Motor { get; set; } = new(); + public Battery Battery { get; set; } = new(); +} diff --git a/RobotApp.VDA5050/FactsheetExtend/Rotate.cs b/RobotApp.VDA5050/FactsheetExtend/Rotate.cs new file mode 100644 index 0000000..ea85c7f --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/Rotate.cs @@ -0,0 +1,9 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class Rotate +{ + public double Angular_vel_max { get; set; } + public double Angular_vel_min { get; set; } + public double Acceleration_rotate { get; set; } + public double Tolerances_rotate { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/Safety.cs b/RobotApp.VDA5050/FactsheetExtend/Safety.cs new file mode 100644 index 0000000..14d89b7 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/Safety.cs @@ -0,0 +1,8 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class Safety +{ + public bool Use_camera_safety { get; set; } + public CameraSafety Camera_safety { get; set; } = new(); + public ForkSafety Fork_safety { get; set; } = new(); +} diff --git a/RobotApp.VDA5050/FactsheetExtend/ServerParam.cs b/RobotApp.VDA5050/FactsheetExtend/ServerParam.cs new file mode 100644 index 0000000..8a45ccc --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/ServerParam.cs @@ -0,0 +1,14 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +#nullable disable + +public class ServerParam +{ + public string Server_ip { get; set; } + public string Server_port { get; set; } + public string Keepalive { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string Client_protocol { get; set; } + public string Client_id { get; set; } +} diff --git a/RobotApp.VDA5050/FactsheetExtend/VlMarker.cs b/RobotApp.VDA5050/FactsheetExtend/VlMarker.cs new file mode 100644 index 0000000..cbfd3c1 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/VlMarker.cs @@ -0,0 +1,21 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class VlMarker +{ + public bool Use_odometry { get; set; } + public uint V_angle { get; set; } + public double Length_v { get; set; } + public double Length_l { get; set; } + public double X_laser { get; set; } + public double Y_laser { get; set; } + public bool Flip_laser { get; set; } + public double Rotate_laser { get; set; } + public uint Frequence_control { get; set; } + public double Angle_min { get; set; } + public double Angle_max { get; set; } + public double Max_init_x { get; set; } + public double Max_init_y { get; set; } + public double Max_init_yaw { get; set; } + + public LineSegment Line_segment { get; set; } = new(); +} diff --git a/RobotApp.VDA5050/FactsheetExtend/Xloc.cs b/RobotApp.VDA5050/FactsheetExtend/Xloc.cs new file mode 100644 index 0000000..f87da47 --- /dev/null +++ b/RobotApp.VDA5050/FactsheetExtend/Xloc.cs @@ -0,0 +1,16 @@ +namespace RobotApp.VDA5050.FactsheetExtend; + +public class Xloc +{ + public double Front_vls_width { get; set; } + public double Front_vls_pose_x { get; set; } + public double Front_vls_pose_y { get; set; } + public double Front_vls_pose_yaw { get; set; } + public uint Front_vls_source_id { get; set; } + + public double Rear_vls_width { get; set; } + public double Rear_vls_pose_x { get; set; } + public double Rear_vls_pose_y { get; set; } + public double Rear_vls_pose_yaw { get; set; } + public uint Rear_vls_source_id { get; set; } +} diff --git a/RobotApp.VDA5050/InstantAction/ActionParameter.cs b/RobotApp.VDA5050/InstantAction/ActionParameter.cs new file mode 100644 index 0000000..b701b60 --- /dev/null +++ b/RobotApp.VDA5050/InstantAction/ActionParameter.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.InstantAction; + +#nullable disable +public class ActionParameter +{ + [Required] + public string Key { get; set; } = ""; + [Required] + public string Value { get; set; } = ""; +} diff --git a/RobotApp.VDA5050/InstantAction/Actions.cs b/RobotApp.VDA5050/InstantAction/Actions.cs new file mode 100644 index 0000000..55d6dd5 --- /dev/null +++ b/RobotApp.VDA5050/InstantAction/Actions.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.InstantAction; + +#nullable disable + +public enum BlockingType +{ + NONE, + SOFT, + HARD +} +public class Action +{ + [Required] + public string ActionType { get; set; } = ""; + [Required] + public string ActionId { get; set; } = ""; + public string ActionDescription { get; set; } = ""; + [Required] + public string BlockingType { get; set; } = ""; + public ActionParameter[] ActionParameters { get; set; } = []; +} diff --git a/RobotApp.VDA5050/InstantAction/InstantActionsMsg.cs b/RobotApp.VDA5050/InstantAction/InstantActionsMsg.cs new file mode 100644 index 0000000..17355e3 --- /dev/null +++ b/RobotApp.VDA5050/InstantAction/InstantActionsMsg.cs @@ -0,0 +1,13 @@ +namespace RobotApp.VDA5050.InstantAction; + +#nullable disable + +public class InstantActionsMsg +{ + public uint HeaderId { get; set; } + public string Timestamp { get; set; } = ""; + public string Version { get; set; } = ""; + public string Manufacturer { get; set; } = ""; + public string SerialNumber { get; set; } = ""; + public Action[] Actions { get; set; } = []; +} diff --git a/RobotApp.VDA5050/Order/Corridor.cs b/RobotApp.VDA5050/Order/Corridor.cs new file mode 100644 index 0000000..0adc0b9 --- /dev/null +++ b/RobotApp.VDA5050/Order/Corridor.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Order; + +public enum CorridorRefPoint +{ + KINEMATICCENTER, + CONTOUR +} + +public class Corridor +{ + [Required] + public double LeftWidth { get; set; } + [Required] + public double RightWidth { get; set; } + public string CorridorRefPoint { get; set; } = ""; +} diff --git a/RobotApp.VDA5050/Order/Edge.cs b/RobotApp.VDA5050/Order/Edge.cs new file mode 100644 index 0000000..ca9c5ee --- /dev/null +++ b/RobotApp.VDA5050/Order/Edge.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Order; + +#nullable disable + +public class Edge +{ + [Required] + public string EdgeId { get; set; } = ""; + [Required] + public int SequenceId { get; set; } + public string EdgeDescription { get; set; } = ""; + [Required] + public bool Released { get; set; } + [Required] + public string StartNodeId { get; set; } = ""; + [Required] + public string EndNodeId { get; set; } = ""; + public double MaxSpeed { get; set; } + public double MaxHeight { get; set; } + public double MinHeight { get; set; } + public double Orientation { get; set; } + public string OrientationType { get; set; } = ""; + public string Direction { get; set; } = ""; + public bool RotationAllowed { get; set; } + public double MaxRotationSpeed { get; set; } + public double Length { get; set; } + public Trajectory Trajectory { get; set; } + public Corridor Corridor { get; set; } = new(); + [Required] + public InstantAction.Action[] Actions { get; set; } = []; +} diff --git a/RobotApp.VDA5050/Order/EdgeLog.cs b/RobotApp.VDA5050/Order/EdgeLog.cs new file mode 100644 index 0000000..b0ce72e --- /dev/null +++ b/RobotApp.VDA5050/Order/EdgeLog.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Order; + +#nullable disable +public class EdgeLog +{ + + [Required] + public string EdgeId { get; set; } = ""; + public string EdgeDescription { get; set; } = ""; + [Required] + public string StartNodeId { get; set; } = ""; + [Required] + public string EndNodeId { get; set; } = ""; + public Trajectory Trajectory { get; set; } = new(); + public InstantAction.Action[] Actions { get; set; } = []; +} diff --git a/RobotApp.VDA5050/Order/Node.cs b/RobotApp.VDA5050/Order/Node.cs new file mode 100644 index 0000000..335c4bb --- /dev/null +++ b/RobotApp.VDA5050/Order/Node.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Order; + +#nullable disable + +public class Node +{ + [Required] + public string NodeId { get; set; } = ""; + [Required] + public int SequenceId { get; set; } + public string NodeDescription { get; set; } = ""; + [Required] + public bool Released { get; set; } + public NodePosition NodePosition { get; set; } + [Required] + public InstantAction.Action[] Actions { get; set; } = []; +} diff --git a/RobotApp.VDA5050/Order/NodeLog.cs b/RobotApp.VDA5050/Order/NodeLog.cs new file mode 100644 index 0000000..1fa5452 --- /dev/null +++ b/RobotApp.VDA5050/Order/NodeLog.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Order; + +#nullable disable + +public class NodeLog +{ + [Required] + public string NodeId { get; set; } = ""; + public string NodeDescription { get; set; } = ""; + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + [Required] + public double Theta { get; set; } + public InstantAction.Action[] Actions { get; set; } = []; +} diff --git a/RobotApp.VDA5050/Order/NodePosition.cs b/RobotApp.VDA5050/Order/NodePosition.cs new file mode 100644 index 0000000..5086851 --- /dev/null +++ b/RobotApp.VDA5050/Order/NodePosition.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Order; + +#nullable disable + +public class NodePosition +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + public double Theta { get; set; } + public double AllowedDeviationXY { get; set; } + public double AllowedDeviationTheta { get; set; } + [Required] + public string MapId { get; set; } = ""; + public string MapDescription { get; set; } = ""; +} diff --git a/RobotApp.VDA5050/Order/OrderLog.cs b/RobotApp.VDA5050/Order/OrderLog.cs new file mode 100644 index 0000000..5b047f0 --- /dev/null +++ b/RobotApp.VDA5050/Order/OrderLog.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Order; + +#nullable disable + +public class OrderLog +{ + [Required] + public string Timestamp { get; set; } = ""; + [Required] + public string SerialNumber { get; set; } = ""; + [Required] + public string OrderId { get; set; } = ""; + [Required] + public int OrderUpdateId { get; set; } + [Required] + public NodeLog[] Nodes { get; set; } = []; + [Required] + public EdgeLog[] Edges { get; set; } = []; +} diff --git a/RobotApp.VDA5050/Order/OrderMsg.cs b/RobotApp.VDA5050/Order/OrderMsg.cs new file mode 100644 index 0000000..b160e36 --- /dev/null +++ b/RobotApp.VDA5050/Order/OrderMsg.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Order; + +#nullable disable + +public class OrderMsg +{ + [Required] + public uint HeaderId { get; set; } + [Required] + public string Timestamp { get; set; } = ""; + [Required] + public string Version { get; set; } = ""; + [Required] + public string Manufacturer { get; set; } = ""; + [Required] + public string SerialNumber { get; set; } = ""; + [Required] + public string OrderId { get; set; } = ""; + [Required] + public int OrderUpdateId { get; set; } + public string ZoneSetId { get; set; } = ""; + [Required] + public Node[] Nodes { get; set; } = []; + [Required] + public Edge[] Edges { get; set; } = []; + +} diff --git a/RobotApp.VDA5050/Order/Trajectory.cs b/RobotApp.VDA5050/Order/Trajectory.cs new file mode 100644 index 0000000..a81d2da --- /dev/null +++ b/RobotApp.VDA5050/Order/Trajectory.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Order; + +#nullable disable + +public class ControlPoint +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + public double Weight { get; set; } +} +public class Trajectory +{ + [Required] + public int Degree { get; set; } + [Required] + public double[] KnotVector { get; set; } = []; + [Required] + public ControlPoint[] ControlPoints { get; set; } = []; +} diff --git a/RobotApp.VDA5050/RobotApp.VDA5050.csproj b/RobotApp.VDA5050/RobotApp.VDA5050.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/RobotApp.VDA5050/RobotApp.VDA5050.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/RobotApp.VDA5050/State/ActionState.cs b/RobotApp.VDA5050/State/ActionState.cs new file mode 100644 index 0000000..df853e7 --- /dev/null +++ b/RobotApp.VDA5050/State/ActionState.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.State; + +#nullable disable + +public enum ActionStatus +{ + WAITING, + INITIALIZING, + RUNNING, + PAUSED, + FINISHED, + FAILED, +} +public class ActionState +{ + public string ActionType { get; set; } + [Required] + public string ActionId { get; set; } + public string ActionDescription { get; set; } + [Required] + public string ActionStatus { get; set; } + public string ResultDescription { get; set; } +} diff --git a/RobotApp.VDA5050/State/BatteryState.cs b/RobotApp.VDA5050/State/BatteryState.cs new file mode 100644 index 0000000..fcfe479 --- /dev/null +++ b/RobotApp.VDA5050/State/BatteryState.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.State; + +public class BatteryState +{ + [Required] + public double BatteryCharge { get; set; } + public double BatteryVoltage { get; set; } + [Range(0, 100, ErrorMessage = "Value for BatteryHealth must be between 0 and 100.")] + public double BatteryHealth { get; set; } + [Required] + public bool Charging { get; set; } + public double Reach { get; set; } +} diff --git a/RobotApp.VDA5050/State/EdgeState.cs b/RobotApp.VDA5050/State/EdgeState.cs new file mode 100644 index 0000000..9d02a0c --- /dev/null +++ b/RobotApp.VDA5050/State/EdgeState.cs @@ -0,0 +1,19 @@ +using RobotApp.VDA5050.Order; +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.State; + +#nullable disable + +public class EdgeState +{ + + [Required] + public string EdgeId { get; set; } + [Required] + public int SequenceId { get; set; } + public string EdgeDescription { get; set; } + [Required] + public bool Released { get; set; } + public Trajectory Trajectory { get; set; } +} diff --git a/RobotApp.VDA5050/State/Error.cs b/RobotApp.VDA5050/State/Error.cs new file mode 100644 index 0000000..eabc52d --- /dev/null +++ b/RobotApp.VDA5050/State/Error.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.State; + +#nullable disable + +public enum ErrorLevel +{ + NONE, + WARNING, + FATAL +} +public class ErrorReferences +{ + [Required] + public string ReferenceKey { get; set; } + [Required] + public string ReferenceValue { get; set; } +} +public class Error +{ + [Required] + public string ErrorType { get; set; } + public ErrorReferences[] ErrorReferences { get; set; } + public string ErrorDescription { get; set; } + public string ErrorHint { get; set; } + [Required] + public string ErrorLevel { get; set; } +} diff --git a/RobotApp.VDA5050/State/Information.cs b/RobotApp.VDA5050/State/Information.cs new file mode 100644 index 0000000..287f695 --- /dev/null +++ b/RobotApp.VDA5050/State/Information.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.State; + +#nullable disable + +public enum InfoLevel +{ + INFO, + DEBUG +} +public class InfomationReferences +{ + [Required] + public string ReferenceKey { get; set; } + [Required] + public string ReferenceValue { get; set; } +} +public class Information +{ + [Required] + public string InfoType { get; set; } + public InfomationReferences[] InfoReferences { get; set; } + public string InfoDescription { get; set; } + [Required] + public string InfoLevel { get; set; } +} diff --git a/RobotApp.VDA5050/State/Load.cs b/RobotApp.VDA5050/State/Load.cs new file mode 100644 index 0000000..4f32e25 --- /dev/null +++ b/RobotApp.VDA5050/State/Load.cs @@ -0,0 +1,16 @@ +using RobotApp.VDA5050.Factsheet; + +namespace RobotApp.VDA5050.State; + +#nullable disable + +public class Load +{ + public string LoadId { get; set; } + public string LoadType { get; set; } + public string LoadPosition { get; set; } + public BoundingBoxReference BoundingBoxReference { get; set; } + public LoadDimensions LoadDimensions { get; set; } + public double Weight { get; set; } + +} diff --git a/RobotApp.VDA5050/State/Map.cs b/RobotApp.VDA5050/State/Map.cs new file mode 100644 index 0000000..1d9e41b --- /dev/null +++ b/RobotApp.VDA5050/State/Map.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.State; + +public enum MapStatus +{ + Enabled, + Disabled, +} + +public class Map +{ + [Required] + public string MapId { get; set; } = string.Empty; + [Required] + public string MapVersion { get; set; } = string.Empty; + public string MapDescription { get; set; } = string.Empty; + [Required] + public string MapStatus { get; set; } = string.Empty; +} diff --git a/RobotApp.VDA5050/State/NodeState.cs b/RobotApp.VDA5050/State/NodeState.cs new file mode 100644 index 0000000..7b884d6 --- /dev/null +++ b/RobotApp.VDA5050/State/NodeState.cs @@ -0,0 +1,30 @@ +using RobotApp.VDA5050.Order; +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.State; + +#nullable disable + +public class NodeState +{ + [Required] + public string NodeId { get; set; } + [Required] + public int SequenceId { get; set; } + public string NodeDescription { get; set; } + [Required] + public bool Released { get; set; } + public NodePosition NodePosition { get; set; } +} + +public class NodePosition +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + public double Theta { get; set; } + [Required] + public string MapId { get; set; } = ""; + +} \ No newline at end of file diff --git a/RobotApp.VDA5050/State/SafetyState.cs b/RobotApp.VDA5050/State/SafetyState.cs new file mode 100644 index 0000000..a49f0cc --- /dev/null +++ b/RobotApp.VDA5050/State/SafetyState.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.State; + +public enum EStop +{ + AUTOACK, + MANUAL, + REMOTE, + NONE, +} + +public class SafetyState +{ + [Required] + public string? EStop { get; set; } + + [Required] + public bool FieldViolation { get; set; } +} diff --git a/RobotApp.VDA5050/State/StateMsg.cs b/RobotApp.VDA5050/State/StateMsg.cs new file mode 100644 index 0000000..d06e68f --- /dev/null +++ b/RobotApp.VDA5050/State/StateMsg.cs @@ -0,0 +1,61 @@ +using RobotApp.VDA5050.Visualization; +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.State; + +#nullable disable + +public enum OperatingMode +{ + AUTOMATIC, + SEMIAUTOMATIC, + MANUAL, + SERVICE, + TEACHIN, +} +public class StateMsg +{ + [Required] + public uint HeaderId { get; set; } + [Required] + public string Timestamp { get; set; } + [Required] + public string Version { get; set; } + [Required] + public string Manufacturer { get; set; } + [Required] + public string SerialNumber { get; set; } + public Map[] Maps { get; set; } = []; + [Required] + public string OrderId { get; set; } + [Required] + public int OrderUpdateId { get; set; } + public string ZoneSetId { get; set; } + [Required] + public string LastNodeId { get; set; } + [Required] + public int LastNodeSequenceId { get; set; } + [Required] + public bool Driving { get; set; } + public bool Paused { get; set; } + public bool NewBaseRequest { get; set; } + public double DistanceSinceLastNode { get; set; } + [Required] + public string OperatingMode { get; set; } + [Required] + public NodeState[] NodeStates { get; set; } = []; + [Required] + public EdgeState[] EdgeStates { get; set; } = []; + public AgvPosition AgvPosition { get; set; } = new(); + public Velocity Velocity { get; set; } = new(); + public Load[] Loads { get; set; } = []; + [Required] + public ActionState[] ActionStates { get; set; } = []; + [Required] + public BatteryState BatteryState { get; set; } = new(); + [Required] + public Error[] Errors { get; set; } = []; + public Information[] Information { get; set; } = []; + [Required] + public SafetyState SafetyState { get; set; } = new(); +} diff --git a/RobotApp.VDA5050/Type/ActionType.cs b/RobotApp.VDA5050/Type/ActionType.cs new file mode 100644 index 0000000..58c473d --- /dev/null +++ b/RobotApp.VDA5050/Type/ActionType.cs @@ -0,0 +1,35 @@ +namespace RobotApp.VDA5050.Type; + +public enum ActionType +{ + startPause, // No actionParameters + stopPause, // No actionParameters + startCharging, // ActionParameters {CHARGER_IP, CHARGER_PORT, X, Y, THETA} + stopCharging, // ActionParameters {X, Y, THETA} + initPosition, // ActionParameters {X, Y, THETA, MAP_ID, LAST_NODE_ID} + stateRequest, // No actionParameters + logReport, // No actionParameters + pick, // No actionParameters + drop, // No actionParameters + detectObject, // No actionParameters + finePositioning, // ActionParameters {X, Y, THETA, MAP_ID, LAST_NODE_ID} + waitForTrigger, // No actionParameters + cancelOrder, // No actionParameters + factsheetRequest, // No actionParameters + + setDynparam, // ActionParameters {PARAM_NAME, PARAM_TYPE, PARAM_VALUE} + saveDynparamRuntime, // No actionParameters + loadDynparamRuntime, // No actionParameters + loadDynparamDefault, // No actionParameters + getDynparamRuntime, // No actionParameters + + controlLight, // ActionParameters {LIGHT_TYPE, CONTROL_TYPE} + controlFan, // ActionParameters {CONTROL_TYPE} + controlSpeaker, // ActionParameters {SONG_NUMBER, CONTROL_TYPE} + controlSafetyField, // ActionParameters {FIELD_TYPE, CONTROL_TYPE} + startInPallet, // ActionParameters {X, Y, THETA} + startOutPallet, // ActionParameters {X, Y, THETA} + + rotate, // ActionParameters {THETA} + setMap, // ActionParameter {MAP_ID} +} diff --git a/RobotApp.VDA5050/VDA5050Helper.cs b/RobotApp.VDA5050/VDA5050Helper.cs new file mode 100644 index 0000000..6fc8be0 --- /dev/null +++ b/RobotApp.VDA5050/VDA5050Helper.cs @@ -0,0 +1,29 @@ +using RobotApp.VDA5050.State; +using System.Text; + +namespace RobotApp.VDA5050; + +public class VDA5050Helper +{ + public static string ConvertErrorDetail(IEnumerable errors) + { + string errorsType = ""; + foreach (var error in errors) + { + if (error == errors.Last()) errorsType += $"{error.ErrorType}"; + else errorsType += $"{error.ErrorType}, "; + } + StringBuilder errorStr = new($"Robot có lỗi: [{errorsType}]\n"); + foreach (var error in errors) + { + string errorDes = $"- {error.ErrorType}: {error.ErrorDescription}\n"; + errorStr.Append(errorDes.PadLeft(errorDes.Length + 3)); + foreach (var refer in error.ErrorReferences) + { + string errorRefer = $"+ {refer.ReferenceKey}: {refer.ReferenceValue}\n"; + errorStr.Append(errorRefer.PadLeft(errorRefer.Length + 9)); + } + } + return !errors.Any() ? "" : errorStr.ToString(); + } +} diff --git a/RobotApp.VDA5050/VDA5050Setting.cs b/RobotApp.VDA5050/VDA5050Setting.cs new file mode 100644 index 0000000..58f59b0 --- /dev/null +++ b/RobotApp.VDA5050/VDA5050Setting.cs @@ -0,0 +1,19 @@ +namespace RobotApp.VDA5050; + +#nullable disable + +public class VDA5050Setting +{ + public bool ServerEnable { get; set; } + public string HostServer { get; set; } + public int Port { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + public string Manufacturer { get; set; } + public string Version { get; set; } + public int Repeat { get; set; } + public int ConnectionTimeoutSeconds { get; set; } + public int ConnectionBacklog { set; get; } + public int KeepAliveInterval { get; set; } + public int CheckingRobotMsgTimout { get; set; } +} diff --git a/RobotApp.VDA5050/VDA5050Topic.cs b/RobotApp.VDA5050/VDA5050Topic.cs new file mode 100644 index 0000000..af26ef4 --- /dev/null +++ b/RobotApp.VDA5050/VDA5050Topic.cs @@ -0,0 +1,12 @@ +namespace RobotApp.VDA5050; + +public static class VDA5050Topic +{ + public const string Connection = "connection"; + public const string Order = "order"; + public const string InstantActions = "instantActions"; + public const string State = "state"; + public const string Visualization = "visualization"; + public const string Factsheet = "factsheet"; + public const string FactsheetExtend = "factsheetExtend"; // custom by TungNV +} diff --git a/RobotApp.VDA5050/Visualization/AgvPosition.cs b/RobotApp.VDA5050/Visualization/AgvPosition.cs new file mode 100644 index 0000000..a19e59e --- /dev/null +++ b/RobotApp.VDA5050/Visualization/AgvPosition.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotApp.VDA5050.Visualization; + +#nullable disable + +public class AgvPosition +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + [Required] + public string MapId { get; set; } + [Required] + public double Theta { get; set; } + [Required] + public bool PositionInitialized { get; set; } + public double LocalizationScore { get; set; } + public double DeviationRange { get; set; } +} diff --git a/RobotApp.VDA5050/Visualization/Velocity.cs b/RobotApp.VDA5050/Visualization/Velocity.cs new file mode 100644 index 0000000..b4ae591 --- /dev/null +++ b/RobotApp.VDA5050/Visualization/Velocity.cs @@ -0,0 +1,8 @@ +namespace RobotApp.VDA5050.Visualization; + +public class Velocity +{ + public double Vx { get; set; } + public double Vy { get; set; } + public double Omega { get; set; } +} diff --git a/RobotApp.VDA5050/Visualization/Visualizationmsg.cs b/RobotApp.VDA5050/Visualization/Visualizationmsg.cs new file mode 100644 index 0000000..3bac23f --- /dev/null +++ b/RobotApp.VDA5050/Visualization/Visualizationmsg.cs @@ -0,0 +1,16 @@ +namespace RobotApp.VDA5050.Visualization; + +#nullable disable + +public class VisualizationMsg +{ + public uint HeaderId { get; set; } + public string Timestamp { get; set; } + public string Version { get; set; } + public string Manufacturer { get; set; } + public string SerialNumber { get; set; } + public string MapId { get; set; } + public string MapDescription { get; set; } + public AgvPosition AgvPosition { get; set; } = new(); + public Velocity Velocity { get; set; } = new(); +} diff --git a/RobotApp.sln b/RobotApp.sln new file mode 100644 index 0000000..0a573f0 --- /dev/null +++ b/RobotApp.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35707.178 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp.Client", "RobotApp.Client\RobotApp.Client.csproj", "{8A1A11A9-3DA4-48B7-8FAA-98DBAFDE1701}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp.VDA5050", "RobotApp.VDA5050\RobotApp.VDA5050.csproj", "{617FD155-904A-44E6-AD1A-6BC878421F9F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}.Release|Any CPU.Build.0 = Release|Any CPU + {8A1A11A9-3DA4-48B7-8FAA-98DBAFDE1701}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A1A11A9-3DA4-48B7-8FAA-98DBAFDE1701}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A1A11A9-3DA4-48B7-8FAA-98DBAFDE1701}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A1A11A9-3DA4-48B7-8FAA-98DBAFDE1701}.Release|Any CPU.Build.0 = Release|Any CPU + {617FD155-904A-44E6-AD1A-6BC878421F9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {617FD155-904A-44E6-AD1A-6BC878421F9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {617FD155-904A-44E6-AD1A-6BC878421F9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {617FD155-904A-44E6-AD1A-6BC878421F9F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/RobotApp/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs b/RobotApp/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..3dbdb5b --- /dev/null +++ b/RobotApp/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -0,0 +1,113 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using RobotApp.Components.Account.Pages; +using RobotApp.Components.Account.Pages.Manage; +using RobotApp.Data; + +namespace Microsoft.AspNetCore.Routing +{ + internal static class IdentityComponentsEndpointRouteBuilderExtensions + { + // These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project. + public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + var accountGroup = endpoints.MapGroup("/Account"); + + accountGroup.MapPost("/PerformExternalLogin", ( + HttpContext context, + [FromServices] SignInManager signInManager, + [FromForm] string provider, + [FromForm] string returnUrl) => + { + IEnumerable> query = [ + new("ReturnUrl", returnUrl), + new("Action", ExternalLogin.LoginCallbackAction)]; + + var redirectUrl = UriHelper.BuildRelative( + context.Request.PathBase, + "/Account/ExternalLogin", + QueryString.Create(query)); + + var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return TypedResults.Challenge(properties, [provider]); + }); + + accountGroup.MapPost("/Logout", async ( + ClaimsPrincipal user, + [FromServices] SignInManager signInManager, + [FromForm] string returnUrl) => + { + await signInManager.SignOutAsync(); + return TypedResults.LocalRedirect($"~/{returnUrl}"); + }); + + var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization(); + + manageGroup.MapPost("/LinkExternalLogin", async ( + HttpContext context, + [FromServices] SignInManager signInManager, + [FromForm] string provider) => + { + // Clear the existing external cookie to ensure a clean login process + await context.SignOutAsync(IdentityConstants.ExternalScheme); + + var redirectUrl = UriHelper.BuildRelative( + context.Request.PathBase, + "/Account/Manage/ExternalLogins", + QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction)); + + var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User)); + return TypedResults.Challenge(properties, [provider]); + }); + + var loggerFactory = endpoints.ServiceProvider.GetRequiredService(); + var downloadLogger = loggerFactory.CreateLogger("DownloadPersonalData"); + + manageGroup.MapPost("/DownloadPersonalData", async ( + HttpContext context, + [FromServices] UserManager userManager, + [FromServices] AuthenticationStateProvider authenticationStateProvider) => + { + var user = await userManager.GetUserAsync(context.User); + if (user is null) + { + return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); + } + + var userId = await userManager.GetUserIdAsync(user); + downloadLogger.LogInformation("User with ID '{UserId}' asked for their personal data.", userId); + + // Only include personal data for download + var personalData = new Dictionary(); + var personalDataProps = typeof(ApplicationUser).GetProperties().Where( + prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); + foreach (var p in personalDataProps) + { + personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); + } + + var logins = await userManager.GetLoginsAsync(user); + foreach (var l in logins) + { + personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); + } + + personalData.Add("Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!); + var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData); + + context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json"); + return TypedResults.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json"); + }); + + return accountGroup; + } + } +} diff --git a/RobotApp/Components/Account/IdentityNoOpEmailSender.cs b/RobotApp/Components/Account/IdentityNoOpEmailSender.cs new file mode 100644 index 0000000..7253841 --- /dev/null +++ b/RobotApp/Components/Account/IdentityNoOpEmailSender.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using RobotApp.Data; + +namespace RobotApp.Components.Account +{ + // Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation. + internal sealed class IdentityNoOpEmailSender : IEmailSender + { + private readonly IEmailSender emailSender = new NoOpEmailSender(); + + public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) => + emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here."); + + public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) => + emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here."); + + public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) => + emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); + } +} diff --git a/RobotApp/Components/Account/IdentityRedirectManager.cs b/RobotApp/Components/Account/IdentityRedirectManager.cs new file mode 100644 index 0000000..61cdac5 --- /dev/null +++ b/RobotApp/Components/Account/IdentityRedirectManager.cs @@ -0,0 +1,59 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; + +namespace RobotApp.Components.Account +{ + internal sealed class IdentityRedirectManager(NavigationManager navigationManager) + { + public const string StatusCookieName = "Identity.StatusMessage"; + + private static readonly CookieBuilder StatusCookieBuilder = new() + { + SameSite = SameSiteMode.Strict, + HttpOnly = true, + IsEssential = true, + MaxAge = TimeSpan.FromSeconds(5), + }; + + [DoesNotReturn] + public void RedirectTo(string? uri) + { + uri ??= ""; + + // Prevent open redirects. + if (!Uri.IsWellFormedUriString(uri, UriKind.Relative)) + { + uri = navigationManager.ToBaseRelativePath(uri); + } + + // During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect. + // So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown. + navigationManager.NavigateTo(uri); + throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering."); + } + + [DoesNotReturn] + public void RedirectTo(string uri, Dictionary queryParameters) + { + var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path); + var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters); + RedirectTo(newUri); + } + + [DoesNotReturn] + public void RedirectToWithStatus(string uri, string message, HttpContext context) + { + context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context)); + RedirectTo(uri); + } + + private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path); + + [DoesNotReturn] + public void RedirectToCurrentPage() => RedirectTo(CurrentPath); + + [DoesNotReturn] + public void RedirectToCurrentPageWithStatus(string message, HttpContext context) + => RedirectToWithStatus(CurrentPath, message, context); + } +} diff --git a/RobotApp/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs b/RobotApp/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs new file mode 100644 index 0000000..8b446f4 --- /dev/null +++ b/RobotApp/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs @@ -0,0 +1,48 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using RobotApp.Data; + +namespace RobotApp.Components.Account +{ + // This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user + // every 30 minutes an interactive circuit is connected. + internal sealed class IdentityRevalidatingAuthenticationStateProvider( + ILoggerFactory loggerFactory, + IServiceScopeFactory scopeFactory, + IOptions options) + : RevalidatingServerAuthenticationStateProvider(loggerFactory) + { + protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); + + protected override async Task ValidateAuthenticationStateAsync( + AuthenticationState authenticationState, CancellationToken cancellationToken) + { + // Get the user manager from a new scope to ensure it fetches fresh data + await using var scope = scopeFactory.CreateAsyncScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + return await ValidateSecurityStampAsync(userManager, authenticationState.User); + } + + private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal) + { + var user = await userManager.GetUserAsync(principal); + if (user is null) + { + return false; + } + else if (!userManager.SupportsUserSecurityStamp) + { + return true; + } + else + { + var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType); + var userStamp = await userManager.GetSecurityStampAsync(user); + return principalStamp == userStamp; + } + } + } +} diff --git a/RobotApp/Components/Account/IdentityUserAccessor.cs b/RobotApp/Components/Account/IdentityUserAccessor.cs new file mode 100644 index 0000000..2f9da85 --- /dev/null +++ b/RobotApp/Components/Account/IdentityUserAccessor.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using RobotApp.Data; + +namespace RobotApp.Components.Account +{ + internal sealed class IdentityUserAccessor(UserManager userManager, IdentityRedirectManager redirectManager) + { + public async Task GetRequiredUserAsync(HttpContext context) + { + var user = await userManager.GetUserAsync(context.User); + + if (user is null) + { + redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context); + } + + return user; + } + } +} diff --git a/RobotApp/Components/Account/Pages/AccessDenied.razor b/RobotApp/Components/Account/Pages/AccessDenied.razor new file mode 100644 index 0000000..905dec3 --- /dev/null +++ b/RobotApp/Components/Account/Pages/AccessDenied.razor @@ -0,0 +1,8 @@ +@page "/Account/AccessDenied" + +Access denied + +
+

Access denied

+

You do not have access to this resource.

+
diff --git a/RobotApp/Components/Account/Pages/ConfirmEmail.razor b/RobotApp/Components/Account/Pages/ConfirmEmail.razor new file mode 100644 index 0000000..3735133 --- /dev/null +++ b/RobotApp/Components/Account/Pages/ConfirmEmail.razor @@ -0,0 +1,48 @@ +@page "/Account/ConfirmEmail" + +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using RobotApp.Data + +@inject UserManager UserManager +@inject IdentityRedirectManager RedirectManager + +Confirm email + +

Confirm email

+ + +@code { + private string? statusMessage; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromQuery] + private string? UserId { get; set; } + + [SupplyParameterFromQuery] + private string? Code { get; set; } + + protected override async Task OnInitializedAsync() + { + if (UserId is null || Code is null) + { + RedirectManager.RedirectTo(""); + } + + var user = await UserManager.FindByIdAsync(UserId); + if (user is null) + { + HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; + statusMessage = $"Error loading user with ID {UserId}"; + } + else + { + var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); + var result = await UserManager.ConfirmEmailAsync(user, code); + statusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email."; + } + } +} diff --git a/RobotApp/Components/Account/Pages/ConfirmEmailChange.razor b/RobotApp/Components/Account/Pages/ConfirmEmailChange.razor new file mode 100644 index 0000000..f23f5f7 --- /dev/null +++ b/RobotApp/Components/Account/Pages/ConfirmEmailChange.razor @@ -0,0 +1,68 @@ +@page "/Account/ConfirmEmailChange" + +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using RobotApp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityRedirectManager RedirectManager + +Confirm email change + +

Confirm email change

+ + + +@code { + private string? message; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromQuery] + private string? UserId { get; set; } + + [SupplyParameterFromQuery] + private string? Email { get; set; } + + [SupplyParameterFromQuery] + private string? Code { get; set; } + + protected override async Task OnInitializedAsync() + { + if (UserId is null || Email is null || Code is null) + { + RedirectManager.RedirectToWithStatus( + "Account/Login", "Error: Invalid email change confirmation link.", HttpContext); + } + + var user = await UserManager.FindByIdAsync(UserId); + if (user is null) + { + message = "Unable to find user with Id '{userId}'"; + return; + } + + var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); + var result = await UserManager.ChangeEmailAsync(user, Email, code); + if (!result.Succeeded) + { + message = "Error changing email."; + return; + } + + // In our UI email and user name are one and the same, so when we update the email + // we need to update the user name. + var setUserNameResult = await UserManager.SetUserNameAsync(user, Email); + if (!setUserNameResult.Succeeded) + { + message = "Error changing user name."; + return; + } + + await SignInManager.RefreshSignInAsync(user); + message = "Thank you for confirming your email change."; + } +} diff --git a/RobotApp/Components/Account/Pages/ExternalLogin.razor b/RobotApp/Components/Account/Pages/ExternalLogin.razor new file mode 100644 index 0000000..11a4d15 --- /dev/null +++ b/RobotApp/Components/Account/Pages/ExternalLogin.razor @@ -0,0 +1,205 @@ +@page "/Account/ExternalLogin" + +@using System.ComponentModel.DataAnnotations +@using System.Security.Claims +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using RobotApp.Data + +@inject SignInManager SignInManager +@inject UserManager UserManager +@inject IUserStore UserStore +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Register + + +

Register

+

Associate your @ProviderDisplayName account.

+
+ +
+ You've successfully authenticated with @ProviderDisplayName. + Please enter an email address for this site below and click the Register button to finish + logging in. +
+ +
+
+ + + +
+ + + +
+ +
+
+
+ +@code { + public const string LoginCallbackAction = "LoginCallback"; + + private string? message; + private ExternalLoginInfo? externalLoginInfo; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? RemoteError { get; set; } + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + [SupplyParameterFromQuery] + private string? Action { get; set; } + + private string? ProviderDisplayName => externalLoginInfo?.ProviderDisplayName; + + protected override async Task OnInitializedAsync() + { + if (RemoteError is not null) + { + RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext); + } + + var info = await SignInManager.GetExternalLoginInfoAsync(); + if (info is null) + { + RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext); + } + + externalLoginInfo = info; + + if (HttpMethods.IsGet(HttpContext.Request.Method)) + { + if (Action == LoginCallbackAction) + { + await OnLoginCallbackAsync(); + return; + } + + // We should only reach this page via the login callback, so redirect back to + // the login page if we get here some other way. + RedirectManager.RedirectTo("Account/Login"); + } + } + + private async Task OnLoginCallbackAsync() + { + if (externalLoginInfo is null) + { + RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext); + } + + // Sign in the user with this external login provider if the user already has a login. + var result = await SignInManager.ExternalLoginSignInAsync( + externalLoginInfo.LoginProvider, + externalLoginInfo.ProviderKey, + isPersistent: false, + bypassTwoFactor: true); + + if (result.Succeeded) + { + Logger.LogInformation( + "{Name} logged in with {LoginProvider} provider.", + externalLoginInfo.Principal.Identity?.Name, + externalLoginInfo.LoginProvider); + RedirectManager.RedirectTo(ReturnUrl); + } + else if (result.IsLockedOut) + { + RedirectManager.RedirectTo("Account/Lockout"); + } + + // If the user does not have an account, then ask the user to create an account. + if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) + { + Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? ""; + } + } + + private async Task OnValidSubmitAsync() + { + if (externalLoginInfo is null) + { + RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information during confirmation.", HttpContext); + } + + var emailStore = GetEmailStore(); + var user = CreateUser(); + + await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); + await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); + + var result = await UserManager.CreateAsync(user); + if (result.Succeeded) + { + result = await UserManager.AddLoginAsync(user, externalLoginInfo); + if (result.Succeeded) + { + Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider); + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code }); + await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + + // If account confirmation is required, we need to show the link if we don't have a real email sender + if (UserManager.Options.SignIn.RequireConfirmedAccount) + { + RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email }); + } + + await SignInManager.SignInAsync(user, isPersistent: false, externalLoginInfo.LoginProvider); + RedirectManager.RedirectTo(ReturnUrl); + } + } + + message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}"; + } + + private ApplicationUser CreateUser() + { + try + { + return Activator.CreateInstance(); + } + catch + { + throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " + + $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor"); + } + } + + private IUserEmailStore GetEmailStore() + { + if (!UserManager.SupportsUserEmail) + { + throw new NotSupportedException("The default UI requires a user store with email support."); + } + return (IUserEmailStore)UserStore; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + } +} diff --git a/RobotApp/Components/Account/Pages/ForgotPassword.razor b/RobotApp/Components/Account/Pages/ForgotPassword.razor new file mode 100644 index 0000000..0629665 --- /dev/null +++ b/RobotApp/Components/Account/Pages/ForgotPassword.razor @@ -0,0 +1,68 @@ +@page "/Account/ForgotPassword" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using RobotApp.Data + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Forgot your password? + +

Forgot your password?

+

Enter your email.

+
+
+
+ + + + +
+ + + +
+ +
+
+
+ +@code { + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + private async Task OnValidSubmitAsync() + { + var user = await UserManager.FindByEmailAsync(Input.Email); + if (user is null || !(await UserManager.IsEmailConfirmedAsync(user))) + { + // Don't reveal that the user does not exist or is not confirmed + RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); + } + + // For more information on how to enable account confirmation and password reset please + // visit https://go.microsoft.com/fwlink/?LinkID=532713 + var code = await UserManager.GeneratePasswordResetTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri, + new Dictionary { ["code"] = code }); + + await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + + RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + } +} diff --git a/RobotApp/Components/Account/Pages/ForgotPasswordConfirmation.razor b/RobotApp/Components/Account/Pages/ForgotPasswordConfirmation.razor new file mode 100644 index 0000000..a771a3a --- /dev/null +++ b/RobotApp/Components/Account/Pages/ForgotPasswordConfirmation.razor @@ -0,0 +1,8 @@ +@page "/Account/ForgotPasswordConfirmation" + +Forgot password confirmation + +

Forgot password confirmation

+

+ Please check your email to reset your password. +

diff --git a/RobotApp/Components/Account/Pages/InvalidPasswordReset.razor b/RobotApp/Components/Account/Pages/InvalidPasswordReset.razor new file mode 100644 index 0000000..561b651 --- /dev/null +++ b/RobotApp/Components/Account/Pages/InvalidPasswordReset.razor @@ -0,0 +1,8 @@ +@page "/Account/InvalidPasswordReset" + +Invalid password reset + +

Invalid password reset

+

+ The password reset link is invalid. +

diff --git a/RobotApp/Components/Account/Pages/InvalidUser.razor b/RobotApp/Components/Account/Pages/InvalidUser.razor new file mode 100644 index 0000000..e61fe5d --- /dev/null +++ b/RobotApp/Components/Account/Pages/InvalidUser.razor @@ -0,0 +1,7 @@ +@page "/Account/InvalidUser" + +Invalid user + +

Invalid user

+ + diff --git a/RobotApp/Components/Account/Pages/Lockout.razor b/RobotApp/Components/Account/Pages/Lockout.razor new file mode 100644 index 0000000..017e31d --- /dev/null +++ b/RobotApp/Components/Account/Pages/Lockout.razor @@ -0,0 +1,8 @@ +@page "/Account/Lockout" + +Locked out + +
+

Locked out

+ +
diff --git a/RobotApp/Components/Account/Pages/Login.razor b/RobotApp/Components/Account/Pages/Login.razor new file mode 100644 index 0000000..547f1ee --- /dev/null +++ b/RobotApp/Components/Account/Pages/Login.razor @@ -0,0 +1,128 @@ +@page "/Account/Login" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject SignInManager SignInManager +@inject ILogger Logger +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Log in + +

Log in

+
+
+
+ + + +

Use a local account to log in.

+
+ +
+ + + +
+
+ + + +
+
+ +
+
+ +
+ +
+
+
+
+
+

Use another service to log in.

+
+ +
+
+
+ +@code { + private string? errorMessage; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + if (HttpMethods.IsGet(HttpContext.Request.Method)) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + } + } + + public async Task LoginUser() + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); + if (result.Succeeded) + { + Logger.LogInformation("User logged in."); + RedirectManager.RedirectTo(ReturnUrl); + } + else if (result.RequiresTwoFactor) + { + RedirectManager.RedirectTo( + "Account/LoginWith2fa", + new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe }); + } + else if (result.IsLockedOut) + { + Logger.LogWarning("User account locked out."); + RedirectManager.RedirectTo("Account/Lockout"); + } + else + { + errorMessage = "Error: Invalid login attempt."; + } + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } = ""; + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } +} diff --git a/RobotApp/Components/Account/Pages/LoginWith2fa.razor b/RobotApp/Components/Account/Pages/LoginWith2fa.razor new file mode 100644 index 0000000..d7d7750 --- /dev/null +++ b/RobotApp/Components/Account/Pages/LoginWith2fa.razor @@ -0,0 +1,101 @@ +@page "/Account/LoginWith2fa" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject SignInManager SignInManager +@inject UserManager UserManager +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Two-factor authentication + +

Two-factor authentication

+
+ +

Your login is protected with an authenticator app. Enter your authenticator code below.

+
+
+ + + + + +
+ + + +
+
+ +
+
+ +
+
+
+
+

+ Don't have access to your authenticator device? You can + log in with a recovery code. +

+ +@code { + private string? message; + private ApplicationUser user = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + [SupplyParameterFromQuery] + private bool RememberMe { get; set; } + + protected override async Task OnInitializedAsync() + { + // Ensure the user has gone through the username & password screen first + user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ?? + throw new InvalidOperationException("Unable to load two-factor authentication user."); + } + + private async Task OnValidSubmitAsync() + { + var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty); + var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine); + var userId = await UserManager.GetUserIdAsync(user); + + if (result.Succeeded) + { + Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId); + RedirectManager.RedirectTo(ReturnUrl); + } + else if (result.IsLockedOut) + { + Logger.LogWarning("User with ID '{UserId}' account locked out.", userId); + RedirectManager.RedirectTo("Account/Lockout"); + } + else + { + Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId); + message = "Error: Invalid authenticator code."; + } + } + + private sealed class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Authenticator code")] + public string? TwoFactorCode { get; set; } + + [Display(Name = "Remember this machine")] + public bool RememberMachine { get; set; } + } +} diff --git a/RobotApp/Components/Account/Pages/LoginWithRecoveryCode.razor b/RobotApp/Components/Account/Pages/LoginWithRecoveryCode.razor new file mode 100644 index 0000000..5719e8a --- /dev/null +++ b/RobotApp/Components/Account/Pages/LoginWithRecoveryCode.razor @@ -0,0 +1,85 @@ +@page "/Account/LoginWithRecoveryCode" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject SignInManager SignInManager +@inject UserManager UserManager +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Recovery code verification + +

Recovery code verification

+
+ +

+ You have requested to log in with a recovery code. This login will not be remembered until you provide + an authenticator app code at log in or disable 2FA and log in again. +

+
+
+ + + +
+ + + +
+ +
+
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + // Ensure the user has gone through the username & password screen first + user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ?? + throw new InvalidOperationException("Unable to load two-factor authentication user."); + } + + private async Task OnValidSubmitAsync() + { + var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); + + var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + + var userId = await UserManager.GetUserIdAsync(user); + + if (result.Succeeded) + { + Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId); + RedirectManager.RedirectTo(ReturnUrl); + } + else if (result.IsLockedOut) + { + Logger.LogWarning("User account locked out."); + RedirectManager.RedirectTo("Account/Lockout"); + } + else + { + Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId); + message = "Error: Invalid recovery code entered."; + } + } + + private sealed class InputModel + { + [Required] + [DataType(DataType.Text)] + [Display(Name = "Recovery Code")] + public string RecoveryCode { get; set; } = ""; + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/ChangePassword.razor b/RobotApp/Components/Account/Pages/Manage/ChangePassword.razor new file mode 100644 index 0000000..096cf8f --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/ChangePassword.razor @@ -0,0 +1,96 @@ +@page "/Account/Manage/ChangePassword" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Change password + +

Change password

+ +
+
+ + + +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + private bool hasPassword; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + hasPassword = await UserManager.HasPasswordAsync(user); + if (!hasPassword) + { + RedirectManager.RedirectTo("Account/Manage/SetPassword"); + } + } + + private async Task OnValidSubmitAsync() + { + var changePasswordResult = await UserManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); + if (!changePasswordResult.Succeeded) + { + message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}"; + return; + } + + await SignInManager.RefreshSignInAsync(user); + Logger.LogInformation("User changed their password successfully."); + + RedirectManager.RedirectToCurrentPageWithStatus("Your password has been changed", HttpContext); + } + + private sealed class InputModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } = ""; + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } = ""; + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = ""; + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/DeletePersonalData.razor b/RobotApp/Components/Account/Pages/Manage/DeletePersonalData.razor new file mode 100644 index 0000000..751c6eb --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/DeletePersonalData.razor @@ -0,0 +1,86 @@ +@page "/Account/Manage/DeletePersonalData" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Delete Personal Data + + + +

Delete Personal Data

+ + + +
+ + + + @if (requirePassword) + { +
+ + + +
+ } + +
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + private bool requirePassword; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + Input ??= new(); + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + requirePassword = await UserManager.HasPasswordAsync(user); + } + + private async Task OnValidSubmitAsync() + { + if (requirePassword && !await UserManager.CheckPasswordAsync(user, Input.Password)) + { + message = "Error: Incorrect password."; + return; + } + + var result = await UserManager.DeleteAsync(user); + if (!result.Succeeded) + { + throw new InvalidOperationException("Unexpected error occurred deleting user."); + } + + await SignInManager.SignOutAsync(); + + var userId = await UserManager.GetUserIdAsync(user); + Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); + + RedirectManager.RedirectToCurrentPage(); + } + + private sealed class InputModel + { + [DataType(DataType.Password)] + public string Password { get; set; } = ""; + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/Disable2fa.razor b/RobotApp/Components/Account/Pages/Manage/Disable2fa.razor new file mode 100644 index 0000000..3ba6dfa --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/Disable2fa.razor @@ -0,0 +1,64 @@ +@page "/Account/Manage/Disable2fa" + +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject UserManager UserManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Disable two-factor authentication (2FA) + + +

Disable two-factor authentication (2FA)

+ + + +
+
+ + + +
+ +@code { + private ApplicationUser user = default!; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + + if (HttpMethods.IsGet(HttpContext.Request.Method) && !await UserManager.GetTwoFactorEnabledAsync(user)) + { + throw new InvalidOperationException("Cannot disable 2FA for user as it's not currently enabled."); + } + } + + private async Task OnSubmitAsync() + { + var disable2faResult = await UserManager.SetTwoFactorEnabledAsync(user, false); + if (!disable2faResult.Succeeded) + { + throw new InvalidOperationException("Unexpected error occurred disabling 2FA."); + } + + var userId = await UserManager.GetUserIdAsync(user); + Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userId); + RedirectManager.RedirectToWithStatus( + "Account/Manage/TwoFactorAuthentication", + "2fa has been disabled. You can reenable 2fa when you setup an authenticator app", + HttpContext); + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/Email.razor b/RobotApp/Components/Account/Pages/Manage/Email.razor new file mode 100644 index 0000000..3696e62 --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/Email.razor @@ -0,0 +1,123 @@ +@page "/Account/Manage/Email" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using RobotApp.Data + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject IdentityUserAccessor UserAccessor +@inject NavigationManager NavigationManager + +Manage email + +

Manage email

+ + +
+
+
+ + + + + + @if (isEmailConfirmed) + { +
+ +
+ +
+ +
+ } + else + { +
+ + + +
+ } +
+ + + +
+ +
+
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + private string? email; + private bool isEmailConfirmed; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm(FormName = "change-email")] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + email = await UserManager.GetEmailAsync(user); + isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(user); + + Input.NewEmail ??= email; + } + + private async Task OnValidSubmitAsync() + { + if (Input.NewEmail is null || Input.NewEmail == email) + { + message = "Your email is unchanged."; + return; + } + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code }); + + await EmailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); + + message = "Confirmation link to change email sent. Please check your email."; + } + + private async Task OnSendEmailVerificationAsync() + { + if (email is null) + { + return; + } + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code }); + + await EmailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(callbackUrl)); + + message = "Verification email sent. Please check your email."; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "New email")] + public string? NewEmail { get; set; } + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/EnableAuthenticator.razor b/RobotApp/Components/Account/Pages/Manage/EnableAuthenticator.razor new file mode 100644 index 0000000..92be186 --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/EnableAuthenticator.razor @@ -0,0 +1,172 @@ +@page "/Account/Manage/EnableAuthenticator" + +@using System.ComponentModel.DataAnnotations +@using System.Globalization +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject UserManager UserManager +@inject IdentityUserAccessor UserAccessor +@inject UrlEncoder UrlEncoder +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Configure authenticator app + +@if (recoveryCodes is not null) +{ + +} +else +{ + +

Configure authenticator app

+
+

To use an authenticator app go through the following steps:

+
    +
  1. +

    + Download a two-factor authenticator app like Microsoft Authenticator for + Android and + iOS or + Google Authenticator for + Android and + iOS. +

    +
  2. +
  3. +

    Scan the QR Code or enter this key @sharedKey into your two factor authenticator app. Spaces and casing do not matter.

    + +
    +
    +
  4. +
  5. +

    + Once you have scanned the QR code or input the key above, your two factor authentication app will provide you + with a unique code. Enter the code in the confirmation box below. +

    +
    +
    + + +
    + + + +
    + + +
    +
    +
    +
  6. +
+
+} + +@code { + private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; + + private string? message; + private ApplicationUser user = default!; + private string? sharedKey; + private string? authenticatorUri; + private IEnumerable? recoveryCodes; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + + await LoadSharedKeyAndQrCodeUriAsync(user); + } + + private async Task OnValidSubmitAsync() + { + // Strip spaces and hyphens + var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); + + var is2faTokenValid = await UserManager.VerifyTwoFactorTokenAsync( + user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); + + if (!is2faTokenValid) + { + message = "Error: Verification code is invalid."; + return; + } + + await UserManager.SetTwoFactorEnabledAsync(user, true); + var userId = await UserManager.GetUserIdAsync(user); + Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId); + + message = "Your authenticator app has been verified."; + + if (await UserManager.CountRecoveryCodesAsync(user) == 0) + { + recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + } + else + { + RedirectManager.RedirectToWithStatus("Account/Manage/TwoFactorAuthentication", message, HttpContext); + } + } + + private async ValueTask LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user) + { + // Load the authenticator key & QR code URI to display on the form + var unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user); + if (string.IsNullOrEmpty(unformattedKey)) + { + await UserManager.ResetAuthenticatorKeyAsync(user); + unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user); + } + + sharedKey = FormatKey(unformattedKey!); + + var email = await UserManager.GetEmailAsync(user); + authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!); + } + + private string FormatKey(string unformattedKey) + { + var result = new StringBuilder(); + int currentPosition = 0; + while (currentPosition + 4 < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' '); + currentPosition += 4; + } + if (currentPosition < unformattedKey.Length) + { + result.Append(unformattedKey.AsSpan(currentPosition)); + } + + return result.ToString().ToLowerInvariant(); + } + + private string GenerateQrCodeUri(string email, string unformattedKey) + { + return string.Format( + CultureInfo.InvariantCulture, + AuthenticatorUriFormat, + UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"), + UrlEncoder.Encode(email), + unformattedKey); + } + + private sealed class InputModel + { + [Required] + [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Text)] + [Display(Name = "Verification Code")] + public string Code { get; set; } = ""; + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/ExternalLogins.razor b/RobotApp/Components/Account/Pages/Manage/ExternalLogins.razor new file mode 100644 index 0000000..ff4792c --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/ExternalLogins.razor @@ -0,0 +1,140 @@ +@page "/Account/Manage/ExternalLogins" + +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IUserStore UserStore +@inject IdentityRedirectManager RedirectManager + +Manage your external logins + + +@if (currentLogins?.Count > 0) +{ +

Registered Logins

+ + + @foreach (var login in currentLogins) + { + + + + + } + +
@login.ProviderDisplayName + @if (showRemoveButton) + { +
+ +
+ + + +
+ + } + else + { + @:   + } +
+} +@if (otherLogins?.Count > 0) +{ +

Add another service to log in.

+
+
+ +
+

+ @foreach (var provider in otherLogins) + { + + } +

+
+ +} + +@code { + public const string LinkLoginCallbackAction = "LinkLoginCallback"; + + private ApplicationUser user = default!; + private IList? currentLogins; + private IList? otherLogins; + private bool showRemoveButton; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private string? LoginProvider { get; set; } + + [SupplyParameterFromForm] + private string? ProviderKey { get; set; } + + [SupplyParameterFromQuery] + private string? Action { get; set; } + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + currentLogins = await UserManager.GetLoginsAsync(user); + otherLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()) + .Where(auth => currentLogins.All(ul => auth.Name != ul.LoginProvider)) + .ToList(); + + string? passwordHash = null; + if (UserStore is IUserPasswordStore userPasswordStore) + { + passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted); + } + + showRemoveButton = passwordHash is not null || currentLogins.Count > 1; + + if (HttpMethods.IsGet(HttpContext.Request.Method) && Action == LinkLoginCallbackAction) + { + await OnGetLinkLoginCallbackAsync(); + } + } + + private async Task OnSubmitAsync() + { + var result = await UserManager.RemoveLoginAsync(user, LoginProvider!, ProviderKey!); + if (!result.Succeeded) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not removed.", HttpContext); + } + + await SignInManager.RefreshSignInAsync(user); + RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed.", HttpContext); + } + + private async Task OnGetLinkLoginCallbackAsync() + { + var userId = await UserManager.GetUserIdAsync(user); + var info = await SignInManager.GetExternalLoginInfoAsync(userId); + if (info is null) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: Could not load external login info.", HttpContext); + } + + var result = await UserManager.AddLoginAsync(user, info); + if (!result.Succeeded) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not added. External logins can only be associated with one account.", HttpContext); + } + + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + + RedirectManager.RedirectToCurrentPageWithStatus("The external login was added.", HttpContext); + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor b/RobotApp/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor new file mode 100644 index 0000000..03163a3 --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor @@ -0,0 +1,68 @@ +@page "/Account/Manage/GenerateRecoveryCodes" + +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject UserManager UserManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Generate two-factor authentication (2FA) recovery codes + +@if (recoveryCodes is not null) +{ + +} +else +{ +

Generate two-factor authentication (2FA) recovery codes

+ +
+
+ + + +
+} + +@code { + private string? message; + private ApplicationUser user = default!; + private IEnumerable? recoveryCodes; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + + var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(user); + if (!isTwoFactorEnabled) + { + throw new InvalidOperationException("Cannot generate recovery codes for user because they do not have 2FA enabled."); + } + } + + private async Task OnSubmitAsync() + { + var userId = await UserManager.GetUserIdAsync(user); + recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + message = "You have generated new recovery codes."; + + Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId); + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/Index.razor b/RobotApp/Components/Account/Pages/Manage/Index.razor new file mode 100644 index 0000000..77f4f19 --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/Index.razor @@ -0,0 +1,77 @@ +@page "/Account/Manage" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager + +Profile + +

Profile

+ + +
+
+ + + +
+ + +
+
+ + + +
+ +
+
+
+ +@code { + private ApplicationUser user = default!; + private string? username; + private string? phoneNumber; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + username = await UserManager.GetUserNameAsync(user); + phoneNumber = await UserManager.GetPhoneNumberAsync(user); + + Input.PhoneNumber ??= phoneNumber; + } + + private async Task OnValidSubmitAsync() + { + if (Input.PhoneNumber != phoneNumber) + { + var setPhoneResult = await UserManager.SetPhoneNumberAsync(user, Input.PhoneNumber); + if (!setPhoneResult.Succeeded) + { + RedirectManager.RedirectToCurrentPageWithStatus("Error: Failed to set phone number.", HttpContext); + } + } + + await SignInManager.RefreshSignInAsync(user); + RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext); + } + + private sealed class InputModel + { + [Phone] + [Display(Name = "Phone number")] + public string? PhoneNumber { get; set; } + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/PersonalData.razor b/RobotApp/Components/Account/Pages/Manage/PersonalData.razor new file mode 100644 index 0000000..851eb54 --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/PersonalData.razor @@ -0,0 +1,34 @@ +@page "/Account/Manage/PersonalData" + +@inject IdentityUserAccessor UserAccessor + +Personal Data + + +

Personal Data

+ +
+
+

Your account contains personal data that you have given us. This page allows you to download or delete that data.

+

+ Deleting this data will permanently remove your account, and this cannot be recovered. +

+
+ + + +

+ Delete +

+
+
+ +@code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + _ = await UserAccessor.GetRequiredUserAsync(HttpContext); + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/ResetAuthenticator.razor b/RobotApp/Components/Account/Pages/Manage/ResetAuthenticator.razor new file mode 100644 index 0000000..97bca61 --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/ResetAuthenticator.razor @@ -0,0 +1,52 @@ +@page "/Account/Manage/ResetAuthenticator" + +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager +@inject ILogger Logger + +Reset authenticator key + + +

Reset authenticator key

+ +
+
+ + + +
+ +@code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + private async Task OnSubmitAsync() + { + var user = await UserAccessor.GetRequiredUserAsync(HttpContext); + await UserManager.SetTwoFactorEnabledAsync(user, false); + await UserManager.ResetAuthenticatorKeyAsync(user); + var userId = await UserManager.GetUserIdAsync(user); + Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId); + + await SignInManager.RefreshSignInAsync(user); + + RedirectManager.RedirectToWithStatus( + "Account/Manage/EnableAuthenticator", + "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.", + HttpContext); + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/SetPassword.razor b/RobotApp/Components/Account/Pages/Manage/SetPassword.razor new file mode 100644 index 0000000..3048d81 --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/SetPassword.razor @@ -0,0 +1,87 @@ +@page "/Account/Manage/SetPassword" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager + +Set password + +

Set your password

+ +

+ You do not have a local username/password for this site. Add a local + account so you can log in without an external login. +

+
+
+ + + +
+ + + +
+
+ + + +
+ +
+
+
+ +@code { + private string? message; + private ApplicationUser user = default!; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + protected override async Task OnInitializedAsync() + { + user = await UserAccessor.GetRequiredUserAsync(HttpContext); + + var hasPassword = await UserManager.HasPasswordAsync(user); + if (hasPassword) + { + RedirectManager.RedirectTo("Account/Manage/ChangePassword"); + } + } + + private async Task OnValidSubmitAsync() + { + var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!); + if (!addPasswordResult.Succeeded) + { + message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}"; + return; + } + + await SignInManager.RefreshSignInAsync(user); + RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext); + } + + private sealed class InputModel + { + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string? NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string? ConfirmPassword { get; set; } + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/TwoFactorAuthentication.razor b/RobotApp/Components/Account/Pages/Manage/TwoFactorAuthentication.razor new file mode 100644 index 0000000..c4a1052 --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/TwoFactorAuthentication.razor @@ -0,0 +1,101 @@ +@page "/Account/Manage/TwoFactorAuthentication" + +@using Microsoft.AspNetCore.Http.Features +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject UserManager UserManager +@inject SignInManager SignInManager +@inject IdentityUserAccessor UserAccessor +@inject IdentityRedirectManager RedirectManager + +Two-factor authentication (2FA) + + +

Two-factor authentication (2FA)

+@if (canTrack) +{ + if (is2faEnabled) + { + if (recoveryCodesLeft == 0) + { +
+ You have no recovery codes left. +

You must generate a new set of recovery codes before you can log in with a recovery code.

+
+ } + else if (recoveryCodesLeft == 1) + { +
+ You have 1 recovery code left. +

You can generate a new set of recovery codes.

+
+ } + else if (recoveryCodesLeft <= 3) + { +
+ You have @recoveryCodesLeft recovery codes left. +

You should generate a new set of recovery codes.

+
+ } + + if (isMachineRemembered) + { +
+ + + + } + + Disable 2FA + Reset recovery codes + } + +

Authenticator app

+ @if (!hasAuthenticator) + { + Add authenticator app + } + else + { + Set up authenticator app + Reset authenticator app + } +} +else +{ +
+ Privacy and cookie policy have not been accepted. +

You must accept the policy before you can enable two factor authentication.

+
+} + +@code { + private bool canTrack; + private bool hasAuthenticator; + private int recoveryCodesLeft; + private bool is2faEnabled; + private bool isMachineRemembered; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + var user = await UserAccessor.GetRequiredUserAsync(HttpContext); + canTrack = HttpContext.Features.Get()?.CanTrack ?? true; + hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null; + is2faEnabled = await UserManager.GetTwoFactorEnabledAsync(user); + isMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user); + recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user); + } + + private async Task OnSubmitForgetBrowserAsync() + { + await SignInManager.ForgetTwoFactorClientAsync(); + + RedirectManager.RedirectToCurrentPageWithStatus( + "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.", + HttpContext); + } +} diff --git a/RobotApp/Components/Account/Pages/Manage/_Imports.razor b/RobotApp/Components/Account/Pages/Manage/_Imports.razor new file mode 100644 index 0000000..ada5bb0 --- /dev/null +++ b/RobotApp/Components/Account/Pages/Manage/_Imports.razor @@ -0,0 +1,2 @@ +@layout ManageLayout +@attribute [Microsoft.AspNetCore.Authorization.Authorize] diff --git a/RobotApp/Components/Account/Pages/Register.razor b/RobotApp/Components/Account/Pages/Register.razor new file mode 100644 index 0000000..0335e66 --- /dev/null +++ b/RobotApp/Components/Account/Pages/Register.razor @@ -0,0 +1,145 @@ +@page "/Account/Register" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using RobotApp.Data + +@inject UserManager UserManager +@inject IUserStore UserStore +@inject SignInManager SignInManager +@inject IEmailSender EmailSender +@inject ILogger Logger +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Register + +

Register

+ +
+
+ + + +

Create a new account.

+
+ +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+
+

Use another service to register.

+
+ +
+
+
+ +@code { + private IEnumerable? identityErrors; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; + + public async Task RegisterUser(EditContext editContext) + { + var user = CreateUser(); + + await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); + var emailStore = GetEmailStore(); + await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); + var result = await UserManager.CreateAsync(user, Input.Password); + + if (!result.Succeeded) + { + identityErrors = result.Errors; + return; + } + + Logger.LogInformation("User created a new account with password."); + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl }); + + await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + + if (UserManager.Options.SignIn.RequireConfirmedAccount) + { + RedirectManager.RedirectTo( + "Account/RegisterConfirmation", + new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl }); + } + + await SignInManager.SignInAsync(user, isPersistent: false); + RedirectManager.RedirectTo(ReturnUrl); + } + + private ApplicationUser CreateUser() + { + try + { + return Activator.CreateInstance(); + } + catch + { + throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " + + $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor."); + } + } + + private IUserEmailStore GetEmailStore() + { + if (!UserManager.SupportsUserEmail) + { + throw new NotSupportedException("The default UI requires a user store with email support."); + } + return (IUserEmailStore)UserStore; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } = ""; + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } = ""; + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = ""; + } +} diff --git a/RobotApp/Components/Account/Pages/RegisterConfirmation.razor b/RobotApp/Components/Account/Pages/RegisterConfirmation.razor new file mode 100644 index 0000000..f903e0e --- /dev/null +++ b/RobotApp/Components/Account/Pages/RegisterConfirmation.razor @@ -0,0 +1,68 @@ +@page "/Account/RegisterConfirmation" + +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using RobotApp.Data + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Register confirmation + +

Register confirmation

+ + + +@if (emailConfirmationLink is not null) +{ +

+ This app does not currently have a real email sender registered, see these docs for how to configure a real email sender. + Normally this would be emailed: Click here to confirm your account +

+} +else +{ +

Please check your email to confirm your account.

+} + +@code { + private string? emailConfirmationLink; + private string? statusMessage; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromQuery] + private string? Email { get; set; } + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + if (Email is null) + { + RedirectManager.RedirectTo(""); + } + + var user = await UserManager.FindByEmailAsync(Email); + if (user is null) + { + HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; + statusMessage = "Error finding user for unspecified email"; + } + else if (EmailSender is IdentityNoOpEmailSender) + { + // Once you add a real email sender, you should remove this code that lets you confirm the account + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + emailConfirmationLink = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl }); + } + } +} diff --git a/RobotApp/Components/Account/Pages/ResendEmailConfirmation.razor b/RobotApp/Components/Account/Pages/ResendEmailConfirmation.razor new file mode 100644 index 0000000..c4fe4a5 --- /dev/null +++ b/RobotApp/Components/Account/Pages/ResendEmailConfirmation.razor @@ -0,0 +1,68 @@ +@page "/Account/ResendEmailConfirmation" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using RobotApp.Data + +@inject UserManager UserManager +@inject IEmailSender EmailSender +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Resend email confirmation + +

Resend email confirmation

+

Enter your email.

+
+ +
+
+ + + +
+ + + +
+ +
+
+
+ +@code { + private string? message; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + private async Task OnValidSubmitAsync() + { + var user = await UserManager.FindByEmailAsync(Input.Email!); + if (user is null) + { + message = "Verification email sent. Please check your email."; + return; + } + + var userId = await UserManager.GetUserIdAsync(user); + var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); + code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); + var callbackUrl = NavigationManager.GetUriWithQueryParameters( + NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, + new Dictionary { ["userId"] = userId, ["code"] = code }); + await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); + + message = "Verification email sent. Please check your email."; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + } +} diff --git a/RobotApp/Components/Account/Pages/ResetPassword.razor b/RobotApp/Components/Account/Pages/ResetPassword.razor new file mode 100644 index 0000000..cdb0a0c --- /dev/null +++ b/RobotApp/Components/Account/Pages/ResetPassword.razor @@ -0,0 +1,103 @@ +@page "/Account/ResetPassword" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using RobotApp.Data + +@inject IdentityRedirectManager RedirectManager +@inject UserManager UserManager + +Reset password + +

Reset password

+

Reset your password.

+
+
+
+ + + + + + +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+
+ +@code { + private IEnumerable? identityErrors; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? Code { get; set; } + + private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; + + protected override void OnInitialized() + { + if (Code is null) + { + RedirectManager.RedirectTo("Account/InvalidPasswordReset"); + } + + Input.Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); + } + + private async Task OnValidSubmitAsync() + { + var user = await UserManager.FindByEmailAsync(Input.Email); + if (user is null) + { + // Don't reveal that the user does not exist + RedirectManager.RedirectTo("Account/ResetPasswordConfirmation"); + } + + var result = await UserManager.ResetPasswordAsync(user, Input.Code, Input.Password); + if (result.Succeeded) + { + RedirectManager.RedirectTo("Account/ResetPasswordConfirmation"); + } + + identityErrors = result.Errors; + } + + private sealed class InputModel + { + [Required] + [EmailAddress] + public string Email { get; set; } = ""; + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + public string Password { get; set; } = ""; + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = ""; + + [Required] + public string Code { get; set; } = ""; + } +} diff --git a/RobotApp/Components/Account/Pages/ResetPasswordConfirmation.razor b/RobotApp/Components/Account/Pages/ResetPasswordConfirmation.razor new file mode 100644 index 0000000..247e96e --- /dev/null +++ b/RobotApp/Components/Account/Pages/ResetPasswordConfirmation.razor @@ -0,0 +1,7 @@ +@page "/Account/ResetPasswordConfirmation" +Reset password confirmation + +

Reset password confirmation

+

+ Your password has been reset. Please click here to log in. +

diff --git a/RobotApp/Components/Account/Pages/_Imports.razor b/RobotApp/Components/Account/Pages/_Imports.razor new file mode 100644 index 0000000..9b94163 --- /dev/null +++ b/RobotApp/Components/Account/Pages/_Imports.razor @@ -0,0 +1,2 @@ +@using RobotApp.Components.Account.Shared +@attribute [ExcludeFromInteractiveRouting] diff --git a/RobotApp/Components/Account/Shared/ExternalLoginPicker.razor b/RobotApp/Components/Account/Shared/ExternalLoginPicker.razor new file mode 100644 index 0000000..652875f --- /dev/null +++ b/RobotApp/Components/Account/Shared/ExternalLoginPicker.razor @@ -0,0 +1,43 @@ +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject SignInManager SignInManager +@inject IdentityRedirectManager RedirectManager + +@if (externalLogins.Length == 0) +{ +
+

+ There are no external authentication services configured. See this article + about setting up this ASP.NET application to support logging in via external services. +

+
+} +else +{ +
+
+ + +

+ @foreach (var provider in externalLogins) + { + + } +

+
+
+} + +@code { + private AuthenticationScheme[] externalLogins = []; + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToArray(); + } +} diff --git a/RobotApp/Components/Account/Shared/ManageLayout.razor b/RobotApp/Components/Account/Shared/ManageLayout.razor new file mode 100644 index 0000000..41579b1 --- /dev/null +++ b/RobotApp/Components/Account/Shared/ManageLayout.razor @@ -0,0 +1,17 @@ +@inherits LayoutComponentBase +@layout RobotApp.Client.Layout.MainLayout + +

Manage your account

+ +
+

Change your account settings

+
+
+
+ +
+
+ @Body +
+
+
diff --git a/RobotApp/Components/Account/Shared/ManageNavMenu.razor b/RobotApp/Components/Account/Shared/ManageNavMenu.razor new file mode 100644 index 0000000..590b276 --- /dev/null +++ b/RobotApp/Components/Account/Shared/ManageNavMenu.razor @@ -0,0 +1,37 @@ +@using Microsoft.AspNetCore.Identity +@using RobotApp.Data + +@inject SignInManager SignInManager + + + +@code { + private bool hasExternalLogins; + + protected override async Task OnInitializedAsync() + { + hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); + } +} diff --git a/RobotApp/Components/Account/Shared/ShowRecoveryCodes.razor b/RobotApp/Components/Account/Shared/ShowRecoveryCodes.razor new file mode 100644 index 0000000..aa92e11 --- /dev/null +++ b/RobotApp/Components/Account/Shared/ShowRecoveryCodes.razor @@ -0,0 +1,28 @@ + +

Recovery codes

+ +
+
+ @foreach (var recoveryCode in RecoveryCodes) + { +
+ @recoveryCode +
+ } +
+
+ +@code { + [Parameter] + public string[] RecoveryCodes { get; set; } = []; + + [Parameter] + public string? StatusMessage { get; set; } +} diff --git a/RobotApp/Components/Account/Shared/StatusMessage.razor b/RobotApp/Components/Account/Shared/StatusMessage.razor new file mode 100644 index 0000000..12cd544 --- /dev/null +++ b/RobotApp/Components/Account/Shared/StatusMessage.razor @@ -0,0 +1,29 @@ +@if (!string.IsNullOrEmpty(DisplayMessage)) +{ + var statusMessageClass = DisplayMessage.StartsWith("Error") ? "danger" : "success"; + +} + +@code { + private string? messageFromCookie; + + [Parameter] + public string? Message { get; set; } + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + private string? DisplayMessage => Message ?? messageFromCookie; + + protected override void OnInitialized() + { + messageFromCookie = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName]; + + if (messageFromCookie is not null) + { + HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName); + } + } +} diff --git a/RobotApp/Components/App.razor b/RobotApp/Components/App.razor new file mode 100644 index 0000000..1bc17c5 --- /dev/null +++ b/RobotApp/Components/App.razor @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + +@code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + private IComponentRenderMode? PageRenderMode => + HttpContext.AcceptsInteractiveRouting() ? InteractiveAuto : null; +} diff --git a/RobotApp/Components/Pages/Error.razor b/RobotApp/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/RobotApp/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/RobotApp/Components/_Imports.razor b/RobotApp/Components/_Imports.razor new file mode 100644 index 0000000..d24d96a --- /dev/null +++ b/RobotApp/Components/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using RobotApp +@using RobotApp.Client +@using RobotApp.Components diff --git a/RobotApp/Data/ApplicationDbContext.cs b/RobotApp/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..094a3d9 --- /dev/null +++ b/RobotApp/Data/ApplicationDbContext.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace RobotApp.Data +{ + public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext(options) + { + } +} diff --git a/RobotApp/Data/ApplicationUser.cs b/RobotApp/Data/ApplicationUser.cs new file mode 100644 index 0000000..a44f465 --- /dev/null +++ b/RobotApp/Data/ApplicationUser.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity; + +namespace RobotApp.Data +{ + // Add profile data for application users by adding properties to the ApplicationUser class + public class ApplicationUser : IdentityUser + { + } + +} diff --git a/RobotApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/RobotApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs new file mode 100644 index 0000000..ea472ef --- /dev/null +++ b/RobotApp/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs @@ -0,0 +1,279 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotApp.Data; + +#nullable disable + +namespace RobotApp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("00000000000000_CreateIdentitySchema")] + partial class CreateIdentitySchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotApp.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs b/RobotApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs new file mode 100644 index 0000000..e525594 --- /dev/null +++ b/RobotApp/Data/Migrations/00000000000000_CreateIdentitySchema.cs @@ -0,0 +1,224 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RobotApp.Migrations +{ + /// + public partial class CreateIdentitySchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + RoleId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/RobotApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/RobotApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..4515ae9 --- /dev/null +++ b/RobotApp/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,276 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotApp.Data; + +#nullable disable + +namespace RobotApp.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotApp.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RobotApp.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotApp/Program.cs b/RobotApp/Program.cs new file mode 100644 index 0000000..5bf0725 --- /dev/null +++ b/RobotApp/Program.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using RobotApp.Client.Pages; +using RobotApp.Components; +using RobotApp.Components.Account; +using RobotApp.Data; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddInteractiveWebAssemblyComponents() + .AddAuthenticationStateSerialization(); + +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddAuthentication(options => + { + options.DefaultScheme = IdentityConstants.ApplicationScheme; + options.DefaultSignInScheme = IdentityConstants.ExternalScheme; + }) + .AddIdentityCookies(); + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); +builder.Services.AddDbContext(options => + options.UseSqlServer(connectionString)); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + +builder.Services.AddIdentityCore(options => options.SignIn.RequireConfirmedAccount = true) + .AddEntityFrameworkStores() + .AddSignInManager() + .AddDefaultTokenProviders(); + +builder.Services.AddSingleton, IdentityNoOpEmailSender>(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); + app.UseMigrationsEndPoint(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + + +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(RobotApp.Client._Imports).Assembly); + +// Add additional endpoints required by the Identity /Account Razor components. +app.MapAdditionalIdentityEndpoints(); + +app.Run(); diff --git a/RobotApp/Properties/launchSettings.json b/RobotApp/Properties/launchSettings.json new file mode 100644 index 0000000..b4aecb3 --- /dev/null +++ b/RobotApp/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7150;http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/RobotApp/Properties/serviceDependencies.json b/RobotApp/Properties/serviceDependencies.json new file mode 100644 index 0000000..d8177e0 --- /dev/null +++ b/RobotApp/Properties/serviceDependencies.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "mssql1": { + "type": "mssql", + "connectionId": "ConnectionStrings:DefaultConnection" + } + } +} \ No newline at end of file diff --git a/RobotApp/Properties/serviceDependencies.local.json b/RobotApp/Properties/serviceDependencies.local.json new file mode 100644 index 0000000..299aa9a --- /dev/null +++ b/RobotApp/Properties/serviceDependencies.local.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "mssql1": { + "type": "mssql.local", + "connectionId": "ConnectionStrings:DefaultConnection" + } + } +} \ No newline at end of file diff --git a/RobotApp/RobotApp.csproj b/RobotApp/RobotApp.csproj new file mode 100644 index 0000000..3c36dac --- /dev/null +++ b/RobotApp/RobotApp.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + enable + aspnet-RobotApp-1f61caa2-bbbb-40cd-88b6-409b408a84ea + + + + + + + + + + + + + + + + diff --git a/RobotApp/appsettings.Development.json b/RobotApp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/RobotApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/RobotApp/appsettings.json b/RobotApp/appsettings.json new file mode 100644 index 0000000..3b09a76 --- /dev/null +++ b/RobotApp/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-RobotApp-1f61caa2-bbbb-40cd-88b6-409b408a84ea;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/RobotApp/wwwroot/app.css b/RobotApp/wwwroot/app.css new file mode 100644 index 0000000..73a69d6 --- /dev/null +++ b/RobotApp/wwwroot/app.css @@ -0,0 +1,60 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +a, .btn-link { + color: #006bb7; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} + +.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { + color: var(--bs-secondary-color); + text-align: end; +} + +.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { + text-align: start; +} \ No newline at end of file diff --git a/RobotApp/wwwroot/favicon.png b/RobotApp/wwwroot/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8422b59695935d180d11d5dbe99653e711097819 GIT binary patch literal 1148 zcmV-?1cUpDP)9h26h2-Cs%i*@Moc3?#6qJID|D#|3|2Hn7gTIYEkr|%Xjp);YgvFmB&0#2E2b=| zkVr)lMv9=KqwN&%obTp-$<51T%rx*NCwceh-E+=&e(oLO`@Z~7gybJ#U|^tB2Pai} zRN@5%1qsZ1e@R(XC8n~)nU1S0QdzEYlWPdUpH{wJ2Pd4V8kI3BM=)sG^IkUXF2-j{ zrPTYA6sxpQ`Q1c6mtar~gG~#;lt=s^6_OccmRd>o{*=>)KS=lM zZ!)iG|8G0-9s3VLm`bsa6e ze*TlRxAjXtm^F8V`M1%s5d@tYS>&+_ga#xKGb|!oUBx3uc@mj1%=MaH4GR0tPBG_& z9OZE;->dO@`Q)nr<%dHAsEZRKl zedN6+3+uGHejJp;Q==pskSAcRcyh@6mjm2z-uG;s%dM-u0*u##7OxI7wwyCGpS?4U zBFAr(%GBv5j$jS@@t@iI8?ZqE36I^4t+P^J9D^ELbS5KMtZ z{Qn#JnSd$15nJ$ggkF%I4yUQC+BjDF^}AtB7w348EL>7#sAsLWs}ndp8^DsAcOIL9 zTOO!!0!k2`9BLk25)NeZp7ev>I1Mn={cWI3Yhx2Q#DnAo4IphoV~R^c0x&nw*MoIV zPthX?{6{u}sMS(MxD*dmd5rU(YazQE59b|TsB5Tm)I4a!VaN@HYOR)DwH1U5y(E)z zQqQU*B%MwtRQ$%x&;1p%ANmc|PkoFJZ%<-uq%PX&C!c-7ypis=eP+FCeuv+B@h#{4 zGx1m0PjS~FJt}3mdt4c!lel`1;4W|03kcZRG+DzkTy|7-F~eDsV2Tx!73dM0H0CTh zl)F-YUkE1zEzEW(;JXc|KR5{ox%YTh{$%F$a36JP6Nb<0%#NbSh$dMYF-{ z1_x(Vx)}fs?5_|!5xBTWiiIQHG<%)*e=45Fhjw_tlnmlixq;mUdC$R8v#j( zhQ$9YR-o%i5Uc`S?6EC51!bTRK=Xkyb<18FkCKnS2;o*qlij1YA@-nRpq#OMTX&RbL<^2q@0qja!uIvI;j$6>~k@IMwD42=8$$!+R^@5o6HX(*n~