Skip to content
This repository has been archived by the owner on Jul 9, 2024. It is now read-only.

Commit

Permalink
Implementing user sign-in.
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 committed Apr 27, 2024
1 parent e82b38a commit e0441c2
Show file tree
Hide file tree
Showing 23 changed files with 490 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Logitar.Master.Contracts.Accounts;
using MediatR;

namespace Logitar.Master.Application.Accounts.Commands;

public record SignInCommand(SignInPayload Payload) : IRequest<SignInCommandResult>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using Logitar.Master.Contracts.Accounts;
using Logitar.Portal.Contracts.Messages;
using Logitar.Portal.Contracts.Tokens;
using Logitar.Portal.Contracts.Users;
using MediatR;

namespace Logitar.Master.Application.Accounts.Commands;

internal class SignInCommandHandler : IRequestHandler<SignInCommand, SignInCommandResult>
{
private const string AuthenticationTokenType = "auth+jwt";
private const string PasswordlessTemplate = "AccountAuthentication";

private readonly IMessageService _messageService;
private readonly ITokenService _tokenService;
private readonly IUserService _userService;

public SignInCommandHandler(IMessageService messageService, ITokenService tokenService, IUserService userService)
{
_messageService = messageService;
_tokenService = tokenService;
_userService = userService;
}

public async Task<SignInCommandResult> Handle(SignInCommand command, CancellationToken cancellationToken)
{
SignInPayload payload = command.Payload;
// TODO(fpion): validation payload

if (payload.Credentials != null)
{
return await HandleCredentialsAsync(payload.Credentials, payload.Locale, cancellationToken);
}

throw new InvalidOperationException($"The {nameof(SignInPayload)} is not valid.");
}

private async Task<SignInCommandResult> HandleCredentialsAsync(Credentials credentials, string locale, CancellationToken cancellationToken)
{
User? user = await _userService.FindAsync(credentials.EmailAddress, cancellationToken);
if (user == null || !user.HasPassword)
{
Email email = user?.Email ?? new(credentials.EmailAddress);
CreatedToken token = await _tokenService.CreateAsync(user?.GetSubject(), email, AuthenticationTokenType, cancellationToken);
Dictionary<string, string> variables = new()
{
["Token"] = token.Token
};
SentMessages sentMessages = user == null
? await _messageService.SendAsync(PasswordlessTemplate, email, locale, variables, cancellationToken)
: await _messageService.SendAsync(PasswordlessTemplate, user, locale, variables, cancellationToken);
SentMessage sentMessage = sentMessages.ToSentMessage(email);
return SignInCommandResult.AuthenticationLinkSent(sentMessage);
}
else if (credentials.Password == null)
{
return SignInCommandResult.RequirePassword();
}

throw new NotImplementedException(); // TODO(fpion): implement
}
}
10 changes: 10 additions & 0 deletions backend/src/Logitar.Master.Application/Accounts/IMessageService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Logitar.Portal.Contracts.Messages;
using Logitar.Portal.Contracts.Users;

namespace Logitar.Master.Application.Accounts;

