From 32664fea017c6ce2d6519619df762b18da0e3354 Mon Sep 17 00:00:00 2001 From: Rafael Date: Thu, 23 May 2024 19:11:12 -0300 Subject: [PATCH] . --- .docker/.dockerignore | 19 ++ .docker/docker-compose.yaml | 39 +++ .docker/dockerfile | 21 ++ .editorconfig | 8 + .gitattributes | 2 + .github/workflows/build.yaml | 44 ++++ .gitignore | 18 ++ license.md | 5 + postman.json | 1 + readme.md | 242 ++++++++++++++++++ source/.vscode/launch.json | 22 ++ source/.vscode/tasks.json | 42 +++ .../Architecture.Application.csproj | 24 ++ source/Application/Auth/AuthHandler.cs | 42 +++ source/Application/Auth/AuthRequest.cs | 3 + .../Application/Auth/AuthRequestValidator.cs | 10 + source/Application/Auth/AuthResponse.cs | 3 + .../Example/Add/AddExampleHandler.cs | 30 +++ .../Example/Add/AddExampleRequest.cs | 3 + .../Example/Add/AddExampleRequestValidator.cs | 6 + .../Example/Delete/DeleteExampleHandler.cs | 28 ++ .../Example/Delete/DeleteExampleRequest.cs | 3 + .../Delete/DeleteExampleRequestValidator.cs | 6 + .../Example/Get/GetExampleHandler.cs | 17 ++ .../Example/Get/GetExampleRequest.cs | 3 + .../Example/Get/GetExampleRequestValidator.cs | 6 + .../Example/Grid/GridExampleHandler.cs | 17 ++ .../Example/Grid/GridExampleRequest.cs | 3 + .../Grid/GridExampleRequestValidator.cs | 6 + .../Example/List/ListExampleHandler.cs | 17 ++ .../Example/List/ListExampleRequest.cs | 3 + .../Example/Update/UpdateExampleHandler.cs | 30 +++ .../Example/Update/UpdateExampleRequest.cs | 9 + .../Update/UpdateExampleRequestValidator.cs | 10 + source/Application/File/Add/AddFileHandler.cs | 13 + source/Application/File/Add/AddFileRequest.cs | 3 + .../File/Add/AddFileRequestValidator.cs | 6 + source/Application/File/Get/GetFileHandler.cs | 13 + source/Application/File/Get/GetFileRequest.cs | 3 + .../File/Get/GetFileRequestValidator.cs | 6 + source/Application/User/Add/AddUserHandler.cs | 44 ++++ source/Application/User/Add/AddUserRequest.cs | 3 + .../User/Add/AddUserRequestValidator.cs | 12 + .../User/Delete/DeleteUserHandler.cs | 33 +++ .../User/Delete/DeleteUserRequest.cs | 3 + .../User/Delete/DeleteUserRequestValidator.cs | 6 + source/Application/User/Get/GetUserHandler.cs | 17 ++ source/Application/User/Get/GetUserRequest.cs | 3 + .../User/Get/GetUserRequestValidator.cs | 6 + .../Application/User/Grid/GridUserHandler.cs | 17 ++ .../Application/User/Grid/GridUserRequest.cs | 3 + .../User/Grid/GridUserRequestValidator.cs | 6 + .../User/Inactivate/InactivateUserHandler.cs | 32 +++ .../User/Inactivate/InactivateUserRequest.cs | 3 + .../InactivateUserRequestValidator.cs | 6 + .../Application/User/List/ListUserHandler.cs | 17 ++ .../Application/User/List/ListUserRequest.cs | 3 + .../User/Update/UpdateUserHandler.cs | 36 +++ .../User/Update/UpdateUserRequest.cs | 9 + .../User/Update/UpdateUserRequestValidator.cs | 11 + source/Application/Validators.cs | 20 ++ source/Architecture.sln | 49 ++++ source/Database/Architecture.Database.csproj | 28 ++ source/Database/Auth/AuthConfiguration.cs | 29 +++ source/Database/Auth/AuthRepository.cs | 10 + source/Database/Auth/IAuthRepository.cs | 8 + source/Database/Context/Context.cs | 8 + source/Database/Context/ContextFactory.cs | 11 + source/Database/Context/ContextSeed.cs | 27 ++ .../Database/Example/ExampleConfiguration.cs | 15 ++ source/Database/Example/ExampleRepository.cs | 14 + source/Database/Example/IExampleRepository.cs | 10 + .../00000000000000_Initial.Designer.cs | 150 +++++++++++ .../Migrations/00000000000000_Initial.cs | 135 ++++++++++ .../Migrations/ContextModelSnapshot.cs | 147 +++++++++++ source/Database/User/IUserRepository.cs | 10 + source/Database/User/UserConfiguration.cs | 21 ++ source/Database/User/UserRepository.cs | 19 ++ source/Directory.Build.props | 9 + source/Directory.Packages.props | 22 ++ source/Domain/Architecture.Domain.csproj | 8 + source/Domain/Auth.cs | 32 +++ source/Domain/Example.cs | 10 + source/Domain/Roles.cs | 9 + source/Domain/Status.cs | 8 + source/Domain/User.cs | 31 +++ source/Model/Architecture.Model.csproj | 1 + source/Model/ExampleModel.cs | 8 + source/Model/UserModel.cs | 10 + source/Web/AppSettings.json | 50 ++++ source/Web/AppStrings.json | 6 + source/Web/Architecture.Web.csproj | 37 +++ source/Web/Controllers/AuthController.cs | 10 + source/Web/Controllers/BaseController.cs | 7 + source/Web/Controllers/ExampleController.cs | 29 +++ source/Web/Controllers/FileController.cs | 13 + source/Web/Controllers/UserController.cs | 32 +++ source/Web/Frontend/.npmrc | 1 + source/Web/Frontend/angular.json | 75 ++++++ source/Web/Frontend/package.json | 35 +++ source/Web/Frontend/proxy.json | 10 + .../Web/Frontend/src/app/app.can.activate.ts | 10 + source/Web/Frontend/src/app/app.component.ts | 4 + .../Web/Frontend/src/app/app.error.handler.ts | 14 + .../Frontend/src/app/app.http.interceptor.ts | 13 + source/Web/Frontend/src/app/app.module.ts | 25 ++ source/Web/Frontend/src/app/app.routes.ts | 29 +++ .../components/button/button.component.html | 1 + .../app/components/button/button.component.ts | 11 + .../Frontend/src/app/components/component.ts | 25 ++ .../app/components/file/file.component.html | 15 ++ .../src/app/components/file/file.component.ts | 50 ++++ .../src/app/components/file/file.model.ts | 3 + .../src/app/components/file/file.service.ts | 31 +++ .../src/app/components/file/upload.model.ts | 3 + .../components/grid/filter/filter.model.ts | 3 + .../components/grid/filter/filters.model.ts | 25 ++ .../components/grid/grid-parameters.model.ts | 9 + .../src/app/components/grid/grid.model.ts | 7 + .../src/app/components/grid/grid.service.ts | 39 +++ .../grid/order/order.component.html | 7 + .../components/grid/order/order.component.ts | 25 ++ .../app/components/grid/order/order.model.ts | 3 + .../components/grid/page/page.component.html | 9 + .../components/grid/page/page.component.ts | 48 ++++ .../app/components/grid/page/page.model.ts | 3 + .../app/components/input/input.component.html | 12 + .../app/components/input/input.component.ts | 14 + .../input/password.input.component.ts | 27 ++ .../components/input/text.input.component.ts | 27 ++ .../app/components/label/label.component.html | 1 + .../app/components/label/label.component.ts | 11 + .../select/comment.select.component.ts | 34 +++ .../src/app/components/select/option.model.ts | 3 + .../select/post.select.component.ts | 34 +++ .../components/select/select.component.html | 12 + .../app/components/select/select.component.ts | 31 +++ .../select/user.select.component.ts | 32 +++ .../app/layouts/footer/footer.component.html | 3 + .../app/layouts/footer/footer.component.ts | 8 + .../app/layouts/header/header.component.html | 3 + .../app/layouts/header/header.component.ts | 8 + .../layout-nav/layout-nav.component.html | 12 + .../layout-nav/layout-nav.component.ts | 18 ++ .../app/layouts/layout/layout.component.html | 7 + .../app/layouts/layout/layout.component.ts | 16 ++ .../src/app/layouts/nav/nav.component.html | 34 +++ .../src/app/layouts/nav/nav.component.ts | 17 ++ .../Web/Frontend/src/app/models/auth.model.ts | 4 + .../Web/Frontend/src/app/models/user.model.ts | 8 + .../src/app/pages/auth/auth.component.html | 17 ++ .../src/app/pages/auth/auth.component.ts | 35 +++ .../src/app/pages/files/files.component.html | 13 + .../src/app/pages/files/files.component.ts | 19 ++ .../src/app/pages/form/form.component.html | 26 ++ .../src/app/pages/form/form.component.ts | 35 +++ .../src/app/pages/home/home.component.html | 1 + .../src/app/pages/home/home.component.ts | 8 + .../app/pages/list/grid/grid.component.html | 31 +++ .../src/app/pages/list/grid/grid.component.ts | 61 +++++ .../src/app/pages/list/list.component.html | 3 + .../src/app/pages/list/list.component.ts | 12 + .../Frontend/src/app/services/auth.service.ts | 32 +++ .../src/app/services/modal.service.ts | 8 + .../Frontend/src/app/services/user.service.ts | 26 ++ .../src/app/settings/settings.model.ts | 3 + .../src/app/settings/settings.service.ts | 12 + source/Web/Frontend/src/assets/settings.json | 3 + source/Web/Frontend/src/favicon.ico | Bin 0 -> 948 bytes source/Web/Frontend/src/index.html | 20 ++ source/Web/Frontend/src/main.ts | 4 + source/Web/Frontend/src/styles/style.scss | 71 +++++ source/Web/Frontend/tsconfig.json | 43 ++++ source/Web/Program.cs | 28 ++ source/Web/Properties/launchSettings.json | 13 + source/global.json | 6 + 176 files changed, 3538 insertions(+) create mode 100644 .docker/.dockerignore create mode 100644 .docker/docker-compose.yaml create mode 100644 .docker/dockerfile create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/build.yaml create mode 100644 .gitignore create mode 100644 license.md create mode 100644 postman.json create mode 100644 readme.md create mode 100644 source/.vscode/launch.json create mode 100644 source/.vscode/tasks.json create mode 100644 source/Application/Architecture.Application.csproj create mode 100644 source/Application/Auth/AuthHandler.cs create mode 100644 source/Application/Auth/AuthRequest.cs create mode 100644 source/Application/Auth/AuthRequestValidator.cs create mode 100644 source/Application/Auth/AuthResponse.cs create mode 100644 source/Application/Example/Add/AddExampleHandler.cs create mode 100644 source/Application/Example/Add/AddExampleRequest.cs create mode 100644 source/Application/Example/Add/AddExampleRequestValidator.cs create mode 100644 source/Application/Example/Delete/DeleteExampleHandler.cs create mode 100644 source/Application/Example/Delete/DeleteExampleRequest.cs create mode 100644 source/Application/Example/Delete/DeleteExampleRequestValidator.cs create mode 100644 source/Application/Example/Get/GetExampleHandler.cs create mode 100644 source/Application/Example/Get/GetExampleRequest.cs create mode 100644 source/Application/Example/Get/GetExampleRequestValidator.cs create mode 100644 source/Application/Example/Grid/GridExampleHandler.cs create mode 100644 source/Application/Example/Grid/GridExampleRequest.cs create mode 100644 source/Application/Example/Grid/GridExampleRequestValidator.cs create mode 100644 source/Application/Example/List/ListExampleHandler.cs create mode 100644 source/Application/Example/List/ListExampleRequest.cs create mode 100644 source/Application/Example/Update/UpdateExampleHandler.cs create mode 100644 source/Application/Example/Update/UpdateExampleRequest.cs create mode 100644 source/Application/Example/Update/UpdateExampleRequestValidator.cs create mode 100644 source/Application/File/Add/AddFileHandler.cs create mode 100644 source/Application/File/Add/AddFileRequest.cs create mode 100644 source/Application/File/Add/AddFileRequestValidator.cs create mode 100644 source/Application/File/Get/GetFileHandler.cs create mode 100644 source/Application/File/Get/GetFileRequest.cs create mode 100644 source/Application/File/Get/GetFileRequestValidator.cs create mode 100644 source/Application/User/Add/AddUserHandler.cs create mode 100644 source/Application/User/Add/AddUserRequest.cs create mode 100644 source/Application/User/Add/AddUserRequestValidator.cs create mode 100644 source/Application/User/Delete/DeleteUserHandler.cs create mode 100644 source/Application/User/Delete/DeleteUserRequest.cs create mode 100644 source/Application/User/Delete/DeleteUserRequestValidator.cs create mode 100644 source/Application/User/Get/GetUserHandler.cs create mode 100644 source/Application/User/Get/GetUserRequest.cs create mode 100644 source/Application/User/Get/GetUserRequestValidator.cs create mode 100644 source/Application/User/Grid/GridUserHandler.cs create mode 100644 source/Application/User/Grid/GridUserRequest.cs create mode 100644 source/Application/User/Grid/GridUserRequestValidator.cs create mode 100644 source/Application/User/Inactivate/InactivateUserHandler.cs create mode 100644 source/Application/User/Inactivate/InactivateUserRequest.cs create mode 100644 source/Application/User/Inactivate/InactivateUserRequestValidator.cs create mode 100644 source/Application/User/List/ListUserHandler.cs create mode 100644 source/Application/User/List/ListUserRequest.cs create mode 100644 source/Application/User/Update/UpdateUserHandler.cs create mode 100644 source/Application/User/Update/UpdateUserRequest.cs create mode 100644 source/Application/User/Update/UpdateUserRequestValidator.cs create mode 100644 source/Application/Validators.cs create mode 100644 source/Architecture.sln create mode 100644 source/Database/Architecture.Database.csproj create mode 100644 source/Database/Auth/AuthConfiguration.cs create mode 100644 source/Database/Auth/AuthRepository.cs create mode 100644 source/Database/Auth/IAuthRepository.cs create mode 100644 source/Database/Context/Context.cs create mode 100644 source/Database/Context/ContextFactory.cs create mode 100644 source/Database/Context/ContextSeed.cs create mode 100644 source/Database/Example/ExampleConfiguration.cs create mode 100644 source/Database/Example/ExampleRepository.cs create mode 100644 source/Database/Example/IExampleRepository.cs create mode 100644 source/Database/Migrations/00000000000000_Initial.Designer.cs create mode 100644 source/Database/Migrations/00000000000000_Initial.cs create mode 100644 source/Database/Migrations/ContextModelSnapshot.cs create mode 100644 source/Database/User/IUserRepository.cs create mode 100644 source/Database/User/UserConfiguration.cs create mode 100644 source/Database/User/UserRepository.cs create mode 100644 source/Directory.Build.props create mode 100644 source/Directory.Packages.props create mode 100644 source/Domain/Architecture.Domain.csproj create mode 100644 source/Domain/Auth.cs create mode 100644 source/Domain/Example.cs create mode 100644 source/Domain/Roles.cs create mode 100644 source/Domain/Status.cs create mode 100644 source/Domain/User.cs create mode 100644 source/Model/Architecture.Model.csproj create mode 100644 source/Model/ExampleModel.cs create mode 100644 source/Model/UserModel.cs create mode 100644 source/Web/AppSettings.json create mode 100644 source/Web/AppStrings.json create mode 100644 source/Web/Architecture.Web.csproj create mode 100644 source/Web/Controllers/AuthController.cs create mode 100644 source/Web/Controllers/BaseController.cs create mode 100644 source/Web/Controllers/ExampleController.cs create mode 100644 source/Web/Controllers/FileController.cs create mode 100644 source/Web/Controllers/UserController.cs create mode 100644 source/Web/Frontend/.npmrc create mode 100644 source/Web/Frontend/angular.json create mode 100644 source/Web/Frontend/package.json create mode 100644 source/Web/Frontend/proxy.json create mode 100644 source/Web/Frontend/src/app/app.can.activate.ts create mode 100644 source/Web/Frontend/src/app/app.component.ts create mode 100644 source/Web/Frontend/src/app/app.error.handler.ts create mode 100644 source/Web/Frontend/src/app/app.http.interceptor.ts create mode 100644 source/Web/Frontend/src/app/app.module.ts create mode 100644 source/Web/Frontend/src/app/app.routes.ts create mode 100644 source/Web/Frontend/src/app/components/button/button.component.html create mode 100644 source/Web/Frontend/src/app/components/button/button.component.ts create mode 100644 source/Web/Frontend/src/app/components/component.ts create mode 100644 source/Web/Frontend/src/app/components/file/file.component.html create mode 100644 source/Web/Frontend/src/app/components/file/file.component.ts create mode 100644 source/Web/Frontend/src/app/components/file/file.model.ts create mode 100644 source/Web/Frontend/src/app/components/file/file.service.ts create mode 100644 source/Web/Frontend/src/app/components/file/upload.model.ts create mode 100644 source/Web/Frontend/src/app/components/grid/filter/filter.model.ts create mode 100644 source/Web/Frontend/src/app/components/grid/filter/filters.model.ts create mode 100644 source/Web/Frontend/src/app/components/grid/grid-parameters.model.ts create mode 100644 source/Web/Frontend/src/app/components/grid/grid.model.ts create mode 100644 source/Web/Frontend/src/app/components/grid/grid.service.ts create mode 100644 source/Web/Frontend/src/app/components/grid/order/order.component.html create mode 100644 source/Web/Frontend/src/app/components/grid/order/order.component.ts create mode 100644 source/Web/Frontend/src/app/components/grid/order/order.model.ts create mode 100644 source/Web/Frontend/src/app/components/grid/page/page.component.html create mode 100644 source/Web/Frontend/src/app/components/grid/page/page.component.ts create mode 100644 source/Web/Frontend/src/app/components/grid/page/page.model.ts create mode 100644 source/Web/Frontend/src/app/components/input/input.component.html create mode 100644 source/Web/Frontend/src/app/components/input/input.component.ts create mode 100644 source/Web/Frontend/src/app/components/input/password.input.component.ts create mode 100644 source/Web/Frontend/src/app/components/input/text.input.component.ts create mode 100644 source/Web/Frontend/src/app/components/label/label.component.html create mode 100644 source/Web/Frontend/src/app/components/label/label.component.ts create mode 100644 source/Web/Frontend/src/app/components/select/comment.select.component.ts create mode 100644 source/Web/Frontend/src/app/components/select/option.model.ts create mode 100644 source/Web/Frontend/src/app/components/select/post.select.component.ts create mode 100644 source/Web/Frontend/src/app/components/select/select.component.html create mode 100644 source/Web/Frontend/src/app/components/select/select.component.ts create mode 100644 source/Web/Frontend/src/app/components/select/user.select.component.ts create mode 100644 source/Web/Frontend/src/app/layouts/footer/footer.component.html create mode 100644 source/Web/Frontend/src/app/layouts/footer/footer.component.ts create mode 100644 source/Web/Frontend/src/app/layouts/header/header.component.html create mode 100644 source/Web/Frontend/src/app/layouts/header/header.component.ts create mode 100644 source/Web/Frontend/src/app/layouts/layout-nav/layout-nav.component.html create mode 100644 source/Web/Frontend/src/app/layouts/layout-nav/layout-nav.component.ts create mode 100644 source/Web/Frontend/src/app/layouts/layout/layout.component.html create mode 100644 source/Web/Frontend/src/app/layouts/layout/layout.component.ts create mode 100644 source/Web/Frontend/src/app/layouts/nav/nav.component.html create mode 100644 source/Web/Frontend/src/app/layouts/nav/nav.component.ts create mode 100644 source/Web/Frontend/src/app/models/auth.model.ts create mode 100644 source/Web/Frontend/src/app/models/user.model.ts create mode 100644 source/Web/Frontend/src/app/pages/auth/auth.component.html create mode 100644 source/Web/Frontend/src/app/pages/auth/auth.component.ts create mode 100644 source/Web/Frontend/src/app/pages/files/files.component.html create mode 100644 source/Web/Frontend/src/app/pages/files/files.component.ts create mode 100644 source/Web/Frontend/src/app/pages/form/form.component.html create mode 100644 source/Web/Frontend/src/app/pages/form/form.component.ts create mode 100644 source/Web/Frontend/src/app/pages/home/home.component.html create mode 100644 source/Web/Frontend/src/app/pages/home/home.component.ts create mode 100644 source/Web/Frontend/src/app/pages/list/grid/grid.component.html create mode 100644 source/Web/Frontend/src/app/pages/list/grid/grid.component.ts create mode 100644 source/Web/Frontend/src/app/pages/list/list.component.html create mode 100644 source/Web/Frontend/src/app/pages/list/list.component.ts create mode 100644 source/Web/Frontend/src/app/services/auth.service.ts create mode 100644 source/Web/Frontend/src/app/services/modal.service.ts create mode 100644 source/Web/Frontend/src/app/services/user.service.ts create mode 100644 source/Web/Frontend/src/app/settings/settings.model.ts create mode 100644 source/Web/Frontend/src/app/settings/settings.service.ts create mode 100644 source/Web/Frontend/src/assets/settings.json create mode 100644 source/Web/Frontend/src/favicon.ico create mode 100644 source/Web/Frontend/src/index.html create mode 100644 source/Web/Frontend/src/main.ts create mode 100644 source/Web/Frontend/src/styles/style.scss create mode 100644 source/Web/Frontend/tsconfig.json create mode 100644 source/Web/Program.cs create mode 100644 source/Web/Properties/launchSettings.json create mode 100644 source/global.json diff --git a/.docker/.dockerignore b/.docker/.dockerignore new file mode 100644 index 00000000..0560cd01 --- /dev/null +++ b/.docker/.dockerignore @@ -0,0 +1,19 @@ +**/*.bat +**/*.log +**/*.md +**/*.yml +**/.dockerignore +**/.editorconfig +**/.git +**/.gitattributes +**/.github +**/.gitignore +**/.vs +**/.vscode +**/bin +**/dist +**/dockerfile +**/node_modules +**/obj +**/package-lock.json +**/postman.json diff --git a/.docker/docker-compose.yaml b/.docker/docker-compose.yaml new file mode 100644 index 00000000..0fc87166 --- /dev/null +++ b/.docker/docker-compose.yaml @@ -0,0 +1,39 @@ +version: "3.8" +services: + web: + image: architecture/web + container_name: architecture_web + restart: always + build: + context: .. + dockerfile: ./.docker/dockerfile + environment: + - Authentication__Schemes__Bearer__SigningKeys__1__Issuer=Issuer + - Authentication__Schemes__Bearer__SigningKeys__1__Value=E7F87FB927DB404E9F027E0826AFF62B + - Authentication__Schemes__Bearer__ValidAudience=Audience + - Authentication__Schemes__Bearer__ValidIssuer=Issuer + - ConnectionStrings__Context=Server=architecture_database;Database=Database;User Id=sa;Password=P4ssW0rd!;TrustServerCertificate=true; + - Serilog__WriteTo__1__Args__path=/app/logs/ + depends_on: + - database + networks: + - network + ports: + - 8090:80 + database: + image: mcr.microsoft.com/mssql/server + container_name: architecture_database + restart: always + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=P4ssW0rd! + networks: + - network + ports: + - 1433:1433 + volumes: + - database:/var/opt/mssql +networks: + network: +volumes: + database: diff --git a/.docker/dockerfile b/.docker/dockerfile new file mode 100644 index 00000000..7b73e89f --- /dev/null +++ b/.docker/dockerfile @@ -0,0 +1,21 @@ +# DotNet +FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS dotnet +COPY source ./source +RUN dotnet publish ./source/Web/Architecture.Web.csproj --configuration Release --output /dist + +# Angular +FROM node:20-alpine AS angular +WORKDIR ./source/Web/Frontend +COPY source/Web/Frontend/package.json . +RUN npm run restore +COPY source/Web/Frontend . +RUN npm run publish + +# Runtime +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine +RUN apk add --no-cache icu-libs +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false +WORKDIR /app +COPY --from=dotnet /dist . +COPY --from=angular /source/Web/Frontend/dist ./wwwroot +ENTRYPOINT ["dotnet", "Architecture.Web.dll"] diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..de197596 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..254d1f38 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.cs diff=csharp diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..a33a37b3 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,44 @@ +name: build +on: + push: + branches: [main] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: DotNet Setup + uses: actions/setup-dotnet@v4 + with: + global-json-file: source/global.json + + - name: DotNet Publish + run: dotnet publish source/Web --configuration Release --output web + + - name: Node Setup + uses: actions/setup-node@v4 + with: + node-version: 20 + check-latest: true + + - name: Node Publish + run: | + cd source/Web/Frontend + npm run restore + npm run publish + + - name: Artifact Prepare + run: | + mkdir web/wwwroot + rm --force --recursive web/*.pdb + rm --force --recursive web/Frontend/* + mv --force source/Web/Frontend/dist/* web/wwwroot + jq '.api = "https://github.com"' web/wwwroot/assets/settings.json > tmp && mv tmp web/wwwroot/assets/settings.json + + - name: Artifact Upload + uses: actions/upload-artifact@v4 + with: + name: web + path: web diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..bb99e2d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*.bat +*.log +*.pdb +*.suo +*.tmp +*.user +.angular +.vs +[Bb]in +[Dd]ebug +[Ll]og +[Oo]bj +[Rr]elease +[Rr]eleases +[Tt]est[Rr]esults +dist +node_modules +package-lock.json diff --git a/license.md b/license.md new file mode 100644 index 00000000..1f95d26c --- /dev/null +++ b/license.md @@ -0,0 +1,5 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/postman.json b/postman.json new file mode 100644 index 00000000..c8ea3e00 --- /dev/null +++ b/postman.json @@ -0,0 +1 @@ +{"version":1,"collections":[{"id":"33c5f635-8255-4611-80f9-d118f46a3e79","uid":"0-33c5f635-8255-4611-80f9-d118f46a3e79","name":"Architecture","description":null,"auth":null,"events":null,"variables":[],"order":[],"folders_order":["81c988e6-322d-477b-82fb-a2311d46f5ee","5c1f304b-c68c-460f-b2c5-7640506303e4"],"protocolProfileBehavior":{},"createdAt":"2022-12-22T16:25:55.910Z","folders":[{"id":"81c988e6-322d-477b-82fb-a2311d46f5ee","uid":"0-81c988e6-322d-477b-82fb-a2311d46f5ee","name":"Auths","description":null,"auth":null,"events":null,"collection":"33c5f635-8255-4611-80f9-d118f46a3e79","folder":null,"order":["e8086da9-b94a-4237-807d-5356339920e1"],"folders_order":[],"owner":"0","protocolProfileBehavior":{},"createdAt":"2022-12-22T16:26:15.867Z","collectionId":"33c5f635-8255-4611-80f9-d118f46a3e79","folderId":"81c988e6-322d-477b-82fb-a2311d46f5ee"},{"id":"5c1f304b-c68c-460f-b2c5-7640506303e4","uid":"0-5c1f304b-c68c-460f-b2c5-7640506303e4","name":"Users","description":null,"auth":null,"events":null,"collection":"33c5f635-8255-4611-80f9-d118f46a3e79","folder":null,"order":["65930fac-cab7-4ad6-aa4e-739e6eae1341","ba37094c-077a-45d3-8e49-8a4e178104d2","00b49d78-b89c-4860-9821-907686457d1f","79cb2f42-5350-4ea6-81dc-840d68e0205d","05f84113-b9e0-4d73-aa46-67c9e2dd012e","5536ad2b-492e-457e-8b2b-c395935019a8","331a8da3-4eb7-44ec-a1d2-05736e5b8ffe"],"folders_order":[],"owner":"0","protocolProfileBehavior":{},"createdAt":"2022-12-22T16:26:22.305Z","collectionId":"33c5f635-8255-4611-80f9-d118f46a3e79","folderId":"5c1f304b-c68c-460f-b2c5-7640506303e4"}],"requests":[{"id":"00b49d78-b89c-4860-9821-907686457d1f","uid":"0-00b49d78-b89c-4860-9821-907686457d1f","name":"Grid","url":"{{url}}/users/grid?page.index=1&page.size=2&order.ascending=false&order.property=name","description":null,"data":null,"dataOptions":{"raw":{"language":"json"}},"dataMode":null,"headerData":[{"key":"Authorization","value":"Bearer {{token}}","description":"","type":"default","enabled":true}],"method":"GET","pathVariableData":[],"queryParams":[{"key":"page.index","value":"1","equals":true,"description":null,"enabled":true},{"key":"page.size","value":"2","equals":true,"description":null,"enabled":true},{"key":"order.ascending","value":"false","equals":true,"description":null,"enabled":true},{"key":"order.property","value":"name","equals":true,"description":null,"enabled":true}],"auth":null,"events":[{"listen":"test","script":{"id":"93bcbfda-357b-42bb-b01c-868e9bceb6cb","exec":[""],"type":"text/javascript"}}],"folder":"5c1f304b-c68c-460f-b2c5-7640506303e4","responses_order":[],"preRequestScript":null,"tests":null,"currentHelper":null,"helperAttributes":null,"collectionId":"33c5f635-8255-4611-80f9-d118f46a3e79","headers":"Authorization: Bearer {{token}}\n","pathVariables":{}},{"id":"05f84113-b9e0-4d73-aa46-67c9e2dd012e","uid":"0-05f84113-b9e0-4d73-aa46-67c9e2dd012e","name":"Inactivate","url":"{{url}}/users/2/inactivate","description":null,"data":null,"dataOptions":{"raw":{"language":"json"}},"dataMode":null,"headerData":[{"key":"Authorization","value":"Bearer {{token}}","description":"","type":"default","enabled":true}],"method":"PATCH","pathVariableData":[],"queryParams":[],"auth":null,"events":[{"listen":"test","script":{"id":"50a8236f-9914-483d-bfad-f60e4aaa3123","exec":[""],"type":"text/javascript"}}],"folder":"5c1f304b-c68c-460f-b2c5-7640506303e4","responses_order":[],"preRequestScript":null,"tests":null,"currentHelper":null,"helperAttributes":null,"collectionId":"33c5f635-8255-4611-80f9-d118f46a3e79","headers":"Authorization: Bearer {{token}}\n","pathVariables":{}},{"id":"331a8da3-4eb7-44ec-a1d2-05736e5b8ffe","uid":"0-331a8da3-4eb7-44ec-a1d2-05736e5b8ffe","name":"Delete","url":"{{url}}/users/2","description":null,"data":null,"dataOptions":{"raw":{"language":"json"}},"dataMode":null,"headerData":[{"key":"Authorization","value":"Bearer {{token}}","description":"","type":"default","enabled":true}],"method":"DELETE","pathVariableData":[],"queryParams":[],"auth":null,"events":[{"listen":"test","script":{"id":"b0a40e44-b5ad-430c-99dd-1ceed2707cf3","exec":[""],"type":"text/javascript"}}],"folder":"5c1f304b-c68c-460f-b2c5-7640506303e4","responses_order":[],"preRequestScript":null,"tests":null,"currentHelper":null,"helperAttributes":null,"collectionId":"33c5f635-8255-4611-80f9-d118f46a3e79","headers":"Authorization: Bearer {{token}}\n","pathVariables":{}},{"id":"5536ad2b-492e-457e-8b2b-c395935019a8","uid":"0-5536ad2b-492e-457e-8b2b-c395935019a8","name":"Update","url":"{{url}}/users/2","description":null,"data":[],"dataOptions":{"raw":{"language":"json"}},"dataMode":"raw","headerData":[{"key":"Authorization","value":"Bearer {{token}}","description":"","type":"default","enabled":true}],"method":"PUT","pathVariableData":[],"queryParams":[],"auth":null,"events":[{"listen":"test","script":{"id":"dc19629f-14c9-45c7-93f3-768b3b127134","exec":[""],"type":"text/javascript"}}],"folder":"5c1f304b-c68c-460f-b2c5-7640506303e4","responses_order":[],"preRequestScript":null,"tests":null,"currentHelper":null,"helperAttributes":null,"collectionId":"33c5f635-8255-4611-80f9-d118f46a3e79","rawModeData":"{\r\n \"id\": 2,\r\n \"name\": \"{{$randomFirstName}}\",\r\n \"email\": \"{{$randomEmail}}\"\r\n}","headers":"Authorization: Bearer {{token}}\n","pathVariables":{}},{"id":"65930fac-cab7-4ad6-aa4e-739e6eae1341","uid":"0-65930fac-cab7-4ad6-aa4e-739e6eae1341","name":"Add","url":"{{url}}/users","description":null,"data":[],"dataOptions":{"raw":{"language":"json"}},"dataMode":"raw","headerData":[{"key":"Authorization","value":"Bearer {{token}}","description":"","type":"default","enabled":true}],"method":"POST","pathVariableData":[],"queryParams":[],"auth":null,"events":[{"listen":"test","script":{"id":"8eb69cd7-1039-4975-a6a8-d3d133b62058","exec":[""],"type":"text/javascript"}}],"folder":"5c1f304b-c68c-460f-b2c5-7640506303e4","responses_order":[],"preRequestScript":null,"tests":null,"currentHelper":null,"helperAttributes":null,"collectionId":"33c5f635-8255-4611-80f9-d118f46a3e79","rawModeData":"{\r\n \"name\": \"{{$randomFirstName}}\",\r\n \"email\": \"{{$randomEmail}}\",\r\n \"login\": \"{{$randomUserName}}\",\r\n \"password\": \"{{$randomPassword}}\"\r\n}","headers":"Authorization: Bearer {{token}}\n","pathVariables":{}},{"id":"79cb2f42-5350-4ea6-81dc-840d68e0205d","uid":"0-79cb2f42-5350-4ea6-81dc-840d68e0205d","name":"Get","url":"{{url}}/users/1","description":null,"data":null,"dataOptions":{"raw":{"language":"json"}},"dataMode":null,"headerData":[{"key":"Authorization","value":"Bearer {{token}}","description":"","type":"default","enabled":true}],"method":"GET","pathVariableData":[],"queryParams":[],"auth":null,"events":[{"listen":"test","script":{"id":"c082a4b7-d864-410a-8429-faafda68a487","exec":[""],"type":"text/javascript"}}],"folder":"5c1f304b-c68c-460f-b2c5-7640506303e4","responses_order":[],"preRequestScript":null,"tests":null,"currentHelper":null,"helperAttributes":null,"collectionId":"33c5f635-8255-4611-80f9-d118f46a3e79","headers":"Authorization: Bearer {{token}}\n","pathVariables":{}},{"id":"ba37094c-077a-45d3-8e49-8a4e178104d2","uid":"0-ba37094c-077a-45d3-8e49-8a4e178104d2","name":"List","url":"{{url}}/users","description":null,"data":null,"dataOptions":{"raw":{"language":"json"}},"dataMode":null,"headerData":[{"key":"Authorization","value":"Bearer {{token}}","description":"","type":"default","enabled":true}],"method":"GET","pathVariableData":[],"queryParams":[],"auth":null,"events":[{"listen":"test","script":{"id":"dcd82279-283f-4602-9d96-cf3157f68229","exec":[""],"type":"text/javascript"}}],"folder":"5c1f304b-c68c-460f-b2c5-7640506303e4","responses_order":[],"preRequestScript":null,"tests":null,"currentHelper":null,"helperAttributes":null,"collectionId":"33c5f635-8255-4611-80f9-d118f46a3e79","headers":"Authorization: Bearer {{token}}\n","pathVariables":{}},{"id":"e8086da9-b94a-4237-807d-5356339920e1","uid":"0-e8086da9-b94a-4237-807d-5356339920e1","name":"Auth","url":"{{url}}/auths","description":null,"data":[],"dataOptions":{"raw":{"language":"json"}},"dataMode":"raw","headerData":[],"method":"POST","pathVariableData":[],"queryParams":[],"auth":null,"events":[{"listen":"test","script":{"id":"cd2620be-f3c3-4df4-808c-ab890f2e47f0","exec":["pm.environment.set(\"token\", pm.response.json().token);"],"type":"text/javascript"}}],"folder":"81c988e6-322d-477b-82fb-a2311d46f5ee","responses_order":[],"preRequestScript":null,"tests":"pm.environment.set(\"token\", pm.response.json().token);","currentHelper":null,"helperAttributes":null,"collectionId":"33c5f635-8255-4611-80f9-d118f46a3e79","rawModeData":"{\r\n \"login\": \"admin\",\r\n \"password\": \"admin\"\r\n}","headers":"","pathVariables":{}}]}],"environments":[{"id":"71dc994b-6187-46e5-a3af-0052b162cbaa","name":"Local","values":[{"key":"url","value":"https://localhost:8090","type":"default","enabled":true},{"key":"token","value":"","type":"default","enabled":true}]},{"id":"bf00f882-45c1-4705-974f-fcfceda9c039","name":"My Workspace - globals","values":[]}],"headerPresets":[],"globals":[]} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 00000000..92d58af3 --- /dev/null +++ b/readme.md @@ -0,0 +1,242 @@ +# ARCHITECTURE + +![](https://github.com/rafaelfgx/Architecture/actions/workflows/build.yaml/badge.svg) + +This project is an example of architecture using new technologies and best practices. + +The goal is to learn and share knowledge and use it as reference for new projects. + +## PRINCIPLES and PATTERNS + +* Clean Architecture +* Clean Code +* SOLID Principles +* KISS Principle +* DRY Principle +* Fail Fast Principle +* Common Closure Principle +* Common Reuse Principle +* Acyclic Dependencies Principle +* Mediator Pattern +* Result Pattern +* Folder-by-Feature Structure +* Separation of Concerns + +## BENEFITS + +* Simple and evolutionary architecture. +* Standardized and centralized flow for validation, log, security, return, etc. +* Avoid cyclical references. +* Avoid unnecessary dependency injection. +* Segregation by feature instead of technical type. +* Single responsibility for each request and response. +* Simplicity of unit testing. + +## TECHNOLOGIES + +* [.NET](https://dotnet.microsoft.com/download) +* [ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core) +* [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core) +* [C#](https://docs.microsoft.com/en-us/dotnet/csharp) +* [Angular](https://angular.io/docs) +* [UIkit](https://getuikit.com/docs/introduction) + +## RUN + +
+Command Line + +#### Prerequisites + +* [.NET SDK](https://dotnet.microsoft.com/download/dotnet) +* [SQL Server](https://go.microsoft.com/fwlink/?linkid=866662) +* [Node](https://nodejs.org) +* [Angular CLI](https://cli.angular.io) + +#### Steps + +1. Open directory **source\Web\Frontend** in command line and execute **npm i**. +2. Open directory **source\Web** in command line and execute **dotnet run**. +3. Open . + +
+ +
+Visual Studio Code + +#### Prerequisites + +* [.NET SDK](https://dotnet.microsoft.com/download/dotnet) +* [SQL Server](https://go.microsoft.com/fwlink/?linkid=866662) +* [Node](https://nodejs.org) +* [Angular CLI](https://cli.angular.io) +* [Visual Studio Code](https://code.visualstudio.com) +* [C# Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp) + +#### Steps + +1. Open directory **source\Web\Frontend** in command line and execute **npm i**. +2. Open **source** directory in Visual Studio Code. +3. Press **F5**. + +
+ +
+Visual Studio + +#### Prerequisites + +* [Visual Studio](https://visualstudio.microsoft.com) +* [Node](https://nodejs.org) +* [Angular CLI](https://cli.angular.io) + +#### Steps + +1. Open directory **source\Web\Frontend** in command line and execute **npm i**. +2. Open **source\Architecture.sln** in Visual Studio. +3. Set **Architecture.Web** as startup project. +4. Press **F5**. + +
+ +
+Docker + +#### Prerequisites + +* [Docker](https://www.docker.com/get-started) + +#### Steps + +1. Execute **docker compose up --build -d** in docker directory. +2. Open . + +
+ +## PACKAGES + +**Source:** [https://github.com/rafaelfgx/DotNetCore](https://github.com/rafaelfgx/DotNetCore) + +**Published:** [https://www.nuget.org/profiles/rafaelfgx](https://www.nuget.org/profiles/rafaelfgx) + +## LAYERS + +**Web:** Frontend and Backend. + +**Application:** Flow control. + +**Domain:** Business rules and domain logic. + +**Model:** Data transfer objects. + +**Database:** Data persistence. + +## WEB + +### FRONTEND + +### Service + +It is the interface between frontend and backend and has logic that does not belong in components. + +### Guard + +It validates if a route can be activated. + +### ErrorHandler + +It provides a hook for centralized exception handling. + +### HttpInterceptor + +It intercepts and handles an HttpRequest or HttpResponse. + +### BACKEND + +### Controller + +It has no any logic, business rules or dependencies other than mediator. + +## APPLICATION + +It has only business flow, not business rules. + +### Request + +It has properties representing the request. + +### Request Validator + +It has rules for validating the request. + +### Response + +It has properties representing the response. + +### Handler + +It is responsible for the business flow and processing a request to return a response. + +It call factories, repositories, unit of work, services or mediator, but it has no business rules. + +### Factory + +It creates a complex object. + +Any change to object affects compile time rather than runtime. + +## DOMAIN + +It has no any references to any layer. + +It has aggregates, entities, value objects and services. + +### Aggregate + +It defines a consistency boundary around one or more entities. + +The purpose is to model transactional invariants. + +One entity in an aggregate is the root, any other entities in the aggregate are children of the root. + +### Entity + +It has unique identity. Identity may span multiple bounded contexts and may endure beyond the lifetime. + +Changing properties is only allowed through internal business methods in the entity, not through direct access to the properties. + +### Value Object + +It has no identity and it is immutable. + +It is defined only by the values ​​of its properties. + +To update a value object, you must create a new instance to replace the old one. + +It can have methods that encapsulate domain logic, but these methods must have no side effects on the state. + +### Services + +It performs domain operations and business rules. + +It is stateless and has no operations that are not a part of an entity or value object. + +## MODEL + +It has properties to transport and return data. + +## DATABASE + +It encapsulates data persistence. + +### Context + +It configures the connection and represents the database. + +### Entity Configuration + +It configures the entity and its properties in the database. + +### Repository + +It inherits from the generic repository and only implements specific methods. \ No newline at end of file diff --git a/source/.vscode/launch.json b/source/.vscode/launch.json new file mode 100644 index 00000000..04c45803 --- /dev/null +++ b/source/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Web", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/Web/bin/Debug/net8.0/Architecture.Web.dll", + "cwd": "${workspaceFolder}/Web", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + } + ] +} diff --git a/source/.vscode/tasks.json b/source/.vscode/tasks.json new file mode 100644 index 00000000..fc4d24dd --- /dev/null +++ b/source/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Web/Architecture.Web.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Web/Architecture.Web.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/Web/Architecture.Web.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/source/Application/Architecture.Application.csproj b/source/Application/Architecture.Application.csproj new file mode 100644 index 00000000..f13167b5 --- /dev/null +++ b/source/Application/Architecture.Application.csproj @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/Application/Auth/AuthHandler.cs b/source/Application/Auth/AuthHandler.cs new file mode 100644 index 00000000..4570ef76 --- /dev/null +++ b/source/Application/Auth/AuthHandler.cs @@ -0,0 +1,42 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record AuthHandler : IHandler +{ + private readonly IAuthRepository _authRepository; + private readonly IHashService _hashService; + private readonly IJwtService _jwtService; + private readonly IStringLocalizer _stringLocalizer; + + public AuthHandler + ( + IAuthRepository authRepository, + IHashService hashService, + IJwtService jwtService, + IStringLocalizer stringLocalizer + ) + { + _authRepository = authRepository; + _hashService = hashService; + _jwtService = jwtService; + _stringLocalizer = stringLocalizer; + } + + public async Task> HandleAsync(AuthRequest request) + { + var unauthorizedResult = new Result(Unauthorized, _stringLocalizer[nameof(Unauthorized)]); + + var auth = await _authRepository.GetByLoginAsync(request.Login); + + if (auth is null) return unauthorizedResult; + + if (!_hashService.Validate(auth.Password, request.Password, auth.Salt.ToString())) return unauthorizedResult; + + var token = _jwtService.Encode(auth.Id.ToString(), auth.Roles.ToArray()); + + var response = new AuthResponse(token); + + return new Result(OK, response); + } +} diff --git a/source/Application/Auth/AuthRequest.cs b/source/Application/Auth/AuthRequest.cs new file mode 100644 index 00000000..1aff68f3 --- /dev/null +++ b/source/Application/Auth/AuthRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record AuthRequest(string Login, string Password); diff --git a/source/Application/Auth/AuthRequestValidator.cs b/source/Application/Auth/AuthRequestValidator.cs new file mode 100644 index 00000000..d0c1954d --- /dev/null +++ b/source/Application/Auth/AuthRequestValidator.cs @@ -0,0 +1,10 @@ +namespace Architecture.Application; + +public sealed class AuthRequestValidator : AbstractValidator +{ + public AuthRequestValidator() + { + RuleFor(request => request.Login).Login(); + RuleFor(request => request.Password).Password(); + } +} diff --git a/source/Application/Auth/AuthResponse.cs b/source/Application/Auth/AuthResponse.cs new file mode 100644 index 00000000..b32d652f --- /dev/null +++ b/source/Application/Auth/AuthResponse.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record AuthResponse(string Token); diff --git a/source/Application/Example/Add/AddExampleHandler.cs b/source/Application/Example/Add/AddExampleHandler.cs new file mode 100644 index 00000000..9716cb13 --- /dev/null +++ b/source/Application/Example/Add/AddExampleHandler.cs @@ -0,0 +1,30 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record AddExampleHandler : IHandler +{ + private readonly IExampleRepository _exampleRepository; + private readonly IUnitOfWork _unitOfWork; + + public AddExampleHandler + ( + IExampleRepository exampleRepository, + IUnitOfWork unitOfWork + ) + { + _exampleRepository = exampleRepository; + _unitOfWork = unitOfWork; + } + + public async Task> HandleAsync(AddExampleRequest request) + { + var entity = new Example(request.Name); + + await _exampleRepository.AddAsync(entity); + + await _unitOfWork.SaveChangesAsync(); + + return new Result(Created, entity.Id); + } +} diff --git a/source/Application/Example/Add/AddExampleRequest.cs b/source/Application/Example/Add/AddExampleRequest.cs new file mode 100644 index 00000000..5adc154a --- /dev/null +++ b/source/Application/Example/Add/AddExampleRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record AddExampleRequest(string Name); diff --git a/source/Application/Example/Add/AddExampleRequestValidator.cs b/source/Application/Example/Add/AddExampleRequestValidator.cs new file mode 100644 index 00000000..7a8f569d --- /dev/null +++ b/source/Application/Example/Add/AddExampleRequestValidator.cs @@ -0,0 +1,6 @@ +namespace Architecture.Application; + +public sealed class AddExampleRequestValidator : AbstractValidator +{ + public AddExampleRequestValidator() => RuleFor(request => request.Name).Name(); +} diff --git a/source/Application/Example/Delete/DeleteExampleHandler.cs b/source/Application/Example/Delete/DeleteExampleHandler.cs new file mode 100644 index 00000000..f40d2567 --- /dev/null +++ b/source/Application/Example/Delete/DeleteExampleHandler.cs @@ -0,0 +1,28 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record DeleteExampleHandler : IHandler +{ + private readonly IExampleRepository _exampleRepository; + private readonly IUnitOfWork _unitOfWork; + + public DeleteExampleHandler + ( + IExampleRepository exampleRepository, + IUnitOfWork unitOfWork + ) + { + _exampleRepository = exampleRepository; + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(DeleteExampleRequest request) + { + await _exampleRepository.DeleteAsync(request.Id); + + await _unitOfWork.SaveChangesAsync(); + + return new Result(NoContent); + } +} diff --git a/source/Application/Example/Delete/DeleteExampleRequest.cs b/source/Application/Example/Delete/DeleteExampleRequest.cs new file mode 100644 index 00000000..db47a43c --- /dev/null +++ b/source/Application/Example/Delete/DeleteExampleRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record DeleteExampleRequest(long Id); diff --git a/source/Application/Example/Delete/DeleteExampleRequestValidator.cs b/source/Application/Example/Delete/DeleteExampleRequestValidator.cs new file mode 100644 index 00000000..ee6c237d --- /dev/null +++ b/source/Application/Example/Delete/DeleteExampleRequestValidator.cs @@ -0,0 +1,6 @@ +namespace Architecture.Application; + +public sealed class DeleteExampleRequestValidator : AbstractValidator +{ + public DeleteExampleRequestValidator() => RuleFor(request => request.Id).Id(); +} diff --git a/source/Application/Example/Get/GetExampleHandler.cs b/source/Application/Example/Get/GetExampleHandler.cs new file mode 100644 index 00000000..46385763 --- /dev/null +++ b/source/Application/Example/Get/GetExampleHandler.cs @@ -0,0 +1,17 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record GetExampleHandler : IHandler +{ + private readonly IExampleRepository _exampleRepository; + + public GetExampleHandler(IExampleRepository exampleRepository) => _exampleRepository = exampleRepository; + + public async Task> HandleAsync(GetExampleRequest request) + { + var model = await _exampleRepository.GetModelAsync(request.Id); + + return new Result(model is null ? NotFound : OK, model); + } +} diff --git a/source/Application/Example/Get/GetExampleRequest.cs b/source/Application/Example/Get/GetExampleRequest.cs new file mode 100644 index 00000000..281b22d2 --- /dev/null +++ b/source/Application/Example/Get/GetExampleRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record GetExampleRequest(long Id); diff --git a/source/Application/Example/Get/GetExampleRequestValidator.cs b/source/Application/Example/Get/GetExampleRequestValidator.cs new file mode 100644 index 00000000..104e1507 --- /dev/null +++ b/source/Application/Example/Get/GetExampleRequestValidator.cs @@ -0,0 +1,6 @@ +namespace Architecture.Application; + +public sealed class GetExampleRequestValidator : AbstractValidator +{ + public GetExampleRequestValidator() => RuleFor(request => request.Id).Id(); +} diff --git a/source/Application/Example/Grid/GridExampleHandler.cs b/source/Application/Example/Grid/GridExampleHandler.cs new file mode 100644 index 00000000..d91831d7 --- /dev/null +++ b/source/Application/Example/Grid/GridExampleHandler.cs @@ -0,0 +1,17 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record GridExampleHandler : IHandler> +{ + private readonly IExampleRepository _exampleRepository; + + public GridExampleHandler(IExampleRepository exampleRepository) => _exampleRepository = exampleRepository; + + public async Task>> HandleAsync(GridExampleRequest request) + { + var grid = await _exampleRepository.GridAsync(request); + + return new Result>(grid is null ? NotFound : OK, grid); + } +} diff --git a/source/Application/Example/Grid/GridExampleRequest.cs b/source/Application/Example/Grid/GridExampleRequest.cs new file mode 100644 index 00000000..7c9b6f72 --- /dev/null +++ b/source/Application/Example/Grid/GridExampleRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record GridExampleRequest : GridParameters; diff --git a/source/Application/Example/Grid/GridExampleRequestValidator.cs b/source/Application/Example/Grid/GridExampleRequestValidator.cs new file mode 100644 index 00000000..0d4c6cf1 --- /dev/null +++ b/source/Application/Example/Grid/GridExampleRequestValidator.cs @@ -0,0 +1,6 @@ +namespace Architecture.Application; + +public sealed class GridExampleRequestValidator : AbstractValidator +{ + public GridExampleRequestValidator() => RuleFor(request => request).Grid(); +} diff --git a/source/Application/Example/List/ListExampleHandler.cs b/source/Application/Example/List/ListExampleHandler.cs new file mode 100644 index 00000000..8b9cee80 --- /dev/null +++ b/source/Application/Example/List/ListExampleHandler.cs @@ -0,0 +1,17 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record ListExampleHandler : IHandler> +{ + private readonly IExampleRepository _exampleRepository; + + public ListExampleHandler(IExampleRepository exampleRepository) => _exampleRepository = exampleRepository; + + public async Task>> HandleAsync(ListExampleRequest request) + { + var list = await _exampleRepository.ListModelAsync(); + + return new Result>(list is null ? NotFound : OK, list); + } +} diff --git a/source/Application/Example/List/ListExampleRequest.cs b/source/Application/Example/List/ListExampleRequest.cs new file mode 100644 index 00000000..afd2e574 --- /dev/null +++ b/source/Application/Example/List/ListExampleRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record ListExampleRequest; diff --git a/source/Application/Example/Update/UpdateExampleHandler.cs b/source/Application/Example/Update/UpdateExampleHandler.cs new file mode 100644 index 00000000..ec3ba92e --- /dev/null +++ b/source/Application/Example/Update/UpdateExampleHandler.cs @@ -0,0 +1,30 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record UpdateExampleHandler : IHandler +{ + private readonly IExampleRepository _exampleRepository; + private readonly IUnitOfWork _unitOfWork; + + public UpdateExampleHandler + ( + IExampleRepository exampleRepository, + IUnitOfWork unitOfWork + ) + { + _exampleRepository = exampleRepository; + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(UpdateExampleRequest request) + { + var entity = new Example(request.Id, request.Name); + + await _exampleRepository.UpdateAsync(entity); + + await _unitOfWork.SaveChangesAsync(); + + return new Result(NoContent); + } +} diff --git a/source/Application/Example/Update/UpdateExampleRequest.cs b/source/Application/Example/Update/UpdateExampleRequest.cs new file mode 100644 index 00000000..3727fadc --- /dev/null +++ b/source/Application/Example/Update/UpdateExampleRequest.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Architecture.Application; + +public sealed record UpdateExampleRequest(string Name) +{ + [JsonIgnore] + public long Id { get; set; } +} diff --git a/source/Application/Example/Update/UpdateExampleRequestValidator.cs b/source/Application/Example/Update/UpdateExampleRequestValidator.cs new file mode 100644 index 00000000..a993378f --- /dev/null +++ b/source/Application/Example/Update/UpdateExampleRequestValidator.cs @@ -0,0 +1,10 @@ +namespace Architecture.Application; + +public sealed class UpdateExampleRequestValidator : AbstractValidator +{ + public UpdateExampleRequestValidator() + { + RuleFor(request => request.Id).Id(); + RuleFor(request => request.Name).Name(); + } +} diff --git a/source/Application/File/Add/AddFileHandler.cs b/source/Application/File/Add/AddFileHandler.cs new file mode 100644 index 00000000..bf3af36d --- /dev/null +++ b/source/Application/File/Add/AddFileHandler.cs @@ -0,0 +1,13 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record AddFileHandler : IHandler> +{ + public async Task>> HandleAsync(AddFileRequest request) + { + var files = await request.Files.SaveAsync("Files"); + + return new Result>(Created, files); + } +} diff --git a/source/Application/File/Add/AddFileRequest.cs b/source/Application/File/Add/AddFileRequest.cs new file mode 100644 index 00000000..9a4a2d19 --- /dev/null +++ b/source/Application/File/Add/AddFileRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record AddFileRequest(IEnumerable Files); diff --git a/source/Application/File/Add/AddFileRequestValidator.cs b/source/Application/File/Add/AddFileRequestValidator.cs new file mode 100644 index 00000000..6358bef4 --- /dev/null +++ b/source/Application/File/Add/AddFileRequestValidator.cs @@ -0,0 +1,6 @@ +namespace Architecture.Application; + +public sealed class AddFileRequestValidator : AbstractValidator +{ + public AddFileRequestValidator() => RuleFor(request => request.Files).Files(); +} diff --git a/source/Application/File/Get/GetFileHandler.cs b/source/Application/File/Get/GetFileHandler.cs new file mode 100644 index 00000000..9d3047c0 --- /dev/null +++ b/source/Application/File/Get/GetFileHandler.cs @@ -0,0 +1,13 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record GetFileHandler : IHandler +{ + public async Task> HandleAsync(GetFileRequest request) + { + var file = await BinaryFile.ReadAsync("Files", request.Id); + + return new Result(file is null ? NotFound : OK, file); + } +} diff --git a/source/Application/File/Get/GetFileRequest.cs b/source/Application/File/Get/GetFileRequest.cs new file mode 100644 index 00000000..83903c97 --- /dev/null +++ b/source/Application/File/Get/GetFileRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record GetFileRequest(Guid Id); diff --git a/source/Application/File/Get/GetFileRequestValidator.cs b/source/Application/File/Get/GetFileRequestValidator.cs new file mode 100644 index 00000000..004ae538 --- /dev/null +++ b/source/Application/File/Get/GetFileRequestValidator.cs @@ -0,0 +1,6 @@ +namespace Architecture.Application; + +public sealed class GetFileRequestValidator : AbstractValidator +{ + public GetFileRequestValidator() => RuleFor(request => request.Id).Guid(); +} diff --git a/source/Application/User/Add/AddUserHandler.cs b/source/Application/User/Add/AddUserHandler.cs new file mode 100644 index 00000000..d6e7713e --- /dev/null +++ b/source/Application/User/Add/AddUserHandler.cs @@ -0,0 +1,44 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record AddUserHandler : IHandler +{ + private readonly IAuthRepository _authRepository; + private readonly IHashService _hashService; + private readonly IUnitOfWork _unitOfWork; + private readonly IUserRepository _userRepository; + + public AddUserHandler + ( + IAuthRepository authRepository, + IHashService hashService, + IUnitOfWork unitOfWork, + IUserRepository userRepository + ) + { + _authRepository = authRepository; + _hashService = hashService; + _unitOfWork = unitOfWork; + _userRepository = userRepository; + } + + public async Task> HandleAsync(AddUserRequest request) + { + var user = new User(request.Name, request.Email); + + var auth = new Auth(request.Login, request.Password, user); + + var password = _hashService.Create(auth.Password, auth.Salt.ToString()); + + auth.UpdatePassword(password); + + await _userRepository.AddAsync(user); + + await _authRepository.AddAsync(auth); + + await _unitOfWork.SaveChangesAsync(); + + return new Result(Created, user.Id); + } +} diff --git a/source/Application/User/Add/AddUserRequest.cs b/source/Application/User/Add/AddUserRequest.cs new file mode 100644 index 00000000..89674b93 --- /dev/null +++ b/source/Application/User/Add/AddUserRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record AddUserRequest(string Name, string Email, string Login, string Password); diff --git a/source/Application/User/Add/AddUserRequestValidator.cs b/source/Application/User/Add/AddUserRequestValidator.cs new file mode 100644 index 00000000..c2103bf0 --- /dev/null +++ b/source/Application/User/Add/AddUserRequestValidator.cs @@ -0,0 +1,12 @@ +namespace Architecture.Application; + +public sealed class AddUserRequestValidator : AbstractValidator +{ + public AddUserRequestValidator() + { + RuleFor(request => request.Name).Name(); + RuleFor(request => request.Email).Email(); + RuleFor(request => request.Login).Login(); + RuleFor(request => request.Password).Password(); + } +} diff --git a/source/Application/User/Delete/DeleteUserHandler.cs b/source/Application/User/Delete/DeleteUserHandler.cs new file mode 100644 index 00000000..9d7bd42b --- /dev/null +++ b/source/Application/User/Delete/DeleteUserHandler.cs @@ -0,0 +1,33 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record DeleteUserHandler : IHandler +{ + private readonly IUnitOfWork _unitOfWork; + private readonly IAuthRepository _authRepository; + private readonly IUserRepository _userRepository; + + public DeleteUserHandler + ( + IUnitOfWork unitOfWork, + IAuthRepository authRepository, + IUserRepository userRepository + ) + { + _unitOfWork = unitOfWork; + _authRepository = authRepository; + _userRepository = userRepository; + } + + public async Task HandleAsync(DeleteUserRequest request) + { + await _authRepository.DeleteByUserIdAsync(request.Id); + + await _userRepository.DeleteAsync(request.Id); + + await _unitOfWork.SaveChangesAsync(); + + return new Result(NoContent); + } +} diff --git a/source/Application/User/Delete/DeleteUserRequest.cs b/source/Application/User/Delete/DeleteUserRequest.cs new file mode 100644 index 00000000..63ea8e9e --- /dev/null +++ b/source/Application/User/Delete/DeleteUserRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record DeleteUserRequest(long Id); diff --git a/source/Application/User/Delete/DeleteUserRequestValidator.cs b/source/Application/User/Delete/DeleteUserRequestValidator.cs new file mode 100644 index 00000000..9740046b --- /dev/null +++ b/source/Application/User/Delete/DeleteUserRequestValidator.cs @@ -0,0 +1,6 @@ +namespace Architecture.Application; + +public sealed class DeleteUserRequestValidator : AbstractValidator +{ + public DeleteUserRequestValidator() => RuleFor(request => request.Id).Id(); +} diff --git a/source/Application/User/Get/GetUserHandler.cs b/source/Application/User/Get/GetUserHandler.cs new file mode 100644 index 00000000..ae0ff727 --- /dev/null +++ b/source/Application/User/Get/GetUserHandler.cs @@ -0,0 +1,17 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record GetUserHandler : IHandler +{ + private readonly IUserRepository _userRepository; + + public GetUserHandler(IUserRepository userRepository) => _userRepository = userRepository; + + public async Task> HandleAsync(GetUserRequest request) + { + var user = await _userRepository.GetModelAsync(request.Id); + + return new Result(user is null ? NotFound : OK, user); + } +} diff --git a/source/Application/User/Get/GetUserRequest.cs b/source/Application/User/Get/GetUserRequest.cs new file mode 100644 index 00000000..3bd70570 --- /dev/null +++ b/source/Application/User/Get/GetUserRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record GetUserRequest(long Id); diff --git a/source/Application/User/Get/GetUserRequestValidator.cs b/source/Application/User/Get/GetUserRequestValidator.cs new file mode 100644 index 00000000..8e39c742 --- /dev/null +++ b/source/Application/User/Get/GetUserRequestValidator.cs @@ -0,0 +1,6 @@ +namespace Architecture.Application; + +public sealed class GetUserRequestValidator : AbstractValidator +{ + public GetUserRequestValidator() => RuleFor(request => request.Id).Id(); +} diff --git a/source/Application/User/Grid/GridUserHandler.cs b/source/Application/User/Grid/GridUserHandler.cs new file mode 100644 index 00000000..b17f9c86 --- /dev/null +++ b/source/Application/User/Grid/GridUserHandler.cs @@ -0,0 +1,17 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record GridUserHandler : IHandler> +{ + private readonly IUserRepository _userRepository; + + public GridUserHandler(IUserRepository userRepository) => _userRepository = userRepository; + + public async Task>> HandleAsync(GridUserRequest request) + { + var grid = await _userRepository.GridAsync(request); + + return new Result>(grid is null ? NotFound : OK, grid); + } +} diff --git a/source/Application/User/Grid/GridUserRequest.cs b/source/Application/User/Grid/GridUserRequest.cs new file mode 100644 index 00000000..f73ffc6e --- /dev/null +++ b/source/Application/User/Grid/GridUserRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record GridUserRequest : GridParameters; diff --git a/source/Application/User/Grid/GridUserRequestValidator.cs b/source/Application/User/Grid/GridUserRequestValidator.cs new file mode 100644 index 00000000..c7412b38 --- /dev/null +++ b/source/Application/User/Grid/GridUserRequestValidator.cs @@ -0,0 +1,6 @@ +namespace Architecture.Application; + +public sealed class GridUserRequestValidator : AbstractValidator +{ + public GridUserRequestValidator() => RuleFor(request => request).Grid(); +} diff --git a/source/Application/User/Inactivate/InactivateUserHandler.cs b/source/Application/User/Inactivate/InactivateUserHandler.cs new file mode 100644 index 00000000..130fc45d --- /dev/null +++ b/source/Application/User/Inactivate/InactivateUserHandler.cs @@ -0,0 +1,32 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record InactivateUserHandler : IHandler +{ + private readonly IUnitOfWork _unitOfWork; + private readonly IUserRepository _userRepository; + + public InactivateUserHandler + ( + IUnitOfWork unitOfWork, + IUserRepository userRepository + ) + { + _unitOfWork = unitOfWork; + _userRepository = userRepository; + } + + public async Task HandleAsync(InactivateUserRequest request) + { + var user = new User(request.Id); + + user.Inactivate(); + + await _userRepository.UpdatePartialAsync(new { user.Id, user.Status }); + + await _unitOfWork.SaveChangesAsync(); + + return new Result(NoContent); + } +} diff --git a/source/Application/User/Inactivate/InactivateUserRequest.cs b/source/Application/User/Inactivate/InactivateUserRequest.cs new file mode 100644 index 00000000..06d81632 --- /dev/null +++ b/source/Application/User/Inactivate/InactivateUserRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record InactivateUserRequest(long Id); diff --git a/source/Application/User/Inactivate/InactivateUserRequestValidator.cs b/source/Application/User/Inactivate/InactivateUserRequestValidator.cs new file mode 100644 index 00000000..5f5fff6d --- /dev/null +++ b/source/Application/User/Inactivate/InactivateUserRequestValidator.cs @@ -0,0 +1,6 @@ +namespace Architecture.Application; + +public sealed class InactivateUserRequestValidator : AbstractValidator +{ + public InactivateUserRequestValidator() => RuleFor(request => request.Id).Id(); +} diff --git a/source/Application/User/List/ListUserHandler.cs b/source/Application/User/List/ListUserHandler.cs new file mode 100644 index 00000000..7d60f69a --- /dev/null +++ b/source/Application/User/List/ListUserHandler.cs @@ -0,0 +1,17 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record ListUserHandler : IHandler> +{ + private readonly IUserRepository _userRepository; + + public ListUserHandler(IUserRepository userRepository) => _userRepository = userRepository; + + public async Task>> HandleAsync(ListUserRequest request) + { + var users = await _userRepository.ListModelAsync(); + + return new Result>(users is null ? NotFound : OK, users); + } +} diff --git a/source/Application/User/List/ListUserRequest.cs b/source/Application/User/List/ListUserRequest.cs new file mode 100644 index 00000000..9e02ef7e --- /dev/null +++ b/source/Application/User/List/ListUserRequest.cs @@ -0,0 +1,3 @@ +namespace Architecture.Application; + +public sealed record ListUserRequest; diff --git a/source/Application/User/Update/UpdateUserHandler.cs b/source/Application/User/Update/UpdateUserHandler.cs new file mode 100644 index 00000000..a59fbb0d --- /dev/null +++ b/source/Application/User/Update/UpdateUserHandler.cs @@ -0,0 +1,36 @@ +using static System.Net.HttpStatusCode; + +namespace Architecture.Application; + +public sealed record UpdateUserHandler : IHandler +{ + private readonly IUnitOfWork _unitOfWork; + private readonly IUserRepository _userRepository; + + public UpdateUserHandler + ( + IUnitOfWork unitOfWork, + IUserRepository userRepository + ) + { + _unitOfWork = unitOfWork; + _userRepository = userRepository; + } + + public async Task HandleAsync(UpdateUserRequest request) + { + var user = await _userRepository.GetAsync(request.Id); + + if (user is null) return new Result(NotFound); + + user.UpdateName(request.Name); + + user.UpdateEmail(request.Email); + + await _userRepository.UpdateAsync(user); + + await _unitOfWork.SaveChangesAsync(); + + return new Result(NoContent); + } +} diff --git a/source/Application/User/Update/UpdateUserRequest.cs b/source/Application/User/Update/UpdateUserRequest.cs new file mode 100644 index 00000000..f3fd373c --- /dev/null +++ b/source/Application/User/Update/UpdateUserRequest.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Architecture.Application; + +public sealed record UpdateUserRequest(string Name, string Email) +{ + [JsonIgnore] + public long Id { get; set; } +} diff --git a/source/Application/User/Update/UpdateUserRequestValidator.cs b/source/Application/User/Update/UpdateUserRequestValidator.cs new file mode 100644 index 00000000..88adad92 --- /dev/null +++ b/source/Application/User/Update/UpdateUserRequestValidator.cs @@ -0,0 +1,11 @@ +namespace Architecture.Application; + +public sealed class UpdateUserRequestValidator : AbstractValidator +{ + public UpdateUserRequestValidator() + { + RuleFor(request => request.Id).Id(); + RuleFor(request => request.Name).Name(); + RuleFor(request => request.Email).Email(); + } +} diff --git a/source/Application/Validators.cs b/source/Application/Validators.cs new file mode 100644 index 00000000..77de19f9 --- /dev/null +++ b/source/Application/Validators.cs @@ -0,0 +1,20 @@ +namespace Architecture.Application; + +public static class Validators +{ + public static IRuleBuilderOptions Email(this IRuleBuilder builder) => builder.NotEmpty().EmailAddress(); + + public static IRuleBuilderOptions> Files(this IRuleBuilder> builder) => builder.NotEmpty(); + + public static IRuleBuilderOptions Grid(this IRuleBuilder builder) => builder.NotEmpty(); + + public static IRuleBuilderOptions Guid(this IRuleBuilder builder) => builder.NotEmpty(); + + public static IRuleBuilderOptions Id(this IRuleBuilder builder) => builder.NotEmpty().GreaterThan(0); + + public static IRuleBuilderOptions Login(this IRuleBuilder builder) => builder.NotEmpty(); + + public static IRuleBuilderOptions Name(this IRuleBuilder builder) => builder.NotEmpty().MinimumLength(3); + + public static IRuleBuilderOptions Password(this IRuleBuilder builder) => builder.NotEmpty(); +} diff --git a/source/Architecture.sln b/source/Architecture.sln new file mode 100644 index 00000000..d8ca49f7 --- /dev/null +++ b/source/Architecture.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 17.2.32616.157 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Architecture.Application", "Application\Architecture.Application.csproj", "{E73DEC73-6251-4981-BB9B-D50896B80955}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Architecture.Database", "Database\Architecture.Database.csproj", "{10FB2153-D624-4971-A25D-A7ECC5D84DCD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Architecture.Domain", "Domain\Architecture.Domain.csproj", "{2CB1DC0F-7144-4707-8168-D19961C1814B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Architecture.Web", "Web\Architecture.Web.csproj", "{FEE1A75A-095E-44F6-B4A5-CA066F0338D6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Architecture.Model", "Model\Architecture.Model.csproj", "{9C097830-8BE9-4417-866B-CB9078CDD95E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E73DEC73-6251-4981-BB9B-D50896B80955}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E73DEC73-6251-4981-BB9B-D50896B80955}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E73DEC73-6251-4981-BB9B-D50896B80955}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E73DEC73-6251-4981-BB9B-D50896B80955}.Release|Any CPU.Build.0 = Release|Any CPU + {10FB2153-D624-4971-A25D-A7ECC5D84DCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10FB2153-D624-4971-A25D-A7ECC5D84DCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10FB2153-D624-4971-A25D-A7ECC5D84DCD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10FB2153-D624-4971-A25D-A7ECC5D84DCD}.Release|Any CPU.Build.0 = Release|Any CPU + {2CB1DC0F-7144-4707-8168-D19961C1814B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2CB1DC0F-7144-4707-8168-D19961C1814B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2CB1DC0F-7144-4707-8168-D19961C1814B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2CB1DC0F-7144-4707-8168-D19961C1814B}.Release|Any CPU.Build.0 = Release|Any CPU + {FEE1A75A-095E-44F6-B4A5-CA066F0338D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEE1A75A-095E-44F6-B4A5-CA066F0338D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEE1A75A-095E-44F6-B4A5-CA066F0338D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEE1A75A-095E-44F6-B4A5-CA066F0338D6}.Release|Any CPU.Build.0 = Release|Any CPU + {9C097830-8BE9-4417-866B-CB9078CDD95E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C097830-8BE9-4417-866B-CB9078CDD95E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C097830-8BE9-4417-866B-CB9078CDD95E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C097830-8BE9-4417-866B-CB9078CDD95E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A6271FF1-0DFA-4557-9E90-16FD5A74237F} + EndGlobalSection +EndGlobal diff --git a/source/Database/Architecture.Database.csproj b/source/Database/Architecture.Database.csproj new file mode 100644 index 00000000..0a175fea --- /dev/null +++ b/source/Database/Architecture.Database.csproj @@ -0,0 +1,28 @@ + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + true + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/Database/Auth/AuthConfiguration.cs b/source/Database/Auth/AuthConfiguration.cs new file mode 100644 index 00000000..9a8137d3 --- /dev/null +++ b/source/Database/Auth/AuthConfiguration.cs @@ -0,0 +1,29 @@ +namespace Architecture.Database; + +public sealed class AuthConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Auth), nameof(Auth)); + + builder.HasKey(entity => entity.Id); + + builder.Property(entity => entity.Id).ValueGeneratedOnAdd().IsRequired(); + + builder.Property(entity => entity.Login).HasMaxLength(100).IsRequired(); + + builder.Property(entity => entity.Password).HasMaxLength(1000).IsRequired(); + + builder.Property(entity => entity.Salt).HasMaxLength(1000).IsRequired(); + + builder.Property(entity => entity.Roles).IsRequired(); + + builder.HasOne(entity => entity.User).WithOne().HasForeignKey("UserId").IsRequired(); + + builder.HasIndex(entity => entity.Login).IsUnique(); + + builder.HasIndex(entity => entity.Salt).IsUnique(); + + builder.HasIndex("UserId").IsUnique(); + } +} diff --git a/source/Database/Auth/AuthRepository.cs b/source/Database/Auth/AuthRepository.cs new file mode 100644 index 00000000..3f7628bc --- /dev/null +++ b/source/Database/Auth/AuthRepository.cs @@ -0,0 +1,10 @@ +namespace Architecture.Database; + +public sealed class AuthRepository : EFRepository, IAuthRepository +{ + public AuthRepository(Context context) : base(context) { } + + public Task DeleteByUserIdAsync(long userId) => DeleteAsync(entity => entity.User.Id == userId); + + public Task GetByLoginAsync(string login) => Queryable.SingleOrDefaultAsync(entity => entity.Login == login); +} diff --git a/source/Database/Auth/IAuthRepository.cs b/source/Database/Auth/IAuthRepository.cs new file mode 100644 index 00000000..4b8e92e2 --- /dev/null +++ b/source/Database/Auth/IAuthRepository.cs @@ -0,0 +1,8 @@ +namespace Architecture.Database; + +public interface IAuthRepository : IRepository +{ + Task DeleteByUserIdAsync(long userId); + + Task GetByLoginAsync(string login); +} diff --git a/source/Database/Context/Context.cs b/source/Database/Context/Context.cs new file mode 100644 index 00000000..18b4dd8d --- /dev/null +++ b/source/Database/Context/Context.cs @@ -0,0 +1,8 @@ +namespace Architecture.Database; + +public sealed class Context : DbContext +{ + public Context(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) => builder.ApplyConfigurationsFromAssembly(typeof(Context).Assembly).Seed(); +} diff --git a/source/Database/Context/ContextFactory.cs b/source/Database/Context/ContextFactory.cs new file mode 100644 index 00000000..4dd3f15d --- /dev/null +++ b/source/Database/Context/ContextFactory.cs @@ -0,0 +1,11 @@ +namespace Architecture.Database; + +public sealed class ContextFactory : IDesignTimeDbContextFactory +{ + public Context CreateDbContext(string[] args) + { + const string connectionString = "Server=(localdb)\\MSSQLLocalDB;Database=ContextFactory;"; + + return new Context(new DbContextOptionsBuilder().UseSqlServer(connectionString).Options); + } +} diff --git a/source/Database/Context/ContextSeed.cs b/source/Database/Context/ContextSeed.cs new file mode 100644 index 00000000..48e01bcb --- /dev/null +++ b/source/Database/Context/ContextSeed.cs @@ -0,0 +1,27 @@ +namespace Architecture.Database; + +public static class ContextSeed +{ + public static void Seed(this ModelBuilder builder) => builder.SeedUsers(); + + private static void SeedUsers(this ModelBuilder builder) + { + builder.Entity(entity => entity.HasData(new + { + Id = 1L, + Name = "Administrator", + Email = "administrator@administrator.com", + Status = Status.Active + })); + + builder.Entity(entity => entity.HasData(new + { + Id = 1L, + Login = "admin", + Password = "O34uMN1Vho2IYcSM7nlXEqn57RZ8VEUsJwH++sFr0i3MSHJVx8J3PQGjhLR3s5i4l0XWUnCnymQ/EbRmzvLy8uMWREZu7vZI+BqebjAl5upYKMMQvlEcBeyLcRRTTBpYpv80m/YCZQmpig4XFVfIViLLZY/Kr5gBN5dkQf25rK+u88gtSXAyPDkW+hVS+dW4AmxrnaNFZks0Zzcd5xlb12wcF/q96cc4htHFzvOH9jtN98N5EBIXSvdUVnFc9kBuRTVytATZA7gITbs//hkxvNQ3eody5U9hjdH6D+AP0vVt5unZlTZ+gInn8Ze7AC5o6mn0Y3ylGO1CBJSHU9c/BcFY9oknn+XYk9ySCoDGctMqDbvOBcvSTBkKcrCzev2KnX7xYmC3yNz1Q5oPVKgnq4mc1iuldMlCxse/IDGMJB2FRdTCLV5KNS4IZ1GB+dw3tMvcEEtmXmgT2zKN5+kUkOxhlv5g1ZLgXzWjVJeKb5uZpsn3WK5kt8T+kzMoxHd5i8HxsU2uvy9GopxAnaV0WNsBPqTGkRllSxARl4ZN3hJqUla553RT49tJxbs+N03OmkYhjq+L0aKJ1AC+7G+rdjegiAQZB+3mdE7a2Pne2kYtpMoCmNMKdm9jOOVpfXJqZMQul9ltJSlAY6LPrHFUB3mw61JBNzx+sZgYN29GfDY=", + Salt = new Guid("79005744-e69a-4b09-996b-08fe0b70cbb9"), + Roles = Roles.User | Roles.Admin, + UserId = 1L + })); + } +} diff --git a/source/Database/Example/ExampleConfiguration.cs b/source/Database/Example/ExampleConfiguration.cs new file mode 100644 index 00000000..5f52da45 --- /dev/null +++ b/source/Database/Example/ExampleConfiguration.cs @@ -0,0 +1,15 @@ +namespace Architecture.Database; + +public sealed class ExampleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(Example), nameof(Example)); + + builder.HasKey(entity => entity.Id); + + builder.Property(entity => entity.Id).ValueGeneratedOnAdd().IsRequired(); + + builder.Property(entity => entity.Name).HasMaxLength(250).IsRequired(); + } +} diff --git a/source/Database/Example/ExampleRepository.cs b/source/Database/Example/ExampleRepository.cs new file mode 100644 index 00000000..8696ba7b --- /dev/null +++ b/source/Database/Example/ExampleRepository.cs @@ -0,0 +1,14 @@ +namespace Architecture.Database; + +public sealed class ExampleRepository : EFRepository, IExampleRepository +{ + public ExampleRepository(Context context) : base(context) { } + + public static Expression> Model => entity => new ExampleModel { Id = entity.Id, Name = entity.Name }; + + public Task GetModelAsync(long id) => Queryable.Where(entity => entity.Id == id).Select(Model).SingleOrDefaultAsync(); + + public Task> GridAsync(GridParameters parameters) => Queryable.Select(Model).GridAsync(parameters); + + public async Task> ListModelAsync() => await Queryable.Select(Model).ToListAsync(); +} diff --git a/source/Database/Example/IExampleRepository.cs b/source/Database/Example/IExampleRepository.cs new file mode 100644 index 00000000..6a185113 --- /dev/null +++ b/source/Database/Example/IExampleRepository.cs @@ -0,0 +1,10 @@ +namespace Architecture.Database; + +public interface IExampleRepository : IRepository +{ + Task GetModelAsync(long id); + + Task> GridAsync(GridParameters parameters); + + Task> ListModelAsync(); +} diff --git a/source/Database/Migrations/00000000000000_Initial.Designer.cs b/source/Database/Migrations/00000000000000_Initial.Designer.cs new file mode 100644 index 00000000..b0fa32ff --- /dev/null +++ b/source/Database/Migrations/00000000000000_Initial.Designer.cs @@ -0,0 +1,150 @@ +// +using System; +using Architecture.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Architecture.Database.Migrations +{ + [DbContext(typeof(Context))] + [Migration("00000000000000_Initial")] + partial class Initial + { + /// + 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("Architecture.Domain.Auth", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Login") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Roles") + .HasColumnType("int"); + + b.Property("Salt") + .HasMaxLength(1000) + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.HasIndex("Salt") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Auth", "Auth"); + + b.HasData( + new + { + Id = 1L, + Login = "admin", + Password = "O34uMN1Vho2IYcSM7nlXEqn57RZ8VEUsJwH++sFr0i3MSHJVx8J3PQGjhLR3s5i4l0XWUnCnymQ/EbRmzvLy8uMWREZu7vZI+BqebjAl5upYKMMQvlEcBeyLcRRTTBpYpv80m/YCZQmpig4XFVfIViLLZY/Kr5gBN5dkQf25rK+u88gtSXAyPDkW+hVS+dW4AmxrnaNFZks0Zzcd5xlb12wcF/q96cc4htHFzvOH9jtN98N5EBIXSvdUVnFc9kBuRTVytATZA7gITbs//hkxvNQ3eody5U9hjdH6D+AP0vVt5unZlTZ+gInn8Ze7AC5o6mn0Y3ylGO1CBJSHU9c/BcFY9oknn+XYk9ySCoDGctMqDbvOBcvSTBkKcrCzev2KnX7xYmC3yNz1Q5oPVKgnq4mc1iuldMlCxse/IDGMJB2FRdTCLV5KNS4IZ1GB+dw3tMvcEEtmXmgT2zKN5+kUkOxhlv5g1ZLgXzWjVJeKb5uZpsn3WK5kt8T+kzMoxHd5i8HxsU2uvy9GopxAnaV0WNsBPqTGkRllSxARl4ZN3hJqUla553RT49tJxbs+N03OmkYhjq+L0aKJ1AC+7G+rdjegiAQZB+3mdE7a2Pne2kYtpMoCmNMKdm9jOOVpfXJqZMQul9ltJSlAY6LPrHFUB3mw61JBNzx+sZgYN29GfDY=", + Roles = 3, + Salt = new Guid("79005744-e69a-4b09-996b-08fe0b70cbb9"), + UserId = 1L + }); + }); + + modelBuilder.Entity("Architecture.Domain.Example", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.HasKey("Id"); + + b.ToTable("Example", "Example"); + }); + + modelBuilder.Entity("Architecture.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("User", "User"); + + b.HasData( + new + { + Id = 1L, + Email = "administrator@administrator.com", + Name = "Administrator", + Status = 1 + }); + }); + + modelBuilder.Entity("Architecture.Domain.Auth", b => + { + b.HasOne("Architecture.Domain.User", "User") + .WithOne() + .HasForeignKey("Architecture.Domain.Auth", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/source/Database/Migrations/00000000000000_Initial.cs b/source/Database/Migrations/00000000000000_Initial.cs new file mode 100644 index 00000000..7c8b7856 --- /dev/null +++ b/source/Database/Migrations/00000000000000_Initial.cs @@ -0,0 +1,135 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Architecture.Database.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "Auth"); + + migrationBuilder.EnsureSchema( + name: "Example"); + + migrationBuilder.EnsureSchema( + name: "User"); + + migrationBuilder.CreateTable( + name: "Example", + schema: "Example", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(250)", maxLength: 250, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Example", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "User", + schema: "User", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(250)", maxLength: 250, nullable: false), + Email = table.Column(type: "nvarchar(250)", maxLength: 250, nullable: false), + Status = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_User", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Auth", + schema: "Auth", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Login = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Password = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: false), + Salt = table.Column(type: "uniqueidentifier", maxLength: 1000, nullable: false), + Roles = table.Column(type: "int", nullable: false), + UserId = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Auth", x => x.Id); + table.ForeignKey( + name: "FK_Auth_User_UserId", + column: x => x.UserId, + principalSchema: "User", + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + schema: "User", + table: "User", + columns: new[] { "Id", "Email", "Name", "Status" }, + values: new object[] { 1L, "administrator@administrator.com", "Administrator", 1 }); + + migrationBuilder.InsertData( + schema: "Auth", + table: "Auth", + columns: new[] { "Id", "Login", "Password", "Roles", "Salt", "UserId" }, + values: new object[] { 1L, "admin", "O34uMN1Vho2IYcSM7nlXEqn57RZ8VEUsJwH++sFr0i3MSHJVx8J3PQGjhLR3s5i4l0XWUnCnymQ/EbRmzvLy8uMWREZu7vZI+BqebjAl5upYKMMQvlEcBeyLcRRTTBpYpv80m/YCZQmpig4XFVfIViLLZY/Kr5gBN5dkQf25rK+u88gtSXAyPDkW+hVS+dW4AmxrnaNFZks0Zzcd5xlb12wcF/q96cc4htHFzvOH9jtN98N5EBIXSvdUVnFc9kBuRTVytATZA7gITbs//hkxvNQ3eody5U9hjdH6D+AP0vVt5unZlTZ+gInn8Ze7AC5o6mn0Y3ylGO1CBJSHU9c/BcFY9oknn+XYk9ySCoDGctMqDbvOBcvSTBkKcrCzev2KnX7xYmC3yNz1Q5oPVKgnq4mc1iuldMlCxse/IDGMJB2FRdTCLV5KNS4IZ1GB+dw3tMvcEEtmXmgT2zKN5+kUkOxhlv5g1ZLgXzWjVJeKb5uZpsn3WK5kt8T+kzMoxHd5i8HxsU2uvy9GopxAnaV0WNsBPqTGkRllSxARl4ZN3hJqUla553RT49tJxbs+N03OmkYhjq+L0aKJ1AC+7G+rdjegiAQZB+3mdE7a2Pne2kYtpMoCmNMKdm9jOOVpfXJqZMQul9ltJSlAY6LPrHFUB3mw61JBNzx+sZgYN29GfDY=", 3, new Guid("79005744-e69a-4b09-996b-08fe0b70cbb9"), 1L }); + + migrationBuilder.CreateIndex( + name: "IX_Auth_Login", + schema: "Auth", + table: "Auth", + column: "Login", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Auth_Salt", + schema: "Auth", + table: "Auth", + column: "Salt", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Auth_UserId", + schema: "Auth", + table: "Auth", + column: "UserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_User_Email", + schema: "User", + table: "User", + column: "Email", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Auth", + schema: "Auth"); + + migrationBuilder.DropTable( + name: "Example", + schema: "Example"); + + migrationBuilder.DropTable( + name: "User", + schema: "User"); + } + } +} diff --git a/source/Database/Migrations/ContextModelSnapshot.cs b/source/Database/Migrations/ContextModelSnapshot.cs new file mode 100644 index 00000000..578d200e --- /dev/null +++ b/source/Database/Migrations/ContextModelSnapshot.cs @@ -0,0 +1,147 @@ +// +using System; +using Architecture.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Architecture.Database.Migrations +{ + [DbContext(typeof(Context))] + partial class ContextModelSnapshot : 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("Architecture.Domain.Auth", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Login") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Roles") + .HasColumnType("int"); + + b.Property("Salt") + .HasMaxLength(1000) + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Login") + .IsUnique(); + + b.HasIndex("Salt") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("Auth", "Auth"); + + b.HasData( + new + { + Id = 1L, + Login = "admin", + Password = "O34uMN1Vho2IYcSM7nlXEqn57RZ8VEUsJwH++sFr0i3MSHJVx8J3PQGjhLR3s5i4l0XWUnCnymQ/EbRmzvLy8uMWREZu7vZI+BqebjAl5upYKMMQvlEcBeyLcRRTTBpYpv80m/YCZQmpig4XFVfIViLLZY/Kr5gBN5dkQf25rK+u88gtSXAyPDkW+hVS+dW4AmxrnaNFZks0Zzcd5xlb12wcF/q96cc4htHFzvOH9jtN98N5EBIXSvdUVnFc9kBuRTVytATZA7gITbs//hkxvNQ3eody5U9hjdH6D+AP0vVt5unZlTZ+gInn8Ze7AC5o6mn0Y3ylGO1CBJSHU9c/BcFY9oknn+XYk9ySCoDGctMqDbvOBcvSTBkKcrCzev2KnX7xYmC3yNz1Q5oPVKgnq4mc1iuldMlCxse/IDGMJB2FRdTCLV5KNS4IZ1GB+dw3tMvcEEtmXmgT2zKN5+kUkOxhlv5g1ZLgXzWjVJeKb5uZpsn3WK5kt8T+kzMoxHd5i8HxsU2uvy9GopxAnaV0WNsBPqTGkRllSxARl4ZN3hJqUla553RT49tJxbs+N03OmkYhjq+L0aKJ1AC+7G+rdjegiAQZB+3mdE7a2Pne2kYtpMoCmNMKdm9jOOVpfXJqZMQul9ltJSlAY6LPrHFUB3mw61JBNzx+sZgYN29GfDY=", + Roles = 3, + Salt = new Guid("79005744-e69a-4b09-996b-08fe0b70cbb9"), + UserId = 1L + }); + }); + + modelBuilder.Entity("Architecture.Domain.Example", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.HasKey("Id"); + + b.ToTable("Example", "Example"); + }); + + modelBuilder.Entity("Architecture.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("User", "User"); + + b.HasData( + new + { + Id = 1L, + Email = "administrator@administrator.com", + Name = "Administrator", + Status = 1 + }); + }); + + modelBuilder.Entity("Architecture.Domain.Auth", b => + { + b.HasOne("Architecture.Domain.User", "User") + .WithOne() + .HasForeignKey("Architecture.Domain.Auth", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/source/Database/User/IUserRepository.cs b/source/Database/User/IUserRepository.cs new file mode 100644 index 00000000..dabea18f --- /dev/null +++ b/source/Database/User/IUserRepository.cs @@ -0,0 +1,10 @@ +namespace Architecture.Database; + +public interface IUserRepository : IRepository +{ + Task GetModelAsync(long id); + + Task> GridAsync(GridParameters parameters); + + Task> ListModelAsync(); +} diff --git a/source/Database/User/UserConfiguration.cs b/source/Database/User/UserConfiguration.cs new file mode 100644 index 00000000..ee13bdad --- /dev/null +++ b/source/Database/User/UserConfiguration.cs @@ -0,0 +1,21 @@ +namespace Architecture.Database; + +public sealed class UserConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(User), nameof(User)); + + builder.HasKey(entity => entity.Id); + + builder.Property(entity => entity.Id).ValueGeneratedOnAdd().IsRequired(); + + builder.Property(entity => entity.Name).HasMaxLength(250).IsRequired(); + + builder.Property(entity => entity.Email).HasMaxLength(250).IsRequired(); + + builder.Property(entity => entity.Status).IsRequired(); + + builder.HasIndex(entity => entity.Email).IsUnique(); + } +} diff --git a/source/Database/User/UserRepository.cs b/source/Database/User/UserRepository.cs new file mode 100644 index 00000000..451514a7 --- /dev/null +++ b/source/Database/User/UserRepository.cs @@ -0,0 +1,19 @@ +namespace Architecture.Database; + +public sealed class UserRepository : EFRepository, IUserRepository +{ + public UserRepository(Context context) : base(context) { } + + public static Expression> Model => entity => new UserModel + { + Id = entity.Id, + Name = entity.Name, + Email = entity.Email + }; + + public Task GetModelAsync(long id) => Queryable.Where(entity => entity.Id == id).Select(Model).SingleOrDefaultAsync(); + + public Task> GridAsync(GridParameters parameters) => Queryable.Select(Model).GridAsync(parameters); + + public async Task> ListModelAsync() => await Queryable.Select(Model).ToListAsync(); +} diff --git a/source/Directory.Build.props b/source/Directory.Build.props new file mode 100644 index 00000000..f853aaca --- /dev/null +++ b/source/Directory.Build.props @@ -0,0 +1,9 @@ + + + enable + false + latest + en + net8.0 + + diff --git a/source/Directory.Packages.props b/source/Directory.Packages.props new file mode 100644 index 00000000..5c078edd --- /dev/null +++ b/source/Directory.Packages.props @@ -0,0 +1,22 @@ + + + false + true + true + + + + + + + + + + + + + + + + + diff --git a/source/Domain/Architecture.Domain.csproj b/source/Domain/Architecture.Domain.csproj new file mode 100644 index 00000000..2726b1f4 --- /dev/null +++ b/source/Domain/Architecture.Domain.csproj @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/source/Domain/Auth.cs b/source/Domain/Auth.cs new file mode 100644 index 00000000..51832535 --- /dev/null +++ b/source/Domain/Auth.cs @@ -0,0 +1,32 @@ +namespace Architecture.Domain; + +public sealed class Auth : Entity +{ + public Auth + ( + string login, + string password, + User user + ) + { + Login = login; + Password = password; + User = user; + Salt = Guid.NewGuid(); + Roles = Roles.User; + } + + public Auth() { } + + public string Login { get; } + + public string Password { get; private set; } + + public Guid Salt { get; } + + public Roles Roles { get; } + + public User User { get; } + + public void UpdatePassword(string password) => Password = password; +} diff --git a/source/Domain/Example.cs b/source/Domain/Example.cs new file mode 100644 index 00000000..d66ec516 --- /dev/null +++ b/source/Domain/Example.cs @@ -0,0 +1,10 @@ +namespace Architecture.Domain; + +public sealed class Example : Entity +{ + public Example(long id, string name) : this(name) => Id = id; + + public Example(string name) => Name = name; + + public string Name { get; } +} diff --git a/source/Domain/Roles.cs b/source/Domain/Roles.cs new file mode 100644 index 00000000..6a032630 --- /dev/null +++ b/source/Domain/Roles.cs @@ -0,0 +1,9 @@ +namespace Architecture.Domain; + +[Flags] +public enum Roles +{ + None = 0, + User = 1, + Admin = 2 +} diff --git a/source/Domain/Status.cs b/source/Domain/Status.cs new file mode 100644 index 00000000..1ec0e204 --- /dev/null +++ b/source/Domain/Status.cs @@ -0,0 +1,8 @@ +namespace Architecture.Domain; + +public enum Status +{ + None = 0, + Active = 1, + Inactive = 2 +} diff --git a/source/Domain/User.cs b/source/Domain/User.cs new file mode 100644 index 00000000..7beac86e --- /dev/null +++ b/source/Domain/User.cs @@ -0,0 +1,31 @@ +namespace Architecture.Domain; + +public sealed class User : Entity +{ + public User + ( + string name, + string email + ) + { + Name = name; + Email = email; + Activate(); + } + + public User(long id) => Id = id; + + public string Name { get; private set; } + + public string Email { get; private set; } + + public Status Status { get; private set; } + + public void UpdateName(string name) => Name = name; + + public void UpdateEmail(string email) => Email = email; + + public void Activate() => Status = Status.Active; + + public void Inactivate() => Status = Status.Inactive; +} diff --git a/source/Model/Architecture.Model.csproj b/source/Model/Architecture.Model.csproj new file mode 100644 index 00000000..2f04a8bd --- /dev/null +++ b/source/Model/Architecture.Model.csproj @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/Model/ExampleModel.cs b/source/Model/ExampleModel.cs new file mode 100644 index 00000000..bcfa814b --- /dev/null +++ b/source/Model/ExampleModel.cs @@ -0,0 +1,8 @@ +namespace Architecture.Model; + +public sealed record ExampleModel +{ + public long Id { get; init; } + + public string Name { get; init; } +} diff --git a/source/Model/UserModel.cs b/source/Model/UserModel.cs new file mode 100644 index 00000000..fb064ba9 --- /dev/null +++ b/source/Model/UserModel.cs @@ -0,0 +1,10 @@ +namespace Architecture.Model; + +public sealed record UserModel +{ + public long Id { get; init; } + + public string Name { get; init; } + + public string Email { get; init; } +} diff --git a/source/Web/AppSettings.json b/source/Web/AppSettings.json new file mode 100644 index 00000000..26920892 --- /dev/null +++ b/source/Web/AppSettings.json @@ -0,0 +1,50 @@ +{ + "AllowedHosts": "*", + "Authentication": { + "Schemes": { + "Bearer": { + "ValidIssuer": "Issuer", + "ValidAudience": "Audience", + "SigningKeys": [ + { + "Issuer": "Issuer", + "Value": "58a97cd766d741e8a21b8d3c4279652469801dc8da844fa5bf80afeea85aa472" + } + ] + } + } + }, + "ConnectionStrings": { + "Context": "Server=(localdb)\\MSSQLLocalDB;Database=Database;" + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File" + ], + "MinimumLevel": { + "Default": "Information", + "Microsoft": "Information", + "System": "Information" + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss}] [{Level:u}] [{SourceContext}] {Message:lj}{NewLine}{Exception}{NewLine}" + } + }, + { + "Name": "File", + "Args": { + "formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog", + "path": "Logs\\.log", + "restrictedToMinimumLevel": "Error", + "rollingInterval": "Day", + "rollOnFileSizeLimit": "true", + "shared": "true" + } + } + ] + } +} diff --git a/source/Web/AppStrings.json b/source/Web/AppStrings.json new file mode 100644 index 00000000..9620fce6 --- /dev/null +++ b/source/Web/AppStrings.json @@ -0,0 +1,6 @@ +{ + "Unauthorized": { + "en": "Invalid login and password!", + "pt": "Login e senha inválidos!" + } +} diff --git a/source/Web/Architecture.Web.csproj b/source/Web/Architecture.Web.csproj new file mode 100644 index 00000000..50adea30 --- /dev/null +++ b/source/Web/Architecture.Web.csproj @@ -0,0 +1,37 @@ + + + Frontend + http://localhost:4200 + npm start + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/Web/Controllers/AuthController.cs b/source/Web/Controllers/AuthController.cs new file mode 100644 index 00000000..0b36dee5 --- /dev/null +++ b/source/Web/Controllers/AuthController.cs @@ -0,0 +1,10 @@ +namespace Architecture.Web; + +[ApiController] +[Route("api/auths")] +public sealed class AuthController : BaseController +{ + [AllowAnonymous] + [HttpPost] + public IActionResult Auth(AuthRequest request) => Mediator.HandleAsync(request).ApiResult(); +} diff --git a/source/Web/Controllers/BaseController.cs b/source/Web/Controllers/BaseController.cs new file mode 100644 index 00000000..6075ef7d --- /dev/null +++ b/source/Web/Controllers/BaseController.cs @@ -0,0 +1,7 @@ +namespace Architecture.Web; + +[ApiController] +public abstract class BaseController : ControllerBase +{ + protected IMediator Mediator => HttpContext.RequestServices.GetRequiredService(); +} diff --git a/source/Web/Controllers/ExampleController.cs b/source/Web/Controllers/ExampleController.cs new file mode 100644 index 00000000..30d4cecc --- /dev/null +++ b/source/Web/Controllers/ExampleController.cs @@ -0,0 +1,29 @@ +namespace Architecture.Web; + +[ApiController] +[Route("api/examples")] +public sealed class ExampleController : BaseController +{ + [HttpPost] + public IActionResult Add(AddExampleRequest request) => Mediator.HandleAsync(request).ApiResult(); + + [HttpDelete("{id}")] + public IActionResult Delete(long id) => Mediator.HandleAsync(new DeleteExampleRequest(id)).ApiResult(); + + [HttpGet("{id}")] + public IActionResult Get(long id) => Mediator.HandleAsync(new GetExampleRequest(id)).ApiResult(); + + [HttpGet("grid")] + public IActionResult Grid([FromQuery] GridExampleRequest request) => Mediator.HandleAsync>(request).ApiResult(); + + [HttpGet] + public IActionResult List() => Mediator.HandleAsync>(new ListExampleRequest()).ApiResult(); + + [HttpPut("{id}")] + public IActionResult Update(long id, UpdateExampleRequest request) + { + request.Id = id; + + return Mediator.HandleAsync(request).ApiResult(); + } +} diff --git a/source/Web/Controllers/FileController.cs b/source/Web/Controllers/FileController.cs new file mode 100644 index 00000000..1771f7ad --- /dev/null +++ b/source/Web/Controllers/FileController.cs @@ -0,0 +1,13 @@ +namespace Architecture.Web; + +[ApiController] +[Route("api/files")] +public sealed class FileController : BaseController +{ + [DisableRequestSizeLimit] + [HttpPost] + public IActionResult Add() => Mediator.HandleAsync>(new AddFileRequest(Request.Files())).ApiResult(); + + [HttpGet("{id}")] + public IActionResult Get(Guid id) => Mediator.HandleAsync(new GetFileRequest(id)).ApiResult(); +} diff --git a/source/Web/Controllers/UserController.cs b/source/Web/Controllers/UserController.cs new file mode 100644 index 00000000..406c7d88 --- /dev/null +++ b/source/Web/Controllers/UserController.cs @@ -0,0 +1,32 @@ +namespace Architecture.Web; + +[ApiController] +[Route("api/users")] +public sealed class UserController : BaseController +{ + [HttpPost] + public IActionResult Add(AddUserRequest request) => Mediator.HandleAsync(request).ApiResult(); + + [HttpDelete("{id}")] + public IActionResult Delete(long id) => Mediator.HandleAsync(new DeleteUserRequest(id)).ApiResult(); + + [HttpGet("{id}")] + public IActionResult Get(long id) => Mediator.HandleAsync(new GetUserRequest(id)).ApiResult(); + + [HttpGet("grid")] + public IActionResult Grid([FromQuery] GridUserRequest request) => Mediator.HandleAsync>(request).ApiResult(); + + [HttpPatch("{id}/inactivate")] + public IActionResult Inactivate(long id) => Mediator.HandleAsync(new InactivateUserRequest(id)).ApiResult(); + + [HttpGet] + public IActionResult List() => Mediator.HandleAsync>(new ListUserRequest()).ApiResult(); + + [HttpPut("{id}")] + public IActionResult Update(long id, UpdateUserRequest request) + { + request.Id = id; + + return Mediator.HandleAsync(request).ApiResult(); + } +} diff --git a/source/Web/Frontend/.npmrc b/source/Web/Frontend/.npmrc new file mode 100644 index 00000000..9005ac9b --- /dev/null +++ b/source/Web/Frontend/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/source/Web/Frontend/angular.json b/source/Web/Frontend/angular.json new file mode 100644 index 00000000..be33c5a9 --- /dev/null +++ b/source/Web/Frontend/angular.json @@ -0,0 +1,75 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "cli": { + "analytics": false + }, + "projects": { + "frontend": { + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "inlineStyleLanguage": "scss", + "index": "src/index.html", + "main": "src/main.ts", + "outputPath": "dist", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "scripts": [ + "node_modules/uikit/dist/js/uikit.min.js", + "node_modules/uikit/dist/js/uikit-icons.min.js" + ], + "styles": [ + "node_modules/uikit/dist/css/uikit.min.css", + "src/styles/style.scss" + ] + }, + "configurations": { + "development": { + "buildOptimizer": false, + "extractLicenses": false, + "namedChunks": true, + "optimization": false, + "sourceMap": true, + "vendorChunk": true + }, + "production": { + "outputHashing": "all" + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "frontend:build:production" + }, + "development": { + "buildTarget": "frontend:build:development", + "proxyConfig": "proxy.json" + } + }, + "defaultConfiguration": "development" + } + }, + "prefix": "app", + "projectType": "application", + "root": "", + "sourceRoot": "src", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + } + } + } +} diff --git a/source/Web/Frontend/package.json b/source/Web/Frontend/package.json new file mode 100644 index 00000000..c00910e4 --- /dev/null +++ b/source/Web/Frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@angular/common": "18.0.0", + "@angular/compiler": "18.0.0", + "@angular/core": "18.0.0", + "@angular/forms": "18.0.0", + "@angular/platform-browser": "18.0.0", + "@angular/platform-browser-dynamic": "18.0.0", + "@angular/router": "18.0.0", + "rxjs": "7.8.1", + "uikit": "3.21.3", + "zone.js": "0.14.6" + }, + "devDependencies": { + "@angular/cli": "18.0.0", + "@angular/compiler-cli": "18.0.0", + "@angular-devkit/build-angular": "18.0.0", + "typescript": "5.4.5" + }, + "browserslist": [ + "last 1 Chrome version", + "last 1 Edge version", + "last 1 Firefox version", + "last 1 Opera version", + "last 1 Safari version", + "not dead" + ], + "scripts": { + "restore": "npm install", + "start": "ng serve", + "publish": "ng build --configuration production" + } +} diff --git a/source/Web/Frontend/proxy.json b/source/Web/Frontend/proxy.json new file mode 100644 index 00000000..18c4b6fc --- /dev/null +++ b/source/Web/Frontend/proxy.json @@ -0,0 +1,10 @@ +{ + "/swagger": { + "target": "https://localhost:8090", + "secure": false + }, + "/api/**": { + "target": "https://localhost:8090", + "secure": false + } +} diff --git a/source/Web/Frontend/src/app/app.can.activate.ts b/source/Web/Frontend/src/app/app.can.activate.ts new file mode 100644 index 00000000..fdea1293 --- /dev/null +++ b/source/Web/Frontend/src/app/app.can.activate.ts @@ -0,0 +1,10 @@ +import { inject } from "@angular/core"; +import { AppAuthService } from "./services/auth.service"; +import { CanActivateFn } from "@angular/router"; + +export const appCanActivate: CanActivateFn = () => { + const appAuthService = inject(AppAuthService); + if (appAuthService.authenticated()) { return true; } + appAuthService.signin(); + return false; +}; diff --git a/source/Web/Frontend/src/app/app.component.ts b/source/Web/Frontend/src/app/app.component.ts new file mode 100644 index 00000000..f646b577 --- /dev/null +++ b/source/Web/Frontend/src/app/app.component.ts @@ -0,0 +1,4 @@ +import { Component } from "@angular/core"; + +@Component({ selector: "app", template: "" }) +export class AppComponent { } diff --git a/source/Web/Frontend/src/app/app.error.handler.ts b/source/Web/Frontend/src/app/app.error.handler.ts new file mode 100644 index 00000000..75e4bbcc --- /dev/null +++ b/source/Web/Frontend/src/app/app.error.handler.ts @@ -0,0 +1,14 @@ +import { HttpErrorResponse } from "@angular/common/http"; +import { ErrorHandler, Injectable } from "@angular/core"; +import { AppModalService } from "./services/modal.service"; + +@Injectable({ providedIn: "root" }) +export class AppErrorHandler implements ErrorHandler { + constructor(private readonly appModalService: AppModalService) { } + + handleError(error: any) { + if (error instanceof HttpErrorResponse && error.error) { + this.appModalService.alert(error.error); + } + } +} diff --git a/source/Web/Frontend/src/app/app.http.interceptor.ts b/source/Web/Frontend/src/app/app.http.interceptor.ts new file mode 100644 index 00000000..88660579 --- /dev/null +++ b/source/Web/Frontend/src/app/app.http.interceptor.ts @@ -0,0 +1,13 @@ +import { HttpInterceptorFn } from "@angular/common/http"; +import { inject } from "@angular/core"; +import { AppAuthService } from "./services/auth.service"; + +export const appHttpInterceptor: HttpInterceptorFn = (request, next) => { + const appAuthService = inject(AppAuthService); + + request = request.clone({ + setHeaders: { Authorization: `Bearer ${appAuthService.token()}` } + }); + + return next(request); +}; diff --git a/source/Web/Frontend/src/app/app.module.ts b/source/Web/Frontend/src/app/app.module.ts new file mode 100644 index 00000000..c4ef96bf --- /dev/null +++ b/source/Web/Frontend/src/app/app.module.ts @@ -0,0 +1,25 @@ +import { provideHttpClient, withInterceptors } from "@angular/common/http"; +import { APP_INITIALIZER, ErrorHandler, NgModule } from "@angular/core"; +import { BrowserModule } from "@angular/platform-browser"; +import { RouterOutlet, provideRouter } from "@angular/router"; +import { AppComponent } from "./app.component"; +import { AppErrorHandler } from "./app.error.handler"; +import { appHttpInterceptor } from "./app.http.interceptor"; +import { ROUTES } from "./app.routes"; +import { AppSettingsService } from "./settings/settings.service"; + +@NgModule({ + bootstrap: [AppComponent], + declarations: [AppComponent], + imports: [ + BrowserModule, + RouterOutlet + ], + providers: [ + { provide: ErrorHandler, useClass: AppErrorHandler }, + { provide: APP_INITIALIZER, useFactory: (_: AppSettingsService) => () => { }, multi: true }, + provideRouter(ROUTES), + provideHttpClient(withInterceptors([appHttpInterceptor])) + ] +}) +export class AppModule { } diff --git a/source/Web/Frontend/src/app/app.routes.ts b/source/Web/Frontend/src/app/app.routes.ts new file mode 100644 index 00000000..9c60396e --- /dev/null +++ b/source/Web/Frontend/src/app/app.routes.ts @@ -0,0 +1,29 @@ +import { Routes } from "@angular/router"; +import { appCanActivate } from "./app.can.activate"; +import { AppLayoutComponent } from "./layouts/layout/layout.component"; +import { AppLayoutNavComponent } from "./layouts/layout-nav/layout-nav.component"; + +export const ROUTES: Routes = [ + { + path: "", + component: AppLayoutComponent, + children: [ + { path: "", loadComponent: () => import("./pages/auth/auth.component").then((response) => response.AppAuthComponent) } + ] + }, + { + path: "main", + component: AppLayoutNavComponent, + canActivate: [appCanActivate], + children: [ + { path: "files", loadComponent: () => import("./pages/files/files.component").then((response) => response.AppFilesComponent) }, + { path: "form", loadComponent: () => import("./pages/form/form.component").then((response) => response.AppFormComponent) }, + { path: "home", loadComponent: () => import("./pages/home/home.component").then((response) => response.AppHomeComponent) }, + { path: "list", loadComponent: () => import("./pages/list/list.component").then((response) => response.AppListComponent) } + ] + }, + { + path: "**", + redirectTo: "" + } +]; diff --git a/source/Web/Frontend/src/app/components/button/button.component.html b/source/Web/Frontend/src/app/components/button/button.component.html new file mode 100644 index 00000000..4d612e54 --- /dev/null +++ b/source/Web/Frontend/src/app/components/button/button.component.html @@ -0,0 +1 @@ + diff --git a/source/Web/Frontend/src/app/components/button/button.component.ts b/source/Web/Frontend/src/app/components/button/button.component.ts new file mode 100644 index 00000000..ca4856de --- /dev/null +++ b/source/Web/Frontend/src/app/components/button/button.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "app-button", + templateUrl: "./button.component.html", + standalone: true +}) +export class AppButtonComponent { + @Input() disabled = false; + @Input() text!: string; +} diff --git a/source/Web/Frontend/src/app/components/component.ts b/source/Web/Frontend/src/app/components/component.ts new file mode 100644 index 00000000..dc0d591a --- /dev/null +++ b/source/Web/Frontend/src/app/components/component.ts @@ -0,0 +1,25 @@ +import { ControlValueAccessor } from "@angular/forms"; + +export abstract class AppComponent implements ControlValueAccessor { + abstract class: string; + abstract disabled: boolean; + abstract formControlName: string; + abstract text: string; + private _value!: Value; + + get value(): Value { return this._value; } + + set value(value: Value) { + if (this.value === value) return; + this._value = value; + if (this.onChange) this.onChange(value); + } + + registerOnChange(onChange: any) { this.onChange = onChange; } + + registerOnTouched(_: () => void) { } + + writeValue(value: Value) { this.value = value; } + + private onChange!: (value: Value) => void; +} diff --git a/source/Web/Frontend/src/app/components/file/file.component.html b/source/Web/Frontend/src/app/components/file/file.component.html new file mode 100644 index 00000000..b79334ae --- /dev/null +++ b/source/Web/Frontend/src/app/components/file/file.component.html @@ -0,0 +1,15 @@ +
+ + +
+ +
+
    + @for (upload of uploads; track upload.id) { +
  • {{ upload.id }}: {{ upload.progress }}%
  • + } + @for (file of value; track file.name) { +
  • {{ file.name }}
  • + } +
