From acaeb2cce8be74422a0432d61f69c94702c6fcec Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Thu, 30 Jan 2025 12:26:47 +0100 Subject: [PATCH 01/12] Added Configuration settings --- .../Configuration/ConfigurationSettings.cs | 15 +++++++++++++++ .../Configuration/IConfigurationSettings.cs | 7 +++++++ exercise.wwwapi/Program.cs | 5 +++++ .../appsettings.Development.example.json | 15 +++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 exercise.wwwapi/Configuration/ConfigurationSettings.cs create mode 100644 exercise.wwwapi/Configuration/IConfigurationSettings.cs create mode 100644 exercise.wwwapi/appsettings.Development.example.json 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/Program.cs b/exercise.wwwapi/Program.cs index 47f22ef..15aa824 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -1,3 +1,5 @@ +using exercise.wwwapi.Configuration; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -5,6 +7,9 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +// AddScoped +builder.Services.AddScoped(); + var app = builder.Build(); // Configure the HTTP request pipeline. 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" + } +} From 3e9aaca69cfe05c2b4c8ac86ec3cda194de7c636 Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Thu, 30 Jan 2025 12:45:48 +0100 Subject: [PATCH 02/12] Added Authorization and Authentication to WebApplicationBuilder --- exercise.wwwapi/Program.cs | 34 ++++++++++++++++++++++++++ exercise.wwwapi/exercise.wwwapi.csproj | 14 +++++++++++ 2 files changed, 48 insertions(+) diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 15aa824..60cb365 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -1,4 +1,8 @@ +using System.Text; using exercise.wwwapi.Configuration; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); @@ -10,6 +14,28 @@ // AddScoped builder.Services.AddScoped(); +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(); + + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -19,7 +45,15 @@ app.UseSwaggerUI(); } +app.UseCors(x => x + .AllowAnyMethod() + .AllowAnyHeader() + .SetIsOriginAllowed(origin => true) + .AllowCredentials()); + app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); app.Run(); \ No newline at end of file diff --git a/exercise.wwwapi/exercise.wwwapi.csproj b/exercise.wwwapi/exercise.wwwapi.csproj index 56929a8..029d8f1 100644 --- a/exercise.wwwapi/exercise.wwwapi.csproj +++ b/exercise.wwwapi/exercise.wwwapi.csproj @@ -8,8 +8,22 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + From 96e8d0aa163f8466f6489382ce65048855371e54 Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Thu, 30 Jan 2025 12:48:02 +0100 Subject: [PATCH 03/12] Added Cors services --- exercise.wwwapi/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 60cb365..82e6484 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -35,6 +35,7 @@ builder.Services.AddAuthorization(); +builder.Services.AddCors(); var app = builder.Build(); From 8b1da6055ea3f33e66aa0fbd38dda80608831464 Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Thu, 30 Jan 2025 12:56:57 +0100 Subject: [PATCH 04/12] Added Authentication to Swagger --- exercise.wwwapi/Program.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 82e6484..39eb5c7 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -3,13 +3,25 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; 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" + }); + +}); // AddScoped builder.Services.AddScoped(); From 5d85824fa0ed5908c5e855c9eb30c9ba281e9647 Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Thu, 30 Jan 2025 13:22:14 +0100 Subject: [PATCH 05/12] Added DatabaseContext --- exercise.wwwapi/Data/DatabaseContext.cs | 21 +++++++++++++++++++++ exercise.wwwapi/Program.cs | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 exercise.wwwapi/Data/DatabaseContext.cs diff --git a/exercise.wwwapi/Data/DatabaseContext.cs b/exercise.wwwapi/Data/DatabaseContext.cs new file mode 100644 index 0000000..ed2a68f --- /dev/null +++ b/exercise.wwwapi/Data/DatabaseContext.cs @@ -0,0 +1,21 @@ +using exercise.wwwapi.Configuration; +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 OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql(_conf.GetValue("ConnectionStrings:DefaultConnectionString")!); + base.OnConfiguring(optionsBuilder); + } + } +} diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 39eb5c7..9ab0441 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -1,5 +1,6 @@ using System.Text; using exercise.wwwapi.Configuration; +using exercise.wwwapi.Data; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.IdentityModel.Tokens; @@ -25,6 +26,7 @@ // AddScoped builder.Services.AddScoped(); +builder.Services.AddDbContext(); var conf = new ConfigurationSettings(); builder.Services.AddAuthentication(x => From 5912a31f40559a6ac956da52eb3254af8a9418aa Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Thu, 30 Jan 2025 14:25:04 +0100 Subject: [PATCH 06/12] Added DTO, Repository, User model --- .../DTO/Interfaces/DTO_Response.cs | 36 +++++++++ .../DTO/Interfaces/IDTO_Request_create.cs | 13 ++++ .../DTO/Interfaces/IDTO_Request_delete.cs | 13 ++++ .../DTO/Interfaces/IDTO_Request_update.cs | 12 +++ exercise.wwwapi/DTO/Payload.cs | 16 ++++ exercise.wwwapi/DTO/Request/Create_User.cs | 27 +++++++ exercise.wwwapi/DTO/Response/Get_User.cs | 23 ++++++ exercise.wwwapi/Data/DatabaseContext.cs | 3 + exercise.wwwapi/Endpoints/UserEndpoints.cs | 25 +++++++ .../Extensions/Http_context_extension.cs | 13 ++++ .../Models/Interfaces/ICustomModel.cs | 6 ++ exercise.wwwapi/Models/User.cs | 18 +++++ exercise.wwwapi/Program.cs | 5 ++ exercise.wwwapi/Repository/IRepository.cs | 15 ++++ exercise.wwwapi/Repository/Repository.cs | 74 +++++++++++++++++++ 15 files changed, 299 insertions(+) create mode 100644 exercise.wwwapi/DTO/Interfaces/DTO_Response.cs create mode 100644 exercise.wwwapi/DTO/Interfaces/IDTO_Request_create.cs create mode 100644 exercise.wwwapi/DTO/Interfaces/IDTO_Request_delete.cs create mode 100644 exercise.wwwapi/DTO/Interfaces/IDTO_Request_update.cs create mode 100644 exercise.wwwapi/DTO/Payload.cs create mode 100644 exercise.wwwapi/DTO/Request/Create_User.cs create mode 100644 exercise.wwwapi/DTO/Response/Get_User.cs create mode 100644 exercise.wwwapi/Endpoints/UserEndpoints.cs create mode 100644 exercise.wwwapi/Extensions/Http_context_extension.cs create mode 100644 exercise.wwwapi/Models/Interfaces/ICustomModel.cs create mode 100644 exercise.wwwapi/Models/User.cs create mode 100644 exercise.wwwapi/Repository/IRepository.cs create mode 100644 exercise.wwwapi/Repository/Repository.cs diff --git a/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs b/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs new file mode 100644 index 0000000..f106650 --- /dev/null +++ b/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs @@ -0,0 +1,36 @@ +using api_cinema_challenge.Models; +using api_cinema_challenge.Models.Interfaces; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace api_cinema_challenge.DTO.Interfaces +{ + public interface IDTO_Respons { + public void Initialize(Y model); + } + public abstract class DTO_Response : IDTO_Respons + where DTO_Type : DTO_Response, new() + where Model_type : class, ICustomModel + { + public DTO_Response() { } + public abstract void Initialize(Model_type model); + 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; + } + } +} diff --git a/exercise.wwwapi/DTO/Interfaces/IDTO_Request_create.cs b/exercise.wwwapi/DTO/Interfaces/IDTO_Request_create.cs new file mode 100644 index 0000000..ddd2120 --- /dev/null +++ b/exercise.wwwapi/DTO/Interfaces/IDTO_Request_create.cs @@ -0,0 +1,13 @@ +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; + +namespace api_cinema_challenge.DTO.Interfaces +{ + public interface IDTO_Request_create + where DTO_Type : IDTO_Request_create + where Model_type : class, ICustomModel, new() + { + public abstract static Task create(IRepository repo, DTO_Type dto, params object[] pathargs); + } +} diff --git a/exercise.wwwapi/DTO/Interfaces/IDTO_Request_delete.cs b/exercise.wwwapi/DTO/Interfaces/IDTO_Request_delete.cs new file mode 100644 index 0000000..3245a37 --- /dev/null +++ b/exercise.wwwapi/DTO/Interfaces/IDTO_Request_delete.cs @@ -0,0 +1,13 @@ +using api_cinema_challenge.Models; +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; + +namespace api_cinema_challenge.DTO.Interfaces +{ + public interface IDTO_Request_delete + where DTO_Type : IDTO_Request_delete + where Model_type : class, ICustomModel, new() + { + public abstract static Task delete(IRepository repo, params object[] id); + } +} diff --git a/exercise.wwwapi/DTO/Interfaces/IDTO_Request_update.cs b/exercise.wwwapi/DTO/Interfaces/IDTO_Request_update.cs new file mode 100644 index 0000000..7c78150 --- /dev/null +++ b/exercise.wwwapi/DTO/Interfaces/IDTO_Request_update.cs @@ -0,0 +1,12 @@ +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; + +namespace api_cinema_challenge.DTO.Interfaces +{ + public interface IDTO_Request_update + where DTO_Type : IDTO_Request_update + where Model_Type : class,ICustomModel, new() + { + public abstract static Task update(DTO_Type dto, IRepository repo, params object[] id); + } +} diff --git a/exercise.wwwapi/DTO/Payload.cs b/exercise.wwwapi/DTO/Payload.cs new file mode 100644 index 0000000..c34d068 --- /dev/null +++ b/exercise.wwwapi/DTO/Payload.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace api_cinema_challenge.DTO +{ + + public class Payload + { + public Payload() + { + } + public string Status { get; set; } + public T Data { get; set; } + } + +} diff --git a/exercise.wwwapi/DTO/Request/Create_User.cs b/exercise.wwwapi/DTO/Request/Create_User.cs new file mode 100644 index 0000000..cdf5b3f --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Create_User.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations.Schema; +using api_cinema_challenge.DTO.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; + +namespace exercise.wwwapi.DTO.Request +{ + public class Create_User : IDTO_Request_create + { + public string Username { get; set; } + public string PasswordHash { get; set; } + public string Email { get; set; } + + public static Task create(IRepository repo, Create_User dto, params object[] pathargs) + { + User u = new User + { + Username = dto.Username, + PasswordHash = dto.PasswordHash, + Email = dto.Email + }; + return repo.CreateEntry(u); + } + + + } +} diff --git a/exercise.wwwapi/DTO/Response/Get_User.cs b/exercise.wwwapi/DTO/Response/Get_User.cs new file mode 100644 index 0000000..7761a3f --- /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; } + + public 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 index ed2a68f..35a6cf0 100644 --- a/exercise.wwwapi/Data/DatabaseContext.cs +++ b/exercise.wwwapi/Data/DatabaseContext.cs @@ -1,4 +1,5 @@ using exercise.wwwapi.Configuration; +using exercise.wwwapi.Models; using Microsoft.EntityFrameworkCore; namespace exercise.wwwapi.Data @@ -17,5 +18,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) optionsBuilder.UseNpgsql(_conf.GetValue("ConnectionStrings:DefaultConnectionString")!); base.OnConfiguring(optionsBuilder); } + + public DbSet Users { get; set; } } } diff --git a/exercise.wwwapi/Endpoints/UserEndpoints.cs b/exercise.wwwapi/Endpoints/UserEndpoints.cs new file mode 100644 index 0000000..a4bb49f --- /dev/null +++ b/exercise.wwwapi/Endpoints/UserEndpoints.cs @@ -0,0 +1,25 @@ + +using api_cinema_challenge.DTO; +using api_cinema_challenge.Repository; +using exercise.wwwapi.DTO.Request; +using exercise.wwwapi.DTO.Response; +using exercise.wwwapi.Models; + +namespace exercise.wwwapi.Endpoints +{ + public static class UserEndpoints + { + public static void ConfigureUserEndpoints(this WebApplication app) + { + var usergroup = app.MapGroup("user"); + usergroup.MapPost("/", CreateUser); + } + + private static async Task CreateUser(HttpContext context, IRepository repo, Create_User dto) + { + User? user = await Create_User.create(repo, dto); + return user == null ? TypedResults.BadRequest() : TypedResults.Ok(Get_User.toPayload(user)); + + } + } +} diff --git a/exercise.wwwapi/Extensions/Http_context_extension.cs b/exercise.wwwapi/Extensions/Http_context_extension.cs new file mode 100644 index 0000000..e6cdc4f --- /dev/null +++ b/exercise.wwwapi/Extensions/Http_context_extension.cs @@ -0,0 +1,13 @@ +namespace api_cinema_challenge.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/Interfaces/ICustomModel.cs b/exercise.wwwapi/Models/Interfaces/ICustomModel.cs new file mode 100644 index 0000000..2cccf92 --- /dev/null +++ b/exercise.wwwapi/Models/Interfaces/ICustomModel.cs @@ -0,0 +1,6 @@ +namespace api_cinema_challenge.Models.Interfaces +{ + public interface ICustomModel + { + } +} diff --git a/exercise.wwwapi/Models/User.cs b/exercise.wwwapi/Models/User.cs new file mode 100644 index 0000000..609257a --- /dev/null +++ b/exercise.wwwapi/Models/User.cs @@ -0,0 +1,18 @@ +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; } + } +} diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 9ab0441..97dfe09 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -1,6 +1,9 @@ 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.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.IdentityModel.Tokens; @@ -26,6 +29,7 @@ // AddScoped builder.Services.AddScoped(); +builder.Services.AddScoped, Repository>(); builder.Services.AddDbContext(); var conf = new ConfigurationSettings(); @@ -70,5 +74,6 @@ app.UseAuthentication(); app.UseAuthorization(); +app.ConfigureUserEndpoints(); 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..19fd31b --- /dev/null +++ b/exercise.wwwapi/Repository/IRepository.cs @@ -0,0 +1,15 @@ +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..21eae33 --- /dev/null +++ b/exercise.wwwapi/Repository/Repository.cs @@ -0,0 +1,74 @@ +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; + } + + + + } +} From b192ef91ae83fdeb3c16cda2b6e08f5e4e4c52f6 Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Thu, 30 Jan 2025 18:13:20 +0100 Subject: [PATCH 07/12] Added DTO, handle payload fail scenario... --- .../DTO/Interfaces/DTO_Auth_Request.cs | 35 +++++++++++++ .../DTO/Interfaces/DTO_Response.cs | 1 + .../DTO/Request/Auth_Login_User.cs | 51 +++++++++++++++++++ exercise.wwwapi/DTO/Request/Create_User.cs | 4 +- exercise.wwwapi/DTO/Response/Fail.cs | 43 ++++++++++++++++ exercise.wwwapi/Endpoints/AuthEndpoints.cs | 43 ++++++++++++++++ exercise.wwwapi/Endpoints/UserEndpoints.cs | 25 --------- 7 files changed, 175 insertions(+), 27 deletions(-) create mode 100644 exercise.wwwapi/DTO/Interfaces/DTO_Auth_Request.cs create mode 100644 exercise.wwwapi/DTO/Request/Auth_Login_User.cs create mode 100644 exercise.wwwapi/DTO/Response/Fail.cs create mode 100644 exercise.wwwapi/Endpoints/AuthEndpoints.cs delete mode 100644 exercise.wwwapi/Endpoints/UserEndpoints.cs diff --git a/exercise.wwwapi/DTO/Interfaces/DTO_Auth_Request.cs b/exercise.wwwapi/DTO/Interfaces/DTO_Auth_Request.cs new file mode 100644 index 0000000..2496003 --- /dev/null +++ b/exercise.wwwapi/DTO/Interfaces/DTO_Auth_Request.cs @@ -0,0 +1,35 @@ +using System.Net; +using System.Text.Json.Serialization; +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Configuration; + +namespace api_cinema_challenge.DTO.Interfaces +{ + 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.get(repo); + if (model == null) throw new HttpRequestException("requested object does not exist", null, HttpStatusCode.NotFound); + if (!await dto.verify(repo, model)) throw new HttpRequestException("Wrong password", null, HttpStatusCode.NotFound); + + dto._auth_token = await dto.createToken(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 get(IRepository repo); + protected abstract Task verify(IRepository repo, Model_Type model); + protected abstract Task createToken(IRepository repo, Model_Type model, IConfigurationSettings conf); + } +} diff --git a/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs b/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs index f106650..b4caf79 100644 --- a/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs +++ b/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs @@ -13,6 +13,7 @@ public abstract class DTO_Response : IDTO_Respons toPayload(Model_type model, string status = "success") { var a = new DTO_Type(); 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..4d7f369 --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Auth_Login_User.cs @@ -0,0 +1,51 @@ +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 createToken(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), + }; + + 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 get(IRepository repo) + { + return await repo.GetEntry(x => x.Where(x => x.Username == this.Username)); + } + + protected override async Task verify(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_User.cs b/exercise.wwwapi/DTO/Request/Create_User.cs index cdf5b3f..c8d092a 100644 --- a/exercise.wwwapi/DTO/Request/Create_User.cs +++ b/exercise.wwwapi/DTO/Request/Create_User.cs @@ -8,7 +8,7 @@ namespace exercise.wwwapi.DTO.Request public class Create_User : IDTO_Request_create { public string Username { get; set; } - public string PasswordHash { get; set; } + public string Password { get; set; } public string Email { get; set; } public static Task create(IRepository repo, Create_User dto, params object[] pathargs) @@ -16,7 +16,7 @@ public class Create_User : IDTO_Request_create User u = new User { Username = dto.Username, - PasswordHash = dto.PasswordHash, + PasswordHash = BCrypt.Net.BCrypt.HashPassword(dto.Password), Email = dto.Email }; return repo.CreateEntry(u); diff --git a/exercise.wwwapi/DTO/Response/Fail.cs b/exercise.wwwapi/DTO/Response/Fail.cs new file mode 100644 index 0000000..15a2a78 --- /dev/null +++ b/exercise.wwwapi/DTO/Response/Fail.cs @@ -0,0 +1,43 @@ +using System.Net; +using api_cinema_challenge.DTO; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace exercise.wwwapi.DTO.Response +{ + 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.InternalServerError , TypedResults.InternalServerError } + }; + + static public IResult Payload(string msg, Func,IResult> func, params object[] args) + { + 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 { Message = ex.Message }; + + if (Fail.keys.ContainsKey(ex.StatusCode.Value)) + return keys[ex.StatusCode.Value].Invoke(failLoad); + + return TypedResults.BadRequest(failLoad); + } + + + + + } +} diff --git a/exercise.wwwapi/Endpoints/AuthEndpoints.cs b/exercise.wwwapi/Endpoints/AuthEndpoints.cs new file mode 100644 index 0000000..5ba59ad --- /dev/null +++ b/exercise.wwwapi/Endpoints/AuthEndpoints.cs @@ -0,0 +1,43 @@ + +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 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, 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); + User? user = await Create_User.create(repo, dto); + return user == null ? TypedResults.BadRequest() : TypedResults.Ok(Get_User.toPayload(user)); + + } + } +} diff --git a/exercise.wwwapi/Endpoints/UserEndpoints.cs b/exercise.wwwapi/Endpoints/UserEndpoints.cs deleted file mode 100644 index a4bb49f..0000000 --- a/exercise.wwwapi/Endpoints/UserEndpoints.cs +++ /dev/null @@ -1,25 +0,0 @@ - -using api_cinema_challenge.DTO; -using api_cinema_challenge.Repository; -using exercise.wwwapi.DTO.Request; -using exercise.wwwapi.DTO.Response; -using exercise.wwwapi.Models; - -namespace exercise.wwwapi.Endpoints -{ - public static class UserEndpoints - { - public static void ConfigureUserEndpoints(this WebApplication app) - { - var usergroup = app.MapGroup("user"); - usergroup.MapPost("/", CreateUser); - } - - private static async Task CreateUser(HttpContext context, IRepository repo, Create_User dto) - { - User? user = await Create_User.create(repo, dto); - return user == null ? TypedResults.BadRequest() : TypedResults.Ok(Get_User.toPayload(user)); - - } - } -} From 61d6b83d9fa0a126542e61fe051a32ab845c8c02 Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Thu, 30 Jan 2025 18:38:58 +0100 Subject: [PATCH 08/12] added BlogPosts model and endpoints --- exercise.wwwapi/Data/DatabaseContext.cs | 1 + exercise.wwwapi/Endpoints/BlogEndpoints.cs | 40 ++++++++++++++++++++++ exercise.wwwapi/Models/BlogPost.cs | 19 ++++++++++ 3 files changed, 60 insertions(+) create mode 100644 exercise.wwwapi/Endpoints/BlogEndpoints.cs create mode 100644 exercise.wwwapi/Models/BlogPost.cs diff --git a/exercise.wwwapi/Data/DatabaseContext.cs b/exercise.wwwapi/Data/DatabaseContext.cs index 35a6cf0..670645e 100644 --- a/exercise.wwwapi/Data/DatabaseContext.cs +++ b/exercise.wwwapi/Data/DatabaseContext.cs @@ -20,5 +20,6 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) } public DbSet Users { get; set; } + public DbSet BlogPosts { get; set; } } } diff --git a/exercise.wwwapi/Endpoints/BlogEndpoints.cs b/exercise.wwwapi/Endpoints/BlogEndpoints.cs new file mode 100644 index 0000000..4bf9980 --- /dev/null +++ b/exercise.wwwapi/Endpoints/BlogEndpoints.cs @@ -0,0 +1,40 @@ +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; + +namespace exercise.wwwapi.Endpoints +{ + public static class BlogEndpoints + { + public static void ConfigureBlogEndpoints(this WebApplication app) + { + var usergroup = app.MapGroup("/blogposts"); + usergroup.MapPost("/", CreatePost); + usergroup.MapPut("/{id}", EditPost); + usergroup.MapGet("/", GetAllPosts); + } + + [Authorize] + private static async Task EditPost(HttpContext context, IRepository repo) + { + return TypedResults.Ok(); + } + + [Authorize] + private static async Task GetAllPosts(HttpContext context, IRepository repo) + { + return TypedResults.Ok(); + } + + [Authorize] + private static async Task CreatePost(HttpContext context, IRepository repo) + { + return TypedResults.Ok(); + + } + } +} diff --git a/exercise.wwwapi/Models/BlogPost.cs b/exercise.wwwapi/Models/BlogPost.cs new file mode 100644 index 0000000..ab28767 --- /dev/null +++ b/exercise.wwwapi/Models/BlogPost.cs @@ -0,0 +1,19 @@ +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] + public int Id { get; set; } + [Column("user_id")] + public int AuthorId { get; set; } + [Column("text")] + public string Text { get; set; } + + + } +} From dca3bf3fffd2be2996703dc7ffcfa1799fb1112e Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Fri, 31 Jan 2025 13:23:07 +0100 Subject: [PATCH 09/12] Added SecurityRequirment for Swagger... otherwise requests was never authorized --- exercise.wwwapi/Program.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 97dfe09..493b667 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -24,7 +24,20 @@ In = ParameterLocation.Header, Scheme = "Bearer" }); - + setupActions.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); }); // AddScoped From c0f6353b5268910d78e66e9c1cad14b239c0d911 Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Fri, 31 Jan 2025 13:23:48 +0100 Subject: [PATCH 10/12] Added: extension methood, endpoints, payload, request/response dtos and more --- .../DTO/Interfaces/DTO_Request_create.cs | 20 +++++++ .../DTO/Interfaces/DTO_Request_update.cs | 22 ++++++++ .../DTO/Interfaces/DTO_Response.cs | 53 +++++++++++++++---- .../DTO/Interfaces/IDTO_Request_create.cs | 13 ----- .../DTO/Interfaces/IDTO_Request_update.cs | 12 ----- exercise.wwwapi/DTO/Payload.cs | 4 +- .../DTO/Request/Create_BlogPost.cs | 22 ++++++++ exercise.wwwapi/DTO/Request/Create_User.cs | 15 +++--- .../DTO/Request/Update_BlogPost.cs | 27 ++++++++++ exercise.wwwapi/DTO/Response/Get_BlogPost.cs | 20 +++++++ exercise.wwwapi/Endpoints/AuthEndpoints.cs | 12 +++-- exercise.wwwapi/Endpoints/BlogEndpoints.cs | 32 ++++++++--- .../Extensions/Http_context_extension.cs | 2 +- exercise.wwwapi/Models/BlogPost.cs | 2 +- exercise.wwwapi/Program.cs | 6 ++- 15 files changed, 202 insertions(+), 60 deletions(-) create mode 100644 exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs create mode 100644 exercise.wwwapi/DTO/Interfaces/DTO_Request_update.cs delete mode 100644 exercise.wwwapi/DTO/Interfaces/IDTO_Request_create.cs delete mode 100644 exercise.wwwapi/DTO/Interfaces/IDTO_Request_update.cs create mode 100644 exercise.wwwapi/DTO/Request/Create_BlogPost.cs create mode 100644 exercise.wwwapi/DTO/Request/Update_BlogPost.cs create mode 100644 exercise.wwwapi/DTO/Response/Get_BlogPost.cs diff --git a/exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs b/exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs new file mode 100644 index 0000000..8c8e457 --- /dev/null +++ b/exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs @@ -0,0 +1,20 @@ +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; + +namespace api_cinema_challenge.DTO.Interfaces +{ + public abstract class DTO_Request_create + where Model_type : class, ICustomModel, new() + { + public abstract Model_type returnNewInstanceModel(params object[] pathargs); + public async Task Create(IRepository repo, params object[] pathargs) + { + var model = returnNewInstanceModel(); + var createdEntity = await repo.CreateEntry(model); + if (createdEntity == null) throw new HttpRequestException("Bad Creation request", null, System.Net.HttpStatusCode.BadRequest); + + return createdEntity; + } + } +} diff --git a/exercise.wwwapi/DTO/Interfaces/DTO_Request_update.cs b/exercise.wwwapi/DTO/Interfaces/DTO_Request_update.cs new file mode 100644 index 0000000..4400b5d --- /dev/null +++ b/exercise.wwwapi/DTO/Interfaces/DTO_Request_update.cs @@ -0,0 +1,22 @@ +using System.Net; +using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; + +namespace api_cinema_challenge.DTO.Interfaces +{ + public abstract class DTO_Request_update + where Model_Type : class,ICustomModel, new() + { + protected abstract Func, IQueryable> getId(params object[] id); + protected abstract Model_Type returnUpdatedInstanceModel(Model_Type originalModelData); + public async Task Update(IRepository repo, 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); + var model = returnUpdatedInstanceModel(fetchedModel); + + return await repo.UpdateEntry(query, model); + } + } +} diff --git a/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs b/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs index b4caf79..7fb5829 100644 --- a/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs +++ b/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs @@ -1,5 +1,6 @@ using api_cinema_challenge.Models; using api_cinema_challenge.Models.Interfaces; +using api_cinema_challenge.Repository; using Microsoft.EntityFrameworkCore.Metadata; namespace api_cinema_challenge.DTO.Interfaces @@ -7,31 +8,65 @@ namespace api_cinema_challenge.DTO.Interfaces public interface IDTO_Respons { public void Initialize(Y model); } - public abstract class DTO_Response : IDTO_Respons - where DTO_Type : DTO_Response, new() - where Model_type : class, ICustomModel + public abstract class DTO_Response : IDTO_Respons + where DTO_Type : DTO_Response, new() + where Model_Type : class, ICustomModel { public DTO_Response() { } - public abstract void Initialize(Model_type model); - - public static Payload toPayload(Model_type model, string status = "success") + public abstract void Initialize(Model_Type model); + + public static async Task> Gets(IRepository repo, Func, IQueryable>? WhereQuery = null) + { + IEnumerable list; + if (WhereQuery == null) list = await repo.GetEntries(); + else list = await repo.GetEntries(WhereQuery); + + return list.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(); + var p = new Payload(); p.Data = a; p.Status = status; return p; } - public static Payload, Model_type> toPayload(IEnumerable models, string status = "success") + 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>(); + 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, Func, IQueryable>? WhereQuery = null, 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/Interfaces/IDTO_Request_create.cs b/exercise.wwwapi/DTO/Interfaces/IDTO_Request_create.cs deleted file mode 100644 index ddd2120..0000000 --- a/exercise.wwwapi/DTO/Interfaces/IDTO_Request_create.cs +++ /dev/null @@ -1,13 +0,0 @@ -using api_cinema_challenge.Models.Interfaces; -using api_cinema_challenge.Repository; -using exercise.wwwapi.Models; - -namespace api_cinema_challenge.DTO.Interfaces -{ - public interface IDTO_Request_create - where DTO_Type : IDTO_Request_create - where Model_type : class, ICustomModel, new() - { - public abstract static Task create(IRepository repo, DTO_Type dto, params object[] pathargs); - } -} diff --git a/exercise.wwwapi/DTO/Interfaces/IDTO_Request_update.cs b/exercise.wwwapi/DTO/Interfaces/IDTO_Request_update.cs deleted file mode 100644 index 7c78150..0000000 --- a/exercise.wwwapi/DTO/Interfaces/IDTO_Request_update.cs +++ /dev/null @@ -1,12 +0,0 @@ -using api_cinema_challenge.Models.Interfaces; -using api_cinema_challenge.Repository; - -namespace api_cinema_challenge.DTO.Interfaces -{ - public interface IDTO_Request_update - where DTO_Type : IDTO_Request_update - where Model_Type : class,ICustomModel, new() - { - public abstract static Task update(DTO_Type dto, IRepository repo, params object[] id); - } -} diff --git a/exercise.wwwapi/DTO/Payload.cs b/exercise.wwwapi/DTO/Payload.cs index c34d068..8c6c1e2 100644 --- a/exercise.wwwapi/DTO/Payload.cs +++ b/exercise.wwwapi/DTO/Payload.cs @@ -6,9 +6,7 @@ namespace api_cinema_challenge.DTO public class Payload { - public Payload() - { - } + public Payload(){} public string Status { get; set; } public T Data { get; set; } } diff --git a/exercise.wwwapi/DTO/Request/Create_BlogPost.cs b/exercise.wwwapi/DTO/Request/Create_BlogPost.cs new file mode 100644 index 0000000..19b0df3 --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Create_BlogPost.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations.Schema; +using api_cinema_challenge.DTO.Interfaces; +using api_cinema_challenge.Repository; +using exercise.wwwapi.Models; + +namespace exercise.wwwapi.DTO.Request +{ + public class Create_BlogPost : DTO_Request_create + { + public int AuthorId { get; set; } + public string Text { get; set; } + + public override BlogPost returnNewInstanceModel(params object[] pathargs) + { + return new BlogPost + { + AuthorId = this.AuthorId, + Text = this.Text + }; + } + } +} diff --git a/exercise.wwwapi/DTO/Request/Create_User.cs b/exercise.wwwapi/DTO/Request/Create_User.cs index c8d092a..602ad9b 100644 --- a/exercise.wwwapi/DTO/Request/Create_User.cs +++ b/exercise.wwwapi/DTO/Request/Create_User.cs @@ -5,23 +5,20 @@ namespace exercise.wwwapi.DTO.Request { - public class Create_User : IDTO_Request_create + public class Create_User : DTO_Request_create { public string Username { get; set; } public string Password { get; set; } public string Email { get; set; } - public static Task create(IRepository repo, Create_User dto, params object[] pathargs) + public override User returnNewInstanceModel(params object[] pathargs) { - User u = new User + return new User { - Username = dto.Username, - PasswordHash = BCrypt.Net.BCrypt.HashPassword(dto.Password), - Email = dto.Email + Username = this.Username, + PasswordHash = BCrypt.Net.BCrypt.HashPassword(this.Password), + Email = this.Email }; - return repo.CreateEntry(u); } - - } } diff --git a/exercise.wwwapi/DTO/Request/Update_BlogPost.cs b/exercise.wwwapi/DTO/Request/Update_BlogPost.cs new file mode 100644 index 0000000..b9fa02b --- /dev/null +++ b/exercise.wwwapi/DTO/Request/Update_BlogPost.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations.Schema; +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 returnUpdatedInstanceModel(BlogPost originalModelData) + { + return new BlogPost + { + Id = originalModelData.Id, + AuthorId = originalModelData.AuthorId, + Text = this.Text ?? originalModelData.Text, + }; + } + } +} diff --git a/exercise.wwwapi/DTO/Response/Get_BlogPost.cs b/exercise.wwwapi/DTO/Response/Get_BlogPost.cs new file mode 100644 index 0000000..1cd91ae --- /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; } + + public override void Initialize(BlogPost model) + { + Id = model.Id; + AuthorId = model.AuthorId; + Text = model.Text; + } + } +} diff --git a/exercise.wwwapi/Endpoints/AuthEndpoints.cs b/exercise.wwwapi/Endpoints/AuthEndpoints.cs index 5ba59ad..9079c1e 100644 --- a/exercise.wwwapi/Endpoints/AuthEndpoints.cs +++ b/exercise.wwwapi/Endpoints/AuthEndpoints.cs @@ -35,9 +35,15 @@ private static async Task LoginUser(HttpContext context, IRepository CreateUser(HttpContext context, IRepository repo, 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); - User? user = await Create_User.create(repo, dto); - return user == null ? TypedResults.BadRequest() : TypedResults.Ok(Get_User.toPayload(user)); - + try + { + User user = await dto.Create(repo); + return TypedResults.Ok(Get_User.toPayload(user)); + } + catch (HttpRequestException ex) + { + return Fail.Payload(ex); + } } } } diff --git a/exercise.wwwapi/Endpoints/BlogEndpoints.cs b/exercise.wwwapi/Endpoints/BlogEndpoints.cs index 4bf9980..3c42e5a 100644 --- a/exercise.wwwapi/Endpoints/BlogEndpoints.cs +++ b/exercise.wwwapi/Endpoints/BlogEndpoints.cs @@ -1,4 +1,5 @@ -using api_cinema_challenge.Repository; +using wwwapi.Extensions; +using api_cinema_challenge.Repository; using exercise.wwwapi.Configuration; using exercise.wwwapi.DTO.Request; using exercise.wwwapi.DTO.Response; @@ -19,21 +20,38 @@ public static void ConfigureBlogEndpoints(this WebApplication app) } [Authorize] - private static async Task EditPost(HttpContext context, IRepository repo) + private static async Task EditPost(HttpContext context, IRepository repo, Update_BlogPost dto, int id ) { - return TypedResults.Ok(); + try + { + var updated = await dto.Update(repo, id); + return TypedResults.Created(context.Get_endpointUrl(id),Get_BlogPost.toPayload(updated)); + } + catch (HttpRequestException ex) + { + return Fail.Payload(ex); + } } [Authorize] - private static async Task GetAllPosts(HttpContext context, IRepository repo) + private static async Task GetAllPosts(HttpContext context, IRepository repo) { - return TypedResults.Ok(); + + return TypedResults.Ok(await Get_BlogPost.toPayload(repo)); } [Authorize] - private static async Task CreatePost(HttpContext context, IRepository repo) + private static async Task CreatePost(HttpContext context, IRepository repo, Create_BlogPost dto) { - return TypedResults.Ok(); + try + { + BlogPost post = await dto.Create(repo); + return TypedResults.Created(context.Get_endpointUrl(post.Id), Get_BlogPost.toPayload(post)); + } + 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 index e6cdc4f..591baca 100644 --- a/exercise.wwwapi/Extensions/Http_context_extension.cs +++ b/exercise.wwwapi/Extensions/Http_context_extension.cs @@ -1,4 +1,4 @@ -namespace api_cinema_challenge.Extensions +namespace wwwapi.Extensions { public static class Http_context_extension { diff --git a/exercise.wwwapi/Models/BlogPost.cs b/exercise.wwwapi/Models/BlogPost.cs index ab28767..7b4d2c5 100644 --- a/exercise.wwwapi/Models/BlogPost.cs +++ b/exercise.wwwapi/Models/BlogPost.cs @@ -7,7 +7,7 @@ namespace exercise.wwwapi.Models [Table("blog_posts")] public class BlogPost :ICustomModel { - [Key] + [Key, Column("id")] public int Id { get; set; } [Column("user_id")] public int AuthorId { get; set; } diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 493b667..a27265a 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -38,11 +38,12 @@ Array.Empty() } }); -}); + }); // AddScoped builder.Services.AddScoped(); builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); builder.Services.AddDbContext(); var conf = new ConfigurationSettings(); @@ -87,6 +88,7 @@ app.UseAuthentication(); app.UseAuthorization(); -app.ConfigureUserEndpoints(); +app.ConfigureBlogEndpoints(); +app.ConfigureAuthEndpoints(); app.Run(); \ No newline at end of file From 8e3fee95310af00658d08fc132777aca314041be Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Fri, 31 Jan 2025 14:11:04 +0100 Subject: [PATCH 11/12] Core is complete --- .../DTO/Interfaces/DTO_Request_create.cs | 2 +- .../DTO/Request/Create_BlogPost.cs | 3 +- exercise.wwwapi/Data/DatabaseContext.cs | 18 +++++++++- exercise.wwwapi/Data/Seeder.cs | 36 +++++++++++++++++++ exercise.wwwapi/Endpoints/BlogEndpoints.cs | 5 +-- exercise.wwwapi/Models/BlogPost.cs | 3 +- exercise.wwwapi/Models/User.cs | 3 ++ 7 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 exercise.wwwapi/Data/Seeder.cs diff --git a/exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs b/exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs index 8c8e457..49e39ea 100644 --- a/exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs +++ b/exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs @@ -10,7 +10,7 @@ public abstract class DTO_Request_create public abstract Model_type returnNewInstanceModel(params object[] pathargs); public async Task Create(IRepository repo, params object[] pathargs) { - var model = returnNewInstanceModel(); + var model = returnNewInstanceModel(pathargs); var createdEntity = await repo.CreateEntry(model); if (createdEntity == null) throw new HttpRequestException("Bad Creation request", null, System.Net.HttpStatusCode.BadRequest); diff --git a/exercise.wwwapi/DTO/Request/Create_BlogPost.cs b/exercise.wwwapi/DTO/Request/Create_BlogPost.cs index 19b0df3..97f4b32 100644 --- a/exercise.wwwapi/DTO/Request/Create_BlogPost.cs +++ b/exercise.wwwapi/DTO/Request/Create_BlogPost.cs @@ -7,14 +7,13 @@ namespace exercise.wwwapi.DTO.Request { public class Create_BlogPost : DTO_Request_create { - public int AuthorId { get; set; } public string Text { get; set; } public override BlogPost returnNewInstanceModel(params object[] pathargs) { return new BlogPost { - AuthorId = this.AuthorId, + AuthorId = (int)pathargs[0], Text = this.Text }; } diff --git a/exercise.wwwapi/Data/DatabaseContext.cs b/exercise.wwwapi/Data/DatabaseContext.cs index 670645e..700e853 100644 --- a/exercise.wwwapi/Data/DatabaseContext.cs +++ b/exercise.wwwapi/Data/DatabaseContext.cs @@ -12,11 +12,27 @@ public DatabaseContext(DbContextOptions options, IConfigurationSettings conf) : _conf = conf; this.Database.EnsureCreated(); } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + + modelBuilder.Entity() + .HasOne(p => p.User) + .WithMany(p => p.BlogPosts) + .HasForeignKey(p => p.AuthorId); + // Seed data + Seeder seeder = new Seeder(); + modelBuilder.Entity(). + HasData(seeder.Users); + modelBuilder.Entity(). + HasData(seeder.BlogPosts); + + base.OnModelCreating(modelBuilder); + } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql(_conf.GetValue("ConnectionStrings:DefaultConnectionString")!); - base.OnConfiguring(optionsBuilder); + base.OnConfiguring(optionsBuilder); } public DbSet Users { get; set; } diff --git a/exercise.wwwapi/Data/Seeder.cs b/exercise.wwwapi/Data/Seeder.cs new file mode 100644 index 0000000..56bb4d3 --- /dev/null +++ b/exercise.wwwapi/Data/Seeder.cs @@ -0,0 +1,36 @@ +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")}, + new User{ Id = 2, + Username = "Flurp", + Email = "flurp@bob.bob" , + PasswordHash= BCrypt.Net.BCrypt.HashPassword("Oaks123")}, + new User{ Id = 3, + Username = "Glorp", + Email = "glorp@bob.bob" , + PasswordHash= BCrypt.Net.BCrypt.HashPassword("Glorps first post!")} + }; + 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... "}, + }; + } + public List BlogPosts { get; set; } + public List Users { get; set; } + } +} diff --git a/exercise.wwwapi/Endpoints/BlogEndpoints.cs b/exercise.wwwapi/Endpoints/BlogEndpoints.cs index 3c42e5a..7cff4a3 100644 --- a/exercise.wwwapi/Endpoints/BlogEndpoints.cs +++ b/exercise.wwwapi/Endpoints/BlogEndpoints.cs @@ -6,6 +6,7 @@ using exercise.wwwapi.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; +using System.Security.Claims; namespace exercise.wwwapi.Endpoints { @@ -41,11 +42,11 @@ private static async Task GetAllPosts(HttpContext context, IRepository< } [Authorize] - private static async Task CreatePost(HttpContext context, IRepository repo, Create_BlogPost dto) + private static async Task CreatePost(HttpContext context, IRepository repo, ClaimsPrincipal user, Create_BlogPost dto) { try { - BlogPost post = await dto.Create(repo); + BlogPost post = await dto.Create(repo, int.Parse(user.FindFirst(ClaimTypes.Sid).Value)); return TypedResults.Created(context.Get_endpointUrl(post.Id), Get_BlogPost.toPayload(post)); } catch (HttpRequestException ex) diff --git a/exercise.wwwapi/Models/BlogPost.cs b/exercise.wwwapi/Models/BlogPost.cs index 7b4d2c5..4dca46e 100644 --- a/exercise.wwwapi/Models/BlogPost.cs +++ b/exercise.wwwapi/Models/BlogPost.cs @@ -13,7 +13,8 @@ public class BlogPost :ICustomModel public int AuthorId { get; set; } [Column("text")] public string Text { get; set; } - + // Navigation properties + public User User { get; set; } } } diff --git a/exercise.wwwapi/Models/User.cs b/exercise.wwwapi/Models/User.cs index 609257a..2a81fe3 100644 --- a/exercise.wwwapi/Models/User.cs +++ b/exercise.wwwapi/Models/User.cs @@ -14,5 +14,8 @@ public class User : ICustomModel public string PasswordHash { get; set; } [Column("email")] public string Email { get; set; } + + // Navigation properties + public List BlogPosts { get; set; } } } From 053f0ef2de06aea29a01735b07cfd03825919ded Mon Sep 17 00:00:00 2001 From: Lowe Raivio Date: Sat, 1 Feb 2025 21:41:41 +0100 Subject: [PATCH 12/12] Core + ALL Extensions completed... --- .../DTO_Auth_Request.cs | 16 +- .../DTO/AbstractClasses/DTO_Request_create.cs | 30 +++ .../DTO/AbstractClasses/DTO_Request_delete.cs | 28 +++ .../DTO_Request_update.cs | 12 +- .../DTO/AbstractClasses/DTO_Response.cs | 194 ++++++++++++++++++ .../DTO/Interfaces/DTO_Request_create.cs | 20 -- .../DTO/Interfaces/DTO_Response.cs | 72 ------- .../DTO/Interfaces/IDTO_Request_delete.cs | 13 -- .../DTO/Request/Auth_Login_User.cs | 7 +- .../DTO/Request/Create_BlogPost.cs | 11 +- .../DTO/Request/Create_BlogPostComment.cs | 42 ++++ .../DTO/Request/Create_Following.cs | 36 ++++ exercise.wwwapi/DTO/Request/Create_User.cs | 11 +- .../DTO/Request/Remove_Following.cs | 29 +++ .../DTO/Request/Update_BlogPost.cs | 11 +- exercise.wwwapi/DTO/Response/Get_BlogPost.cs | 2 +- .../DTO/Response/Get_BlogPostWithComments.cs | 29 +++ exercise.wwwapi/DTO/Response/Get_Comment.cs | 18 ++ exercise.wwwapi/DTO/Response/Get_Follows.cs | 21 ++ exercise.wwwapi/DTO/Response/Get_User.cs | 2 +- exercise.wwwapi/Data/DatabaseContext.cs | 31 ++- exercise.wwwapi/Data/Seeder.cs | 26 ++- exercise.wwwapi/Endpoints/AuthEndpoints.cs | 8 +- exercise.wwwapi/Endpoints/BlogEndpoints.cs | 30 ++- exercise.wwwapi/Endpoints/FollowEndpoints.cs | 71 +++++++ exercise.wwwapi/Models/BlogPost.cs | 1 + exercise.wwwapi/Models/Comment.cs | 23 +++ .../Models/Interfaces/ICustomModel.cs | 3 + exercise.wwwapi/Models/User.cs | 5 + exercise.wwwapi/Models/UserFollows.cs | 18 ++ .../{DTO/Response => Payload}/Fail.cs | 30 ++- exercise.wwwapi/{DTO => Payload}/Payload.cs | 6 +- exercise.wwwapi/Program.cs | 4 +- exercise.wwwapi/Repository/IRepository.cs | 3 +- exercise.wwwapi/Repository/Repository.cs | 3 +- exercise.wwwapi/exercise.wwwapi.csproj | 1 + 36 files changed, 718 insertions(+), 149 deletions(-) rename exercise.wwwapi/DTO/{Interfaces => AbstractClasses}/DTO_Auth_Request.cs (55%) create mode 100644 exercise.wwwapi/DTO/AbstractClasses/DTO_Request_create.cs create mode 100644 exercise.wwwapi/DTO/AbstractClasses/DTO_Request_delete.cs rename exercise.wwwapi/DTO/{Interfaces => AbstractClasses}/DTO_Request_update.cs (53%) create mode 100644 exercise.wwwapi/DTO/AbstractClasses/DTO_Response.cs delete mode 100644 exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs delete mode 100644 exercise.wwwapi/DTO/Interfaces/DTO_Response.cs delete mode 100644 exercise.wwwapi/DTO/Interfaces/IDTO_Request_delete.cs create mode 100644 exercise.wwwapi/DTO/Request/Create_BlogPostComment.cs create mode 100644 exercise.wwwapi/DTO/Request/Create_Following.cs create mode 100644 exercise.wwwapi/DTO/Request/Remove_Following.cs create mode 100644 exercise.wwwapi/DTO/Response/Get_BlogPostWithComments.cs create mode 100644 exercise.wwwapi/DTO/Response/Get_Comment.cs create mode 100644 exercise.wwwapi/DTO/Response/Get_Follows.cs create mode 100644 exercise.wwwapi/Endpoints/FollowEndpoints.cs create mode 100644 exercise.wwwapi/Models/Comment.cs create mode 100644 exercise.wwwapi/Models/UserFollows.cs rename exercise.wwwapi/{DTO/Response => Payload}/Fail.cs (62%) rename exercise.wwwapi/{DTO => Payload}/Payload.cs (66%) diff --git a/exercise.wwwapi/DTO/Interfaces/DTO_Auth_Request.cs b/exercise.wwwapi/DTO/AbstractClasses/DTO_Auth_Request.cs similarity index 55% rename from exercise.wwwapi/DTO/Interfaces/DTO_Auth_Request.cs rename to exercise.wwwapi/DTO/AbstractClasses/DTO_Auth_Request.cs index 2496003..abbb327 100644 --- a/exercise.wwwapi/DTO/Interfaces/DTO_Auth_Request.cs +++ b/exercise.wwwapi/DTO/AbstractClasses/DTO_Auth_Request.cs @@ -3,9 +3,13 @@ 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() { @@ -13,11 +17,11 @@ public abstract class DTO_Auth_Request private string? _auth_token = null; public static async Task authenticate(DTO_Auth_Request dto, IRepository repo, IConfigurationSettings conf) { - Model_Type? model = await dto.get(repo); + Model_Type? model = await dto.ReturnCreatedInstanceModel(repo); if (model == null) throw new HttpRequestException("requested object does not exist", null, HttpStatusCode.NotFound); - if (!await dto.verify(repo, model)) throw new HttpRequestException("Wrong password", null, HttpStatusCode.NotFound); + if (!await dto.VerifyRequestedModelAgainstDTO(repo, model)) throw new HttpRequestException("Wrong password", null, HttpStatusCode.NotFound); - dto._auth_token = await dto.createToken(repo, model, conf); + dto._auth_token = await dto.CreateAndReturnJWTToken(repo, model, conf); return dto._auth_token; } public static Payload toPayloadAuth(DTO_Auth_Request dto) @@ -28,8 +32,8 @@ public static Payload toPayloadAuth(DTO_Auth_Request return p; } - protected abstract Task get(IRepository repo); - protected abstract Task verify(IRepository repo, Model_Type model); - protected abstract Task createToken(IRepository repo, Model_Type model, IConfigurationSettings conf); + 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/Interfaces/DTO_Request_update.cs b/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_update.cs similarity index 53% rename from exercise.wwwapi/DTO/Interfaces/DTO_Request_update.cs rename to exercise.wwwapi/DTO/AbstractClasses/DTO_Request_update.cs index 4400b5d..810328c 100644 --- a/exercise.wwwapi/DTO/Interfaces/DTO_Request_update.cs +++ b/exercise.wwwapi/DTO/AbstractClasses/DTO_Request_update.cs @@ -1,20 +1,26 @@ 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 Model_Type returnUpdatedInstanceModel(Model_Type originalModelData); - public async Task Update(IRepository repo, 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 = returnUpdatedInstanceModel(fetchedModel); + 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/Interfaces/DTO_Request_create.cs b/exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs deleted file mode 100644 index 49e39ea..0000000 --- a/exercise.wwwapi/DTO/Interfaces/DTO_Request_create.cs +++ /dev/null @@ -1,20 +0,0 @@ -using api_cinema_challenge.Models.Interfaces; -using api_cinema_challenge.Repository; -using exercise.wwwapi.Models; - -namespace api_cinema_challenge.DTO.Interfaces -{ - public abstract class DTO_Request_create - where Model_type : class, ICustomModel, new() - { - public abstract Model_type returnNewInstanceModel(params object[] pathargs); - public async Task Create(IRepository repo, params object[] pathargs) - { - var model = returnNewInstanceModel(pathargs); - var createdEntity = await repo.CreateEntry(model); - if (createdEntity == null) throw new HttpRequestException("Bad Creation request", null, System.Net.HttpStatusCode.BadRequest); - - return createdEntity; - } - } -} diff --git a/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs b/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs deleted file mode 100644 index 7fb5829..0000000 --- a/exercise.wwwapi/DTO/Interfaces/DTO_Response.cs +++ /dev/null @@ -1,72 +0,0 @@ -using api_cinema_challenge.Models; -using api_cinema_challenge.Models.Interfaces; -using api_cinema_challenge.Repository; -using Microsoft.EntityFrameworkCore.Metadata; - -namespace api_cinema_challenge.DTO.Interfaces -{ - public interface IDTO_Respons { - public void Initialize(Y model); - } - public abstract class DTO_Response : IDTO_Respons - where DTO_Type : DTO_Response, new() - where Model_Type : class, ICustomModel - { - public DTO_Response() { } - public abstract void Initialize(Model_Type model); - - public static async Task> Gets(IRepository repo, Func, IQueryable>? WhereQuery = null) - { - IEnumerable list; - if (WhereQuery == null) list = await repo.GetEntries(); - else list = await repo.GetEntries(WhereQuery); - - return list.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, Func, IQueryable>? WhereQuery = null, 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/Interfaces/IDTO_Request_delete.cs b/exercise.wwwapi/DTO/Interfaces/IDTO_Request_delete.cs deleted file mode 100644 index 3245a37..0000000 --- a/exercise.wwwapi/DTO/Interfaces/IDTO_Request_delete.cs +++ /dev/null @@ -1,13 +0,0 @@ -using api_cinema_challenge.Models; -using api_cinema_challenge.Models.Interfaces; -using api_cinema_challenge.Repository; - -namespace api_cinema_challenge.DTO.Interfaces -{ - public interface IDTO_Request_delete - where DTO_Type : IDTO_Request_delete - where Model_type : class, ICustomModel, new() - { - public abstract static Task delete(IRepository repo, params object[] id); - } -} diff --git a/exercise.wwwapi/DTO/Request/Auth_Login_User.cs b/exercise.wwwapi/DTO/Request/Auth_Login_User.cs index 4d7f369..8546170 100644 --- a/exercise.wwwapi/DTO/Request/Auth_Login_User.cs +++ b/exercise.wwwapi/DTO/Request/Auth_Login_User.cs @@ -15,13 +15,14 @@ public class Auth_Login_User : DTO_Auth_Request public string Username { get; set; } public string Password { get; set; } - protected override async Task createToken(IRepository repo, User model, IConfigurationSettings conf) + 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")!)); @@ -35,12 +36,12 @@ protected override async Task createToken(IRepository repo, User m return jwt; } - protected override async Task get(IRepository repo) + protected override async Task ReturnCreatedInstanceModel(IRepository repo) { return await repo.GetEntry(x => x.Where(x => x.Username == this.Username)); } - protected override async Task verify(IRepository repo, User model) + protected override async Task VerifyRequestedModelAgainstDTO(IRepository repo, User model) { if (!BCrypt.Net.BCrypt.Verify(this.Password, model.PasswordHash)) return false; diff --git a/exercise.wwwapi/DTO/Request/Create_BlogPost.cs b/exercise.wwwapi/DTO/Request/Create_BlogPost.cs index 97f4b32..635b810 100644 --- a/exercise.wwwapi/DTO/Request/Create_BlogPost.cs +++ b/exercise.wwwapi/DTO/Request/Create_BlogPost.cs @@ -1,7 +1,9 @@ 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 { @@ -9,13 +11,18 @@ public class Create_BlogPost : DTO_Request_create { public string Text { get; set; } - public override BlogPost returnNewInstanceModel(params object[] pathargs) + public override BlogPost CreateAndReturnNewInstance(ClaimsPrincipal user,params object[] pathargs) { return new BlogPost { - AuthorId = (int)pathargs[0], + 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 index 602ad9b..658fa58 100644 --- a/exercise.wwwapi/DTO/Request/Create_User.cs +++ b/exercise.wwwapi/DTO/Request/Create_User.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Claims; using api_cinema_challenge.DTO.Interfaces; using api_cinema_challenge.Repository; using exercise.wwwapi.Models; @@ -11,14 +12,20 @@ public class Create_User : DTO_Request_create public string Password { get; set; } public string Email { get; set; } - public override User returnNewInstanceModel(params object[] pathargs) + 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 + 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 index b9fa02b..251f226 100644 --- a/exercise.wwwapi/DTO/Request/Update_BlogPost.cs +++ b/exercise.wwwapi/DTO/Request/Update_BlogPost.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations.Schema; +using System.Security.Claims; using api_cinema_challenge.DTO.Interfaces; using api_cinema_challenge.Repository; using exercise.wwwapi.Models; @@ -14,7 +15,7 @@ protected override Func, IQueryable> getId(params return x => x.Where(x => x.Id == (int)id[0]); } - protected override BlogPost returnUpdatedInstanceModel(BlogPost originalModelData) + protected override BlogPost CreateAndReturnUpdatedInstance(BlogPost originalModelData) { return new BlogPost { @@ -23,5 +24,13 @@ protected override BlogPost returnUpdatedInstanceModel(BlogPost originalModelDat 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 index 1cd91ae..4850032 100644 --- a/exercise.wwwapi/DTO/Response/Get_BlogPost.cs +++ b/exercise.wwwapi/DTO/Response/Get_BlogPost.cs @@ -10,7 +10,7 @@ public class Get_BlogPost : DTO_Response public int AuthorId { get; set; } public string Text { get; set; } - public override void Initialize(BlogPost model) + protected override void _Initialize(BlogPost model) { Id = model.Id; AuthorId = model.AuthorId; 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 index 7761a3f..277ea55 100644 --- a/exercise.wwwapi/DTO/Response/Get_User.cs +++ b/exercise.wwwapi/DTO/Response/Get_User.cs @@ -11,7 +11,7 @@ public class Get_User : DTO_Response public string PasswordHash { get; set; } public string Email { get; set; } - public override void Initialize(User model) + protected override void _Initialize(User model) { Id = model.Id; Username = model.Username; diff --git a/exercise.wwwapi/Data/DatabaseContext.cs b/exercise.wwwapi/Data/DatabaseContext.cs index 700e853..9baf2bd 100644 --- a/exercise.wwwapi/Data/DatabaseContext.cs +++ b/exercise.wwwapi/Data/DatabaseContext.cs @@ -1,4 +1,5 @@ -using exercise.wwwapi.Configuration; +using System.Xml.Linq; +using exercise.wwwapi.Configuration; using exercise.wwwapi.Models; using Microsoft.EntityFrameworkCore; @@ -14,18 +15,44 @@ public DatabaseContext(DbContextOptions options, IConfigurationSettings conf) : } 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); } @@ -37,5 +64,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder 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 index 56bb4d3..29362d4 100644 --- a/exercise.wwwapi/Data/Seeder.cs +++ b/exercise.wwwapi/Data/Seeder.cs @@ -11,15 +11,23 @@ public Seeder() new User{ Id = 1, Username = "Bob", Email = "Bob@bob.bob" , - PasswordHash= BCrypt.Net.BCrypt.HashPassword("Tacos_with_pudding_and_burger_and_lassanga")}, + 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")}, + PasswordHash= BCrypt.Net.BCrypt.HashPassword("Oaks123"), + Role = "User"}, new User{ Id = 3, Username = "Glorp", Email = "glorp@bob.bob" , - PasswordHash= BCrypt.Net.BCrypt.HashPassword("Glorps first post!")} + 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..."}, @@ -29,8 +37,20 @@ public Seeder() 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 index 9079c1e..51234b0 100644 --- a/exercise.wwwapi/Endpoints/AuthEndpoints.cs +++ b/exercise.wwwapi/Endpoints/AuthEndpoints.cs @@ -1,4 +1,5 @@  +using System.Security.Claims; using api_cinema_challenge.DTO; using api_cinema_challenge.DTO.Interfaces; using api_cinema_challenge.Repository; @@ -6,6 +7,7 @@ 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 @@ -32,13 +34,13 @@ private static async Task LoginUser(HttpContext context, IRepository CreateUser(HttpContext context, IRepository repo, Create_User dto) + 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 user = await dto.Create(repo); - return TypedResults.Ok(Get_User.toPayload(user)); + User createduser = await dto.Create(repo, user); + return TypedResults.Ok(Get_User.toPayload(createduser)); } catch (HttpRequestException ex) { diff --git a/exercise.wwwapi/Endpoints/BlogEndpoints.cs b/exercise.wwwapi/Endpoints/BlogEndpoints.cs index 7cff4a3..94641e4 100644 --- a/exercise.wwwapi/Endpoints/BlogEndpoints.cs +++ b/exercise.wwwapi/Endpoints/BlogEndpoints.cs @@ -7,6 +7,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using System.Security.Claims; +using System.Net; +using exercise.wwwapi.Payload; namespace exercise.wwwapi.Endpoints { @@ -16,16 +18,18 @@ 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, int id ) + private static async Task EditPost(HttpContext context, IRepository repo, Update_BlogPost dto, ClaimsPrincipal user, int id ) { try { - var updated = await dto.Update(repo, id); + var updated = await dto.Update(repo, user, id); return TypedResults.Created(context.Get_endpointUrl(id),Get_BlogPost.toPayload(updated)); } catch (HttpRequestException ex) @@ -33,6 +37,19 @@ private static async Task EditPost(HttpContext context, IRepository 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) @@ -41,12 +58,19 @@ private static async Task GetAllPosts(HttpContext context, IRepository< 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, int.Parse(user.FindFirst(ClaimTypes.Sid).Value)); + BlogPost post = await dto.Create(repo, user); return TypedResults.Created(context.Get_endpointUrl(post.Id), Get_BlogPost.toPayload(post)); } catch (HttpRequestException 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/Models/BlogPost.cs b/exercise.wwwapi/Models/BlogPost.cs index 4dca46e..a1f5f98 100644 --- a/exercise.wwwapi/Models/BlogPost.cs +++ b/exercise.wwwapi/Models/BlogPost.cs @@ -16,5 +16,6 @@ public class BlogPost :ICustomModel // 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 index 2cccf92..69d24b8 100644 --- a/exercise.wwwapi/Models/Interfaces/ICustomModel.cs +++ b/exercise.wwwapi/Models/Interfaces/ICustomModel.cs @@ -1,5 +1,8 @@ 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 index 2a81fe3..148021a 100644 --- a/exercise.wwwapi/Models/User.cs +++ b/exercise.wwwapi/Models/User.cs @@ -14,8 +14,13 @@ public class User : ICustomModel 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/DTO/Response/Fail.cs b/exercise.wwwapi/Payload/Fail.cs similarity index 62% rename from exercise.wwwapi/DTO/Response/Fail.cs rename to exercise.wwwapi/Payload/Fail.cs index 15a2a78..e50f284 100644 --- a/exercise.wwwapi/DTO/Response/Fail.cs +++ b/exercise.wwwapi/Payload/Fail.cs @@ -1,9 +1,11 @@ using System.Net; -using api_cinema_challenge.DTO; using Microsoft.AspNetCore.Http.HttpResults; -namespace exercise.wwwapi.DTO.Response +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>() @@ -13,31 +15,41 @@ static public class Fail {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, params object[] args) + 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 { Message = ex.Message }; - - if (Fail.keys.ContainsKey(ex.StatusCode.Value)) + 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/DTO/Payload.cs b/exercise.wwwapi/Payload/Payload.cs similarity index 66% rename from exercise.wwwapi/DTO/Payload.cs rename to exercise.wwwapi/Payload/Payload.cs index 8c6c1e2..cba276e 100644 --- a/exercise.wwwapi/DTO/Payload.cs +++ b/exercise.wwwapi/Payload/Payload.cs @@ -1,12 +1,12 @@ using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore.Metadata; -namespace api_cinema_challenge.DTO +namespace exercise.wwwapi.Payload { - public class Payload + public class Payload { - public 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 a27265a..be47644 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -5,7 +5,6 @@ using exercise.wwwapi.Endpoints; using exercise.wwwapi.Models; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; @@ -44,6 +43,8 @@ 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(); @@ -90,5 +91,6 @@ 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 index 19fd31b..92e82d2 100644 --- a/exercise.wwwapi/Repository/IRepository.cs +++ b/exercise.wwwapi/Repository/IRepository.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using api_cinema_challenge.Models.Interfaces; +using Microsoft.EntityFrameworkCore; namespace api_cinema_challenge.Repository { diff --git a/exercise.wwwapi/Repository/Repository.cs b/exercise.wwwapi/Repository/Repository.cs index 21eae33..effe995 100644 --- a/exercise.wwwapi/Repository/Repository.cs +++ b/exercise.wwwapi/Repository/Repository.cs @@ -1,4 +1,5 @@ -using exercise.wwwapi.Data; +using api_cinema_challenge.Models.Interfaces; +using exercise.wwwapi.Data; using Microsoft.EntityFrameworkCore; diff --git a/exercise.wwwapi/exercise.wwwapi.csproj b/exercise.wwwapi/exercise.wwwapi.csproj index 029d8f1..73d2ca6 100644 --- a/exercise.wwwapi/exercise.wwwapi.csproj +++ b/exercise.wwwapi/exercise.wwwapi.csproj @@ -10,6 +10,7 @@ +