Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6564b10
Created BlogMinimalApi project
lakatoshv Feb 26, 2026
ab764a0
Add Startup configure services installers
lakatoshv Feb 26, 2026
fee110d
Add Api versioning installer
lakatoshv Feb 26, 2026
8f04486
Add Application base installer
lakatoshv Feb 26, 2026
d7c45fb
Add Application services installer
lakatoshv Feb 27, 2026
2336bed
Add Authentication and authorization installer
lakatoshv Feb 27, 2026
3cdf743
Add Cache installer
lakatoshv Feb 27, 2026
dd2af3e
Add Cors installer
lakatoshv Feb 27, 2026
16731ad
Add Data repositories installer
lakatoshv Feb 28, 2026
287dbea
Add Db installer
lakatoshv Feb 28, 2026
f035faa
Add Email installer
lakatoshv Mar 1, 2026
0d67ed9
Add Health Check configuration
lakatoshv Mar 1, 2026
2b4fef1
Add Health check installer
lakatoshv Mar 2, 2026
f7007c7
Add Identity installer
lakatoshv Mar 2, 2026
b420e01
Add Mediator installer
lakatoshv Mar 2, 2026
8dd9b6b
Add Swagger installer
lakatoshv Mar 2, 2026
becb151
Add cached attribute
lakatoshv Mar 3, 2026
b68c1ab
Add Swagger filter
lakatoshv Mar 3, 2026
66f5e0e
Add validation filter
lakatoshv Mar 3, 2026
a407354
Add swagger requests and responses examples
lakatoshv Mar 3, 2026
71aac43
Add view models
lakatoshv Mar 4, 2026
3541028
Add mappers
lakatoshv Mar 4, 2026
9ec7138
Configure authentication
lakatoshv Mar 4, 2026
2b56149
Add base configuration
lakatoshv Mar 4, 2026
d4ed07b
Configure routes
lakatoshv Mar 5, 2026
f238b25
Configure swagger
lakatoshv Mar 5, 2026
b280967
Configure Program file
lakatoshv Mar 5, 2026
cd8b18d
Set up Accounts Api endpoints
lakatoshv Mar 5, 2026
c0ab7f9
Set up Comments api endpoints
lakatoshv Mar 6, 2026
4928125
Set up Health Check Api endpoints
lakatoshv Mar 6, 2026
ff94782
Set up Messages Api endpoints
lakatoshv Mar 6, 2026
aab8575
Set up Posts Api endpoints
lakatoshv Mar 6, 2026
4ab07b7
Configure Routes
lakatoshv Mar 7, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,5 @@
/BlogWebApp/Tests/Blog.UnitTests/bin/Debug/net10.0
/Sdk/Blog.Sdk.ExampleUsage/bin/Debug/net10.0
/BlogWebApp/Aspire/Blog.Aspire/Blog.Aspire.AppHost/bin/Debug/net10.0
/BlogWebApp/BlogMinimalApi/bin/Debug/net10.0
/BlogWebApp/BlogMinimalApi/obj
187 changes: 187 additions & 0 deletions BlogWebApp/BlogMinimalApi/ApiEndpoints/AccountsApiEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
using Asp.Versioning;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test

using AutoMapper;
using Blog.CommonServices.EmailServices.Interfaces;
using Blog.Contracts.V1;
using Blog.Contracts.V1.Requests.UsersRequests;
using Blog.Contracts.V1.Responses.Chart;
using Blog.Contracts.V1.Responses.UsersResponses;
using Blog.Data.Models;
using Blog.EntityServices.EntityFrameworkServices.Identity.Auth;
using Blog.EntityServices.EntityFrameworkServices.Identity.Registration;
using Blog.EntityServices.EntityFrameworkServices.Identity.User;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace BlogMinimalApi.ApiEndpoints;

/// <summary>
/// Accounts Api endpoints.
/// </summary>
public class AccountsApiEndpoints : IRoutesInstaller
{
public void InstallApiRoutes(WebApplication app)
{
var versionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1, 0))
.ReportApiVersions()
.Build();

var group = app.MapGroup(ApiRoutes.AccountsController.Accounts)
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0)
.WithTags("Accounts");

// -------------------------
// PUBLIC ENDPOINTS
// -------------------------

var publicGroup = group.AllowAnonymous();