+
diff --git a/source/Web/Frontend/src/app/components/file/file.component.ts b/source/Web/Frontend/src/app/components/file/file.component.ts new file mode 100644 index 00000000..8b79564f --- /dev/null +++ b/source/Web/Frontend/src/app/components/file/file.component.ts @@ -0,0 +1,50 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { AppComponent } from "src/app/components/component"; +import { AppFileService } from "./file.service"; +import { FileModel } from "./file.model"; +import { UploadModel } from "./upload.model"; + +@Component({ + selector: "app-file", + templateUrl: "./file.component.html", + standalone: true, + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: AppFileComponent, multi: true }], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule + ] +}) +export class AppFileComponent extends AppComponent { + @Input() class!: string; + @Input() disabled = false; + @Input() formControlName!: string; + @Input() text!: string; + + uploads = new Array(); + + constructor(private readonly appFileService: AppFileService) { + super(); + } + + change(files: FileList | null) { + if (!files) { return; } + + for (let index = 0; index < files.length; index++) { + const file = files.item(index) as File; + const upload = new UploadModel(file.name, 0); + this.uploads.push(upload); + + this.appFileService.upload(file).subscribe((result: UploadModel) => { + upload.progress = result.progress; + + if (result.id) { + this.value.push(new FileModel(result.id, file.name)); + this.uploads = this.uploads.filter((x) => x.progress < 100); + } + }); + } + } +} diff --git a/source/Web/Frontend/src/app/components/file/file.model.ts b/source/Web/Frontend/src/app/components/file/file.model.ts new file mode 100644 index 00000000..3e61a54a --- /dev/null +++ b/source/Web/Frontend/src/app/components/file/file.model.ts @@ -0,0 +1,3 @@ +export class FileModel { + constructor(public id: string, public name: string) { } +} diff --git a/source/Web/Frontend/src/app/components/file/file.service.ts b/source/Web/Frontend/src/app/components/file/file.service.ts new file mode 100644 index 00000000..4d561c5d --- /dev/null +++ b/source/Web/Frontend/src/app/components/file/file.service.ts @@ -0,0 +1,31 @@ +import { HttpClient, HttpEventType, HttpRequest } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { Observable } from "rxjs"; +import { UploadModel } from "./upload.model"; + +@Injectable({ providedIn: "root" }) +export class AppFileService { + constructor(private readonly http: HttpClient) { } + + upload(file: File): Observable { + const formData = new FormData(); + formData.append(file.name, file); + + const request = new HttpRequest("POST", "api/files", formData, { reportProgress: true }); + + return new Observable((observable: any) => { + this.http.request(request).subscribe((event: any) => { + if (event.type === HttpEventType.Response) { + return observable.next(new UploadModel(event.body[0].id, 100)); + } + + if (event.type === HttpEventType.UploadProgress && event.total) { + const progress = Math.round(100 * event.loaded / event.total); + return observable.next(new UploadModel("", progress)); + } + + return observable.next(new UploadModel("", 0)); + }); + }); + } +} diff --git a/source/Web/Frontend/src/app/components/file/upload.model.ts b/source/Web/Frontend/src/app/components/file/upload.model.ts new file mode 100644 index 00000000..925eedab --- /dev/null +++ b/source/Web/Frontend/src/app/components/file/upload.model.ts @@ -0,0 +1,3 @@ +export class UploadModel { + constructor(public id: string, public progress: number) { } +} diff --git a/source/Web/Frontend/src/app/components/grid/filter/filter.model.ts b/source/Web/Frontend/src/app/components/grid/filter/filter.model.ts new file mode 100644 index 00000000..ac29fdb0 --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/filter/filter.model.ts @@ -0,0 +1,3 @@ +export class FilterModel { + constructor(public property: string, public comparison: string, public value: any) { } +} diff --git a/source/Web/Frontend/src/app/components/grid/filter/filters.model.ts b/source/Web/Frontend/src/app/components/grid/filter/filters.model.ts new file mode 100644 index 00000000..6b7781f8 --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/filter/filters.model.ts @@ -0,0 +1,25 @@ +import { FormGroup } from "@angular/forms"; +import { FilterModel } from "./filter.model"; + +export class FiltersModel extends Array { + add(property: string, comparison: string, value: any) { + if (!property || !value) { return; } + this.removeIndex(this.findIndex(x => x.property === property && x.comparison === comparison)); + this.push(new FilterModel(property, comparison, value)); + } + + addFromFormGroup(form: FormGroup) { + if (!form || !form.controls) { return; } + Object.keys(form.controls).forEach(key => this.add(key, "", form.controls[key].value)); + } + + remove(property: string) { + if (!property) { return; } + this.removeIndex(this.findIndex(x => x.property === property)); + } + + private removeIndex(index: number) { + if (index < 0) { return; } + this.splice(index, 1); + } +} diff --git a/source/Web/Frontend/src/app/components/grid/grid-parameters.model.ts b/source/Web/Frontend/src/app/components/grid/grid-parameters.model.ts new file mode 100644 index 00000000..fa36ff67 --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/grid-parameters.model.ts @@ -0,0 +1,9 @@ +import { FiltersModel } from "./filter/filters.model"; +import { OrderModel } from "./order/order.model"; +import { PageModel } from "./page/page.model"; + +export class GridParametersModel { + filters = new FiltersModel(); + order = new OrderModel(); + page = new PageModel(); +} diff --git a/source/Web/Frontend/src/app/components/grid/grid.model.ts b/source/Web/Frontend/src/app/components/grid/grid.model.ts new file mode 100644 index 00000000..f89a0f54 --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/grid.model.ts @@ -0,0 +1,7 @@ +import { GridParametersModel } from "./grid-parameters.model"; + +export class GridModel { + count = 0; + list = new Array(); + parameters = new GridParametersModel(); +} diff --git a/source/Web/Frontend/src/app/components/grid/grid.service.ts b/source/Web/Frontend/src/app/components/grid/grid.service.ts new file mode 100644 index 00000000..7b306163 --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/grid.service.ts @@ -0,0 +1,39 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { FiltersModel } from "./filter/filters.model"; +import { GridModel } from "./grid.model"; +import { GridParametersModel } from "./grid-parameters.model"; +import { OrderModel } from "./order/order.model"; +import { PageModel } from "./page/page.model"; + +@Injectable({ providedIn: "root" }) +export class GridService { + constructor(private readonly http: HttpClient) { } + + get(url: string, parameters: GridParametersModel) { + return this.http.get>(url + this.queryString(parameters)); + } + + private queryString(parameters: GridParametersModel): string { + let url = "?"; + + parameters.page = parameters.page ?? new PageModel(); + url += `page.index=${parameters.page.index}&`; + url += `page.size=${parameters.page.size}&`; + + parameters.order = parameters.order ?? new OrderModel(); + url += `order.property=${parameters.order.property ?? ""}&`; + url += `order.ascending=${parameters.order.ascending}&`; + + parameters.filters = parameters.filters ?? new FiltersModel(); + parameters.filters.forEach((filter, index) => { + url += `filters[${index}].property=${filter.property}&`; + url += `filters[${index}].comparison=${filter.comparison ?? ""}&`; + url += `filters[${index}].value=${filter.value}&`; + }); + + url = url.slice(0, url.length - 1); + + return url; + } +} diff --git a/source/Web/Frontend/src/app/components/grid/order/order.component.html b/source/Web/Frontend/src/app/components/grid/order/order.component.html new file mode 100644 index 00000000..2f0ade17 --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/order/order.component.html @@ -0,0 +1,7 @@ +{{text}} + +@if (order.property === property && order.ascending) { + +} @else { + +} diff --git a/source/Web/Frontend/src/app/components/grid/order/order.component.ts b/source/Web/Frontend/src/app/components/grid/order/order.component.ts new file mode 100644 index 00000000..c4f5c615 --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/order/order.component.ts @@ -0,0 +1,25 @@ +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { OrderModel } from "./order.model"; + +@Component({ + selector: "app-order", + templateUrl: "./order.component.html", + standalone: true, + imports: [ + CommonModule + ] +}) +export class AppOrderComponent { + @Output() readonly changed = new EventEmitter(); + @Input() order!: OrderModel; + @Input() property!: string; + @Input() text!: string; + + click() { + this.order = this.order ?? new OrderModel(); + this.order.property = this.property; + this.order.ascending = !this.order.ascending; + this.changed.emit(); + } +} diff --git a/source/Web/Frontend/src/app/components/grid/order/order.model.ts b/source/Web/Frontend/src/app/components/grid/order/order.model.ts new file mode 100644 index 00000000..5186388c --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/order/order.model.ts @@ -0,0 +1,3 @@ +export class OrderModel { + constructor(public property: string = "", public ascending: boolean = true) { } +} diff --git a/source/Web/Frontend/src/app/components/grid/page/page.component.html b/source/Web/Frontend/src/app/components/grid/page/page.component.html new file mode 100644 index 00000000..6bc39618 --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/page/page.component.html @@ -0,0 +1,9 @@ +@if (this.pages > 1) { +
+ + + {{this.page.index}} of {{pages}} + + +
+} diff --git a/source/Web/Frontend/src/app/components/grid/page/page.component.ts b/source/Web/Frontend/src/app/components/grid/page/page.component.ts new file mode 100644 index 00000000..1a7d44de --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/page/page.component.ts @@ -0,0 +1,48 @@ +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { PageModel } from "./page.model"; + +@Component({ + selector: "app-page", + templateUrl: "./page.component.html", + standalone: true, + imports: [ + CommonModule + ] +}) +export class AppPageComponent { + get count(): number { + return this._count; + } + + @Input("count") + set count(count: number) { + this._count = count; + this.setPages(); + } + + get page(): PageModel { + return this._page; + } + + @Input("page") + set page(page: PageModel) { + this._page = page; + this.setPages(); + } + + @Output() readonly changed = new EventEmitter(); + + pages = 0; + private _count = 0; + private _page = new PageModel(); + + change(index: number) { + this.page.index = index; + this.changed.emit(); + } + + setPages() { + this.pages = Math.ceil(this.count / this.page.size); + } +} diff --git a/source/Web/Frontend/src/app/components/grid/page/page.model.ts b/source/Web/Frontend/src/app/components/grid/page/page.model.ts new file mode 100644 index 00000000..cb8cbee0 --- /dev/null +++ b/source/Web/Frontend/src/app/components/grid/page/page.model.ts @@ -0,0 +1,3 @@ +export class PageModel { + constructor(public index: number = 1, public size: number = 10) { } +} diff --git a/source/Web/Frontend/src/app/components/input/input.component.html b/source/Web/Frontend/src/app/components/input/input.component.html new file mode 100644 index 00000000..30900187 --- /dev/null +++ b/source/Web/Frontend/src/app/components/input/input.component.html @@ -0,0 +1,12 @@ + diff --git a/source/Web/Frontend/src/app/components/input/input.component.ts b/source/Web/Frontend/src/app/components/input/input.component.ts new file mode 100644 index 00000000..5c362e7f --- /dev/null +++ b/source/Web/Frontend/src/app/components/input/input.component.ts @@ -0,0 +1,14 @@ +import { AppComponent } from "src/app/components/component"; + +export abstract class AppInputComponent extends AppComponent { + type!: string; + + constructor(type: string) { + super(); + this.type = type; + } + + input($event: any): void { + this.value = $event.target.value; + } +} diff --git a/source/Web/Frontend/src/app/components/input/password.input.component.ts b/source/Web/Frontend/src/app/components/input/password.input.component.ts new file mode 100644 index 00000000..a12a7d22 --- /dev/null +++ b/source/Web/Frontend/src/app/components/input/password.input.component.ts @@ -0,0 +1,27 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { AppInputComponent } from "./input.component"; + +@Component({ + selector: "app-input-password", + templateUrl: "./input.component.html", + standalone: true, + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: AppInputPasswordComponent, multi: true }], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule + ] +}) +export class AppInputPasswordComponent extends AppInputComponent { + @Input() autofocus = false; + @Input() class!: string; + @Input() disabled = false; + @Input() formControlName!: string; + @Input() text!: string; + + constructor() { + super("password"); + } +} diff --git a/source/Web/Frontend/src/app/components/input/text.input.component.ts b/source/Web/Frontend/src/app/components/input/text.input.component.ts new file mode 100644 index 00000000..87fd800f --- /dev/null +++ b/source/Web/Frontend/src/app/components/input/text.input.component.ts @@ -0,0 +1,27 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { AppInputComponent } from "./input.component"; + +@Component({ + selector: "app-input-text", + templateUrl: "./input.component.html", + standalone: true, + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: AppInputTextComponent, multi: true }], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule + ] +}) +export class AppInputTextComponent extends AppInputComponent { + @Input() autofocus = false; + @Input() class!: string; + @Input() disabled = false; + @Input() formControlName!: string; + @Input() text!: string; + + constructor() { + super("text"); + } +} diff --git a/source/Web/Frontend/src/app/components/label/label.component.html b/source/Web/Frontend/src/app/components/label/label.component.html new file mode 100644 index 00000000..28be30c0 --- /dev/null +++ b/source/Web/Frontend/src/app/components/label/label.component.html @@ -0,0 +1 @@ + diff --git a/source/Web/Frontend/src/app/components/label/label.component.ts b/source/Web/Frontend/src/app/components/label/label.component.ts new file mode 100644 index 00000000..b3a2ae8f --- /dev/null +++ b/source/Web/Frontend/src/app/components/label/label.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "app-label", + templateUrl: "./label.component.html", + standalone: true +}) +export class AppLabelComponent { + @Input() for!: string; + @Input() text!: string; +} diff --git a/source/Web/Frontend/src/app/components/select/comment.select.component.ts b/source/Web/Frontend/src/app/components/select/comment.select.component.ts new file mode 100644 index 00000000..4b11be35 --- /dev/null +++ b/source/Web/Frontend/src/app/components/select/comment.select.component.ts @@ -0,0 +1,34 @@ +import { CommonModule } from "@angular/common"; +import { HttpClient } from "@angular/common/http"; +import { Component, Input } from "@angular/core"; +import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { Observable, of } from "rxjs"; +import { map, mergeMap, toArray } from "rxjs/operators"; +import { AppSelectComponent } from "./select.component"; +import { OptionModel } from "./option.model"; + +@Component({ + selector: "app-select-comment", + templateUrl: "./select.component.html", + standalone: true, + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: AppSelectCommentComponent, multi: true }], + imports: [CommonModule, FormsModule, ReactiveFormsModule] +}) +export class AppSelectCommentComponent extends AppSelectComponent { + @Input() autofocus = false; + @Input() child!: AppSelectComponent; + @Input() class!: string; + @Input() disabled = false; + @Input() formControlName!: string; + @Input() text!: string; + + constructor(private readonly http: HttpClient) { super(); this.load(); } + + get(postId: number): Observable { + if (!postId) return of(); + + return this.http + .get(`https://jsonplaceholder.cypress.io/comments?postId=${postId}`) + .pipe(mergeMap((option: any) => option), map((option: any) => new OptionModel(option.id, option.name)), toArray()); + } +} diff --git a/source/Web/Frontend/src/app/components/select/option.model.ts b/source/Web/Frontend/src/app/components/select/option.model.ts new file mode 100644 index 00000000..d062d4c1 --- /dev/null +++ b/source/Web/Frontend/src/app/components/select/option.model.ts @@ -0,0 +1,3 @@ +export class OptionModel { + constructor(public value: any, public text: string) { } +} diff --git a/source/Web/Frontend/src/app/components/select/post.select.component.ts b/source/Web/Frontend/src/app/components/select/post.select.component.ts new file mode 100644 index 00000000..33abd203 --- /dev/null +++ b/source/Web/Frontend/src/app/components/select/post.select.component.ts @@ -0,0 +1,34 @@ +import { CommonModule } from "@angular/common"; +import { HttpClient } from "@angular/common/http"; +import { Component, Input } from "@angular/core"; +import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { Observable, of } from "rxjs"; +import { map, mergeMap, toArray } from "rxjs/operators"; +import { AppSelectComponent } from "./select.component"; +import { OptionModel } from "./option.model"; + +@Component({ + selector: "app-select-post", + templateUrl: "./select.component.html", + standalone: true, + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: AppSelectPostComponent, multi: true }], + imports: [CommonModule, FormsModule, ReactiveFormsModule] +}) +export class AppSelectPostComponent extends AppSelectComponent { + @Input() autofocus = false; + @Input() child!: AppSelectComponent; + @Input() class!: string; + @Input() disabled = false; + @Input() formControlName!: string; + @Input() text!: string; + + constructor(private readonly http: HttpClient) { super(); this.load(); } + + get(userId: number): Observable { + if (!userId) return of(); + + return this.http + .get(`https://jsonplaceholder.cypress.io/posts?userId=${userId}`) + .pipe(mergeMap((option: any) => option), map((option: any) => new OptionModel(option.id, option.title)), toArray()); + } +} diff --git a/source/Web/Frontend/src/app/components/select/select.component.html b/source/Web/Frontend/src/app/components/select/select.component.html new file mode 100644 index 00000000..c416bb75 --- /dev/null +++ b/source/Web/Frontend/src/app/components/select/select.component.html @@ -0,0 +1,12 @@ + diff --git a/source/Web/Frontend/src/app/components/select/select.component.ts b/source/Web/Frontend/src/app/components/select/select.component.ts new file mode 100644 index 00000000..276c0a35 --- /dev/null +++ b/source/Web/Frontend/src/app/components/select/select.component.ts @@ -0,0 +1,31 @@ +import { Observable } from "rxjs"; +import { AppComponent } from "src/app/components/component"; +import { OptionModel } from "src/app/components/select/option.model"; + +export abstract class AppSelectComponent extends AppComponent { + abstract child: AppSelectComponent; + + options = new Array(); + + clear = () => this.options = new Array(); + + abstract get(parameter?: any): Observable; + + load = (parameter?: any) => this.get(parameter).subscribe((options: OptionModel[]) => this.options = options); + + override writeValue(value: any) { this.value = value; this.change(); } + + change() { + if (!this.child) return; + + let child = this.child; + + while (child) { + child.value = undefined; + child.clear(); + child = child.child; + } + + this.child.load(this.value); + } +} diff --git a/source/Web/Frontend/src/app/components/select/user.select.component.ts b/source/Web/Frontend/src/app/components/select/user.select.component.ts new file mode 100644 index 00000000..ab3b903f --- /dev/null +++ b/source/Web/Frontend/src/app/components/select/user.select.component.ts @@ -0,0 +1,32 @@ +import { CommonModule } from "@angular/common"; +import { HttpClient } from "@angular/common/http"; +import { Component, Input } from "@angular/core"; +import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { Observable } from "rxjs"; +import { map, mergeMap, toArray } from "rxjs/operators"; +import { AppSelectComponent } from "./select.component"; +import { OptionModel } from "./option.model"; + +@Component({ + selector: "app-select-user", + templateUrl: "./select.component.html", + standalone: true, + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: AppSelectUserComponent, multi: true }], + imports: [CommonModule, FormsModule, ReactiveFormsModule] +}) +export class AppSelectUserComponent extends AppSelectComponent { + @Input() autofocus = false; + @Input() child!: AppSelectComponent; + @Input() class!: string; + @Input() disabled = false; + @Input() formControlName!: string; + @Input() text!: string; + + constructor(private readonly http: HttpClient) { super(); this.load(); } + + get(_: any): Observable { + return this.http + .get("https://jsonplaceholder.cypress.io/users") + .pipe(mergeMap((option: any) => option), map((option: any) => new OptionModel(option.id, option.name)), toArray()); + } +} diff --git a/source/Web/Frontend/src/app/layouts/footer/footer.component.html b/source/Web/Frontend/src/app/layouts/footer/footer.component.html new file mode 100644 index 00000000..27825f6e --- /dev/null +++ b/source/Web/Frontend/src/app/layouts/footer/footer.component.html @@ -0,0 +1,3 @@ +
+ Copyright - All Rights Reserved +
diff --git a/source/Web/Frontend/src/app/layouts/footer/footer.component.ts b/source/Web/Frontend/src/app/layouts/footer/footer.component.ts new file mode 100644 index 00000000..b09d7654 --- /dev/null +++ b/source/Web/Frontend/src/app/layouts/footer/footer.component.ts @@ -0,0 +1,8 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-footer", + templateUrl: "./footer.component.html", + standalone: true +}) +export class AppFooterComponent { } diff --git a/source/Web/Frontend/src/app/layouts/header/header.component.html b/source/Web/Frontend/src/app/layouts/header/header.component.html new file mode 100644 index 00000000..e011d22c --- /dev/null +++ b/source/Web/Frontend/src/app/layouts/header/header.component.html @@ -0,0 +1,3 @@ +
+

