Skip to content

Commit

Permalink
Merge pull request #30 from sentemon/implement-application-and-infras…
Browse files Browse the repository at this point in the history
…tructure-logic-file-service

Implement application and infrastructure logic file service
  • Loading branch information
sentemon authored Jan 12, 2025
2 parents 65e9d8a + 2bc847f commit 72b160b
Show file tree
Hide file tree
Showing 81 changed files with 1,135 additions and 450 deletions.
7 changes: 7 additions & 0 deletions FitnessApp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileService.Persistence", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileService.Infrastructure", "backend\src\FileService\FileService.Infrastructure\FileService.Infrastructure.csproj", "{4938EBCD-F47E-4ABF-AD79-A5DEB42341DA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.Authentication", "backend\src\Shared\Shared.Authentication\Shared.Authentication.csproj", "{3D6AACD8-2067-4D54-8D97-1E3A6A3B9399}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -189,6 +191,10 @@ Global
{4938EBCD-F47E-4ABF-AD79-A5DEB42341DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4938EBCD-F47E-4ABF-AD79-A5DEB42341DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4938EBCD-F47E-4ABF-AD79-A5DEB42341DA}.Release|Any CPU.Build.0 = Release|Any CPU
{3D6AACD8-2067-4D54-8D97-1E3A6A3B9399}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D6AACD8-2067-4D54-8D97-1E3A6A3B9399}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D6AACD8-2067-4D54-8D97-1E3A6A3B9399}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D6AACD8-2067-4D54-8D97-1E3A6A3B9399}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{2E44C019-ED30-4671-A878-D2F7B6071024} = {84AE3F00-9A59-4A8D-A737-67AB74E5D1FD}
Expand Down Expand Up @@ -222,5 +228,6 @@ Global
{222CCDC4-3D79-4954-B7E0-736D474270D6} = {43F219C3-D6F8-4BA4-890D-4411CCEA3034}
{2F748171-33D6-484F-924C-21539C87483E} = {43F219C3-D6F8-4BA4-890D-4411CCEA3034}
{4938EBCD-F47E-4ABF-AD79-A5DEB42341DA} = {43F219C3-D6F8-4BA4-890D-4411CCEA3034}
{3D6AACD8-2067-4D54-8D97-1E3A6A3B9399} = {112E9C63-BF62-49C1-AD19-03855535225B}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions backend/src/AuthService/AuthService.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"Keycloak": {
"Url": "http://localhost:8080",
"Realm": "fitness-app-realm",
"Audience": "account",
"ClientId": "fitness-app-client",
"ClientSecret": "client-secret",
"AdminUsername": "admin",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Shared.Application.Abstractions;
using Shared.Application.Common;
using Shared.DTO;
using Shared.DTO.Messages;

namespace AuthService.Application.Commands.Register;

Expand Down Expand Up @@ -34,7 +35,7 @@ public async Task<IResult<KeycloakTokenResponse, Error>> HandleAsync(RegisterCom
_context.Users.Add(user);
await _context.SaveChangesAsync();

await _publishEndpoint.Publish(new UserCreatedEvent(
await _publishEndpoint.Publish(new UserCreatedEventMessage(
user.Id,
user.FirstName,
user.LastName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Shared.Application.Abstractions;
using Shared.Application.Common;
using Shared.DTO;
using Shared.DTO.Messages;

namespace AuthService.Application.Commands.UpdateUser;

Expand Down Expand Up @@ -53,7 +54,7 @@ public async Task<IResult<string, Error>> HandleAsync(UpdateUserCommand command)

await _context.SaveChangesAsync();

await _publishEndpoint.Publish(new UserUpdatedEvent(
await _publishEndpoint.Publish(new UserUpdatedEventMessage(
user.Id,
user.FirstName,
user.LastName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Shared\Shared.Authentication\Shared.Authentication.csproj" />
<ProjectReference Include="..\AuthService.Domain\AuthService.Domain.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
using System.Security.Cryptography;
using System.Text.Json;
using AuthService.Domain.Constants;
using AuthService.Infrastructure.Configurations;
using AuthService.Infrastructure.Interfaces;
using AuthService.Infrastructure.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using Shared.Authentication;

namespace AuthService.Infrastructure;

Expand Down Expand Up @@ -40,39 +36,7 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<IUserService, UserService>();

services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = $"{keycloakConfig.Url}/realms/{keycloakConfig.Realm}";
options.Audience = "account";
options.RequireHttpsMetadata = false;
options.MetadataAddress = $"{keycloakConfig.Url}/realms/fitness-app-realm/.well-known/openid-configuration";
options.IncludeErrorDetails = true;

options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = $"http://localhost:8080/realms/{keycloakConfig.Realm}",
ValidateAudience = true,
ValidAudience = "account",
ValidateLifetime = true,
ValidateIssuerSigningKey = false,
SignatureValidator = (token, parameters) =>
{
var jwt = new JsonWebToken(token);
if (parameters.ValidateIssuer && parameters.ValidIssuer != jwt.Issuer)
{
return null;
}

return jwt;
}
};
});
services.AddCustomAuthentication(configuration);
services.AddAuthorization();

return services;
Expand Down
2 changes: 2 additions & 0 deletions backend/src/AuthService/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ COPY AuthService/AuthService.Application/AuthService.Application.csproj AuthServ
COPY AuthService/AuthService.Domain/AuthService.Domain.csproj AuthService/AuthService.Domain/
COPY AuthService/AuthService.Infrastructure/AuthService.Infrastructure.csproj AuthService/AuthService.Infrastructure/
COPY AuthService/AuthService.Persistence/AuthService.Persistence.csproj AuthService/AuthService.Persistence/

COPY Shared/Shared.Application/Shared.Application.csproj Shared/Shared.Application/
COPY Shared/Shared.Authentication/Shared.Authentication.csproj Shared/Shared.Authentication/
COPY Shared/Shared.DTO/Shared.DTO.csproj Shared/Shared.DTO/

RUN dotnet restore AuthService/AuthService.Api/AuthService.Api.csproj
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
</ItemGroup>

<ItemGroup>
Expand Down
32 changes: 31 additions & 1 deletion backend/src/FileService/FileService.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FileService.Application;
using FileService.Application.Queries.DownloadPost;
using FileService.Domain.Constants;
using FileService.Infrastructure;
using FileService.Persistence;
Expand All @@ -10,10 +11,15 @@

builder.WebHost.UseUrls(hostingUrl ?? throw new ArgumentNullException(nameof(hostingUrl), "Hosting URL is not configured."));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddHttpContextAccessor();

builder.Services
.AddPersistenceServices(builder.Configuration)
.AddInfrastructureServices(builder.Configuration)
.AddApplicationServices();
.AddApplicationServices(builder.Configuration);

var app = builder.Build();

Expand All @@ -23,6 +29,30 @@
dbContext.Database.Migrate();
}

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Fitness App File Service API v1");
c.RoutePrefix = "swagger";
});
}

app.MapGet("/health", () => Results.Ok("Healthy"));

app.MapGet("/files/{blobName}", async (string blobName, DownloadPostQueryHandler downloadPostQueryHandler) =>
{
var command = new DownloadPostQuery(blobName);

var result = await downloadPostQueryHandler.HandleAsync(command);

if (!result.IsSuccess)
{
return Results.NotFound(result.Error.Message);
}

return Results.File(result.Response.Content, result.Response.ContentType);
});

app.Run();
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "graphql",
"launchUrl": "swagger",
"applicationUrl": "http://localhost:8004",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
Expand All @@ -23,7 +23,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "graphql",
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7144;http://localhost:8004",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
Expand Down
7 changes: 6 additions & 1 deletion backend/src/FileService/FileService.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"WebHostUrl": "http://0.0.0.0:8004",
"ConnectionStrings": {
"DatabaseConnectionString": "Host=localhost;Port=5432;Username=postgres;Password=mysecretpasswordfordevelopment;Database=FileDb",
"AzureStorageConnectionString": "AccountName=sentemon;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=https;BlobEndpoint=https://localhost:10000/sentemon"
"AzureStorageConnectionString": "AccountName=sentemon;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;DefaultEndpointsProtocol=http;BlobEndpoint=http://localhost:10000/sentemon"
},
"RabbitMq": {
"Host": "localhost",
"Username": "guest",
"Password": "guest"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Shared.Application.Abstractions;

namespace FileService.Application.Commands.DeletePost;

public record DeletePostCommand(Guid PostId) : ICommand;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using FileService.Domain.Constants;
using FileService.Infrastructure.Interfaces;
using FileService.Persistence;
using Microsoft.EntityFrameworkCore;
using Shared.Application.Abstractions;
using Shared.Application.Common;

namespace FileService.Application.Commands.DeletePost;

public class DeletePostCommandHandler : ICommandHandler<DeletePostCommand, string>
{
private readonly IAzureBlobStorageService _azureBlobStorageService;
private readonly FileDbContext _context;

public DeletePostCommandHandler(IAzureBlobStorageService azureBlobStorageService, FileDbContext context)
{
_azureBlobStorageService = azureBlobStorageService;
_context = context;
}

public async Task<IResult<string, Error>> HandleAsync(DeletePostCommand command)
{
var file = await _context.Files.FirstOrDefaultAsync(f => f.ForeignEntityId == command.PostId);

if (file == null)
{
return Result<string>.Failure(new Error(ResponseMessages.FileNotFound));
}

await _azureBlobStorageService.DeleteAsync(file.BlobName, file.BlobContainerName);

_context.Remove(file);
await _context.SaveChangesAsync();

return Result<string>.Success(ResponseMessages.YouSuccessfullyDeletedFile);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

using FileService.Application.DTOs;
using Shared.Application.Abstractions;

namespace FileService.Application.Commands.UploadPost;

public record UploadPostCommand(UploadPostFileDto UploadPostFileDto, string? UserId) : ICommand;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using FileService.Domain.Constants;
using FileService.Infrastructure.Interfaces;
using FileService.Persistence;
using MassTransit;
using Microsoft.AspNetCore.Http;
using Shared.Application.Common;
using Shared.Application.Abstractions;
using Shared.DTO.Messages;
using File = FileService.Domain.Entities.File;

namespace FileService.Application.Commands.UploadPost;

public class UploadPostCommandHandler : ICommandHandler<UploadPostCommand, File>
{
private readonly IAzureBlobStorageService _azureBlobStorageService;
private readonly FileDbContext _context;
private readonly IPublishEndpoint _publishEndpoint;
private readonly IHttpContextAccessor _httpContextAccessor;

public UploadPostCommandHandler(IAzureBlobStorageService azureBlobStorageService, FileDbContext context, IPublishEndpoint publishEndpoint, IHttpContextAccessor httpContextAccessor)
{
_azureBlobStorageService = azureBlobStorageService;
_context = context;
_publishEndpoint = publishEndpoint;
_httpContextAccessor = httpContextAccessor;
}

public async Task<IResult<File, Error>> HandleAsync(UploadPostCommand command)
{
if (command.UserId == null)
{
return Result<File>.Failure(new Error(ResponseMessages.UserIdIsNull));
}

if (command.UploadPostFileDto.FileStream == null)
{
return Result<File>.Failure(new Error(ResponseMessages.FileStreamIsNull));
}

if (command.UploadPostFileDto.ContentType == null)
{
return Result<File>.Failure(new Error(ResponseMessages.ContentTypeNull));
}

var blobName = await _azureBlobStorageService.UploadAsync(
BlobContainerNamesConstants.PostPhotos,
command.UploadPostFileDto.FileStream,
command.UploadPostFileDto.ContentType
);

var file = File.CreateFile(
blobName,
BlobContainerNamesConstants.PostPhotos,
command.UploadPostFileDto.FileStream.Length,
command.UserId,
command.UploadPostFileDto.PostId
);

_context.Add(file);
await _context.SaveChangesAsync();

var request = _httpContextAccessor.HttpContext?.Request;
var host = $"{request?.Scheme}://{request?.Host}";

await _publishEndpoint.Publish(new PostUploadedEventMessage(
file.ForeignEntityId,
$"{host}/file/files/{blobName}"
));

return Result<File>.Success(file);
}
}
Loading

0 comments on commit 72b160b

Please sign in to comment.