Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .env.example

This file was deleted.

77 changes: 77 additions & 0 deletions backend/Platform.Api/Endpoints/AccountEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using Platform.Access.Contracts;
using Platform.Access.Domain.Enums;
using Platform.Access.Domain.Models;
using Platform.Api.Security;
using Platform.Identity.Application;
using Platform.Identity.Contracts;
using Platform.Identity.Domain.Enums;
using Platform.Identity.Domain.Models;

namespace Platform.Api.Endpoints;

public sealed record CreateAccountRequest(string? Email, string? Password)
{
public override string ToString() => "CreateAccountRequest { *** }";
}

public static class AccountEndpoints
{
public static IEndpointRouteBuilder MapAccountEndpoints(this IEndpointRouteBuilder app)
{
app.MapPost("/api/admin/accounts", CreateAccountAsync);
return app;
}

private static async Task<IResult> CreateAccountAsync(
CreateAccountRequest? body,
HttpContext context,
IRequestSubjectResolver subjectResolver,
IAccessDecision accessDecision,
IAccountRegistration registration,
CancellationToken cancellationToken)
{
var correlationId = Guid.NewGuid().ToString("N");

var decision = await accessDecision.DecideAsync(
new AccessRequest(
subjectResolver.Resolve(context),
CreateAccountPolicy.ActionId,
null,
correlationId),
cancellationToken);

if (decision.Outcome != AccessOutcome.Allow)
{
return decision.DenialReason == AccessDenialReason.Unauthenticated
? Results.Json(new { error = "unauthenticated", correlationId },
statusCode: StatusCodes.Status401Unauthorized)
: Results.Json(new { error = "forbidden", correlationId },
statusCode: StatusCodes.Status403Forbidden);
}

if (body is null)
{
return Results.BadRequest(new
{
error = "invalid_input",
errors = new[] { "Telo zahteva je obavezno." },
correlationId
});
}

var result = await registration.RegisterAsync(
new RegisterAccountCommand(body.Email ?? string.Empty, body.Password ?? string.Empty),
cancellationToken);

return result.Outcome switch
{
RegistrationOutcome.Created => Results.Created(
$"/api/admin/accounts/{result.AccountId}",
new { id = result.AccountId, correlationId }),
RegistrationOutcome.IdentifierTaken => Results.Conflict(
new { error = "identifier_taken", correlationId }),
_ => Results.BadRequest(
new { error = "invalid_input", errors = result.Errors, correlationId })
};
}
}
11 changes: 11 additions & 0 deletions backend/Platform.Api/Platform.Api.csproj
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>3fd00482-a54c-426f-9d2a-0f2204817946</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Platform.Access\Platform.Access.csproj" />
<ProjectReference Include="..\Platform.Identity\Platform.Identity.csproj" />
<ProjectReference Include="..\Platform.Audit\Platform.Audit.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.14">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>
58 changes: 47 additions & 11 deletions backend/Platform.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,52 +1,88 @@

using Platform.Access.Composition;
using Platform.Access.Contracts;
using Platform.Access.Domain.Enums;
using Platform.Access.Domain.Models;

using Platform.Identity.Composition;
using Platform.Api.Endpoints;
using Platform.Api.Security;

using Platform.Api.Domain.Identity;
using Platform.Api.Middlewares;
using Platform.Api.Services.Identity;
using Platform.Audit.Composition;

// Oba modula imaju interfejs IAccessDecision, pa se koriste aliasi da ime ne bude dvosmisleno.
// Oba modula imaju IAccessDecision, zato koristimo alias.
using IdentityAccessDecision = Platform.Api.Services.Identity.IAccessDecision;
using PlatformAccessDecision = Platform.Access.Contracts.IAccessDecision;

var builder = WebApplication.CreateBuilder(args);

// Access modul
builder.Services.AddAccessControl();

// IDN-01: registracija naloga
builder.Services.AddIdentityModule();

if (builder.Environment.IsDevelopment())
{
builder.Services.AddSingleton<IRequestSubjectResolver, DevelopmentSubjectResolver>();
}
else
{
builder.Services.AddSingleton<IRequestSubjectResolver, UnauthenticatedSubjectResolver>();
}