TITLE

+
diff --git a/source/Web/Frontend/src/app/layouts/header/header.component.ts b/source/Web/Frontend/src/app/layouts/header/header.component.ts new file mode 100644 index 00000000..08fb0f33 --- /dev/null +++ b/source/Web/Frontend/src/app/layouts/header/header.component.ts @@ -0,0 +1,8 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-header", + templateUrl: "./header.component.html", + standalone: true +}) +export class AppHeaderComponent { } diff --git a/source/Web/Frontend/src/app/layouts/layout-nav/layout-nav.component.html b/source/Web/Frontend/src/app/layouts/layout-nav/layout-nav.component.html new file mode 100644 index 00000000..c841dd47 --- /dev/null +++ b/source/Web/Frontend/src/app/layouts/layout-nav/layout-nav.component.html @@ -0,0 +1,12 @@ + + +
+
+ +
+
+ +
+
+ + diff --git a/source/Web/Frontend/src/app/layouts/layout-nav/layout-nav.component.ts b/source/Web/Frontend/src/app/layouts/layout-nav/layout-nav.component.ts new file mode 100644 index 00000000..429bc502 --- /dev/null +++ b/source/Web/Frontend/src/app/layouts/layout-nav/layout-nav.component.ts @@ -0,0 +1,18 @@ +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { AppFooterComponent } from "src/app/layouts/footer/footer.component"; +import { AppHeaderComponent } from "src/app/layouts/header/header.component"; +import { AppNavComponent } from "src/app/layouts/nav/nav.component"; + +@Component({ + selector: "app-layout-nav", + templateUrl: "./layout-nav.component.html", + standalone: true, + imports: [ + RouterModule, + AppFooterComponent, + AppHeaderComponent, + AppNavComponent + ] +}) +export class AppLayoutNavComponent { } diff --git a/source/Web/Frontend/src/app/layouts/layout/layout.component.html b/source/Web/Frontend/src/app/layouts/layout/layout.component.html new file mode 100644 index 00000000..e51e2c1e --- /dev/null +++ b/source/Web/Frontend/src/app/layouts/layout/layout.component.html @@ -0,0 +1,7 @@ + + +
+ +
+ + diff --git a/source/Web/Frontend/src/app/layouts/layout/layout.component.ts b/source/Web/Frontend/src/app/layouts/layout/layout.component.ts new file mode 100644 index 00000000..56cf4351 --- /dev/null +++ b/source/Web/Frontend/src/app/layouts/layout/layout.component.ts @@ -0,0 +1,16 @@ +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { AppFooterComponent } from "src/app/layouts/footer/footer.component"; +import { AppHeaderComponent } from "src/app/layouts/header/header.component"; + +@Component({ + selector: "app-layout", + templateUrl: "./layout.component.html", + standalone: true, + imports: [ + RouterModule, + AppFooterComponent, + AppHeaderComponent + ] +}) +export class AppLayoutComponent { } diff --git a/source/Web/Frontend/src/app/layouts/nav/nav.component.html b/source/Web/Frontend/src/app/layouts/nav/nav.component.html new file mode 100644 index 00000000..7caa6151 --- /dev/null +++ b/source/Web/Frontend/src/app/layouts/nav/nav.component.html @@ -0,0 +1,34 @@ + diff --git a/source/Web/Frontend/src/app/layouts/nav/nav.component.ts b/source/Web/Frontend/src/app/layouts/nav/nav.component.ts new file mode 100644 index 00000000..84592dae --- /dev/null +++ b/source/Web/Frontend/src/app/layouts/nav/nav.component.ts @@ -0,0 +1,17 @@ +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { AppAuthService } from "src/app/services/auth.service"; + +@Component({ + selector: "app-nav", + templateUrl: "./nav.component.html", + standalone: true, + imports: [ + RouterModule + ] +}) +export class AppNavComponent { + constructor(private readonly appAuthService: AppAuthService) { } + + signout = () => this.appAuthService.signout(); +} diff --git a/source/Web/Frontend/src/app/models/auth.model.ts b/source/Web/Frontend/src/app/models/auth.model.ts new file mode 100644 index 00000000..46f45ae3 --- /dev/null +++ b/source/Web/Frontend/src/app/models/auth.model.ts @@ -0,0 +1,4 @@ +export class AuthModel { + login!: string; + password!: string; +} diff --git a/source/Web/Frontend/src/app/models/user.model.ts b/source/Web/Frontend/src/app/models/user.model.ts new file mode 100644 index 00000000..7ce01ba5 --- /dev/null +++ b/source/Web/Frontend/src/app/models/user.model.ts @@ -0,0 +1,8 @@ +import { AuthModel } from "./auth.model"; + +export class UserModel { + id!: number; + name!: string; + email!: string; + auth!: AuthModel; +} diff --git a/source/Web/Frontend/src/app/pages/auth/auth.component.html b/source/Web/Frontend/src/app/pages/auth/auth.component.html new file mode 100644 index 00000000..222d2e98 --- /dev/null +++ b/source/Web/Frontend/src/app/pages/auth/auth.component.html @@ -0,0 +1,17 @@ +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
diff --git a/source/Web/Frontend/src/app/pages/auth/auth.component.ts b/source/Web/Frontend/src/app/pages/auth/auth.component.ts new file mode 100644 index 00000000..f5d82f9c --- /dev/null +++ b/source/Web/Frontend/src/app/pages/auth/auth.component.ts @@ -0,0 +1,35 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; +import { AppAuthService } from "src/app/services/auth.service"; +import { AppButtonComponent } from "src/app/components/button/button.component"; +import { AppInputPasswordComponent } from "src/app/components/input/password.input.component"; +import { AppInputTextComponent } from "src/app/components/input/text.input.component"; +import { AppLabelComponent } from "src/app/components/label/label.component"; +import { AuthModel } from "src/app/models/auth.model"; + +@Component({ + selector: "app-auth", + templateUrl: "./auth.component.html", + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + AppButtonComponent, + AppInputPasswordComponent, + AppInputTextComponent, + AppLabelComponent + ] +}) +export class AppAuthComponent { + form = inject(FormBuilder).group({ + login: ["admin", Validators.required], + password: ["admin", Validators.required] + }); + + constructor(private readonly appAuthService: AppAuthService) { } + + auth() { + this.appAuthService.auth(this.form.value as AuthModel); + } +} diff --git a/source/Web/Frontend/src/app/pages/files/files.component.html b/source/Web/Frontend/src/app/pages/files/files.component.html new file mode 100644 index 00000000..9c2bf8c1 --- /dev/null +++ b/source/Web/Frontend/src/app/pages/files/files.component.html @@ -0,0 +1,13 @@ +