// Initialize (roles list)
publicGroup.MapGet(ApiRoutes.AccountsController.Initialize,
async (string userId,
RoleManager<ApplicationRole> roleManager,
IMapper mapper) =>
{
if (string.IsNullOrEmpty(userId))
return Results.BadRequest("Something went wrong");

var roles = await roleManager.Roles.ToListAsync();
var response = mapper.Map<List<RoleResponse>>(roles);

return Results.Ok(response);
})
.Produces<List<RoleResponse>>()
.Produces<string>(400);


// Get All Users
publicGroup.MapGet(ApiRoutes.AccountsController.GetAllUsers,
async (UserManager<ApplicationUser> userManager,
IMapper mapper) =>
{
var users = await userManager.Users.Include(u => u.Roles).ToListAsync();
var response = mapper.Map<List<AccountResponse>>(users);

return Results.Ok(response);
})
.Produces<List<AccountResponse>>();


// Login
publicGroup.MapPost(ApiRoutes.AccountsController.Login,
async (LoginRequest credentials,
IAuthService authService) =>
{
if (credentials is null)
return Results.BadRequest();

var jwt = await authService.GetJwtAsync(credentials.Email, credentials.Password);

return jwt is null
? Results.BadRequest("Invalid name or password")
: Results.Ok(jwt);
})
.Produces<string>()
.Produces<string>(400);


// Register
publicGroup.MapPost(ApiRoutes.AccountsController.Register,
async (RegistrationRequest model,
IMapper mapper,
IRegistrationService registrationService,
UserManager<ApplicationUser> userManager) =>
{
if (model is null)
return Results.BadRequest();

model.UserName = model.Email;

var user = mapper.Map<ApplicationUser>(model);
user.CreatedOn = DateTime.UtcNow;
user.SecurityStamp = Guid.NewGuid().ToString();

var result = await registrationService.RegisterAsync(user, model.Password);

if (!result.Succeeded)
return Results.BadRequest(result);

model.Roles ??= ["User"];

foreach (var role in model.Roles)
{
var roleResult = await userManager.AddToRoleAsync(user, role);
if (!roleResult.Succeeded)
return Results.BadRequest(roleResult);
}

return Results.NoContent();
})
.Produces(204)
.Produces<IdentityResult>(400);

// -------------------------
// PRIVATE ENDPOINTS
// -------------------------

var privateGroup = group.RequireAuthorization();

// Send confirmation email
privateGroup.MapGet(ApiRoutes.AccountsController.SendConfirmationEmail,
async (IUserService userService,
IEmailExtensionService emailService,
HttpContext context) =>
{
var email = context.User.Identity?.Name;

if (string.IsNullOrEmpty(email))
return Results.BadRequest();

var token = await userService.GetEmailVerificationTokenAsync(email);
await emailService.SendVerificationEmailAsync(email, token);

return Results.NoContent();
})
.Produces(204);

// Users activity
privateGroup.MapGet(ApiRoutes.AccountsController.UsersActivity,
async (IUserService userService) =>
{
var activity = await userService.GetUsersActivity();

return activity is null
? Results.NotFound()
: Results.Ok(activity);
})
.Produces<ChartDataModel>()
.Produces(404);

// Change password
privateGroup.MapPut(ApiRoutes.AccountsController.ChangePassword,
async (ChangePasswordRequest model,
IUserService userService,
HttpContext context) =>
{
if (model is null)
return Results.BadRequest();

var username = context.User.Identity?.Name;

if (string.IsNullOrEmpty(username))
return Results.BadRequest();

var result = await userService.ChangePasswordAsync(
username,
model.OldPassword,
model.NewPassword);

return result.Succeeded
? Results.NoContent()
: Results.BadRequest(result.Errors);
})
.Produces(204)
.Produces(400);
}
}
198 changes: 198 additions & 0 deletions BlogWebApp/BlogMinimalApi/ApiEndpoints/CommentsApiEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
using Asp.Versioning;
using AutoMapper;
using Blog.Contracts.V1;
using Blog.Contracts.V1.Requests;
using Blog.Contracts.V1.Requests.CommentsRequests;
using Blog.Contracts.V1.Responses;
using Blog.Contracts.V1.Responses.Chart;
using Blog.Contracts.V1.Responses.CommentsResponses;
using Blog.Data.Models;
using Blog.EntityServices.Interfaces;
using Blog.Services.Core.Dtos;

namespace BlogMinimalApi.ApiEndpoints;

/// <summary>
/// Comments Api endpoints.
/// </summary>
public class CommentsApiEndpoints : IRoutesInstaller
{
public void InstallApiRoutes(WebApplication app)
{
var versionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1, 0))
.ReportApiVersions()
.Build();