public interface IMessageService
{
Task<SentMessages> SendAsync(string template, Email email, string locale, Dictionary<string, string> variables, CancellationToken cancellationToken = default);
Task<SentMessages> SendAsync(string template, User user, string locale, Dictionary<string, string> variables, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Logitar.Portal.Contracts.Tokens;
using Logitar.Portal.Contracts.Users;

namespace Logitar.Master.Application.Accounts;

public interface ITokenService
{
Task<CreatedToken> CreateAsync(string? subject, Email email, string type, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Logitar.Portal.Contracts.Users;

namespace Logitar.Master.Application.Accounts;

public interface IUserService
{
Task<User?> FindAsync(string emailAddress, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Logitar.Master.Contracts.Accounts;
using Logitar.Portal.Contracts.Messages;
using Logitar.Portal.Contracts.Users;
using System.Text;

namespace Logitar.Master.Application.Accounts;

internal static class MessageExtensions
{
private const string Base12Table = "2345679ACDEF";
private const string Base32Table = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";

public static string GenerateConfirmationNumber(this SentMessages sentMessages)
{
if (sentMessages.Ids.Count == 0)
{
throw new ArgumentException("No message has been sent.", nameof(sentMessages));
}
else if (sentMessages.Ids.Count > 1)
{
throw new ArgumentException("More than one message have been sent.", nameof(sentMessages));
}

StringBuilder number = new();

string id = Convert.ToBase64String(sentMessages.Ids.Single().ToByteArray());
int total = 0;
for (int i = 0; i <= 2; i++)
{
total += (int)Math.Pow(64, 2 - i) * GetBase64Value(id[i]);
}
for (int i = 3; i >= 0; i--)
{
int divider = (int)Math.Pow(32, i);
int value = total / divider;
total %= divider;
number.Append(GetBase32Character(value));
}
DateTime now = DateTime.UtcNow;
number.Append('-').Append((now.Year % 100).ToString("D2")).Append(now.Month.ToString("D2")).Append(now.Day.ToString("D2")).Append('-');

int minuteRange = ((now.Hour * 60) + now.Minute) / 10;
number.Append(GetBase12Character(minuteRange / 12));
number.Append(GetBase12Character(minuteRange % 12));

return number.ToString();
}
private static char GetBase12Character(int value) => Base12Table[value];
private static char GetBase32Character(int value) => Base32Table[value];
private static int GetBase64Value(char c)
{
if (c >= 'A' && c <= 'Z')
{
return c - 'A';
}
else if (c >= 'a' && c <= 'z')
{
return c - 'a' + 26;
}
else if (c >= '0' && c <= '9')
{
return c - '0' + 52;
}
else if (c == '+')
{
return 62;
}
return 63;
}

public static SentMessage ToSentMessage(this SentMessages sentMessages, Email email)
{
return new SentMessage(sentMessages.GenerateConfirmationNumber(), ContactType.Email, email.Address);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Logitar.Portal.Contracts.Users;

namespace Logitar.Master.Application.Accounts;

internal static class UserExtensions
{
public static string GetSubject(this User user) => user.Id.ToString();
}
7 changes: 7 additions & 0 deletions backend/src/Logitar.Master.Contracts/Accounts/ContactType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Logitar.Master.Contracts.Accounts;

public enum ContactType
{
Email = 0,
Phone = 1
}
17 changes: 17 additions & 0 deletions backend/src/Logitar.Master.Contracts/Accounts/Credentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Logitar.Master.Contracts.Accounts;

public record Credentials
{
public string EmailAddress { get; set; }
public string? Password { get; set; }

public Credentials() : this(string.Empty)
{
}

public Credentials(string emailAddress, string? password = null)
{
EmailAddress = emailAddress;
Password = password;
}
}
23 changes: 23 additions & 0 deletions backend/src/Logitar.Master.Contracts/Accounts/SentMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Logitar.Master.Contracts.Accounts;

public record SentMessage
{
public string ConfirmationNumber { get; set; }
public ContactType ContactType { get; set; }
public string MaskedContact { get; set; }

public SentMessage() : this(string.Empty, string.Empty)
{
}

public SentMessage(string confirmationNumber, string maskedContact) : this(confirmationNumber, default, maskedContact)
{
}

public SentMessage(string confirmationNumber, ContactType contactType, string maskedContact)
{
ConfirmationNumber = confirmationNumber;
ContactType = contactType;
MaskedContact = maskedContact;
}
}
17 changes: 17 additions & 0 deletions backend/src/Logitar.Master.Contracts/Accounts/SignInPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Logitar.Master.Contracts.Accounts;

public record SignInPayload
{
public string Locale { get; set; }

public Credentials? Credentials { get; set; }

public SignInPayload() : this(string.Empty)
{
}

public SignInPayload(string locale)
{
Locale = locale;
}
}
29 changes: 29 additions & 0 deletions backend/src/Logitar.Master.Contracts/Accounts/SignInResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Logitar.Portal.Contracts.Sessions;

namespace Logitar.Master.Contracts.Accounts;

public record SignInCommandResult
{
public SentMessage? AuthenticationLinkSentTo { get; set; }
public bool IsPasswordRequired { get; set; }
public Session? Session { get; set; }

public SignInCommandResult()
{
}

public static SignInCommandResult AuthenticationLinkSent(SentMessage sentMessage) => new()
{
AuthenticationLinkSentTo = sentMessage
};

public static SignInCommandResult RequirePassword() => new()
{
IsPasswordRequired = true
};

public static SignInCommandResult Succeed(Session session) => new()
{
Session = session
};
}
26 changes: 26 additions & 0 deletions backend/src/Logitar.Master/Authentication/BearerTokenService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Logitar.Master.Models.Account;
using Logitar.Master.Settings;
using Logitar.Portal.Contracts.Sessions;

namespace Logitar.Master.Authentication;

internal class BearerTokenService : IBearerTokenService
{
private readonly BearerTokenSettings _settings;

public BearerTokenService(BearerTokenSettings settings)
{
_settings = settings;
}

public TokenResponse GetTokenResponse(Session session)
{
string accessToken = ""; // TODO(fpion): implement

return new TokenResponse(accessToken, _settings.TokenType)
{
ExpiresIn = _settings.LifetimeSeconds,
RefreshToken = session.RefreshToken
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Logitar.Master.Models.Account;
using Logitar.Portal.Contracts.Sessions;

namespace Logitar.Master.Authentication;

public interface IBearerTokenService
{
TokenResponse GetTokenResponse(Session session);
}
51 changes: 51 additions & 0 deletions backend/src/Logitar.Master/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Logitar.Master.Application.Accounts.Commands;
using Logitar.Master.Authentication;
using Logitar.Master.Contracts.Accounts;
using Logitar.Master.Models.Account;
using MediatR;
using Microsoft.AspNetCore.Mvc;

namespace Logitar.Master.Controllers;

[ApiController]
public class AccountController : ControllerBase
{
private readonly IBearerTokenService _bearerTokenService;
private readonly ISender _sender;

public AccountController(IBearerTokenService bearerTokenService, ISender sender)
{
_bearerTokenService = bearerTokenService;
_sender = sender;
}

[HttpPost("/auth/sign/in")]
public async Task<ActionResult<SignInResponse>> SignInAsync([FromBody] SignInPayload payload, CancellationToken cancellationToken)
{
SignInCommandResult result = await _sender.Send(new SignInCommand(payload), cancellationToken);
if (result.Session != null)
{
// TODO(fpion): SignIn
}

return Ok(new SignInResponse(result));
}

[HttpPost("/auth/token")]
public async Task<ActionResult<GetTokenResponse>> TokenAsync([FromBody] GetTokenPayload payload, CancellationToken cancellationToken)
{
if (!string.IsNullOrWhiteSpace(payload.RefreshToken))
{
throw new NotImplementedException(); // TODO(fpion): renew session
}

SignInCommandResult result = await _sender.Send(new SignInCommand(payload), cancellationToken);
GetTokenResponse response = new(result);
if (result.Session != null)
{
response.TokenResponse = _bearerTokenService.GetTokenResponse(result.Session);
}

return Ok(response);
}
}
25 changes: 25 additions & 0 deletions backend/src/Logitar.Master/Models/Account/CurrentUser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Logitar.Portal.Contracts.Users;

namespace Logitar.Master.Models.Account;

public record CurrentUser
{
public string DisplayName { get; set; }
public string? EmailAddress { get; set; }
public string? PictureUrl { get; set; }

public CurrentUser() : this(string.Empty)
{
}

public CurrentUser(string displayName)
{
DisplayName = displayName;
}

public CurrentUser(User user) : this(user.FullName ?? user.UniqueName)
{
EmailAddress = user.Email?.Address;
PictureUrl = user.Picture;
}
}
Loading

0 comments on commit e0441c2

Please sign in to comment.