Files

+ + + +
+

Files

+ +
    + @for (file of files; track file) { +
  • {{ file | json }}
  • + } +
+
diff --git a/source/Web/Frontend/src/app/pages/files/files.component.ts b/source/Web/Frontend/src/app/pages/files/files.component.ts new file mode 100644 index 00000000..ecac219d --- /dev/null +++ b/source/Web/Frontend/src/app/pages/files/files.component.ts @@ -0,0 +1,19 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { AppFileComponent } from "src/app/components/file/file.component"; +import { FileModel } from "src/app/components/file/file.model"; + +@Component({ + selector: "app-files", + templateUrl: "./files.component.html", + standalone: true, + imports: [ + CommonModule, + FormsModule, + AppFileComponent + ] +}) +export class AppFilesComponent { + files = new Array(); +} diff --git a/source/Web/Frontend/src/app/pages/form/form.component.html b/source/Web/Frontend/src/app/pages/form/form.component.html new file mode 100644 index 00000000..094d1a61 --- /dev/null +++ b/source/Web/Frontend/src/app/pages/form/form.component.html @@ -0,0 +1,26 @@ +

Form

+ +
+ + +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
{{ this.form.value | json }}
+
+
+
diff --git a/source/Web/Frontend/src/app/pages/form/form.component.ts b/source/Web/Frontend/src/app/pages/form/form.component.ts new file mode 100644 index 00000000..7de96c8c --- /dev/null +++ b/source/Web/Frontend/src/app/pages/form/form.component.ts @@ -0,0 +1,35 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { AppButtonComponent } from "src/app/components/button/button.component"; +import { AppLabelComponent } from "src/app/components/label/label.component"; +import { AppSelectCommentComponent } from "src/app/components/select/comment.select.component"; +import { AppSelectPostComponent } from "src/app/components/select/post.select.component"; +import { AppSelectUserComponent } from "src/app/components/select/user.select.component"; + +@Component({ + selector: "app-form", + templateUrl: "./form.component.html", + standalone: true, + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + AppButtonComponent, + AppLabelComponent, + AppSelectCommentComponent, + AppSelectPostComponent, + AppSelectUserComponent + ] +}) +export class AppFormComponent { + disabled = false; + + form = inject(FormBuilder).group({ + userId: [0, Validators.required], + postId: [0, Validators.required], + commentId: [0, Validators.required] + }); + + set = () => this.form.patchValue({ userId: 10, postId: 100, commentId: 500 }); +} diff --git a/source/Web/Frontend/src/app/pages/home/home.component.html b/source/Web/Frontend/src/app/pages/home/home.component.html new file mode 100644 index 00000000..277f8e93 --- /dev/null +++ b/source/Web/Frontend/src/app/pages/home/home.component.html @@ -0,0 +1 @@ +

