├── .gitignore ├── API ├── API.csproj ├── Controllers │ ├── ActivitiesController.cs │ ├── BaseController.cs │ ├── FallbackController.cs │ ├── FollowersController.cs │ ├── PhotosController.cs │ ├── ProfilesController.cs │ ├── UserController.cs │ └── ValuesController.cs ├── Middleware │ └── ErrorHandlingMiddleware.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── SignalR │ └── ChatHub.cs ├── Startup.cs └── appsettings.Development.json ├── Application ├── Activities │ ├── ActivityDto.cs │ ├── Attend.cs │ ├── AttendeeDto.cs │ ├── Create.cs │ ├── Delete.cs │ ├── Details.cs │ ├── Edit.cs │ ├── FollowingResolver.cs │ ├── List.cs │ ├── MappingProfile.cs │ └── Unattend.cs ├── Application.csproj ├── Comments │ ├── CommentDto.cs │ ├── Create.cs │ └── MappingProfile.cs ├── Errors │ └── RestException.cs ├── Followers │ ├── Add.cs │ ├── Delete.cs │ └── List.cs ├── Interfaces │ ├── IEmailSender.cs │ ├── IFacebookAccessor.cs │ ├── IJwtGenerator.cs │ ├── IPhotoAccessor.cs │ └── IUserAccessor.cs ├── Photos │ ├── Add.cs │ ├── Delete.cs │ ├── PhotoUploadResult.cs │ └── SetMain.cs ├── Profiles │ ├── Details.cs │ ├── Edit.cs │ ├── IProfileReader.cs │ ├── ListActivities.cs │ ├── Profile.cs │ ├── ProfileReader.cs │ └── UserActivityDto.cs ├── User │ ├── ConfirmEmail.cs │ ├── CurrentUser.cs │ ├── ExternalLogin.cs │ ├── FacebookUserInfo.cs │ ├── Login.cs │ ├── RefreshToken.cs │ ├── Register.cs │ ├── ResendEmailVerification.cs │ └── User.cs └── Validators │ └── ValidatorExtensions.cs ├── Domain ├── Activity.cs ├── AppUser.cs ├── Comment.cs ├── Domain.csproj ├── Photo.cs ├── RefreshToken.cs ├── UserActivity.cs ├── UserFollowing.cs └── Value.cs ├── Infrastructure ├── Class1.cs ├── Email │ ├── EmailSender.cs │ └── SendGridSettings.cs ├── Infrastructure.csproj ├── Photos │ ├── CloudinarySettings.cs │ └── PhotoAccessor.cs └── Security │ ├── FacebookAccessor.cs │ ├── FacebookAppSettings.cs │ ├── IsHostRequirement.cs │ ├── JwtGenerator.cs │ └── UserAccessor.cs ├── Persistence ├── DataContext.cs ├── Migrations │ ├── 20191114070541_InitialCreate.Designer.cs │ ├── 20191114070541_InitialCreate.cs │ ├── 20191114074846_SeedValues.Designer.cs │ ├── 20191114074846_SeedValues.cs │ ├── 20191114085341_ActivityEntityAdded.Designer.cs │ ├── 20191114085341_ActivityEntityAdded.cs │ ├── 20191115041407_AddedIdentity.Designer.cs │ ├── 20191115041407_AddedIdentity.cs │ ├── 20191115071537_UserActivityAdded.Designer.cs │ ├── 20191115071537_UserActivityAdded.cs │ ├── 20191116020804_PhotoEntityAdded.Designer.cs │ ├── 20191116020804_PhotoEntityAdded.cs │ ├── 20191116032259_AddedCommentEntity.Designer.cs │ ├── 20191116032259_AddedCommentEntity.cs │ ├── 20191116055056_AddedFollowingEntity.Designer.cs │ ├── 20191116055056_AddedFollowingEntity.cs │ ├── 20200812044106_RefreshToken.Designer.cs │ ├── 20200812044106_RefreshToken.cs │ └── DataContextModelSnapshot.cs ├── Persistence.csproj └── Seed.cs ├── Reactivities.sln └── client-app ├── .env.development ├── .env.production ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── assets │ ├── categoryImages │ │ ├── culture.jpg │ │ ├── drinks.jpg │ │ ├── film.jpg │ │ ├── food.jpg │ │ ├── music.jpg │ │ └── travel.jpg │ ├── logo.png │ ├── placeholder.png │ └── user.png ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── app │ ├── api │ │ └── agent.ts │ ├── common │ │ ├── form │ │ │ ├── DateInput.tsx │ │ │ ├── ErrorMessage.tsx │ │ │ ├── SelectInput.tsx │ │ │ ├── TextAreaInput.tsx │ │ │ └── TextInput.tsx │ │ ├── modals │ │ │ └── ModalContainer.tsx │ │ ├── options │ │ │ └── categoryOptions.ts │ │ ├── photoUpload │ │ │ ├── PhotoUploadWidget.tsx │ │ │ ├── PhotoWidgetCropper.tsx │ │ │ └── PhotoWidgetDropzone.tsx │ │ └── util │ │ │ └── util.ts │ ├── layout │ │ ├── App.tsx │ │ ├── LoadingComponent.tsx │ │ ├── NotFound.tsx │ │ ├── PrivateRoute.tsx │ │ ├── ScrollToTop.tsx │ │ └── styles.css │ ├── models │ │ ├── activity.ts │ │ ├── profile.ts │ │ └── user.ts │ └── stores │ │ ├── activityStore.ts │ │ ├── commonStore.ts │ │ ├── modalStore.ts │ │ ├── profileStore.ts │ │ ├── rootStore.ts │ │ └── userStore.ts ├── features │ ├── activities │ │ ├── dashboard │ │ │ ├── ActivityDashboard.tsx │ │ │ ├── ActivityFilters.tsx │ │ │ ├── ActivityList.tsx │ │ │ ├── ActivityListItem.tsx │ │ │ ├── ActivityListItemAttendees.tsx │ │ │ └── ActivityListItemPlaceholder.tsx │ │ ├── details │ │ │ ├── ActivityDetailedChat.tsx │ │ │ ├── ActivityDetailedHeader.tsx │ │ │ ├── ActivityDetailedInfo.tsx │ │ │ ├── ActivityDetailedSidebar.tsx │ │ │ └── ActivityDetails.tsx │ │ └── form │ │ │ └── ActivityForm.tsx │ ├── home │ │ └── HomePage.tsx │ ├── nav │ │ └── NavBar.tsx │ ├── profiles │ │ ├── ProfileActivities.tsx │ │ ├── ProfileCard.tsx │ │ ├── ProfileContent.tsx │ │ ├── ProfileDescription.tsx │ │ ├── ProfileEditForm.tsx │ │ ├── ProfileFollowings.tsx │ │ ├── ProfileHeader.tsx │ │ ├── ProfilePage.tsx │ │ └── ProfilePhotos.tsx │ └── user │ │ ├── LoginForm.tsx │ │ ├── RegisterForm.tsx │ │ ├── RegisterSuccess.tsx │ │ ├── SocialLogin.tsx │ │ └── VerifyEmail.tsx ├── index.tsx ├── react-app-env.d.ts └── serviceWorker.ts ├── tsconfig.json └── typings-custom ├── facebook-login-render-props.d.ts ├── react-cropper.d.ts └── react-widgets-date-fns.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | .vscode 4 | .DS_Store 5 | appsettings.json 6 | *.db 7 | wwwroot -------------------------------------------------------------------------------- /API/API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.0 4 | 3d6ff8eb-e8e2-4e79-8f55-a54a532e798c 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /API/Controllers/ActivitiesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Application.Activities; 5 | using Domain; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Mvc; 9 | 10 | namespace API.Controllers 11 | { 12 | public class ActivitiesController : BaseController 13 | { 14 | [HttpGet] 15 | public async Task> List(int? limit, 16 | int? offset, bool isGoing, bool isHost, DateTime? startDate) 17 | { 18 | return await Mediator.Send(new List.Query(limit, 19 | offset, isGoing, isHost, startDate)); 20 | } 21 | 22 | [HttpGet("{id}")] 23 | [Authorize] 24 | public async Task> Details(Guid id) 25 | { 26 | return await Mediator.Send(new Details.Query{Id = id}); 27 | } 28 | 29 | [HttpPost] 30 | public async Task> Create(Create.Command command) 31 | { 32 | return await Mediator.Send(command); 33 | } 34 | 35 | [HttpPut("{id}")] 36 | [Authorize(Policy = "IsActivityHost")] 37 | public async Task> Edit(Guid id, Edit.Command command) 38 | { 39 | command.Id = id; 40 | return await Mediator.Send(command); 41 | } 42 | 43 | [HttpDelete("{id}")] 44 | [Authorize(Policy = "IsActivityHost")] 45 | public async Task> Delete(Guid id) 46 | { 47 | return await Mediator.Send(new Delete.Command{Id = id}); 48 | } 49 | 50 | [HttpPost("{id}/attend")] 51 | public async Task> Attend(Guid id) 52 | { 53 | return await Mediator.Send(new Attend.Command{Id = id}); 54 | } 55 | 56 | [HttpDelete("{id}/attend")] 57 | public async Task> Unattend(Guid id) 58 | { 59 | return await Mediator.Send(new Unattend.Command{Id = id}); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /API/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace API.Controllers 6 | { 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | public class BaseController : ControllerBase 10 | { 11 | private IMediator _mediator; 12 | protected IMediator Mediator => _mediator ?? (_mediator = HttpContext.RequestServices.GetService()); 13 | } 14 | } -------------------------------------------------------------------------------- /API/Controllers/FallbackController.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace API.Controllers 6 | { 7 | [AllowAnonymous] 8 | public class FallbackController : Controller 9 | { 10 | public IActionResult Index() 11 | { 12 | return PhysicalFile(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"), "text/HTML"); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /API/Controllers/FollowersController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Application.Followers; 4 | using Application.Profiles; 5 | using MediatR; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace API.Controllers 9 | { 10 | [Route("api/profiles")] 11 | public class FollowersController : BaseController 12 | { 13 | [HttpPost("{username}/follow")] 14 | public async Task> Follow(string username) 15 | { 16 | return await Mediator.Send(new Add.Command{Username = username}); 17 | } 18 | 19 | [HttpDelete("{username}/follow")] 20 | public async Task> Unfollow(string username) 21 | { 22 | return await Mediator.Send(new Delete.Command{Username = username}); 23 | } 24 | 25 | [HttpGet("{username}/follow")] 26 | public async Task>> GetFollowings(string username, string predicate) 27 | { 28 | return await Mediator.Send(new List.Query{Username = username, Predicate = predicate}); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /API/Controllers/PhotosController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Application.Photos; 3 | using Domain; 4 | using MediatR; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace API.Controllers 8 | { 9 | public class PhotosController : BaseController 10 | { 11 | [HttpPost] 12 | public async Task> Add([FromForm]Add.Command command) 13 | { 14 | return await Mediator.Send(command); 15 | } 16 | 17 | [HttpDelete("{id}")] 18 | public async Task> Delete(string id) 19 | { 20 | return await Mediator.Send(new Delete.Command{Id = id}); 21 | } 22 | 23 | [HttpPost("{id}/setmain")] 24 | public async Task> SetMain(string id) 25 | { 26 | return await Mediator.Send(new SetMain.Command{Id = id}); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /API/Controllers/ProfilesController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Application.Profiles; 4 | using MediatR; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace API.Controllers 8 | { 9 | public class ProfilesController : BaseController 10 | { 11 | [HttpGet("{username}")] 12 | public async Task> Get(string username) 13 | { 14 | return await Mediator.Send(new Details.Query{Username = username}); 15 | } 16 | 17 | [HttpPut] 18 | public async Task> Edit(Edit.Command command) 19 | { 20 | return await Mediator.Send(command); 21 | } 22 | 23 | [HttpGet("{username}/activities")] 24 | public async Task>> GetUserActivities(string username, string predicate) 25 | { 26 | return await Mediator.Send(new ListActivities.Query{Username = username, Predicate = predicate}); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /API/Controllers/UserController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Application.User; 4 | using Domain; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace API.Controllers 10 | { 11 | public class UserController : BaseController 12 | { 13 | [AllowAnonymous] 14 | [HttpPost("login")] 15 | public async Task> Login(Login.Query query) 16 | { 17 | var user = await Mediator.Send(query); 18 | SetTokenCookie(user.RefreshToken); 19 | return user; 20 | } 21 | 22 | [AllowAnonymous] 23 | [HttpPost("register")] 24 | public async Task Register(Register.Command command) 25 | { 26 | command.Origin = Request.Headers["origin"]; 27 | await Mediator.Send(command); 28 | return Ok("Registration successful - please check your email"); 29 | } 30 | 31 | [HttpGet] 32 | public async Task> CurrentUser() 33 | { 34 | var user = await Mediator.Send(new CurrentUser.Query()); 35 | SetTokenCookie(user.RefreshToken); 36 | return user; 37 | } 38 | 39 | [AllowAnonymous] 40 | [HttpPost("facebook")] 41 | public async Task> FacebookLogin(ExternalLogin.Query query) 42 | { 43 | var user = await Mediator.Send(query); 44 | SetTokenCookie(user.RefreshToken); 45 | return user; 46 | } 47 | 48 | [HttpPost("refreshToken")] 49 | public async Task> RefreshToken(Application.User.RefreshToken.Command command) 50 | { 51 | command.RefreshToken = Request.Cookies["refreshToken"]; 52 | var user = await Mediator.Send(command); 53 | SetTokenCookie(user.RefreshToken); 54 | return user; 55 | } 56 | 57 | [AllowAnonymous] 58 | [HttpPost("verifyEmail")] 59 | public async Task VerifyEmail(ConfirmEmail.Command command) 60 | { 61 | var result = await Mediator.Send(command); 62 | if (!result.Succeeded) return BadRequest("Problem verifying email address"); 63 | return Ok("Email confirmed - you can now login"); 64 | } 65 | 66 | [AllowAnonymous] 67 | [HttpGet("resendEmailVerification")] 68 | public async Task ResendEmailVerification([FromQuery]ResendEmailVerification.Query query) 69 | { 70 | query.Origin = Request.Headers["origin"]; 71 | await Mediator.Send(query); 72 | 73 | return Ok("Email verification link sent - please check email"); 74 | } 75 | 76 | private void SetTokenCookie(string refreshToken) 77 | { 78 | var cookieOptions = new CookieOptions 79 | { 80 | HttpOnly = true, 81 | Expires = DateTime.UtcNow.AddDays(7) 82 | }; 83 | Response.Cookies.Append("refreshToken", refreshToken, cookieOptions); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /API/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Domain; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.EntityFrameworkCore; 6 | using Persistence; 7 | 8 | namespace DatingApp.API.Controllers 9 | { 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class ValuesController : ControllerBase 13 | { 14 | private readonly DataContext _context; 15 | public ValuesController(DataContext context) 16 | { 17 | _context = context; 18 | } 19 | 20 | // GET api/values 21 | [HttpGet] 22 | public async Task>> Get() 23 | { 24 | var values = await _context.Values.ToListAsync(); 25 | return Ok(values); 26 | } 27 | 28 | // GET api/values/5 29 | [HttpGet("{id}")] 30 | public async Task> Get(int id) 31 | { 32 | var value = await _context.Values.FindAsync(id); 33 | return Ok(value); 34 | } 35 | 36 | // POST api/values 37 | [HttpPost] 38 | public void Post([FromBody] string value) 39 | { 40 | } 41 | 42 | // PUT api/values/5 43 | [HttpPut("{id}")] 44 | public void Put(int id, [FromBody] string value) 45 | { 46 | } 47 | 48 | // DELETE api/values/5 49 | [HttpDelete("{id}")] 50 | public void Delete(int id) 51 | { 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /API/Middleware/ErrorHandlingMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace API.Middleware 10 | { 11 | public class ErrorHandlingMiddleware 12 | { 13 | private readonly RequestDelegate _next; 14 | private readonly ILogger _logger; 15 | public ErrorHandlingMiddleware(RequestDelegate next, ILogger logger) 16 | { 17 | _logger = logger; 18 | _next = next; 19 | } 20 | 21 | public async Task Invoke(HttpContext context) 22 | { 23 | try 24 | { 25 | await _next(context); 26 | } 27 | catch (Exception ex) 28 | { 29 | await HandleExceptionAsync(context, ex, _logger); 30 | } 31 | } 32 | 33 | private async Task HandleExceptionAsync(HttpContext context, Exception ex, ILogger logger) 34 | { 35 | object errors = null; 36 | 37 | switch (ex) 38 | { 39 | case RestException re: 40 | logger.LogError(ex, "REST ERROR"); 41 | errors = re.Errors; 42 | context.Response.StatusCode = (int)re.Code; 43 | break; 44 | case Exception e: 45 | logger.LogError(ex, "SERVER ERROR"); 46 | errors = string.IsNullOrWhiteSpace(e.Message) ? "Error" : e.Message; 47 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 48 | break; 49 | } 50 | 51 | context.Response.ContentType = "application/json"; 52 | if (errors != null) 53 | { 54 | var result = JsonSerializer.Serialize(new 55 | { 56 | errors 57 | }); 58 | 59 | await context.Response.WriteAsync(result); 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /API/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Domain; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | using Persistence; 10 | 11 | namespace API 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | var host = CreateHostBuilder(args).Build(); 18 | 19 | using (var scope = host.Services.CreateScope()) 20 | { 21 | var services = scope.ServiceProvider; 22 | try 23 | { 24 | var context = services.GetRequiredService(); 25 | var userManager = services.GetRequiredService>(); 26 | context.Database.Migrate(); 27 | Seed.SeedData(context, userManager).Wait(); 28 | } 29 | catch (Exception ex) 30 | { 31 | var logger = services.GetRequiredService>(); 32 | logger.LogError(ex, "An error occurred during migration"); 33 | } 34 | 35 | host.Run(); 36 | } 37 | 38 | } 39 | 40 | public static IHostBuilder CreateHostBuilder(string[] args) => 41 | Host.CreateDefaultBuilder(args) 42 | .ConfigureWebHostDefaults(webBuilder => 43 | { 44 | webBuilder.UseStartup(); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:29129", 8 | "sslPort": 44366 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "API": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "weatherforecast", 24 | "applicationUrl": "http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /API/SignalR/ChatHub.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Security.Claims; 3 | using System.Threading.Tasks; 4 | using Application.Comments; 5 | using MediatR; 6 | using Microsoft.AspNetCore.SignalR; 7 | 8 | namespace API.SignalR 9 | { 10 | public class ChatHub : Hub 11 | { 12 | private readonly IMediator _mediator; 13 | public ChatHub(IMediator mediator) 14 | { 15 | _mediator = mediator; 16 | } 17 | 18 | public async Task SendComment(Create.Command command) 19 | { 20 | string username = GetUsername(); 21 | 22 | command.Username = username; 23 | 24 | var comment = await _mediator.Send(command); 25 | 26 | await Clients.Group(command.ActivityId.ToString()).SendAsync("ReceiveComment", comment); 27 | } 28 | 29 | private string GetUsername() 30 | { 31 | return Context.User?.Claims?.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; 32 | } 33 | 34 | public async Task AddToGroup(string groupName) 35 | { 36 | await Groups.AddToGroupAsync(Context.ConnectionId, groupName); 37 | 38 | var username = GetUsername(); 39 | 40 | await Clients.Group(groupName).SendAsync("Send", $"{username} has joined the group"); 41 | } 42 | 43 | public async Task RemoveFromGroup(string groupName) 44 | { 45 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); 46 | 47 | var username = GetUsername(); 48 | 49 | await Clients.Group(groupName).SendAsync("Send", $"{username} has left the group"); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Data source=reactivities.db" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Debug", 8 | "System": "Information", 9 | "Microsoft": "Information" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Application/Activities/ActivityDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | using Application.Comments; 5 | 6 | namespace Application.Activities 7 | { 8 | public class ActivityDto 9 | { 10 | public Guid Id { get; set; } 11 | public string Title { get; set; } 12 | public string Description { get; set; } 13 | public string Category { get; set; } 14 | public DateTime Date { get; set; } 15 | public string City { get; set; } 16 | public string Venue { get; set; } 17 | 18 | [JsonPropertyName("attendees")] 19 | public ICollection UserActivities { get; set; } 20 | public ICollection Comments { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /Application/Activities/Attend.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using Application.Interfaces; 7 | using Domain; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | using Persistence; 11 | 12 | namespace Application.Activities 13 | { 14 | public class Attend 15 | { 16 | public class Command : IRequest 17 | { 18 | public Guid Id { get; set; } 19 | } 20 | 21 | public class Handler : IRequestHandler 22 | { 23 | private readonly DataContext _context; 24 | private readonly IUserAccessor _userAccessor; 25 | public Handler(DataContext context, IUserAccessor userAccessor) 26 | { 27 | _userAccessor = userAccessor; 28 | _context = context; 29 | } 30 | 31 | public async Task Handle(Command request, CancellationToken cancellationToken) 32 | { 33 | var activity = await _context.Activities.FindAsync(request.Id); 34 | 35 | if (activity == null) 36 | throw new RestException(HttpStatusCode.NotFound, new {Activity = "Cound not find activity"}); 37 | 38 | var user = await _context.Users.SingleOrDefaultAsync(x => 39 | x.UserName == _userAccessor.GetCurrentUsername()); 40 | 41 | var attendance = await _context.UserActivities 42 | .SingleOrDefaultAsync(x => x.ActivityId == activity.Id && 43 | x.AppUserId == user.Id); 44 | 45 | if (attendance != null) 46 | throw new RestException(HttpStatusCode.BadRequest, 47 | new {Attendance = "Already attending this activity"}); 48 | 49 | attendance = new UserActivity 50 | { 51 | Activity = activity, 52 | AppUser = user, 53 | IsHost = false, 54 | DateJoined = DateTime.Now 55 | }; 56 | 57 | _context.UserActivities.Add(attendance); 58 | 59 | var success = await _context.SaveChangesAsync() > 0; 60 | 61 | if (success) return Unit.Value; 62 | 63 | throw new Exception("Problem saving changes"); 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /Application/Activities/AttendeeDto.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Activities 2 | { 3 | public class AttendeeDto 4 | { 5 | public string Username { get; set; } 6 | public string DisplayName { get; set; } 7 | public string Image { get; set; } 8 | public bool IsHost { get; set; } 9 | public bool Following { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /Application/Activities/Create.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Application.Interfaces; 5 | using Domain; 6 | using FluentValidation; 7 | using MediatR; 8 | using Microsoft.EntityFrameworkCore; 9 | using Persistence; 10 | 11 | namespace Application.Activities 12 | { 13 | public class Create 14 | { 15 | public class Command : IRequest 16 | { 17 | public Guid Id { get; set; } 18 | public string Title { get; set; } 19 | public string Description { get; set; } 20 | public string Category { get; set; } 21 | public DateTime Date { get; set; } 22 | public string City { get; set; } 23 | public string Venue { get; set; } 24 | } 25 | 26 | public class CommandValidator : AbstractValidator 27 | { 28 | public CommandValidator() 29 | { 30 | RuleFor(x => x.Title).NotEmpty(); 31 | RuleFor(x => x.Description).NotEmpty(); 32 | RuleFor(x => x.Category).NotEmpty(); 33 | RuleFor(x => x.Date).NotEmpty(); 34 | RuleFor(x => x.City).NotEmpty(); 35 | RuleFor(x => x.Venue).NotEmpty(); 36 | } 37 | } 38 | 39 | public class Handler : IRequestHandler 40 | { 41 | private readonly DataContext _context; 42 | private readonly IUserAccessor _userAccessor; 43 | public Handler(DataContext context, IUserAccessor userAccessor) 44 | { 45 | _userAccessor = userAccessor; 46 | _context = context; 47 | } 48 | 49 | public async Task Handle(Command request, CancellationToken cancellationToken) 50 | { 51 | var activity = new Activity 52 | { 53 | Id = request.Id, 54 | Title = request.Title, 55 | Description = request.Description, 56 | Category = request.Category, 57 | Date = request.Date, 58 | City = request.City, 59 | Venue = request.Venue 60 | }; 61 | 62 | _context.Activities.Add(activity); 63 | 64 | var user = await _context.Users.SingleOrDefaultAsync(x => 65 | x.UserName == _userAccessor.GetCurrentUsername()); 66 | 67 | var attendee = new UserActivity 68 | { 69 | AppUser = user, 70 | Activity = activity, 71 | IsHost = true, 72 | DateJoined = DateTime.Now 73 | }; 74 | 75 | _context.UserActivities.Add(attendee); 76 | 77 | var success = await _context.SaveChangesAsync() > 0; 78 | 79 | if (success) return Unit.Value; 80 | 81 | throw new Exception("Problem saving changes"); 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /Application/Activities/Delete.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using MediatR; 7 | using Persistence; 8 | 9 | namespace Application.Activities 10 | { 11 | public class Delete 12 | { 13 | public class Command : IRequest 14 | { 15 | public Guid Id { get; set; } 16 | } 17 | 18 | public class Handler : IRequestHandler 19 | { 20 | private readonly DataContext _context; 21 | public Handler(DataContext context) 22 | { 23 | _context = context; 24 | } 25 | 26 | public async Task Handle(Command request, CancellationToken cancellationToken) 27 | { 28 | var activity = await _context.Activities.FindAsync(request.Id); 29 | 30 | if (activity == null) 31 | throw new RestException(HttpStatusCode.NotFound, new { Activity = "Not found" }); 32 | 33 | 34 | _context.Remove(activity); 35 | 36 | var success = await _context.SaveChangesAsync() > 0; 37 | 38 | if (success) return Unit.Value; 39 | 40 | throw new Exception("Problem saving changes"); 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Application/Activities/Details.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using AutoMapper; 7 | using Domain; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | using Persistence; 11 | 12 | namespace Application.Activities 13 | { 14 | public class Details 15 | { 16 | public class Query : IRequest 17 | { 18 | public Guid Id { get; set; } 19 | } 20 | 21 | public class Handler : IRequestHandler 22 | { 23 | private readonly DataContext _context; 24 | private readonly IMapper _mapper; 25 | public Handler(DataContext context, IMapper mapper) 26 | { 27 | _mapper = mapper; 28 | this._context = context; 29 | } 30 | 31 | public async Task Handle(Query request, CancellationToken cancellationToken) 32 | { 33 | var activity = await _context.Activities 34 | .FindAsync(request.Id); 35 | 36 | if (activity == null) 37 | throw new RestException(HttpStatusCode.NotFound, new { Activity = "Not found" }); 38 | 39 | var activityToReturn = _mapper.Map(activity); 40 | 41 | return activityToReturn; 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Application/Activities/Edit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using FluentValidation; 7 | using MediatR; 8 | using Persistence; 9 | 10 | namespace Application.Activities 11 | { 12 | public class Edit 13 | { 14 | public class Command : IRequest 15 | { 16 | public Guid Id { get; set; } 17 | public string Title { get; set; } 18 | public string Description { get; set; } 19 | public string Category { get; set; } 20 | public DateTime? Date { get; set; } 21 | public string City { get; set; } 22 | public string Venue { get; set; } 23 | } 24 | 25 | public class CommandValidator : AbstractValidator 26 | { 27 | public CommandValidator() 28 | { 29 | RuleFor(x => x.Title).NotEmpty(); 30 | RuleFor(x => x.Description).NotEmpty(); 31 | RuleFor(x => x.Category).NotEmpty(); 32 | RuleFor(x => x.Date).NotEmpty(); 33 | RuleFor(x => x.City).NotEmpty(); 34 | RuleFor(x => x.Venue).NotEmpty(); 35 | } 36 | } 37 | 38 | public class Handler : IRequestHandler 39 | { 40 | private readonly DataContext _context; 41 | public Handler(DataContext context) 42 | { 43 | _context = context; 44 | } 45 | 46 | public async Task Handle(Command request, CancellationToken cancellationToken) 47 | { 48 | var activity = await _context.Activities.FindAsync(request.Id); 49 | 50 | if (activity == null) 51 | throw new RestException(HttpStatusCode.NotFound, new { Activity = "Not found" }); 52 | 53 | activity.Title = request.Title ?? activity.Title; 54 | activity.Description = request.Description ?? activity.Description; 55 | activity.Category = request.Category ?? activity.Category; 56 | activity.Date = request.Date ?? activity.Date; 57 | activity.City = request.City ?? activity.City; 58 | activity.Venue = request.Venue ?? activity.Venue; 59 | 60 | var success = await _context.SaveChangesAsync() > 0; 61 | 62 | if (success) return Unit.Value; 63 | 64 | throw new Exception("Problem saving changes"); 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Application/Activities/FollowingResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Application.Interfaces; 3 | using AutoMapper; 4 | using Domain; 5 | using Microsoft.EntityFrameworkCore; 6 | using Persistence; 7 | 8 | namespace Application.Activities 9 | { 10 | public class FollowingResolver : IValueResolver 11 | { 12 | private readonly DataContext _context; 13 | private readonly IUserAccessor _userAccessor; 14 | public FollowingResolver(DataContext context, IUserAccessor userAccessor) 15 | { 16 | _userAccessor = userAccessor; 17 | _context = context; 18 | } 19 | 20 | public bool Resolve(UserActivity source, AttendeeDto destination, bool destMember, ResolutionContext context) 21 | { 22 | var currentUser = _context.Users.SingleOrDefaultAsync(x => x.UserName == _userAccessor.GetCurrentUsername()).Result; 23 | 24 | if (currentUser.Followings.Any(x => x.TargetId == source.AppUserId)) 25 | return true; 26 | 27 | return false; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Application/Activities/List.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Application.Interfaces; 7 | using AutoMapper; 8 | using Domain; 9 | using MediatR; 10 | using Microsoft.EntityFrameworkCore; 11 | using Persistence; 12 | 13 | namespace Application.Activities 14 | { 15 | public class List 16 | { 17 | public class ActivitiesEnvelope 18 | { 19 | public List Activities { get; set; } 20 | public int ActivityCount { get; set; } 21 | } 22 | public class Query : IRequest 23 | { 24 | public Query(int? limit, int? offset, bool isGoing, bool isHost, DateTime? startDate) 25 | { 26 | Limit = limit; 27 | Offset = offset; 28 | IsGoing = isGoing; 29 | IsHost = isHost; 30 | StartDate = startDate ?? DateTime.Now; 31 | } 32 | public int? Limit { get; set; } 33 | public int? Offset { get; set; } 34 | public bool IsGoing { get; set; } 35 | public bool IsHost { get; set; } 36 | public DateTime? StartDate { get; set; } 37 | } 38 | 39 | public class Handler : IRequestHandler 40 | { 41 | private readonly DataContext _context; 42 | private readonly IMapper _mapper; 43 | private readonly IUserAccessor _userAccessor; 44 | public Handler(DataContext context, IMapper mapper, IUserAccessor userAccessor) 45 | { 46 | _userAccessor = userAccessor; 47 | _mapper = mapper; 48 | _context = context; 49 | } 50 | 51 | public async Task Handle(Query request, CancellationToken cancellationToken) 52 | { 53 | var queryable = _context.Activities 54 | .Where(x => x.Date >= request.StartDate) 55 | .OrderBy(x => x.Date) 56 | .AsQueryable(); 57 | 58 | if (request.IsGoing && !request.IsHost) 59 | { 60 | queryable = queryable.Where(x => x.UserActivities.Any(a => a.AppUser.UserName == _userAccessor.GetCurrentUsername())); 61 | } 62 | 63 | if (request.IsHost && !request.IsGoing) 64 | { 65 | queryable = queryable.Where(x => x.UserActivities.Any(a => a.AppUser.UserName == _userAccessor.GetCurrentUsername() && a.IsHost)); 66 | } 67 | 68 | var activities = await queryable 69 | .Skip(request.Offset ?? 0) 70 | .Take(request.Limit ?? 3).ToListAsync(); 71 | 72 | return new ActivitiesEnvelope 73 | { 74 | Activities = _mapper.Map, List>(activities), 75 | ActivityCount = queryable.Count() 76 | }; 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Application/Activities/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using AutoMapper; 3 | using Domain; 4 | 5 | namespace Application.Activities 6 | { 7 | public class MappingProfile : Profile 8 | { 9 | public MappingProfile() 10 | { 11 | CreateMap(); 12 | CreateMap() 13 | .ForMember(d => d.Username, o => o.MapFrom(s => s.AppUser.UserName)) 14 | .ForMember(d => d.DisplayName, o => o.MapFrom(s => s.AppUser.DisplayName)) 15 | .ForMember(d => d.Image, o => o.MapFrom(s => s.AppUser.Photos.FirstOrDefault(x => x.IsMain).Url)) 16 | .ForMember(d => d.Following, o => o.MapFrom()); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Application/Activities/Unattend.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using Application.Interfaces; 7 | using MediatR; 8 | using Microsoft.EntityFrameworkCore; 9 | using Persistence; 10 | 11 | namespace Application.Activities 12 | { 13 | public class Unattend 14 | { 15 | public class Command : IRequest 16 | { 17 | public Guid Id { get; set; } 18 | } 19 | 20 | public class Handler : IRequestHandler 21 | { 22 | private readonly DataContext _context; 23 | private readonly IUserAccessor _userAccessor; 24 | public Handler(DataContext context, IUserAccessor userAccessor) 25 | { 26 | _userAccessor = userAccessor; 27 | _context = context; 28 | } 29 | 30 | public async Task Handle(Command request, CancellationToken cancellationToken) 31 | { 32 | var activity = await _context.Activities.FindAsync(request.Id); 33 | 34 | if (activity == null) 35 | throw new RestException(HttpStatusCode.NotFound, new {Activity = "Cound not find activity"}); 36 | 37 | var user = await _context.Users.SingleOrDefaultAsync(x => 38 | x.UserName == _userAccessor.GetCurrentUsername()); 39 | 40 | var attendance = await _context.UserActivities 41 | .SingleOrDefaultAsync(x => x.ActivityId == activity.Id && 42 | x.AppUserId == user.Id); 43 | 44 | if (attendance == null) 45 | return Unit.Value; 46 | 47 | if (attendance.IsHost) 48 | throw new RestException(HttpStatusCode.BadRequest, new {Attendance = "You cannot remove yourself as host"}); 49 | 50 | _context.UserActivities.Remove(attendance); 51 | 52 | var success = await _context.SaveChangesAsync() > 0; 53 | 54 | if (success) return Unit.Value; 55 | 56 | throw new Exception("Problem saving changes"); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Application/Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | netcoreapp3.0 13 | 14 | -------------------------------------------------------------------------------- /Application/Comments/CommentDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Application.Comments 4 | { 5 | public class CommentDto 6 | { 7 | public Guid Id { get; set; } 8 | public string Body { get; set; } 9 | public DateTime CreatedAt { get; set; } 10 | public string Username { get; set; } 11 | public string DisplayName { get; set; } 12 | public string Image { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /Application/Comments/Create.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using AutoMapper; 7 | using Domain; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | using Persistence; 11 | 12 | namespace Application.Comments 13 | { 14 | public class Create 15 | { 16 | public class Command : IRequest 17 | { 18 | public string Body { get; set; } 19 | public Guid ActivityId { get; set; } 20 | public string Username { get; set; } 21 | } 22 | 23 | public class Handler : IRequestHandler 24 | { 25 | private readonly DataContext _context; 26 | private readonly IMapper _mapper; 27 | public Handler(DataContext context, IMapper mapper) 28 | { 29 | _mapper = mapper; 30 | _context = context; 31 | } 32 | 33 | public async Task Handle(Command request, CancellationToken cancellationToken) 34 | { 35 | var activity = await _context.Activities.FindAsync(request.ActivityId); 36 | 37 | if (activity == null) 38 | throw new RestException(HttpStatusCode.NotFound, new {Activity = "Not found"}); 39 | 40 | var user = await _context.Users.SingleOrDefaultAsync(x => x.UserName == request.Username); 41 | 42 | var comment = new Comment 43 | { 44 | Author = user, 45 | Activity = activity, 46 | Body = request.Body, 47 | CreatedAt = DateTime.Now 48 | }; 49 | 50 | activity.Comments.Add(comment); 51 | 52 | var success = await _context.SaveChangesAsync() > 0; 53 | 54 | if (success) return _mapper.Map(comment); 55 | 56 | throw new Exception("Problem saving changes"); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Application/Comments/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using AutoMapper; 3 | using Domain; 4 | 5 | namespace Application.Comments 6 | { 7 | public class MappingProfile : Profile 8 | { 9 | public MappingProfile() 10 | { 11 | CreateMap() 12 | .ForMember(d => d.Username, o => o.MapFrom(s => s.Author.UserName)) 13 | .ForMember(d => d.DisplayName, o => o.MapFrom(s => s.Author.DisplayName)) 14 | .ForMember(d => d.Image, o => o.MapFrom(s => s.Author.Photos.FirstOrDefault(x => x.IsMain).Url)); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Application/Errors/RestException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace Application.Errors 5 | { 6 | public class RestException : Exception 7 | { 8 | public RestException(HttpStatusCode code, object errors = null) 9 | { 10 | Code = code; 11 | Errors = errors; 12 | } 13 | 14 | public HttpStatusCode Code { get; } 15 | public object Errors { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /Application/Followers/Add.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using Application.Interfaces; 7 | using Domain; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | using Persistence; 11 | 12 | namespace Application.Followers 13 | { 14 | public class Add 15 | { 16 | public class Command : IRequest 17 | { 18 | public string Username { get; set; } 19 | } 20 | 21 | public class Handler : IRequestHandler 22 | { 23 | private readonly DataContext _context; 24 | private readonly IUserAccessor _userAccessor; 25 | public Handler(DataContext context, IUserAccessor userAccessor) 26 | { 27 | _userAccessor = userAccessor; 28 | _context = context; 29 | } 30 | 31 | public async Task Handle(Command request, CancellationToken cancellationToken) 32 | { 33 | var observer = await _context.Users.SingleOrDefaultAsync(x => x.UserName == _userAccessor.GetCurrentUsername()); 34 | 35 | var target = await _context.Users.SingleOrDefaultAsync(x => x.UserName == request.Username); 36 | 37 | if (target == null) 38 | throw new RestException(HttpStatusCode.NotFound, new {User = "Not found"}); 39 | 40 | var following = await _context.Followings.SingleOrDefaultAsync(x => x.ObserverId == observer.Id && x.TargetId == target.Id); 41 | 42 | if (following != null) 43 | throw new RestException(HttpStatusCode.BadRequest, new {User = "You are already following this user"}); 44 | 45 | if (following == null) 46 | { 47 | following = new UserFollowing 48 | { 49 | Observer = observer, 50 | Target = target 51 | }; 52 | 53 | _context.Followings.Add(following); 54 | } 55 | 56 | var success = await _context.SaveChangesAsync() > 0; 57 | 58 | if (success) return Unit.Value; 59 | 60 | throw new Exception("Problem saving changes"); 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /Application/Followers/Delete.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using Application.Interfaces; 7 | using Domain; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | using Persistence; 11 | 12 | namespace Application.Followers 13 | { 14 | public class Delete 15 | { 16 | public class Command : IRequest 17 | { 18 | public string Username { get; set; } 19 | } 20 | 21 | public class Handler : IRequestHandler 22 | { 23 | private readonly DataContext _context; 24 | private readonly IUserAccessor _userAccessor; 25 | public Handler(DataContext context, IUserAccessor userAccessor) 26 | { 27 | _userAccessor = userAccessor; 28 | _context = context; 29 | } 30 | 31 | public async Task Handle(Command request, CancellationToken cancellationToken) 32 | { 33 | var observer = await _context.Users.SingleOrDefaultAsync(x => x.UserName == _userAccessor.GetCurrentUsername()); 34 | 35 | var target = await _context.Users.SingleOrDefaultAsync(x => x.UserName == request.Username); 36 | 37 | if (target == null) 38 | throw new RestException(HttpStatusCode.NotFound, new { User = "Not found" }); 39 | 40 | var following = await _context.Followings.SingleOrDefaultAsync(x => x.ObserverId == observer.Id && x.TargetId == target.Id); 41 | 42 | if (following == null) 43 | throw new RestException(HttpStatusCode.BadRequest, new { User = "You are not following this user" }); 44 | 45 | if (following != null) 46 | { 47 | _context.Followings.Remove(following); 48 | } 49 | 50 | var success = await _context.SaveChangesAsync() > 0; 51 | 52 | if (success) return Unit.Value; 53 | 54 | throw new Exception("Problem saving changes"); 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /Application/Followers/List.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Profiles; 6 | using Domain; 7 | using MediatR; 8 | using Microsoft.EntityFrameworkCore; 9 | using Persistence; 10 | 11 | namespace Application.Followers 12 | { 13 | public class List 14 | { 15 | public class Query : IRequest> 16 | { 17 | public string Username { get; set; } 18 | public string Predicate { get; set; } 19 | } 20 | 21 | public class Handler : IRequestHandler> 22 | { 23 | private readonly DataContext _context; 24 | private readonly IProfileReader _profileReader; 25 | public Handler(DataContext context, IProfileReader profileReader) 26 | { 27 | _profileReader = profileReader; 28 | _context = context; 29 | } 30 | 31 | public async Task> Handle(Query request, CancellationToken cancellationToken) 32 | { 33 | var queryable = _context.Followings.AsQueryable(); 34 | 35 | var userFollowings = new List(); 36 | var profiles = new List(); 37 | 38 | switch(request.Predicate) 39 | { 40 | case "followers": 41 | { 42 | userFollowings = await queryable.Where(x => 43 | x.Target.UserName == request.Username).ToListAsync(); 44 | 45 | foreach(var follower in userFollowings) 46 | { 47 | profiles.Add(await _profileReader.ReadProfile(follower.Observer.UserName)); 48 | } 49 | break; 50 | } 51 | case "following": 52 | { 53 | userFollowings = await queryable.Where(x => 54 | x.Observer.UserName == request.Username).ToListAsync(); 55 | 56 | foreach(var follower in userFollowings) 57 | { 58 | profiles.Add(await _profileReader.ReadProfile(follower.Target.UserName)); 59 | } 60 | break; 61 | } 62 | } 63 | 64 | return profiles; 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Application/Interfaces/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Application.Interfaces 4 | { 5 | public interface IEmailSender 6 | { 7 | Task SendEmailAsync(string userEmail, string emailSubject, string message); 8 | } 9 | } -------------------------------------------------------------------------------- /Application/Interfaces/IFacebookAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Application.User; 3 | 4 | namespace Application.Interfaces 5 | { 6 | public interface IFacebookAccessor 7 | { 8 | Task FacebookLogin(string accessToken); 9 | } 10 | } -------------------------------------------------------------------------------- /Application/Interfaces/IJwtGenerator.cs: -------------------------------------------------------------------------------- 1 | using Domain; 2 | 3 | namespace Application.Interfaces 4 | { 5 | public interface IJwtGenerator 6 | { 7 | string CreateToken(AppUser user); 8 | RefreshToken GenerateRefreshToken(); 9 | } 10 | } -------------------------------------------------------------------------------- /Application/Interfaces/IPhotoAccessor.cs: -------------------------------------------------------------------------------- 1 | using Application.Photos; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Application.Interfaces 5 | { 6 | public interface IPhotoAccessor 7 | { 8 | PhotoUploadResult AddPhoto(IFormFile file); 9 | string DeletePhoto(string publicId); 10 | } 11 | } -------------------------------------------------------------------------------- /Application/Interfaces/IUserAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Interfaces 2 | { 3 | public interface IUserAccessor 4 | { 5 | string GetCurrentUsername(); 6 | } 7 | } -------------------------------------------------------------------------------- /Application/Photos/Add.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Interfaces; 6 | using Domain; 7 | using MediatR; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.EntityFrameworkCore; 10 | using Persistence; 11 | 12 | namespace Application.Photos 13 | { 14 | public class Add 15 | { 16 | public class Command : IRequest 17 | { 18 | public IFormFile File { get; set; } 19 | } 20 | 21 | public class Handler : IRequestHandler 22 | { 23 | private readonly DataContext _context; 24 | private readonly IUserAccessor _userAccessor; 25 | private readonly IPhotoAccessor _photoAccessor; 26 | public Handler(DataContext context, IUserAccessor userAccessor, IPhotoAccessor photoAccessor) 27 | { 28 | _photoAccessor = photoAccessor; 29 | _userAccessor = userAccessor; 30 | _context = context; 31 | } 32 | 33 | public async Task Handle(Command request, CancellationToken cancellationToken) 34 | { 35 | var photoUploadResult = _photoAccessor.AddPhoto(request.File); 36 | 37 | var user = await _context.Users.SingleOrDefaultAsync(x => x.UserName == _userAccessor.GetCurrentUsername()); 38 | 39 | var photo = new Photo 40 | { 41 | Url = photoUploadResult.Url, 42 | Id = photoUploadResult.PublicId 43 | }; 44 | 45 | if (!user.Photos.Any(x => x.IsMain)) 46 | photo.IsMain = true; 47 | 48 | user.Photos.Add(photo); 49 | 50 | var success = await _context.SaveChangesAsync() > 0; 51 | 52 | if (success) return photo; 53 | 54 | throw new Exception("Problem saving changes"); 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /Application/Photos/Delete.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Application.Errors; 7 | using Application.Interfaces; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | using Persistence; 11 | 12 | namespace Application.Photos 13 | { 14 | public class Delete 15 | { 16 | public class Command : IRequest 17 | { 18 | public string Id { get; set; } 19 | } 20 | 21 | public class Handler : IRequestHandler 22 | { 23 | private readonly DataContext _context; 24 | private readonly IUserAccessor _userAccessor; 25 | private readonly IPhotoAccessor _photoAccessor; 26 | public Handler(DataContext context, IUserAccessor userAccessor, IPhotoAccessor photoAccessor) 27 | { 28 | _photoAccessor = photoAccessor; 29 | _userAccessor = userAccessor; 30 | _context = context; 31 | } 32 | 33 | public async Task Handle(Command request, CancellationToken cancellationToken) 34 | { 35 | var user = await _context.Users.SingleOrDefaultAsync(x => x.UserName == _userAccessor.GetCurrentUsername()); 36 | 37 | var photo = user.Photos.FirstOrDefault(x => x.Id == request.Id); 38 | 39 | if (photo == null) 40 | throw new RestException(HttpStatusCode.NotFound, new {Photo = "Not found"}); 41 | 42 | if (photo.IsMain) 43 | throw new RestException(HttpStatusCode.BadRequest, new {Photo = "You cannot delete your main photo"}); 44 | 45 | var result = _photoAccessor.DeletePhoto(photo.Id); 46 | 47 | if (result == null) 48 | throw new Exception("Problem deleting photo"); 49 | 50 | user.Photos.Remove(photo); 51 | 52 | var success = await _context.SaveChangesAsync() > 0; 53 | 54 | if (success) return Unit.Value; 55 | 56 | throw new Exception("Problem saving changes"); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Application/Photos/PhotoUploadResult.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Photos 2 | { 3 | public class PhotoUploadResult 4 | { 5 | public string PublicId { get; set; } 6 | public string Url { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /Application/Photos/SetMain.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Application.Errors; 7 | using Application.Interfaces; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | using Persistence; 11 | 12 | namespace Application.Photos 13 | { 14 | public class SetMain 15 | { 16 | public class Command : IRequest 17 | { 18 | public string Id { get; set; } 19 | } 20 | 21 | public class Handler : IRequestHandler 22 | { 23 | private readonly DataContext _context; 24 | private readonly IUserAccessor _userAccessor; 25 | public Handler(DataContext context, IUserAccessor userAccessor) 26 | { 27 | _userAccessor = userAccessor; 28 | _context = context; 29 | } 30 | 31 | public async Task Handle(Command request, CancellationToken cancellationToken) 32 | { 33 | var user = await _context.Users.SingleOrDefaultAsync(x => x.UserName == _userAccessor.GetCurrentUsername()); 34 | 35 | var photo = user.Photos.FirstOrDefault(x => x.Id == request.Id); 36 | 37 | if (photo == null) 38 | throw new RestException(HttpStatusCode.NotFound, new {Photo = "Not found"}); 39 | 40 | var currentMain = user.Photos.FirstOrDefault(x => x.IsMain); 41 | 42 | currentMain.IsMain = false; 43 | photo.IsMain = true; 44 | 45 | var success = await _context.SaveChangesAsync() > 0; 46 | 47 | if (success) return Unit.Value; 48 | 49 | throw new Exception("Problem saving changes"); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Application/Profiles/Details.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using MediatR; 5 | using Microsoft.EntityFrameworkCore; 6 | using Persistence; 7 | 8 | namespace Application.Profiles 9 | { 10 | public class Details 11 | { 12 | public class Query : IRequest 13 | { 14 | public string Username { get; set; } 15 | } 16 | 17 | public class Handler : IRequestHandler 18 | { 19 | private readonly IProfileReader _profileReader; 20 | public Handler(IProfileReader profileReader) 21 | { 22 | _profileReader = profileReader; 23 | } 24 | 25 | public async Task Handle(Query request, CancellationToken cancellationToken) 26 | { 27 | return await _profileReader.ReadProfile(request.Username); 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Application/Profiles/Edit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Application.Interfaces; 5 | using FluentValidation; 6 | using MediatR; 7 | using Microsoft.EntityFrameworkCore; 8 | using Persistence; 9 | 10 | namespace Application.Profiles 11 | { 12 | public class Edit 13 | { 14 | public class Command : IRequest 15 | { 16 | public string DisplayName { get; set; } 17 | public string Bio { get; set; } 18 | } 19 | 20 | public class CommandValidator : AbstractValidator 21 | { 22 | public CommandValidator() 23 | { 24 | RuleFor(x => x.DisplayName).NotEmpty(); 25 | } 26 | } 27 | 28 | public class Handler : IRequestHandler 29 | { 30 | private readonly DataContext _context; 31 | private readonly IUserAccessor _userAccessor; 32 | public Handler(DataContext context, IUserAccessor userAccessor) 33 | { 34 | _userAccessor = userAccessor; 35 | _context = context; 36 | } 37 | 38 | public async Task Handle(Command request, CancellationToken cancellationToken) 39 | { 40 | var user = await _context.Users.SingleOrDefaultAsync(x => x.UserName == _userAccessor.GetCurrentUsername()); 41 | 42 | user.DisplayName = request.DisplayName ?? user.DisplayName; 43 | user.Bio = request.Bio ?? user.Bio; 44 | 45 | var success = await _context.SaveChangesAsync() > 0; 46 | 47 | if (success) return Unit.Value; 48 | 49 | throw new Exception("Problem saving changes"); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Application/Profiles/IProfileReader.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Application.Profiles 4 | { 5 | public interface IProfileReader 6 | { 7 | Task ReadProfile(string username); 8 | } 9 | } -------------------------------------------------------------------------------- /Application/Profiles/ListActivities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Application.Errors; 8 | using MediatR; 9 | using Microsoft.EntityFrameworkCore; 10 | using Persistence; 11 | 12 | namespace Application.Profiles 13 | { 14 | public class ListActivities 15 | { 16 | public class Query : IRequest> 17 | { 18 | public string Username { get; set; } 19 | public string Predicate { get; set; } 20 | } 21 | 22 | public class Handler : IRequestHandler> 23 | { 24 | private readonly DataContext _context; 25 | public Handler(DataContext context) 26 | { 27 | _context = context; 28 | } 29 | 30 | public async Task> Handle(Query request, 31 | CancellationToken cancellationToken) 32 | { 33 | var user = await _context.Users.SingleOrDefaultAsync(x => x.UserName == request.Username); 34 | 35 | if (user == null) 36 | throw new RestException(HttpStatusCode.NotFound, new { User = "Not found" }); 37 | 38 | var queryable = user.UserActivities 39 | .OrderBy(a => a.Activity.Date) 40 | .AsQueryable(); 41 | 42 | switch (request.Predicate) 43 | { 44 | case "past": 45 | queryable = queryable.Where(a => a.Activity.Date <= DateTime.Now); 46 | break; 47 | case "hosting": 48 | queryable = queryable.Where(a => a.IsHost); 49 | break; 50 | default: 51 | queryable = queryable.Where(a => a.Activity.Date >= DateTime.Now); 52 | break; 53 | } 54 | 55 | var activities = queryable.ToList(); 56 | var activitiesToReturn = new List(); 57 | 58 | foreach (var activity in activities) 59 | { 60 | var userActivity = new UserActivityDto 61 | { 62 | Id = activity.Activity.Id, 63 | Title = activity.Activity.Title, 64 | Category = activity.Activity.Category, 65 | Date = activity.Activity.Date 66 | }; 67 | 68 | activitiesToReturn.Add(userActivity); 69 | } 70 | 71 | return activitiesToReturn; 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /Application/Profiles/Profile.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | using Domain; 4 | 5 | namespace Application.Profiles 6 | { 7 | public class Profile 8 | { 9 | public string DisplayName { get; set; } 10 | public string Username { get; set; } 11 | public string Image { get; set; } 12 | public string Bio { get; set; } 13 | 14 | [JsonPropertyName("following")] 15 | public bool IsFollowed { get; set; } 16 | public int FollowersCount { get; set; } 17 | public int FollowingCount { get; set; } 18 | public ICollection Photos { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /Application/Profiles/ProfileReader.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Application.Errors; 5 | using Application.Interfaces; 6 | using Microsoft.EntityFrameworkCore; 7 | using Persistence; 8 | 9 | namespace Application.Profiles 10 | { 11 | public class ProfileReader : IProfileReader 12 | { 13 | private readonly DataContext _context; 14 | private readonly IUserAccessor _userAccessor; 15 | public ProfileReader(DataContext context, IUserAccessor userAccessor) 16 | { 17 | _userAccessor = userAccessor; 18 | _context = context; 19 | } 20 | 21 | public async Task ReadProfile(string username) 22 | { 23 | var user = await _context.Users.SingleOrDefaultAsync(x => x.UserName == username); 24 | 25 | if (user == null) 26 | throw new RestException(HttpStatusCode.NotFound, new { User = "Not found" }); 27 | 28 | var currentUser = await _context.Users.SingleOrDefaultAsync(x => x.UserName == _userAccessor.GetCurrentUsername()); 29 | 30 | var profile = new Profile 31 | { 32 | DisplayName = user.DisplayName, 33 | Username = user.UserName, 34 | Image = user.Photos.FirstOrDefault(x => x.IsMain)?.Url, 35 | Photos = user.Photos, 36 | Bio = user.Bio, 37 | FollowersCount = user.Followers.Count(), 38 | FollowingCount = user.Followings.Count(), 39 | }; 40 | 41 | if (currentUser.Followings.Any(x => x.TargetId == user.Id)) 42 | { 43 | profile.IsFollowed = true; 44 | } 45 | 46 | return profile; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Application/Profiles/UserActivityDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Application.Profiles 4 | { 5 | public class UserActivityDto 6 | { 7 | public Guid Id { get; set; } 8 | public string Title { get; set; } 9 | public string Category { get; set; } 10 | public DateTime Date { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /Application/User/ConfirmEmail.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Domain; 5 | using FluentValidation; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.WebUtilities; 9 | 10 | namespace Application.User 11 | { 12 | public class ConfirmEmail 13 | { 14 | public class Command : IRequest 15 | { 16 | public string Token { get; set; } 17 | public string Email { get; set; } 18 | } 19 | 20 | public class CommandValidator : AbstractValidator 21 | { 22 | public CommandValidator() 23 | { 24 | RuleFor(x => x.Email).NotEmpty(); 25 | RuleFor(x => x.Token).NotEmpty(); 26 | } 27 | } 28 | 29 | public class Handler : IRequestHandler 30 | { 31 | private readonly UserManager _userManager; 32 | public Handler(UserManager userManager) 33 | { 34 | _userManager = userManager; 35 | } 36 | 37 | public async Task Handle(Command request, CancellationToken cancellationToken) 38 | { 39 | var user = await _userManager.FindByEmailAsync(request.Email); 40 | var decodedTokenBytes = WebEncoders.Base64UrlDecode(request.Token); 41 | var decodedToken = Encoding.UTF8.GetString(decodedTokenBytes); 42 | return await _userManager.ConfirmEmailAsync(user, decodedToken); 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Application/User/CurrentUser.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Application.Interfaces; 5 | using Domain; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Identity; 8 | using Persistence; 9 | 10 | namespace Application.User 11 | { 12 | public class CurrentUser 13 | { 14 | public class Query : IRequest { } 15 | 16 | public class Handler : IRequestHandler 17 | { 18 | private readonly UserManager _userManager; 19 | private readonly IJwtGenerator _jwtGenerator; 20 | private readonly IUserAccessor _userAccessor; 21 | public Handler(UserManager userManager, IJwtGenerator jwtGenerator, IUserAccessor userAccessor) 22 | { 23 | _userAccessor = userAccessor; 24 | _jwtGenerator = jwtGenerator; 25 | _userManager = userManager; 26 | } 27 | 28 | public async Task Handle(Query request, CancellationToken cancellationToken) 29 | { 30 | var user = await _userManager.FindByNameAsync(_userAccessor.GetCurrentUsername()); 31 | 32 | var refreshToken = _jwtGenerator.GenerateRefreshToken(); 33 | user.RefreshTokens.Add(refreshToken); 34 | await _userManager.UpdateAsync(user); 35 | 36 | return new User(user, _jwtGenerator, refreshToken.Token); 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Application/User/ExternalLogin.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using Application.Interfaces; 7 | using Domain; 8 | using MediatR; 9 | using Microsoft.AspNetCore.Identity; 10 | 11 | namespace Application.User 12 | { 13 | public class ExternalLogin 14 | { 15 | public class Query : IRequest 16 | { 17 | public string AccessToken { get; set; } 18 | } 19 | 20 | public class Handler : IRequestHandler 21 | { 22 | private readonly UserManager _userManager; 23 | private readonly IFacebookAccessor _facebookAccessor; 24 | private readonly IJwtGenerator _jwtGenerator; 25 | public Handler(UserManager userManager, IFacebookAccessor facebookAccessor, IJwtGenerator jwtGenerator) 26 | { 27 | _jwtGenerator = jwtGenerator; 28 | _facebookAccessor = facebookAccessor; 29 | _userManager = userManager; 30 | 31 | } 32 | 33 | public async Task Handle(Query request, CancellationToken cancellationToken) 34 | { 35 | var userInfo = await _facebookAccessor.FacebookLogin(request.AccessToken); 36 | 37 | if (userInfo == null) 38 | throw new RestException(HttpStatusCode.BadRequest, new {User = "Problem validating token"}); 39 | 40 | var user = await _userManager.FindByEmailAsync(userInfo.Email); 41 | 42 | var refreshToken = _jwtGenerator.GenerateRefreshToken(); 43 | 44 | if (user != null) 45 | { 46 | user.RefreshTokens.Add(refreshToken); 47 | await _userManager.UpdateAsync(user); 48 | return new User(user, _jwtGenerator, refreshToken.Token); 49 | } 50 | 51 | user = new AppUser 52 | { 53 | DisplayName = userInfo.Name, 54 | Id = userInfo.Id, 55 | Email = userInfo.Email, 56 | UserName = "fb_" + userInfo.Id, 57 | EmailConfirmed = true 58 | }; 59 | 60 | var photo = new Photo 61 | { 62 | Id = "fb_" + userInfo.Id, 63 | Url = userInfo.Picture.Data.Url, 64 | IsMain = true 65 | }; 66 | 67 | user.Photos.Add(photo); 68 | user.RefreshTokens.Add(refreshToken); 69 | 70 | var result = await _userManager.CreateAsync(user); 71 | 72 | if (!result.Succeeded) 73 | throw new RestException(HttpStatusCode.BadRequest, new {User = "Problem creating user"}); 74 | 75 | return new User(user, _jwtGenerator, refreshToken.Token); 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /Application/User/FacebookUserInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Application.User 2 | { 3 | public class FacebookUserInfo 4 | { 5 | public string Id { get; set; } 6 | public string Name { get; set; } 7 | public string Email { get; set; } 8 | public FacebookPictureData Picture { get; set; } 9 | } 10 | 11 | public class FacebookPictureData 12 | { 13 | public FacebookPicture Data { get; set; } 14 | } 15 | 16 | public class FacebookPicture 17 | { 18 | public string Url { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /Application/User/Login.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Application.Errors; 6 | using Application.Interfaces; 7 | using Domain; 8 | using FluentValidation; 9 | using MediatR; 10 | using Microsoft.AspNetCore.Identity; 11 | using Persistence; 12 | 13 | namespace Application.User 14 | { 15 | public class Login 16 | { 17 | public class Query : IRequest 18 | { 19 | public string Email { get; set; } 20 | public string Password { get; set; } 21 | } 22 | 23 | public class QueryValidator : AbstractValidator 24 | { 25 | public QueryValidator() 26 | { 27 | RuleFor(x => x.Email).NotEmpty(); 28 | RuleFor(x => x.Password).NotEmpty(); 29 | } 30 | } 31 | 32 | public class Handler : IRequestHandler 33 | { 34 | private readonly UserManager _userManager; 35 | private readonly SignInManager _signInManager; 36 | private readonly IJwtGenerator _jwtGenerator; 37 | public Handler(UserManager userManager, SignInManager signInManager, IJwtGenerator jwtGenerator) 38 | { 39 | _jwtGenerator = jwtGenerator; 40 | _signInManager = signInManager; 41 | _userManager = userManager; 42 | } 43 | 44 | public async Task Handle(Query request, CancellationToken cancellationToken) 45 | { 46 | var user = await _userManager.FindByEmailAsync(request.Email); 47 | 48 | if (user == null) 49 | throw new RestException(HttpStatusCode.Unauthorized); 50 | 51 | if (!user.EmailConfirmed) throw new RestException(HttpStatusCode.BadRequest, new {Email = "Email is not confirmed"}); 52 | 53 | var result = await _signInManager 54 | .CheckPasswordSignInAsync(user, request.Password, false); 55 | 56 | if (result.Succeeded) 57 | { 58 | var refreshToken = _jwtGenerator.GenerateRefreshToken(); 59 | user.RefreshTokens.Add(refreshToken); 60 | await _userManager.UpdateAsync(user); 61 | return new User(user, _jwtGenerator, refreshToken.Token); 62 | } 63 | 64 | throw new RestException(HttpStatusCode.Unauthorized); 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Application/User/RefreshToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Application.Errors; 7 | using Application.Interfaces; 8 | using Domain; 9 | using MediatR; 10 | using Microsoft.AspNetCore.Identity; 11 | 12 | namespace Application.User 13 | { 14 | public class RefreshToken 15 | { 16 | public class Command : IRequest 17 | { 18 | public string RefreshToken { get; set; } 19 | } 20 | 21 | public class Handler : IRequestHandler 22 | { 23 | private readonly UserManager _userManager; 24 | private readonly IJwtGenerator _jwtGenerator; 25 | private readonly IUserAccessor _userAccessor; 26 | public Handler(UserManager userManager, IJwtGenerator jwtGenerator, IUserAccessor userAccessor) 27 | { 28 | _userAccessor = userAccessor; 29 | _jwtGenerator = jwtGenerator; 30 | _userManager = userManager; 31 | } 32 | 33 | public async Task Handle(Command request, CancellationToken cancellationToken) 34 | { 35 | var user = await _userManager.FindByNameAsync(_userAccessor.GetCurrentUsername()); 36 | 37 | var oldToken = user.RefreshTokens.SingleOrDefault(x => x.Token == request.RefreshToken); 38 | 39 | if (oldToken != null && !oldToken.IsActive) throw new RestException(HttpStatusCode.Unauthorized); 40 | 41 | if (oldToken != null) 42 | { 43 | oldToken.Revoked = DateTime.UtcNow; 44 | } 45 | 46 | var newRefreshToken = _jwtGenerator.GenerateRefreshToken(); 47 | user.RefreshTokens.Add(newRefreshToken); 48 | 49 | await _userManager.UpdateAsync(user); 50 | 51 | return new User(user, _jwtGenerator, newRefreshToken.Token); 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Application/User/ResendEmailVerification.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Application.Interfaces; 5 | using Domain; 6 | using MediatR; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.WebUtilities; 9 | 10 | namespace Application.User 11 | { 12 | public class ResendEmailVerification 13 | { 14 | public class Query : IRequest 15 | { 16 | public string Email { get; set; } 17 | public string Origin { get; set; } 18 | } 19 | 20 | public class Handler : IRequestHandler 21 | { 22 | private readonly UserManager _userManager; 23 | private readonly IEmailSender _emailSender; 24 | public Handler(UserManager userManager, IEmailSender emailSender) 25 | { 26 | _emailSender = emailSender; 27 | _userManager = userManager; 28 | } 29 | 30 | public async Task Handle(Query request, CancellationToken cancellationToken) 31 | { 32 | var user = await _userManager.FindByEmailAsync(request.Email); 33 | 34 | var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); 35 | token = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(token)); 36 | 37 | var verifyUrl = $"{request.Origin}/user/verifyEmail?token={token}&email={request.Email}"; 38 | 39 | var message = $"

Please click the below link to verify your email address:

{verifyUrl}>

"; 40 | 41 | await _emailSender.SendEmailAsync(request.Email, "Please verify email address", message); 42 | 43 | return Unit.Value; 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Application/User/User.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Text.Json.Serialization; 3 | using Application.Interfaces; 4 | using Domain; 5 | 6 | namespace Application.User 7 | { 8 | public class User 9 | { 10 | public User(AppUser user, IJwtGenerator jwtGenerator, string refreshToken) 11 | { 12 | DisplayName = user.DisplayName; 13 | Token = jwtGenerator.CreateToken(user); 14 | Username = user.UserName; 15 | Image = user.Photos.FirstOrDefault(x => x.IsMain)?.Url; 16 | RefreshToken = refreshToken; 17 | 18 | } 19 | public string DisplayName { get; set; } 20 | public string Token { get; set; } 21 | public string Username { get; set; } 22 | public string Image { get; set; } 23 | 24 | [JsonIgnore] 25 | public string RefreshToken { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /Application/Validators/ValidatorExtensions.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace Application.Validators 4 | { 5 | public static class ValidatorExtensions 6 | { 7 | public static IRuleBuilder Password(this IRuleBuilder ruleBuilder) 8 | { 9 | var options = ruleBuilder 10 | .NotEmpty() 11 | .MinimumLength(6).WithMessage("Password must be at least 6 characters") 12 | .Matches("[A-Z]").WithMessage("Password must contain 1 uppercase letter") 13 | .Matches("[a-z]").WithMessage("Password must have at least 1 lowercase character") 14 | .Matches("[0-9]").WithMessage("Password must contain a number") 15 | .Matches("[^a-zA-Z0-9]").WithMessage("Password must contain non alphanumeric"); 16 | 17 | return options; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Domain/Activity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Domain 5 | { 6 | public class Activity 7 | { 8 | public Guid Id { get; set; } 9 | public string Title { get; set; } 10 | public string Description { get; set; } 11 | public string Category { get; set; } 12 | public DateTime Date { get; set; } 13 | public string City { get; set; } 14 | public string Venue { get; set; } 15 | public virtual ICollection UserActivities { get; set; } 16 | public virtual ICollection Comments { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /Domain/AppUser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using Microsoft.AspNetCore.Identity; 4 | 5 | namespace Domain 6 | { 7 | public class AppUser : IdentityUser 8 | { 9 | public AppUser() 10 | { 11 | Photos = new Collection(); 12 | } 13 | 14 | public string DisplayName { get; set; } 15 | public string Bio { get; set; } 16 | public virtual ICollection UserActivities { get; set; } 17 | public virtual ICollection Photos { get; set; } 18 | public virtual ICollection Followings { get; set; } 19 | public virtual ICollection Followers { get; set; } 20 | public virtual ICollection RefreshTokens { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /Domain/Comment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Domain 4 | { 5 | public class Comment 6 | { 7 | public Guid Id { get; set; } 8 | public string Body { get; set; } 9 | public virtual AppUser Author { get; set; } 10 | public virtual Activity Activity { get; set; } 11 | public DateTime CreatedAt { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.0 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Domain/Photo.cs: -------------------------------------------------------------------------------- 1 | namespace Domain 2 | { 3 | public class Photo 4 | { 5 | public string Id { get; set; } 6 | public string Url { get; set; } 7 | public bool IsMain { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /Domain/RefreshToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Domain 4 | { 5 | public class RefreshToken 6 | { 7 | public int Id { get; set; } 8 | public virtual AppUser AppUser { get; set; } 9 | public string Token { get; set; } 10 | public DateTime Expires { get; set; } = DateTime.UtcNow.AddDays(7); 11 | public bool IsExpired => DateTime.UtcNow >= Expires; 12 | public DateTime? Revoked { get; set; } 13 | public bool IsActive => Revoked == null & !IsExpired; 14 | } 15 | } -------------------------------------------------------------------------------- /Domain/UserActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Domain 4 | { 5 | public class UserActivity 6 | { 7 | public string AppUserId { get; set; } 8 | public virtual AppUser AppUser { get; set; } 9 | public Guid ActivityId { get; set; } 10 | public virtual Activity Activity { get; set; } 11 | public DateTime DateJoined { get; set; } 12 | public bool IsHost { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /Domain/UserFollowing.cs: -------------------------------------------------------------------------------- 1 | namespace Domain 2 | { 3 | public class UserFollowing 4 | { 5 | public string ObserverId { get; set; } 6 | public virtual AppUser Observer { get; set; } 7 | public string TargetId { get; set; } 8 | public virtual AppUser Target { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /Domain/Value.cs: -------------------------------------------------------------------------------- 1 | namespace Domain 2 | { 3 | public class Value 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Infrastructure/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Infrastructure 4 | { 5 | public class Class1 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Infrastructure/Email/EmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Application.Interfaces; 3 | using Microsoft.Extensions.Options; 4 | using SendGrid; 5 | using SendGrid.Helpers.Mail; 6 | 7 | namespace Infrastructure.Email 8 | { 9 | public class EmailSender : IEmailSender 10 | { 11 | private readonly IOptions _settings; 12 | public EmailSender(IOptions settings) 13 | { 14 | _settings = settings; 15 | } 16 | 17 | public async Task SendEmailAsync(string userEmail, string emailSubject, string message) 18 | { 19 | var client = new SendGridClient(_settings.Value.Key); 20 | var msg = new SendGridMessage 21 | { 22 | From = new EmailAddress("trycatchlearn@outlook.com", _settings.Value.User), 23 | Subject = emailSubject, 24 | PlainTextContent = message, 25 | HtmlContent = message 26 | }; 27 | msg.AddTo(new EmailAddress(userEmail)); 28 | msg.SetClickTracking(false, false); 29 | 30 | await client.SendEmailAsync(msg); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Infrastructure/Email/SendGridSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.Email 2 | { 3 | public class SendGridSettings 4 | { 5 | public string User { get; set; } 6 | public string Key { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | netcoreapp3.0 12 | 13 | -------------------------------------------------------------------------------- /Infrastructure/Photos/CloudinarySettings.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.Photos 2 | { 3 | public class CloudinarySettings 4 | { 5 | public string CloudName { get; set; } 6 | public string ApiKey { get; set; } 7 | public string ApiSecret { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /Infrastructure/Photos/PhotoAccessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Application.Interfaces; 3 | using Application.Photos; 4 | using CloudinaryDotNet; 5 | using CloudinaryDotNet.Actions; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace Infrastructure.Photos 10 | { 11 | public class PhotoAccessor : IPhotoAccessor 12 | { 13 | private readonly Cloudinary _cloudinary; 14 | public PhotoAccessor(IOptions config) 15 | { 16 | var acc = new Account 17 | ( 18 | config.Value.CloudName, 19 | config.Value.ApiKey, 20 | config.Value.ApiSecret 21 | ); 22 | 23 | _cloudinary = new Cloudinary(acc); 24 | } 25 | 26 | public PhotoUploadResult AddPhoto(IFormFile file) 27 | { 28 | var uploadResult = new ImageUploadResult(); 29 | 30 | if (file.Length > 0) 31 | { 32 | using (var stream = file.OpenReadStream()) 33 | { 34 | var uploadParams = new ImageUploadParams 35 | { 36 | File = new FileDescription(file.FileName, stream), 37 | Transformation = new Transformation().Height(500).Width(500).Crop("fill").Gravity("face") 38 | }; 39 | uploadResult = _cloudinary.Upload(uploadParams); 40 | } 41 | } 42 | 43 | if (uploadResult.Error != null) 44 | throw new Exception(uploadResult.Error.Message); 45 | 46 | return new PhotoUploadResult 47 | { 48 | PublicId = uploadResult.PublicId, 49 | Url = uploadResult.SecureUri.AbsoluteUri 50 | }; 51 | } 52 | 53 | public string DeletePhoto(string publicId) 54 | { 55 | var deleteParams = new DeletionParams(publicId); 56 | 57 | var result = _cloudinary.Destroy(deleteParams); 58 | 59 | return result.Result == "ok" ? result.Result : null; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Infrastructure/Security/FacebookAccessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Threading.Tasks; 5 | using Application.Interfaces; 6 | using Application.User; 7 | using Microsoft.Extensions.Options; 8 | using Newtonsoft.Json; 9 | 10 | namespace Infrastructure.Security 11 | { 12 | public class FacebookAccessor : IFacebookAccessor 13 | { 14 | private readonly HttpClient _httpClient; 15 | private readonly IOptions _config; 16 | public FacebookAccessor(IOptions config) 17 | { 18 | _config = config; 19 | _httpClient = new HttpClient 20 | { 21 | BaseAddress = new System.Uri("https://graph.facebook.com/") 22 | }; 23 | _httpClient.DefaultRequestHeaders 24 | .Accept 25 | .Add(new MediaTypeWithQualityHeaderValue("application/json")); 26 | } 27 | 28 | public async Task FacebookLogin(string accessToken) 29 | { 30 | // verify token is valid 31 | var verifyToken = await _httpClient.GetAsync($"debug_token?input_token={accessToken}&access_token={_config.Value.AppId}|{_config.Value.AppSecret}"); 32 | 33 | if (!verifyToken.IsSuccessStatusCode) 34 | return null; 35 | 36 | var result = await GetAsync(accessToken, "me", "fields=name,email,picture.width(100).height(100)"); 37 | 38 | return result; 39 | } 40 | 41 | private async Task GetAsync(string accessToken, string endpoint, string args) 42 | { 43 | var response = await _httpClient.GetAsync($"{endpoint}?access_token={accessToken}&{args}"); 44 | 45 | if (!response.IsSuccessStatusCode) 46 | return default(T); 47 | 48 | var result = await response.Content.ReadAsStringAsync(); 49 | 50 | return JsonConvert.DeserializeObject(result); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Infrastructure/Security/FacebookAppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Infrastructure.Security 2 | { 3 | public class FacebookAppSettings 4 | { 5 | public string AppId { get; set; } 6 | public string AppSecret { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /Infrastructure/Security/IsHostRequirement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Claims; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Http; 7 | using Persistence; 8 | 9 | namespace Infrastructure.Security 10 | { 11 | public class IsHostRequirement : IAuthorizationRequirement 12 | { 13 | } 14 | 15 | public class IsHostRequirementHandler : AuthorizationHandler 16 | { 17 | private readonly IHttpContextAccessor _httpContextAccessor; 18 | private readonly DataContext _context; 19 | public IsHostRequirementHandler(IHttpContextAccessor httpContextAccessor, DataContext context) 20 | { 21 | _context = context; 22 | _httpContextAccessor = httpContextAccessor; 23 | } 24 | 25 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsHostRequirement requirement) 26 | { 27 | var currentUserName = _httpContextAccessor.HttpContext.User?.Claims?.SingleOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; 28 | 29 | var activityId = Guid.Parse(_httpContextAccessor.HttpContext.Request.RouteValues.SingleOrDefault(x => x.Key == "id").Value.ToString()); 30 | 31 | var activity = _context.Activities.FindAsync(activityId).Result; 32 | 33 | var host = activity.UserActivities.FirstOrDefault(x => x.IsHost); 34 | 35 | if (host?.AppUser?.UserName == currentUserName) 36 | context.Succeed(requirement); 37 | 38 | return Task.CompletedTask; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Infrastructure/Security/JwtGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IdentityModel.Tokens.Jwt; 4 | using System.Security.Claims; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using Application.Interfaces; 8 | using Domain; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.IdentityModel.Tokens; 11 | 12 | namespace Infrastructure.Security 13 | { 14 | public class JwtGenerator : IJwtGenerator 15 | { 16 | private readonly SymmetricSecurityKey _key; 17 | public JwtGenerator(IConfiguration config) 18 | { 19 | _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])); 20 | } 21 | public string CreateToken(AppUser user) 22 | { 23 | var claims = new List 24 | { 25 | new Claim(JwtRegisteredClaimNames.NameId, user.UserName) 26 | }; 27 | 28 | // generate signing credentials 29 | var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature); 30 | 31 | var tokenDescriptor = new SecurityTokenDescriptor 32 | { 33 | Subject = new ClaimsIdentity(claims), 34 | Expires = DateTime.Now.AddMinutes(2), 35 | SigningCredentials = creds 36 | }; 37 | 38 | var tokenHandler = new JwtSecurityTokenHandler(); 39 | 40 | var token = tokenHandler.CreateToken(tokenDescriptor); 41 | 42 | return tokenHandler.WriteToken(token); 43 | } 44 | 45 | public RefreshToken GenerateRefreshToken() 46 | { 47 | var randomNumber = new byte[32]; 48 | using var rng = RandomNumberGenerator.Create(); 49 | rng.GetBytes(randomNumber); 50 | return new RefreshToken{ 51 | Token = Convert.ToBase64String(randomNumber) 52 | }; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Infrastructure/Security/UserAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Security.Claims; 3 | using Application.Interfaces; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Infrastructure.Security 7 | { 8 | public class UserAccessor : IUserAccessor 9 | { 10 | private readonly IHttpContextAccessor _httpContextAccessor; 11 | public UserAccessor(IHttpContextAccessor httpContextAccessor) 12 | { 13 | _httpContextAccessor = httpContextAccessor; 14 | } 15 | 16 | public string GetCurrentUsername() 17 | { 18 | var username = _httpContextAccessor.HttpContext.User?.Claims?.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value; 19 | 20 | return username; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Persistence/DataContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Domain; 3 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Persistence 7 | { 8 | public class DataContext : IdentityDbContext 9 | { 10 | public DataContext(DbContextOptions options) : base(options) 11 | { 12 | } 13 | 14 | public DbSet Values { get; set; } 15 | public DbSet Activities { get; set; } 16 | public DbSet UserActivities { get; set; } 17 | public DbSet Photos { get; set; } 18 | public DbSet Comments { get; set; } 19 | public DbSet Followings { get; set; } 20 | 21 | 22 | 23 | 24 | protected override void OnModelCreating(ModelBuilder builder) 25 | { 26 | base.OnModelCreating(builder); 27 | 28 | builder.Entity() 29 | .HasData( 30 | new Value { Id = 1, Name = "Value 101" }, 31 | new Value { Id = 2, Name = "Value 102" }, 32 | new Value { Id = 3, Name = "Value 103" } 33 | ); 34 | 35 | builder.Entity(x => x.HasKey(ua => 36 | new { ua.AppUserId, ua.ActivityId })); 37 | 38 | builder.Entity() 39 | .HasOne(u => u.AppUser) 40 | .WithMany(a => a.UserActivities) 41 | .HasForeignKey(u => u.AppUserId); 42 | 43 | builder.Entity() 44 | .HasOne(a => a.Activity) 45 | .WithMany(u => u.UserActivities) 46 | .HasForeignKey(a => a.ActivityId); 47 | 48 | builder.Entity(b => 49 | { 50 | b.HasKey(k => new { k.ObserverId, k.TargetId }); 51 | 52 | b.HasOne(o => o.Observer) 53 | .WithMany(f => f.Followings) 54 | .HasForeignKey(o => o.ObserverId) 55 | .OnDelete(DeleteBehavior.Restrict); 56 | 57 | b.HasOne(o => o.Target) 58 | .WithMany(f => f.Followers) 59 | .HasForeignKey(o => o.TargetId) 60 | .OnDelete(DeleteBehavior.Restrict); 61 | }); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Persistence/Migrations/20191114070541_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Persistence; 7 | 8 | namespace Persistence.Migrations 9 | { 10 | [DbContext(typeof(DataContext))] 11 | [Migration("20191114070541_InitialCreate")] 12 | partial class InitialCreate 13 | { 14 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "3.0.0"); 19 | 20 | modelBuilder.Entity("Domain.Value", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("INTEGER"); 25 | 26 | b.Property("Name") 27 | .HasColumnType("TEXT"); 28 | 29 | b.HasKey("Id"); 30 | 31 | b.ToTable("Values"); 32 | }); 33 | #pragma warning restore 612, 618 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Persistence/Migrations/20191114070541_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Persistence.Migrations 5 | { 6 | public partial class InitialCreate : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Values", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false) 15 | .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn) 16 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn) 17 | .Annotation("Sqlite:Autoincrement", true), 18 | Name = table.Column(nullable: true) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Values", x => x.Id); 23 | }); 24 | } 25 | 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.DropTable( 29 | name: "Values"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Persistence/Migrations/20191114074846_SeedValues.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Persistence; 7 | 8 | namespace Persistence.Migrations 9 | { 10 | [DbContext(typeof(DataContext))] 11 | [Migration("20191114074846_SeedValues")] 12 | partial class SeedValues 13 | { 14 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "3.0.0"); 19 | 20 | modelBuilder.Entity("Domain.Value", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("INTEGER"); 25 | 26 | b.Property("Name") 27 | .HasColumnType("TEXT"); 28 | 29 | b.HasKey("Id"); 30 | 31 | b.ToTable("Values"); 32 | 33 | b.HasData( 34 | new 35 | { 36 | Id = 1, 37 | Name = "Value 101" 38 | }, 39 | new 40 | { 41 | Id = 2, 42 | Name = "Value 102" 43 | }, 44 | new 45 | { 46 | Id = 3, 47 | Name = "Value 103" 48 | }); 49 | }); 50 | #pragma warning restore 612, 618 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Persistence/Migrations/20191114074846_SeedValues.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Persistence.Migrations 4 | { 5 | public partial class SeedValues : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.InsertData( 10 | table: "Values", 11 | columns: new[] { "Id", "Name" }, 12 | values: new object[] { 1, "Value 101" }); 13 | 14 | migrationBuilder.InsertData( 15 | table: "Values", 16 | columns: new[] { "Id", "Name" }, 17 | values: new object[] { 2, "Value 102" }); 18 | 19 | migrationBuilder.InsertData( 20 | table: "Values", 21 | columns: new[] { "Id", "Name" }, 22 | values: new object[] { 3, "Value 103" }); 23 | } 24 | 25 | protected override void Down(MigrationBuilder migrationBuilder) 26 | { 27 | migrationBuilder.DeleteData( 28 | table: "Values", 29 | keyColumn: "Id", 30 | keyValue: 1); 31 | 32 | migrationBuilder.DeleteData( 33 | table: "Values", 34 | keyColumn: "Id", 35 | keyValue: 2); 36 | 37 | migrationBuilder.DeleteData( 38 | table: "Values", 39 | keyColumn: "Id", 40 | keyValue: 3); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Persistence/Migrations/20191114085341_ActivityEntityAdded.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Persistence; 8 | 9 | namespace Persistence.Migrations 10 | { 11 | [DbContext(typeof(DataContext))] 12 | [Migration("20191114085341_ActivityEntityAdded")] 13 | partial class ActivityEntityAdded 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "3.0.0"); 20 | 21 | modelBuilder.Entity("Domain.Activity", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd() 25 | .HasColumnType("TEXT"); 26 | 27 | b.Property("Category") 28 | .HasColumnType("TEXT"); 29 | 30 | b.Property("City") 31 | .HasColumnType("TEXT"); 32 | 33 | b.Property("Date") 34 | .HasColumnType("TEXT"); 35 | 36 | b.Property("Description") 37 | .HasColumnType("TEXT"); 38 | 39 | b.Property("Title") 40 | .HasColumnType("TEXT"); 41 | 42 | b.Property("Venue") 43 | .HasColumnType("TEXT"); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.ToTable("Activities"); 48 | }); 49 | 50 | modelBuilder.Entity("Domain.Value", b => 51 | { 52 | b.Property("Id") 53 | .ValueGeneratedOnAdd() 54 | .HasColumnType("INTEGER"); 55 | 56 | b.Property("Name") 57 | .HasColumnType("TEXT"); 58 | 59 | b.HasKey("Id"); 60 | 61 | b.ToTable("Values"); 62 | 63 | b.HasData( 64 | new 65 | { 66 | Id = 1, 67 | Name = "Value 101" 68 | }, 69 | new 70 | { 71 | Id = 2, 72 | Name = "Value 102" 73 | }, 74 | new 75 | { 76 | Id = 3, 77 | Name = "Value 103" 78 | }); 79 | }); 80 | #pragma warning restore 612, 618 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Persistence/Migrations/20191114085341_ActivityEntityAdded.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Persistence.Migrations 5 | { 6 | public partial class ActivityEntityAdded : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Activities", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | Title = table.Column(nullable: true), 16 | Description = table.Column(nullable: true), 17 | Category = table.Column(nullable: true), 18 | Date = table.Column(nullable: false), 19 | City = table.Column(nullable: true), 20 | Venue = table.Column(nullable: true) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Activities", x => x.Id); 25 | }); 26 | } 27 | 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.DropTable( 31 | name: "Activities"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Persistence/Migrations/20191115071537_UserActivityAdded.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Persistence.Migrations 5 | { 6 | public partial class UserActivityAdded : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "UserActivities", 12 | columns: table => new 13 | { 14 | AppUserId = table.Column(nullable: false), 15 | ActivityId = table.Column(nullable: false), 16 | DateJoined = table.Column(nullable: false), 17 | IsHost = table.Column(nullable: false) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_UserActivities", x => new { x.AppUserId, x.ActivityId }); 22 | table.ForeignKey( 23 | name: "FK_UserActivities_Activities_ActivityId", 24 | column: x => x.ActivityId, 25 | principalTable: "Activities", 26 | principalColumn: "Id", 27 | onDelete: ReferentialAction.Cascade); 28 | table.ForeignKey( 29 | name: "FK_UserActivities_AspNetUsers_AppUserId", 30 | column: x => x.AppUserId, 31 | principalTable: "AspNetUsers", 32 | principalColumn: "Id", 33 | onDelete: ReferentialAction.Cascade); 34 | }); 35 | 36 | migrationBuilder.CreateIndex( 37 | name: "IX_UserActivities_ActivityId", 38 | table: "UserActivities", 39 | column: "ActivityId"); 40 | } 41 | 42 | protected override void Down(MigrationBuilder migrationBuilder) 43 | { 44 | migrationBuilder.DropTable( 45 | name: "UserActivities"); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Persistence/Migrations/20191116020804_PhotoEntityAdded.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Persistence.Migrations 4 | { 5 | public partial class PhotoEntityAdded : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.AddColumn( 10 | name: "Bio", 11 | table: "AspNetUsers", 12 | nullable: true); 13 | 14 | migrationBuilder.CreateTable( 15 | name: "Photos", 16 | columns: table => new 17 | { 18 | Id = table.Column(nullable: false), 19 | Url = table.Column(nullable: true), 20 | IsMain = table.Column(nullable: false), 21 | AppUserId = table.Column(nullable: true) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_Photos", x => x.Id); 26 | table.ForeignKey( 27 | name: "FK_Photos_AspNetUsers_AppUserId", 28 | column: x => x.AppUserId, 29 | principalTable: "AspNetUsers", 30 | principalColumn: "Id", 31 | onDelete: ReferentialAction.Restrict); 32 | }); 33 | 34 | migrationBuilder.CreateIndex( 35 | name: "IX_Photos_AppUserId", 36 | table: "Photos", 37 | column: "AppUserId"); 38 | } 39 | 40 | protected override void Down(MigrationBuilder migrationBuilder) 41 | { 42 | migrationBuilder.DropTable( 43 | name: "Photos"); 44 | 45 | migrationBuilder.DropColumn( 46 | name: "Bio", 47 | table: "AspNetUsers"); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Persistence/Migrations/20191116032259_AddedCommentEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Persistence.Migrations 5 | { 6 | public partial class AddedCommentEntity : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Comments", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | Body = table.Column(nullable: true), 16 | AuthorId = table.Column(nullable: true), 17 | ActivityId = table.Column(nullable: true), 18 | CreatedAt = table.Column(nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Comments", x => x.Id); 23 | table.ForeignKey( 24 | name: "FK_Comments_Activities_ActivityId", 25 | column: x => x.ActivityId, 26 | principalTable: "Activities", 27 | principalColumn: "Id", 28 | onDelete: ReferentialAction.Restrict); 29 | table.ForeignKey( 30 | name: "FK_Comments_AspNetUsers_AuthorId", 31 | column: x => x.AuthorId, 32 | principalTable: "AspNetUsers", 33 | principalColumn: "Id", 34 | onDelete: ReferentialAction.Restrict); 35 | }); 36 | 37 | migrationBuilder.CreateIndex( 38 | name: "IX_Comments_ActivityId", 39 | table: "Comments", 40 | column: "ActivityId"); 41 | 42 | migrationBuilder.CreateIndex( 43 | name: "IX_Comments_AuthorId", 44 | table: "Comments", 45 | column: "AuthorId"); 46 | } 47 | 48 | protected override void Down(MigrationBuilder migrationBuilder) 49 | { 50 | migrationBuilder.DropTable( 51 | name: "Comments"); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Persistence/Migrations/20191116055056_AddedFollowingEntity.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace Persistence.Migrations 4 | { 5 | public partial class AddedFollowingEntity : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.CreateTable( 10 | name: "Followings", 11 | columns: table => new 12 | { 13 | ObserverId = table.Column(nullable: false), 14 | TargetId = table.Column(nullable: false) 15 | }, 16 | constraints: table => 17 | { 18 | table.PrimaryKey("PK_Followings", x => new { x.ObserverId, x.TargetId }); 19 | table.ForeignKey( 20 | name: "FK_Followings_AspNetUsers_ObserverId", 21 | column: x => x.ObserverId, 22 | principalTable: "AspNetUsers", 23 | principalColumn: "Id", 24 | onDelete: ReferentialAction.Restrict); 25 | table.ForeignKey( 26 | name: "FK_Followings_AspNetUsers_TargetId", 27 | column: x => x.TargetId, 28 | principalTable: "AspNetUsers", 29 | principalColumn: "Id", 30 | onDelete: ReferentialAction.Restrict); 31 | }); 32 | 33 | migrationBuilder.CreateIndex( 34 | name: "IX_Followings_TargetId", 35 | table: "Followings", 36 | column: "TargetId"); 37 | } 38 | 39 | protected override void Down(MigrationBuilder migrationBuilder) 40 | { 41 | migrationBuilder.DropTable( 42 | name: "Followings"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Persistence/Migrations/20200812044106_RefreshToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Persistence.Migrations 5 | { 6 | public partial class RefreshToken : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "RefreshToken", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false) 15 | .Annotation("Sqlite:Autoincrement", true), 16 | AppUserId = table.Column(nullable: true), 17 | Token = table.Column(nullable: true), 18 | Expires = table.Column(nullable: false), 19 | Revoked = table.Column(nullable: true) 20 | }, 21 | constraints: table => 22 | { 23 | table.PrimaryKey("PK_RefreshToken", x => x.Id); 24 | table.ForeignKey( 25 | name: "FK_RefreshToken_AspNetUsers_AppUserId", 26 | column: x => x.AppUserId, 27 | principalTable: "AspNetUsers", 28 | principalColumn: "Id", 29 | onDelete: ReferentialAction.Restrict); 30 | }); 31 | 32 | migrationBuilder.CreateIndex( 33 | name: "IX_RefreshToken_AppUserId", 34 | table: "RefreshToken", 35 | column: "AppUserId"); 36 | } 37 | 38 | protected override void Down(MigrationBuilder migrationBuilder) 39 | { 40 | migrationBuilder.DropTable( 41 | name: "RefreshToken"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Persistence/Persistence.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | netcoreapp3.0 14 | 15 | -------------------------------------------------------------------------------- /client-app/.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=http://localhost:5000/api 2 | REACT_APP_API_CHAT_URL=http://localhost:5000/chat -------------------------------------------------------------------------------- /client-app/.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=/api 2 | REACT_APP_API_CHAT_URL=/chat -------------------------------------------------------------------------------- /client-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /client-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@microsoft/signalr": "^3.1.7", 7 | "@types/cropperjs": "1.1.5", 8 | "@types/jest": "26.0.9", 9 | "@types/node": "14.0.27", 10 | "@types/react": "16.9.46", 11 | "@types/react-dom": "16.9.8", 12 | "@types/react-facebook-login": "^4.1.2", 13 | "@types/react-infinite-scroller": "^1.2.1", 14 | "@types/react-router-dom": "^5.1.5", 15 | "@types/react-widgets": "^4.4.2", 16 | "@types/revalidate": "^1.1.2", 17 | "@types/uuid": "^8.3.0", 18 | "axios": "^0.19.2", 19 | "date-fns": "^2.15.0", 20 | "final-form": "^4.20.1", 21 | "mobx": "^5.15.5", 22 | "mobx-react-lite": "^2.0.7", 23 | "query-string": "^6.13.1", 24 | "react": "^16.13.1", 25 | "react-cropper": "1.2.0", 26 | "react-dom": "^16.13.1", 27 | "react-dropzone": "^11.0.3", 28 | "react-facebook-login": "^4.1.1", 29 | "react-final-form": "^6.5.1", 30 | "react-infinite-scroller": "^1.2.4", 31 | "react-router-dom": "^5.2.0", 32 | "react-scripts": "3.4.3", 33 | "react-toastify": "^6.0.8", 34 | "react-widgets": "^4.5.0", 35 | "react-widgets-date-fns": "^4.1.0", 36 | "revalidate": "^1.2.0", 37 | "semantic-ui-css": "^2.4.1", 38 | "semantic-ui-react": "^1.2.0", 39 | "typescript": "3.9.7", 40 | "uuid": "^8.3.0" 41 | }, 42 | "scripts": { 43 | "start": "react-scripts start", 44 | "build": "react-scripts build", 45 | "postbuild": "mv build ../API/wwwroot", 46 | "test": "react-scripts test", 47 | "eject": "react-scripts eject" 48 | }, 49 | "eslintConfig": { 50 | "extends": "react-app" 51 | }, 52 | "browserslist": { 53 | "production": [ 54 | ">0.2%", 55 | "not dead", 56 | "not op_mini all" 57 | ], 58 | "development": [ 59 | "last 1 chrome version", 60 | "last 1 firefox version", 61 | "last 1 safari version" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client-app/public/assets/categoryImages/culture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/assets/categoryImages/culture.jpg -------------------------------------------------------------------------------- /client-app/public/assets/categoryImages/drinks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/assets/categoryImages/drinks.jpg -------------------------------------------------------------------------------- /client-app/public/assets/categoryImages/film.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/assets/categoryImages/film.jpg -------------------------------------------------------------------------------- /client-app/public/assets/categoryImages/food.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/assets/categoryImages/food.jpg -------------------------------------------------------------------------------- /client-app/public/assets/categoryImages/music.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/assets/categoryImages/music.jpg -------------------------------------------------------------------------------- /client-app/public/assets/categoryImages/travel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/assets/categoryImages/travel.jpg -------------------------------------------------------------------------------- /client-app/public/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/assets/logo.png -------------------------------------------------------------------------------- /client-app/public/assets/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/assets/placeholder.png -------------------------------------------------------------------------------- /client-app/public/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/assets/user.png -------------------------------------------------------------------------------- /client-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/favicon.ico -------------------------------------------------------------------------------- /client-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | Reactivities 23 | 24 | 25 | 30 | 31 |
32 |
35 |
36 |
Loading app...
37 |
38 |
39 |
40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /client-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/logo192.png -------------------------------------------------------------------------------- /client-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TryCatchLearn/Reactivities30/29515e234efbe2f483bf9010d056f5e31a259f7a/client-app/public/logo512.png -------------------------------------------------------------------------------- /client-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /client-app/src/app/common/form/DateInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FieldRenderProps } from 'react-final-form'; 3 | import { FormFieldProps, Form, Label } from 'semantic-ui-react'; 4 | import {DateTimePicker} from 'react-widgets'; 5 | 6 | interface IProps 7 | extends FieldRenderProps, 8 | FormFieldProps {} 9 | 10 | const DateInput: React.FC = ({ 11 | id = null, 12 | input, 13 | width, 14 | placeholder, 15 | date = false, 16 | time = false, 17 | meta: { touched, error }, 18 | ...rest 19 | }) => { 20 | return ( 21 | 22 | e.preventDefault()} 28 | date={date} 29 | time={time} 30 | {...rest} 31 | /> 32 | {touched && error && ( 33 | 36 | )} 37 | 38 | ) 39 | } 40 | 41 | export default DateInput 42 | -------------------------------------------------------------------------------- /client-app/src/app/common/form/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AxiosResponse } from 'axios'; 3 | import { Message } from 'semantic-ui-react'; 4 | 5 | interface IProps { 6 | error: AxiosResponse; 7 | text?: string; 8 | } 9 | 10 | const ErrorMessage: React.FC = ({ error, text }) => { 11 | return ( 12 | 13 | {error.statusText} 14 | {error.data && Object.keys(error.data.errors).length > 0 && ( 15 | 16 | {Object.values(error.data.errors).flat().map((err: any, i) => ( 17 | {err} 18 | ))} 19 | 20 | )} 21 | {text && } 22 | 23 | ); 24 | }; 25 | 26 | export default ErrorMessage; 27 | -------------------------------------------------------------------------------- /client-app/src/app/common/form/SelectInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FieldRenderProps } from 'react-final-form'; 3 | import { FormFieldProps, Form, Label, Select } from 'semantic-ui-react'; 4 | 5 | interface IProps 6 | extends FieldRenderProps, 7 | FormFieldProps {} 8 | 9 | const SelectInput: React.FC = ({ 10 | input, 11 | width, 12 | options, 13 | placeholder, 14 | meta: { touched, error } 15 | }) => { 16 | return ( 17 | 18 |