From 49b4d346c59321e6d89660c636d4a1003cbe2112 Mon Sep 17 00:00:00 2001 From: trycatchlearn Date: Sun, 20 Sep 2020 13:23:14 +0700 Subject: [PATCH] PhotoManagementChallengeComplete --- API/Controllers/AdminController.cs | 61 ++- API/Controllers/UsersController.cs | 11 +- API/DTOs/PhotoDto.cs | 1 + API/DTOs/PhotoForApprovalDto.cs | 10 + API/Data/DataContext.cs | 8 +- .../20200920041909_PhotoApproval.Designer.cs | 454 ++++++++++++++++++ .../20200920041909_PhotoApproval.cs | 24 + .../Migrations/DataContextModelSnapshot.cs | 3 + API/Data/PhotoRepository.cs | 45 ++ API/Data/Seed.cs | 2 + API/Data/UnitOfWork.cs | 2 + API/Data/UserRepository.cs | 19 +- API/Entities/Photo.cs | 1 + API/Interfaces/IPhotoRepository.cs | 14 + API/Interfaces/IUnitOfWork.cs | 7 +- API/Interfaces/IUserRepository.cs | 3 +- API/datingapp.db | Bin 159744 -> 163840 bytes client/src/app/_models/photo.ts | 2 + client/src/app/_services/admin.service.ts | 13 + .../photo-management.component.css | 5 + .../photo-management.component.html | 12 +- .../photo-management.component.ts | 25 +- .../photo-editor/photo-editor.component.css | 13 + .../photo-editor/photo-editor.component.html | 15 +- 24 files changed, 722 insertions(+), 28 deletions(-) create mode 100644 API/DTOs/PhotoForApprovalDto.cs create mode 100644 API/Data/Migrations/20200920041909_PhotoApproval.Designer.cs create mode 100644 API/Data/Migrations/20200920041909_PhotoApproval.cs create mode 100644 API/Data/PhotoRepository.cs create mode 100644 API/Interfaces/IPhotoRepository.cs diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs index 1bd2856..78c2137 100644 --- a/API/Controllers/AdminController.cs +++ b/API/Controllers/AdminController.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Threading.Tasks; using API.Entities; +using API.Interfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; @@ -11,8 +12,13 @@ namespace API.Controllers public class AdminController : BaseApiController { private readonly UserManager _userManager; - public AdminController(UserManager userManager) + private readonly IUnitOfWork _unitOfWork; + private readonly IPhotoService _photoService; + public AdminController(UserManager userManager, IUnitOfWork unitOfWork, + IPhotoService photoService) { + _photoService = photoService; + _unitOfWork = unitOfWork; _userManager = userManager; } @@ -60,9 +66,58 @@ public async Task EditRoles(string username, [FromQuery] string ro [Authorize(Policy = "ModeratePhotoRole")] [HttpGet("photos-to-moderate")] - public ActionResult GetPhotosForModeration() + public async Task GetPhotosForModeration() { - return Ok("Admins or moderators can see this"); + var photos = await _unitOfWork.PhotoRepository.GetUnapprovedPhotos(); + + return Ok(photos); + } + + [Authorize(Policy = "ModeratePhotoRole")] + [HttpPost("approve-photo/{photoId}")] + public async Task ApprovePhoto(int photoId) + { + var photo = await _unitOfWork.PhotoRepository.GetPhotoById(photoId); + + if (photo == null) return NotFound("Could not find photo"); + + photo.IsApproved = true; + + var user = await _unitOfWork.UserRepository.GetUserByPhotoId(photoId); + + if (!user.Photos.Any(x => x.IsMain)) photo.IsMain = true; + + await _unitOfWork.Complete(); + + return Ok(); } + + [Authorize(Policy = "ModeratePhotoRole")] + [HttpPost("reject-photo/{photoId}")] + public async Task RejectPhoto(int photoId) + { + var photo = await _unitOfWork.PhotoRepository.GetPhotoById(photoId); + + if (photo == null) return NotFound("Could not find photo"); + + if (photo.PublicId != null) + { + var result = await _photoService.DeletePhotoAsync(photo.PublicId); + + if (result.Result == "ok") + { + _unitOfWork.PhotoRepository.RemovePhoto(photo); + } + } + else + { + _unitOfWork.PhotoRepository.RemovePhoto(photo); + } + + await _unitOfWork.Complete(); + + return Ok(); + } + } } \ No newline at end of file diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index f64432a..6c7e667 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -50,7 +50,10 @@ public async Task>> GetUsers([FromQuery] Use [HttpGet("{username}", Name = "GetUser")] public async Task> GetUser(string username) { - return await _unitOfWork.UserRepository.GetMemberAsync(username); + var currentUsername = User.GetUsername(); + return await _unitOfWork.UserRepository.GetMemberAsync(username, + isCurrentUser: currentUsername == username + ); } [HttpPut] @@ -83,11 +86,6 @@ public async Task> AddPhoto(IFormFile file) PublicId = result.PublicId }; - if (user.Photos.Count == 0) - { - photo.IsMain = true; - } - user.Photos.Add(photo); if (await _unitOfWork.Complete()) @@ -95,7 +93,6 @@ public async Task> AddPhoto(IFormFile file) return CreatedAtRoute("GetUser", new { username = user.UserName }, _mapper.Map(photo)); } - return BadRequest("Problem addding photo"); } diff --git a/API/DTOs/PhotoDto.cs b/API/DTOs/PhotoDto.cs index 8c4bded..d296cc0 100644 --- a/API/DTOs/PhotoDto.cs +++ b/API/DTOs/PhotoDto.cs @@ -5,5 +5,6 @@ public class PhotoDto public int Id { get; set; } public string Url { get; set; } public bool IsMain { get; set; } + public bool IsApproved { get; set; } } } \ No newline at end of file diff --git a/API/DTOs/PhotoForApprovalDto.cs b/API/DTOs/PhotoForApprovalDto.cs new file mode 100644 index 0000000..0619f0e --- /dev/null +++ b/API/DTOs/PhotoForApprovalDto.cs @@ -0,0 +1,10 @@ +namespace API.DTOs +{ + public class PhotoForApprovalDto + { + public int Id { get; set; } + public string Url { get; set; } + public string Username { get; set; } + public bool IsApproved { get; set; } + } +} \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index bc53901..0bd2325 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -19,6 +19,7 @@ public DataContext(DbContextOptions options) : base(options) public DbSet Likes { get; set; } public DbSet Messages { get; set; } + public DbSet Photos { get; set; } public DbSet Groups { get; set; } public DbSet Connections { get; set; } @@ -26,11 +27,6 @@ protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); - builder.Entity() - .HasMany(x => x.Connections) - .WithOne() - .OnDelete(DeleteBehavior.Cascade); - builder.Entity() .HasMany(ur => ur.UserRoles) .WithOne(u => u.User) @@ -69,6 +65,8 @@ protected override void OnModelCreating(ModelBuilder builder) .WithMany(m => m.MessagesSent) .OnDelete(DeleteBehavior.Restrict); + builder.Entity().HasQueryFilter(p => p.IsApproved); + builder.ApplyUtcDateTimeConverter(); } } diff --git a/API/Data/Migrations/20200920041909_PhotoApproval.Designer.cs b/API/Data/Migrations/20200920041909_PhotoApproval.Designer.cs new file mode 100644 index 0000000..4b3ce5c --- /dev/null +++ b/API/Data/Migrations/20200920041909_PhotoApproval.Designer.cs @@ -0,0 +1,454 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20200920041909_PhotoApproval")] + partial class PhotoApproval + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.0-preview.8.20407.4"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("City") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Country") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("DateOfBirth") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("Gender") + .HasColumnType("TEXT"); + + b.Property("Interests") + .HasColumnType("TEXT"); + + b.Property("Introduction") + .HasColumnType("TEXT"); + + b.Property("KnownAs") + .HasColumnType("TEXT"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("LookingFor") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.Connection", b => + { + b.Property("ConnectionId") + .HasColumnType("TEXT"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("ConnectionId"); + + b.HasIndex("GroupName"); + + b.ToTable("Connections"); + }); + + modelBuilder.Entity("API.Entities.Group", b => + { + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Name"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("API.Entities.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("DateRead") + .HasColumnType("TEXT"); + + b.Property("MessageSent") + .HasColumnType("TEXT"); + + b.Property("RecipientDeleted") + .HasColumnType("INTEGER"); + + b.Property("RecipientId") + .HasColumnType("INTEGER"); + + b.Property("RecipientUsername") + .HasColumnType("TEXT"); + + b.Property("SenderDeleted") + .HasColumnType("INTEGER"); + + b.Property("SenderId") + .HasColumnType("INTEGER"); + + b.Property("SenderUsername") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RecipientId"); + + b.HasIndex("SenderId"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("API.Entities.Photo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("IsApproved") + .HasColumnType("INTEGER"); + + b.Property("IsMain") + .HasColumnType("INTEGER"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Photos"); + }); + + modelBuilder.Entity("API.Entities.UserLike", b => + { + b.Property("SourceUserId") + .HasColumnType("INTEGER"); + + b.Property("LikedUserId") + .HasColumnType("INTEGER"); + + b.HasKey("SourceUserId", "LikedUserId"); + + b.HasIndex("LikedUserId"); + + b.ToTable("Likes"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.Connection", b => + { + b.HasOne("API.Entities.Group", null) + .WithMany("Connections") + .HasForeignKey("GroupName"); + }); + + modelBuilder.Entity("API.Entities.Message", b => + { + b.HasOne("API.Entities.AppUser", "Recipient") + .WithMany("MessagesReceived") + .HasForeignKey("RecipientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "Sender") + .WithMany("MessagesSent") + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.Photo", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Photos") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.UserLike", b => + { + b.HasOne("API.Entities.AppUser", "LikedUser") + .WithMany("LikedByUsers") + .HasForeignKey("LikedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "SourceUser") + .WithMany("LikedUsers") + .HasForeignKey("SourceUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20200920041909_PhotoApproval.cs b/API/Data/Migrations/20200920041909_PhotoApproval.cs new file mode 100644 index 0000000..228996f --- /dev/null +++ b/API/Data/Migrations/20200920041909_PhotoApproval.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class PhotoApproval : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsApproved", + table: "Photos", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsApproved", + table: "Photos"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 60c2af8..b199bbf 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -232,6 +232,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AppUserId") .HasColumnType("INTEGER"); + b.Property("IsApproved") + .HasColumnType("INTEGER"); + b.Property("IsMain") .HasColumnType("INTEGER"); diff --git a/API/Data/PhotoRepository.cs b/API/Data/PhotoRepository.cs new file mode 100644 index 0000000..67963b7 --- /dev/null +++ b/API/Data/PhotoRepository.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.DTOs; +using API.Entities; +using API.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace API.Data +{ + public class PhotoRepository : IPhotoRepository + { + private readonly DataContext _context; + public PhotoRepository(DataContext context) + { + _context = context; + } + + public async Task GetPhotoById(int id) + { + return await _context.Photos + .IgnoreQueryFilters() + .SingleOrDefaultAsync(x => x.Id == id); + } + + public async Task> GetUnapprovedPhotos() + { + return await _context.Photos + .IgnoreQueryFilters() + .Where(p => p.IsApproved == false) + .Select(u => new PhotoForApprovalDto + { + Id = u.Id, + Username = u.AppUser.UserName, + Url = u.Url, + IsApproved = u.IsApproved + }).ToListAsync(); + } + + public void RemovePhoto(Photo photo) + { + _context.Photos.Remove(photo); + } + } +} \ No newline at end of file diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 3acd01f..40a4f4f 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Text.Json; using System.Threading.Tasks; using API.Entities; @@ -32,6 +33,7 @@ public static async Task SeedUsers(UserManager userManager, foreach (var user in users) { + user.Photos.First().IsApproved = true; user.UserName = user.UserName.ToLower(); await userManager.CreateAsync(user, "Pa$$w0rd"); await userManager.AddToRoleAsync(user, "Member"); diff --git a/API/Data/UnitOfWork.cs b/API/Data/UnitOfWork.cs index e72f623..6354366 100644 --- a/API/Data/UnitOfWork.cs +++ b/API/Data/UnitOfWork.cs @@ -20,6 +20,8 @@ public UnitOfWork(DataContext context, IMapper mapper) public ILikesRepository LikesRepository => new LikesRepository(_context); + public IPhotoRepository PhotoRepository => new PhotoRepository(_context); + public async Task Complete() { return await _context.SaveChangesAsync() > 0; diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index a381ada..08af609 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -22,12 +22,16 @@ public UserRepository(DataContext context, IMapper mapper) _context = context; } - public async Task GetMemberAsync(string username) + public async Task GetMemberAsync(string username, bool isCurrentUser) { - return await _context.Users + var query = _context.Users .Where(x => x.UserName == username) .ProjectTo(_mapper.ConfigurationProvider) - .SingleOrDefaultAsync(); + .AsQueryable(); + + if (isCurrentUser) query = query.IgnoreQueryFilters(); + + return await query.FirstOrDefaultAsync(); } public async Task> GetMembersAsync(UserParams userParams) @@ -58,6 +62,15 @@ public async Task GetUserByIdAsync(int id) return await _context.Users.FindAsync(id); } + public async Task GetUserByPhotoId(int photoId) + { + return await _context.Users + .Include(p => p.Photos) + .IgnoreQueryFilters() + .Where(p => p.Photos.Any(p => p.Id == photoId)) + .FirstOrDefaultAsync(); + } + public async Task GetUserByUsernameAsync(string username) { return await _context.Users diff --git a/API/Entities/Photo.cs b/API/Entities/Photo.cs index 0d92f4c..3af59d4 100644 --- a/API/Entities/Photo.cs +++ b/API/Entities/Photo.cs @@ -8,6 +8,7 @@ public class Photo public int Id { get; set; } public string Url { get; set; } public bool IsMain { get; set; } + public bool IsApproved { get; set; } public string PublicId { get; set; } public AppUser AppUser { get; set; } public int AppUserId { get; set; } diff --git a/API/Interfaces/IPhotoRepository.cs b/API/Interfaces/IPhotoRepository.cs new file mode 100644 index 0000000..dd6c250 --- /dev/null +++ b/API/Interfaces/IPhotoRepository.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using API.DTOs; +using API.Entities; + +namespace API.Interfaces +{ + public interface IPhotoRepository + { + Task> GetUnapprovedPhotos(); + Task GetPhotoById(int id); + void RemovePhoto(Photo photo); + } +} \ No newline at end of file diff --git a/API/Interfaces/IUnitOfWork.cs b/API/Interfaces/IUnitOfWork.cs index d818a86..b1e5be3 100644 --- a/API/Interfaces/IUnitOfWork.cs +++ b/API/Interfaces/IUnitOfWork.cs @@ -4,9 +4,10 @@ namespace API.Interfaces { public interface IUnitOfWork { - IUserRepository UserRepository {get; } - IMessageRepository MessageRepository {get;} - ILikesRepository LikesRepository {get; } + IUserRepository UserRepository { get; } + IMessageRepository MessageRepository { get; } + ILikesRepository LikesRepository { get; } + IPhotoRepository PhotoRepository { get; } Task Complete(); bool HasChanges(); } diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs index 90f0047..bce01c4 100644 --- a/API/Interfaces/IUserRepository.cs +++ b/API/Interfaces/IUserRepository.cs @@ -13,7 +13,8 @@ public interface IUserRepository Task GetUserByIdAsync(int id); Task GetUserByUsernameAsync(string username); Task> GetMembersAsync(UserParams userParams); - Task GetMemberAsync(string username); + Task GetMemberAsync(string username, bool isCurrentUser); Task GetUserGender(string username); + Task GetUserByPhotoId(int photoId); } } \ No newline at end of file diff --git a/API/datingapp.db b/API/datingapp.db index cf0589cb0896ffa0ae16f4ca20d7a1ba92a4c9a4..6a4605c8777ccaa2407be946c47f3426b628918f 100644 GIT binary patch delta 4742 zcmeHLYm6kcPmk-XTRT% znQnw;<3vPJf=Dz*O(fz(f`F7D1vB7Lh_Mnywh^9!?%7o~ zut6e-KZvWUQq{TloOAEF=R5bDx^Z*k#*NKuxlE!yGIy=+pskK}8QYGHAZ$S zz_$Bar&o6W+Nmj2v#FiOU0}uW zZo)V0CCyQ`+cJj^ycn=?PACk`!KU4DnOM$mcwvihJnWRs;ZUub!_8CTdvCw%DPrYO z06uR6{|3GaejB_6z6ZVu-T<$GSHYiwKLIb|DgPe$Jl6MN@X)G*AY2z#92*2!yl%Uf zPuGI5b@JlzdmnLuuPg-pGx$gFZSeQtZ?KrZ0)GKs27d%z0AImEo(8Kc4jbV*ec_iF zVvmLh1~H7tu%AA6NeT~ zT9nz61URd>y=T`oL!!V8iLSNW^@}pzGvg zU!1NH;Fb;C!jAY&@W3Wve^~2k_^^GQ zIeTiC_{`eM)`y7B8UGlOT(J|>*+)FYHDdEa_wTM>x_p`V1;_m0G2(Bxb{~xn&!dxh z(%-)S8x!2JK&izXY#Pr!mJ2tgo#Q8-&X} z|J*V0fi)a=ZQxbh@e1%>z4&mpRg+qgaxj(}81-bTk`aVK>Tt0ackjuP=^L2ob@0H0 z_hdPH`!mGq6A!t;m+l>To(Eq#d*mkZsYT$~Alzrqf0qdD{@TjTokzje4I6j~{36JK z%kJ0Rzv=F}dDq*nmt48+*S70hZ*JYd@xHQkJiOo}Y#Zo<;)bi%>K~fs7V#_7Mx);Fy4O1}hx>Y~J2vVabuiu6yKS%0U=G{kDbw=S+c@HS$GuipHM|p=qbMZM z3`cwKC2_yes}6v3!{%HlJs!5&(=JqnNq1uP)gF5^8ls<6DUaO@dhaE1p>*iRD_tmc zZV#HZX{{j~jcdov>9}joI%>=OiM0w)q`W^A`9q0It*$%v-9BnrjnS}iI6G7idy~lw zj`i8`|3IridG951Y2d9p1|Chc4y?`e7V5W9V*(kqc667{!hdU5d7fjC_g*3wO1aH- z>`J!!#^(IOzWpi?nI(1OHg&$UZ})C;&fA~P<)C?Q-@dlVt=}fDFNr$>zvNuIO_#arO}Cs%g<;N;~` z{M7#+{r{(r{`dcH9sSso9@{>9D`kIi<=s3dm@17k4=8Z>s8xBKt3iD7YAUT!Dujl` zoxPs7zhKwp=qPhUk1EN0y)tGyTq6{q3No*%oq?a7aAgkWk9hyInN&TMe$rIbd?KO9 zF;2({<0BN(arT&tW;h6oj4X!|p@{5@hoiAfUQ)O~P$?zC{`q>zer=DTSd+3unq)=6 z!nqNGq()f+DN?K|&`^VrH_nuj{W*soP`d3ygJU8?Gu4wi>9A=AJFMP@fx}{g9%!0@ zvm!2>l*fY>@_U$ZymBN~+o)NldPgC8h|{$+$M|ybY$cMFqM1sm%%)@|kx8O-xKc?+ ziqd@YK0CQ*XpEs#2&dK>W0I^OaHPNq8Yu{r%IXHCGrD#5();XxYCjGrfs!XD!)iPk zU_+H^NFQV)44P(?f>~_!jAXhK4ST{_fio2!&s!xtEES`J9nlvyl|nglaFB}fp?oDF zF({{Gm_jJP6oPzFQRGAprKLc4Fdo(FrVH=ms_h51t^V2%9PhAi;aA*F-m~+@@ssZD zcRDW@F8mB*b6&*6`+OUV=LeC^DLd~=bUQ}3V?l#5Xig*r3n7x#@w)JSOT+6FdmX}kif3tt76n97G%hoCDeC(2YU9&Q>S~a>^kaDDpHS@= z^@v%d*ml+6vMEXFDL&dejMf>xL`|)d#mnhXq!tZDeD#`c97S7PF~lbNrFwnVNOYT? zkaW0kB-U4suuLVzWs1=dEQj)WHp@#)vXU%Sm{4Ty)a+N@E7GQln@J)@*GQILRz{YH zeVd~2>P^UK=H2U~uS2{NVV`3tNOLEu*sIbm}lJSk9##= zE6Vix5mC+!y=WX9`C7;u_4Ee5a2}zK!u8{U!<@{bT*qUmCDrOoEnys$aJF7%6)9Pg zvk@f}zE6P#87dW}5=p~MO3unGKi}4|L*jf^TvT`@E*EgsNAcJfF>pwpS4CE2=tb#w z_I}-t9C|37E2-lnZ$9e@rs+mjhQY+Nl8mzTUMyYhkMjx3tPkSViFy=Cgv#bM{2Z6le9*22v?Stj+p<5T!gMeaUZgbD2lWw@+7rD?=FR| z;~t7xKsTm#%pY-hxaeDQu@prw>c9Ww?{cS&#zwcF%*JZgk-sydVm;OHhsM23v65pU zT`Ux7wwE@JiiwUF7N`EcXBg6HPt98@2+cgkayK&qowVAOT6bqzoZ}07WjQ8wH>;WH?&%qUa^APJ@AqoAoN{JnXCIPw zSNqVDqSj7GQE;Jbmu!?27`qDY4^mK$BVtu>IG|iU#{uIQsEQo}F_n-2DPlq{$FmB0 zR$4`O67U!QP>iHcJNtG2`g`@)-`9^lHu~6mwkzpmq}J7)C^Lldah5;_US~Qf<7JZ)3H8v^r^j1NBh*)rz3qj z+^1*ybjYB?R%2NU{WxcKFcyo=cA(KU6V>Fq;M?G@z@LM!fj^Om~Zh$X>-=_L} z7F-96Z+sxwz88e8( zAiID3zIpmeAI4(5Gggb)<}gGW1vd>N-=>TG9rzabOYk-o>W{!py6|Vg&duIO?stG! zjS>F@{t^5=_#5y|I_4elXW&in74Rw@bsg;d{u9={*Poa%PfI$dK_nqoAVgu2EJ>_N zB!wkJ6%<_~0#}B6_doUH7A@%W6ufA0Rm25YES4Jcf~=8lceSN@0yVF$Uf$m9CcWwS zM$=dEhNLA_PcPSJs;!m>#c(YkyXWST&>ct{)Qm=h!pQHgX2S)_10tCl#-a)KZsNg-u~xV`T^y>3yx-L|LEY1G#gT;AsMLb_X2 zx4eyXpgdm^q+Dv#D_3JH!C*_;PS)M)xXG(nZO zScy|Hs~|zdf`DaBSNF!A{S?#lb+@Hfs9acE%H^BBLVDecxtW|o(s^ExHeqOMdxL~l z*DqJL`1&?<`?O+Y(;Mc!dGG4w$eO&cF*g^B&11hOkV}X1*dO&0A?U*ye=aGe-BIib z7FTK=#Ry9WpgU!U?&@d3_rO1ccWG$u?cMi$6+D%&?_7If%V7nto4_p^kMDrjX%v1D z1jnt+gT{2Wvs`O5^!AvAd7%Hk+H80A_9(+#RGTWh(yVnx&4-^4SF;+M;1vt&ocH;>_6=^ zKX=CZ@nz@mK6%I-vog$U`#*Qte8s%~!ddgKc|YVb{|^7bG4P^^=AIvd_rc%N&HfkE zCU1d11h3K@wGUq0;~#qx?0&v!PSJtaje)mlR`@ne&2Q6TU!ke&OW-$YD!Wd7@(Hke zy=Q)ux=J-s_KV;bfJ(nkf!>F#R(bN`g1@P{-OXY&vz^W6LZa^8(3UEOk^6?F{~xqi zDwQw|fY}kj) z#;UX5`FvGhS!oKKz_Fan^0JfXW;uQq5f>yF3jFCDh(yUSorBsrbgzBVZMsUK zC}3CbbWcoyqKoiE;N=5O@7Owh7DmJXG5{l!2cq@CIwksZ0!|!$dZ$GXI9NtR za`X;$4WBzrx&z@jc@|5vJQYENJUQ-#V;r7fejr@BSVVY$ z7Mb{O+;BLDcVU`thrPeg)fyFNTUSb# z=yPHrXQ?JtJ1RjJgjz9!y6?C}Adb0BX zu+O5gd=&kUI_z^tfQGPCCugnFXtoWJ>$a zdxIUZQPt@hhwGe*%)3NEfT#FGFgTn!+Y&Bk+t=GcfhQ(!9Z;PUyR?XC#`~$T8nl^(`k|;^Z{W zg(Mn!cke%OmNz(1hDR|O89ugS?#`*y-8`IyoQrNhne^@o@k17rsi_B!ikun1*=&<{ z=Rpq7a&VR>G^=v7qCGD55r_H=A8o+c&~asZHAC4uBELV4BQ8$H@}1tM9AOqC>NZ}I zj{vdNK4mhzeSzlu6WXF#0nhS+ixUV&c=(_YGmM_J*dK74dRLE~JWi#e)EdWCptF&Y z4R}dJ!X2(W;2;;x*F+v1KM`hNssBP0G>b$R&qGeW%mLEN6lp{>pRrMi z>jz)mnqekrsrSY~A^mmm2CbTY7kH2AWa@S6@s}S}(3Hd2Ef;Mx7Fok!L)2IdxJ{|) zAzEPG-nlgKdpmDF`QiU>FaJ-smmmFKZ7=t4$C;bf2lx`*u(Y42t(MBO^aentEfbGe zQIIsum$4v8``^eguQHmiR?wPlIht8ZmJ)UEywB6Ftff1t?&XXoY8ElII( zKC_nYiUmFFr=8i#+GgIGEM91etwbpl0{48vNMf_zg5~utrv^y^_dN& zUdU&~GT+eG3R|I4T__}$a7PiOR+$tU8_*Np+VsqKn}N&e=zN;Ww6K6ZLJH!TXWo|= z{X|TpeJL@PFW^illJbOi*B@cnXjYXl(ZRev4xo|Y0CuaEVDz!u)yeZ6NiQo%0q{fD#ru(p^}R|2tk zvE|>amPF$wCz~MYcsvMGUNMN`Vj`Q3#9=m?$s~L+!Q*SMb*nYq$e}}~e=*rxXa3c4 zjkG!h*-A89s9%?2-EmG;;oi#c3k zk!p0>gr|)a|cPlFf>Re2NtO9vBRV<|FYCF64`TFAN4E3%esKbLk_9 z$f8QicNUlFjRMjYkyTWU1{NEn01z1rcY)tI1qee>RAG1pF^jeND%>27d2l*CHhoy*4RD(Y;dQ)R!r>?i)5SmmpA zF_%^KmE>xz5v1Pt#IvG^Q^iEY7dW4fCKiL~Y#L_#9zpOVbKc0VLo*_>q^T<8HC9vU zwFiA18jVnRI(MwK|h7{ry;}c|MOfNfiNrkY)@B1puEA%q2wJK+%Oc#ba z+nH2JDN=7Pdm8Z?mx~7@;jKz!rQK0+3MNX+@v^%(+HBCDKCQ;tSTsV*uamJSuNiiOF2T*c8llL#wm96G~0y`^uF#rGn diff --git a/client/src/app/_models/photo.ts b/client/src/app/_models/photo.ts index 3a4e368..179a9dd 100644 --- a/client/src/app/_models/photo.ts +++ b/client/src/app/_models/photo.ts @@ -2,4 +2,6 @@ export interface Photo { id: number; url: string; isMain: boolean; + isApproved: boolean; + username?: string; } diff --git a/client/src/app/_services/admin.service.ts b/client/src/app/_services/admin.service.ts index 9fb1d3a..c5ffe20 100644 --- a/client/src/app/_services/admin.service.ts +++ b/client/src/app/_services/admin.service.ts @@ -1,6 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { environment } from 'src/environments/environment'; +import { Photo } from '../_models/photo'; import { User } from '../_models/user'; @Injectable({ @@ -18,4 +19,16 @@ export class AdminService { updateUserRoles(username: string, roles: string[]) { return this.http.post(this.baseUrl + 'admin/edit-roles/' + username + '?roles=' + roles, {}); } + + getPhotosForApproval() { + return this.http.get(this.baseUrl + 'admin/photos-to-moderate'); + } + + approvePhoto(photoId: number) { + return this.http.post(this.baseUrl + 'admin/approve-photo/' + photoId, {}); + } + + rejectPhoto(photoId: number) { + return this.http.post(this.baseUrl + 'admin/reject-photo/' + photoId, {}); + } } diff --git a/client/src/app/admin/photo-management/photo-management.component.css b/client/src/app/admin/photo-management/photo-management.component.css index e69de29..e1ea2c9 100644 --- a/client/src/app/admin/photo-management/photo-management.component.css +++ b/client/src/app/admin/photo-management/photo-management.component.css @@ -0,0 +1,5 @@ +img.img-thumbnail { + height: 150px; + min-width: 150px !important; + margin-bottom: 2px; + } \ No newline at end of file diff --git a/client/src/app/admin/photo-management/photo-management.component.html b/client/src/app/admin/photo-management/photo-management.component.html index 407f884..52222ff 100644 --- a/client/src/app/admin/photo-management/photo-management.component.html +++ b/client/src/app/admin/photo-management/photo-management.component.html @@ -1 +1,11 @@ -

photo-management works!

+
+
+

{{photo.username}}

+ {{photo.username}} + +
+ + +
+
+
\ No newline at end of file diff --git a/client/src/app/admin/photo-management/photo-management.component.ts b/client/src/app/admin/photo-management/photo-management.component.ts index ef62455..d9c46f2 100644 --- a/client/src/app/admin/photo-management/photo-management.component.ts +++ b/client/src/app/admin/photo-management/photo-management.component.ts @@ -1,4 +1,6 @@ import { Component, OnInit } from '@angular/core'; +import { Photo } from 'src/app/_models/photo'; +import { AdminService } from 'src/app/_services/admin.service'; @Component({ selector: 'app-photo-management', @@ -6,10 +8,31 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./photo-management.component.css'] }) export class PhotoManagementComponent implements OnInit { + photos: Photo[]; - constructor() { } + constructor(private adminService: AdminService) { } ngOnInit(): void { + this.getPhotosForApproval(); } + getPhotosForApproval() { + this.adminService.getPhotosForApproval().subscribe(photos => { + this.photos = photos; + }) + } + + approvePhoto(photoId) { + this.adminService.approvePhoto(photoId).subscribe(() => { + this.photos.splice(this.photos.findIndex(p => p.id === photoId), 1); + }) + } + + rejectPhoto(photoId) { + this.adminService.rejectPhoto(photoId).subscribe(() => { + this.photos.splice(this.photos.findIndex(p => p.id === photoId), 1); + }) + } + + } diff --git a/client/src/app/members/photo-editor/photo-editor.component.css b/client/src/app/members/photo-editor/photo-editor.component.css index 5cecf93..08c7512 100644 --- a/client/src/app/members/photo-editor/photo-editor.component.css +++ b/client/src/app/members/photo-editor/photo-editor.component.css @@ -10,4 +10,17 @@ img.img-thumbnail { input[type=file] { color: transparent; +} + +.not-approved { + opacity: 0.2; +} + +.img-wrapper { + position: relative +} + +.img-text { + position: absolute; + bottom: 30%; } \ No newline at end of file diff --git a/client/src/app/members/photo-editor/photo-editor.component.html b/client/src/app/members/photo-editor/photo-editor.component.html index 13055f9..2ffdd15 100644 --- a/client/src/app/members/photo-editor/photo-editor.component.html +++ b/client/src/app/members/photo-editor/photo-editor.component.html @@ -1,9 +1,16 @@
-
- {{photo.url}} +
+ {{photo.url}} + +
+ Awaiting approval +
+