Git Product home page Git Product logo

edulink's People

Contributors

dariomrk avatar josipasaravanja avatar

Watchers

 avatar

edulink's Issues

Configure host properties

https://api.github.com/dariomrk/EduLink/blob/a78df3fef8acf5d91187e648af211afc32665ad2/backend/Api/Program.cs#L26

    {
        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();

Implement UserController

On branch: backend/feat/24-user-controller

  • Add Endpoints.cs to Api
  • Add User routes to Endpoints.cs as per API spec
  • Add UsersController.cs to Api/Controllers
  • Define controller methods as per API specification doc (no authorization)

Generate Stripe related information during registration

https://api.github.com/dariomrk/EduLink/blob/e1bb3f8587bba0b89f0792dfde1cd8859e27db27/backend/Application/Services/IdentityService.cs#L84

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
    }
}

Dizajn

  • Izrada logoa projekta
    Potpuna razrada (hi-fi) wireframe-a proizvoda
    Minimalno 60% posto razrađenog UI-a
    Funkcionalan prototip aplikacije (ako UI nije gotov, wireframe prototip je okej ili kombinacija wire i UI) - dakle ovo treba sadržavati više od onoga što će developeri napraviti, oni imaju bar 3 screena za implementaciju a prototip treba sadržavat više od toga
    Materijali marketingu za oglase/objave

UserService

  • Create IUserService interface
  • Implement UserService
  • Add required UserDto mappings
  • [ ] Implement UserValidator

Implement configuration for IsAppointmentCancelableAsync

  • Add field to appsettings.json for setting the appointment cancellation limit (hours)

https://api.github.com/dariomrk/EduLink/blob/315ba322a098ce8d83ea9d5c76f18f28ff96da96/backend/Application/Services/AppointmentService.cs#L222

        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);
        }
    }

Implement Stripe integration

https://api.github.com/dariomrk/EduLink/blob/a78df3fef8acf5d91187e648af211afc32665ad2/backend/Application/Services/TutoringPostService.cs#L56

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;
        }
    }
}

Implement sorting by distance from the user in SortAppointments

https://api.github.com/dariomrk/EduLink/blob/e4db31d4a0fceb673ac509348702e6f41481481a/backend/Application/Extensions/AppointmentExtensions.cs#L17

                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),

AppointmentService

  • Add IAppointmentService interface
  • Implement AppointmentService
  • Add Appointment mappings
  • Add AppointmentValidator

Category card

Props:

  • Image (default to a placeholder image, base64)
  • Title
  • Subtitle
  • Color

Image

Instructor card

Props:

  • Image (default to a placeholder, base64)
  • Title
  • Distance (default to "unknown distance")
  • Average rating (default hidden)
  • Review count (default hidden)
  • IsVerified (default hidden)

Image

Add pagination and sorting to GetAllFromCountryAsync

https://api.github.com/dariomrk/EduLink/blob/a442c53022c3de86923a4fb260dcfc2c7fbb29e2/backend/Application/Services/LocationService.cs#L31

            _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)

NotFoundPage <Route path="*" element={<NotFoundPage />} /> */}

https://api.github.com/dariomrk/EduLink/blob/c70603428db4f533cef19100ff5417088e2d4909/frontend/src/router/Router.jsx#L9

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

IdentityService

  • Add IIdentityService interface
  • Implement IdentityService
  • Add Identity mappings
  • [ ] Add IdentityValidator

Configure services and repositories

https://api.github.com/dariomrk/EduLink/blob/a78df3fef8acf5d91187e648af211afc32665ad2/backend/Api/Program.cs#L37

    {
        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();

Implement sorting by distance from the user in SortTutoringPosts

https://api.github.com/dariomrk/EduLink/blob/a78df3fef8acf5d91187e648af211afc32665ad2/backend/Application/Extensions/TutoringPostExtensions.cs#L21

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(),
            };
        }
    }
}

Clean up Program.cs

  • Separate service registration
  • Separate configuration
  • Separate middleware registration

Implement configuration for IsAppointmentCancelableAsync

  • Add field to appsettings.json for setting the appointment cancellation limit (hours)

https://api.github.com/dariomrk/EduLink/blob/315ba322a098ce8d83ea9d5c76f18f28ff96da96/backend/Application/Services/AppointmentService.cs#L222

        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);
        }
    }

Configure logging

  • Add logging to ExceptionHandlingMiddleware
  • Configure Serilog
  • Add Serilog file sink