Home

diff --git a/source/Web/Frontend/src/app/pages/home/home.component.ts b/source/Web/Frontend/src/app/pages/home/home.component.ts new file mode 100644 index 00000000..64201e2e --- /dev/null +++ b/source/Web/Frontend/src/app/pages/home/home.component.ts @@ -0,0 +1,8 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-home", + templateUrl: "./home.component.html", + standalone: true +}) +export class AppHomeComponent { } diff --git a/source/Web/Frontend/src/app/pages/list/grid/grid.component.html b/source/Web/Frontend/src/app/pages/list/grid/grid.component.html new file mode 100644 index 00000000..c85eb39f --- /dev/null +++ b/source/Web/Frontend/src/app/pages/list/grid/grid.component.html @@ -0,0 +1,31 @@ +
+ + + + + + + + + + @for (item of grid.list; track item.id) { + + + + + + } + +
+ + + + + + + + +
{{ item.id }}{{ item.name }}{{ item.email }}
+
+ + diff --git a/source/Web/Frontend/src/app/pages/list/grid/grid.component.ts b/source/Web/Frontend/src/app/pages/list/grid/grid.component.ts new file mode 100644 index 00000000..57a61a67 --- /dev/null +++ b/source/Web/Frontend/src/app/pages/list/grid/grid.component.ts @@ -0,0 +1,61 @@ +import { CommonModule } from "@angular/common"; +import { Component, inject } from "@angular/core"; +import { FormBuilder, FormControl, ReactiveFormsModule } from "@angular/forms"; +import { debounceTime } from "rxjs/operators"; +import { AppButtonComponent } from "src/app/components/button/button.component"; +import { AppInputTextComponent } from "src/app/components/input/text.input.component"; +import { AppOrderComponent } from "src/app/components/grid/order/order.component"; +import { AppPageComponent } from "src/app/components/grid/page/page.component"; +import { AppUserService } from "src/app/services/user.service"; +import { GridModel } from "src/app/components/grid/grid.model"; +import { GridParametersModel } from "src/app/components/grid/grid-parameters.model"; +import { UserModel } from "src/app/models/user.model"; + +@Component({ + selector: "app-list-grid", + templateUrl: "./grid.component.html", + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + AppOrderComponent, + AppPageComponent, + AppButtonComponent, + AppInputTextComponent + ] +}) +export class AppListGridComponent { + filters = inject(FormBuilder).group({ + Id: new FormControl(), + Name: new FormControl(), + Email: new FormControl() + }); + + grid = new GridModel(); + + constructor(private readonly appUserService: AppUserService) { + this.init(); + } + + load() { + this.appUserService.grid(this.grid.parameters).subscribe((grid) => this.grid = grid); + } + + private filter() { + this.reset(); + this.grid.parameters.filters.addFromFormGroup(this.filters); + this.load(); + } + + private init() { + this.reset(); + this.grid.parameters.order.property = "Id"; + this.filters.valueChanges.pipe(debounceTime(0)).subscribe(() => this.filter()); + this.load(); + } + + private reset() { + this.grid = new GridModel(); + this.grid.parameters = new GridParametersModel(); + } +} diff --git a/source/Web/Frontend/src/app/pages/list/list.component.html b/source/Web/Frontend/src/app/pages/list/list.component.html new file mode 100644 index 00000000..1b00444d --- /dev/null +++ b/source/Web/Frontend/src/app/pages/list/list.component.html @@ -0,0 +1,3 @@ +

