Compare commits
21 Commits
0b6bb019b6
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| fda184726f | |||
| d26b230f91 | |||
| 710735f56d | |||
|
9452732730
|
|||
| 53190c2db0 | |||
|
a9f29fff6e
|
|||
| ee46cf09f4 | |||
|
d853ac7486
|
|||
| 06675219e0 | |||
| b75c4152fb | |||
|
3407f9ec51
|
|||
| f544bc0b2c | |||
|
74bae6d90c
|
|||
| 9d6ba08be2 | |||
|
2598adb286
|
|||
| b10b6cca60 | |||
|
bbb4fec581
|
|||
| ecf84016dc | |||
|
10f8d36727
|
|||
| 2c4f1ff440 | |||
|
93a78e4614
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
||||
<changelist name="Uncommitted_changes_before_Checkout_at_11_03_26,_18_04_[Changes]" date="1773248701579" recycled="true" deleted="true">
|
||||
<option name="PATH" value="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/shelf/Uncommitted_changes_before_Checkout_at_11_03_26,_18_04_[Changes]/shelved.patch" />
|
||||
<option name="DESCRIPTION" value="Uncommitted changes before Checkout at 11.03.26, 18:04 [Changes]" />
|
||||
</changelist>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
||||
<changelist name="Uncommitted_changes_before_Update_at_09_03_26,_12_36_[Changes]" date="1773056200358" recycled="false" toDelete="true">
|
||||
<option name="PATH" value="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/shelf/Uncommitted_changes_before_Update_at_09_03_26,_12_36_[Changes]/shelved.patch" />
|
||||
<option name="DESCRIPTION" value="Uncommitted changes before Update at 09.03.26, 12:36 [Changes]" />
|
||||
</changelist>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
||||
<changelist name="Uncommitted_changes_before_Update_at_11_03_26,_18_06_[Changes]" date="1773248835929" recycled="true" deleted="true">
|
||||
<option name="PATH" value="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/shelf/Uncommitted_changes_before_Update_at_11_03_26,_18_06_[Changes]/shelved.patch" />
|
||||
<option name="DESCRIPTION" value="Uncommitted changes before Update at 11.03.26, 18:06 [Changes]" />
|
||||
</changelist>
|
||||
2
.idea/.idea.DotNetAngular/.idea/watcherTasks.xml
generated
2
.idea/.idea.DotNetAngular/.idea/watcherTasks.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions">
|
||||
<TaskOptions isEnabled="true">
|
||||
<TaskOptions isEnabled="false">
|
||||
<option name="arguments" value="$FileName$:$FileNameWithoutExtension$.css" />
|
||||
<option name="checkSyntaxErrors" value="true" />
|
||||
<option name="description" />
|
||||
|
||||
137
.idea/.idea.DotNetAngular/.idea/workspace.xml
generated
137
.idea/.idea.DotNetAngular/.idea/workspace.xml
generated
@@ -13,17 +13,41 @@
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="1ac72a4a-52ad-4e70-9b15-c330b1ed3e7a" name="Changes" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/inspectionProfiles/Project_Default.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.html" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.scss" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.spec.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/admin-dashboard/admin-dashboard.component.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.html" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.scss" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.spec.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/unauthorized/unauthorized.component.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.html" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.scss" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.spec.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-dashboard/user-dashboard.component.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-table/user-table.component.html" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-table/user-table.component.scss" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-table/user-table.component.spec.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/user-table/user-table.component.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/admin.guard.spec.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/admin.guard.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/authentication.guard.spec.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/authentication.guard.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/guest.guard.spec.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/guards/guest.guard.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/interceptors/token.interceptor.spec.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/interceptors/token.interceptor.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.html" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.scss" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.spec.ts" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/shared/popup-modal/popup-modal.component.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.DotNetAngular/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/API/API.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/API/API.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/API/Extension/ServiceCollectionExtensions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/API/Extension/ServiceCollectionExtensions.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/API/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/API/Program.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Application/Application.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/Application/Application.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/user.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/infrastructure/services/user.service.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Domain/Domain.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/Domain/Domain.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/Infrastructure/Infrastructure.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/src/Infrastructure/Infrastructure.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/tests/Application.FunctionalTest/Application.FunctionalTest.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/tests/Application.FunctionalTest/Application.FunctionalTest.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/tests/Application.UnitTest/Application.UnitTest.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/tests/Application.UnitTest/Application.UnitTest.csproj" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/app.config.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/app.config.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/app.routes.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/app.routes.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/domain/entities/user.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/domain/entities/user.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/authentication/login/login.component.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/authentication/login/login.component.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/header/header.component.html" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/header/header.component.html" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/header/header.component.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/ClientApp/src/app/presentation/components/header/header.component.ts" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -34,7 +58,7 @@
|
||||
<option name="firstShow" value="false" />
|
||||
</component>
|
||||
<component name="EmbeddingIndexingInfo">
|
||||
<option name="cachedIndexableFilesCount" value="2" />
|
||||
<option name="cachedIndexableFilesCount" value="17" />
|
||||
<option name="fileBasedEmbeddingIndicesEnabled" value="true" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
@@ -46,6 +70,7 @@
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="file://$PROJECT_DIR$/dotnet-tools.json" root0="FORCE_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/src/ClientApp/package-lock.json" root0="SKIP_HIGHLIGHTING" />
|
||||
<setting file="file://$PROJECT_DIR$/src/ClientApp/package.json" root0="FORCE_HIGHLIGHTING" />
|
||||
</component>
|
||||
@@ -54,6 +79,9 @@
|
||||
<urls />
|
||||
</component>
|
||||
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
|
||||
<component name="ProblemsViewState">
|
||||
<option name="selectedTabId" value="Toolset" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 5
|
||||
}</component>
|
||||
@@ -71,21 +99,21 @@
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
||||
"git-widget-placeholder": "feature/dotnet",
|
||||
"git-widget-placeholder": "feature/guards",
|
||||
"junie.onboarding.icon.badge.shown": "true",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "vcs.Git",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"ts.external.directory.path": "/home/natlinux/RiderProjects/DotNetAngular/src/ClientApp/node_modules/typescript/lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
<component name="RecapUselessUpdatesCounter">
|
||||
<option name="suspendCountdown" value="1" />
|
||||
<option name="suspendCountdown" value="0" />
|
||||
</component>
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.API: Angular_dev">
|
||||
<configuration name="Application.FunctionalTest" type="DotNetProject" factoryName=".NET Project">
|
||||
@@ -210,7 +238,22 @@
|
||||
<workItem from="1770204235235" duration="1487000" />
|
||||
<workItem from="1770294298887" duration="1216000" />
|
||||
<workItem from="1770295524085" duration="8729000" />
|
||||
<workItem from="1770308954185" duration="2271000" />
|
||||
<workItem from="1770308954185" duration="9016000" />
|
||||
<workItem from="1770369615769" duration="3456000" />
|
||||
<workItem from="1770373101221" duration="2750000" />
|
||||
<workItem from="1770377559983" duration="818000" />
|
||||
<workItem from="1770379380312" duration="1252000" />
|
||||
<workItem from="1770382791102" duration="1343000" />
|
||||
<workItem from="1771239672703" duration="359000" />
|
||||
<workItem from="1771331713319" duration="2451000" />
|
||||
<workItem from="1771503996420" duration="1283000" />
|
||||
<workItem from="1772664092787" duration="54000" />
|
||||
<workItem from="1772664174287" duration="646000" />
|
||||
<workItem from="1773053172796" duration="1432000" />
|
||||
<workItem from="1773597121547" duration="689000" />
|
||||
<workItem from="1773599776473" duration="3799000" />
|
||||
<workItem from="1773605341198" duration="3147000" />
|
||||
<workItem from="1773678089390" duration="1025000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="updating template">
|
||||
<option name="closed" value="true" />
|
||||
@@ -236,7 +279,47 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770306489835</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="4" />
|
||||
<task id="LOCAL-00004" summary="update to dotnet 10">
|
||||
<option name="closed" value="true" />
|
||||
<created>1770318472632</created>
|
||||
<option name="number" value="00004" />
|
||||
<option name="presentableId" value="LOCAL-00004" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1770318472632</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00005" summary="update nuget packages to new versions">
|
||||
<option name="closed" value="true" />
|
||||
<created>1771506336945</created>
|
||||
<option name="number" value="00005" />
|
||||
<option name="presentableId" value="LOCAL-00005" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1771506336945</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00006" summary="update NuGet Package to new Version">
|
||||
<option name="closed" value="true" />
|
||||
<created>1773597809588</created>
|
||||
<option name="number" value="00006" />
|
||||
<option name="presentableId" value="LOCAL-00006" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1773597809588</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00007" summary="update Angular core to new version">
|
||||
<option name="closed" value="true" />
|
||||
<created>1773598259701</created>
|
||||
<option name="number" value="00007" />
|
||||
<option name="presentableId" value="LOCAL-00007" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1773598259701</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00008" summary="fix dropdown">
|
||||
<option name="closed" value="true" />
|
||||
<created>1773601314558</created>
|
||||
<option name="number" value="00008" />
|
||||
<option name="presentableId" value="LOCAL-00008" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1773601314558</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="9" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -245,6 +328,21 @@
|
||||
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="RECENT_FILTERS">
|
||||
<map>
|
||||
<entry key="Branch">
|
||||
<value>
|
||||
<list>
|
||||
<RecentGroup>
|
||||
<option name="FILTER_VALUES">
|
||||
<option value="feature/dotnet" />
|
||||
</option>
|
||||
</RecentGroup>
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
@@ -260,7 +358,12 @@
|
||||
<MESSAGE value="updating template" />
|
||||
<MESSAGE value="update to angular 20" />
|
||||
<MESSAGE value="login.component, register.component" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="login.component, register.component" />
|
||||
<MESSAGE value="update to dotnet 10" />
|
||||
<MESSAGE value="update nuget packages to new versions" />
|
||||
<MESSAGE value="update NuGet Package to new Version" />
|
||||
<MESSAGE value="update Angular core to new version" />
|
||||
<MESSAGE value="fix dropdown" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="fix dropdown" />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
|
||||
13
dotnet-tools.json
Normal file
13
dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.4",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,22 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>9c947c10-2373-4590-92a9-e5fe6b759c69</UserSecretsId>
|
||||
<SpaRoot>..\ClientApp\</SpaRoot>
|
||||
<SpaProxyServerUrl>http://localhost:44492</SpaProxyServerUrl>
|
||||
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="10.0.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="10.0.5" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.5" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.5" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Application\Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
45
src/API/Controllers/AuthController.cs
Normal file
45
src/API/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using API.Extension;
|
||||
using Application.DTOs;
|
||||
using Application.Interfaces;
|
||||
using Application.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class AuthController(IAuthenticationService authService) : BaseApiController
|
||||
{
|
||||
[HttpPost("register")]
|
||||
public async Task<IResult> Register(RegisterRequest registerRequest)
|
||||
{
|
||||
var response = await authService.RegisterAsync(registerRequest);
|
||||
return response.ToHttpResponse();
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IResult> Login(LoginRequest loginRequest)
|
||||
{
|
||||
var response = await authService.LoginAsync(loginRequest);
|
||||
return response.ToHttpResponse();
|
||||
}
|
||||
|
||||
[HttpPost("refresh-token")]
|
||||
public async Task<IResult> RefreshToken(RefreshTokenRequest refreshTokenRequest)
|
||||
{
|
||||
var response = await authService.RefreshTokensAsync(refreshTokenRequest);
|
||||
return response.ToHttpResponse();
|
||||
}
|
||||
|
||||
[HttpPost("send-reset-email/{email}")]
|
||||
public async Task<IResult> SendResetEmail(string email)
|
||||
{
|
||||
var response = await authService.SendResetEmailAsync(email);
|
||||
return response.ToHttpResponse();
|
||||
}
|
||||
|
||||
[HttpPost("reset-password")]
|
||||
public async Task<IResult> ResetPassword(ResetPasswordDto resetPasswordDto)
|
||||
{
|
||||
var response = await authService.ResetPasswordAsync(resetPasswordDto);
|
||||
return response.ToHttpResponse();
|
||||
}
|
||||
}
|
||||
63
src/API/Controllers/UserController.cs
Normal file
63
src/API/Controllers/UserController.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using API.Extension;
|
||||
using Application.Interfaces;
|
||||
using Application.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public class UserController(IUserService userService) : BaseApiController
|
||||
{
|
||||
[Authorize(Roles = "SuperAdmin, Admin")]
|
||||
[HttpGet]
|
||||
public async Task<IResult> GetAllUsers(
|
||||
[FromQuery] int pageNumber = 1,
|
||||
[FromQuery] int pageSize = 10)
|
||||
{
|
||||
var users = await userService.GetAsync(pageNumber, pageSize);
|
||||
return users.ToHttpResponse();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPut]
|
||||
public async Task<IResult> UpdateUser([FromBody] UserUpdateRequest userUpdateRequest)
|
||||
{
|
||||
var result = await userService.UpdateAsync(userUpdateRequest);
|
||||
return result.ToHttpResponse();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IResult> DeleteUser(int id)
|
||||
{
|
||||
var currentUserId = int.Parse(User.FindFirst("UserId")!.Value);
|
||||
var result = await userService.DeleteAsync(id, currentUserId);
|
||||
return result.ToHttpResponse();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IResult> GetUserById(int id)
|
||||
{
|
||||
var user = await userService.GetUserByIdAsync(id);
|
||||
return user.ToHttpResponse();
|
||||
}
|
||||
|
||||
[Authorize(Roles = "SuperAdmin")]
|
||||
[HttpPost("assign-role")]
|
||||
public async Task<IResult> AssignRole([FromBody] AssingRoleRequest roleRequest)
|
||||
|
||||
{
|
||||
var result = await userService.AssignRoleAsync(roleRequest);
|
||||
return result.ToHttpResponse();
|
||||
}
|
||||
|
||||
[Authorize(Roles = "SuperAdmin")]
|
||||
[HttpDelete("revoke-role")]
|
||||
public async Task<IResult> RevokeRole([FromBody] AssingRoleRequest roleRequest)
|
||||
{
|
||||
var result = await userService.RevokeRoleAsync(roleRequest);
|
||||
return result.ToHttpResponse();
|
||||
}
|
||||
|
||||
}
|
||||
32
src/API/Extension/ResultExtension.cs
Normal file
32
src/API/Extension/ResultExtension.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Application.Common.Results;
|
||||
|
||||
namespace API.Extension;
|
||||
|
||||
public static class ResultExtension
|
||||
{
|
||||
public static IResult ToHttpResponse(this Result result)
|
||||
{
|
||||
if (result.IsSuccess) return Results.Ok(result);
|
||||
|
||||
return MapErrorResponse(result.Error, result);
|
||||
}
|
||||
|
||||
public static IResult ToHttpResponse<T>(this Result<T> result)
|
||||
{
|
||||
if (result.IsSuccess)
|
||||
return Results.Ok(result);
|
||||
return MapErrorResponse(result.Error, result);
|
||||
}
|
||||
|
||||
private static IResult MapErrorResponse(Error? error, object result)
|
||||
{
|
||||
return error?.Code switch
|
||||
{
|
||||
ErrorTypeConstant.ValidationError => Results.BadRequest(result),
|
||||
ErrorTypeConstant.NotFound => Results.NotFound(result),
|
||||
ErrorTypeConstant.Forbidden => Results.Forbid(),
|
||||
ErrorTypeConstant.Unauthorized => Results.Unauthorized(),
|
||||
_ => Results.Problem(error?.Message, statusCode: 500)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi;
|
||||
|
||||
namespace API.Extension;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddWebServices(this IServiceCollection services)
|
||||
public static IServiceCollection AddWebServices(this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
services.AddSwaggerGen(options =>
|
||||
{
|
||||
@@ -12,7 +16,43 @@ public static class ServiceCollectionExtensions
|
||||
options.SwaggerDoc("v1", new OpenApiInfo { Title = "DotNetAngular API", Version = "v1" });
|
||||
// update names of the api
|
||||
options.SwaggerGeneratorOptions.DocumentFilters.Add(new LowerCaseDocumentFilter());
|
||||
|
||||
// configure JWT authentication
|
||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "Bearer",
|
||||
BearerFormat = "JWT",
|
||||
In = ParameterLocation.Header,
|
||||
Description =
|
||||
"JWT Authorization header using the Bearer scheme. Enter your token in the text input below."
|
||||
});
|
||||
|
||||
options.AddSecurityRequirement(doc =>
|
||||
{
|
||||
var schemeRef = new OpenApiSecuritySchemeReference("Bearer", doc);
|
||||
return new OpenApiSecurityRequirement
|
||||
{
|
||||
{ schemeRef, new List<string>() }
|
||||
};
|
||||
});
|
||||
});
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = configuration["Jwt:Issuer"],
|
||||
ValidAudience = configuration["Jwt:Audience"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey
|
||||
(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ??
|
||||
throw new InvalidOperationException("Jwt:Key is not configured"))),
|
||||
ClockSkew = TimeSpan.Zero // remove delay of token expiration time
|
||||
};
|
||||
});
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
using API.Extension;
|
||||
using Application.Extensions;
|
||||
using Infrastructure.Context;
|
||||
using Infrastructure.Extensions;
|
||||
using Infrastructure.Utilities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
@@ -7,7 +12,18 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
// Add services to the container.
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddWebServices();
|
||||
builder.Services.AddWebServices(builder.Configuration);
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
builder.Services.AddApplication();
|
||||
|
||||
// PostgreSql Database for development
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
{
|
||||
var postgreSqlSettings =
|
||||
builder.Configuration.GetRequiredSection("PostgreSqlSettings").Get<PostgreSqlSettings>();
|
||||
var connectionString = postgreSqlSettings?.ConnectionString;
|
||||
options.UseNpgsql(connectionString);
|
||||
});
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
|
||||
@@ -5,5 +5,18 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"PostgreSqlSettings": {
|
||||
"Database": "template_db",
|
||||
"Username": "natlinux"
|
||||
},
|
||||
"jwt": {
|
||||
"Key": "veryveryveryveryveryveryverysecretkey",
|
||||
"Issuer": "https://localhost:7091",
|
||||
"Audience": "http://localhost:5184"
|
||||
},
|
||||
"EmailSettings": {
|
||||
"SmtpServer": "smtp.gmail.com",
|
||||
"Port": 465
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,20 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="MailKit" Version="4.15.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.3.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.3.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
||||
<PackageReference Include="MimeKit" Version="4.15.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
6
src/Application/Common/Results/Error.cs
Normal file
6
src/Application/Common/Results/Error.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Application.Common.Results;
|
||||
|
||||
public sealed record Error(string Code, string Message)
|
||||
{
|
||||
internal static Error None => new(ErrorTypeConstant.None, string.Empty);
|
||||
}
|
||||
11
src/Application/Common/Results/ErrorTypeConstant.cs
Normal file
11
src/Application/Common/Results/ErrorTypeConstant.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Application.Common.Results;
|
||||
|
||||
public class ErrorTypeConstant
|
||||
{
|
||||
public const string None = "None";
|
||||
public const string ValidationError = "ValidationError";
|
||||
public const string InternalServerError = "InternalServerError";
|
||||
public const string NotFound = "NotFoundError";
|
||||
public const string Unauthorized = "UnauthorizedError";
|
||||
public const string Forbidden = "ForbiddenError";
|
||||
}
|
||||
37
src/Application/Common/Results/Result.cs
Normal file
37
src/Application/Common/Results/Result.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace Application.Common.Results;
|
||||
|
||||
public class Result
|
||||
{
|
||||
protected Result(bool isSuccess, Error error)
|
||||
{
|
||||
if ((isSuccess && error != Error.None) || (!isSuccess && error == Error.None))
|
||||
throw new InvalidOperationException("Invalid operation");
|
||||
IsSuccess = isSuccess;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public bool IsSuccess { get; }
|
||||
public bool IsFailure => !IsSuccess;
|
||||
|
||||
public Error Error { get; }
|
||||
|
||||
public static Result Success()
|
||||
{
|
||||
return new Result(true, Error.None);
|
||||
}
|
||||
|
||||
public static Result<TValue> Success<TValue>(TValue value)
|
||||
{
|
||||
return new Result<TValue>(value, true, Error.None);
|
||||
}
|
||||
|
||||
public static Result Failure(Error error)
|
||||
{
|
||||
return new Result(false, error);
|
||||
}
|
||||
|
||||
public static Result<TValue> Failure<TValue>(Error error)
|
||||
{
|
||||
return new Result<TValue>(default!, false, error);
|
||||
}
|
||||
}
|
||||
13
src/Application/Common/Results/TResult.cs
Normal file
13
src/Application/Common/Results/TResult.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Application.Common.Results;
|
||||
|
||||
public class Result<TValue> : Result
|
||||
{
|
||||
private readonly TValue _value;
|
||||
|
||||
protected internal Result(TValue value, bool isSuccess, Error error) : base(isSuccess, error)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public TValue Value => IsSuccess ? _value : throw new InvalidOperationException("No value for failure result");
|
||||
}
|
||||
12
src/Application/DTOs/PagedResult.cs
Normal file
12
src/Application/DTOs/PagedResult.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Application.DTOs;
|
||||
|
||||
public class PagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = [];
|
||||
public int TotalCount { get; set; }
|
||||
public int PageNumber { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||
public bool HasPreviousPage => PageNumber > 1;
|
||||
public bool HasNext => PageNumber < TotalPages;
|
||||
}
|
||||
8
src/Application/DTOs/ResetPasswordDto.cs
Normal file
8
src/Application/DTOs/ResetPasswordDto.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Application.DTOs;
|
||||
|
||||
public record ResetPasswordDto
|
||||
{
|
||||
public required string Email { get; init; }
|
||||
public required string EmailToken { get; init; }
|
||||
public required string NewPassword { get; init; }
|
||||
}
|
||||
10
src/Application/DTOs/UserDto.cs
Normal file
10
src/Application/DTOs/UserDto.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Application.DTOs;
|
||||
|
||||
public record UserDto
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public required string Email { get; init; }
|
||||
public required string Username { get; init; }
|
||||
public DateTime LastLogin { get; init; }
|
||||
public List<string> Roles { get; init; } = [];
|
||||
}
|
||||
29
src/Application/Errors/AuthError.cs
Normal file
29
src/Application/Errors/AuthError.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Application.Common.Results;
|
||||
|
||||
namespace Application.Errors;
|
||||
|
||||
public static class AuthError
|
||||
{
|
||||
public static Error InvalidRegisterRequest => new(ErrorTypeConstant.ValidationError, "Invalid register request");
|
||||
|
||||
public static Error EmailAlreadyExists => new(ErrorTypeConstant.ValidationError, "E-Mail already exists");
|
||||
|
||||
public static Error UsernameAlreadyExists => new(ErrorTypeConstant.ValidationError, "Username already exists");
|
||||
public static Error InvalidLoginRequest => new(ErrorTypeConstant.ValidationError, "Invalid login request");
|
||||
|
||||
public static Error UserNotFound => new(ErrorTypeConstant.NotFound, "User not found");
|
||||
|
||||
public static Error InvalidPassword => new(ErrorTypeConstant.ValidationError, "Invalid Password");
|
||||
public static Error InvalidResetLink => new(ErrorTypeConstant.ValidationError, "Invalid reset link");
|
||||
|
||||
|
||||
public static Error CreateInvalidLoginRequestError(IEnumerable<string> errors)
|
||||
{
|
||||
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
|
||||
}
|
||||
|
||||
public static Error CreateInvalidRegisterRequestError(IEnumerable<string> errors)
|
||||
{
|
||||
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
|
||||
}
|
||||
}
|
||||
54
src/Application/Errors/UserError.cs
Normal file
54
src/Application/Errors/UserError.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Application.Common.Results;
|
||||
|
||||
namespace Application.Errors;
|
||||
|
||||
public static class UserError
|
||||
{
|
||||
public static Error InternalServerError =>
|
||||
new(ErrorTypeConstant.InternalServerError, "something went wrong");
|
||||
|
||||
public static Error UserNotFound =>
|
||||
new(ErrorTypeConstant.NotFound, "User not found");
|
||||
|
||||
public static Error UserCookieConsentNotFound =>
|
||||
new(ErrorTypeConstant.NotFound, "Cookie consent not found for user");
|
||||
|
||||
public static Error FailedToAssignRole =>
|
||||
new(ErrorTypeConstant.InternalServerError, "failed to assign role");
|
||||
|
||||
public static Error FailedToRevokeRole =>
|
||||
new(ErrorTypeConstant.InternalServerError, "failed to revoke role");
|
||||
|
||||
public static Error UserAlreadyHasRole =>
|
||||
new(ErrorTypeConstant.ValidationError, "User already has role");
|
||||
|
||||
public static Error UserHasNoRole =>
|
||||
new(ErrorTypeConstant.ValidationError, "User already has no role");
|
||||
|
||||
public static Error FailedToRevokeCookieConsent =>
|
||||
new(ErrorTypeConstant.InternalServerError, "failed to revoke cookie consent");
|
||||
|
||||
public static Error UserAlreadyHasCookieConsent =>
|
||||
new(ErrorTypeConstant.ValidationError, "User already has cookie consent");
|
||||
|
||||
public static Error UserHasNoCookieConsent =>
|
||||
new(ErrorTypeConstant.ValidationError, "User already has no cookie consent");
|
||||
|
||||
public static Error CannotDeleteYourself =>
|
||||
new(ErrorTypeConstant.ValidationError, "You cannot delete your own account");
|
||||
|
||||
public static Error CreateInvalidUserUpdateRequestError(IEnumerable<string> errors)
|
||||
{
|
||||
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
|
||||
}
|
||||
|
||||
public static Error CreateInvalidCookieConsentError(IEnumerable<string> errors)
|
||||
{
|
||||
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
|
||||
}
|
||||
|
||||
public static Error CreateInvalidLoginRequestError(IEnumerable<string> errors)
|
||||
{
|
||||
return new Error(ErrorTypeConstant.ValidationError, string.Join(", ", errors));
|
||||
}
|
||||
}
|
||||
20
src/Application/Extensions/ServiceCollectionExtensions.cs
Normal file
20
src/Application/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Application.Interfaces;
|
||||
using Application.Services;
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Application.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddApplication(this IServiceCollection services)
|
||||
{
|
||||
services.AddValidatorsFromAssembly(typeof(ServiceCollectionExtensions).Assembly);
|
||||
services.AddScoped<IAuthenticationService, AuthenticationService>();
|
||||
services.AddScoped<IJwtService, JwtService>();
|
||||
services.AddScoped<IEmailService, EmailService>();
|
||||
services.AddScoped<IUserService, UserService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
14
src/Application/Interfaces/IAuthenticationService.cs
Normal file
14
src/Application/Interfaces/IAuthenticationService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Application.Common.Results;
|
||||
using Application.DTOs;
|
||||
using Application.Models;
|
||||
|
||||
namespace Application.Interfaces;
|
||||
|
||||
public interface IAuthenticationService
|
||||
{
|
||||
Task<Result> RegisterAsync(RegisterRequest request);
|
||||
Task<Result> LoginAsync(LoginRequest request);
|
||||
Task<Result> RefreshTokensAsync(RefreshTokenRequest request);
|
||||
Task<Result> SendResetEmailAsync(string email);
|
||||
Task<Result> ResetPasswordAsync(ResetPasswordDto resetPasswordDto);
|
||||
}
|
||||
8
src/Application/Interfaces/IEmailService.cs
Normal file
8
src/Application/Interfaces/IEmailService.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Application.Models;
|
||||
|
||||
namespace Application.Interfaces;
|
||||
|
||||
public interface IEmailService
|
||||
{
|
||||
void SendEmailAsync(EmailRequest emailRequest);
|
||||
}
|
||||
10
src/Application/Interfaces/IJwtService.cs
Normal file
10
src/Application/Interfaces/IJwtService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Domain.Entities;
|
||||
|
||||
namespace Application.Interfaces;
|
||||
|
||||
public interface IJwtService
|
||||
{
|
||||
Task<string> GenerateTokenAsync(User user);
|
||||
Task<string> GenerateAndSaveRefreshTokenAsync(User user);
|
||||
Task<User?> ValidateRefreshTokenAsync(int userId, string refreshToken);
|
||||
}
|
||||
15
src/Application/Interfaces/IUserService.cs
Normal file
15
src/Application/Interfaces/IUserService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Application.Common.Results;
|
||||
using Application.DTOs;
|
||||
using Application.Models;
|
||||
|
||||
namespace Application.Interfaces;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
Task<Result<PagedResult<UserDto>>> GetAsync(int pageNumber = 1, int pageSize = 10);
|
||||
Task<Result<string>> UpdateAsync(UserUpdateRequest user);
|
||||
Task<Result<string>> DeleteAsync(int id, int currentUserId);
|
||||
Task<Result<UserDto>> GetUserByIdAsync(int id);
|
||||
Task<Result<string>> AssignRoleAsync(AssingRoleRequest roleRequest);
|
||||
Task<Result<string>> RevokeRoleAsync(AssingRoleRequest roleRequest);
|
||||
}
|
||||
3
src/Application/Models/AssingRoleRequest.cs
Normal file
3
src/Application/Models/AssingRoleRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public record AssingRoleRequest(int UserId, int RoleId);
|
||||
15
src/Application/Models/EmailRequest.cs
Normal file
15
src/Application/Models/EmailRequest.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public class EmailRequest
|
||||
{
|
||||
public EmailRequest(string to, string subject, string content)
|
||||
{
|
||||
To = to;
|
||||
Subject = subject;
|
||||
Content = content;
|
||||
}
|
||||
|
||||
public string To { get; set; }
|
||||
public string Subject { get; set; }
|
||||
public string Content { get; set; }
|
||||
}
|
||||
3
src/Application/Models/LoginRequest.cs
Normal file
3
src/Application/Models/LoginRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public record LoginRequest(string Email, string Password);
|
||||
7
src/Application/Models/RefreshTokenRequest.cs
Normal file
7
src/Application/Models/RefreshTokenRequest.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public class RefreshTokenRequest
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
public required string RefreshToken { get; set; }
|
||||
}
|
||||
7
src/Application/Models/RegisterRequest.cs
Normal file
7
src/Application/Models/RegisterRequest.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public record RegisterRequest(
|
||||
string Username,
|
||||
string Email,
|
||||
string Password
|
||||
);
|
||||
7
src/Application/Models/TokenResponse.cs
Normal file
7
src/Application/Models/TokenResponse.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public class TokenResponse
|
||||
{
|
||||
public required string AccessToken { get; set; }
|
||||
public required string RefreshToken { get; set; }
|
||||
}
|
||||
3
src/Application/Models/UserUpdateRequest.cs
Normal file
3
src/Application/Models/UserUpdateRequest.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Application.Models;
|
||||
|
||||
public record UserUpdateRequest(int Id, string Username, string Email);
|
||||
147
src/Application/Services/AuthenticationService.cs
Normal file
147
src/Application/Services/AuthenticationService.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System.Security.Cryptography;
|
||||
using Application.Common.Results;
|
||||
using Application.DTOs;
|
||||
using Application.Errors;
|
||||
using Application.Interfaces;
|
||||
using Application.Models;
|
||||
using Application.Validators;
|
||||
using Domain.Entities;
|
||||
using Domain.Interface;
|
||||
using Infrastructure.Utilities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Application.Services;
|
||||
|
||||
public class AuthenticationService(
|
||||
IUnitOfWork unitOfWork,
|
||||
IUserRepository iUserRepository,
|
||||
LoginRequestValidator loginRequestValidator,
|
||||
RegisterRequestValidator registerRequestValidator,
|
||||
IJwtService jwtService,
|
||||
IEmailService emailService) : IAuthenticationService
|
||||
{
|
||||
public async Task<Result> RegisterAsync(RegisterRequest registerRequest)
|
||||
{
|
||||
var validationResult = await registerRequestValidator.ValidateAsync(registerRequest);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
var errors = validationResult.Errors.Select(x => x.ErrorMessage);
|
||||
return Result.Failure(AuthError.CreateInvalidRegisterRequestError(errors));
|
||||
}
|
||||
|
||||
var emailExists = await iUserRepository.GetUserByEmailAsync(registerRequest.Email);
|
||||
if (emailExists is not null) return Result.Failure(AuthError.EmailAlreadyExists);
|
||||
|
||||
var usernameExists = await iUserRepository.GetUserByUsernameAsync(registerRequest.Username);
|
||||
if (usernameExists is not null) return Result.Failure(AuthError.UsernameAlreadyExists);
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Username = registerRequest.Username,
|
||||
Email = registerRequest.Email,
|
||||
Password = registerRequest.Password,
|
||||
UserRoles = [new UserRole { RoleId = 3 }]
|
||||
};
|
||||
|
||||
var passwordHasher = new PasswordHasher<User>();
|
||||
var hashedPassword = passwordHasher.HashPassword(user, registerRequest.Password);
|
||||
user.Password = hashedPassword;
|
||||
|
||||
await iUserRepository.AddAsync(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return Result.Success("User registered successfully.");
|
||||
}
|
||||
|
||||
public async Task<Result> LoginAsync(LoginRequest loginRequest)
|
||||
{
|
||||
var validationResult = await loginRequestValidator.ValidateAsync(loginRequest);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
var errors = validationResult.Errors.Select(x => x.ErrorMessage);
|
||||
return Result.Failure(AuthError.CreateInvalidLoginRequestError(errors));
|
||||
}
|
||||
|
||||
var (email, password) = loginRequest;
|
||||
var user = await iUserRepository.GetUserByEmailAsync(email);
|
||||
if (user is null) return Result.Failure(AuthError.UserNotFound);
|
||||
|
||||
var passwordHasher = new PasswordHasher<User>();
|
||||
var verificationResult = passwordHasher.VerifyHashedPassword(user, user.Password, password);
|
||||
if (verificationResult == PasswordVerificationResult.Failed)
|
||||
return Result.Failure(AuthError.InvalidPassword);
|
||||
|
||||
user.LastLogin = DateTime.UtcNow;
|
||||
iUserRepository.Update(user);
|
||||
|
||||
|
||||
var token = new TokenResponse
|
||||
{
|
||||
AccessToken = await jwtService.GenerateTokenAsync(user),
|
||||
RefreshToken = await jwtService.GenerateAndSaveRefreshTokenAsync(user)
|
||||
};
|
||||
var result = new
|
||||
{
|
||||
Token = token, user.Username
|
||||
};
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
|
||||
public async Task<Result> RefreshTokensAsync(RefreshTokenRequest request)
|
||||
{
|
||||
var user = await jwtService.ValidateRefreshTokenAsync(request.UserId, request.RefreshToken);
|
||||
if (user is null) return Result.Failure(AuthError.UserNotFound);
|
||||
|
||||
var result = new TokenResponse
|
||||
{
|
||||
AccessToken = await jwtService.GenerateTokenAsync(user),
|
||||
RefreshToken = await jwtService.GenerateAndSaveRefreshTokenAsync(user)
|
||||
};
|
||||
|
||||
return Result.Success(result);
|
||||
}
|
||||
|
||||
public async Task<Result> SendResetEmailAsync(string email)
|
||||
{
|
||||
var user = await iUserRepository.GetUserByEmailAsync(email);
|
||||
if (user is null) return Result.Failure(AuthError.UserNotFound);
|
||||
|
||||
var tokenBytes = RandomNumberGenerator.GetBytes(64);
|
||||
var emailToken = Convert.ToBase64String(tokenBytes);
|
||||
user.ResetPasswordToken = emailToken;
|
||||
user.ResetPasswordTokenExpiryTime = DateTime.UtcNow.AddMinutes(15);
|
||||
var emailModel = new EmailRequest(email, "Reset Password!!", EmailBody.EmailStringBody(email, emailToken));
|
||||
emailService.SendEmailAsync(emailModel);
|
||||
iUserRepository.Update(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return Result.Success("Reset email sent successfully");
|
||||
}
|
||||
|
||||
public async Task<Result> ResetPasswordAsync(ResetPasswordDto resetPasswordDto)
|
||||
{
|
||||
// Normalize the incoming token if '+' was converted to space by transport layers.
|
||||
var normalizedToken = (resetPasswordDto.EmailToken ?? string.Empty).Replace(" ", "+");
|
||||
|
||||
var user = await iUserRepository.GetUserByEmailAsync(resetPasswordDto.Email);
|
||||
if (user is null) return Result.Failure(AuthError.UserNotFound);
|
||||
|
||||
var tokenCode = user.ResetPasswordToken;
|
||||
var emailTokenExpiryTime = user.ResetPasswordTokenExpiryTime;
|
||||
|
||||
// Validate token and expiration using UTC to match stored times
|
||||
if (string.IsNullOrWhiteSpace(normalizedToken) || tokenCode != normalizedToken ||
|
||||
emailTokenExpiryTime < DateTime.UtcNow) return Result.Failure(AuthError.InvalidResetLink);
|
||||
|
||||
var passwordHasher = new PasswordHasher<User>();
|
||||
var hashedPassword = passwordHasher.HashPassword(user, resetPasswordDto.NewPassword);
|
||||
user.Password = hashedPassword;
|
||||
|
||||
// Invalidate the reset token after successful use
|
||||
user.ResetPasswordToken = null;
|
||||
user.ResetPasswordTokenExpiryTime = default;
|
||||
|
||||
iUserRepository.Update(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return Result.Success("Password reset successfully");
|
||||
}
|
||||
}
|
||||
49
src/Application/Services/EmailService.cs
Normal file
49
src/Application/Services/EmailService.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Application.Interfaces;
|
||||
using Application.Models;
|
||||
using MailKit.Net.Smtp;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MimeKit;
|
||||
using MimeKit.Text;
|
||||
|
||||
namespace Application.Services;
|
||||
|
||||
public class EmailService(IConfiguration config, Func<ISmtpClient>? smtpClientFactory)
|
||||
: IEmailService
|
||||
{
|
||||
private readonly Func<ISmtpClient> _smtpClientFactory = smtpClientFactory ?? (() => new SmtpClient());
|
||||
|
||||
public EmailService(IConfiguration config) : this(config, null)
|
||||
{
|
||||
}
|
||||
|
||||
public void SendEmailAsync(EmailRequest emailRequest)
|
||||
{
|
||||
var emailMessage = new MimeMessage();
|
||||
var from = config["EmailSettings:From"];
|
||||
emailMessage.From.Add(new MailboxAddress("RssReader", from));
|
||||
emailMessage.To.Add(new MailboxAddress(emailRequest.To, emailRequest.To));
|
||||
emailMessage.Subject = emailRequest.Subject;
|
||||
emailMessage.Body = new TextPart(TextFormat.Html)
|
||||
{
|
||||
Text = string.Format(emailRequest.Content)
|
||||
};
|
||||
|
||||
var client = _smtpClientFactory();
|
||||
try
|
||||
{
|
||||
client.Connect(config["EmailSettings:SmtpServer"], 465, true);
|
||||
client.Authenticate(config["EmailSettings:From"], config["EmailSettings:Password"]);
|
||||
client.Send(emailMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
client.Disconnect(true);
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/Application/Services/JwtService.cs
Normal file
65
src/Application/Services/JwtService.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Application.Interfaces;
|
||||
using Domain.Entities;
|
||||
using Domain.Interface;
|
||||
using Infrastructure.Utilities;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Application.Services;
|
||||
|
||||
public class JwtService(
|
||||
IConfiguration configuration,
|
||||
IUserRepository userRepository,
|
||||
IUnitOfWork unitOfWork) : IJwtService
|
||||
{
|
||||
public async Task<string> GenerateTokenAsync(User user)
|
||||
{
|
||||
var secretKey = configuration["Jwt:Key"];
|
||||
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey!));
|
||||
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
|
||||
var roles = await userRepository.GetUserRolesByEmailAsync(user.Email);
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Email, user.Email),
|
||||
new("UserId", user.Id.ToString()),
|
||||
new("username", user.Username)
|
||||
};
|
||||
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
|
||||
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(claims),
|
||||
Expires = DateTime.UtcNow.AddDays(1), // set a token expiration time
|
||||
SigningCredentials = credentials,
|
||||
Issuer = configuration["Jwt:Issuer"],
|
||||
Audience = configuration["Jwt:Audience"]
|
||||
};
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var securityToken = tokenHandler.CreateToken(tokenDescriptor);
|
||||
return tokenHandler.WriteToken(securityToken);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAndSaveRefreshTokenAsync(User user)
|
||||
{
|
||||
var refreshToken = GenerateRefreshTokenHelper.GenerateRefreshToken();
|
||||
user.RefreshToken = refreshToken;
|
||||
user.RefreshTokenExpiryTime = DateTime.UtcNow.AddDays(5);
|
||||
userRepository.Update(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
|
||||
public async Task<User?> ValidateRefreshTokenAsync(int userId, string refreshToken)
|
||||
{
|
||||
var user = await userRepository.GetUserByIdAsync(userId);
|
||||
if (user is null || user.RefreshToken != refreshToken
|
||||
|| user.RefreshTokenExpiryTime <= DateTime.UtcNow)
|
||||
return null;
|
||||
return user;
|
||||
}
|
||||
}
|
||||
163
src/Application/Services/UserService.cs
Normal file
163
src/Application/Services/UserService.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using Application.Common.Results;
|
||||
using Application.DTOs;
|
||||
using Application.Errors;
|
||||
using Application.Interfaces;
|
||||
using Application.Models;
|
||||
using Application.Validators;
|
||||
using Domain.Interface;
|
||||
|
||||
namespace Application.Services;
|
||||
|
||||
public class UserService(
|
||||
IUnitOfWork unitOfWork,
|
||||
UserUpdateRequestValidator userUpdateRequestValidator,
|
||||
IUserRepository userRepository,
|
||||
IUserRoleRepository userRoleRepository) : IUserService
|
||||
{
|
||||
public async Task<Result<PagedResult<UserDto>>> GetAsync(int pageNumber = 1, int pageSize = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
// fetch all users
|
||||
var users = await userRepository.GetAllAsync();
|
||||
var totalCount = users.Count;
|
||||
var pagedItems = users
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(user => new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Username = user.Username,
|
||||
LastLogin = user.LastLogin,
|
||||
Roles = user.UserRoles.Select(x => x.Role?.Name ?? "Unknown").ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var pageResult = new PagedResult<UserDto>
|
||||
{
|
||||
Items = pagedItems,
|
||||
TotalCount = totalCount,
|
||||
PageNumber = pageNumber,
|
||||
PageSize = pageSize
|
||||
};
|
||||
return Result.Success(pageResult);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> UpdateAsync(UserUpdateRequest userUpdateRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
// validate request
|
||||
var validationResult = await userUpdateRequestValidator.ValidateAsync(userUpdateRequest);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
var errors = validationResult.Errors.Select(a => a.ErrorMessage);
|
||||
return Result.Failure<string>(UserError.CreateInvalidUserUpdateRequestError(errors));
|
||||
}
|
||||
|
||||
// check if a user exists
|
||||
var user = await userRepository.GetByIdAsync(userUpdateRequest.Id);
|
||||
if (user == null) return Result.Failure<string>(UserError.UserNotFound);
|
||||
|
||||
// update user
|
||||
user.Username = userUpdateRequest.Username;
|
||||
user.Email = userUpdateRequest.Email;
|
||||
userRepository.Update(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return Result.Success("User updated successfully");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> DeleteAsync(int id, int currentUserId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Prevent users from deleting themselves
|
||||
if (id == currentUserId) return Result.Failure<string>(UserError.CannotDeleteYourself);
|
||||
|
||||
var user = await userRepository.GetByIdAsync(id);
|
||||
if (user == null) return Result.Failure<string>(UserError.UserNotFound);
|
||||
|
||||
userRepository.Delete(user);
|
||||
await unitOfWork.CommitAsync();
|
||||
return Result.Success("User deleted successfully");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<UserDto>> GetUserByIdAsync(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await userRepository.GetByIdAsync(id);
|
||||
if (user is null) return Result.Failure<UserDto>(UserError.UserNotFound);
|
||||
var userDetails = new UserDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Email = user.Email,
|
||||
Username = user.Username,
|
||||
LastLogin = user.LastLogin,
|
||||
Roles = user.UserRoles.Select(x => x.Role?.Name ?? "Unknown").ToList()
|
||||
};
|
||||
return Result.Success(userDetails);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> AssignRoleAsync(AssingRoleRequest roleRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isUserHasRole = userRoleRepository.HasRoleAsync(roleRequest.UserId, roleRequest.RoleId);
|
||||
if (isUserHasRole.Result) return Result.Failure<string>(UserError.UserAlreadyHasRole);
|
||||
var result = await userRoleRepository.AddRoleAsync(roleRequest.UserId, roleRequest.RoleId);
|
||||
return result
|
||||
? Result.Success("Role assigned successfully")
|
||||
: Result.Failure<string>(UserError.FailedToAssignRole);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> RevokeRoleAsync(AssingRoleRequest roleRequest)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isUserHasRole = userRoleRepository.HasRoleAsync(roleRequest.UserId, roleRequest.RoleId);
|
||||
if (!isUserHasRole.Result) return Result.Failure<string>(UserError.UserHasNoRole);
|
||||
|
||||
var result = await userRoleRepository.RemoveRoleAsync(roleRequest.UserId, roleRequest.RoleId);
|
||||
return result
|
||||
? Result.Success("Role revoked successfully")
|
||||
: Result.Failure<string>(UserError.FailedToRevokeRole);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Application/Validators/LoginRequestValidator.cs
Normal file
17
src/Application/Validators/LoginRequestValidator.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Application.Models;
|
||||
using FluentValidation;
|
||||
|
||||
namespace Application.Validators;
|
||||
|
||||
public class LoginRequestValidator : AbstractValidator<LoginRequest>
|
||||
{
|
||||
public LoginRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required.")
|
||||
.EmailAddress().WithMessage("Email is not valid.");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("Password is required.");
|
||||
}
|
||||
}
|
||||
29
src/Application/Validators/RegisterRequestValidator.cs
Normal file
29
src/Application/Validators/RegisterRequestValidator.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Application.Models;
|
||||
using FluentValidation;
|
||||
|
||||
namespace Application.Validators;
|
||||
|
||||
public class RegisterRequestValidator : AbstractValidator<RegisterRequest>
|
||||
{
|
||||
public RegisterRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("{PropertyName} is required.")
|
||||
.EmailAddress().WithMessage("{PropertyName} is not valid.");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("{PropertyName} is required.")
|
||||
.MinimumLength(10).WithMessage("{PropertyName} must be at least 10 characters long.")
|
||||
.Matches("[A-Z]").WithMessage("{PropertyName} must contain at least one uppercase letter.")
|
||||
.Matches("[a-z]").WithMessage("{PropertyName} must contain at least one lowercase letter.")
|
||||
.Matches("[0-9]").WithMessage("{PropertyName} must contain at least one digit.")
|
||||
.Matches("[^a-zA-Z0-9]").WithMessage("{PropertyName} must contain at least one special character.");
|
||||
|
||||
RuleFor(x => x.Username)
|
||||
.NotEmpty().WithMessage("{PropertyName} is required.")
|
||||
.MinimumLength(3).WithMessage("{PropertyName} must be at least 3 characters long.")
|
||||
.MaximumLength(20).WithMessage("{PropertyName} must not exceed 20 characters.")
|
||||
.Matches("^[a-zA-Z0-9_]*$")
|
||||
.WithMessage("{PropertyName} can only contain letters, numbers, and underscores.");
|
||||
}
|
||||
}
|
||||
21
src/Application/Validators/UserUpdateRequestValidator.cs
Normal file
21
src/Application/Validators/UserUpdateRequestValidator.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Application.Models;
|
||||
using FluentValidation;
|
||||
|
||||
namespace Application.Validators;
|
||||
|
||||
public class UserUpdateRequestValidator : AbstractValidator<UserUpdateRequest>
|
||||
{
|
||||
public UserUpdateRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Username)
|
||||
.NotEmpty().WithMessage("Username is required.")
|
||||
.MaximumLength(50).WithMessage(user => "Username must not exceed 50 characters.");
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required.")
|
||||
.EmailAddress().WithMessage("Email is invalid.");
|
||||
|
||||
RuleFor(x => x.Id)
|
||||
.NotEmpty().GreaterThan(0).WithMessage("Id is required.");
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,8 @@
|
||||
"sourceMap": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.development.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
9598
src/ClientApp/package-lock.json
generated
Normal file
9598
src/ClientApp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,14 +12,14 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^21.1.2",
|
||||
"@angular/common": "^21.1.2",
|
||||
"@angular/compiler": "^21.1.2",
|
||||
"@angular/core": "^21.1.2",
|
||||
"@angular/forms": "^21.1.2",
|
||||
"@angular/platform-browser": "^21.1.2",
|
||||
"@angular/platform-browser-dynamic": "^21.1.2",
|
||||
"@angular/router": "^21.1.2",
|
||||
"@angular/animations": "^21.2.4",
|
||||
"@angular/common": "^21.2.4",
|
||||
"@angular/compiler": "^21.2.4",
|
||||
"@angular/core": "^21.2.4",
|
||||
"@angular/forms": "^21.2.4",
|
||||
"@angular/platform-browser": "^21.2.4",
|
||||
"@angular/platform-browser-dynamic": "^21.2.4",
|
||||
"@angular/router": "^21.2.4",
|
||||
"@auth0/angular-jwt": "^5.2.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^20.0.0",
|
||||
"bootstrap": "^5.3.8",
|
||||
@@ -29,9 +29,9 @@
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/build": "^21.1.2",
|
||||
"@angular/cli": "^21.1.2",
|
||||
"@angular/compiler-cli": "^21.1.2",
|
||||
"@angular/build": "^21.2.2",
|
||||
"@angular/cli": "^21.2.2",
|
||||
"@angular/compiler-cli": "^21.2.4",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.2.0",
|
||||
"karma": "~6.4.0",
|
||||
|
||||
@@ -2,7 +2,12 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import {provideHttpClient, withFetch, withInterceptors} from "@angular/common/http";
|
||||
import {tokenInterceptor} from "./presentation/interceptors/token.interceptor";
|
||||
import {provideClientHydration, withEventReplay} from "@angular/platform-browser";
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
|
||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes),
|
||||
provideHttpClient(withFetch(), withInterceptors([tokenInterceptor
|
||||
])), provideClientHydration(withEventReplay())]
|
||||
};
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import {RegisterComponent} from "./presentation/authentication/register/register.component";
|
||||
import {LoginComponent} from "./presentation/authentication/login/login.component";
|
||||
import {StartpageComponent} from "./presentation/components/startpage/startpage.component";
|
||||
import {AdminDashboardComponent} from "./presentation/components/admin-dashboard/admin-dashboard.component";
|
||||
import {UserDashboardComponent} from "./presentation/components/user-dashboard/user-dashboard.component";
|
||||
import {UnauthorizedComponent} from "./presentation/components/unauthorized/unauthorized.component";
|
||||
import {guestGuard} from "./presentation/guards/guest.guard";
|
||||
import {AuthenticationGuard} from "./presentation/guards/authentication.guard";
|
||||
import {adminGuard} from "./presentation/guards/admin.guard";
|
||||
import {UserTableComponent} from "./presentation/components/user-table/user-table.component";
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '', redirectTo: 'login', pathMatch: 'full'},
|
||||
{path: 'register', component: RegisterComponent},
|
||||
{path: 'login', component: LoginComponent}
|
||||
{path: '', redirectTo: 'startpage', pathMatch: 'full'},
|
||||
{path: 'register', component: RegisterComponent, canActivate: [guestGuard]},
|
||||
{path: 'login', component: LoginComponent, canActivate: [guestGuard]},
|
||||
{path: 'startpage', component: StartpageComponent, canActivate: [guestGuard]},
|
||||
{path: 'user-table', component: UserTableComponent, canActivate: [adminGuard]},
|
||||
{path: 'admin-dashboard', component: AdminDashboardComponent, canActivate: [adminGuard]},
|
||||
{path: 'user-dashboard', component: UserDashboardComponent, canActivate: [AuthenticationGuard]},
|
||||
{path: 'unauthorized', component: UnauthorizedComponent, canActivate: [AuthenticationGuard]}
|
||||
];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {RssFeed} from "./rss-feed";
|
||||
import {UserRole} from "./user-role";
|
||||
|
||||
export interface User {
|
||||
@@ -8,9 +7,5 @@ export interface User {
|
||||
password: string;
|
||||
lastLogin: Date;
|
||||
|
||||
rssFeedId: number;
|
||||
rssFeed: RssFeed;
|
||||
|
||||
UserRoles: UserRole[];
|
||||
rssFeeds: RssFeed [];
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class AuthService implements IAuthService {
|
||||
}
|
||||
|
||||
signOut() {
|
||||
if (!this.isBrowser) {
|
||||
if (this.isBrowser) {
|
||||
localStorage.clear();
|
||||
this.router.navigate(['/login']).catch(error => {
|
||||
console.error('Navigation error:', error);
|
||||
|
||||
@@ -66,7 +66,8 @@ export class LoginComponent {
|
||||
this.userStore.setEmailForStore(tokenPayload.email);
|
||||
this.userStore.setRoleForStore(tokenPayload.role);
|
||||
this.loadingService.hide()
|
||||
this.router.navigate(['rss-feed-overview']).then(success => {
|
||||
// TODO redirect to...
|
||||
this.router.navigate(['user-dashboard']).then(success => {
|
||||
if (success) {
|
||||
this.loginForm.reset();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="container-fluid d-flex justify-content-center">
|
||||
<div class="card mt-2 col-2 me-2" [routerLink]="['/user-table']" style="cursor: pointer;">
|
||||
<div class="card-body text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" fill="blue" class="bi bi-people-fill " viewBox="0 0 16 16">
|
||||
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6m-5.784 6A2.24 2.24 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.3 6.3 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1zM4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5"/>
|
||||
</svg>
|
||||
<h4 class="my-3">Users</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminDashboardComponent } from './admin-dashboard.component';
|
||||
|
||||
describe('AdminDashboardComponent', () => {
|
||||
let component: AdminDashboardComponent;
|
||||
let fixture: ComponentFixture<AdminDashboardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AdminDashboardComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminDashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {RouterLink} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-dashboard',
|
||||
imports: [
|
||||
RouterLink
|
||||
],
|
||||
templateUrl: './admin-dashboard.component.html',
|
||||
styleUrl: './admin-dashboard.component.scss',
|
||||
standalone: true
|
||||
})
|
||||
export class AdminDashboardComponent {
|
||||
|
||||
}
|
||||
@@ -1,28 +1,11 @@
|
||||
<nav class="navbar navbar-expand-lg border-bottom position-relative bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
@if (showOverviewTools){
|
||||
<div class="sidebar-toggle p-2 me-1 text-secondary" data-bs-target="#offcanvasExample" data-bs-toggle="offcanvas"
|
||||
type="button">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-list"
|
||||
viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd"
|
||||
d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
<a class="navbar-brand" [routerLink]="['/#']" title="Refresh" data-bs-target="#offcanvasExample" data-bs-dismiss="offcanvas">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-app" viewBox="0 0 16 16">
|
||||
<path d="M11 2a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zM5 1a4 4 0 0 0-4 4v6a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4z"/>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
<button aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"
|
||||
class="navbar-toggler"
|
||||
data-bs-target="#navbarSupportedContent" data-bs-toggle="collapse" type="button">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Dropdown -->
|
||||
@if (!showDropdown){
|
||||
<ul class="navbar-nav ms-auto" data-bs-target="#offcanvasExample" data-bs-dismiss="offcanvas">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {Component, inject, OnInit, viewChild} from '@angular/core';
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {DarkModeService} from '../../../infrastructure/services/dark-mode.service';
|
||||
import {FormsModule, NgForm} from "@angular/forms";
|
||||
import {ToastService} from "../../../infrastructure/services/toast.service";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {ActivatedRoute, NavigationEnd, Router, RouterLink} from "@angular/router";
|
||||
import {AuthService} from "../../../infrastructure/services/auth-service";
|
||||
import {UserStoreService} from "../../../infrastructure/services/user-store.service";
|
||||
@@ -19,7 +18,6 @@ import {filter} from "rxjs/operators";
|
||||
})
|
||||
export class HeaderComponent implements OnInit {
|
||||
|
||||
toastService = inject(ToastService);
|
||||
darkModeService = inject(DarkModeService);
|
||||
userStore = inject(UserStoreService);
|
||||
|
||||
@@ -50,9 +48,9 @@ export class HeaderComponent implements OnInit {
|
||||
while (route.firstChild) route = route.firstChild;
|
||||
|
||||
// Header-Logic
|
||||
const headerRoutes = ['/login', '/register', '/legal' , '/start'];
|
||||
this.showDropdown = headerRoutes.some(route => event.urlAfterRedirects.startsWith(route));
|
||||
this.showOverviewTools = event.urlAfterRedirects.startsWith('/rss-feed-overview');
|
||||
// TODO hide dropdown for login register and legal
|
||||
const headerRoutes = ['/login', '/register', '/legal', '/startpage'];
|
||||
this.showDropdown = headerRoutes.some(route => event.urlAfterRedirects.startsWith(route))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<p>startpage works!</p>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StartpageComponent } from './startpage.component';
|
||||
|
||||
describe('StartpageComponent', () => {
|
||||
let component: StartpageComponent;
|
||||
let fixture: ComponentFixture<StartpageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StartpageComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(StartpageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-startpage',
|
||||
imports: [],
|
||||
templateUrl: './startpage.component.html',
|
||||
styleUrl: './startpage.component.scss',
|
||||
})
|
||||
export class StartpageComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<p>unauthorized works!</p>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UnauthorizedComponent } from './unauthorized.component';
|
||||
|
||||
describe('UnauthorizedComponent', () => {
|
||||
let component: UnauthorizedComponent;
|
||||
let fixture: ComponentFixture<UnauthorizedComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UnauthorizedComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UnauthorizedComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unauthorized',
|
||||
imports: [],
|
||||
templateUrl: './unauthorized.component.html',
|
||||
styleUrl: './unauthorized.component.scss',
|
||||
})
|
||||
export class UnauthorizedComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<div class="container-fluid">
|
||||
<div class="align-items-center justify-content-center mt-3 d-flex">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center ">
|
||||
<div class="col-lg-3">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="150" height="150" class="bi bi-person-fill bg-primary rounded-circle" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
|
||||
</svg>
|
||||
<h4 class="my-3">{{username}}</h4>
|
||||
<!-- <button class="btn btn-primary">Update Picture</button>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header text-center">
|
||||
<h2 class="mb-4">Personal Data</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-1"><strong>Username:</strong> <br> {{username}}</p> <br>
|
||||
<p class="mb-1"><strong>E-Mail:</strong> <br>{{email}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="col-md-6">-->
|
||||
<!-- <div class="card mb-3" style="height: 92%">-->
|
||||
<!-- <div class="card-header text-center">-->
|
||||
<!-- <h2 class="mb-4">Payment Method</h2>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="card-body">-->
|
||||
<!-- <p class="mb-1"></p>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<a class="text-decoration-none">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<!-- <div class="card mb-3">-->
|
||||
<!-- <div class="card-header text-center">-->
|
||||
<!-- <h4 class="my-3">Change password</h4>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="card-body d-flex flex-wrap">-->
|
||||
<!-- <input type="password" class="form-control mt-2" placeholder="Enter new password">-->
|
||||
<!-- <input type="password" class="form-control mt-2" placeholder="Repeat new password">-->
|
||||
<!-- <button class="btn btn-primary mt-4">Change password</button>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<!-- <div class="card mb-3">-->
|
||||
<!-- <div class="card-header text-center">-->
|
||||
<!-- <h4 class="my-3">Change E-Mail</h4>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="card-body d-flex flex-wrap">-->
|
||||
<!-- <input type="password" class="form-control mt-2" placeholder="Enter new E-Mail">-->
|
||||
<!-- <input type="password" class="form-control mt-2" placeholder="Repeat new E-Mail">-->
|
||||
<!-- <button class="btn btn-primary mt-4">Change E-Mail</button>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserDashboardComponent } from './user-dashboard.component';
|
||||
|
||||
describe('UserDashboardComponent', () => {
|
||||
let component: UserDashboardComponent;
|
||||
let fixture: ComponentFixture<UserDashboardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserDashboardComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserDashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {Title} from "@angular/platform-browser";
|
||||
import {UserStoreService} from "../../../infrastructure/services/user-store.service";
|
||||
import {AuthService} from "../../../infrastructure/services/auth-service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-dashboard',
|
||||
imports: [],
|
||||
templateUrl: './user-dashboard.component.html',
|
||||
styleUrl: './user-dashboard.component.scss',
|
||||
})
|
||||
export class UserDashboardComponent {
|
||||
public username!: string;
|
||||
public email!: string;
|
||||
public userId!: number;
|
||||
userStore = inject(UserStoreService);
|
||||
private authenticateService = inject(AuthService);
|
||||
title = inject(Title)
|
||||
|
||||
constructor() {
|
||||
//TODO App Name
|
||||
this.title.setTitle('User Dashboard | App Name');
|
||||
}
|
||||
ngOnInit(): void {
|
||||
this.userStore.getUsernameFromStore()
|
||||
.subscribe(val => {
|
||||
const usernameFromToken = this.authenticateService.getUsernameFromToken();
|
||||
this.username = val || usernameFromToken
|
||||
});
|
||||
|
||||
this.userStore.getEmailFromStore()
|
||||
.subscribe(val => {
|
||||
const emailFromToken = this.authenticateService.getEmailFromToken();
|
||||
this.email = val || emailFromToken
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<div class="container-fluid p-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-center text-dark"><h4>User-List</h4></div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-striped table-hover table-bordered table-sm mb-0">
|
||||
<thead>
|
||||
<tr class="text-center">
|
||||
<th>#</th>
|
||||
<th (click)="sort('username')" class="sortable">
|
||||
Username
|
||||
<i class="bi"
|
||||
[ngClass]="{
|
||||
'bi bi-arrow-up': sortColumn === 'username' && sortDirection === 'asc',
|
||||
'bi bi-arrow-down': sortColumn === 'username' && sortDirection === 'desc',
|
||||
'bi bi-arrow-down-up': sortColumn !== 'username'
|
||||
}"></i>
|
||||
</th>
|
||||
<th (click)="sort('email')" class="sortable">
|
||||
E-Mail
|
||||
<i class="bi"
|
||||
[ngClass]="{
|
||||
'bi bi-arrow-up': sortColumn === 'email' && sortDirection === 'asc',
|
||||
'bi bi-arrow-down': sortColumn === 'email' && sortDirection === 'desc',
|
||||
'bi bi-arrow-down-up': sortColumn !== 'email'
|
||||
}"></i>
|
||||
</th>
|
||||
<th (click)="sort('lastLogin')" class="sortable">
|
||||
Last Login
|
||||
<i class="bi"
|
||||
[ngClass]="{
|
||||
'bi bi-arrow-up': sortColumn === 'lastLogin' && sortDirection === 'asc',
|
||||
'bi bi-arrow-down': sortColumn === 'lastLogin' && sortDirection === 'desc',
|
||||
'bi bi-arrow-down-up': sortColumn !== 'lastLogin'
|
||||
}"></i>
|
||||
</th>
|
||||
<th>Action</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
@for (user of users; track user.id; let i = $index;) {
|
||||
<tr>
|
||||
<td class="text-center">{{ i + 1 }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td class="text-center">{{ user.lastLogin | date:'dd.MM.yyyy HH:mm:ss':'Europe/Berlin' }}</td>
|
||||
<td class="d-flex justify-content-center">
|
||||
<button (click)="selectUser(user)" class="btn btn-sm btn-danger"
|
||||
data-bs-target="#warningModal"
|
||||
data-bs-toggle="modal"
|
||||
><i class="bi bi-trash3-fill"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination Buttons Bootstrap -->
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center mt-3">
|
||||
|
||||
<!-- Previous Button -->
|
||||
<li class="page-item me-1" [class.disabled]="pageNumber === 1">
|
||||
<a class="btn btn-primary" (click)="previousPage()" href="javascript:void(0)" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Dynamic Page Buttons -->
|
||||
@for (Page of [].constructor(totalPages); track Page ;let i = $index;) {
|
||||
<li class="page-item me-1">
|
||||
<a class="btn btn-outline-primary" href="javascript:void(0)" (click)="goToPage(i + 1)">
|
||||
{{ i + 1 }}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
<!-- Next Button -->
|
||||
<li class="page-item me-1" [class.disabled]="pageNumber === totalPages">
|
||||
<a class="btn btn-primary" (click)="nextPage()" href="javascript:void(0)" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center mb-3">
|
||||
<span>Page {{ pageNumber }} of {{ totalPages }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-popup-modal
|
||||
[title]="'Warning'"
|
||||
[headerClass]="'bg-warning text-dark'"
|
||||
(confirm)="handleConfirm()">
|
||||
<p>
|
||||
Do you really want to delete
|
||||
<strong class="text-danger">{{ selectedUser?.username }}</strong> ?
|
||||
</p></app-popup-modal>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserTableComponent } from './user-table.component';
|
||||
|
||||
describe('UserTableComponent', () => {
|
||||
let component: UserTableComponent;
|
||||
let fixture: ComponentFixture<UserTableComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserTableComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserTableComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {User} from "../../../domain/entities/user";
|
||||
import {UserService} from "../../../infrastructure/services/user.service";
|
||||
import {ToastService} from "../../../infrastructure/services/toast.service";
|
||||
import {Title} from "@angular/platform-browser";
|
||||
import {PopupModalComponent} from "../../shared/popup-modal/popup-modal.component";
|
||||
import {DatePipe, NgClass} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-table',
|
||||
imports: [
|
||||
PopupModalComponent,
|
||||
DatePipe,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './user-table.component.html',
|
||||
styleUrl: './user-table.component.scss',
|
||||
standalone: true
|
||||
})
|
||||
export class UserTableComponent {
|
||||
public users: User[] = [];
|
||||
selectedUser: User | null = null;
|
||||
|
||||
pageNumber: number = 1;
|
||||
pageSize: number = 10;
|
||||
totalPages: number = 0;
|
||||
|
||||
sortColumn: string = '';
|
||||
sortDirection: 'asc' | 'desc' = 'asc';
|
||||
|
||||
private apiService = inject(UserService)
|
||||
toastService = inject(ToastService);
|
||||
title = inject(Title)
|
||||
|
||||
constructor() {
|
||||
this.title.setTitle('User Table | RSS Feed Reader');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.getAllUsers()
|
||||
}
|
||||
getAllUsers(): void {
|
||||
this.apiService.getAllUsers(this.pageNumber, this.pageSize).subscribe(result => {
|
||||
this.users = result.value.items;
|
||||
this.totalPages = result.value.totalPages;
|
||||
this.sort(this.sortColumn);
|
||||
});
|
||||
}
|
||||
nextPage(): void {
|
||||
if (this.pageNumber < this.totalPages) {
|
||||
this.pageNumber++;
|
||||
this.getAllUsers();
|
||||
}
|
||||
}
|
||||
|
||||
previousPage(): void {
|
||||
if (this.pageNumber > 1) {
|
||||
this.pageNumber--;
|
||||
this.getAllUsers();
|
||||
}
|
||||
}
|
||||
goToPage(page: number): void {
|
||||
if (page >= 1 && page <= this.totalPages) {
|
||||
this.pageNumber = page;
|
||||
this.getAllUsers();
|
||||
}
|
||||
}
|
||||
// Sortiermethode
|
||||
sort(column: string): void {
|
||||
if (this.sortColumn === column) {
|
||||
// Wenn die Spalte bereits sortiert ist, wechsle die Richtung
|
||||
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
// Andernfalls aufsteigend sortieren
|
||||
this.sortDirection = 'asc';
|
||||
this.sortColumn = column;
|
||||
}
|
||||
|
||||
// Sortiere die Tabelle basierend auf Spalte und Richtung
|
||||
this.users.sort((a: any, b: any) => {
|
||||
const valueA = a[column];
|
||||
const valueB = b[column];
|
||||
if (valueA < valueB) {
|
||||
return this.sortDirection === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (valueA > valueB) {
|
||||
return this.sortDirection === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
deleteUser(id: number): void {
|
||||
this.apiService.deleteUser(id).subscribe({
|
||||
next: (result) => {
|
||||
console.log('delete',result);
|
||||
this.toastService.show(result.value, {
|
||||
classname: 'bg-success text-light',
|
||||
delay: 3000
|
||||
})
|
||||
this.users = this.users.filter(user => user.id !== id);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(err.error.error.message);
|
||||
this.toastService.show(err.error.error.message, {
|
||||
classname: 'bg-danger text-light',
|
||||
delay: 3000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectUser(user: User): void {
|
||||
this.selectedUser = user;
|
||||
console.log('selectedUser', user);
|
||||
}
|
||||
handleConfirm() {
|
||||
this.deleteUser(this.selectedUser!.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {CanActivateFn} from '@angular/router';
|
||||
|
||||
import {adminGuard} from './admin.guard';
|
||||
|
||||
describe('adminGuard', () => {
|
||||
const executeGuard: CanActivateFn = (...guardParameters) =>
|
||||
TestBed.runInInjectionContext(() => adminGuard(...guardParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeGuard).toBeTruthy();
|
||||
});
|
||||
});
|
||||
43
src/ClientApp/src/app/presentation/guards/admin.guard.ts
Normal file
43
src/ClientApp/src/app/presentation/guards/admin.guard.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {CanActivateFn, Router} from '@angular/router';
|
||||
import {inject} from "@angular/core";
|
||||
import {AuthService} from "../../infrastructure/services/auth-service";
|
||||
import {ToastService} from "../../infrastructure/services/toast.service";
|
||||
import {UserStoreService} from "../../infrastructure/services/user-store.service";
|
||||
import {catchError, map} from 'rxjs/operators';
|
||||
import {of} from 'rxjs';
|
||||
|
||||
export const adminGuard: CanActivateFn = (route, state) => {
|
||||
|
||||
|
||||
const userStore = inject(UserStoreService);
|
||||
const authenticateService = inject(AuthService);
|
||||
const toastService = inject(ToastService);
|
||||
const router = inject(Router);
|
||||
|
||||
return userStore.getRoleFromStore().pipe(
|
||||
map(val => {
|
||||
const roleFromToken = authenticateService.getRoleFromToken();
|
||||
const role = val || roleFromToken;
|
||||
|
||||
if (role === "Admin" || role === "SuperAdmin") {
|
||||
return true;
|
||||
} else {
|
||||
toastService.show("Access denied. Admins only.", {
|
||||
classname: 'bg-warning text-dark',
|
||||
delay: 3000
|
||||
});
|
||||
void router.navigate(['/unauthorized']);
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
catchError(() => {
|
||||
toastService.show("An error occurred. Please try again later.", {
|
||||
classname: 'bg-danger text-light',
|
||||
delay: 2000
|
||||
});
|
||||
void router.navigate(['/login']);
|
||||
return of(false);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {AuthenticationGuard} from './authentication.guard';
|
||||
import {provideHttpClient} from "@angular/common/http";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
import {of} from "rxjs";
|
||||
|
||||
describe('AuthenticationGuard', () => {
|
||||
let guard: AuthenticationGuard;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(),
|
||||
{ provide: ActivatedRoute, useValue: { data: of({}), firstChild: null } },
|
||||
]
|
||||
});
|
||||
guard = TestBed.inject(AuthenticationGuard);
|
||||
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(guard).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {CanActivate, Router} from '@angular/router';
|
||||
import {AuthService} from "../../infrastructure/services/auth-service";
|
||||
import {ToastService} from "../../infrastructure/services/toast.service";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthenticationGuard implements CanActivate {
|
||||
toastService = inject(ToastService);
|
||||
private authenticateService = inject(AuthService)
|
||||
private router = inject(Router)
|
||||
|
||||
canActivate(): boolean {
|
||||
if (this.authenticateService.isLoggedIn()) {
|
||||
return true;
|
||||
} else {
|
||||
this.toastService.show("Please Login first", {
|
||||
classname: 'bg-warning text-dark',
|
||||
delay: 2000
|
||||
});
|
||||
this.router.navigate(['/login']).catch(error => {
|
||||
console.error('Navigation error:', error);
|
||||
})
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanActivateFn } from '@angular/router';
|
||||
|
||||
import { guestGuard } from './guest.guard';
|
||||
|
||||
describe('guestGuard', () => {
|
||||
const executeGuard: CanActivateFn = (...guardParameters) =>
|
||||
TestBed.runInInjectionContext(() => guestGuard(...guardParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(executeGuard).toBeTruthy();
|
||||
});
|
||||
});
|
||||
16
src/ClientApp/src/app/presentation/guards/guest.guard.ts
Normal file
16
src/ClientApp/src/app/presentation/guards/guest.guard.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {CanActivateFn, Router} from '@angular/router';
|
||||
import {inject} from "@angular/core";
|
||||
import {AuthService} from "../../infrastructure/services/auth-service";
|
||||
|
||||
|
||||
export const guestGuard: CanActivateFn = (route, state) => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
if (!authService.isLoggedIn()){
|
||||
return true;
|
||||
}
|
||||
// TODO rout
|
||||
void router.navigate(['/user-dashboard']);
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {HttpInterceptorFn} from '@angular/common/http';
|
||||
|
||||
import {tokenInterceptor} from './token.interceptor';
|
||||
|
||||
describe('tokenInterceptor', () => {
|
||||
const interceptor: HttpInterceptorFn = (req, next) =>
|
||||
TestBed.runInInjectionContext(() => tokenInterceptor(req, next));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(interceptor).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import {HttpErrorResponse, HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest} from '@angular/common/http';
|
||||
import {inject} from "@angular/core";
|
||||
import {AuthService} from "../../infrastructure/services/auth-service";
|
||||
import {catchError, Observable, switchMap, throwError} from "rxjs";
|
||||
import {ToastService} from "../../infrastructure/services/toast.service";
|
||||
import {Router} from '@angular/router';
|
||||
import {RefreshTokenRequest} from "../models/refresh-token-request";
|
||||
|
||||
export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const authService = inject(AuthService);
|
||||
const token = authService.getToken();
|
||||
const toastService = inject(ToastService);
|
||||
const router = inject(Router);
|
||||
|
||||
if (token) {
|
||||
req = req.clone({
|
||||
setHeaders: {Authorization: `Bearer ${token}`}
|
||||
});
|
||||
}
|
||||
|
||||
return next(req).pipe(
|
||||
catchError((err: any) => {
|
||||
if (err instanceof HttpErrorResponse) {
|
||||
if (err.status === 401) {
|
||||
return handleUnAuthorizedError(req, next, authService, router, toastService);
|
||||
}
|
||||
}
|
||||
console.log("interceptor error",err.error?.error?.message);
|
||||
return throwError(() => err);
|
||||
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
function handleUnAuthorizedError(
|
||||
req: HttpRequest<any>,
|
||||
next: HttpHandlerFn,
|
||||
authenticateService: AuthService,
|
||||
router: Router,
|
||||
toastService: ToastService
|
||||
): Observable<HttpEvent<any>> {
|
||||
|
||||
const refreshTokenRequest = new RefreshTokenRequest();
|
||||
refreshTokenRequest.refreshToken = authenticateService.getRefreshToken()!;
|
||||
refreshTokenRequest.userId = authenticateService.getUserIdFromToken()!;
|
||||
|
||||
return authenticateService.renewToken(refreshTokenRequest).pipe(
|
||||
switchMap((data) => {
|
||||
const tokenResponse = data.value;
|
||||
authenticateService.storeRefreshToken(tokenResponse.refreshToken);
|
||||
authenticateService.storeToken(tokenResponse.accessToken);
|
||||
|
||||
req = req.clone({
|
||||
setHeaders: {Authorization: `Bearer ${tokenResponse.accessToken}`}
|
||||
});
|
||||
|
||||
return next(req);
|
||||
}),
|
||||
catchError((err) => {
|
||||
return throwError(() => {
|
||||
authenticateService.signOut();
|
||||
router.navigate(['login']).then(success => {
|
||||
if (success) {
|
||||
toastService.show(err.error?.error?.message ||'Token is expired, login again!', {
|
||||
classname: 'bg-warning text-light',
|
||||
delay: 2000
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="modal fade" id="warningModal" tabindex="-1" aria-labelledby="warningModalLabel" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" [ngClass]="headerClass">
|
||||
<h1 class="modal-title fs-5 text-center w-100" id="warningModalLabel">{{ title }}</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ng-content></ng-content>
|
||||
@if (body) {
|
||||
<p>{{ body }}</p>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">No</button>
|
||||
<button type="button" class="btn btn-warning" (click)="onConfirm()" data-bs-dismiss="modal">Yes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PopupModalComponent } from './popup-modal.component';
|
||||
|
||||
describe('PopupModalComponent', () => {
|
||||
let component: PopupModalComponent;
|
||||
let fixture: ComponentFixture<PopupModalComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PopupModalComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PopupModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {NgClass} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'app-popup-modal',
|
||||
imports: [
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './popup-modal.component.html',
|
||||
styleUrl: './popup-modal.component.scss',
|
||||
standalone: true
|
||||
})
|
||||
export class PopupModalComponent {
|
||||
|
||||
|
||||
@Input() headerClass: string = 'bg-light';
|
||||
@Input() title: string = '';
|
||||
@Input() body: string = '';
|
||||
|
||||
@Output() confirm = new EventEmitter<void>();
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
|
||||
onConfirm() {
|
||||
this.confirm.emit();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import 'bootstrap';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
9
src/Domain/Entities/Role.cs
Normal file
9
src/Domain/Entities/Role.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class Role
|
||||
{
|
||||
public required int Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
|
||||
public List<UserRole> UserRoles { get; set; } = [];
|
||||
}
|
||||
17
src/Domain/Entities/User.cs
Normal file
17
src/Domain/Entities/User.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public required string Username { get; set; }
|
||||
public required string Email { get; set; }
|
||||
public required string Password { get; set; }
|
||||
public DateTime LastLogin { get; set; }
|
||||
public string? RefreshToken { get; set; }
|
||||
public DateTime? RefreshTokenExpiryTime { get; set; }
|
||||
public string? ResetPasswordToken { get; set; }
|
||||
public DateTime ResetPasswordTokenExpiryTime { get; set; }
|
||||
|
||||
public List<UserRole> UserRoles { get; set; } = [];
|
||||
|
||||
}
|
||||
10
src/Domain/Entities/UserRole.cs
Normal file
10
src/Domain/Entities/UserRole.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Domain.Entities;
|
||||
|
||||
public class UserRole
|
||||
{
|
||||
public int UserId { get; set; }
|
||||
public User? User { get; set; }
|
||||
|
||||
public int RoleId { get; set; }
|
||||
public Role? Role { get; set; }
|
||||
}
|
||||
10
src/Domain/Interface/IGenericRepository.cs
Normal file
10
src/Domain/Interface/IGenericRepository.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Domain.Interface;
|
||||
|
||||
public interface IGenericRepository<TEntity> where TEntity : class
|
||||
{
|
||||
Task<TEntity?> GetByIdAsync(int id);
|
||||
Task<IReadOnlyList<TEntity>> GetAllAsync();
|
||||
Task<TEntity> AddAsync(TEntity entity);
|
||||
TEntity Update(TEntity entity);
|
||||
void Delete(TEntity entity);
|
||||
}
|
||||
7
src/Domain/Interface/IUnitOfWork.cs
Normal file
7
src/Domain/Interface/IUnitOfWork.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Domain.Interface;
|
||||
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
void Commit();
|
||||
Task CommitAsync();
|
||||
}
|
||||
12
src/Domain/Interface/IUserRepository.cs
Normal file
12
src/Domain/Interface/IUserRepository.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Domain.Entities;
|
||||
|
||||
namespace Domain.Interface;
|
||||
|
||||
public interface IUserRepository : IGenericRepository<User>
|
||||
{
|
||||
// extra implementation
|
||||
Task<User?> GetUserByEmailAsync(string email);
|
||||
Task<User?> GetUserByUsernameAsync(string username);
|
||||
Task<List<string>> GetUserRolesByEmailAsync(string email);
|
||||
Task<User?> GetUserByIdAsync(int id);
|
||||
}
|
||||
8
src/Domain/Interface/IUserRoleRepository.cs
Normal file
8
src/Domain/Interface/IUserRoleRepository.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Domain.Interface;
|
||||
|
||||
public interface IUserRoleRepository
|
||||
{
|
||||
Task<bool> AddRoleAsync(int userId, int roleId);
|
||||
Task<bool> RemoveRoleAsync(int userId, int roleId);
|
||||
Task<bool> HasRoleAsync(int userId, int roleId);
|
||||
}
|
||||
27
src/Infrastructure/Configuration/RoleConfiguration.cs
Normal file
27
src/Infrastructure/Configuration/RoleConfiguration.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Infrastructure.Configuration;
|
||||
|
||||
public class RoleConfiguration : IEntityTypeConfiguration<Role>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Role> builder)
|
||||
{
|
||||
builder.ToTable("Roles", "auth");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50);
|
||||
|
||||
builder.HasMany(x => x.UserRoles)
|
||||
.WithOne(x => x.Role)
|
||||
.HasForeignKey(x => x.RoleId);
|
||||
|
||||
builder.HasData(
|
||||
new Role { Id = 1, Name = "SuperAdmin" },
|
||||
new Role { Id = 2, Name = "Admin" },
|
||||
new Role { Id = 3, Name = "User" }
|
||||
);
|
||||
}
|
||||
}
|
||||
47
src/Infrastructure/Configuration/UserConfiguration.cs
Normal file
47
src/Infrastructure/Configuration/UserConfiguration.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Infrastructure.Configuration;
|
||||
|
||||
public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> builder)
|
||||
{
|
||||
builder.ToTable("Users", "auth");
|
||||
|
||||
builder.HasKey(k => k.Id);
|
||||
|
||||
builder.Property(x => x.Username)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
builder.Property(x => x.Password)
|
||||
.IsRequired()
|
||||
.HasMaxLength(250)
|
||||
.HasColumnType("varchar(250)");
|
||||
|
||||
builder.Property(x => x.Email)
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)")
|
||||
.HasAnnotation("RegularExpression", @"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$");
|
||||
|
||||
builder.HasMany(x => x.UserRoles)
|
||||
.WithOne(x => x.User)
|
||||
.HasForeignKey(x => x.UserId);
|
||||
|
||||
builder.HasData(
|
||||
new User
|
||||
{
|
||||
Id = 1, Username = "Superadmin", Email = "superadmin@wenske-services-development.de",
|
||||
Password = "AQAAAAIAAYagAAAAEADJEu1s5qUJyP4gDUrBGyqSNtKU2IKBpZm0JqfyvOkJnqVeOHZBUrEhNr7IdQRDBQ=="
|
||||
},
|
||||
new User
|
||||
{
|
||||
Id = 2, Username = "Admin", Email = "admin@wenske-services-development.de",
|
||||
Password = "AQAAAAIAAYagAAAAEIOUiJUfMrM1Lpt4Ae3FLQOB/Bk6WHtndRAWUp132afVunMvRqkT6Hhh+27kkNW8YQ=="
|
||||
});
|
||||
}
|
||||
}
|
||||
25
src/Infrastructure/Configuration/UserRoleConfiguration.cs
Normal file
25
src/Infrastructure/Configuration/UserRoleConfiguration.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Infrastructure.Configuration;
|
||||
|
||||
public class UserRoleConfiguration : IEntityTypeConfiguration<UserRole>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UserRole> builder)
|
||||
{
|
||||
builder.ToTable("UserRoles", "auth");
|
||||
builder.HasKey(x => new { x.UserId, x.RoleId });
|
||||
builder.HasOne(x => x.User)
|
||||
.WithMany(x => x.UserRoles)
|
||||
.HasForeignKey(x => x.UserId);
|
||||
|
||||
builder.HasOne(x => x.Role)
|
||||
.WithMany(x => x.UserRoles)
|
||||
.HasForeignKey(x => x.RoleId);
|
||||
|
||||
builder.HasData(
|
||||
new UserRole { UserId = 1, RoleId = 1 },
|
||||
new UserRole { UserId = 2, RoleId = 2 });
|
||||
}
|
||||
}
|
||||
16
src/Infrastructure/Context/ApplicationDbContext.cs
Normal file
16
src/Infrastructure/Context/ApplicationDbContext.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Infrastructure.Context;
|
||||
|
||||
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<Role> Roles { get; set; }
|
||||
public DbSet<UserRole> UserRoles { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user