Implement login timestamp

https://api.github.com/dariomrk/EduLink/blob/177876b7cf25a9b8aa5138345fce895796fdd03a/backend/Api/Program.cs#L94

                });
            #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

Implement FieldService

https://api.github.com/dariomrk/EduLink/blob/177876b7cf25a9b8aa5138345fce895796fdd03a/backend/Api/Program.cs#L75

                });
            #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

Check whether it is possible to overwrite an existing review

  • ReviewAppointmentAsStudentAsync

  • ReviewAppointmentAsTutorAsync

https://api.github.com/dariomrk/EduLink/blob/315ba322a098ce8d83ea9d5c76f18f28ff96da96/backend/Application/Services/AppointmentService.cs#L135

        }

        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(

Implement sorting by distance from the user in SortTutors

https://api.github.com/dariomrk/EduLink/blob/a78df3fef8acf5d91187e648af211afc32665ad2/backend/Application/Extensions/UserExtensions.cs#L30

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(),
            };
        }
    }
}

Improve mobile number validation during registration

  • Send verification code

https://api.github.com/dariomrk/EduLink/blob/e1bb3f8587bba0b89f0792dfde1cd8859e27db27/backend/Application/Validators/RegisterRequestValidator.cs#L68

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 validators

Refactor validator rules where specific property is not the only part of the rule
Example:

RuleFor(message => message.RecipientUsername)

// to

RuleFor(message => message)

MessageService

  • Add IMessageService interface
  • Implement MessageService
  • Add Message mappings
  • Add MessageValidator

Simplify validator message interpolation

FluentValidation .WithMessage has a Func<T, TProperty, string> messageProvider argument overload.

.WithMessage((instance, validatedProperty) => $"{instance} {validatedProperty}");

enhancement enhancement

TutoringPostService

  • Add ITutoringPostService interface
  • Implement TutoringPostService
  • Add Post mappings
  • Add PostValidator

Fix total tutoring hours calculation

appointment.StartAt.AddMinutes(appointment.DurationMinutes)

< DateTime.UtcNow.Add(appointment.StartAt.Offset))

? tutor.TutoringAppointments.Sum(x => x.DurationMinutes) / 60

: 0,

https://api.github.com/dariomrk/EduLink/blob/a78df3fef8acf5d91187e648af211afc32665ad2/backend/Application/Dtos/User/Mappings.cs#L28

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,
                }
            });
    }
}

Marketing

  • Segmentacija - target audience i pain points skupine, postoji li slična aplikacija na tržištu i po čemu se vaša ističe
  • Suradnja s dizajnom i devom za UI i UX - psihološke cake koje ta ciljana publika ima, Npr: takozvani digitalni domoroci drugačije scrollaju i koriste aplikacije naspram digitalnih migranata.
  • Projekcija budžeta
  • Marketinški plan:
  • Vizualni identitet kampanje (s dizajnerima)
  • Detaljan plan za organski marketing - od dizajnera tražite materijale za 2 objave
  • Detaljan plan plaćenih oglasa - od dizajnera tražite materijale za 2 oglasa
  • Search Engine Optimisation (SEO) i Search Engine Marketing (SEM) - suradnja s devovima, napisati kakve će oni korake poduzeti
  • 1 SEO optimiziran blog post (uz plan kad bi se objavilo na webu)
  • Tekstovi u aplikaciji
  • Public Relations - mediji, Press Release i timeline kampanje
  • Marketinške aktivnosti za budućnost

Search bar component

Props:

  • OnSearch callback
  • OnChange callback (triggers the fetch)
  • Items (items fetched from the backend)
  • Profile image (default to a placeholder, base64)
  • Placeholder (search bar placeholder text)

Image

Implement Refresh Token generation and consumption

https://api.github.com/dariomrk/EduLink/blob/e1bb3f8587bba0b89f0792dfde1cd8859e27db27/backend/Application/Services/IdentityService.cs#L135

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
    }
}

Implement configuration for time frame duration

https://api.github.com/dariomrk/EduLink/blob/e0b6bdf21f2099abc732900649c090283c091e69/backend/Application/Validators/TutoringPostRequestValidator.cs#L55

                    .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.");

Implement TutoringPostController

  • Add TutoringPost routes to Endpoints.cs as per API spec
  • Add TutoringPostController.cs to Api/Controllers
  • Define controller methods as per API specification doc (no authorization)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.