diff --git a/exercise.wwwapi/Configuration/ConfigurationSettings.cs b/exercise.wwwapi/Configuration/ConfigurationSettings.cs new file mode 100644 index 0000000..5f84d68 --- /dev/null +++ b/exercise.wwwapi/Configuration/ConfigurationSettings.cs @@ -0,0 +1,15 @@ +namespace exercise.wwwapi.Configuration +{ + public class ConfigurationSettings : IConfigurationSettings + { + IConfiguration _conf; + public ConfigurationSettings() + { + _conf = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + } + public T GetValue(string key) + { + return _conf.GetValue(key)!; + } + } +} diff --git a/exercise.wwwapi/Configuration/IConfigurationSettings.cs b/exercise.wwwapi/Configuration/IConfigurationSettings.cs new file mode 100644 index 0000000..790e098 --- /dev/null +++ b/exercise.wwwapi/Configuration/IConfigurationSettings.cs @@ -0,0 +1,7 @@ +namespace exercise.wwwapi.Configuration +{ + public interface IConfigurationSettings + { + public T GetValue(string key); + } +} diff --git a/exercise.wwwapi/DTO/AbstractClasses/DTO_Auth_Request.cs b/exercise.wwwapi/DTO/AbstractClasses/DTO_Auth_Request.cs new file mode 100644 index 0000000..abbb327 --- /dev/null +++ b/exercise.wwwapi/DTO/AbstractClasses/DTO_Auth_Request.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Text.Json.Serialization; +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Configuration; +using exercise.wwwapi.Payload; + +namespace api_cinema_challenge.DTO.Interfaces +{ + /// + /// Special base class inherited by anything requireing authentication, returns a payload with token; + /// + public abstract class DTO_Auth_Request + where Model_Type : class,ICustomModel, new() + { + [JsonIgnore] + private string? _auth_token = null; + public static async Task authenticate(DTO_Auth_Request dto, IRepository repo, IConfigurationSettings conf) + { + Model_Type? model = await dto.ReturnCreatedInstanceModel(repo); + if (model == null) throw new HttpRequestException("requested object does not exist", null, HttpStatusCode.NotFound); + if (!await dto.VerifyRequestedModelAgainstDTO(repo, model)) throw new HttpRequestException("Wrong password", null, HttpStatusCode.NotFound); + + dto._auth_token = await dto.CreateAndReturnJWTToken(repo, model, conf); + return dto._auth_token; + } + public static Payload toPayloadAuth(DTO_Auth_Request dto) + { + var p = new Payload(); + p.Data = dto._auth_token; + p.Status = dto._auth_token != null ? "success" : "failure"; + return p; + } + + protected abstract Task ReturnCreatedInstanceModel(IRepository repo); + protected abstract Task VerifyRequestedModelAgainstDTO(IRepository repo, Model_Type model); + protected abstract Task CreateAndReturnJWTToken(IRepository repo, Model_Type model, IConfigurationSettings conf); + } +} diff --git a/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_create.cs b/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_create.cs new file mode 100644 index 0000000..42ab345 --- /dev/null +++ b/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_create.cs @@ -0,0 +1,30 @@ +using System.Security.Claims; +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; + +namespace api_cinema_challenge.DTO.Interfaces +{ + /// + /// Special base class for all DTO that preforms a new Creation + /// + public abstract class DTO_Request_create + where Model_type : class, ICustomModel, new() + { + protected abstract Func, IQueryable> GetEntryWithIncludes(Model_type createdEntity , params object[] id); + public abstract Model_type CreateAndReturnNewInstance(ClaimsPrincipal user,params object[] pathargs); + protected virtual bool CheckConditionForValidCreate(ClaimsPrincipal user, Model_type createdModel,params object[] pathargs) + { + return true; + } + public async Task Create(IRepository repo, ClaimsPrincipal user, params object[] pathargs) + { + var model = CreateAndReturnNewInstance(user, pathargs); + if(!CheckConditionForValidCreate(user, model, pathargs)) throw new HttpRequestException($"Create condition was not met for creating {typeof(Model_type).Name}", null, System.Net.HttpStatusCode.BadRequest); + var createdEntity = await repo.CreateEntry(model); + if (createdEntity == null) throw new HttpRequestException("Bad Creation request", null, System.Net.HttpStatusCode.BadRequest); + + return await repo.GetEntry(GetEntryWithIncludes(createdEntity, pathargs)); + } + } +} diff --git a/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_delete.cs b/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_delete.cs new file mode 100644 index 0000000..14be7a6 --- /dev/null +++ b/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_delete.cs @@ -0,0 +1,28 @@ +using System.Net; +using System.Security.Claims; +using api_cinema_challenge.Models; +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; + +namespace api_cinema_challenge.DTO.Interfaces +{ + /// + /// Special base class for all DTO that preforms a deletion + /// + public abstract class DTO_Request_delete + where Model_Type : class, ICustomModel, new() + { + protected abstract Func, IQueryable> getId(params object[] id); + protected abstract bool VerifRightsToDelete(ClaimsPrincipal user, Model_Type fetchedModel); + public async Task Delete(IRepository repo, ClaimsPrincipal user,params object[] id) + { + var query = getId(id); + var fetchedModel = await repo.GetEntry(query); + if (fetchedModel == null) throw new HttpRequestException("requested object does not exist", null, HttpStatusCode.NotFound); + if (!VerifRightsToDelete(user, fetchedModel)) throw new HttpRequestException($"requested object is not owned by user {int.Parse(user.FindFirst(ClaimTypes.Sid)!.Value)}", null, HttpStatusCode.Unauthorized); + + return await repo.DeleteEntry(query); + } + } +} diff --git a/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_update.cs b/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_update.cs new file mode 100644 index 0000000..810328c --- /dev/null +++ b/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_update.cs @@ -0,0 +1,28 @@ +using System.Net; +using System.Security.Claims; +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; + +namespace api_cinema_challenge.DTO.Interfaces +{ + /// + /// Special base class for all DTO that updates a entity + /// + public abstract class DTO_Request_update + where Model_Type : class,ICustomModel, new() + { + protected abstract Func, IQueryable> getId(params object[] id); + protected abstract bool VerifRightsToUpdate(ClaimsPrincipal user, Model_Type fetchedModel); + protected abstract Model_Type CreateAndReturnUpdatedInstance(Model_Type originalModelData); + public async Task Update(IRepository repo, ClaimsPrincipal user, params object[] id) + { + var query = getId(id); + var fetchedModel = await repo.GetEntry(query); + if (!VerifRightsToUpdate(user, fetchedModel)) throw new HttpRequestException($"requested object is not owned by user {int.Parse(user.FindFirst(ClaimTypes.Sid)!.Value)}", null, HttpStatusCode.Unauthorized); + if (fetchedModel == null) throw new HttpRequestException("requested object does not exist", null, HttpStatusCode.NotFound); + var model = CreateAndReturnUpdatedInstance(fetchedModel); + + return await repo.UpdateEntry(query, model); + } + } +} diff --git a/exercise.wwwapi/DTO/AbstractClasses/DTO_Response.cs b/exercise.wwwapi/DTO/AbstractClasses/DTO_Response.cs new file mode 100644 index 0000000..f02da66 --- /dev/null +++ b/exercise.wwwapi/DTO/AbstractClasses/DTO_Response.cs @@ -0,0 +1,194 @@ +using System.Diagnostics; +using api_cinema_challenge.Models; +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; +using exercise.wwwapi.Payload; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace api_cinema_challenge.DTO.Interfaces +{ + public interface IDTO_Defines_Include + where Model_Type : class, ICustomModel + { + public static abstract Func, IQueryable> _includeData(); + } + /// + /// The abstract DTO_Response class contains many generic functions to turn a model into a DTO, and package into a payload... + /// + public abstract class DTO_Response + where DTO_Type : DTO_Response, new() + where Model_Type : class, ICustomModel + { + public DTO_Response() { } + protected abstract void _Initialize(Model_Type model); + + public void Initialize(Model_Type model) + { + try + { + _Initialize(model); + } + catch(ArgumentNullException ex) + { + throw new NotImplementedException($"{this.GetType().Name} does not implement the `{nameof(IDTO_Defines_Include)}` interface; Implement that interface to avoid null references by returning the include query"); + } + catch (NullReferenceException ex) + { + throw new NotImplementedException($"{this.GetType().Name} does not implement the `{nameof(IDTO_Defines_Include)}` interface; Implement that interface to avoid null references by returning the include query"); + } + } + + public static async Task> Gets(IRepository repo) + { + IEnumerable list = await repo.GetEntries(); + + return list.Select(x => { var a = new DTO_Type(); a.Initialize(x); return a; }).ToList(); + } + public static async Task> Gets(IRepository repo, Func, IQueryable>? WhereQuery) + { + IEnumerable list = await repo.GetEntries(WhereQuery); + + return list.Select(x => { var a = new DTO_Type(); a.Initialize(x); return a; }).ToList(); + } + + public static async Task> Gets(IRepository repo, Func, IQueryable>? WhereQuery = null, Func, IEnumerable>? selectorfunc = null) + where T : class, ICustomModel + { + IEnumerable list; + if (WhereQuery == null) list = selectorfunc!.Invoke(await repo.GetEntries()).ToList(); + else + { + list = selectorfunc.Invoke(await repo.GetEntries(WhereQuery)).ToList(); + + } + + return list.Select(x => { var a = new DTO_Type(); a.Initialize(x); return a; }).ToList(); + } + + public static IEnumerable Gets(IEnumerable models, Func, IEnumerable>? selectorfunc) + where T : class, ICustomModel + { + IEnumerable list = selectorfunc.Invoke(models).ToList(); + + return list.Select(x => { var a = new DTO_Type(); a.Initialize(x); return a; }).ToList(); + } + + public static IEnumerable Gets(IEnumerable models) + { + return models.Select(x => { var a = new DTO_Type(); a.Initialize(x); return a; }).ToList(); + } + + public static Payload toPayload(Model_Type model, string status = "success") + { + var a = new DTO_Type(); + a.Initialize(model); + + var p = new Payload(); + p.Data = a; + p.Status = status; + return p; + } + public static Payload, Model_Type> toPayload(IEnumerable models, string status = "success") + { + var list = models.Select(x => { var a = new DTO_Type(); a.Initialize(x); return a; }).ToList(); + + var p = new Payload,Model_Type>(); + p.Data = list; + p.Status = status; + return p; + } + public static Payload, Model_Type> toPayload(IEnumerable modelsDtoList, string status = "success") + { + var p = new Payload,Model_Type>(); + p.Data = modelsDtoList; + p.Status = status; + return p; + } + + public static async Task, Model_Type>> toPayload(IRepository repo, string status = "success") + + { + try + { + var p = new Payload, Model_Type>(); + var include_interf = typeof(DTO_Type).GetInterface(typeof(IDTO_Defines_Include).Name); + if (include_interf != null) + { + var methodInfo = typeof(DTO_Type).GetMethod( + nameof(IDTO_Defines_Include._includeData), + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public); + try + { + var queryArgs = (Func, IQueryable>)methodInfo.Invoke(null,null); + p.Data = await Gets(repo, queryArgs); + } + catch(ArgumentNullException ex) + { + throw new NotImplementedException($"{typeof(DTO_Type).Name} does not Include all members of {ex.TargetSite.DeclaringType.Name} in their `{nameof(IDTO_Defines_Include)}` function; Ensure to include all required includes for {ex.TargetSite.DeclaringType.Name} dto class"); + } + catch(NullReferenceException ex) + { + throw new NotImplementedException($"{typeof(DTO_Type).Name} does not Include all members of {ex.TargetSite.DeclaringType.Name} in their `{nameof(IDTO_Defines_Include)}` function; Ensure to include all required includes for {ex.TargetSite.DeclaringType.Name} dto class"); + } + } + else + p.Data = await Gets(repo); + p.Status = status; + return p; + } + catch (HttpRequestException ex) + { + var p = new Payload, Model_Type>(); + p.Data = []; + p.Status = "Failure"; + return p; + } + + } + + public static async Task, Model_Type>> toPayload(IRepository repo, Func, IQueryable> WhereQuery + , Func, IEnumerable>? selectorfunc + , string status = "success" + ) + where T : class, ICustomModel + { + + try + { + var p = new Payload, Model_Type>(); + p.Data = await Gets(repo, WhereQuery, selectorfunc); + p.Status = status; + return p; + } + catch (HttpRequestException ex) + { + var p = new Payload, Model_Type>(); + p.Data = []; + p.Status = "Failure"; + return p; + } + } + + public static async Task, Model_Type>> toPayload(IRepository repo, Func, IQueryable>? WhereQuery, string status = "success") + { + try + { + var p = new Payload, Model_Type>(); + p.Data = await Gets(repo, WhereQuery); + p.Status = status; + return p; + } + catch (HttpRequestException ex) + { + var p = new Payload, Model_Type>(); + p.Data = []; + p.Status = "Failure"; + return p; + } + + } + + + } +} diff --git a/exercise.wwwapi/DTO/Request/Auth_Login_User.cs b/exercise.wwwapi/DTO/Request/Auth_Login_User.cs new file mode 100644 index 0000000..8546170 --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Auth_Login_User.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using api_cinema_challenge.DTO.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Configuration; +using exercise.wwwapi.Models; +using Microsoft.IdentityModel.Tokens; + +namespace exercise.wwwapi.DTO.Request +{ + public class Auth_Login_User : DTO_Auth_Request + { + public string Username { get; set; } + public string Password { get; set; } + + protected override async Task CreateAndReturnJWTToken(IRepository repo, User model, IConfigurationSettings conf) + { + List claims = new List + { + new Claim(ClaimTypes.Sid, model.Id.ToString()), + new Claim(ClaimTypes.Name, model.Username), + new Claim(ClaimTypes.Email, model.Email), + new Claim(ClaimTypes.Role, model.Role), + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(conf.GetValue("AppSettings:Token")!)); + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature); + var token = new JwtSecurityToken( + claims: claims, + expires: DateTime.Now.AddDays(1), + signingCredentials: credentials + ); + var jwt = new JwtSecurityTokenHandler().WriteToken(token); + return jwt; + } + + protected override async Task ReturnCreatedInstanceModel(IRepository repo) + { + return await repo.GetEntry(x => x.Where(x => x.Username == this.Username)); + } + + protected override async Task VerifyRequestedModelAgainstDTO(IRepository repo, User model) + { + if (!BCrypt.Net.BCrypt.Verify(this.Password, model.PasswordHash)) + return false; + + return true; + } + } +} diff --git a/exercise.wwwapi/DTO/Request/Create_BlogPost.cs b/exercise.wwwapi/DTO/Request/Create_BlogPost.cs new file mode 100644 index 0000000..635b810 --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Create_BlogPost.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Claims; +using api_cinema_challenge.DTO.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; +using Microsoft.EntityFrameworkCore; + +namespace exercise.wwwapi.DTO.Request +{ + public class Create_BlogPost : DTO_Request_create + { + public string Text { get; set; } + + public override BlogPost CreateAndReturnNewInstance(ClaimsPrincipal user,params object[] pathargs) + { + return new BlogPost + { + AuthorId = int.Parse(user.FindFirst(ClaimTypes.Sid).Value), + Text = this.Text + }; + } + + protected override Func, IQueryable> GetEntryWithIncludes(BlogPost createdEntity, params object[] id) + { + return x => x.Where(x => x.Id == createdEntity.Id).Include(x => x.User).Include(x => x.Comments); + } + } +} diff --git a/exercise.wwwapi/DTO/Request/Create_BlogPostComment.cs b/exercise.wwwapi/DTO/Request/Create_BlogPostComment.cs new file mode 100644 index 0000000..e1ad908 --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Create_BlogPostComment.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Claims; +using System.Text.Json.Serialization; +using api_cinema_challenge.DTO.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; +using Microsoft.EntityFrameworkCore; + +namespace exercise.wwwapi.DTO.Request +{ + public class Create_BlogPostComment : DTO_Request_create, IDTO_Defines_Include + { + [JsonIgnore] + public int BlogPostId { get; set; } + public string Text { get; set; } + + public static Func, IQueryable> _includeData() + { + return x => x.Include(x => x.User); + } + + public override Comment CreateAndReturnNewInstance(ClaimsPrincipal user, params object[] pathargs) + { + return new Comment + { + UserId = int.Parse(user.FindFirst(ClaimTypes.Sid).Value), + BlogPostId = (int)pathargs[0], + Text = this.Text + }; + } + + protected override bool CheckConditionForValidCreate(ClaimsPrincipal user, Comment model, params object[] pathargs) + { + return model.UserId == int.Parse(user.FindFirst(ClaimTypes.Sid).Value); + } + + protected override Func, IQueryable> GetEntryWithIncludes(Comment createdEntity, params object[] id) + { + return x => x.Where(x => x.Id == createdEntity.Id).Include(x => x.User).Include(x => x.BlogPost); + } + } +} diff --git a/exercise.wwwapi/DTO/Request/Create_Following.cs b/exercise.wwwapi/DTO/Request/Create_Following.cs new file mode 100644 index 0000000..f84fbdf --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Create_Following.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Claims; +using System.Text.Json.Serialization; +using api_cinema_challenge.DTO.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; + +namespace exercise.wwwapi.DTO.Request +{ + public class Create_Following : DTO_Request_create + { + [JsonIgnore] + public int FollowerId{ get; set; } + [JsonIgnore] + public int FollowingId{ get; set; } + + public override UserFollows CreateAndReturnNewInstance(ClaimsPrincipal user, params object[] pathargs) + { + return new UserFollows + { + //FollowerId = int.Parse(user.FindFirst(ClaimTypes.Sid).Value), // better but does not align with criteria... + FollowerId = (int)pathargs[0], + FollowingsId = (int)pathargs[1] + }; + } + protected override bool CheckConditionForValidCreate(ClaimsPrincipal user, UserFollows createdModel, params object[] pathargs) + { + return int.Parse(user.Claims.First().Value) == createdModel.FollowerId; + } + + protected override Func, IQueryable> GetEntryWithIncludes(UserFollows createdEntity, params object[] id) + { + return x => x.Where(x => x.FollowerId == createdEntity.FollowerId && x.FollowingsId == createdEntity.FollowingsId); + } + } +} diff --git a/exercise.wwwapi/DTO/Request/Create_User.cs b/exercise.wwwapi/DTO/Request/Create_User.cs new file mode 100644 index 0000000..658fa58 --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Create_User.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Claims; +using api_cinema_challenge.DTO.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; + +namespace exercise.wwwapi.DTO.Request +{ + public class Create_User : DTO_Request_create + { + public string Username { get; set; } + public string Password { get; set; } + public string Email { get; set; } + + public override User CreateAndReturnNewInstance(ClaimsPrincipal user, params object[] pathargs) + { + return new User + { + Username = this.Username, + PasswordHash = BCrypt.Net.BCrypt.HashPassword(this.Password), + Email = this.Email, + Role = "User" + }; + } + + protected override Func, IQueryable> GetEntryWithIncludes(User createdEntity, params object[] id) + { + return x => x.Where(x => x.Id == createdEntity.Id); + } + } +} diff --git a/exercise.wwwapi/DTO/Request/Remove_Following.cs b/exercise.wwwapi/DTO/Request/Remove_Following.cs new file mode 100644 index 0000000..da6ac2b --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Remove_Following.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Claims; +using System.Text.Json.Serialization; +using api_cinema_challenge.DTO.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; + +namespace exercise.wwwapi.DTO.Request +{ + public class Delete_Following : DTO_Request_delete + { + [JsonIgnore] + public int FollowerId{ get; set; } + [JsonIgnore] + public int FollowingId{ get; set; } + + + protected override Func, IQueryable> getId(params object[] id) + { + return x => x.Where(x => x.FollowerId == (int)id[0] && x.FollowingsId == (int)id[1]); + } + + protected override bool VerifRightsToDelete(ClaimsPrincipal user, UserFollows fetchedModel) + { + return int.Parse(user.FindFirst(ClaimTypes.Sid).Value) == fetchedModel.FollowerId + || user.IsInRole("Administrator"); + } + } +} diff --git a/exercise.wwwapi/DTO/Request/Update_BlogPost.cs b/exercise.wwwapi/DTO/Request/Update_BlogPost.cs new file mode 100644 index 0000000..251f226 --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Update_BlogPost.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Claims; +using api_cinema_challenge.DTO.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; + +namespace exercise.wwwapi.DTO.Request +{ + public class Update_BlogPost : DTO_Request_update + { + public string? Text { get; set; } + + protected override Func, IQueryable> getId(params object[] id) + { + return x => x.Where(x => x.Id == (int)id[0]); + } + + protected override BlogPost CreateAndReturnUpdatedInstance(BlogPost originalModelData) + { + return new BlogPost + { + Id = originalModelData.Id, + AuthorId = originalModelData.AuthorId, + Text = this.Text ?? originalModelData.Text, + }; + } + + protected override bool VerifRightsToUpdate(ClaimsPrincipal user, BlogPost fetchedModel) + { + + return + int.Parse(user.FindFirst(ClaimTypes.Sid)!.Value) == fetchedModel.AuthorId + || user.IsInRole("Administrator"); + } + } +} diff --git a/exercise.wwwapi/DTO/Response/Get_BlogPost.cs b/exercise.wwwapi/DTO/Response/Get_BlogPost.cs new file mode 100644 index 0000000..4850032 --- /dev/null +++ b/exercise.wwwapi/DTO/Response/Get_BlogPost.cs @@ -0,0 +1,20 @@ +using api_cinema_challenge.DTO.Interfaces; +using exercise.wwwapi.Models; +using Microsoft.AspNetCore.Identity; + +namespace exercise.wwwapi.DTO.Response +{ + public class Get_BlogPost : DTO_Response + { + public int Id { get; set; } + public int AuthorId { get; set; } + public string Text { get; set; } + + protected override void _Initialize(BlogPost model) + { + Id = model.Id; + AuthorId = model.AuthorId; + Text = model.Text; + } + } +} diff --git a/exercise.wwwapi/DTO/Response/Get_BlogPostWithComments.cs b/exercise.wwwapi/DTO/Response/Get_BlogPostWithComments.cs new file mode 100644 index 0000000..0394cb2 --- /dev/null +++ b/exercise.wwwapi/DTO/Response/Get_BlogPostWithComments.cs @@ -0,0 +1,29 @@ +using api_cinema_challenge.DTO.Interfaces; +using exercise.wwwapi.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace exercise.wwwapi.DTO.Response +{ + public class Get_BlogPostWithComments : DTO_Response, IDTO_Defines_Include + { + public int Id { get; set; } + public int AuthorId { get; set; } + public string Text { get; set; } + public List Comments { get; set; } + + public static Func, IQueryable> _includeData() + { + return x => x.Include(x => x.Comments).ThenInclude(x => x.User); + } + + protected override void _Initialize(BlogPost model) + { + Id = model.Id; + AuthorId = model.AuthorId; + Text = model.Text; + Comments = Get_Comment.Gets(model.Comments).ToList(); + } + + } +} diff --git a/exercise.wwwapi/DTO/Response/Get_Comment.cs b/exercise.wwwapi/DTO/Response/Get_Comment.cs new file mode 100644 index 0000000..ce16a36 --- /dev/null +++ b/exercise.wwwapi/DTO/Response/Get_Comment.cs @@ -0,0 +1,18 @@ +using api_cinema_challenge.DTO.Interfaces; +using exercise.wwwapi.Models; +using Microsoft.AspNetCore.Identity; + +namespace exercise.wwwapi.DTO.Response +{ + public class Get_Comment : DTO_Response + { + public string poster_name { get; set; } + public string Text { get; set; } + + protected override void _Initialize(Comment model) + { + poster_name = model.User.Username; + Text = model.Text; + } + } +} diff --git a/exercise.wwwapi/DTO/Response/Get_Follows.cs b/exercise.wwwapi/DTO/Response/Get_Follows.cs new file mode 100644 index 0000000..ac0775b --- /dev/null +++ b/exercise.wwwapi/DTO/Response/Get_Follows.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; +using api_cinema_challenge.DTO.Interfaces; +using exercise.wwwapi.Models; +using Microsoft.AspNetCore.Identity; + +namespace exercise.wwwapi.DTO.Response +{ + public class Get_Follows : DTO_Response + { + public int FollowerId { get; set; } + public int FollowingsId { get; set; } + + protected override void _Initialize(UserFollows model) + { + FollowerId = model.FollowerId; + FollowingsId = model.FollowingsId; + + + } + } +} diff --git a/exercise.wwwapi/DTO/Response/Get_User.cs b/exercise.wwwapi/DTO/Response/Get_User.cs new file mode 100644 index 0000000..277ea55 --- /dev/null +++ b/exercise.wwwapi/DTO/Response/Get_User.cs @@ -0,0 +1,23 @@ +using api_cinema_challenge.DTO.Interfaces; +using exercise.wwwapi.Models; +using Microsoft.AspNetCore.Identity; + +namespace exercise.wwwapi.DTO.Response +{ + public class Get_User : DTO_Response + { + public int Id { get; set; } + public string Username { get; set; } + public string PasswordHash { get; set; } + public string Email { get; set; } + + protected override void _Initialize(User model) + { + Id = model.Id; + Username = model.Username; + PasswordHash = model.PasswordHash; + Email = model.Email; + + } + } +} diff --git a/exercise.wwwapi/Data/DatabaseContext.cs b/exercise.wwwapi/Data/DatabaseContext.cs new file mode 100644 index 0000000..9baf2bd --- /dev/null +++ b/exercise.wwwapi/Data/DatabaseContext.cs @@ -0,0 +1,70 @@ +using System.Xml.Linq; +using exercise.wwwapi.Configuration; +using exercise.wwwapi.Models; +using Microsoft.EntityFrameworkCore; + +namespace exercise.wwwapi.Data +{ + public class DatabaseContext : DbContext + { + private IConfigurationSettings _conf; + public DatabaseContext(DbContextOptions options, IConfigurationSettings conf) : base(options) + { + _conf = conf; + this.Database.EnsureCreated(); + } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(p => new { p.FollowingsId, p.FollowerId }); + + + + modelBuilder.Entity() + .HasOne(p => p.User) + .WithMany(p => p.BlogPosts) + .HasForeignKey(p => p.AuthorId); + + modelBuilder.Entity() + .HasOne(p => p.User) + .WithMany(p => p.Comments) + .HasForeignKey(p => p.UserId); + + modelBuilder.Entity() + .HasOne(p => p.BlogPost) + .WithMany(p => p.Comments) + .HasForeignKey(p => p.BlogPostId); + + modelBuilder.Entity() + .HasOne(p => p.Follower) + .WithMany(p => p.Followers) + .HasForeignKey(x => x.FollowerId); + + modelBuilder.Entity() + .HasOne(p => p.Following) + .WithMany(p => p.Followings) + .HasForeignKey(p => p.FollowingsId); + + // Seed data + Seeder seeder = new Seeder(); + modelBuilder.Entity(). + HasData(seeder.Users); + modelBuilder.Entity(). + HasData(seeder.BlogPosts); + modelBuilder.Entity(). + HasData(seeder.Comments); + + base.OnModelCreating(modelBuilder); + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql(_conf.GetValue("ConnectionStrings:DefaultConnectionString")!); + base.OnConfiguring(optionsBuilder); + } + + public DbSet Users { get; set; } + public DbSet BlogPosts { get; set; } + public DbSet UserFollows { get; set; } + public DbSet Comments { get; set; } + } +} diff --git a/exercise.wwwapi/Data/Seeder.cs b/exercise.wwwapi/Data/Seeder.cs new file mode 100644 index 0000000..29362d4 --- /dev/null +++ b/exercise.wwwapi/Data/Seeder.cs @@ -0,0 +1,56 @@ +using exercise.wwwapi.Models; + +namespace exercise.wwwapi.Data +{ + public class Seeder + { + public Seeder() + { + Users = new List() + { + new User{ Id = 1, + Username = "Bob", + Email = "Bob@bob.bob" , + PasswordHash= BCrypt.Net.BCrypt.HashPassword("Tacos_with_pudding_and_burger_and_lassanga"), + Role = "User"}, + new User{ Id = 2, + Username = "Flurp", + Email = "flurp@bob.bob" , + PasswordHash= BCrypt.Net.BCrypt.HashPassword("Oaks123"), + Role = "User"}, + new User{ Id = 3, + Username = "Glorp", + Email = "glorp@bob.bob" , + PasswordHash= BCrypt.Net.BCrypt.HashPassword("glorps_password"), + Role = "User"}, + new User{ Id = 4, + Username = "admin", + Email = "admin@admin.admin" , + PasswordHash= BCrypt.Net.BCrypt.HashPassword("admin"), + Role = "Administrator"} + }; + BlogPosts = new List() { + new BlogPost{ Id = 1, AuthorId = 1, Text = "Bob hungry today..."}, + new BlogPost{ Id = 2, AuthorId = 2, Text = "Flurp not want tree, flurp has better things to do..."}, + new BlogPost{ Id = 3, AuthorId = 3, Text = "Where Glorp make password to use blog? Glorp got story to tell. Help"}, + new BlogPost{ Id = 4, AuthorId = 2, Text = "Ha Ha, Glorp don't know REST"}, + new BlogPost{ Id = 5, AuthorId = 3, Text = "Glorp ate rock once, Glorp hurt teeth"}, + new BlogPost{ Id = 6, AuthorId = 1, Text = "Maybe will try rock today, still hungry... "}, + }; + + Comments = new List() { + new Comment{ Id=1, BlogPostId=6, UserId=3, Text = "No... Read my post. Glorp hurt tooth"}, + new Comment{ Id=2, BlogPostId=6, UserId=2, Text = "Do it!"}, + new Comment{ Id=3, BlogPostId=6, UserId=3, Text = "How do I login? need to tell Bob not to eat rock..."}, + new Comment{ Id=4, BlogPostId=5, UserId=1, Text = "But was it a smooth or a jagged rock?"}, + new Comment{ Id=5, BlogPostId=5, UserId=3, Text = "glorps_password"}, + new Comment{ Id=6, BlogPostId=5, UserId=3, Text = "How do I remove?"}, + new Comment{ Id=7, BlogPostId=5, UserId=3, Text = "loook at me, i'm dumb glorp. I eat rocks. haha. I am so stopid"}, + new Comment{ Id=8, BlogPostId=5, UserId=3, Text = "I DID NOT DO THAT"}, + }; + } + public List BlogPosts { get; set; } + public List Users { get; set; } + public List Comments { get; set; } + } +} diff --git a/exercise.wwwapi/Endpoints/AuthEndpoints.cs b/exercise.wwwapi/Endpoints/AuthEndpoints.cs new file mode 100644 index 0000000..51234b0 --- /dev/null +++ b/exercise.wwwapi/Endpoints/AuthEndpoints.cs @@ -0,0 +1,51 @@ + +using System.Security.Claims; +using api_cinema_challenge.DTO; +using api_cinema_challenge.DTO.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Configuration; +using exercise.wwwapi.DTO.Request; +using exercise.wwwapi.DTO.Response; +using exercise.wwwapi.Models; +using exercise.wwwapi.Payload; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace exercise.wwwapi.Endpoints +{ + public static class AuthEndpoints + { + public static void ConfigureAuthEndpoints(this WebApplication app) + { + var usergroup = app.MapGroup("auth"); + usergroup.MapPost("/register", CreateUser); + usergroup.MapPost("/login", LoginUser); + } + + private static async Task LoginUser(HttpContext context, IRepository repo, IConfigurationSettings conf,Auth_Login_User dto) + { + try + { + await Auth_Login_User.authenticate(dto, repo, conf); + return TypedResults.Ok(Auth_Login_User.toPayloadAuth(dto)); + } + catch (HttpRequestException ex) + { + return Fail.Payload(ex); + } + } + + private static async Task CreateUser(HttpContext context, IRepository repo, ClaimsPrincipal user,Create_User dto) + { + if (await repo.GetEntry(x => x.Where(x => x.Username == dto.Username)) != null) return Fail.Payload("user already existed with that name", TypedResults.Conflict); + try + { + User createduser = await dto.Create(repo, user); + return TypedResults.Ok(Get_User.toPayload(createduser)); + } + catch (HttpRequestException ex) + { + return Fail.Payload(ex); + } + } + } +} diff --git a/exercise.wwwapi/Endpoints/BlogEndpoints.cs b/exercise.wwwapi/Endpoints/BlogEndpoints.cs new file mode 100644 index 0000000..94641e4 --- /dev/null +++ b/exercise.wwwapi/Endpoints/BlogEndpoints.cs @@ -0,0 +1,83 @@ +using wwwapi.Extensions; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Configuration; +using exercise.wwwapi.DTO.Request; +using exercise.wwwapi.DTO.Response; +using exercise.wwwapi.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using System.Security.Claims; +using System.Net; +using exercise.wwwapi.Payload; + +namespace exercise.wwwapi.Endpoints +{ + public static class BlogEndpoints + { + public static void ConfigureBlogEndpoints(this WebApplication app) + { + var usergroup = app.MapGroup("/blogposts"); + usergroup.MapPost("/", CreatePost); + usergroup.MapPost("/{id}/comment", CreatePostComment); + usergroup.MapPut("/{id}", EditPost); + usergroup.MapGet("/", GetAllPosts); + usergroup.MapGet("withcomments/", GetAllUsersPosts_withComments); + } + + [Authorize] + private static async Task EditPost(HttpContext context, IRepository repo, Update_BlogPost dto, ClaimsPrincipal user, int id ) + { + try + { + var updated = await dto.Update(repo, user, id); + return TypedResults.Created(context.Get_endpointUrl(id),Get_BlogPost.toPayload(updated)); + } + catch (HttpRequestException ex) + { + return Fail.Payload(ex); + } + } + [Authorize] + private static async Task CreatePostComment(HttpContext context, IRepository repo, Create_BlogPostComment dto, ClaimsPrincipal user, int id ) + { + try + { + var updated = await dto.Create(repo, user, id); + return TypedResults.Created(context.Get_endpointUrl(id), Get_Comment.toPayload(updated)); + } + catch (HttpRequestException ex) + { + return Fail.Payload(ex); + } + } + + [Authorize] + private static async Task GetAllPosts(HttpContext context, IRepository repo) + { + + return TypedResults.Ok(await Get_BlogPost.toPayload(repo)); + } + + [Authorize] + private static async Task GetAllUsersPosts_withComments(HttpContext context, IRepository repo) + { + + return TypedResults.Ok(await Get_BlogPostWithComments.toPayload(repo)); + } + + [Authorize] + private static async Task CreatePost(HttpContext context, IRepository repo, ClaimsPrincipal user, Create_BlogPost dto) + { + try + { + BlogPost post = await dto.Create(repo, user); + return TypedResults.Created(context.Get_endpointUrl(post.Id), Get_BlogPost.toPayload(post)); + } + catch (HttpRequestException ex) + { + return Fail.Payload(ex); + } + + } + } +} diff --git a/exercise.wwwapi/Endpoints/FollowEndpoints.cs b/exercise.wwwapi/Endpoints/FollowEndpoints.cs new file mode 100644 index 0000000..5f7e27a --- /dev/null +++ b/exercise.wwwapi/Endpoints/FollowEndpoints.cs @@ -0,0 +1,71 @@ +using wwwapi.Extensions; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Configuration; +using exercise.wwwapi.DTO.Request; +using exercise.wwwapi.DTO.Response; +using exercise.wwwapi.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using System.Security.Claims; +using System.Net; +using Microsoft.EntityFrameworkCore; +using exercise.wwwapi.Payload; + +namespace exercise.wwwapi.Endpoints +{ + public static class FollowEndpoints + { + public static void ConfigureFollowEndpoints(this WebApplication app) + { + var usergroup = app.MapGroup("/"); + usergroup.MapPost("user/{userId}/follows/{otherUserId}", CreateFollowing); + usergroup.MapPost("user/{userId}/unfollows/{otherUserId}", RemoveFollowing); + usergroup.MapGet("viewall/{userId}", GetAllUsersPosts); + } + + [Authorize] + private static async Task GetAllUsersPosts(HttpContext context, IRepository repo, IRepository uf_repo, IRepository user_repo, ClaimsPrincipal user, int userId) + { + // Prohibit other users from seeing who anothe user follows... + if (int.Parse(user.FindFirst(ClaimTypes.Sid).Value) != userId) return Fail.Payload("Unauthorized access denied", HttpStatusCode.Unauthorized); + + return TypedResults.Ok( + await Get_BlogPost.toPayload( + uf_repo, + x => x.Where(x => x.FollowerId == userId) + .Include(x => x.Following) + .ThenInclude(x => x.BlogPosts), + x => x.SelectMany(x => x.Following.BlogPosts) + )); + } + + [Authorize] + private static async Task CreateFollowing(HttpContext context, IRepository repo, ClaimsPrincipal user, Create_Following dto, int userId, int otherUserId) + { + try + { + UserFollows follow = await dto.Create(repo, user, userId, otherUserId); + return TypedResults.Created(context.Get_endpointUrl(follow.FollowingsId), Get_Follows.toPayload(follow)); + } + catch (HttpRequestException ex) + { + return Fail.Payload(ex); + } + + } + [Authorize] + private static async Task RemoveFollowing(HttpContext context, IRepository repo, ClaimsPrincipal user, Delete_Following dto, int userId, int otherUserId) + { + try + { + UserFollows follow = await dto.Delete(repo, user, userId, otherUserId); + return TypedResults.Ok(Get_Follows.toPayload(follow)); + } + catch (HttpRequestException ex) + { + return Fail.Payload(ex); + } + } + + } +} diff --git a/exercise.wwwapi/Extensions/Http_context_extension.cs b/exercise.wwwapi/Extensions/Http_context_extension.cs new file mode 100644 index 0000000..591baca --- /dev/null +++ b/exercise.wwwapi/Extensions/Http_context_extension.cs @@ -0,0 +1,13 @@ +namespace wwwapi.Extensions +{ + public static class Http_context_extension + { + public static string Get_endpointUrl(this HttpContext context, T indexVal) + { + string schemeType = context.Request.Scheme; + string local = context.Request.Host.ToUriComponent(); + string path = context.Request.Path; + return string.Format($"{schemeType}://{local}{path}/{{0}}", indexVal); + } + } +} diff --git a/exercise.wwwapi/Models/BlogPost.cs b/exercise.wwwapi/Models/BlogPost.cs new file mode 100644 index 0000000..a1f5f98 --- /dev/null +++ b/exercise.wwwapi/Models/BlogPost.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using api_cinema_challenge.Models.Interfaces; + +namespace exercise.wwwapi.Models +{ + [Table("blog_posts")] + public class BlogPost :ICustomModel + { + [Key, Column("id")] + public int Id { get; set; } + [Column("user_id")] + public int AuthorId { get; set; } + [Column("text")] + public string Text { get; set; } + + // Navigation properties + public User User { get; set; } + public List Comments { get; set; } + } +} diff --git a/exercise.wwwapi/Models/Comment.cs b/exercise.wwwapi/Models/Comment.cs new file mode 100644 index 0000000..3354cf2 --- /dev/null +++ b/exercise.wwwapi/Models/Comment.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using api_cinema_challenge.Models.Interfaces; + +namespace exercise.wwwapi.Models +{ + [Table("comments")] + public class Comment : ICustomModel + { + [Key,Column("id")] + public int Id { get; set; } + [Column("blog_post_id")] + public int BlogPostId { get; set; } + [Column("user_id")] + public int UserId { get; set; } + [Column("text")] + public string Text { get; set; } + + // Navigation properties + public BlogPost BlogPost { get; set; } + public User User { get; set; } + } +} diff --git a/exercise.wwwapi/Models/Interfaces/ICustomModel.cs b/exercise.wwwapi/Models/Interfaces/ICustomModel.cs new file mode 100644 index 0000000..69d24b8 --- /dev/null +++ b/exercise.wwwapi/Models/Interfaces/ICustomModel.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.Models.Interfaces +{ + /// + /// Dummy interface all Models must implement in order to improve typesafety in the generic abstract Response and Request classess + /// + public interface ICustomModel + { + } +} diff --git a/exercise.wwwapi/Models/User.cs b/exercise.wwwapi/Models/User.cs new file mode 100644 index 0000000..148021a --- /dev/null +++ b/exercise.wwwapi/Models/User.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations.Schema; +using api_cinema_challenge.Models.Interfaces; + +namespace exercise.wwwapi.Models +{ + [Table("users")] + public class User : ICustomModel + { + [Column("id")] + public int Id { get; set; } + [Column("username")] + public string Username { get; set; } + [Column("passwordhash")] + public string PasswordHash { get; set; } + [Column("email")] + public string Email { get; set; } + [Column("role")] + public string Role { get; set; } + + // Navigation properties + public List BlogPosts { get; set; } + public List Followers{ get; set; } + public List Followings { get; set; } + public List Comments { get; set; } + } +} diff --git a/exercise.wwwapi/Models/UserFollows.cs b/exercise.wwwapi/Models/UserFollows.cs new file mode 100644 index 0000000..b70119a --- /dev/null +++ b/exercise.wwwapi/Models/UserFollows.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations.Schema; +using api_cinema_challenge.Models.Interfaces; + +namespace exercise.wwwapi.Models +{ + [Table("user_follows")] + public class UserFollows : ICustomModel + { + [Column("user_who_follows_id")] + public int FollowerId { get; set; } + [Column("user_to_follow_id")] + public int FollowingsId { get; set; } + + // Navigation properties + public User Follower { get; set; } + public User Following { get; set; } + } +} diff --git a/exercise.wwwapi/Payload/Fail.cs b/exercise.wwwapi/Payload/Fail.cs new file mode 100644 index 0000000..e50f284 --- /dev/null +++ b/exercise.wwwapi/Payload/Fail.cs @@ -0,0 +1,55 @@ +using System.Net; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace exercise.wwwapi.Payload +{ + /// + /// This is a payload helper to make it easier to return a payload based on bad requests... + /// + static public class Fail + { + static private Dictionary> keys = new Dictionary>() + { + {HttpStatusCode.NotFound , TypedResults.NotFound}, + {HttpStatusCode.BadRequest , TypedResults.BadRequest}, + {HttpStatusCode.Conflict , TypedResults.Conflict}, + {HttpStatusCode.OK , TypedResults.Ok}, + {HttpStatusCode.UnprocessableContent , TypedResults.UnprocessableEntity}, + {HttpStatusCode.Unauthorized , obj => TypedResults.Unauthorized()}, + {HttpStatusCode.InternalServerError , TypedResults.InternalServerError } + }; + + static public IResult Payload(string msg, Func, IResult> func) + { + var failLoad = new Payload(); + failLoad.Status = "Failure"; + failLoad.Data = new { Message = msg }; + + return func(failLoad); + } + + static public IResult Payload(HttpRequestException ex) + { + var failLoad = new Payload(); + failLoad.Status = "Failure"; + failLoad.Data = new { ex.Message }; + + if (keys.ContainsKey(ex.StatusCode.Value)) + return keys[ex.StatusCode.Value].Invoke(failLoad); + + return TypedResults.BadRequest(failLoad); + } + + static public IResult Payload(string msg, HttpStatusCode code) + { + var failLoad = new Payload(); + failLoad.Status = "Failure"; + failLoad.Data = new { Message = msg }; + + if (keys.ContainsKey(code)) + return keys[code].Invoke(failLoad); + + return TypedResults.BadRequest(failLoad); + } + } +} diff --git a/exercise.wwwapi/Payload/Payload.cs b/exercise.wwwapi/Payload/Payload.cs new file mode 100644 index 0000000..cba276e --- /dev/null +++ b/exercise.wwwapi/Payload/Payload.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace exercise.wwwapi.Payload +{ + + public class Payload + { + public Payload() { } + public string Status { get; set; } + public T Data { get; set; } + } + +} diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 47f22ef..be47644 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -1,9 +1,74 @@ +using System.Text; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Configuration; +using exercise.wwwapi.Data; +using exercise.wwwapi.Endpoints; +using exercise.wwwapi.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(setupActions => + { + setupActions.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "provide `Bearer ` to authenticate", + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Header, + Scheme = "Bearer" + }); + setupActions.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + }); + +// AddScoped +builder.Services.AddScoped(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddDbContext(); + +var conf = new ConfigurationSettings(); +builder.Services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer( x => + { + x.TokenValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(conf.GetValue("AppSettings:Token"))), + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + ValidateIssuerSigningKey = false + }; + }); + +builder.Services.AddAuthorization(); + +builder.Services.AddCors(); var app = builder.Build(); @@ -14,7 +79,18 @@ app.UseSwaggerUI(); } +app.UseCors(x => x + .AllowAnyMethod() + .AllowAnyHeader() + .SetIsOriginAllowed(origin => true) + .AllowCredentials()); + app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); +app.ConfigureBlogEndpoints(); +app.ConfigureAuthEndpoints(); +app.ConfigureFollowEndpoints(); app.Run(); \ No newline at end of file diff --git a/exercise.wwwapi/Repository/IRepository.cs b/exercise.wwwapi/Repository/IRepository.cs new file mode 100644 index 0000000..92e82d2 --- /dev/null +++ b/exercise.wwwapi/Repository/IRepository.cs @@ -0,0 +1,16 @@ +using api_cinema_challenge.Models.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository +{ + + public interface IRepository where T : class + { + Task> GetEntries(params Func, IQueryable>[] includes); + Task GetEntry(Func, IQueryable> id, params Func, IQueryable>[] expressions); + Task CreateEntry(T entry); + Task UpdateEntry(Func, IQueryable> id, T entry); + Task DeleteEntry(Func, IQueryable> id); + } + +} diff --git a/exercise.wwwapi/Repository/Repository.cs b/exercise.wwwapi/Repository/Repository.cs new file mode 100644 index 0000000..effe995 --- /dev/null +++ b/exercise.wwwapi/Repository/Repository.cs @@ -0,0 +1,75 @@ +using api_cinema_challenge.Models.Interfaces; +using exercise.wwwapi.Data; +using Microsoft.EntityFrameworkCore; + + +namespace api_cinema_challenge.Repository +{ + public class Repository : IRepository where T : class, new() + { + private DatabaseContext _databaseContext; + private DbSet _table = null!; + + public Repository(DatabaseContext db) + { + _databaseContext = db; + _table = db.Set(); + } + + public async Task> GetEntries(params Func, IQueryable>[] includes) + { + IQueryable q = _table.AsQueryable(); + + foreach (var inc in includes) + q = inc.Invoke(q); + + return await q.ToArrayAsync(); + } + + public async Task GetEntry(Func, IQueryable> id, params Func, IQueryable>[] expressions) + { + IQueryable q = _table.AsQueryable(); + + q = id.Invoke(q); + foreach (var ex in expressions) + { + q = ex.Invoke(q); + } + + return await q.FirstOrDefaultAsync(); + } + public async Task CreateEntry(T entry) + { + var a = await _table.AddAsync(entry); + await _databaseContext.SaveChangesAsync(); + return entry; + } + public async Task UpdateEntry(Func, IQueryable> id, T entry) + { + IQueryable q = _table.AsQueryable(); + q = id.Invoke(q); + var foundEntry = await q.FirstOrDefaultAsync(); + + if (foundEntry == null) return null; + _table.Remove(foundEntry); + await _table.AddAsync(entry); + + await _databaseContext.SaveChangesAsync(); + return await id.Invoke(q).FirstAsync(); + } + public async Task DeleteEntry(Func, IQueryable> id) + { + IQueryable q = _table.AsQueryable(); + q = id.Invoke(q); + var foundEntry = await q.FirstOrDefaultAsync(); + + if (foundEntry == null) return null; + _table.Remove(foundEntry); + await _databaseContext.SaveChangesAsync(); + return foundEntry; + } + + + + } +} diff --git a/exercise.wwwapi/appsettings.Development.example.json b/exercise.wwwapi/appsettings.Development.example.json new file mode 100644 index 0000000..7a9d42d --- /dev/null +++ b/exercise.wwwapi/appsettings.Development.example.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "JwtTokenSettings": { + "ValidIssuer": "YourCompanyServer", + "ValidAudience": "YourProductAudience", + "SymmetricSecurityKey": "SOME_RANDOM_SECRET", + "JwtRegisteredClaimNamesSub": "SOME_RANDOM_CODE" + } +} diff --git a/exercise.wwwapi/exercise.wwwapi.csproj b/exercise.wwwapi/exercise.wwwapi.csproj index 56929a8..73d2ca6 100644 --- a/exercise.wwwapi/exercise.wwwapi.csproj +++ b/exercise.wwwapi/exercise.wwwapi.csproj @@ -8,8 +8,23 @@ + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + +