Skip to content

Commit

Permalink
commiting
Browse files Browse the repository at this point in the history
  • Loading branch information
pc authored and pc committed Jul 1, 2024
1 parent 04b1bea commit 530389e
Show file tree
Hide file tree
Showing 59 changed files with 2,573 additions and 57 deletions.
3 changes: 3 additions & 0 deletions DougShared/Dtos/AuthResponseDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace DoughnutMaui.Shared.Dtos;

public record AuthResponseDto(LoggedInUser User, string Token);
2 changes: 2 additions & 0 deletions DougShared/Dtos/ChangePasswordDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
namespace DoughnutMaui.Shared.Dtos;
public record ChangePasswordDto(string OldPassword, string NewPassword);
5 changes: 5 additions & 0 deletions DougShared/Dtos/DougDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace DoughnutMaui.Shared.Dtos;
public record struct DougOptionDto(string Flavor, string Topping);

public record DougDto(int Id, string Name, string Image, double Price, DougOptionDto[] Options);

3 changes: 3 additions & 0 deletions DougShared/Dtos/LoggedInUser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace DoughnutMaui.Shared.Dtos;

public record LoggedInUser(Guid Id, string Name, string Email, string Address);
15 changes: 15 additions & 0 deletions DougShared/Dtos/OrderDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace DoughnutMaui.Shared.Dtos;

public record OrderItemDto(long Id, int IcecreamId, string Name, int Quantity, double Price, string Flavor, string Topping)
{
public double TotalPrice => Quantity * Price;
}
public record OrderDto(long Id, DateTime OrderdAt, double TotalPrice, int ItemsCount = 0)
{
public string ItemCountDisplay => ItemsCount + (ItemsCount > 1 ? " Items" : " Item");
}

public record OrderPlaceDto(OrderDto Order, OrderItemDto[] Items);



7 changes: 7 additions & 0 deletions DougShared/Dtos/ResultDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace DoughnutMaui.Shared.Dtos;

public record ResultDto(bool IsSuccess, string? ErrorMessage)
{
public static ResultDto Success() => new(true, null);
public static ResultDto Failure(string? errorMessage) => new(false, errorMessage);
}
7 changes: 7 additions & 0 deletions DougShared/Dtos/ResultWithDataDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace DoughnutMaui.Shared.Dtos;

public record ResultWithDataDto<TData>(bool IsSuccess, TData Data, string? ErrorMessage)
{
public static ResultWithDataDto<TData> Success(TData data) => new(true, data, null);
public static ResultWithDataDto<TData> Failure(string? errorMessage) => new(false, default, errorMessage);
}
3 changes: 3 additions & 0 deletions DougShared/Dtos/SigninRequestDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace DoughnutMaui.Shared.Dtos;

public record SigninRequestDto(string Email, string Password);
2 changes: 2 additions & 0 deletions DougShared/Dtos/SignupRequestDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
namespace DoughnutMaui.Shared.Dtos;
public record SignupRequestDto(string Name, string Email, string Password, string Address);
48 changes: 48 additions & 0 deletions DoughnutApi/Endpoints/Endpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using DoughnutMaui.Api.Services;
using DoughnutMaui.Shared.Dtos;
using System.Security.Claims;

namespace DoughnutMaui.Api.Endpoints;

public static class Endpoints
{
private static Guid GetUserId(this ClaimsPrincipal principal) =>
Guid.Parse(principal.FindFirstValue(ClaimTypes.NameIdentifier)!);

public static IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder app)
{
app.MapPost("/api/auth/signup",
async (SignupRequestDto dto, AuthService authService) =>
TypedResults.Ok(await authService.SignupAsync(dto)));

app.MapPost("/api/auth/signin",
async (SigninRequestDto dto, AuthService authService) =>
TypedResults.Ok(await authService.SigninAsync(dto)));

app.MapPost("/api/auth/change-password",
async (ChangePasswordDto dto, ClaimsPrincipal principal, AuthService authService) =>
TypedResults.Ok(await authService.ChangePasswordAsync(dto, principal.GetUserId()))
)
.RequireAuthorization();

app.MapGet("/api/icecreams",
async(DougService icecreamService) =>
TypedResults.Ok(await icecreamService.GetIcecreamsAsync()));

var orderGroup = app.MapGroup("/api/orders").RequireAuthorization();

orderGroup.MapPost("/place-order",
async (OrderPlaceDto dto, ClaimsPrincipal principal, OrderService orderService) =>
await orderService.PlaceOrderAsync(dto, principal.GetUserId()));

orderGroup.MapGet("",
async (ClaimsPrincipal principal, OrderService orderService) =>
TypedResults.Ok(await orderService.GetUserOrdersAsync(principal.GetUserId())));

orderGroup.MapGet("/{orderId:long}/items",
async (long orderId, ClaimsPrincipal principal, OrderService orderService) =>
TypedResults.Ok(await orderService.GetUserOrderItemsAsync(orderId, principal.GetUserId())));

return app;
}
}
82 changes: 82 additions & 0 deletions DoughnutApi/Services/AuthService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using DoughnutMaui.Api.Data;
using DoughnutMaui.Api.Data.Entities;
using DoughnutMaui.Shared.Dtos;
using Microsoft.EntityFrameworkCore;