var group = app.MapGroup(ApiRoutes.CommentsController.Comments)
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0)
.WithTags("Comments");

// -------------------------
// PUBLIC
// -------------------------

var publicGroup = group.AllowAnonymous();

// Get All Comments
publicGroup.MapGet("/",
async (ICommentsService service, IMapper mapper) =>
{
var comments = await service.GetAllAsync();
var response = mapper.Map<List<CommentResponse>>(comments);

return Results.Ok(response);
})
.Produces<List<CommentResponse>>();


// Comments Activity
publicGroup.MapGet(ApiRoutes.CommentsController.CommentsActivity,
async (ICommentsService service) =>
{
var activity = await service.GetCommentsActivity();

return activity is null
? Results.NotFound()
: Results.Ok(activity);
})
.Produces<ChartDataModel>()
.Produces(404);


// Get Paged Comments
publicGroup.MapPost(ApiRoutes.CommentsController.GetCommentsByFilter,
async (SortParametersRequest? sortParameters,
ICommentsService service,
IMapper mapper) =>
{
sortParameters ??= new SortParametersRequest();
sortParameters.CurrentPage ??= 1;
sortParameters.PageSize = 10;

var result = await service.GetPagedComments(
mapper.Map<SortParametersDto>(sortParameters));

return result is null
? Results.NotFound()
: Results.Ok(mapper.Map<PagedCommentsResponse>(result));
})
.Produces<PagedCommentsResponse>()
.Produces(404);


// Get Comments By Post
publicGroup.MapPost(ApiRoutes.CommentsController.GetCommentsByPost,
async (int id,
SortParametersRequest? sortParameters,
ICommentsService service,
IMapper mapper) =>
{
sortParameters ??= new SortParametersRequest();
sortParameters.CurrentPage ??= 1;
sortParameters.PageSize = 10;

var result = await service.GetPagedCommentsByPostId(
id,
mapper.Map<SortParametersDto>(sortParameters));

return result is null
? Results.NotFound()
: Results.Ok(mapper.Map<PagedCommentsResponse>(result));
})
.Produces<PagedCommentsResponse>()
.Produces(404);


// Get Comment By ID
publicGroup.MapGet("/{id:int}",
async (int id,
ICommentsService service,
IMapper mapper) =>
{
var comment = await service.GetCommentAsync(id);

return comment is null
? Results.NotFound()
: Results.Ok(mapper.Map<CommentResponse>(comment));
})
.WithName(ApiRoutes.CommentsController.GetComment)
.Produces<CommentResponse>()
.Produces(404);

// -------------------------
// PRIVATE (Authorized)
// -------------------------

var privateGroup = group.RequireAuthorization();

// Create Comment
privateGroup.MapPost(ApiRoutes.CommentsController.CreateComment,
async (CreateCommentRequest request,
ICommentsService service,
IMapper mapper,
HttpContext context) =>
{
var comment = mapper.Map<Comment>(request);
comment.CreatedAt = DateTime.UtcNow;

await service.InsertAsync(comment);

var location =
$"{context.Request.Scheme}://{context.Request.Host}" +
$"{ApiRoutes.CommentsController.Comments}/{comment.Id}";

return Results.Created(location, new CreatedResponse<int>
{
Id = comment.Id
});
})
.Produces<CreatedResponse<int>>(201)
.Produces(400);


// Edit Comment
privateGroup.MapPut(ApiRoutes.CommentsController.EditComment,
async (int id,
UpdateCommentRequest request,
ICommentsService service,
IMapper mapper) =>
{
var origin = await service.GetCommentAsync(id);

if (origin is null || !origin.UserId.Equals(request.UserId))
return Results.NotFound();

request.CreatedAt = DateTime.UtcNow;

var updated = mapper.Map(request, origin);

await service.UpdateAsync(updated);

var refreshed = await service.GetCommentAsync(id);

return Results.Ok(mapper.Map<CommentResponse>(refreshed));
})
.Produces<CommentResponse>()
.Produces(404);


// Delete Comment
privateGroup.MapDelete(ApiRoutes.CommentsController.DeleteComment,
async (int id,
ICommentsService service) =>
{
var comment = await service.GetCommentAsync(id);

if (comment is null)
return Results.NotFound();

await service.DeleteAsync(comment);

return Results.Ok(new CreatedResponse<int> { Id = id });
})
.Produces<CreatedResponse<int>>()
.Produces(404);
}
}
Loading
Loading