diff --git a/ProjectUserManager b/ProjectUserManager new file mode 160000 index 0000000..7d01fe0 --- /dev/null +++ b/ProjectUserManager @@ -0,0 +1 @@ +Subproject commit 7d01fe0e43a363d5c5edfe3279b970a589fd1e45 diff --git a/UserManagement.API/Controllers/AuthController.cs b/UserManagement.API/Controllers/AuthController.cs new file mode 100644 index 0000000..8e23370 --- /dev/null +++ b/UserManagement.API/Controllers/AuthController.cs @@ -0,0 +1,86 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using UserManagement.Application.Dtos.Auth; +using UserManagement.Application.Interfaces; +using Swashbuckle.AspNetCore.Annotations; + +namespace UserManagement.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class AuthController : ControllerBase + { + // CTOR + private readonly IUserService _userService; + private readonly IAuthService _authService; + public AuthController(IUserService userService, IAuthService authService) + { + _userService = userService; + _authService = authService; + } + + // LOGIN + [AllowAnonymous] + [HttpPost("login")] + [SwaggerOperation(Summary = "Login")] + public async Task Login([FromBody] LoginDto login) + { + // Validate user + var user = await _userService.GetByUsernameAsync(login.Username); + if (user == null) + { + return Unauthorized(); + } + + // Validate login credentials + var isValid = await _authService.ValidateAsync(login.Username, login.Password); + if (!isValid) + { + return Unauthorized(); + } + + // Create claims based on the user information + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.Surname, user.LastName ?? ""), + new Claim(ClaimTypes.GivenName, user.FirstName ?? ""), + new Claim(ClaimTypes.Role, user?.Role?.Name.ToString() ?? "") + }; + + // Create a ClaimsIdentity based on the created claims + var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + + // Set the authentication properties + var authProperties = new AuthenticationProperties + { + IsPersistent = true, + AllowRefresh = true, + ExpiresUtc = DateTime.UtcNow.AddMinutes(60) + }; + + // Sign in user using cookie-based authentication + await HttpContext.SignInAsync( + CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(claimsIdentity), + authProperties + ); + + return Ok(); + } + + // LOGOUT + [HttpPost("logout")] + [SwaggerOperation(Summary = "Logout")] + public async Task Logout() + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + return Ok(); + } + } +} diff --git a/UserManagement.API/Controllers/RoleController.cs b/UserManagement.API/Controllers/RoleController.cs new file mode 100644 index 0000000..cddd5ee --- /dev/null +++ b/UserManagement.API/Controllers/RoleController.cs @@ -0,0 +1,133 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using UserManagement.Application.Dtos.Incomming; +using UserManagement.Application.Interfaces; + +namespace UserManagement.API.Controllers +{ + [Route("api/[controller]")] + [ApiController] + //[Authorize(Roles = "Admin")] + public class RoleController : ControllerBase + { + // CTOR + private readonly IRoleService _roleService; + public RoleController(IRoleService roleService) + { + _roleService = roleService; + } + + // CREATE + [HttpPost] + [SwaggerOperation(Summary = "Create Role")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateRole([FromBody] CreatingRoleDto creatingRoleDto) + { + // Validate incomming model + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + try + { + // Try to add role asynchronously + var result = await _roleService.AddRoleAsync(creatingRoleDto); + + // If role is successfully created, return a CreatedAtAction response with the created resource + if (result is not null) + { + var id = result.Id; + var createdResource = new { Id = id }; + var actionName = nameof(GetRoleById); + var routeValue = new { id = createdResource.Id }; + return CreatedAtAction(actionName, routeValue, createdResource); + } + else + { + return BadRequest("geht nix"); + } + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + // READ ALL + [HttpGet] + [SwaggerOperation(Summary = "Get all Roles")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetRoles() + { + var roles = await _roleService.GetAllAsync(); + return Ok(roles); + } + + // READ BY ID + [HttpGet("id/{id}", Name = "GetRoleById")] + [SwaggerOperation(Summary = "Get Role by Id")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetRoleById(int id) + { + if (id <= 0) + { + return BadRequest("Invalid Id"); + } + var role = await _roleService.GetByIdAsync(id); + if (role == null) + { + return NotFound(); + } + return Ok(role); + } + + // READ BY NAME + [HttpGet("name/{name}", Name = "GetRoleByName")] + [SwaggerOperation(Summary = "Get Role by Name")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetRoleByName(string name) + { + if (string.IsNullOrEmpty(name)) + { + return BadRequest("Name cannot be empty"); + } + var role = await _roleService.GetByNameAsync(name); + if (role == null) + { + return NotFound(); + } + return Ok(role); + } + + // UPDATE + [HttpPut("id/{id}", Name = "UpdateRole")] + [SwaggerOperation(Summary = "Update Role")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task UpdateRole(int id, UpdatingRoleDto updatingRoleDto) + { + var updated = await _roleService.UpdateRoleAsync(updatingRoleDto); + return Ok(updated); + } + + // DELETE + [HttpDelete("id/{id}", Name = "DeleteRole")] + [SwaggerOperation(Summary = "Delete Role")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task DeleteRole([FromRoute] int id) + { + await _roleService.DeleteRoleAsync(id); + return Ok(); + } + } +} diff --git a/UserManagement.API/Controllers/UserController.cs b/UserManagement.API/Controllers/UserController.cs new file mode 100644 index 0000000..4bb97bb --- /dev/null +++ b/UserManagement.API/Controllers/UserController.cs @@ -0,0 +1,133 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using UserManagement.Application.Dtos.Incomming; +using UserManagement.Application.Interfaces; + +namespace UserManagement.API.Controllers +{ + [Route("api/[controller]")] + [ApiController] + //[Authorize(Roles = "Admin")] + public class UserController : Controller + { + // CTOR + private readonly IUserService _userService; + public UserController(IUserService userService) + { + _userService = userService; + } + + // CREATE + [HttpPost] + [SwaggerOperation(Summary = "Create User")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task CreateUser([FromBody] CreatingUserDto creatingUserDto) + { + // Validate incomming model + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + try + { + // Try to add user asynchronously + var result = await _userService.AddUserAsync(creatingUserDto); + + // If user is successfully created, return a CreatedAtAction response with the created resource + if (result is not null) + { + var id = result.Id; + var createdResource = new { Id = id }; + var actionName = nameof(GetUserById); + var routeValue = new { id = createdResource.Id }; + return CreatedAtAction(actionName, routeValue, createdResource); + } + else + { + return BadRequest("Creation failed"); + } + } + catch (Exception ex) + { + return StatusCode(StatusCodes.Status500InternalServerError, ex.Message); + } + } + + // READ ALL + [HttpGet] + [SwaggerOperation(Summary = "Get all Users")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetAllUsers() + { + var users = await _userService.GetUsersAsync(); + return Ok(users); + } + + // READ BY ID + [HttpGet("id/{id}", Name = "GetUserById")] + [SwaggerOperation(Summary = "Get User by Id")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetUserById(int id) + { + if (id <= 0) + { + return BadRequest("Invalid Id"); + } + var user = await _userService.GetByIdAsync(id); + if (user == null) + { + return NotFound(); + } + return Ok(user); + } + + // READ BY USERNAME + [HttpGet("username/{username}", Name = "GetUserByUsername")] + [SwaggerOperation(Summary = "Get User by Username")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetUserByUsername(string username) + { + if (string.IsNullOrEmpty(username)) + { + return BadRequest("Username connot be empty"); + } + var user = await _userService.GetByUsernameAsync(username); + if (user == null) + { + return NotFound(); + } + return Ok(user); + } + + // UPDATE + [HttpPut("id/{id}", Name = "UpdateUser")] + [SwaggerOperation(Summary = "Update User")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task UpdateUser(int id, UpdatingUserDto updatingUserDto) + { + var updated = await _userService.UpdateUserAsync(updatingUserDto); + return Ok(updated); + } + + // DELETE + [HttpDelete("id/{id}", Name = "DeleteUser")] + [SwaggerOperation(Summary = "Delete User")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task DeleteUser([FromBody] int id) + { + await _userService.DeleteUserAsync(id); + return Ok(); + } + } +} diff --git a/UserManagement.API/Migrations/20240821105311_Initial.Designer.cs b/UserManagement.API/Migrations/20240821105311_Initial.Designer.cs new file mode 100644 index 0000000..a2df229 --- /dev/null +++ b/UserManagement.API/Migrations/20240821105311_Initial.Designer.cs @@ -0,0 +1,107 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using UserManagement.Infrastructure; + +#nullable disable + +namespace UserManagement.API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240821105311_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("UserManagement.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("ID"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime") + .HasColumnName("CREATION_DATE"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ROLE"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("UserManagement.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("ID"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("FIRST_NAME"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("LAST_NAME"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("PASSWORD"); + + b.Property("ROLE") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int") + .HasColumnName("ROLE_ID"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("USER_NAME"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("UserManagement.Domain.Entities.User", b => + { + b.HasOne("UserManagement.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/UserManagement.API/Migrations/20240821105311_Initial.cs b/UserManagement.API/Migrations/20240821105311_Initial.cs new file mode 100644 index 0000000..b9139a9 --- /dev/null +++ b/UserManagement.API/Migrations/20240821105311_Initial.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace UserManagement.API.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + ID = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ROLE = table.Column(type: "nvarchar(max)", nullable: false), + CREATION_DATE = table.Column(type: "datetime", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + ID = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + USER_NAME = table.Column(type: "nvarchar(max)", nullable: false), + FIRST_NAME = table.Column(type: "nvarchar(max)", nullable: false), + LAST_NAME = table.Column(type: "nvarchar(max)", nullable: false), + PASSWORD = table.Column(type: "nvarchar(max)", nullable: false), + ROLE_ID = table.Column(type: "int", nullable: false), + ROLE = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.ID); + table.ForeignKey( + name: "FK_Users_Roles_ROLE_ID", + column: x => x.ROLE_ID, + principalTable: "Roles", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_ROLE_ID", + table: "Users", + column: "ROLE_ID"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Roles"); + } + } +} diff --git a/UserManagement.API/Migrations/ApplicationDbContextModelSnapshot.cs b/UserManagement.API/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..4c1466e --- /dev/null +++ b/UserManagement.API/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,104 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using UserManagement.Infrastructure; + +#nullable disable + +namespace UserManagement.API.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("UserManagement.Domain.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("ID"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime") + .HasColumnName("CREATION_DATE"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ROLE"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("UserManagement.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("ID"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("FIRST_NAME"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("LAST_NAME"); + + b.Property("Password") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("PASSWORD"); + + b.Property("ROLE") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int") + .HasColumnName("ROLE_ID"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("USER_NAME"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("UserManagement.Domain.Entities.User", b => + { + b.HasOne("UserManagement.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/UserManagement.API/Program.cs b/UserManagement.API/Program.cs new file mode 100644 index 0000000..d9be6be --- /dev/null +++ b/UserManagement.API/Program.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.EntityFrameworkCore; +using UserManagement.Application.Interfaces; +using UserManagement.Application.MappingProfiles; +using UserManagement.Application.Services; +using UserManagement.Infrastructure; +using UserManagement.Infrastructure.Interfaces; +using UserManagement.Infrastructure.Repositories; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(s => s.EnableAnnotations()); + +builder.Services.AddAutoMapper(typeof(BasicDtoMappingProfile).Assembly); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); + +builder.Services.AddDbContext(options => +{ + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly("UserManagement.API")); +}); +builder.Services.AddMemoryCache(); + +builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => + { + options.Cookie.HttpOnly = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + options.Cookie.SameSite = SameSiteMode.Strict; + options.LoginPath = "/api/auth/login"; + options.LogoutPath = "/api/auth/logout"; + }); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminOnly", policy => + policy.RequireRole("Admin")); +}); + +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/UserManagement.API/Properties/launchSettings.json b/UserManagement.API/Properties/launchSettings.json new file mode 100644 index 0000000..d6e0adc --- /dev/null +++ b/UserManagement.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:7080", + "sslPort": 44338 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5084", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7288;http://localhost:5084", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/UserManagement.API/UserManagement.API.csproj b/UserManagement.API/UserManagement.API.csproj new file mode 100644 index 0000000..48a72ef --- /dev/null +++ b/UserManagement.API/UserManagement.API.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/UserManagement.API/UserManagement.API.http b/UserManagement.API/UserManagement.API.http new file mode 100644 index 0000000..29da354 --- /dev/null +++ b/UserManagement.API/UserManagement.API.http @@ -0,0 +1,6 @@ +@UserManagement.API_HostAddress = http://localhost:5084 + +GET {{UserManagement.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/UserManagement.API/appsettings.Development.json b/UserManagement.API/appsettings.Development.json new file mode 100644 index 0000000..ff66ba6 --- /dev/null +++ b/UserManagement.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/UserManagement.API/appsettings.json b/UserManagement.API/appsettings.json new file mode 100644 index 0000000..9c23c60 --- /dev/null +++ b/UserManagement.API/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=UserManagement;Trusted_Connection=True;TrustServerCertificate=True;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/UserManagement.App/Dtos/Auth/LoginDto.cs b/UserManagement.App/Dtos/Auth/LoginDto.cs new file mode 100644 index 0000000..5dfff24 --- /dev/null +++ b/UserManagement.App/Dtos/Auth/LoginDto.cs @@ -0,0 +1,4 @@ +namespace UserManagement.Application.Dtos.Auth +{ + public record LoginDto(string Username, string Password); +} diff --git a/UserManagement.App/Dtos/Incomming/CreatingRoleDto.cs b/UserManagement.App/Dtos/Incomming/CreatingRoleDto.cs new file mode 100644 index 0000000..24a97fb --- /dev/null +++ b/UserManagement.App/Dtos/Incomming/CreatingRoleDto.cs @@ -0,0 +1,7 @@ +namespace UserManagement.Application.Dtos.Incomming +{ + public class CreatingRoleDto + { + public string Name { get; set; } + } +} diff --git a/UserManagement.App/Dtos/Incomming/CreatingUserDto.cs b/UserManagement.App/Dtos/Incomming/CreatingUserDto.cs new file mode 100644 index 0000000..e128f78 --- /dev/null +++ b/UserManagement.App/Dtos/Incomming/CreatingUserDto.cs @@ -0,0 +1,15 @@ +namespace UserManagement.Application.Dtos.Incomming +{ + public class CreatingUserDto + { + public string UserName { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Password { get; init; } + + public int RoleId { get; set; } + } +} diff --git a/UserManagement.App/Dtos/Incomming/UpdatingRoleDto.cs b/UserManagement.App/Dtos/Incomming/UpdatingRoleDto.cs new file mode 100644 index 0000000..a366d92 --- /dev/null +++ b/UserManagement.App/Dtos/Incomming/UpdatingRoleDto.cs @@ -0,0 +1,9 @@ +namespace UserManagement.Application.Dtos.Incomming +{ + public class UpdatingRoleDto + { + public int Id { get; set; } + + public string Name { get; set; } + } +} diff --git a/UserManagement.App/Dtos/Incomming/UpdatingUserDto.cs b/UserManagement.App/Dtos/Incomming/UpdatingUserDto.cs new file mode 100644 index 0000000..79b6140 --- /dev/null +++ b/UserManagement.App/Dtos/Incomming/UpdatingUserDto.cs @@ -0,0 +1,17 @@ +namespace UserManagement.Application.Dtos.Incomming +{ + public class UpdatingUserDto + { + public int Id { get; set; } + + public string UserName { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Password { get; init; } + + public int RoleId { get; set; } + } +} diff --git a/UserManagement.App/Dtos/Outgoing/ReadingRoleDto.cs b/UserManagement.App/Dtos/Outgoing/ReadingRoleDto.cs new file mode 100644 index 0000000..6af3c1b --- /dev/null +++ b/UserManagement.App/Dtos/Outgoing/ReadingRoleDto.cs @@ -0,0 +1,9 @@ +namespace UserManagement.Application.Dtos.Outgoing +{ + public class ReadingRoleDto + { + public int Id { get; set; } + + public string Name { get; set; } + } +} diff --git a/UserManagement.App/Dtos/Outgoing/ReadingUserDto.cs b/UserManagement.App/Dtos/Outgoing/ReadingUserDto.cs new file mode 100644 index 0000000..cf22adb --- /dev/null +++ b/UserManagement.App/Dtos/Outgoing/ReadingUserDto.cs @@ -0,0 +1,15 @@ +namespace UserManagement.Application.Dtos.Outgoing +{ + public class ReadingUserDto + { + public int Id { get; set; } + + public string UserName { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public ReadingRoleDto? Role { get; set; } + } +} diff --git a/UserManagement.App/Interfaces/IAuthService.cs b/UserManagement.App/Interfaces/IAuthService.cs new file mode 100644 index 0000000..85135c2 --- /dev/null +++ b/UserManagement.App/Interfaces/IAuthService.cs @@ -0,0 +1,8 @@ +namespace UserManagement.Application.Interfaces +{ + public interface IAuthService + { + // AUTHENTICATE + Task ValidateAsync(string username, string password); + } +} diff --git a/UserManagement.App/Interfaces/IRoleService.cs b/UserManagement.App/Interfaces/IRoleService.cs new file mode 100644 index 0000000..282f00f --- /dev/null +++ b/UserManagement.App/Interfaces/IRoleService.cs @@ -0,0 +1,27 @@ +using UserManagement.Application.Dtos.Incomming; +using UserManagement.Application.Dtos.Outgoing; +using UserManagement.Domain.Entities; + +namespace UserManagement.Application.Interfaces +{ + public interface IRoleService + { + // CREATE + Task AddRoleAsync(CreatingRoleDto creatingRoleDto); + + // READ ALL + Task> GetAllAsync(); + + // READ BY ID + Task GetByIdAsync(int id); + + // READ BY NAME + Task GetByNameAsync(string name); + + // UPDATE + Task UpdateRoleAsync(UpdatingRoleDto updatingRoleDto); + + // DELETE + Task DeleteRoleAsync(int id); + } +} diff --git a/UserManagement.App/Interfaces/IUserService.cs b/UserManagement.App/Interfaces/IUserService.cs new file mode 100644 index 0000000..48f771a --- /dev/null +++ b/UserManagement.App/Interfaces/IUserService.cs @@ -0,0 +1,30 @@ +using UserManagement.Application.Dtos.Incomming; +using UserManagement.Application.Dtos.Outgoing; +using UserManagement.Domain.Entities; + +namespace UserManagement.Application.Interfaces +{ + public interface IUserService + { + // CREATE + Task AddUserAsync(CreatingUserDto creatingUserDto); + + // READ ALL + Task> GetUsersAsync(); + + // READ BY ID + Task GetByIdAsync(int id); + + // READ BY USERNAME + Task GetByUsernameAsync(string username); + + // UPDATE + Task UpdateUserAsync(UpdatingUserDto updatingUserDto); + + // UPDATE USER ROLE + Task UpdateUserRoleAsync(int userId, int roleId); + + // DELETE + Task DeleteUserAsync(int id); + } +} diff --git a/UserManagement.App/MappingProfiles/BasicDtoMappingProfile.cs b/UserManagement.App/MappingProfiles/BasicDtoMappingProfile.cs new file mode 100644 index 0000000..d349942 --- /dev/null +++ b/UserManagement.App/MappingProfiles/BasicDtoMappingProfile.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using UserManagement.Application.Dtos.Incomming; +using UserManagement.Application.Dtos.Outgoing; +using UserManagement.Domain.Entities; + +namespace UserManagement.Application.MappingProfiles +{ + public class BasicDtoMappingProfile : Profile + { + public BasicDtoMappingProfile() + { + // ROLE + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + + // USER + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } + } +} diff --git a/UserManagement.App/Services/AuthService.cs b/UserManagement.App/Services/AuthService.cs new file mode 100644 index 0000000..5cefdcf --- /dev/null +++ b/UserManagement.App/Services/AuthService.cs @@ -0,0 +1,23 @@ +using UserManagement.Application.Interfaces; +using UserManagement.Infrastructure.Interfaces; + +namespace UserManagement.Application.Services +{ + public class AuthService : IAuthService + { + // CTOR + private IUserRepository _userRepository; + public AuthService(IUserRepository userRepository) + { + _userRepository = userRepository; + } + + // AUTHENTICATE + public async Task ValidateAsync(string username, string password) + { + var user = await _userRepository.GetByUsernameAsync(username); + + return user?.Password == password; + } + } +} diff --git a/UserManagement.App/Services/RoleService.cs b/UserManagement.App/Services/RoleService.cs new file mode 100644 index 0000000..e5d5c19 --- /dev/null +++ b/UserManagement.App/Services/RoleService.cs @@ -0,0 +1,73 @@ +using AutoMapper; +using UserManagement.Application.Dtos.Incomming; +using UserManagement.Application.Dtos.Outgoing; +using UserManagement.Application.Interfaces; +using UserManagement.Domain.Entities; +using UserManagement.Infrastructure.Interfaces; + +namespace UserManagement.Application.Services +{ + public class RoleService : IRoleService + { + // CTOR + private readonly IRoleRepository _roleRepository; + private readonly IMapper _mapper; + public RoleService(IRoleRepository roleRepository, IMapper mapper) + { + _roleRepository = roleRepository; + _mapper = mapper; + } + + // CREATE + public async Task AddRoleAsync(CreatingRoleDto creatingRoleDto) + { + var role = _mapper.Map(creatingRoleDto); + var created = await _roleRepository.AddAsync(role); + return created; + } + + // READ ALL + public async Task> GetAllAsync() + { + var roles = await _roleRepository.GetAllAsync(); + var readDto = _mapper.Map>(roles); + return readDto; + } + + // READ BY ID + public async Task GetByIdAsync(int id) + { + var role = await _roleRepository.GetByIdAsync(id); + var readDto = _mapper.Map(role); + return readDto; + } + + // READ BY NAME + public async Task GetByNameAsync(string name) + { + var role = await _roleRepository.GetByNameAsync(name); + var readDto = _mapper.Map(role); + return readDto; + } + + // UPDATE + public async Task UpdateRoleAsync(UpdatingRoleDto updatingRoleDto) + { + var role = _mapper.Map(updatingRoleDto); + bool isUpdated = await _roleRepository.UpdateAsync(role); + return isUpdated; + } + + // DELETE + public async Task DeleteRoleAsync(int id) + { + Role? role = await _roleRepository.GetByIdAsync(id); + + if (role is null) + return false; + + bool isDeleted = await _roleRepository.DeleteAsync(role); + return isDeleted; + } + } +} diff --git a/UserManagement.App/Services/UserService.cs b/UserManagement.App/Services/UserService.cs new file mode 100644 index 0000000..f4df33e --- /dev/null +++ b/UserManagement.App/Services/UserService.cs @@ -0,0 +1,102 @@ +using AutoMapper; +using UserManagement.Application.Dtos.Incomming; +using UserManagement.Application.Dtos.Outgoing; +using UserManagement.Application.Interfaces; +using UserManagement.Domain.Entities; +using UserManagement.Infrastructure.Interfaces; + +namespace UserManagement.Application.Services +{ + public class UserService : IUserService + { + // CTOR + private readonly IUserRepository _userRepository; + private readonly IRoleRepository _roleRepository; + private readonly IMapper _mapper; + public UserService(IUserRepository userRepository, IRoleRepository roleRepository, IMapper mapper) + { + _userRepository = userRepository; + _roleRepository = roleRepository; + _mapper = mapper; + } + + // CREATE + public async Task AddUserAsync(CreatingUserDto creatingUserDto) + { + // validating role + var role = await _roleRepository.GetByIdAsync(creatingUserDto.RoleId); + if (role == null) + { + throw new ArgumentException("Role not found"); + } + + // mapping dto to entity + var user = _mapper.Map(creatingUserDto); + var created = await _userRepository.AddAsync(user); + return created; + } + + // READ ALL + public async Task> GetUsersAsync() + { + var users = await _userRepository.GetAllAsync(); + var readDto = _mapper.Map>(users); + return readDto; + } + + // READ BY ID + public async Task GetByIdAsync(int id) + { + var user = await _userRepository.GetByIdAsync(id); + var readDto = _mapper.Map(user); + return readDto; + } + + // READ BY USERNAME + public async Task GetByUsernameAsync(string username) + { + var user = await _userRepository.GetByUsernameAsync(username); + var readDto = _mapper.Map(user); + return readDto; + } + + // UPDATE + public async Task UpdateUserAsync(UpdatingUserDto updatingUserDto) + { + var user = _mapper.Map(updatingUserDto); + bool isUpdated = await _userRepository.UpdateAsync(user); + return isUpdated; + } + + // UPDATE USER ROLE -- die Rolle eines Users aktualisieren + public async Task UpdateUserRoleAsync(int userId, int roleId) + { + var user = await _userRepository.GetByIdAsync(userId); + if (user == null) + { + throw new ArgumentException("User not found"); + } + + var role = await _roleRepository.GetByIdAsync(roleId); + if (role == null) + { + throw new ArgumentException("Role not found"); + } + + user.RoleId = roleId; + await _userRepository.SaveAsync(); + } + + // DELETE + public async Task DeleteUserAsync(int id) + { + User? user = await _userRepository.GetByIdAsync(id); + + if (user is null) + return false; + + bool isDeleted = await _userRepository.DeleteAsync(user); + return isDeleted; + } + } +} diff --git a/UserManagement.App/UserManagement.Application.csproj b/UserManagement.App/UserManagement.Application.csproj new file mode 100644 index 0000000..ef626a0 --- /dev/null +++ b/UserManagement.App/UserManagement.Application.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/UserManagement.Domain/Entities/Role.cs b/UserManagement.Domain/Entities/Role.cs new file mode 100644 index 0000000..b0f9217 --- /dev/null +++ b/UserManagement.Domain/Entities/Role.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace UserManagement.Domain.Entities +{ + public class Role + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Column("ID")] + public int Id { get; set; } + + [Required] + [Column("ROLE")] + public string Name { get; set; } + + [Required] + [Column("CREATION_DATE", TypeName = "datetime")] + public DateTime CreationDate { get; set; } = DateTime.Now; + } +} diff --git a/UserManagement.Domain/Entities/User.cs b/UserManagement.Domain/Entities/User.cs new file mode 100644 index 0000000..30cea47 --- /dev/null +++ b/UserManagement.Domain/Entities/User.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace UserManagement.Domain.Entities +{ + public class User + { + [Column("ID")] + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Required] + [Column("USER_NAME")] + public string UserName { get; set; } + + [Required] + [Column("FIRST_NAME")] + public string FirstName { get; set; } + + [Required] + [Column("LAST_NAME")] + public string LastName { get; set; } + + [Required] + [Column("PASSWORD")] + public string Password { get; init; } + + [Column("ROLE_ID")] + public int RoleId { get; set; } + + [ForeignKey("ROLE")] + public Role? Role { get; set; } + } +} diff --git a/UserManagement.Domain/UserManagement.Domain.csproj b/UserManagement.Domain/UserManagement.Domain.csproj new file mode 100644 index 0000000..b4501b1 --- /dev/null +++ b/UserManagement.Domain/UserManagement.Domain.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/UserManagement.Infrastructure/ApplicationDbContext.cs b/UserManagement.Infrastructure/ApplicationDbContext.cs new file mode 100644 index 0000000..dd32e7e --- /dev/null +++ b/UserManagement.Infrastructure/ApplicationDbContext.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using UserManagement.Domain.Entities; + +namespace UserManagement.Infrastructure +{ + public class ApplicationDbContext : DbContext + { + public ApplicationDbContext(DbContextOptions options) : base(options) + { + + } + + public DbSet Users { get; set; } + public DbSet Roles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasOne(u => u.Role) + .WithMany() + .HasForeignKey(u => u.RoleId); + } + } +} diff --git a/UserManagement.Infrastructure/Interfaces/IRoleRepository.cs b/UserManagement.Infrastructure/Interfaces/IRoleRepository.cs new file mode 100644 index 0000000..36a0890 --- /dev/null +++ b/UserManagement.Infrastructure/Interfaces/IRoleRepository.cs @@ -0,0 +1,25 @@ +using UserManagement.Domain.Entities; + +namespace UserManagement.Infrastructure.Interfaces +{ + public interface IRoleRepository + { + // CREATE + Task AddAsync(Role role); + + // READ ALL + Task> GetAllAsync(); + + // READ BY ID + Task GetByIdAsync(int id); + + // READ BY NAME + Task GetByNameAsync(string name); + + // UPDATE + Task UpdateAsync(Role role); + + // DELETE + Task DeleteAsync(Role role); + } +} diff --git a/UserManagement.Infrastructure/Interfaces/IUserRepository.cs b/UserManagement.Infrastructure/Interfaces/IUserRepository.cs new file mode 100644 index 0000000..90ac205 --- /dev/null +++ b/UserManagement.Infrastructure/Interfaces/IUserRepository.cs @@ -0,0 +1,28 @@ +using UserManagement.Domain.Entities; + +namespace UserManagement.Infrastructure.Interfaces +{ + public interface IUserRepository + { + // CREATE + Task AddAsync(User user); + + // READ ALL + Task> GetAllAsync(); + + // READ BY ID + Task GetByIdAsync(int id); + + // READ BY USERNAME + Task GetByUsernameAsync(string username); + + // UPDATE + Task UpdateAsync(User user); + + // DELETE + Task DeleteAsync(User user); + + // SAVE + Task SaveAsync(); + } +} diff --git a/UserManagement.Infrastructure/Repositories/RoleRepository.cs b/UserManagement.Infrastructure/Repositories/RoleRepository.cs new file mode 100644 index 0000000..4017010 --- /dev/null +++ b/UserManagement.Infrastructure/Repositories/RoleRepository.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore; +using UserManagement.Domain.Entities; +using UserManagement.Infrastructure.Interfaces; + +namespace UserManagement.Infrastructure.Repositories +{ + public class RoleRepository : IRoleRepository + { + // CTOR + private readonly ApplicationDbContext _context; + public RoleRepository(ApplicationDbContext context) + { + _context = context; + } + + // CREATE + public async Task AddAsync(Role role) + { + await _context.Roles.AddAsync(role); + await _context.SaveChangesAsync(); + return role; + } + + // READ ALL + public async Task> GetAllAsync() + { + return await _context.Roles.ToListAsync(); + } + + // READ BY ID + public async Task GetByIdAsync(int id) + { + return await _context.Roles.FindAsync(id); + } + + // READ BY NAME + public async Task GetByNameAsync(string name) + { + return await _context.Roles.FirstOrDefaultAsync(n => n.Name == name); + } + + // UPDATE + public async Task UpdateAsync(Role role) + { + _context.Entry(role).State = EntityState.Modified; + var results = await _context.SaveChangesAsync(); + return results > 0; + } + + // DELETE + public async Task DeleteAsync(Role role) + { + _context.Roles.Remove(role); + var result = await _context.SaveChangesAsync(); + return result > 0; + } + } +} diff --git a/UserManagement.Infrastructure/Repositories/UserRepository.cs b/UserManagement.Infrastructure/Repositories/UserRepository.cs new file mode 100644 index 0000000..4968936 --- /dev/null +++ b/UserManagement.Infrastructure/Repositories/UserRepository.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore; +using UserManagement.Domain.Entities; +using UserManagement.Infrastructure.Interfaces; + +namespace UserManagement.Infrastructure.Repositories +{ + public class UserRepository : IUserRepository + { + // CTOR + private readonly ApplicationDbContext _context; + public UserRepository(ApplicationDbContext context) + { + _context = context; + } + + // CREATE + public async Task AddAsync(User user) + { + await _context.Users.AddAsync(user); + await _context.SaveChangesAsync(); + return user; + } + + // READ ALL + public async Task> GetAllAsync() + { + return await _context.Users.Include(u => u.Role).ToListAsync(); + } + + // READ BY ID + public async Task GetByIdAsync(int id) + { + return await _context.Users.Where(user => user.Id == id).Include(user => user.Role).FirstAsync(); + } + + // READ BY USERNAME + public async Task GetByUsernameAsync(string username) + { + return await _context.Users.Include(user => user.Role).FirstOrDefaultAsync(u => u.UserName == username); + } + + // UPDATE + public async Task UpdateAsync(User user) + { + _context.Entry(user).State = EntityState.Modified; + var results = await _context.SaveChangesAsync(); + return results > 0; + } + + // DELETE + public async Task DeleteAsync(User user) + { + _context.Users.Remove(user); + var result = await _context.SaveChangesAsync(); + return result > 0; + } + + // SAVE + public async Task SaveAsync() + { + var saved = await _context.SaveChangesAsync(); + return saved > 0 ? true : false; + } + } +} diff --git a/UserManagement.Infrastructure/UserManagement.Infrastructure.csproj b/UserManagement.Infrastructure/UserManagement.Infrastructure.csproj new file mode 100644 index 0000000..ba469d3 --- /dev/null +++ b/UserManagement.Infrastructure/UserManagement.Infrastructure.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/UserManagement.sln b/UserManagement.sln new file mode 100644 index 0000000..d28f465 --- /dev/null +++ b/UserManagement.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.34916.146 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManagement.API", "UserManagement.API\UserManagement.API.csproj", "{549C7966-8315-4049-8E6E-5D5B81E846FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManagement.Infrastructure", "UserManagement.Infrastructure\UserManagement.Infrastructure.csproj", "{AC25118D-000D-4063-A67B-452D5F6E7CBE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManagement.Domain", "UserManagement.Domain\UserManagement.Domain.csproj", "{B29D3BAF-0B74-4BC9-B720-8D1C0B8B7624}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManagement.Application", "UserManagement.App\UserManagement.Application.csproj", "{B2DF78C5-323C-4CCE-90D9-1C6B72678475}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {549C7966-8315-4049-8E6E-5D5B81E846FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {549C7966-8315-4049-8E6E-5D5B81E846FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {549C7966-8315-4049-8E6E-5D5B81E846FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {549C7966-8315-4049-8E6E-5D5B81E846FC}.Release|Any CPU.Build.0 = Release|Any CPU + {AC25118D-000D-4063-A67B-452D5F6E7CBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC25118D-000D-4063-A67B-452D5F6E7CBE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC25118D-000D-4063-A67B-452D5F6E7CBE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC25118D-000D-4063-A67B-452D5F6E7CBE}.Release|Any CPU.Build.0 = Release|Any CPU + {B29D3BAF-0B74-4BC9-B720-8D1C0B8B7624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B29D3BAF-0B74-4BC9-B720-8D1C0B8B7624}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B29D3BAF-0B74-4BC9-B720-8D1C0B8B7624}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B29D3BAF-0B74-4BC9-B720-8D1C0B8B7624}.Release|Any CPU.Build.0 = Release|Any CPU + {B2DF78C5-323C-4CCE-90D9-1C6B72678475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2DF78C5-323C-4CCE-90D9-1C6B72678475}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2DF78C5-323C-4CCE-90D9-1C6B72678475}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2DF78C5-323C-4CCE-90D9-1C6B72678475}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9E5A8215-EA5D-45EF-BCA6-2F42899F383F} + EndGlobalSection +EndGlobal