namespace DoughnutMaui.Api.Services;

public class AuthService(DataContext context, TokenService tokenService, PasswordService passwordService)
{
private readonly DataContext _context = context;
private readonly TokenService _tokenService = tokenService;
private readonly PasswordService _passwordService = passwordService;

public async Task<ResultWithDataDto<AuthResponseDto>> SignupAsync(SignupRequestDto dto)
{
if(await _context.Users.AsNoTracking().AnyAsync(u=> u.Email == dto.Email))
{
return ResultWithDataDto<AuthResponseDto>.Failure("Email alredy exists");
}

var user = new User {
Email = dto.Email,
Address = dto.Address,
Name = dto.Name,
};

(user.Salt, user.Hash) = _passwordService.GenerateSaltAndHash(dto.Password);

try
{
await _context.Users.AddAsync(user);
await _context.SaveChangesAsync();
return GenerateAuthResponse(user);
}
catch (Exception ex)
{
return ResultWithDataDto<AuthResponseDto>.Failure(ex.Message);
}
}


public async Task<ResultWithDataDto<AuthResponseDto>> SigninAsync(SigninRequestDto dto)
{
var dbUser = await _context.Users
.AsNoTracking()
.FirstOrDefaultAsync(u=> u.Email == dto.Email);
if (dbUser is null)
return ResultWithDataDto<AuthResponseDto>.Failure("User does not exist");

if(!_passwordService.AreEqual(dto.Password, dbUser.Salt, dbUser.Hash))
return ResultWithDataDto<AuthResponseDto>.Failure("Incorrect password");

return GenerateAuthResponse(dbUser);
}

private ResultWithDataDto<AuthResponseDto> GenerateAuthResponse(User user)
{
var loggedInUser = new LoggedInUser(user.Id, user.Name, user.Email, user.Address);
var token = _tokenService.GenerateJwt(loggedInUser);

var authResponse = new AuthResponseDto(loggedInUser, token);

return ResultWithDataDto<AuthResponseDto>.Success(authResponse);
}

public async Task<ResultDto> ChangePasswordAsync(ChangePasswordDto dto, Guid userId)
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
if (user is null)
return ResultDto.Failure("Invalid request");

if(!_passwordService.AreEqual(dto.OldPassword, user.Salt, user.Hash))
{
return ResultDto.Failure("Incorrect password");
}

(user.Salt, user.Hash) = _passwordService.GenerateSaltAndHash(dto.NewPassword);

await _context.SaveChangesAsync();
return ResultDto.Success();
}
}
25 changes: 25 additions & 0 deletions DoughnutApi/Services/DougService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using DoughnutMaui.Api.Data;
using DoughnutMaui.Shared.Dtos;
using Microsoft.EntityFrameworkCore;

namespace DoughnutMaui.Api.Services;

public class DougService(DataContext context)
{
private readonly DataContext _context = context;

public async Task<DougDto[]> GetIcecreamsAsync() =>
await _context.Icecreams.AsNoTracking()
.Select(i=>
new DougDto(
i.Id,
i.Name,
i.Image,
i.Price,
i.Options
.Select(o=> new DougOptionDto(o.Sprinkle, o.Topping))
.ToArray()
)
)
.ToArrayAsync();
}
62 changes: 62 additions & 0 deletions DoughnutApi/Services/OrderService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using DoughnutMaui.Api.Data;
using DoughnutMaui.Api.Data.Entities;
using DoughnutMaui.Shared.Dtos;
using Microsoft.EntityFrameworkCore;