// Servisi sa main grane
builder.Services.AddAudit();
builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<IAccountRepository, InMemoryAccountRepository>(); // TODO: zameniti trajnim repozitorijumom (PostgreSQL)
builder.Services.AddSingleton<ICredentialAuthenticator, UnavailableCredentialAuthenticator>(); // zameniti IDN-02 implementacijom
builder.Services.AddSingleton<ISessionIssuer, UnavailableSessionIssuer>(); // zameniti IDN-02 implementacijom

builder.Services.AddSingleton<IAccountRepository, InMemoryAccountRepository>();
builder.Services.AddSingleton<ICredentialAuthenticator, UnavailableCredentialAuthenticator>();
builder.Services.AddSingleton<ISessionIssuer, UnavailableSessionIssuer>();
builder.Services.AddSingleton<IAuditWriter, LoggerAuditWriter>();
builder.Services.AddSingleton<IAdminDirectory, ConfigAdminDirectory>();
builder.Services.AddSingleton<IdentityAccessDecision, AdminOnlyAccessDecision>();

builder.Services.AddScoped<ChangeAccountStatus>();
builder.Services.AddScoped<LoginAccount>();

if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<IIdentityContext, StubIdentityContext>();
builder.Services.AddSingleton<ISessionManager, DevAlwaysActiveSessionManager>(); // samo za lokalni razvoj
builder.Services.AddSingleton<ISessionManager, DevAlwaysActiveSessionManager>();
}
else
{
builder.Services.AddScoped<IIdentityContext, AnonymousIdentityContext>();
builder.Services.AddSingleton<ISessionManager, NoOpSessionRevoker>(); // zameniti kad IDN-02 bude gotov
builder.Services.AddSingleton<ISessionManager, NoOpSessionRevoker>();
}

var app = builder.Build();

// Probni nalozi samo za lokalni razvoj (izmišljeni domen example.com).
// IDN-01: politike i endpoint za kreiranje naloga
app.Services.RegisterIdentityAccess();
app.MapAccountEndpoints();

// Probni nalozi samo za lokalni razvoj
if (app.Environment.IsDevelopment())
{
var repo = app.Services.GetRequiredService<IAccountRepository>();
repo.Save(new UserAccount(Guid.Parse("11111111-1111-1111-1111-111111111111"), "admin@example.com"));
repo.Save(new UserAccount(Guid.Parse("22222222-2222-2222-2222-222222222222"), "student@example.com"));
repo.Save(new UserAccount(Guid.Parse("33333333-3333-3333-3333-333333333333"), "other@example.com"));

repo.Save(new UserAccount(
Guid.Parse("11111111-1111-1111-1111-111111111111"),
"admin@example.com"));

repo.Save(new UserAccount(
Guid.Parse("22222222-2222-2222-2222-222222222222"),
"student@example.com"));

repo.Save(new UserAccount(
Guid.Parse("33333333-3333-3333-3333-333333333333"),
"other@example.com"));
}

app.MapGet("/api/health", () => Results.Ok(new
Expand Down Expand Up @@ -85,4 +121,4 @@

app.Run();

public partial class Program { }
public partial class Program { }
13 changes: 13 additions & 0 deletions backend/Platform.Api/Security/DevelopmentSubjectResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Platform.Access.Domain.Enums;
using Platform.Access.Domain.Models;

namespace Platform.Api.Security;

public sealed class DevelopmentSubjectResolver(IConfiguration configuration) : IRequestSubjectResolver
{
public SubjectRef? Resolve(HttpContext context)
{
var id = configuration["Identity:DevSubjectId"];
return string.IsNullOrWhiteSpace(id) ? null : new SubjectRef(id, AccountState.Active);
}
}
8 changes: 8 additions & 0 deletions backend/Platform.Api/Security/IRequestSubjectResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Platform.Access.Domain.Models;

namespace Platform.Api.Security;

public interface IRequestSubjectResolver
{
SubjectRef? Resolve(HttpContext context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Platform.Access.Domain.Models;

namespace Platform.Api.Security;

public sealed class UnauthenticatedSubjectResolver : IRequestSubjectResolver
{
public SubjectRef? Resolve(HttpContext context) => null;
}
Loading
Loading