Fix and cleanup for Events.WebApi
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
|
<Project Path="Events.Auth/Events.Auth.csproj" />
|
||||||
<Project Path="Events.FilesAPI/Events.FilesAPI.csproj" />
|
<Project Path="Events.FilesAPI/Events.FilesAPI.csproj" />
|
||||||
<Project Path="Events.WebAPI.Contract/Events.WebAPI.Contract.csproj" />
|
<Project Path="Events.WebAPI.Contract/Events.WebAPI.Contract.csproj" />
|
||||||
<Project Path="Events.WebAPI.Handlers.EF/Events.WebAPI.Handlers.EF.csproj" />
|
<Project Path="Events.WebAPI.Handlers.EF/Events.WebAPI.Handlers.EF.csproj" />
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace Events.WebAPI.Util.Startup;
|
namespace Events.Auth;
|
||||||
|
|
||||||
public static class AuthSetupExtensions
|
public static class AuthSetupExtensions
|
||||||
{
|
{
|
||||||
public static void SetupAuthenticationAndAuthorization(this IServiceCollection services, IConfiguration configuration)
|
public static void SetupAuthenticationAndAuthorization(this IServiceCollection services, string authority, string audience)
|
||||||
{
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(authority);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(audience);
|
||||||
|
|
||||||
Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();
|
Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();
|
||||||
|
|
||||||
services.AddScoped<IClaimsTransformation, ScopeClaimsTransformation>();
|
services.AddScoped<IClaimsTransformation, ScopeClaimsTransformation>();
|
||||||
@@ -16,8 +21,8 @@ public static class AuthSetupExtensions
|
|||||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.AddJwtBearer(opt =>
|
.AddJwtBearer(opt =>
|
||||||
{
|
{
|
||||||
opt.Authority = configuration["Auth:Authority"];
|
opt.Authority = authority;
|
||||||
opt.Audience = configuration["Auth:Audience"];
|
opt.Audience = audience;
|
||||||
opt.TokenValidationParameters = new TokenValidationParameters
|
opt.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
ValidateAudience = true,
|
ValidateAudience = true,
|
||||||
17
Events-WebApi/Events.Auth/Events.Auth.csproj
Normal file
17
Events-WebApi/Events.Auth/Events.Auth.csproj
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace Events.WebAPI;
|
namespace Events.Auth;
|
||||||
|
|
||||||
public class Policies
|
public static class Policies
|
||||||
{
|
{
|
||||||
public static IEnumerable<KeyValuePair<string, Action<AuthorizationPolicyBuilder>>> All
|
public static IEnumerable<KeyValuePair<string, Action<AuthorizationPolicyBuilder>>> All
|
||||||
{
|
{
|
||||||
@@ -13,19 +13,9 @@ public class Policies
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Action<AuthorizationPolicyBuilder> ReadData
|
public static Action<AuthorizationPolicyBuilder> ReadData =>
|
||||||
{
|
policy => policy.RequireClaim("scope", "events:read");
|
||||||
get
|
|
||||||
{
|
|
||||||
return policy => policy.RequireClaim("scope", "events:read");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Action<AuthorizationPolicyBuilder> EditData
|
public static Action<AuthorizationPolicyBuilder> EditData =>
|
||||||
{
|
policy => policy.RequireClaim("scope", "events:write");
|
||||||
get
|
|
||||||
{
|
|
||||||
return policy => policy.RequireClaim("scope", "events:write");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
namespace Events.WebAPI.Util.Startup;
|
namespace Events.Auth;
|
||||||
|
|
||||||
public sealed class ScopeClaimsTransformation : IClaimsTransformation
|
public sealed class ScopeClaimsTransformation : IClaimsTransformation
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Events.ClientApp
|
# Events.ClientApp
|
||||||
|
|
||||||
`Events.ClientApp` is the Vue 3 front-end for Topic 2.
|
`Events.ClientApp` is the Vue 3 front-end for the `Events-WebApi` solution.
|
||||||
|
|
||||||
It uses:
|
It uses:
|
||||||
|
|
||||||
@@ -9,7 +9,10 @@ It uses:
|
|||||||
- PrimeVue
|
- PrimeVue
|
||||||
- Auth0 Vue SDK
|
- Auth0 Vue SDK
|
||||||
|
|
||||||
It is intended as a companion UI for the `Events.WebAPI` backend and demonstrates how the secured API can be consumed from a browser application.
|
It is intended as a companion UI for:
|
||||||
|
|
||||||
|
- `Events.WebAPI` for CRUD and lookup operations
|
||||||
|
- `Events.FilesAPI` for certificate and Excel downloads
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
@@ -59,7 +62,7 @@ The simplest setup is to copy `.env.example` to `.env.local` and fill in the rea
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Copy-Item Topic2\Events.ClientApp\.env.example Topic2\Events.ClientApp\.env.local
|
Copy-Item .env.example .env.local
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
@@ -83,12 +86,19 @@ Copy-Item Topic2\Events.ClientApp\.env.example Topic2\Events.ClientApp\.env.loca
|
|||||||
### API configuration
|
### API configuration
|
||||||
|
|
||||||
- `VITE_API_BASE_URL`
|
- `VITE_API_BASE_URL`
|
||||||
Base URL of the Web API
|
Base URL of `Events.WebAPI`
|
||||||
|
|
||||||
|
- `VITE_FILES_API_BASE_URL`
|
||||||
|
Base URL of `Events.FilesAPI`
|
||||||
|
|
||||||
If `VITE_API_BASE_URL` is not set, the app falls back to:
|
If `VITE_API_BASE_URL` is not set, the app falls back to:
|
||||||
|
|
||||||
- `https://localhost:7150`
|
- `https://localhost:7150`
|
||||||
|
|
||||||
|
If `VITE_FILES_API_BASE_URL` is not set, the app falls back to:
|
||||||
|
|
||||||
|
- `https://localhost:7296`
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
```env
|
```env
|
||||||
@@ -96,18 +106,21 @@ VITE_AUTH0_DOMAIN=fer-web2.eu.auth0.com
|
|||||||
VITE_AUTH0_CLIENT_ID=whed5Hdb8l1b1fGyyAz7Qrdsb2oKcSh3
|
VITE_AUTH0_CLIENT_ID=whed5Hdb8l1b1fGyyAz7Qrdsb2oKcSh3
|
||||||
VITE_AUTH0_AUDIENCE=https://erasmus-sta-2026/events-api
|
VITE_AUTH0_AUDIENCE=https://erasmus-sta-2026/events-api
|
||||||
VITE_AUTH0_SCOPE=openid profile email events:read events:write
|
VITE_AUTH0_SCOPE=openid profile email events:read events:write
|
||||||
VITE_API_BASE_URL=https://localhost:7150
|
VITE_API_BASE_URL=https://localhost:7295
|
||||||
|
VITE_FILES_API_BASE_URL=https://localhost:7296
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- `VITE_AUTH0_DOMAIN` and `VITE_AUTH0_CLIENT_ID` are required if you want the Auth0 login flow to work.
|
- `VITE_AUTH0_DOMAIN` and `VITE_AUTH0_CLIENT_ID` are required if you want the Auth0 login flow to work.
|
||||||
- `VITE_AUTH0_AUDIENCE` and `VITE_AUTH0_SCOPE` are optional in code, but usually needed if the API expects bearer tokens with a specific audience and scopes.
|
- `VITE_AUTH0_AUDIENCE` and `VITE_AUTH0_SCOPE` are optional in code, but are usually needed if the APIs expect bearer tokens with a specific audience and scopes.
|
||||||
- `VITE_API_BASE_URL` should point to the running `Events.WebAPI` instance for local development.
|
- `VITE_API_BASE_URL` should point to the running `Events.WebAPI` instance for local development.
|
||||||
|
- `VITE_FILES_API_BASE_URL` should point to the running `Events.FilesAPI` instance for local development.
|
||||||
- `.env.local` is for local development and should not be treated as a shared secrets file.
|
- `.env.local` is for local development and should not be treated as a shared secrets file.
|
||||||
|
|
||||||
## What The Client Demonstrates
|
## What The Client Demonstrates
|
||||||
|
|
||||||
- login and token acquisition through Auth0
|
- login and token acquisition through Auth0
|
||||||
- calling the secured Topic 2 API
|
- calling secured `Events.WebAPI` endpoints
|
||||||
- local development against a separately running ASP.NET Core backend
|
- downloading protected files from `Events.FilesAPI`
|
||||||
|
- local development against separately running ASP.NET Core backends
|
||||||
|
|||||||
@@ -14,11 +14,13 @@
|
|||||||
<PackageReference Include="LargeXlsx" Version="2.0.1" />
|
<PackageReference Include="LargeXlsx" Version="2.0.1" />
|
||||||
<PackageReference Include="MassTransit.RabbitMQ" Version="8.5.9" />
|
<PackageReference Include="MassTransit.RabbitMQ" Version="8.5.9" />
|
||||||
<PackageReference Include="MediatR" Version="14.1.0" />
|
<PackageReference Include="MediatR" Version="14.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
|
||||||
<PackageReference Include="PdfSharpCore" Version="1.3.67" />
|
<PackageReference Include="PdfSharpCore" Version="1.3.67" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Events.Auth\Events.Auth.csproj" />
|
||||||
<ProjectReference Include="..\Events.WebAPI.Contract\Events.WebAPI.Contract.csproj" />
|
<ProjectReference Include="..\Events.WebAPI.Contract\Events.WebAPI.Contract.csproj" />
|
||||||
<ProjectReference Include="..\Events.WebAPI.Handlers.EF\Events.WebAPI.Handlers.EF.csproj" />
|
<ProjectReference Include="..\Events.WebAPI.Handlers.EF\Events.WebAPI.Handlers.EF.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Events.Auth;
|
||||||
using Events.FilesAPI.Features.Certificates.Download;
|
using Events.FilesAPI.Features.Certificates.Download;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace Events.FilesAPI.Features.Certificates;
|
namespace Events.FilesAPI.Features.Certificates;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize(Policy = nameof(Policies.ReadData))]
|
||||||
[Route("Registrations")]
|
[Route("Registrations")]
|
||||||
public class DownloadCertificateController : ControllerBase
|
public class DownloadCertificateController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Events.Auth;
|
||||||
using Events.FilesAPI.Features.RegistrationsExcel.Download;
|
using Events.FilesAPI.Features.RegistrationsExcel.Download;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace Events.FilesAPI.Features.RegistrationsExcel;
|
namespace Events.FilesAPI.Features.RegistrationsExcel;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
|
[Authorize(Policy = nameof(Policies.ReadData))]
|
||||||
[Route("Events")]
|
[Route("Events")]
|
||||||
public class DownloadRegistrationsExcelController : ControllerBase
|
public class DownloadRegistrationsExcelController : ControllerBase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
using Events.FilesAPI.Features.Certificates;
|
using Events.Auth;
|
||||||
using Events.FilesAPI.Features.RegistrationsExcel;
|
|
||||||
using Events.FilesAPI.Infrastructure.Messaging;
|
using Events.FilesAPI.Infrastructure.Messaging;
|
||||||
using Events.FilesAPI.Infrastructure.Options;
|
using Events.FilesAPI.Infrastructure.Options;
|
||||||
using Events.WebAPI.Handlers.EF.Data.Postgres;
|
using Events.WebAPI.Handlers.EF.Data.Postgres;
|
||||||
using MediatR;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -11,7 +9,7 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
builder.Services.AddDbContext<EventsContext>(options =>
|
builder.Services.AddDbContext<EventsContext>(options =>
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("EventDB")));
|
options.UseNpgsql(builder.Configuration.GetConnectionString("EventsPostgres")));
|
||||||
|
|
||||||
builder.Services.AddOptions<GeneratedFilesOptions>()
|
builder.Services.AddOptions<GeneratedFilesOptions>()
|
||||||
.Bind(builder.Configuration.GetSection("Paths"))
|
.Bind(builder.Configuration.GetSection("Paths"))
|
||||||
@@ -24,9 +22,26 @@ builder.Services.AddOptions<GeneratedFilesOptions>()
|
|||||||
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
|
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
|
||||||
|
|
||||||
builder.Services.SetupMassTransit(builder.Configuration);
|
builder.Services.SetupMassTransit(builder.Configuration);
|
||||||
|
builder.Services.SetupAuthenticationAndAuthorization(
|
||||||
|
builder.Configuration["Auth:Authority"] ?? throw new InvalidOperationException("Missing configuration value Auth:Authority."),
|
||||||
|
builder.Configuration["Auth:Audience"] ?? throw new InvalidOperationException("Missing configuration value Auth:Audience."));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseCors(builder =>
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.AllowAnyOrigin()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.WithExposedHeaders("Token-Expired", "Content-Disposition");
|
||||||
|
});
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
"OutputPath": "./Certificates"
|
"OutputPath": "./Certificates"
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"EventDB": "Host=localhost;Port=5432;Database=events;Username=sport;Password=go and look in the secrets file;Persist Security Info=True;"
|
"EventsPostgres": "Host=localhost;Port=5432;Database=events;Username=sport;Password=go and look in the secrets file;Persist Security Info=True;"
|
||||||
|
},
|
||||||
|
"Auth": {
|
||||||
|
"Authority": "https://fer-web2.eu.auth0.com/",
|
||||||
|
"Audience": "https://erasmus-sta-2026/events-api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,42 +15,42 @@ public class GenericCommandHandler<TDal, TDto, TPK> : IRequestHandler<AddCommand
|
|||||||
where TDto: IHasIdAsPK<TPK>
|
where TDto: IHasIdAsPK<TPK>
|
||||||
where TPK : IEquatable<TPK>
|
where TPK : IEquatable<TPK>
|
||||||
{
|
{
|
||||||
protected DbContext Ctx { get; }
|
protected DbContext ctx { get; }
|
||||||
protected ILogger Logger { get; }
|
protected ILogger logger { get; }
|
||||||
protected IMapper Mapper { get; }
|
protected IMapper mapper { get; }
|
||||||
|
|
||||||
protected GenericCommandHandler(DbContext ctx, ILogger logger, IMapper mapper)
|
protected GenericCommandHandler(DbContext ctx, ILogger logger, IMapper mapper)
|
||||||
{
|
{
|
||||||
Ctx = ctx;
|
this.ctx = ctx;
|
||||||
Logger = logger;
|
this.logger = logger;
|
||||||
Mapper = mapper;
|
this.mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<TPK> Handle(AddCommand<TDto, TPK> request, CancellationToken cancellationToken)
|
public virtual async Task<TPK> Handle(AddCommand<TDto, TPK> request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var entity = Mapper.Map<TDto, TDal>(request.Dto);
|
var entity = mapper.Map<TDto, TDal>(request.Dto);
|
||||||
Ctx.Add(entity);
|
ctx.Add(entity);
|
||||||
await Ctx.SaveChangesAsync(cancellationToken);
|
await ctx.SaveChangesAsync(cancellationToken);
|
||||||
return entity.Id;
|
return entity.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task Handle(UpdateCommand<TDto> request, CancellationToken cancellationToken)
|
public virtual async Task Handle(UpdateCommand<TDto> request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var entity = await Ctx.Set<TDal>().FindAsync(request.Dto.Id);
|
var entity = await ctx.Set<TDal>().FindAsync(request.Dto.Id);
|
||||||
if (entity != null)
|
if (entity != null)
|
||||||
{
|
{
|
||||||
Mapper.Map(request.Dto, entity);
|
mapper.Map(request.Dto, entity);
|
||||||
await Ctx.SaveChangesAsync(cancellationToken);
|
await ctx.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.LogError($"UpdateCommand<{typeof(TDto).Name}> : Invalid id #{request.Dto.Id}");
|
logger.LogError($"UpdateCommand<{typeof(TDto).Name}> : Invalid id #{request.Dto.Id}");
|
||||||
throw new ArgumentException($"Invalid id: {request.Dto.Id}");
|
throw new ArgumentException($"Invalid id: {request.Dto.Id}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task Handle(DeleteCommand<TDto, TPK> request, CancellationToken cancellationToken)
|
public virtual async Task Handle(DeleteCommand<TDto, TPK> request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await Ctx.Set<TDal>().Where(d => d.Id.Equals(request.Id)).ExecuteDeleteAsync(cancellationToken);
|
await ctx.Set<TDal>().Where(d => d.Id.Equals(request.Id)).ExecuteDeleteAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ public class RegistrationsCommandsHandler : GenericCommandHandler<Registration,
|
|||||||
|
|
||||||
public override async Task Handle(UpdateCommand<RegistrationDTO> request, CancellationToken cancellationToken)
|
public override async Task Handle(UpdateCommand<RegistrationDTO> request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var entity = await Ctx.Set<Registration>().SingleOrDefaultAsync(r => r.Id == request.Dto.Id, cancellationToken);
|
var entity = await ctx.Set<Registration>().SingleOrDefaultAsync(r => r.Id == request.Dto.Id, cancellationToken);
|
||||||
if (entity == null)
|
if (entity == null)
|
||||||
{
|
{
|
||||||
Logger.LogError("UpdateCommand<{DtoName}> : Invalid id #{Id}", typeof(RegistrationDTO).Name, request.Dto.Id);
|
logger.LogError("UpdateCommand<{DtoName}> : Invalid id #{Id}", typeof(RegistrationDTO).Name, request.Dto.Id);
|
||||||
throw new ArgumentException($"Invalid id: {request.Dto.Id}");
|
throw new ArgumentException($"Invalid id: {request.Dto.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,13 +70,13 @@ public class RegistrationsCommandsHandler : GenericCommandHandler<Registration,
|
|||||||
|
|
||||||
public override async Task Handle(DeleteCommand<RegistrationDTO, int> request, CancellationToken cancellationToken)
|
public override async Task Handle(DeleteCommand<RegistrationDTO, int> request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var entity = await Ctx.Set<Registration>()
|
var entity = await ctx.Set<Registration>()
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.SingleOrDefaultAsync(r => r.Id == request.Id, cancellationToken);
|
.SingleOrDefaultAsync(r => r.Id == request.Id, cancellationToken);
|
||||||
|
|
||||||
if (entity == null)
|
if (entity == null)
|
||||||
{
|
{
|
||||||
Logger.LogError("DeleteCommand<{DtoName}> : Invalid id #{Id}", typeof(RegistrationDTO).Name, request.Id);
|
logger.LogError("DeleteCommand<{DtoName}> : Invalid id #{Id}", typeof(RegistrationDTO).Name, request.Id);
|
||||||
throw new ArgumentException($"Invalid id: {request.Id}");
|
throw new ArgumentException($"Invalid id: {request.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Events.WebAPI.Contract.Command;
|
using Events.WebAPI.Contract.Command;
|
||||||
|
using Events.Auth;
|
||||||
using Events.WebAPI.Contract.DTOs;
|
using Events.WebAPI.Contract.DTOs;
|
||||||
using Events.WebAPI.Contract.Queries.Generic;
|
using Events.WebAPI.Contract.Queries.Generic;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Events.Auth;
|
||||||
using Events.WebAPI.Contract.DTOs;
|
using Events.WebAPI.Contract.DTOs;
|
||||||
using Events.WebAPI.Contract.Queries.Generic;
|
using Events.WebAPI.Contract.Queries.Generic;
|
||||||
using Events.WebAPI.Models;
|
using Events.WebAPI.Models;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Events.WebAPI.Contract.DTOs;
|
using Events.WebAPI.Contract.DTOs;
|
||||||
|
using Events.Auth;
|
||||||
using Events.WebAPI.Contract.LookupQueries;
|
using Events.WebAPI.Contract.LookupQueries;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -8,10 +9,10 @@ namespace Events.WebAPI.Controllers;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("[controller]/[action]")]
|
[Route("[controller]/[action]")]
|
||||||
public class LookupController : ControllerBase
|
public class LookupController(IMediator mediator) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<List<IdName<string>>>> Countries(string? text, [FromServices] IMediator mediator)
|
public async Task<ActionResult<List<IdName<string>>>> Countries(string? text)
|
||||||
{
|
{
|
||||||
var countries = await mediator.Send(new LookupCountryQuery { Text = text });
|
var countries = await mediator.Send(new LookupCountryQuery { Text = text });
|
||||||
return countries;
|
return countries;
|
||||||
@@ -19,7 +20,7 @@ public class LookupController : ControllerBase
|
|||||||
|
|
||||||
[Authorize(Policy = nameof(Policies.ReadData))]
|
[Authorize(Policy = nameof(Policies.ReadData))]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<List<IdName<int>>>> People(string? text, string? countryCode, [FromServices] IMediator mediator)
|
public async Task<ActionResult<List<IdName<int>>>> People(string? text, string? countryCode)
|
||||||
{
|
{
|
||||||
var people = await mediator.Send(new LookupPeopleQuery
|
var people = await mediator.Send(new LookupPeopleQuery
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Events.Auth\Events.Auth.csproj" />
|
||||||
<ProjectReference Include="..\Events.WebAPI.Contract\Events.WebAPI.Contract.csproj" />
|
<ProjectReference Include="..\Events.WebAPI.Contract\Events.WebAPI.Contract.csproj" />
|
||||||
<ProjectReference Include="..\Events.WebAPI.Handlers.EF\Events.WebAPI.Handlers.EF.csproj" />
|
<ProjectReference Include="..\Events.WebAPI.Handlers.EF\Events.WebAPI.Handlers.EF.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Events.Auth;
|
||||||
using Events.WebAPI;
|
using Events.WebAPI;
|
||||||
using Events.WebAPI.Contract.Validation.Sport;
|
using Events.WebAPI.Contract.Validation.Sport;
|
||||||
using Events.WebAPI.Contract.Validation;
|
using Events.WebAPI.Contract.Validation;
|
||||||
@@ -23,7 +24,7 @@ builder.Services
|
|||||||
.AddJsonOptions(configure => configure.JsonSerializerOptions.PropertyNamingPolicy = null);
|
.AddJsonOptions(configure => configure.JsonSerializerOptions.PropertyNamingPolicy = null);
|
||||||
|
|
||||||
builder.Services.AddDbContext<EventsContext>(options =>
|
builder.Services.AddDbContext<EventsContext>(options =>
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("EventDB")));
|
options.UseNpgsql(builder.Configuration.GetConnectionString("EventsPostgres")));
|
||||||
|
|
||||||
builder.Services.Configure<SieveOptions>(builder.Configuration.GetSection("Sieve"));
|
builder.Services.Configure<SieveOptions>(builder.Configuration.GetSection("Sieve"));
|
||||||
builder.Services.AddScoped<ISieveProcessor, SieveProcessor>();
|
builder.Services.AddScoped<ISieveProcessor, SieveProcessor>();
|
||||||
@@ -31,12 +32,12 @@ builder.Services.AddScoped<IValidationMessageProvider, ValidationMessageProvider
|
|||||||
|
|
||||||
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
|
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
|
||||||
builder.Services.AddValidatorsFromAssemblyContaining(typeof(AddSportValidator));
|
builder.Services.AddValidatorsFromAssemblyContaining(typeof(AddSportValidator));
|
||||||
builder.Services.AddMediatR(cfg => {
|
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CountriesLookupQueryHandler).Assembly));
|
||||||
cfg.RegisterServicesFromAssembly(typeof(SportsQueryHandler).Assembly);
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.SetupMassTransit(builder.Configuration);
|
builder.Services.SetupMassTransit(builder.Configuration);
|
||||||
builder.Services.SetupAuthenticationAndAuthorization(builder.Configuration);
|
builder.Services.SetupAuthenticationAndAuthorization(
|
||||||
|
builder.Configuration["Auth:Authority"] ?? throw new InvalidOperationException("Missing configuration value Auth:Authority."),
|
||||||
|
builder.Configuration["Auth:Audience"] ?? throw new InvalidOperationException("Missing configuration value Auth:Audience."));
|
||||||
|
|
||||||
#region AutoMapper settings
|
#region AutoMapper settings
|
||||||
Action<IServiceProvider, IMapperConfigurationExpression> mapperConfigAction = (serviceProvider, cfg) =>
|
Action<IServiceProvider, IMapperConfigurationExpression> mapperConfigAction = (serviceProvider, cfg) =>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"Password": "guest"
|
"Password": "guest"
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"EventDB": "Host=localhost;Port=5432;Database=events;Username=sport;Password=go and look in the secrets file;Persist Security Info=True;"
|
"EventsPostgres": "Host=localhost;Port=5432;Database=events;Username=sport;Password=go and look in the secrets file;Persist Security Info=True;"
|
||||||
},
|
},
|
||||||
"Auth": {
|
"Auth": {
|
||||||
"Authority": "https://fer-web2.eu.auth0.com/",
|
"Authority": "https://fer-web2.eu.auth0.com/",
|
||||||
|
|||||||
@@ -1,121 +1,192 @@
|
|||||||
## Solution Overview
|
## Solution Overview
|
||||||
|
|
||||||
|
`Events-WebApi` currently contains these projects:
|
||||||
|
|
||||||
|
- `Events.ClientApp` - Vue 3 + Vite single-page application
|
||||||
|
- `Events.WebAPI` - main ASP.NET Core REST API for CRUD and lookup operations
|
||||||
|
- `Events.FilesAPI` - ASP.NET Core API for certificate and Excel export downloads
|
||||||
|
- `Events.Auth` - shared authentication project for JWT and policy configuration
|
||||||
|
- `Events.WebAPI.Contract` - DTO, command, query, and message contracts
|
||||||
|
- `Events.WebAPI.Handlers.EF` - EF Core models, `DbContext`, and handlers
|
||||||
|
|
||||||
Swagger UI is exposed at:
|
The typical runtime setup is:
|
||||||
|
|
||||||
```text
|
- `Events.ClientApp` calls `Events.WebAPI`
|
||||||
https://localhost:7290/docs
|
- `Events.ClientApp` downloads files from `Events.FilesAPI`
|
||||||
```
|
- `Events.WebAPI` publishes registration events to RabbitMQ
|
||||||
|
- `Events.FilesAPI` consumes those events and synchronizes generated files
|
||||||
|
- both APIs use PostgreSQL and the same Auth0 authority/audience
|
||||||
|
|
||||||
The exact port may vary depending on your local launch profile.
|
## Default Local URLs
|
||||||
|
|
||||||
|
According to the current `launchSettings.json` files:
|
||||||
|
|
||||||
|
- `Events.WebAPI` -> `https://localhost:7295`
|
||||||
|
- `Events.FilesAPI` -> `https://localhost:7296`
|
||||||
|
- Swagger for `Events.WebAPI` -> `https://localhost:7295/docs`
|
||||||
|
- the Vite dev server for `Events.ClientApp` is typically `http://localhost:5173`
|
||||||
|
|
||||||
|
Ports may differ if you change the launch profile or run the projects with a different profile.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- .NET SDK 10.0
|
- .NET SDK 10.0
|
||||||
- Docker Desktop
|
- Node.js 20+
|
||||||
- PostgreSQL, usually via [docker-definitions](docker-definitions/README.md)
|
- PostgreSQL
|
||||||
- RabbitMQ if you want to run the full API with its real MassTransit transport
|
- RabbitMQ
|
||||||
- Node.js 20+ for the client app
|
- An Auth0 tenant if you want real login and bearer-token flows
|
||||||
- An Auth0 tenant if you want to run real bearer-token and browser-login flows outside the test suite
|
|
||||||
|
## Authentication Setup Options
|
||||||
|
|
||||||
|
To use the solution as-is, you need working Auth0 configuration for both:
|
||||||
|
|
||||||
|
- an API application using the configured audience
|
||||||
|
- a SPA application used by `Events.ClientApp`
|
||||||
|
|
||||||
|
In practice, that means you either:
|
||||||
|
|
||||||
|
1. create and configure the required applications in Auth0, then keep the current `Authorize` attributes and SPA login flow
|
||||||
|
2. simplify the solution for local/demo usage by removing `Authorize` attributes from the APIs and removing Auth0-based authorization from the SPA
|
||||||
|
|
||||||
|
If you choose the second option, remember that:
|
||||||
|
|
||||||
|
- `Events.WebAPI` and `Events.FilesAPI` currently expect bearer tokens on protected endpoints
|
||||||
|
- `Events.ClientApp` is wired to request Auth0 access tokens before calling protected APIs
|
||||||
|
- removing authorization from only one layer usually is not enough; the APIs and SPA should be adjusted together
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
`Events.WebAPI` reads settings from:
|
### WebAPI and FilesAPI
|
||||||
|
|
||||||
- [Events.WebAPI/appsettings.json](Topic2/Events.WebAPI/appsettings.json)
|
Both APIs use:
|
||||||
- [Events.WebAPI/appsettings.Development.json](Topic2/Events.WebAPI/appsettings.Development.json)
|
|
||||||
- the shared .NET user secrets store with id `Erasmus-STA-2026`
|
|
||||||
|
|
||||||
Important configuration sections:
|
|
||||||
|
|
||||||
- `ConnectionStrings:EventDB`
|
|
||||||
- `RabbitMq:Host`
|
- `RabbitMq:Host`
|
||||||
- `RabbitMq:Username`
|
- `RabbitMq:Username`
|
||||||
- `RabbitMq:Password`
|
- `RabbitMq:Password`
|
||||||
- `Auth:Authority`
|
- `Auth:Authority`
|
||||||
- `Auth:Audience`
|
- `Auth:Audience`
|
||||||
- `Paths:Certificates`
|
|
||||||
|
|
||||||
The current Auth configuration in [Events.WebAPI/appsettings.json](Topic2/Events.WebAPI/appsettings.json) is:
|
`Events.FilesAPI` additionally uses:
|
||||||
|
|
||||||
- `Auth:Authority=https://fer-web2.eu.auth0.com/`
|
- `Paths:OutputPath`
|
||||||
- `Auth:Audience=https://erasmus-sta-2026/events-api`
|
|
||||||
|
|
||||||
Set the PostgreSQL connection string:
|
Authentication settings are now applied through the shared `Events.Auth` project, but each API still reads its own values from configuration and passes them into the shared setup.
|
||||||
|
|
||||||
|
### Connection String Note
|
||||||
|
|
||||||
|
The current `Program.cs` files for both APIs read the connection string from:
|
||||||
|
|
||||||
|
- `ConnectionStrings:EventsPostgres`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet user-secrets set "ConnectionStrings:EventDB" "Host=localhost;Port=5432;Database=events;Username=sport;Password=your-password;Persist Security Info=True;" --project Topic2\Events.WebAPI\Events.WebAPI.csproj
|
dotnet user-secrets set "ConnectionStrings:EventsPostgres" "Host=localhost;Port=5432;Database=events;Username=sport;Password=your-password;Persist Security Info=True;" --project Events.WebAPI\Events.WebAPI.csproj
|
||||||
|
dotnet user-secrets set "ConnectionStrings:EventsPostgres" "Host=localhost;Port=5432;Database=events;Username=sport;Password=your-password;Persist Security Info=True;" --project Events.FilesAPI\Events.FilesAPI.csproj
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also override RabbitMQ and Auth settings with user secrets if you do not want to keep local values in `appsettings.json`.
|
If needed, you can also override the Auth values with user secrets:
|
||||||
|
|
||||||
For the SPA client, copy `Topic2/Events.ClientApp/.env.example` to `.env.local`. The example file already contains the current Auth0 values used by this repository:
|
```powershell
|
||||||
|
dotnet user-secrets set "Auth:Authority" "https://fer-web2.eu.auth0.com/" --project Events.WebAPI\Events.WebAPI.csproj
|
||||||
|
dotnet user-secrets set "Auth:Audience" "https://erasmus-sta-2026/events-api" --project Events.WebAPI\Events.WebAPI.csproj
|
||||||
|
|
||||||
|
dotnet user-secrets set "Auth:Authority" "https://fer-web2.eu.auth0.com/" --project Events.FilesAPI\Events.FilesAPI.csproj
|
||||||
|
dotnet user-secrets set "Auth:Audience" "https://erasmus-sta-2026/events-api" --project Events.FilesAPI\Events.FilesAPI.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
### ClientApp
|
||||||
|
|
||||||
|
For the SPA client, copy `Events.ClientApp/.env.example` to `.env.local`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item Events.ClientApp\.env.example Events.ClientApp\.env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
The current example includes:
|
||||||
|
|
||||||
- `VITE_AUTH0_DOMAIN=fer-web2.eu.auth0.com`
|
- `VITE_AUTH0_DOMAIN=fer-web2.eu.auth0.com`
|
||||||
- `VITE_AUTH0_CLIENT_ID=whed5Hdb8l1b1fGyyAz7Qrdsb2oKcSh3`
|
- `VITE_AUTH0_CLIENT_ID=whed5Hdb8l1b1fGyyAz7Qrdsb2oKcSh3`
|
||||||
- `VITE_AUTH0_AUDIENCE=https://erasmus-sta-2026/events-api`
|
- `VITE_AUTH0_AUDIENCE=https://erasmus-sta-2026/events-api`
|
||||||
|
- `VITE_AUTH0_SCOPE=openid profile email events:read events:write`
|
||||||
`Paths:Certificates` points to the directory where generated certificates and Excel files are stored. By default it is:
|
- `VITE_API_BASE_URL=https://localhost:7295`
|
||||||
|
- `VITE_FILES_API_BASE_URL=https://localhost:7296`
|
||||||
```text
|
|
||||||
./Certificates
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running Required Infrastructure
|
## Running Required Infrastructure
|
||||||
|
|
||||||
Start PostgreSQL using the repository Docker definitions:
|
Start PostgreSQL using the repository Docker definitions:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
docker compose -f docker-definitions\postgres-eventsdb\docker-compose.yml up -d
|
docker compose -f ..\docker-definitions\postgres-eventsdb\docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Start RabbitMQ if you want the API to use its real MassTransit transport:
|
Start RabbitMQ:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
docker run -d --name rabbitmq-erasmus-sta -p 5672:5672 -p 15672:15672 rabbitmq:4-management
|
docker run -d --name rabbitmq-for-events -p 5672:5672 -p 15672:15672 rabbitmq:4-management
|
||||||
```
|
```
|
||||||
|
|
||||||
The RabbitMQ management UI is usually available at:
|
The RabbitMQ management UI is typically available at:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
http://localhost:15672
|
http://localhost:15672
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running The Web API
|
## Running The APIs
|
||||||
|
|
||||||
|
Restore and build the full solution:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet restore Topic2\Topic2.sln
|
dotnet restore Events-WebApi.slnx
|
||||||
dotnet build Topic2\Topic2.sln
|
dotnet build Events-WebApi.slnx
|
||||||
dotnet run --project Topic2\Events.WebAPI\Events.WebAPI.csproj
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the API is running:
|
Run `Events.WebAPI`:
|
||||||
|
|
||||||
- open Swagger at `/docs`
|
```powershell
|
||||||
- test anonymous lookup endpoints
|
dotnet run --project Events.WebAPI\Events.WebAPI.csproj
|
||||||
- test secured endpoints with a valid bearer token if your Auth0 configuration is set
|
```
|
||||||
|
|
||||||
|
Run `Events.FilesAPI`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project Events.FilesAPI\Events.FilesAPI.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the APIs are running:
|
||||||
|
|
||||||
|
- open Swagger for `Events.WebAPI` at `/docs`
|
||||||
|
- most `WebAPI` endpoints require a bearer token
|
||||||
|
- `FilesAPI` download endpoints are also protected and require the `events:read` scope
|
||||||
|
|
||||||
## Running The Client App
|
## Running The Client App
|
||||||
|
|
||||||
See [Events.ClientApp/README.md](Topic2/Events.ClientApp/README.md) for full details.
|
See [Events.ClientApp/README.md](/C:/GitRepos/FPMOZ-PI/predavanja/Events-WebApi/Events.ClientApp/README.md:1) for more details.
|
||||||
|
|
||||||
Typical local flow:
|
Typical local flow:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd Topic2\Events.ClientApp
|
cd Events.ClientApp
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The client expects:
|
## Generated Files
|
||||||
|
|
||||||
- `VITE_API_BASE_URL` pointing to the running API
|
`Events.FilesAPI` stores generated files in the directory configured by:
|
||||||
- Auth0 SPA settings if login is enabled
|
|
||||||
|
- `Paths:OutputPath`
|
||||||
|
|
||||||
|
According to the current `appsettings.json`, the default is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
./Certificates
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- If the API fails at startup, verify `ConnectionStrings:EventDB`, RabbitMQ connectivity, and `Paths:Certificates`
|
- If an API cannot connect to the database, first verify `ConnectionStrings:EventsPostgres`
|
||||||
- If Swagger opens but secured requests fail, verify `Auth:Authority`, `Auth:Audience`, and the token scopes
|
- If Swagger opens but secured requests return 401 or 403, verify `Auth:Authority`, `Auth:Audience`, and scope claims
|
||||||
- If the client loads but cannot authenticate, verify the values in `.env.local`
|
- If `ClientApp` cannot download files, verify `VITE_FILES_API_BASE_URL`
|
||||||
- If generated certificates or Excel exports are missing, verify that the output directory exists and is writable
|
- If PDF or XLSX files are not generated, verify `Paths:OutputPath` and filesystem permissions
|
||||||
|
- If `dotnet build` reports locked DLLs, one of the API processes is probably still running in the background
|
||||||
|
|||||||
146
Events-WebApi/docs/structurizr-registration-flow.dsl
Normal file
146
Events-WebApi/docs/structurizr-registration-flow.dsl
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
workspace "Events-WebApi" "Structurizr model za rješenje Events-WebApi" {
|
||||||
|
|
||||||
|
model {
|
||||||
|
auth0 = softwareSystem "Auth0" "Vanjski identity provider za prijavu i JWT tokene." {
|
||||||
|
tags "External", "Auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
postgres = softwareSystem "PostgreSQL" "Relacijska baza podataka za domenu događaja." {
|
||||||
|
tags "External", "Database"
|
||||||
|
}
|
||||||
|
|
||||||
|
rabbitmq = softwareSystem "RabbitMQ" "Sabirnica poruka za asinkronu razmjenu događaja." {
|
||||||
|
tags "External", "MessageBus"
|
||||||
|
}
|
||||||
|
|
||||||
|
eventsSystem = softwareSystem "Events-WebApi" "Rješenje za upravljanje događajima, prijavama i generiranim datotekama." {
|
||||||
|
|
||||||
|
clientApp = container "ClientApp" "" "Vue 3 + Vite" {
|
||||||
|
tags "Frontend"
|
||||||
|
}
|
||||||
|
|
||||||
|
webApi = container "WebAPI" "CRUD servisi" "ASP.NET Core Web API" {
|
||||||
|
tags "API", "WebAPI"
|
||||||
|
}
|
||||||
|
|
||||||
|
filesApi = container "FilesAPI" "API za sinkronizaciju i isporuku PDF/XLSX datoteka." "ASP.NET Core Web API" {
|
||||||
|
tags "API", "FilesAPI"
|
||||||
|
}
|
||||||
|
|
||||||
|
clientApp -> auth0 "Prijava i dohvat pristupnog tokena"
|
||||||
|
clientApp -> webApi "CRUD operacije i registracija za događaj" "JSON/HTTPS"
|
||||||
|
clientApp -> filesApi "Preuzimanje certifikata i Excel izvoza" "HTTPS"
|
||||||
|
|
||||||
|
webApi -> auth0 "Validira JWT tokene" "OAuth2/JWT"
|
||||||
|
webApi -> postgres "Čita i zapisuje događaje, osobe, sportove i prijave" "EF Core/Npgsql"
|
||||||
|
webApi -> rabbitmq "Objavljuje RegistrationCreated/Updated/Deleted" "MassTransit/AMQP"
|
||||||
|
|
||||||
|
rabbitmq -> filesApi "Isporučuje registration događaje consumerima" "MassTransit/AMQP"
|
||||||
|
filesApi -> postgres "Čita podatke za generiranje datoteka" "EF Core/Npgsql"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
views {
|
||||||
|
container eventsSystem "Containers" {
|
||||||
|
include clientApp
|
||||||
|
include webApi
|
||||||
|
include filesApi
|
||||||
|
include auth0
|
||||||
|
include postgres
|
||||||
|
include rabbitmq
|
||||||
|
|
||||||
|
animation {
|
||||||
|
clientApp
|
||||||
|
webApi postgres auth0
|
||||||
|
rabbitmq
|
||||||
|
filesApi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic eventsSystem "RegistrationFlow" "Tok registracije za događaj" {
|
||||||
|
clientApp -> webApi "Korisnik u ClientApp šalje prijavu za događaj"
|
||||||
|
webApi -> auth0 "Validacija pristupnog tokena"
|
||||||
|
webApi -> postgres "Spremanje registracije u bazu"
|
||||||
|
webApi -> rabbitmq "Objava događaja RegistrationCreated"
|
||||||
|
rabbitmq -> filesApi "Isporuka događaja RegistrationCreated"
|
||||||
|
filesApi -> postgres "Dohvat podataka za certifikat i Excel"
|
||||||
|
}
|
||||||
|
|
||||||
|
theme default
|
||||||
|
|
||||||
|
styles {
|
||||||
|
element "Container" {
|
||||||
|
background #ffffff
|
||||||
|
color #222222
|
||||||
|
stroke #444444
|
||||||
|
fontSize 34
|
||||||
|
}
|
||||||
|
|
||||||
|
element "Frontend" {
|
||||||
|
shape WebBrowser
|
||||||
|
background #ffffff
|
||||||
|
color #1565c0
|
||||||
|
stroke #42a5f5
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 36
|
||||||
|
}
|
||||||
|
|
||||||
|
element "API" {
|
||||||
|
shape Hexagon
|
||||||
|
background #ffffff
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 36
|
||||||
|
}
|
||||||
|
|
||||||
|
element "WebAPI" {
|
||||||
|
color #1565c0
|
||||||
|
stroke #42a5f5
|
||||||
|
}
|
||||||
|
|
||||||
|
element "FilesAPI" {
|
||||||
|
color #2e7d32
|
||||||
|
stroke #66bb6a
|
||||||
|
}
|
||||||
|
|
||||||
|
element "Database" {
|
||||||
|
shape Cylinder
|
||||||
|
background #ffffff
|
||||||
|
color #1565c0
|
||||||
|
stroke #42a5f5
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 34
|
||||||
|
description false
|
||||||
|
}
|
||||||
|
|
||||||
|
element "MessageBus" {
|
||||||
|
shape Pipe
|
||||||
|
background #fff8e1
|
||||||
|
color #8d6e63
|
||||||
|
stroke #ff9800
|
||||||
|
strokeWidth 3
|
||||||
|
width 760
|
||||||
|
height 140
|
||||||
|
fontSize 34
|
||||||
|
description false
|
||||||
|
}
|
||||||
|
|
||||||
|
element "External" {
|
||||||
|
opacity 85
|
||||||
|
}
|
||||||
|
|
||||||
|
element "Auth" {
|
||||||
|
background #ffffff
|
||||||
|
color #c62828
|
||||||
|
stroke #e53935
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 34
|
||||||
|
}
|
||||||
|
|
||||||
|
relationship "Relationship" {
|
||||||
|
color #666666
|
||||||
|
thickness 2
|
||||||
|
fontSize 34
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
Events-WebApi/docs/structurizr-webapi-crud-flow.dsl
Normal file
184
Events-WebApi/docs/structurizr-webapi-crud-flow.dsl
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
workspace "Events-WebApi CRUD Flow" "Structurizr model za tipični CRUD tok u WebAPI-ju" {
|
||||||
|
|
||||||
|
model {
|
||||||
|
postgres = softwareSystem "PostgreSQL" "" {
|
||||||
|
tags "Database"
|
||||||
|
}
|
||||||
|
|
||||||
|
rabbitmq = softwareSystem "RabbitMQ" "" {
|
||||||
|
tags "MessageBus"
|
||||||
|
}
|
||||||
|
|
||||||
|
eventsSystem = softwareSystem "Events-WebApi" "Rješenje za upravljanje događajima i prijavama." {
|
||||||
|
clientApp = container "ClientApp" "" "Vue 3 + Vite" {
|
||||||
|
tags "Frontend"
|
||||||
|
}
|
||||||
|
|
||||||
|
webApi = container "WebAPI" "CRUD servisi" "ASP.NET Core Web API" {
|
||||||
|
tags "API"
|
||||||
|
|
||||||
|
controller = component "Controller" "Ulazna točka za CRUD nad prijavama." "ASP.NET Core Controller" {
|
||||||
|
tags "Controller"
|
||||||
|
}
|
||||||
|
|
||||||
|
validation = component "Validation" "Validacija DTO-a i poslovnih pravila prije obrade." "MediatR Pipeline + FluentValidation" {
|
||||||
|
tags "Validation"
|
||||||
|
}
|
||||||
|
|
||||||
|
mediator = component "MediatR" "Prosljeđuje command odgovarajućem handleru." "Mediator" {
|
||||||
|
tags "Mediator"
|
||||||
|
}
|
||||||
|
|
||||||
|
handler = component "Handler" "Obrada Add/Update/Delete zahtjeva i objava događaja." "Command Handler" {
|
||||||
|
tags "Handler"
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext = component "DbContext" "EF Core pristup tablicama registration, person, event i sport." "DbContext" {
|
||||||
|
tags "DbContext"
|
||||||
|
}
|
||||||
|
|
||||||
|
publishEndpoint = component "Publisher" "Objava događaja RegistrationCreated/Updated/Deleted." "MassTransit publisher" {
|
||||||
|
tags "Publisher"
|
||||||
|
}
|
||||||
|
|
||||||
|
controller -> validation "Pokreće validaciju kroz pipeline"
|
||||||
|
validation -> mediator "Prosljeđuje valjani zahtjev"
|
||||||
|
mediator -> handler "Poziva command handler"
|
||||||
|
handler -> dbContext "Čita i zapisuje podatke"
|
||||||
|
handler -> publishEndpoint "Objavljuje događaj nakon promjene registracije"
|
||||||
|
}
|
||||||
|
|
||||||
|
clientApp -> controller "Šalje CRUD zahtjev" "JSON/HTTPS"
|
||||||
|
dbContext -> postgres "Persistira promjene" "EF Core/Npgsql"
|
||||||
|
publishEndpoint -> rabbitmq "Šalje poruku" "AMQP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
views {
|
||||||
|
component webApi "WebApiComponents" {
|
||||||
|
include clientApp
|
||||||
|
include controller
|
||||||
|
include validation
|
||||||
|
include mediator
|
||||||
|
include handler
|
||||||
|
include dbContext
|
||||||
|
include publishEndpoint
|
||||||
|
include postgres
|
||||||
|
include rabbitmq
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic webApi "CreateRegistrationFlow" "Tipični CRUD tok: kreiranje prijave za događaj" {
|
||||||
|
clientApp -> controller "Korisnik šalje prijavu za događaj"
|
||||||
|
controller -> validation "Provjera DTO-a i pravila"
|
||||||
|
validation -> mediator "Valjan zahtjev ide dalje"
|
||||||
|
mediator -> handler "Poziv AddCommand handlera"
|
||||||
|
handler -> dbContext "Spremanje registracije"
|
||||||
|
dbContext -> postgres "INSERT registration"
|
||||||
|
handler -> publishEndpoint "Objava RegistrationCreated"
|
||||||
|
publishEndpoint -> rabbitmq "Slanje događaja"
|
||||||
|
autolayout lr
|
||||||
|
}
|
||||||
|
|
||||||
|
theme default
|
||||||
|
|
||||||
|
styles {
|
||||||
|
element "Element" {
|
||||||
|
background #ffffff
|
||||||
|
color #222222
|
||||||
|
stroke #444444
|
||||||
|
fontSize 40
|
||||||
|
}
|
||||||
|
|
||||||
|
element "Frontend" {
|
||||||
|
shape WebBrowser
|
||||||
|
color #1565c0
|
||||||
|
stroke #42a5f5
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 42
|
||||||
|
}
|
||||||
|
|
||||||
|
element "API" {
|
||||||
|
shape Hexagon
|
||||||
|
color #1565c0
|
||||||
|
stroke #42a5f5
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 42
|
||||||
|
}
|
||||||
|
|
||||||
|
element "Controller" {
|
||||||
|
shape RoundedBox
|
||||||
|
color #1e88e5
|
||||||
|
stroke #64b5f6
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 40
|
||||||
|
}
|
||||||
|
|
||||||
|
element "Validation" {
|
||||||
|
shape RoundedBox
|
||||||
|
color #6a1b9a
|
||||||
|
stroke #ab47bc
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 40
|
||||||
|
}
|
||||||
|
|
||||||
|
element "Mediator" {
|
||||||
|
shape RoundedBox
|
||||||
|
color #00897b
|
||||||
|
stroke #4db6ac
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 40
|
||||||
|
}
|
||||||
|
|
||||||
|
element "Handler" {
|
||||||
|
shape RoundedBox
|
||||||
|
color #2e7d32
|
||||||
|
stroke #66bb6a
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 40
|
||||||
|
}
|
||||||
|
|
||||||
|
element "DbContext" {
|
||||||
|
shape RoundedBox
|
||||||
|
color #ef6c00
|
||||||
|
stroke #ffb74d
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 40
|
||||||
|
}
|
||||||
|
|
||||||
|
element "Publisher" {
|
||||||
|
shape RoundedBox
|
||||||
|
color #5d4037
|
||||||
|
stroke #a1887f
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 40
|
||||||
|
}
|
||||||
|
|
||||||
|
element "Database" {
|
||||||
|
shape Cylinder
|
||||||
|
color #1565c0
|
||||||
|
stroke #42a5f5
|
||||||
|
strokeWidth 3
|
||||||
|
fontSize 40
|
||||||
|
description false
|
||||||
|
}
|
||||||
|
|
||||||
|
element "MessageBus" {
|
||||||
|
shape Pipe
|
||||||
|
background #fff8e1
|
||||||
|
color #8d6e63
|
||||||
|
stroke #ff9800
|
||||||
|
strokeWidth 3
|
||||||
|
width 760
|
||||||
|
height 140
|
||||||
|
fontSize 40
|
||||||
|
description false
|
||||||
|
}
|
||||||
|
|
||||||
|
relationship "Relationship" {
|
||||||
|
color #666666
|
||||||
|
thickness 2
|
||||||
|
fontSize 36
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user