ref: project structure

This commit is contained in:
2026-08-19 23:25:08 +07:00
parent 05a93cfc09
commit a1fe31cf70
155 changed files with 3339 additions and 1007 deletions
+19
View File
@@ -0,0 +1,19 @@
using System.Security.Claims;
using Microsoft.IdentityModel.JsonWebTokens;
namespace mws.backend.dotnet.api;
public static class ClaimsExtensions
{
public static Guid GetUserId(this ClaimsPrincipal principal)
{
var value = principal.FindFirstValue(JwtRegisteredClaimNames.Sub)
?? principal.FindFirstValue(ClaimTypes.NameIdentifier)
?? principal.FindFirstValue("sub");
if (value is not null && Guid.TryParse(value, out var id))
{
return id;
}
throw new UnauthorizedAccessException("Invalid user identity");
}
}
+27
View File
@@ -0,0 +1,27 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using mws.backend.dotnet.application.Auth;
using mws.backend.dotnet.application.Users;
namespace mws.backend.dotnet.api.Controllers;
[ApiController]
[Route("api/auth")]
public class AuthController(ISender sender) : ControllerBase
{
[HttpPost("login")]
[AllowAnonymous]
public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest request, CancellationToken ct)
{
var response = await sender.Send(new LoginCommand(request), ct);
return Ok(response);
}
[HttpGet("me")]
[Authorize]
public async Task<ActionResult<UserDto>> GetProfile(CancellationToken ct)
{
return Ok(await sender.Send(new GetProfileQuery(User.GetUserId()), ct));
}
}
+57
View File
@@ -0,0 +1,57 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using mws.backend.dotnet.application.Documents;
namespace mws.backend.dotnet.api.Controllers;
[ApiController]
[Route("api")]
[Authorize]
public class DocumentsController(ISender sender) : ControllerBase
{
[HttpGet("documents/search")]
public async Task<ActionResult<List<DocumentNodeDto>>> Search([FromQuery] string? q, CancellationToken ct)
{
return Ok(await sender.Send(new SearchDocumentsQuery(User.GetUserId(), q ?? string.Empty), ct));
}
[HttpGet("projects/{projectId:guid}/documents")]
public async Task<ActionResult<List<DocumentNodeDto>>> GetTree(Guid projectId, CancellationToken ct)
{
return Ok(await sender.Send(new GetDocumentTreeQuery(User.GetUserId(), projectId), ct));
}
[HttpPost("projects/{projectId:guid}/documents")]
public async Task<ActionResult<DocumentDto>> Create(Guid projectId, [FromBody] CreateDocumentRequest request, CancellationToken ct)
{
var result = await sender.Send(new CreateDocumentCommand(User.GetUserId(), projectId, request), ct);
return CreatedAtAction(nameof(Get), new { id = result.Id }, result);
}
[HttpGet("documents/{id:guid}")]
public async Task<ActionResult<DocumentDto>> Get(Guid id, CancellationToken ct)
{
return Ok(await sender.Send(new GetDocumentQuery(User.GetUserId(), id), ct));
}
[HttpPut("documents/{id:guid}")]
public async Task<ActionResult<DocumentDto>> Update(Guid id, [FromBody] UpdateDocumentRequest request, CancellationToken ct)
{
return Ok(await sender.Send(new UpdateDocumentCommand(User.GetUserId(), id, request), ct));
}
[HttpPut("documents/{id:guid}/move")]
public async Task<ActionResult<DocumentDto>> Move(Guid id, [FromBody] MoveDocumentRequest request, CancellationToken ct)
{
await sender.Send(new MoveDocumentCommand(User.GetUserId(), id, request.NewParentId), ct);
return Ok(await sender.Send(new GetDocumentQuery(User.GetUserId(), id), ct));
}
[HttpDelete("documents/{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await sender.Send(new DeleteDocumentCommand(User.GetUserId(), id), ct);
return NoContent();
}
}
+50
View File
@@ -0,0 +1,50 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.MasterData;
using mws.backend.dotnet.application.Permissions;
namespace mws.backend.dotnet.api.Controllers;
[ApiController]
[Route("api/masterdata")]
[Authorize]
public class MasterDataController(ISender sender, IPermissionService permissions) : ControllerBase
{
[HttpGet("groups/{group}")]
public async Task<ActionResult<List<MasterDataDto>>> GetByGroup(string group, CancellationToken ct)
{
return Ok(await sender.Send(new GetMasterDataByGroupQuery(group), ct));
}
[HttpGet]
public async Task<ActionResult<PagedResult<MasterDataDto>>> GetAll(
[FromQuery] string? group, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
{
await permissions.EnsureAsync(User.GetUserId(), "masterdata", PermissionAction.View, ct);
return Ok(await sender.Send(new GetMasterDataListQuery(group, page, pageSize), ct));
}
[HttpPost]
public async Task<ActionResult<MasterDataDto>> Create([FromBody] SaveMasterDataRequest request, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "masterdata", PermissionAction.Create, ct);
return Ok(await sender.Send(new CreateMasterDataCommand(request), ct));
}
[HttpPut("{id:guid}")]
public async Task<ActionResult<MasterDataDto>> Update(Guid id, [FromBody] SaveMasterDataRequest request, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "masterdata", PermissionAction.Edit, ct);
return Ok(await sender.Send(new UpdateMasterDataCommand(id, request), ct));
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "masterdata", PermissionAction.Delete, ct);
await sender.Send(new DeleteMasterDataCommand(id), ct);
return NoContent();
}
}
+55
View File
@@ -0,0 +1,55 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Permissions;
namespace mws.backend.dotnet.api.Controllers;
[ApiController]
[Route("api/settings/permission")]
[Authorize]
public class PermissionsController(ISender sender, IPermissionService permissions) : ControllerBase
{
[HttpGet("menu")]
public async Task<ActionResult<List<MenuItemDto>>> GetMenu(CancellationToken ct)
{
return Ok(await permissions.GetMenuAsync(User.GetUserId(), ct));
}
[HttpGet]
public async Task<ActionResult<PagedResult<RoleDto>>> GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
{
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.View, ct);
return Ok(await sender.Send(new GetRolesQuery(page, pageSize), ct));
}
[HttpGet("{id:guid}")]
public async Task<ActionResult<RoleDto>> Get(Guid id, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.View, ct);
return Ok(await sender.Send(new GetRoleQuery(id), ct));
}
[HttpPost]
public async Task<ActionResult<RoleDto>> Create([FromBody] SaveRoleRequest request, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Create, ct);
return Ok(await sender.Send(new CreateRoleCommand(request), ct));
}
[HttpPut("{id:guid}")]
public async Task<ActionResult<RoleDto>> Update(Guid id, [FromBody] SaveRoleRequest request, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Edit, ct);
return Ok(await sender.Send(new UpdateRoleCommand(id, request), ct));
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Delete, ct);
await sender.Send(new DeleteRoleCommand(id), ct);
return NoContent();
}
}
@@ -0,0 +1,40 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Projects;
namespace mws.backend.dotnet.api.Controllers;
[ApiController]
[Route("api/projects/{projectId:guid}/members")]
[Authorize]
public class ProjectMembersController(ISender sender) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PagedResult<ProjectMemberDto>>> GetAll(
Guid projectId, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
{
return Ok(await sender.Send(new GetProjectMembersQuery(User.GetUserId(), projectId, page, pageSize), ct));
}
[HttpPost]
public async Task<ActionResult<ProjectMemberDto>> Add(Guid projectId, [FromBody] AddMemberRequest request, CancellationToken ct)
{
return Ok(await sender.Send(new AddProjectMemberCommand(User.GetUserId(), projectId, request), ct));
}
[HttpPut("{userId:guid}/document-permissions")]
public async Task<ActionResult<ProjectMemberDto>> UpdateDocumentPermissions(
Guid projectId, Guid userId, [FromBody] UpdateMemberDocumentPermissionsRequest request, CancellationToken ct)
{
return Ok(await sender.Send(new UpdateMemberDocumentPermissionsCommand(User.GetUserId(), projectId, userId, request), ct));
}
[HttpDelete("{userId:guid}")]
public async Task<IActionResult> Remove(Guid projectId, Guid userId, CancellationToken ct)
{
await sender.Send(new RemoveProjectMemberCommand(User.GetUserId(), projectId, userId), ct);
return NoContent();
}
}
+57
View File
@@ -0,0 +1,57 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Projects;
namespace mws.backend.dotnet.api.Controllers;
[ApiController]
[Route("api/projects")]
[Authorize]
public class ProjectsController(ISender sender) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PagedResult<ProjectDto>>> GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
{
return Ok(await sender.Send(new GetProjectsQuery(User.GetUserId(), page, pageSize), ct));
}
[HttpGet("search")]
public async Task<ActionResult<List<ProjectDto>>> Search([FromQuery] string? q, CancellationToken ct)
{
return Ok(await sender.Send(new SearchProjectsQuery(User.GetUserId(), q ?? string.Empty), ct));
}
[HttpGet("{projectId:guid}")]
public async Task<ActionResult<ProjectDto>> Get(Guid projectId, CancellationToken ct)
{
return Ok(await sender.Send(new GetProjectQuery(User.GetUserId(), projectId), ct));
}
[HttpGet("{projectId:guid}/overview")]
public async Task<ActionResult<ProjectOverviewDto>> Overview(Guid projectId, CancellationToken ct)
{
return Ok(await sender.Send(new GetProjectOverviewQuery(User.GetUserId(), projectId), ct));
}
[HttpPost]
public async Task<ActionResult<ProjectDto>> Create([FromBody] CreateProjectRequest request, CancellationToken ct)
{
var result = await sender.Send(new CreateProjectCommand(User.GetUserId(), request), ct);
return CreatedAtAction(nameof(Get), new { projectId = result.Id }, result);
}
[HttpPut("{projectId:guid}")]
public async Task<ActionResult<ProjectDto>> Update(Guid projectId, [FromBody] UpdateProjectRequest request, CancellationToken ct)
{
return Ok(await sender.Send(new UpdateProjectCommand(User.GetUserId(), projectId, request), ct));
}
[HttpDelete("{projectId:guid}")]
public async Task<IActionResult> Delete(Guid projectId, CancellationToken ct)
{
await sender.Send(new ArchiveProjectCommand(User.GetUserId(), projectId), ct);
return NoContent();
}
}
+71
View File
@@ -0,0 +1,71 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Tasks;
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
namespace mws.backend.dotnet.api.Controllers;
[ApiController]
[Route("api")]
[Authorize]
public class TasksController(ISender sender) : ControllerBase
{
[HttpGet("tasks/search")]
public async Task<ActionResult<List<TaskDto>>> Search([FromQuery] string? q, CancellationToken ct)
{
return Ok(await sender.Send(new SearchTasksQuery(User.GetUserId(), q ?? string.Empty), ct));
}
[HttpGet("projects/{projectId:guid}/tasks")]
public async Task<ActionResult<PagedResult<TaskDto>>> GetAll(
Guid projectId,
[FromQuery] string? status,
[FromQuery] string? priority,
[FromQuery] Guid? assigneeId,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
CancellationToken ct = default)
{
var statusValue = ParseOptional<TaskStatus>(status);
var priorityValue = ParseOptional<TaskPriority>(priority);
return Ok(await sender.Send(new GetTasksQuery(User.GetUserId(), projectId, statusValue, priorityValue, assigneeId, page, pageSize), ct));
}
[HttpPost("projects/{projectId:guid}/tasks")]
public async Task<ActionResult<TaskDto>> Create(Guid projectId, [FromBody] CreateTaskRequest request, CancellationToken ct)
{
var result = await sender.Send(new CreateTaskCommand(User.GetUserId(), projectId, request), ct);
return CreatedAtAction(nameof(Get), new { id = result.Id }, result);
}
[HttpGet("tasks/{id:guid}")]
public async Task<ActionResult<TaskDto>> Get(Guid id, CancellationToken ct)
{
return Ok(await sender.Send(new GetTaskQuery(User.GetUserId(), id), ct));
}
[HttpPut("tasks/{id:guid}")]
public async Task<ActionResult<TaskDto>> Update(Guid id, [FromBody] UpdateTaskRequest request, CancellationToken ct)
{
return Ok(await sender.Send(new UpdateTaskCommand(User.GetUserId(), id, request), ct));
}
[HttpDelete("tasks/{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await sender.Send(new DeleteTaskCommand(User.GetUserId(), id), ct);
return NoContent();
}
private static TEnum? ParseOptional<TEnum>(string? value) where TEnum : struct, Enum
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
return Enum.TryParse<TEnum>(value, ignoreCase: true, out var result) ? result : throw new BadRequestException($"Invalid {typeof(TEnum).Name}: {value}");
}
}
+161
View File
@@ -0,0 +1,161 @@
using AutoMapper;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Permissions;
using mws.backend.dotnet.application.Users;
using mws.backend.dotnet.domain.Users;
using mws.backend.dotnet.infrastructure.Persistence;
namespace mws.backend.dotnet.api.Controllers;
[ApiController]
[Route("api/users")]
[Authorize]
public class UsersController(ISender sender, AppDbContext db, IPermissionService permissions, IMapper mapper) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PagedResult<UserDto>>> GetAll([FromQuery] string? q, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, CancellationToken ct = default)
{
return Ok(await sender.Send(new GetUsersQuery(User.GetUserId(), q, page, pageSize), ct));
}
[HttpPost]
public async Task<ActionResult<UserDto>> Create([FromBody] CreateUserRequest request, CancellationToken ct)
{
return Ok(await sender.Send(new CreateUserCommand(User.GetUserId(), request), ct));
}
[HttpPut("{id:guid}")]
public async Task<ActionResult<UserDto>> Update(Guid id, [FromBody] UpdateUserRequest request, CancellationToken ct)
{
return Ok(await sender.Send(new UpdateUserCommand(User.GetUserId(), id, request), ct));
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await sender.Send(new DeleteUserCommand(User.GetUserId(), id), ct);
return NoContent();
}
[HttpPost("{id:guid}/reset-password")]
public async Task<IActionResult> ResetPassword(Guid id, [FromBody] ResetPasswordRequest request, CancellationToken ct)
{
await sender.Send(new ResetPasswordCommand(User.GetUserId(), id, request), ct);
return NoContent();
}
[HttpGet("list")]
public async Task<ActionResult<List<UserDto>>> GetList([FromQuery] string? q, CancellationToken ct)
{
var query = db.Users.AsQueryable();
if (!string.IsNullOrWhiteSpace(q))
{
var lower = q.Trim().ToLower();
query = query.Where(u => u.Username.ToLower().Contains(lower) || u.DisplayName.ToLower().Contains(lower));
}
var users = await query.OrderBy(u => u.DisplayName).ToListAsync(ct);
return Ok(mapper.Map<List<UserDto>>(users));
}
[HttpGet("{id:guid}/roles")]
public async Task<ActionResult<UserRoleDetailDto>> GetUserRoles(Guid id, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.View, ct);
var user = await db.Users
.Include(u => u.UserRoles)
.ThenInclude(ur => ur.Role)
.FirstOrDefaultAsync(u => u.Id == id, ct);
if (user is null) return NotFound("User not found");
var allRoles = await db.Roles.Include(r => r.Permissions).OrderBy(r => r.Name).ToListAsync(ct);
var assignedRoleIds = user.UserRoles.Select(ur => ur.RoleId).ToHashSet();
var assigned = allRoles.Where(r => assignedRoleIds.Contains(r.Id)).ToList();
var unassigned = allRoles.Where(r => !assignedRoleIds.Contains(r.Id)).ToList();
return Ok(new UserRoleDetailDto
{
UserId = user.Id,
Username = user.Username,
DisplayName = user.DisplayName,
AssignedRoles = mapper.Map<List<RoleDto>>(assigned),
UnassignedRoles = mapper.Map<List<RoleDto>>(unassigned)
});
}
[HttpPost("{id:guid}/roles")]
public async Task<IActionResult> AssignRole(Guid id, [FromBody] AssignRoleRequest req, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Edit, ct);
var user = await db.Users.Include(u => u.UserRoles).FirstOrDefaultAsync(u => u.Id == id, ct);
if (user is null) return NotFound("User not found");
if (!user.UserRoles.Any(ur => ur.RoleId == req.RoleId))
{
user.UserRoles.Add(new UserRole { UserId = id, RoleId = req.RoleId });
await db.SaveChangesAsync(ct);
}
return NoContent();
}
[HttpDelete("{id:guid}/roles/{roleId:guid}")]
public async Task<IActionResult> UnassignRole(Guid id, Guid roleId, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.Edit, ct);
var ur = await db.UserRoles.FirstOrDefaultAsync(x => x.UserId == id && x.RoleId == roleId, ct);
if (ur != null)
{
db.UserRoles.Remove(ur);
await db.SaveChangesAsync(ct);
}
return NoContent();
}
[HttpGet("{id:guid}/permissions")]
public async Task<ActionResult<List<MenuItemDto>>> GetUserPermissions(Guid id, CancellationToken ct)
{
await permissions.EnsureAsync(User.GetUserId(), "permissions", PermissionAction.View, ct);
var roleIds = await db.UserRoles.Where(ur => ur.UserId == id).Select(ur => ur.RoleId).ToListAsync(ct);
var permissionsList = await db.RolePermissions.Where(p => roleIds.Contains(p.RoleId)).ToListAsync(ct);
var merged = permissionsList.GroupBy(p => p.Screen).ToDictionary(
g => g.Key,
g => new
{
CanView = g.Any(p => p.CanView),
CanCreate = g.Any(p => p.CanCreate),
CanEdit = g.Any(p => p.CanEdit),
CanDelete = g.Any(p => p.CanDelete),
}
);
var res = ScreenCatalog.Screens.Select(s =>
{
merged.TryGetValue(s.Key, out var p);
return new MenuItemDto
{
Key = s.Key,
Label = s.Label,
Path = s.Path,
CanView = p?.CanView ?? false,
CanCreate = p?.CanCreate ?? false,
CanEdit = p?.CanEdit ?? false,
CanDelete = p?.CanDelete ?? false,
};
}).ToList();
return Ok(res);
}
}
+46
View File
@@ -0,0 +1,46 @@
using System.Text.Json;
using mws.backend.dotnet.application.Common;
namespace mws.backend.dotnet.api.Middleware;
public class ApiExceptionMiddleware(RequestDelegate next, ILogger<ApiExceptionMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleAsync(context, ex);
}
}
private async Task HandleAsync(HttpContext context, Exception ex)
{
var (statusCode, message) = ex switch
{
BadRequestException => (StatusCodes.Status400BadRequest, ex.Message),
UnauthorizedException => (StatusCodes.Status401Unauthorized, ex.Message),
ForbiddenException => (StatusCodes.Status403Forbidden, ex.Message),
NotFoundException => (StatusCodes.Status404NotFound, ex.Message),
_ => (StatusCodes.Status500InternalServerError, "An unexpected error occurred"),
};
if (statusCode == StatusCodes.Status500InternalServerError)
{
logger.LogError(ex, "Unhandled exception in {Path}", context.Request.Path);
}
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(new { message }));
}
}
public static class ApiExceptionMiddlewareExtensions
{
public static IApplicationBuilder UseApiExceptionMiddleware(this IApplicationBuilder app)
=> app.UseMiddleware<ApiExceptionMiddleware>();
}
+122
View File
@@ -0,0 +1,122 @@
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi;
using mws.backend.dotnet.api.Middleware;
using mws.backend.dotnet.application.Auth;
using mws.backend.dotnet.infrastructure;
using mws.backend.dotnet.infrastructure.Persistence;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(
new System.Text.Json.Serialization.JsonStringEnumConverter());
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "MWS API",
Version = "v1"
});
options.AddSecurityDefinition("bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using Bearer scheme."
});
options.AddSecurityRequirement(document =>
new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("bearer", document)] = []
});
});
builder.Services.AddInfrastructure(builder.Configuration);
var jwtSection = builder.Configuration.GetSection("Jwt");
var jwtSecret = jwtSection["Secret"];
var jwtIssuer = jwtSection["Issuer"];
var jwtAudience = jwtSection["Audience"];
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtAudience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret))
};
options.MapInboundClaims = false;
});
builder.Services.AddAuthorization();
builder.Services.AddCors(options =>
{
var origins = builder.Configuration["Cors:Origins"];
options.AddPolicy("Frontend", policy =>
{
policy
.AllowAnyHeader()
.AllowAnyMethod()
.WithOrigins(origins.Split(';', StringSplitOptions.RemoveEmptyEntries));
});
});
var app = builder.Build();
app.UseApiExceptionMiddleware();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync();
await DbSeeder.SeedAsync(db, scope.ServiceProvider.GetRequiredService<IPasswordHasher>());
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint(
"/swagger/v1/swagger.json",
"MWS API v1");
options.EnablePersistAuthorization();
});
}
app.UseHttpsRedirection();
app.UseCors("Frontend");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
+23
View File
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:2000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:3000;http://localhost:2000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>mws.backend.dotnet.api</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.11">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\application\application.csproj" />
<ProjectReference Include="..\domain\domain.csproj" />
<ProjectReference Include="..\infrastructure\infrastructure.csproj" />
</ItemGroup>
</Project>
+22
View File
@@ -0,0 +1,22 @@
{
"ConnectionStrings": {
"Default": "Host=192.168.2.100;Port=5432;Database=mws;Username=postgres;Password=Pa55w0rd"
},
"Jwt": {
"Secret": "mws-development-secret-key-change-me-in-production-123456",
"Issuer": "Mws",
"Audience": "Mws.Clients",
"Expiration": "12:00:00"
},
"Cors": {
"Origins": "*"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"AllowedHosts": "*"
}
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}