List

+ + diff --git a/source/Web/Frontend/src/app/pages/list/list.component.ts b/source/Web/Frontend/src/app/pages/list/list.component.ts new file mode 100644 index 00000000..68ce537d --- /dev/null +++ b/source/Web/Frontend/src/app/pages/list/list.component.ts @@ -0,0 +1,12 @@ +import { Component } from "@angular/core"; +import { AppListGridComponent } from "./grid/grid.component"; + +@Component({ + selector: "app-list", + templateUrl: "./list.component.html", + standalone: true, + imports: [ + AppListGridComponent + ] +}) +export class AppListComponent { } diff --git a/source/Web/Frontend/src/app/services/auth.service.ts b/source/Web/Frontend/src/app/services/auth.service.ts new file mode 100644 index 00000000..4e7e3a74 --- /dev/null +++ b/source/Web/Frontend/src/app/services/auth.service.ts @@ -0,0 +1,32 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; +import { AuthModel } from "src/app/models/auth.model"; + +@Injectable({ providedIn: "root" }) +export class AppAuthService { + constructor( + private readonly http: HttpClient, + private readonly router: Router) { } + + authenticated = () => !!this.token(); + + auth(auth: AuthModel): void { + this.http + .post("api/auths", auth) + .subscribe((result: any) => { + if (!result || !result.token) return; + localStorage.setItem("token", result.token); + this.router.navigate(["/main/home"]); + }); + } + + signin = () => this.router.navigate(["/auth"]); + + signout() { + localStorage.clear(); + this.signin(); + } + + token = () => localStorage.getItem("token"); +} diff --git a/source/Web/Frontend/src/app/services/modal.service.ts b/source/Web/Frontend/src/app/services/modal.service.ts new file mode 100644 index 00000000..cd1d1b76 --- /dev/null +++ b/source/Web/Frontend/src/app/services/modal.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from "@angular/core"; + +declare let UIkit: any; + +@Injectable({ providedIn: "root" }) +export class AppModalService { + alert = (message: string) => UIkit.modal.alert(message); +} diff --git a/source/Web/Frontend/src/app/services/user.service.ts b/source/Web/Frontend/src/app/services/user.service.ts new file mode 100644 index 00000000..4662cb9b --- /dev/null +++ b/source/Web/Frontend/src/app/services/user.service.ts @@ -0,0 +1,26 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { GridParametersModel } from "src/app/components/grid/grid-parameters.model"; +import { GridService } from "src/app/components/grid/grid.service"; +import { UserModel } from "src/app/models/user.model"; + +@Injectable({ providedIn: "root" }) +export class AppUserService { + constructor( + private readonly http: HttpClient, + private readonly gridService: GridService) { } + + add = (user: UserModel) => this.http.post("api/users", user); + + delete = (id: number) => this.http.delete(`api/users/${id}`); + + get = (id: number) => this.http.get(`api/users/${id}`); + + grid = (parameters: GridParametersModel) => this.gridService.get(`api/users/grid`, parameters); + + inactivate = (id: number) => this.http.patch(`api/users/${id}/inactivate`, {}); + + list = () => this.http.get("api/users"); + + update = (user: UserModel) => this.http.put(`api/users/${user.id}`, user); +} diff --git a/source/Web/Frontend/src/app/settings/settings.model.ts b/source/Web/Frontend/src/app/settings/settings.model.ts new file mode 100644 index 00000000..5dc453da --- /dev/null +++ b/source/Web/Frontend/src/app/settings/settings.model.ts @@ -0,0 +1,3 @@ +export class SettingsModel { + readonly api!: string; +} diff --git a/source/Web/Frontend/src/app/settings/settings.service.ts b/source/Web/Frontend/src/app/settings/settings.service.ts new file mode 100644 index 00000000..8f92f88a --- /dev/null +++ b/source/Web/Frontend/src/app/settings/settings.service.ts @@ -0,0 +1,12 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { SettingsModel } from "./settings.model"; + +@Injectable({ providedIn: "root" }) +export class AppSettingsService { + settings!: SettingsModel; + + constructor(private http: HttpClient) { + this.http.get("./assets/settings.json").subscribe((settings) => this.settings = settings); + } +} diff --git a/source/Web/Frontend/src/assets/settings.json b/source/Web/Frontend/src/assets/settings.json new file mode 100644 index 00000000..dcfc7767 --- /dev/null +++ b/source/Web/Frontend/src/assets/settings.json @@ -0,0 +1,3 @@ +{ + "api": "https://localhost" +} diff --git a/source/Web/Frontend/src/favicon.ico b/source/Web/Frontend/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..997406ad22c29aae95893fb3d666c30258a09537 GIT binary patch literal 948 zcmV;l155mgP)CBYU7IjCFmI-B}4sMJt3^s9NVg!P0 z6hDQy(L`XWMkB@zOLgN$4KYz;j0zZxq9KKdpZE#5@k0crP^5f9KO};h)ZDQ%ybhht z%t9#h|nu0K(bJ ztIkhEr!*UyrZWQ1k2+YkGqDi8Z<|mIN&$kzpKl{cNP=OQzXHz>vn+c)F)zO|Bou>E z2|-d_=qY#Y+yOu1a}XI?cU}%04)zz%anD(XZC{#~WreV!a$7k2Ug`?&CUEc0EtrkZ zL49MB)h!_K{H(*l_93D5tO0;BUnvYlo+;yss%n^&qjt6fZOa+}+FDO(~2>G z2dx@=JZ?DHP^;b7*Y1as5^uphBsh*s*z&MBd?e@I>-9kU>63PjP&^#5YTOb&x^6Cf z?674rmSHB5Fk!{Gv7rv!?qX#ei_L(XtwVqLX3L}$MI|kJ*w(rhx~tc&L&xP#?cQow zX_|gx$wMr3pRZIIr_;;O|8fAjd;1`nOeu5K(pCu7>^3E&D2OBBq?sYa(%S?GwG&_0-s%_v$L@R!5H_fc)lOb9ZoOO#p`Nn`KU z3LTTBtjwo`7(HA6 z7gmO$yTR!5L>Bsg!X8616{JUngg_@&85%>W=mChTR;x4`P=?PJ~oPuy5 zU-L`C@_!34D21{fD~Y8NVnR3t;aqZI3fIhmgmx}$oc-dKDC6Ap$Gy>a!`A*x2L1v0 WcZ@i?LyX}70000 + + + + + + + + + + + + App + + + + + + + diff --git a/source/Web/Frontend/src/main.ts b/source/Web/Frontend/src/main.ts new file mode 100644 index 00000000..77dc808a --- /dev/null +++ b/source/Web/Frontend/src/main.ts @@ -0,0 +1,4 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app/app.module'; + +platformBrowserDynamic().bootstrapModule(AppModule).catch((error: any) => console.error(error)); diff --git a/source/Web/Frontend/src/styles/style.scss b/source/Web/Frontend/src/styles/style.scss new file mode 100644 index 00000000..efbf3d7f --- /dev/null +++ b/source/Web/Frontend/src/styles/style.scss @@ -0,0 +1,71 @@ +app-layout, +app-layout-nav { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +app-header { + background-color: #0A69b7; + padding: 1rem; +} + +app-header * { + color: #FFF; + margin: 0; +} + +main { + align-content: start; + background-color: #FFF; + flex: auto; + padding: 1rem !important; +} + +app-footer { + background-color: #242729; + padding: 0.5rem; + text-align: center; +} + +app-footer * { + color: #BBB; + margin: 0; +} + +input.ng-dirty.ng-invalid, +input.ng-touched.ng-invalid, +select.ng-dirty.ng-invalid, +select.ng-touched.ng-invalid, +textarea.ng-dirty.ng-invalid, +textarea.ng-touched.ng-invalid, +.ng-dirty.ng-invalid>input, +.ng-touched.ng-invalid>input, +.ng-dirty.ng-invalid>select, +.ng-touched.ng-invalid>select, +.ng-dirty.ng-invalid>textarea, +.ng-touched.ng-invalid>textarea { + background-color: #FEF4F6; + border: 0.05rem solid #F0506E; + color: #F0506E; +} + +input:disabled, +select:disabled, +textarea:disabled, +:disabled>input, +:disabled>select, +:disabled>textarea { + background-color: #F8F8F8 !important; + border-color: #E5E5E5 !important; + color: #999 !important; +} + +svg { + vertical-align: text-top; +} + +button, +table thead th { + user-select: none; +} diff --git a/source/Web/Frontend/tsconfig.json b/source/Web/Frontend/tsconfig.json new file mode 100644 index 00000000..bf5c5aa6 --- /dev/null +++ b/source/Web/Frontend/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "allowJs": true, + "baseUrl": "./", + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "importHelpers": true, + "module": "ES2022", + "moduleResolution": "node", + "noErrorTruncation": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "removeComments": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2022", + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": [ + "src/main.ts" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +} diff --git a/source/Web/Program.cs b/source/Web/Program.cs new file mode 100644 index 00000000..1746e513 --- /dev/null +++ b/source/Web/Program.cs @@ -0,0 +1,28 @@ +var builder = WebApplication.CreateBuilder(); + +builder.Host.UseSerilog(); + +builder.Services.AddResponseCompression(); +builder.Services.AddJsonStringLocalizer(); +builder.Services.AddHashService(); +builder.Services.AddJwtService(); +builder.Services.AddAuthorization().AddAuthentication().AddJwtBearer(); +builder.Services.AddContext(options => options.UseSqlServer(builder.Configuration.GetConnectionString(nameof(Context)))); +builder.Services.AddClassesMatchingInterfaces(nameof(Architecture)); +builder.Services.AddMediator(nameof(Architecture)); +builder.Services.AddSwaggerDefault(); +builder.Services.AddControllers().AddJsonOptions().AddAuthorizationPolicy(); + +var application = builder.Build(); + +application.UseException(); +application.UseHsts().UseHttpsRedirection(); +application.UseLocalization("en", "pt"); +application.UseResponseCompression(); +application.UseStaticFiles(); +application.UseSwagger().UseSwaggerUI(); +application.UseRouting(); +application.MapControllers(); +application.MapFallbackToFile("index.html"); + +application.Run(); diff --git a/source/Web/Properties/launchSettings.json b/source/Web/Properties/launchSettings.json new file mode 100644 index 00000000..bb35014e --- /dev/null +++ b/source/Web/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "Run": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + }, + "launchBrowser": true, + "applicationUrl": "https://localhost:8090" + } + } +} diff --git a/source/global.json b/source/global.json new file mode 100644 index 00000000..b64969b5 --- /dev/null +++ b/source/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.300", + "rollForward": "latestMajor" + } +}