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
2 changes: 1 addition & 1 deletion src/Application/Common/Models/UserInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Application.Common.Models;

/// <summary>
/// User information for authentication responses
/// Shared across Login, LoginExternal, Register, and other auth responses
/// Shared across Login, ExternalLogin, Register, and other auth responses
/// </summary>
public sealed record UserInfo
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
using Domain.Common;
using Domain.Constants;

namespace Application.Features.Auth.LoginExternal;
namespace Application.Features.Auth.ExternalLogin;

/// <summary>
/// Command for external provider login (Google, Facebook, etc.)
/// </summary>
public sealed record LoginExternalCommand : ICommand<Result<LoginExternalResponse>>
public sealed record ExternalLoginCommand : ICommand<Result<ExternalLoginResponse>>
{
/// <summary>
/// External provider type (Google, Facebook, GitHub, etc.)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
using TblUser = Domain.Entities.User;
using TblUserExternalLogin = Domain.Entities.UserExternalLogin;

namespace Application.Features.Auth.LoginExternal;
namespace Application.Features.Auth.ExternalLogin;

/// <summary>
/// Handler for external provider login (Google, Facebook, etc.)
Expand All @@ -19,15 +19,15 @@ namespace Application.Features.Auth.LoginExternal;
/// 2. Existing user with external login - just logs in
/// 3. Existing user registered via email - links external login to existing account
/// </summary>
public sealed class LoginExternalCommandHandler(
public sealed class ExternalLoginCommandHandler(
IUserRepository userRepository,
IAuthService authService,
ITokenService tokenService,
IRefreshTokenRepository refreshTokenRepository,
IExternalLoginRepository externalLoginRepository,
IUnitOfWork unitOfWork) : ICommandHandler<LoginExternalCommand, Result<LoginExternalResponse>>
IUnitOfWork unitOfWork) : ICommandHandler<ExternalLoginCommand, Result<ExternalLoginResponse>>
{
public async Task<Result<LoginExternalResponse>> Handle(LoginExternalCommand request, CancellationToken cancellationToken)
public async Task<Result<ExternalLoginResponse>> Handle(ExternalLoginCommand request, CancellationToken cancellationToken)
{
// Step 1: Check if this external login already exists
var existingExternalLogin = await externalLoginRepository.GetByProviderAsync(
Expand All @@ -42,13 +42,13 @@ public async Task<Result<LoginExternalResponse>> Handle(LoginExternalCommand req

if (existingUser == null)
{
return Result.Failure<LoginExternalResponse>(
return Result.Failure<ExternalLoginResponse>(
Error.NotFound("User.NotFound", "User account not found"));
}

if (existingUser.IsDeleted)
{
return Result.Failure<LoginExternalResponse>(
return Result.Failure<ExternalLoginResponse>(
Error.Failure("User.Deactivated", "User account has been deactivated"));
}

Expand Down Expand Up @@ -90,7 +90,7 @@ public async Task<Result<LoginExternalResponse>> Handle(LoginExternalCommand req

if (userByEmail.IsDeleted)
{
return Result.Failure<LoginExternalResponse>(
return Result.Failure<ExternalLoginResponse>(
Error.Failure("User.Deactivated", "User account has been deactivated"));
}

Expand Down Expand Up @@ -150,7 +150,7 @@ public async Task<Result<LoginExternalResponse>> Handle(LoginExternalCommand req

if (!identityCreated)
{
return Result.Failure<LoginExternalResponse>(
return Result.Failure<ExternalLoginResponse>(
Error.Failure("User.IdentityFailed", "Failed to create Identity user"));
}

Expand All @@ -177,7 +177,7 @@ public async Task<Result<LoginExternalResponse>> Handle(LoginExternalCommand req
/// <summary>
/// Helper method to generate token response
/// </summary>
private async Task<Result<LoginExternalResponse>> GenerateTokenResponse(
private async Task<Result<ExternalLoginResponse>> GenerateTokenResponse(
TblUser user,
string provider,
bool isNewUser,
Expand Down Expand Up @@ -213,7 +213,7 @@ private async Task<Result<LoginExternalResponse>> GenerateTokenResponse(
);
await refreshTokenRepository.AddAsync(refreshTokenEntity, cancellationToken);

var response = new LoginExternalResponse
var response = new ExternalLoginResponse
{
AccessToken = accessToken,
RefreshToken = refreshToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
using FluentValidation;

namespace Application.Features.Auth.LoginExternal;
namespace Application.Features.Auth.ExternalLogin;

/// <summary>
/// Validator for external login command
/// </summary>
public sealed class LoginExternalCommandValidator : AbstractValidator<LoginExternalCommand>
public sealed class ExternalLoginCommandValidator : AbstractValidator<ExternalLoginCommand>
{
public LoginExternalCommandValidator()
public ExternalLoginCommandValidator()
{
RuleFor(x => x.Provider)
.IsInEnum()
Expand Down
32 changes: 32 additions & 0 deletions src/Application/Features/Auth/ExternalLogin/ExternalLoginInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace Application.Features.Auth.ExternalLogin;

/// <summary>
/// External login information
/// </summary>
public sealed record ExternalLoginInfo
{
/// <summary>
/// External provider (Google, Facebook, etc.)
/// </summary>
public required string Provider { get; init; }

/// <summary>
/// Provider-specific user identifier
/// </summary>
public required string ProviderKey { get; init; }

/// <summary>
/// Display name from provider
/// </summary>
public string? DisplayName { get; init; }

/// <summary>
/// Avatar URL from provider
/// </summary>
public string? AvatarUrl { get; init; }

/// <summary>
/// When this external login was linked
/// </summary>
public required DateTimeOffset CreatedAt { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using Application.Common.Models;

namespace Application.Features.Auth.LoginExternal;
namespace Application.Features.Auth.ExternalLogin;

/// <summary>
/// External login response
/// </summary>
public sealed record LoginExternalResponse
public sealed record ExternalLoginResponse
{
/// <summary>
/// Access token
Expand Down
25 changes: 14 additions & 11 deletions src/Application/Features/Auth/Logout/LogoutCommandHandler.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
using Application.Common;
using Application.Interfaces.Repositories;
using Domain.Common;

namespace Application.Features.Auth.Logout;

/// <summary>
/// Logout command handler
/// </summary>
public sealed class LogoutCommandHandler : ICommandHandler<LogoutCommand, Result>
public sealed class LogoutCommandHandler(
IRefreshTokenRepository refreshTokenRepository)
: ICommandHandler<LogoutCommand, Result>
{
public LogoutCommandHandler()
{
// TODO: Inject dependencies (ITokenService, IRefreshTokenRepository, etc.)
}

public async Task<Result> Handle(LogoutCommand request, CancellationToken cancellationToken)
{
// TODO: Implement logout logic
// 1. Invalidate refresh token
// 2. Add access token to blacklist (optional)
// Find the refresh token
var storedToken = await refreshTokenRepository.GetByTokenAsync(request.RefreshToken, cancellationToken);

if (storedToken == null || storedToken.RevokedAt.HasValue || storedToken.ExpiresAt < DateTimeOffset.UtcNow)
{
return Result.Failure(Error.Unauthorized("Auth.InvalidRefreshToken", "Invalid refresh token."));
}

await Task.Delay(1, cancellationToken);
throw new NotImplementedException("Logout logic not implemented yet");
// Revoke the refresh token
await refreshTokenRepository.RevokeAsync(request.RefreshToken, "User logged out", cancellationToken);
return Result.Success();
}
}
12 changes: 12 additions & 0 deletions src/Application/Features/User/GetProfile/GetProfileQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Application.Common;
using Application.Features.Auth.ExternalLogin;
using Domain.Common;

namespace Application.Features.User.GetProfile;

/// <summary>
/// Get current user profile query
/// </summary>
public sealed record GetProfileQuery : IQuery<Result<GetUserProfileResponse>>
{
}
60 changes: 60 additions & 0 deletions src/Application/Features/User/GetProfile/GetProfileQueryHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Application.Common;
using Application.Features.Auth.ExternalLogin;
using Application.Interfaces.Repositories;
using Application.Interfaces.Services.Auth;
using Domain.Common;
using Domain.Entities;

namespace Application.Features.User.GetProfile;

/// <summary>
/// Get profile query handler
/// </summary>
public sealed class GetProfileQueryHandler(
ICurrentUserService currentUserService,
IUserRepository userRepository,
IExternalLoginRepository externalLoginRepository)
: IQueryHandler<GetProfileQuery, Result<GetUserProfileResponse>>
{
public async Task<Result<GetUserProfileResponse>> Handle(GetProfileQuery request, CancellationToken cancellationToken)
{
var userId = currentUserService.UserId;
if (userId is null)
{
return Result.Failure<GetUserProfileResponse>(
Error.Unauthorized("User.Unauthenticated", "User is not authenticated"));
}

var user = await userRepository.GetByIdAsync(userId.Value, cancellationToken);
if (user is null)
{
return Result.Failure<GetUserProfileResponse>(
Error.NotFound("User.NotFound", "User not found"));
}

var externalLogins = await externalLoginRepository.GetByUserIdAsync(userId.Value, cancellationToken);

var response = new GetUserProfileResponse
{
Id = user.Id,
Email = user.Email,
FullName = user.FullName,
AvatarUrl = user.AvatarUrl,
Roles = [.. user.Roles],
CreatedAt = user.CreatedAt,
UpdatedAt = user.UpdatedAt,
ExternalLogins = [.. externalLogins.Select(el => new ExternalLoginInfo
{
Provider = el.Provider.ToString(),
ProviderKey = el.ProviderKey,
DisplayName = el.FirstName != null && el.LastName != null
? $"{el.FirstName} {el.LastName}"
: el.FirstName ?? el.LastName,
AvatarUrl = el.AvatarUrl,
CreatedAt = el.CreatedAt
})]
};

return Result.Success(response);
}
}
46 changes: 46 additions & 0 deletions src/Application/Features/User/GetProfile/GetUserProfileResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Application.Features.Auth.ExternalLogin;

namespace Application.Features.User.GetProfile;

public class GetUserProfileResponse
{
/// <summary>
/// User ID
/// </summary>
public required Guid Id { get; init; }

/// <summary>
/// Email address
/// </summary>
public required string Email { get; init; }

/// <summary>
/// Full name
/// </summary>
public required string FullName { get; init; }

/// <summary>
/// Avatar URL
/// </summary>
public string? AvatarUrl { get; init; }

/// <summary>
/// User roles
/// </summary>
public required List<string> Roles { get; init; }

/// <summary>
/// Account creation timestamp
/// </summary>
public required DateTimeOffset CreatedAt { get; init; }

/// <summary>
/// Last update timestamp
/// </summary>
public DateTimeOffset? UpdatedAt { get; init; }

/// <summary>
/// Linked external login accounts
/// </summary>
public required List<ExternalLoginInfo> ExternalLogins { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Application.Common;
using Domain.Common;

namespace Application.Features.User.UpdateProfile;

/// <summary>
/// Update user profile command
/// </summary>
public sealed record UpdateProfileCommand : ICommand<Result<UpdateProfileResponse>>
{
/// <summary>
/// Full name
/// </summary>
public string? FullName { get; init; }

/// <summary>
/// Avatar URL
/// </summary>
public string? AvatarUrl { get; init; }
}
Loading