dariomrk / edulink Goto Github PK
View Code? Open in Web Editor NEW2023 Internship Cup project
2023 Internship Cup project
{
public static void Main(string[] args)
{
WebApplication
.CreateBuilder(args)
.ConfigureHost()
.RegisterApplicationServices()
.Build()
.ConfigureMiddleware()
.Run();
}
}
public static class HostInitializer
{
public static WebApplicationBuilder ConfigureHost(this WebApplicationBuilder builder)
{
var host = builder.Host;
// TODO: Configure host properties
return builder;
}
}
public static class ServiceInitializer
{
public static WebApplicationBuilder RegisterApplicationServices(this WebApplicationBuilder builder)
{
var services = builder.Services;
// TODO: Configure services
#region Controller registration
services.AddControllers()
.AddJsonOptions(options =>
{
// endpoints recieve and send enum string representations
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
#endregion
#region Service registration
// services go here
#endregion
#region Repository registration
// repositories go here
#endregion
var connectionString = ConfigurationHelper
.GetConfiguration()
.GetConnectionString("Database");
services.AddDbContext<EduLinkDbContext>(options =>
options.UseNpgsql(connectionString, options =>
options.UseNetTopologySuite()));
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
return builder;
}
}
public static class MiddlewareInitializer
{
public static WebApplication ConfigureMiddleware(this WebApplication app)
{
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
Blocked by:
On branch: backend/feat/24-user-controller
Endpoints.cs
to Api
User
routes to Endpoints.cs
as per API specUsersController.cs
to Api/Controllers
using Application.Dtos.Indentity;
using Application.Dtos.User;
using Application.Enums;
using Application.Extensions;
using Application.Interfaces;
using Data.Interfaces;
using Data.Models;
using FluentValidation;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace Application.Services
{
public class IdentityService : IIdentityService
{
private readonly IConfiguration _configuration;
private readonly IRepository<User, long> _userRepository;
private readonly IUserService _userService;
private readonly IValidator<RegisterRequestDto> _registerRequestValidator;
private readonly IValidator<LoginRequestDto> _loginRequestValidator;
private readonly IPasswordService _passwordService;
private readonly ILocationService _locationService;
private readonly ILogger<IdentityService> _logger;
public IdentityService(
IConfiguration configuration,
IRepository<User, long> userRepository,
IUserService userService,
IValidator<RegisterRequestDto> registerRequestValidator,
IValidator<LoginRequestDto> loginRequestValidator,
IPasswordService passwordService,
ILocationService locationService,
ILogger<IdentityService> logger)
{
_configuration = configuration;
_userRepository = userRepository;
_userService = userService;
_registerRequestValidator = registerRequestValidator;
_loginRequestValidator = loginRequestValidator;
_passwordService = passwordService;
_locationService = locationService;
_logger = logger;
}
public async Task<(IdentityActionResult Result, TokenResponseDto Token)> LoginAsync(LoginRequestDto loginDto)
{
throw new NotImplementedException();
}
public async Task<(ServiceActionResult Result, UserResponseDto? Created, TokenResponseDto? Token)> RegisterAsync(RegisterRequestDto registerDto)
{
await _registerRequestValidator.ValidateAndThrowAsync(registerDto);
var iterations = _configuration.GetValue<int>("Security:PasswordHashIterations");
var hashLenght = _configuration.GetValue<int>("Security:HashLengthBytes");
var saltLength = _configuration.GetValue<int>("Security:SaltLengthBytes");
var (passwordHash, salt) = _passwordService.GeneratePassword(
registerDto.Password,
iterations,
hashLenght,
saltLength);
var city = await _locationService.FindCity(
registerDto.CountryName,
registerDto.RegionName,
registerDto.CityName);
var user = new User
{
FirstName = registerDto.FirstName,
LastName = registerDto.LastName,
Email = registerDto.Email.ToNormalizedLower(),
Username = registerDto.Username.ToNormalizedLower(),
PasswordHash = passwordHash,
Salt = salt,
DateOfBirth = registerDto.DateOfBirth,
CityId = city.Id,
MobileNumber = registerDto.MobileNumber,
// TODO: Generate Stripe related information during registration
};
var (result, created) = await _userRepository.CreateAsync(user);
if (result is not Data.Enums.RepositoryActionResult.Success)
return (ServiceActionResult.Failed, null, null);
return (
ServiceActionResult.Created,
created!.ToDto(),
new TokenResponseDto
{
Jwt = GenerateUserJwt(
created!.Id,
created!.Username,
created!.Email)
});
}
public string GenerateUserJwt(
long userId,
string username,
string email)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_configuration.GetValue<string>("Identity:TokenSecret")!);
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
new(JwtRegisteredClaimNames.Email, email),
new(JwtRegisteredClaimNames.NameId, username),
};
var tokenDescriptior = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(_configuration.GetValue<int>("Identity:JwtLifetimeMinutes")),
Issuer = _configuration.GetValue<string>(_configuration.GetValue<string>("Identity:Issuer")!),
Audience = _configuration.GetValue<string>(_configuration.GetValue<string>("Identity:Audience")!),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptior);
var jwt = tokenHandler.WriteToken(token);
return jwt;
}
// TODO: Implement Refresh Token generation and consumption
}
}
package-lock.json
from repository rootappsettings.json
for setting the appointment cancellation limit (hours) public async Task<bool> IsAppointmentCancelableAsync(
long id,
CancellationToken cancellationToken = default)
{
// TODO: Implement configuration for IsAppointmentCancelableAsync
// - Add field to `appsettings.json` for setting the appointment cancellation limit (hours)
return await _appointmentRepository.Query()
.AnyAsync(appointment => appointment.Id == id
&& appointment.AppointmentTimeFrame.Start.AddHours(-24)
> DateTime.UtcNow.Add(appointment.AppointmentTimeFrame.Start.Offset),
cancellationToken);
}
public async Task<bool> HasAppointmentPassedAsync(
long id,
CancellationToken cancellationToken = default)
{
return await _appointmentRepository.Query()
.AnyAsync(appointment => appointment.Id == id
&& appointment.AppointmentTimeFrame.End < DateTime.UtcNow.Add(appointment.AppointmentTimeFrame.Start.Offset),
cancellationToken);
}
}
using Application.Dtos.Common;
using Application.Dtos.TutoringPost;
using Application.Enums;
using Application.Exceptions;
using Application.Extensions;
using Application.Interfaces;
using Application.TutoringPost;
using Data.Interfaces;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Application.Services
{
public class TutoringPostService : ITutoringPostService
{
private readonly IValidator<RequestDto> _tutoringPostValidator;
private readonly IRepository<Data.Models.TutoringPost, long> _tutoringPostRepository;
private readonly IFieldService _fieldService;
private readonly IUserService _userService;
private readonly ILogger<TutoringPostService> _logger;
public TutoringPostService(
IValidator<RequestDto> tutoringPostValidator,
IRepository<Data.Models.TutoringPost, long> tutoringPostRepository,
IFieldService fieldService,
IUserService userService,
ILogger<TutoringPostService> logger)
{
_tutoringPostValidator = tutoringPostValidator;
_tutoringPostRepository = tutoringPostRepository;
_fieldService = fieldService;
_userService = userService;
_logger = logger;
}
public async Task<(ServiceActionResult Result, ResponseDto? Created)> CreateTutoringPostAsync(RequestDto creationRequestDto)
{
await _tutoringPostValidator.ValidateAndThrowAsync(creationRequestDto);
var mapped = creationRequestDto.ToModel();
var (SubjectKey, FieldKeys) = await _fieldService.GetSubjectAndFieldKeys(
creationRequestDto.SubjectName,
creationRequestDto.Fields);
FieldKeys
.ForEach(key => mapped.Fields
.Add(new Data.Models.TutoringPostField
{
FieldId = key,
}));
var tutor = await _userService.GetTutorAsync(creationRequestDto.TutorUsername);
mapped.TutorId = tutor.Id;
mapped.IsPaidAd = false; // TODO: Implement stripe integration
var creationResult = await _tutoringPostRepository.CreateAsync(mapped);
if (creationResult.Result is not Data.Enums.RepositoryActionResult.Success)
return (ServiceActionResult.Failed, null);
return (ServiceActionResult.Created, creationResult.Created!.ToDto());
}
public async Task<ResponseDto> GetTutoringPostAsync(long id, CancellationToken cancellationToken = default)
{
var tutoringPost = await _tutoringPostRepository.Query()
.Where(x => x.Id == id)
.ProjectToDto()
.FirstOrDefaultAsync(cancellationToken);
return tutoringPost ?? throw new NotFoundException<Data.Models.TutoringPost>(id);
}
public async Task<ICollection<ResponseDto>> GetTutoringPostsAsync(
string? countryName = null,
string? regionName = null,
string? cityName = null,
RequestPaginationDto? paginationOptions = null,
RequestSortDto? sortOptions = null,
CancellationToken cancellationToken = default)
{
var tutoringPosts = await _tutoringPostRepository.Query()
.Where(tutoringPost => tutoringPost.AvailableTimeSpans
.Any(timeSpan => timeSpan.Start > DateTime.UtcNow.Add(timeSpan.Start.Offset)))
.SortTutoringPosts(sortOptions ?? new RequestSortDto { SortByProperty = SortByProperty.Rating, SortOrder = SortOrder.Descending })
.Paginate(paginationOptions ?? new RequestPaginationDto { Skip = 0, Take = 25 })
.ProjectToDto()
.ToListAsync(cancellationToken);
return tutoringPosts;
}
public async Task<ICollection<ResponseDto>> GetAvailableTutoringPostsAsync(
string? countryName = null,
string? regionName = null,
string? cityName = null,
RequestPaginationDto? paginationOptions = null,
RequestSortDto? sortOptions = null,
CancellationToken cancellationToken = default)
{
var tutoringPosts = await _tutoringPostRepository.Query()
.Where(tutoringPost => tutoringPost.AvailableTimeSpans
.Any(timeSpan => timeSpan.Start > DateTime.UtcNow.Add(timeSpan.Start.Offset)))
.Where(tutoringPost => tutoringPost.AvailableTimeSpans
.Any(timeSpan => timeSpan.TakenByStudent == null))
.SortTutoringPosts(sortOptions ?? new RequestSortDto { SortByProperty = SortByProperty.Rating, SortOrder = SortOrder.Descending })
.Paginate(paginationOptions ?? new RequestPaginationDto { Skip = 0, Take = 25 })
.ProjectToDto()
.ToListAsync(cancellationToken);
return tutoringPosts;
}
}
}
Enums.SortByProperty.Name => throw new NotSupportedRequestException<Appointment>(nameof(SortAppointments), nameof(Enums.SortByProperty.Name)),
Enums.SortByProperty.Distance => throw new NotImplementedException(), // TODO: Implement sorting by distance from the user in SortAppointments
Enums.SortByProperty.Date => appointments.OrderBy(appointment => appointment.AppointmentTimeFrame.Start),
_logger = logger;
}
// TODO: Add pagination and sorting to GetAllFromCountryAsync
public async Task<ICollection<RegionResponseDto>> GetAllFromCountryAsync(
string countryName,
CancellationToken cancellationToken = default)
{
await FindCountry(countryName, cancellationToken);
var result = await _regionRepository.Query()
.Where(region => region.Country.Name == countryName.ToNormalizedLower())
.Select(region => new RegionResponseDto
{
RegionName = region.Name,
Cities = region.Cities
.Select(city => new CityResponseDto
{
CityName = city.Name,
ZipCode = city.ZipCode,
})
.ToList(),
})
.ToListAsync(cancellationToken);
return result;
}
public async Task<CountryResponseDto> FindCountry(
string countryName,
CancellationToken cancellationToken)
import React from 'react'
import {
createBrowserRouter, createRoutesFromElements, RouterProvider
} from 'react-router-dom'
const router = createBrowserRouter(
createRoutesFromElements(
<>
{/* // TODO: NotFoundPage <Route path="*" element={<NotFoundPage />} /> */}
</>
)
)
function Router () {
return (<RouterProvider router={router} />)
}
export default Router
public User Tutor { get; set; } = null!;
public DateTimeOffset CreatedAt { get; set; }
public bool IsCancelled { get; set; }
public long? AudioRecordingId { get; set; } // TODO: Implement audio recording
public File? AudioRecording { get; set; }
public long? StudentsReviewId { get; set; }
public StudentsReview? StudentsReview { get; set; }
{
public static void Main(string[] args)
{
WebApplication
.CreateBuilder(args)
.ConfigureHost()
.RegisterApplicationServices()
.Build()
.ConfigureMiddleware()
.Run();
}
}
public static class HostInitializer
{
public static WebApplicationBuilder ConfigureHost(this WebApplicationBuilder builder)
{
var host = builder.Host;
// TODO: Configure host properties
return builder;
}
}
public static class ServiceInitializer
{
public static WebApplicationBuilder RegisterApplicationServices(this WebApplicationBuilder builder)
{
var services = builder.Services;
// TODO: Configure services
#region Controller registration
services.AddControllers()
.AddJsonOptions(options =>
{
// endpoints recieve and send enum string representations
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
#endregion
#region Service registration
// services go here
#endregion
#region Repository registration
// repositories go here
#endregion
var connectionString = ConfigurationHelper
.GetConfiguration()
.GetConnectionString("Database");
services.AddDbContext<EduLinkDbContext>(options =>
options.UseNpgsql(connectionString, options =>
options.UseNetTopologySuite()));
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
return builder;
}
}
public static class MiddlewareInitializer
{
public static WebApplication ConfigureMiddleware(this WebApplication app)
{
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
using Application.Dtos.Common;
using Application.Enums;
namespace Application.Extensions
{
internal static class TutoringPostExtensions
{
internal static IQueryable<Data.Models.TutoringPost> SortTutoringPosts(this IQueryable<Data.Models.TutoringPost> tutoringPosts, RequestSortDto sortDto)
{
return sortDto.SortByProperty switch
{
SortByProperty.Rating => tutoringPosts.OrderBy(tutoringPost =>
tutoringPost.Tutor.TutoringAppointments
.Where(appointment => appointment.StudentsReview != null)
.Select(appointment => (double?)appointment.StudentsReview!.Stars)
.DefaultIfEmpty(0)
.Average()),
SortByProperty.Name => tutoringPosts.OrderBy(tutoringPost => tutoringPost.Tutor.FirstName),
SortByProperty.Distance => throw new NotImplementedException(), // TODO: Implement sorting by distance from the user in SortTutoringPosts
SortByProperty.Date => tutoringPosts.OrderBy(tutoringPost =>
tutoringPost.AvailableTimeSpans.OrderBy(timeSpan => timeSpan.Start)),
_ => throw new NotSupportedException(),
};
}
}
}
appsettings.json
for setting the appointment cancellation limit (hours) public async Task<bool> IsAppointmentCancelableAsync(
long id,
CancellationToken cancellationToken = default)
{
// TODO: Implement configuration for IsAppointmentCancelableAsync
// - Add field to `appsettings.json` for setting the appointment cancellation limit (hours)
return await _appointmentRepository.Query()
.AnyAsync(appointment => appointment.Id == id
&& appointment.AppointmentTimeFrame.Start.AddHours(-24)
> DateTime.UtcNow.Add(appointment.AppointmentTimeFrame.Start.Offset),
cancellationToken);
}
public async Task<bool> HasAppointmentPassedAsync(
long id,
CancellationToken cancellationToken = default)
{
return await _appointmentRepository.Query()
.AnyAsync(appointment => appointment.Id == id
&& appointment.AppointmentTimeFrame.End < DateTime.UtcNow.Add(appointment.AppointmentTimeFrame.Start.Offset),
cancellationToken);
}
}
ExceptionHandlingMiddleware
});
#endregion
#region Validator registration
services.AddScoped<IValidator<CreateAppointmentRequestDto>, CreateAppointmentRequestValidator>();
services.AddScoped<IValidator<CreateMessageRequestDto>, CreateMessageRequestValidator>();
services.AddScoped<IValidator<RegisterRequestDto>, RegisterRequestValidator>();
services.AddScoped<IValidator<TutoringPostRequestDto>, TutoringPostRequestValidator>();
#endregion
#region Service registration
services.AddScoped<IAppointmentService, AppointmentService>();
// TODO: Implement FieldService
//services.AddScoped<IFieldService, FieldService>();
services.AddScoped<IIdentityService, IdentityService>();
services.AddScoped<ILocationService, LocationService>();
services.AddScoped<IMessageService, MessageService>();
services.AddScoped<IPasswordService, PasswordService>();
services.AddScoped<ITimeFrameService, TimeFrameService>();
services.AddScoped<ITutoringPostService, TutoringPostService>();
services.AddScoped<IUserService, UserService>();
// services go here
#endregion
#region Repository registration
services.AddScoped<IRepository<Appointment, long>, BaseRepository<Appointment, long>>();
services.AddScoped<IRepository<City, long>, BaseRepository<City, long>>();
services.AddScoped<IRepository<Country, long>, BaseRepository<Country, long>>();
services.AddScoped<IRepository<Field, long>, BaseRepository<Field, long>>();
services.AddScoped<IRepository<Data.Models.File, long>, BaseRepository<Data.Models.File, long>>();
services.AddScoped<IRepository<Appointment, long>, BaseRepository<Appointment, long>>();
// TODO: Implement login timestamp
services.AddScoped<IRepository<LoginTimestamp, long>, BaseRepository<LoginTimestamp, long>>();
services.AddScoped<IRepository<Message, long>, BaseRepository<Message, long>>();
services.AddScoped<IRepository<Region, long>, BaseRepository<Region, long>>();
services.AddScoped<IRepository<StudentsReview, long>, BaseRepository<StudentsReview, long>>();
services.AddScoped<IRepository<Subject, long>, BaseRepository<Subject, long>>();
services.AddScoped<IRepository<TimeFrame, long>, BaseRepository<TimeFrame, long>>();
services.AddScoped<IRepository<TutoringPost, long>, BaseRepository<TutoringPost, long>>();
services.AddScoped<IRepository<TutorsReview, long>, BaseRepository<TutorsReview, long>>();
services.AddScoped<IRepository<User, long>, BaseRepository<User, long>>();
#endregion
var connectionString = ConfigurationHelper
});
#endregion
#region Validator registration
services.AddScoped<IValidator<CreateAppointmentRequestDto>, CreateAppointmentRequestValidator>();
services.AddScoped<IValidator<CreateMessageRequestDto>, CreateMessageRequestValidator>();
services.AddScoped<IValidator<RegisterRequestDto>, RegisterRequestValidator>();
services.AddScoped<IValidator<TutoringPostRequestDto>, TutoringPostRequestValidator>();
#endregion
#region Service registration
services.AddScoped<IAppointmentService, AppointmentService>();
// TODO: Implement FieldService
//services.AddScoped<IFieldService, FieldService>();
services.AddScoped<IIdentityService, IdentityService>();
services.AddScoped<ILocationService, LocationService>();
services.AddScoped<IMessageService, MessageService>();
services.AddScoped<IPasswordService, PasswordService>();
services.AddScoped<ITimeFrameService, TimeFrameService>();
services.AddScoped<ITutoringPostService, TutoringPostService>();
services.AddScoped<IUserService, UserService>();
// services go here
#endregion
#region Repository registration
services.AddScoped<IRepository<Appointment, long>, BaseRepository<Appointment, long>>();
services.AddScoped<IRepository<City, long>, BaseRepository<City, long>>();
services.AddScoped<IRepository<Country, long>, BaseRepository<Country, long>>();
services.AddScoped<IRepository<Field, long>, BaseRepository<Field, long>>();
services.AddScoped<IRepository<Data.Models.File, long>, BaseRepository<Data.Models.File, long>>();
services.AddScoped<IRepository<Appointment, long>, BaseRepository<Appointment, long>>();
// TODO: Implement login timestamp
services.AddScoped<IRepository<LoginTimestamp, long>, BaseRepository<LoginTimestamp, long>>();
services.AddScoped<IRepository<Message, long>, BaseRepository<Message, long>>();
services.AddScoped<IRepository<Region, long>, BaseRepository<Region, long>>();
services.AddScoped<IRepository<StudentsReview, long>, BaseRepository<StudentsReview, long>>();
services.AddScoped<IRepository<Subject, long>, BaseRepository<Subject, long>>();
services.AddScoped<IRepository<TimeFrame, long>, BaseRepository<TimeFrame, long>>();
services.AddScoped<IRepository<TutoringPost, long>, BaseRepository<TutoringPost, long>>();
services.AddScoped<IRepository<TutorsReview, long>, BaseRepository<TutorsReview, long>>();
services.AddScoped<IRepository<User, long>, BaseRepository<User, long>>();
#endregion
var connectionString = ConfigurationHelper
ReviewAppointmentAsStudentAsync
ReviewAppointmentAsTutorAsync
}
public async Task<ICollection<ResponseAppointmentDto>> GetFutureAppointmentsAsync(
string username,
SortRequestDto? sortOptions = null,
PaginationRequestDto? paginationOptions = null,
CancellationToken cancellationToken = default)
{
return await _appointmentRepository.Query()
.Where(appointment =>
appointment.Tutor.Username == username.ToNormalizedLower()
|| appointment.AppointmentTimeFrame.TakenByStudentId.HasValue
&& appointment.AppointmentTimeFrame.TakenByStudent!.Username == username.ToNormalizedLower())
.OrderBy(appointment => appointment.AppointmentTimeFrame.Start)
.SortAppointments(sortOptions ?? new SortRequestDto { SortByProperty = SortByProperty.Date, SortOrder = SortOrder.Descending })
.Paginate(paginationOptions ?? new PaginationRequestDto { Skip = 0, Take = 25 })
.ProjectToDto()
.ToListAsync(cancellationToken);
}
public async Task<(ServiceActionResult Result, ResponseAppointmentDto? Updated)> ReviewAppointmentAsStudentAsync(
long appointmentId,
CreateReviewAsStudentRequestDto reviewDto)
{
await GetAppointmentAsync(reviewDto.Username, appointmentId);
await HasAppointmentPassedAsync(appointmentId);
var isAssignedToAppointment = await IsStudentAssignedToAppointmentAsync(reviewDto.Username, appointmentId);
if (isAssignedToAppointment is false)
throw new IdentityException(reviewDto.Username, nameof(ReviewAppointmentAsStudentAsync));
var appointment = await _appointmentRepository.FindByIdAsync(appointmentId);
// TODO: Check whether it is possible to overwrite an existing review
// - `ReviewAppointmentAsStudentAsync`
// - `ReviewAppointmentAsTutorAsync`
appointment!.StudentsReview = reviewDto.ToStudentsReview();
var (result, updated) = await _appointmentRepository.UpdateAsync(appointment);
if (result is not Data.Enums.RepositoryActionResult.Success)
return (ServiceActionResult.Failed, null);
return (ServiceActionResult.Updated, updated!.ToDto());
}
public async Task<(ServiceActionResult Result, ResponseAppointmentDto? Updated)> ReviewAppointmentAsTutorAsync(
long appointmentId,
CreateReviewAsTutorRequestDto reviewDto)
{
await GetAppointmentAsync(reviewDto.Username, appointmentId);
await HasAppointmentPassedAsync(appointmentId);
var isAssignedToAppointment = await IsTutorAssignedToAppointmentAsync(reviewDto.Username, appointmentId);
if (isAssignedToAppointment is false)
throw new IdentityException(reviewDto.Username, nameof(ReviewAppointmentAsStudentAsync));
var appointment = await _appointmentRepository.FindByIdAsync(appointmentId);
appointment!.TutorsReview = reviewDto.ToTutorsReview();
var (result, updated) = await _appointmentRepository.UpdateAsync(appointment);
if (result is not Data.Enums.RepositoryActionResult.Success)
return (ServiceActionResult.Failed, null);
return (ServiceActionResult.Updated, updated!.ToDto());
}
public async Task<bool> IsPartOfPostAsync(
using Application.Dtos.Common;
using Application.Enums;
using Application.Exceptions;
using Data.Models;
namespace Application.Extensions
{
internal static class UserExtensions
{
internal static bool IsTutor(this User user) =>
user.TutoringPosts.Any();
internal static bool IsStudentOfTutor(this User user, string tutorUsername) =>
user.TutoringAppointments
.Any(appointment => appointment.Tutor.Username == tutorUsername.ToNormalizedLower());
internal static IQueryable<User> SortTutors(this IQueryable<User> tutors, RequestSortDto sortDto)
{
return sortDto.SortByProperty switch
{
SortByProperty.Rating => tutors.OrderBy(tutor =>
tutor.TutoringAppointments
.Where(appointment => appointment.StudentsReview != null)
.Select(appointment => (double?)appointment.StudentsReview!.Stars)
.DefaultIfEmpty(0)
.Average()),
SortByProperty.Name => tutors.OrderBy(tutor => tutor.FirstName),
SortByProperty.Distance => throw new NotImplementedException(), // TODO: Implement sorting by distance from the user in SortTutors
SortByProperty.Date => throw new InvalidRequestException<User>(nameof(SortByProperty), nameof(SortByProperty.Date)),
_ => throw new NotSupportedException(),
};
}
}
}
using Application.Dtos.Indentity;
using Application.Extensions;
using Application.Interfaces;
using FluentValidation;
namespace Application.Validators
{
public class RegisterRequestValidator : AbstractValidator<RegisterRequestDto>
{
private readonly IUserService _userService;
private readonly ILocationService _locationService;
public RegisterRequestValidator(
IUserService userService,
ILocationService locationService)
{
_userService = userService;
_locationService = locationService;
RuleFor(register => register.Username)
.NotEmpty()
.MinimumLength(4)
.MustAsync(async (username, cancellationToken) =>
{
var result = await _userService.GetByUsernameOrDefaultAsync(username.ToNormalizedLower());
return result is null;
})
.WithMessage(username => $"Username `{username}` is already taken.");
RuleFor(register => register.Password)
.NotEmpty()
.MinimumLength(8)
.WithMessage("Minimum password length is 8 characters.");
RuleFor(register => register.Email)
.NotEmpty()
.EmailAddress();
RuleFor(register => register.FirstName)
.NotEmpty();
RuleFor(register => register.LastName)
.NotEmpty();
RuleFor(register => register.DateOfBirth)
.NotEmpty()
.LessThan(DateOnly.FromDateTime(DateTime.UtcNow));
RuleFor(register => register.CountryName)
.NotEmpty();
RuleFor(register => register.RegionName)
.NotEmpty();
RuleFor(register => register.CityName)
.NotEmpty();
RuleFor(register => register.CityName)
.MustAsync(async (register, cityName, cancellationToken) =>
await _locationService.CityExists(
register.CountryName,
register.RegionName,
register.CityName,
cancellationToken))
.WithMessage((register, cityName) => $"City `{cityName}` does not exist in `{register.CountryName} - {register.RegionName}`.");
// TODO: Improve mobile number validation during registration
// - Send verification code
RuleFor(register => register.MobileNumber)
.NotEmpty()
.Length(10);
}
}
}
Refactor validator rules where specific property is not the only part of the rule
Example:
RuleFor(message => message.RecipientUsername)
// to
RuleFor(message => message)
[FromQuery] PaginationRequestDto paginationOptions,
CancellationToken cancellationToken)
{
// TODO: Implement controllers
// - Investigate how to get user information from the JWT
throw new NotImplementedException();
}
}
import React from 'react'
import { Outlet } from 'react-router-dom'
// TODO: Configure Layout
function Layout () {
return (
<main>
<Outlet />
</main>
)
}
export default Layout
FluentValidation .WithMessage
has a Func<T, TProperty, string>
messageProvider argument overload.
.WithMessage((instance, validatedProperty) => $"{instance} {validatedProperty}");
enhancement enhancement
public User Tutor { get; set; } = null!;
public DateTimeOffset CreatedAt { get; set; }
public bool IsCancelled { get; set; }
public long? AudioRecordingId { get; set; } // TODO: Implement audio recording
public File? AudioRecording { get; set; }
public long? StudentsReviewId { get; set; }
public StudentsReview? StudentsReview { get; set; }
var tutor = await GetTutorAsync(tutorUsername, cancellationToken);
return await _userRepository.Query()
.Where(user => user.IsStudentOfTutor(tutorUsername)) // TODO: Implement missing sorting and pagination in GetStudentsAsync
.ProjectToDto()
.ToListAsync(cancellationToken);
}
appointment.StartAt.AddMinutes(appointment.DurationMinutes)
< DateTime.UtcNow.Add(appointment.StartAt.Offset))
? tutor.TutoringAppointments.Sum(x => x.DurationMinutes) / 60
: 0,
using Riok.Mapperly.Abstractions;
namespace Application.Dtos.User
{
[Mapper]
internal static partial class UserMappings
{
internal static partial UserDto ToDto(this Data.Models.User user);
internal static partial IQueryable<UserDto> ProjectToDto(this IQueryable<Data.Models.User> users);
internal static IQueryable<UserDto> ProjectTutorToDto(this IQueryable<Data.Models.User> tutors) =>
tutors.Select(tutor => new UserDto
{
Id = tutor.Id,
Username = tutor.Username,
FirstName = tutor.FirstName,
LastName = tutor.LastName,
About = tutor.About,
CityName = tutor.City.Name,
TutorInfo = new TutorInfoDto
{
AverageRating = tutor.TutoringAppointments
.Where(appointment => appointment.StudentsReview != null)
.Any()
? tutor.TutoringAppointments.Average(appointment => appointment.StudentsReview!.Stars)
: null,
// TODO: Fix total tutoring hours calculation
//TotalTutoringHours = tutor.TutoringAppointments
// .Any(appointment =>
// appointment.StartAt.AddMinutes(appointment.DurationMinutes)
// < DateTime.UtcNow.Add(appointment.StartAt.Offset))
// ? tutor.TutoringAppointments.Sum(x => x.DurationMinutes) / 60
// : 0,
}
});
}
}
backend/Data/Models
.backend/Data/Enums
.Resources:
using Application.Dtos.Indentity;
using Application.Dtos.User;
using Application.Enums;
using Application.Extensions;
using Application.Interfaces;
using Data.Interfaces;
using Data.Models;
using FluentValidation;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace Application.Services
{
public class IdentityService : IIdentityService
{
private readonly IConfiguration _configuration;
private readonly IRepository<User, long> _userRepository;
private readonly IUserService _userService;
private readonly IValidator<RegisterRequestDto> _registerRequestValidator;
private readonly IValidator<LoginRequestDto> _loginRequestValidator;
private readonly IPasswordService _passwordService;
private readonly ILocationService _locationService;
private readonly ILogger<IdentityService> _logger;
public IdentityService(
IConfiguration configuration,
IRepository<User, long> userRepository,
IUserService userService,
IValidator<RegisterRequestDto> registerRequestValidator,
IValidator<LoginRequestDto> loginRequestValidator,
IPasswordService passwordService,
ILocationService locationService,
ILogger<IdentityService> logger)
{
_configuration = configuration;
_userRepository = userRepository;
_userService = userService;
_registerRequestValidator = registerRequestValidator;
_loginRequestValidator = loginRequestValidator;
_passwordService = passwordService;
_locationService = locationService;
_logger = logger;
}
public async Task<(IdentityActionResult Result, TokenResponseDto Token)> LoginAsync(LoginRequestDto loginDto)
{
throw new NotImplementedException();
}
public async Task<(ServiceActionResult Result, UserResponseDto? Created, TokenResponseDto? Token)> RegisterAsync(RegisterRequestDto registerDto)
{
await _registerRequestValidator.ValidateAndThrowAsync(registerDto);
var iterations = _configuration.GetValue<int>("Security:PasswordHashIterations");
var hashLenght = _configuration.GetValue<int>("Security:HashLengthBytes");
var saltLength = _configuration.GetValue<int>("Security:SaltLengthBytes");
var (passwordHash, salt) = _passwordService.GeneratePassword(
registerDto.Password,
iterations,
hashLenght,
saltLength);
var city = await _locationService.FindCity(
registerDto.CountryName,
registerDto.RegionName,
registerDto.CityName);
var user = new User
{
FirstName = registerDto.FirstName,
LastName = registerDto.LastName,
Email = registerDto.Email.ToNormalizedLower(),
Username = registerDto.Username.ToNormalizedLower(),
PasswordHash = passwordHash,
Salt = salt,
DateOfBirth = registerDto.DateOfBirth,
CityId = city.Id,
MobileNumber = registerDto.MobileNumber,
// TODO: Generate Stripe related information during registration
};
var (result, created) = await _userRepository.CreateAsync(user);
if (result is not Data.Enums.RepositoryActionResult.Success)
return (ServiceActionResult.Failed, null, null);
return (
ServiceActionResult.Created,
created!.ToDto(),
new TokenResponseDto
{
Jwt = GenerateUserJwt(
created!.Id,
created!.Username,
created!.Email)
});
}
public string GenerateUserJwt(
long userId,
string username,
string email)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_configuration.GetValue<string>("Identity:TokenSecret")!);
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new(JwtRegisteredClaimNames.Sub, userId.ToString()),
new(JwtRegisteredClaimNames.Email, email),
new(JwtRegisteredClaimNames.NameId, username),
};
var tokenDescriptior = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(_configuration.GetValue<int>("Identity:JwtLifetimeMinutes")),
Issuer = _configuration.GetValue<string>(_configuration.GetValue<string>("Identity:Issuer")!),
Audience = _configuration.GetValue<string>(_configuration.GetValue<string>("Identity:Audience")!),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptior);
var jwt = tokenHandler.WriteToken(token);
return jwt;
}
// TODO: Implement Refresh Token generation and consumption
}
}
.WithMessage("An appointment time frame must start in the future.")
.Must(timeFrame =>
timeFrame.Start.AddHours(8) < timeFrame.End)
.WithMessage("A single appointment time frame must be less then 8 hours.")) // TODO: Implement configuration for time frame duration
.Must(_timeFrameService.AnyOverlappingTimeFrames)
.WithMessage("Appointment time frames cannot overlap.");
TutoringPost
routes to Endpoints.cs
as per API specTutoringPostController.cs
to Api/Controllers
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.