namespace DoughnutMaui.Api.Services;

public class OrderService(DataContext context)
{
private readonly DataContext _context = context;

public async Task<ResultDto> PlaceOrderAsync(OrderPlaceDto dto, Guid customerId)
{
var customer = await _context.Users.FirstOrDefaultAsync(u => u.Id == customerId);
if (customer is null)
return ResultDto.Failure("Custome does not exist");

var orderItems = dto.Items.Select(i =>
new OrderItem
{
Flavor = i.Flavor,
DougId = i.IcecreamId,
Name = i.Name,
Price = i.Price,
Quantity = i.Quantity,
Topping = i.Topping,
TotalPrice = i.TotalPrice
});

var order = new Order
{
CustomerId = customerId,
CustomerAddress = customer.Address,
CustomerEmail = customer.Email,
CustomerName = customer.Name,
OrderdAt = DateTime.Now,
TotalPrice = orderItems.Sum(o => o.TotalPrice),
Items = orderItems.ToArray()
};
try
{
await _context.Orders.AddAsync(order);
await _context.SaveChangesAsync();
return ResultDto.Success();
}
catch (Exception ex)
{
return ResultDto.Failure(ex.Message);
}
}

public async Task<OrderDto[]> GetUserOrdersAsync(Guid userId) =>
await _context.Orders
.Where(o => o.CustomerId == userId)
.Select(o => new OrderDto(o.Id, o.OrderdAt, o.TotalPrice, o.Items.Count))
.ToArrayAsync();
public async Task<OrderItemDto[]> GetUserOrderItemsAsync(long orderId, Guid userId) =>
await _context.OrderItems
.Where(i=> i.OrderId == orderId && i.Order.CustomerId == userId)
.Select(i=> new OrderItemDto(i.Id, i.DougId, i.Name, i.Quantity, i.Price, i.Flavor, i.Topping))
.ToArrayAsync();
}
34 changes: 34 additions & 0 deletions DoughnutApi/Services/PasswordService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Security.Cryptography;
using System.Text;

namespace DoughnutMaui.Api.Services;

public class PasswordService
{
private const int SaltSize = 10;
public (string salt, string hashedPassword) GenerateSaltAndHash(string plainPassword)
{
if(string.IsNullOrWhiteSpace(plainPassword))
throw new ArgumentNullException(nameof(plainPassword));

var buffer = RandomNumberGenerator.GetBytes(SaltSize);
var salt = Convert.ToBase64String(buffer);

var hashedPassword = GenerateHashedPassword(plainPassword, salt);

return (salt, hashedPassword);
}

public bool AreEqual(string plainPassword, string salt, string hashedPassword)
{
var newHashedPassword = GenerateHashedPassword(plainPassword, salt);
return newHashedPassword == hashedPassword;
}

private static string GenerateHashedPassword(string plainPassword, string salt) {
var bytes = Encoding.UTF8.GetBytes(plainPassword + salt);
var hash = SHA256.HashData(bytes);

return Convert.ToBase64String(hash);
}
}
57 changes: 57 additions & 0 deletions DoughnutApi/Services/TokenService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using DoughnutMaui.Shared.Dtos;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace DoughnutMaui.Api.Services;

public class TokenService(IConfiguration configuration)
{
private readonly IConfiguration _configuration = configuration;

public static TokenValidationParameters GetTokenValidationParameters(IConfiguration configuration) =>
new()
{
ValidateAudience = false,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["Jwt:Issuer"],
IssuerSigningKey = GetSecurityKey(configuration),
};

public string GenerateJwt(LoggedInUser user)
{
var securityKey = GetSecurityKey(_configuration);
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

var issuer = _configuration["Jwt:Issuer"];
var expireInMinutes = Convert.ToInt32(_configuration["Jwt:ExpireInMinute"]);

Claim[] claims = [
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.StreetAddress, user.Address),
];

var token = new JwtSecurityToken(
issuer: issuer,
audience: "*",
claims: claims,
expires: DateTime.Now.AddMinutes(expireInMinutes),
signingCredentials: credentials);

var jwt = new JwtSecurityTokenHandler().WriteToken(token);

return jwt;
}

private static SymmetricSecurityKey GetSecurityKey(IConfiguration configuration)
{
var secretKey = configuration["Jwt:SecretKey"];
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey!));
return securityKey;
}
}
Loading

0 comments on commit 530389e

Please sign in to comment.