From 8b77773005f9fa5a558f52e1940b97cc009e15a2 Mon Sep 17 00:00:00 2001 From: Sander Rasmussen Date: Thu, 30 Jan 2025 16:01:33 +0100 Subject: [PATCH 1/4] setting things up --- .../Configuration/ConfigurationSettings.cs | 17 +++ .../Configuration/IConfigurationSettings.cs | 7 ++ exercise.wwwapi/Data/DataContext.cs | 30 +++++ exercise.wwwapi/Endpoints/AuthApi.cs | 92 ++++++++++++++ exercise.wwwapi/Endpoints/SecureApi.cs | 29 +++++ .../Helpers/ClaimsPrincipalHelper.cs | 33 +++++ exercise.wwwapi/Models/Payload.cs | 12 ++ exercise.wwwapi/Models/User.cs | 17 +++ exercise.wwwapi/Models/UserRequestDto.cs | 12 ++ exercise.wwwapi/Models/UserResponseDto.cs | 12 ++ exercise.wwwapi/Program.cs | 118 ++++++++++++++++++ exercise.wwwapi/Repository/IRepository.cs | 18 +++ exercise.wwwapi/Repository/Repository.cs | 62 +++++++++ exercise.wwwapi/exercise.wwwapi.csproj | 22 ++++ 14 files changed, 481 insertions(+) create mode 100644 exercise.wwwapi/Configuration/ConfigurationSettings.cs create mode 100644 exercise.wwwapi/Configuration/IConfigurationSettings.cs create mode 100644 exercise.wwwapi/Data/DataContext.cs create mode 100644 exercise.wwwapi/Endpoints/AuthApi.cs create mode 100644 exercise.wwwapi/Endpoints/SecureApi.cs create mode 100644 exercise.wwwapi/Helpers/ClaimsPrincipalHelper.cs create mode 100644 exercise.wwwapi/Models/Payload.cs create mode 100644 exercise.wwwapi/Models/User.cs create mode 100644 exercise.wwwapi/Models/UserRequestDto.cs create mode 100644 exercise.wwwapi/Models/UserResponseDto.cs create mode 100644 exercise.wwwapi/Repository/IRepository.cs create mode 100644 exercise.wwwapi/Repository/Repository.cs diff --git a/exercise.wwwapi/Configuration/ConfigurationSettings.cs b/exercise.wwwapi/Configuration/ConfigurationSettings.cs new file mode 100644 index 0000000..55f6274 --- /dev/null +++ b/exercise.wwwapi/Configuration/ConfigurationSettings.cs @@ -0,0 +1,17 @@ +using System.Configuration; + +namespace exercise.wwwapi.Configuration +{ + public class ConfigurationSettings : IConfigurationSettings + { + IConfiguration _configuration; + public ConfigurationSettings() + { + _configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + } + public string GetValue(string key) + { + return _configuration.GetValue(key)!; + } + } +} \ No newline at end of file diff --git a/exercise.wwwapi/Configuration/IConfigurationSettings.cs b/exercise.wwwapi/Configuration/IConfigurationSettings.cs new file mode 100644 index 0000000..ad90ed5 --- /dev/null +++ b/exercise.wwwapi/Configuration/IConfigurationSettings.cs @@ -0,0 +1,7 @@ +namespace exercise.wwwapi.Configuration +{ + public interface IConfigurationSettings + { + string GetValue(string key); + } +} \ No newline at end of file diff --git a/exercise.wwwapi/Data/DataContext.cs b/exercise.wwwapi/Data/DataContext.cs new file mode 100644 index 0000000..5ba969f --- /dev/null +++ b/exercise.wwwapi/Data/DataContext.cs @@ -0,0 +1,30 @@ +using exercise.wwwapi.Configuration; +using exercise.wwwapi.Models; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Numerics; +using System.Data.SqlClient; + +namespace exercise.wwwapi.Data +{ + public class DataContext : DbContext + { + private string _connectionString; + public DataContext(DbContextOptions options) : base(options) + { + var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + _connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString")!; + this.Database.SetConnectionString(_connectionString); + this.Database.EnsureCreated(); + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + //optionsBuilder.UseInMemoryDatabase(databaseName: "Database"); + // optionsBuilder.UseNpgsql(_connectionString); + optionsBuilder.UseLazyLoadingProxies(); + + } + + public DbSet Users { get; set; } + } +} \ No newline at end of file diff --git a/exercise.wwwapi/Endpoints/AuthApi.cs b/exercise.wwwapi/Endpoints/AuthApi.cs new file mode 100644 index 0000000..dcb5ac3 --- /dev/null +++ b/exercise.wwwapi/Endpoints/AuthApi.cs @@ -0,0 +1,92 @@ +using exercise.wwwapi.Configuration; +using exercise.wwwapi.Helpers; +using exercise.wwwapi.Models; +using exercise.wwwapi.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ActionConstraints; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace exercise.wwwapi.EndPoints +{ + public static class AuthApi + { + public static void ConfigureAuthApi(this WebApplication app) + { + app.MapPost("register", Register); + app.MapPost("login", Login); + app.MapGet("users", GetUsers); + } + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + private static async Task GetUsers(IRepository service, ClaimsPrincipal user) + { + return TypedResults.Ok(service.GetAll()); + } + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + private static async Task Register(UserRequestDto request, IRepository service) + { + + //user exists + if (service.GetAll().Where(u => u.Username == request.Username).Any()) return Results.Conflict(new Payload() { status = "Username already exists!", data = request }); + + string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password); + + var user = new User(); + + user.Username = request.Username; + user.PasswordHash = passwordHash; + user.Email = request.Email; + + service.Insert(user); + service.Save(); + + return Results.Ok(new Payload() { data = "Created Account" }); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + private static async Task Login(UserRequestDto request, IRepository service, IConfigurationSettings config) + { + //user doesn't exist + if (!service.GetAll().Where(u => u.Username == request.Username).Any()) return Results.BadRequest(new Payload() { status = "User does not exist", data = request }); + + User user = service.GetAll().FirstOrDefault(u => u.Username == request.Username)!; + + + if (!BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash)) + { + return Results.BadRequest(new Payload() { status = "Wrong Password", data = request }); + } + string token = CreateToken(user, config); + return Results.Ok(new Payload() { data = token }); + + } + private static string CreateToken(User user, IConfigurationSettings config) + { + List claims = new List + { + new Claim(ClaimTypes.Sid, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.Username), + new Claim(ClaimTypes.Email, user.Email), + + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.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; + } + } +} + diff --git a/exercise.wwwapi/Endpoints/SecureApi.cs b/exercise.wwwapi/Endpoints/SecureApi.cs new file mode 100644 index 0000000..de9a821 --- /dev/null +++ b/exercise.wwwapi/Endpoints/SecureApi.cs @@ -0,0 +1,29 @@ +using exercise.wwwapi.Helpers; +using exercise.wwwapi.Models; +using exercise.wwwapi.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Win32; +using System.Security.Claims; + + +namespace exercise.wwwapi.Endpoints +{ + public static class SecureApi + { + public static void ConfigureSecureApi(this WebApplication app) + { + app.MapGet("message", GetMessage); + + + } + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + private static async Task GetMessage(IRepository service, ClaimsPrincipal user, ILogger logger) + { + logger.LogDebug(new string('*', 1000)); + return TypedResults.Ok(new { LoggedIn = true, UserId = user.UserRealId().ToString(), Email = $"{user.Email()}", Message = "Pulled the userid and email out of the claims" }); + } + } +} \ No newline at end of file diff --git a/exercise.wwwapi/Helpers/ClaimsPrincipalHelper.cs b/exercise.wwwapi/Helpers/ClaimsPrincipalHelper.cs new file mode 100644 index 0000000..cf25295 --- /dev/null +++ b/exercise.wwwapi/Helpers/ClaimsPrincipalHelper.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.CompilerServices; +using System.Security.Claims; + +namespace exercise.wwwapi.Helpers +{ + public static class ClaimsPrincipalHelper + { + public static int? UserRealId(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.Sid); + return int.Parse(claim?.Value); + } + public static string UserId(this ClaimsPrincipal user) + { + IEnumerable claims = user.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier); + return claims.Count() >= 2 ? claims.ElementAt(1).Value : null; + + } + + public static string? Email(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.Email); + return claim?.Value; + } + public static string? Role(this ClaimsPrincipal user) + { + Claim? claim = user.FindFirst(ClaimTypes.Role); + return claim?.Value; + } + + } +} \ No newline at end of file diff --git a/exercise.wwwapi/Models/Payload.cs b/exercise.wwwapi/Models/Payload.cs new file mode 100644 index 0000000..9387705 --- /dev/null +++ b/exercise.wwwapi/Models/Payload.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.wwwapi.Models +{ + [NotMapped] + public class Payload where T : class + { + public string status { get; set; } = "success"; + public T data { get; set; } + } +} \ No newline at end of file diff --git a/exercise.wwwapi/Models/User.cs b/exercise.wwwapi/Models/User.cs new file mode 100644 index 0000000..bcbf3bf --- /dev/null +++ b/exercise.wwwapi/Models/User.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.wwwapi.Models +{ + [Table("users")] + public class User + { + [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; } + } +} \ No newline at end of file diff --git a/exercise.wwwapi/Models/UserRequestDto.cs b/exercise.wwwapi/Models/UserRequestDto.cs new file mode 100644 index 0000000..cca7b33 --- /dev/null +++ b/exercise.wwwapi/Models/UserRequestDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.wwwapi.Models +{ + [NotMapped] + public class UserRequestDto + { + public required string Username { get; set; } + public required string Password { get; set; } + public required string Email { get; set; } + } +} \ No newline at end of file diff --git a/exercise.wwwapi/Models/UserResponseDto.cs b/exercise.wwwapi/Models/UserResponseDto.cs new file mode 100644 index 0000000..c6eb076 --- /dev/null +++ b/exercise.wwwapi/Models/UserResponseDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.wwwapi.Models +{ + [NotMapped] + public class UserResponseDto + { + public string Username { get; set; } + public string PasswordHash { get; set; } + public string Email { get; set; } + } +} \ No newline at end of file diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 47f22ef..45cfa13 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -1,6 +1,104 @@ +using exercise.wwwapi.Configuration; +using exercise.wwwapi.Data; +using exercise.wwwapi.Endpoints; +using exercise.wwwapi.EndPoints; +using exercise.wwwapi.Models; +using exercise.wwwapi.Repository; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; + +using System.Diagnostics; +using System.Text; + + var builder = WebApplication.CreateBuilder(args); +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); +var config = new ConfigurationSettings(); // Add services to the container. +builder.Services.AddScoped(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped>(); +builder.Services.AddDbContext(options => { + + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); + options.LogTo(message => Debug.WriteLine(message)); + +}); +//authentication verifying who they say they are +//authorization verifying what they have access to +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(config.GetValue("AppSettings:Token"))), + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false, + ValidateIssuerSigningKey = false + + }; +}); +builder.Services.AddSwaggerGen(s => +{ + s.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "C# API Authentication", + Description = "Demo of an API using JWT as an authentication method", + Contact = new OpenApiContact + { + Name = "Nigel", + Email = "nigel@nigel.nigel", + Url = new Uri("https://www.boolean.co.uk") + }, + License = new OpenApiLicense + { + Name = "Boolean", + Url = new Uri("https://github.com/boolean-uk/csharp-api-auth") + } + + }); + + s.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "Add an Authorization header with a JWT token using the Bearer scheme see the app.http file for an example.)", + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Header, + Scheme = "Bearer" + }); + + s.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); + +}); +builder.Services.AddAuthorization(); + +builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -12,9 +110,29 @@ { app.UseSwagger(); app.UseSwaggerUI(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/openapi/v1.json", "Demo API"); + }); + //app.MapScalarApiReference(); } +app.UseCors(x => x + .AllowAnyMethod() + .AllowAnyHeader() + .SetIsOriginAllowed(origin => true) // allow any origin + .AllowCredentials()); // allow credentials + app.UseHttpsRedirection(); +app.UseAuthentication(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.ConfigureAuthApi(); + +app.ConfigureSecureApi(); 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..07c9085 --- /dev/null +++ b/exercise.wwwapi/Repository/IRepository.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace exercise.wwwapi.Repository +{ + public interface IRepository where T : class + { + IEnumerable GetAll(); + IEnumerable GetAll(params Expression>[] includeExpressions); + T GetById(object id); + void Insert(T obj); + void Update(T obj); + void Delete(object id); + void Save(); + DbSet Table { get; } + + } +} \ No newline at end of file diff --git a/exercise.wwwapi/Repository/Repository.cs b/exercise.wwwapi/Repository/Repository.cs new file mode 100644 index 0000000..8d3c5d4 --- /dev/null +++ b/exercise.wwwapi/Repository/Repository.cs @@ -0,0 +1,62 @@ +using exercise.wwwapi.Data; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; + +namespace exercise.wwwapi.Repository +{ + public class Repository : IRepository where T : class + { + private DataContext _db; + private DbSet _table = null; + + public Repository(DataContext db) + { + _db = db; + _table = _db.Set(); + } + + public IEnumerable GetAll(params Expression>[] includeExpressions) + { + if (includeExpressions.Any()) + { + var set = includeExpressions + .Aggregate>, IQueryable> + (_table, (current, expression) => current.Include(expression)); + } + return _table.ToList(); + } + + public IEnumerable GetAll() + { + return _table.ToList(); + } + public T GetById(object id) + { + return _table.Find(id); + } + + public void Insert(T obj) + { + _table.Add(obj); + } + public void Update(T obj) + { + _table.Attach(obj); + _db.Entry(obj).State = EntityState.Modified; + } + + public void Delete(object id) + { + T existing = _table.Find(id); + _table.Remove(existing); + } + + + public void Save() + { + _db.SaveChanges(); + } + public DbSet Table { get { return _table; } } + + } +} \ No newline at end of file diff --git a/exercise.wwwapi/exercise.wwwapi.csproj b/exercise.wwwapi/exercise.wwwapi.csproj index 56929a8..6d3a9c3 100644 --- a/exercise.wwwapi/exercise.wwwapi.csproj +++ b/exercise.wwwapi/exercise.wwwapi.csproj @@ -8,8 +8,30 @@ + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + From 5d23c2c25a0b3ffaccf0aa609dd7ef44b8ef648f Mon Sep 17 00:00:00 2001 From: Sander Rasmussen Date: Sat, 1 Feb 2025 19:59:50 +0100 Subject: [PATCH 2/4] core --- exercise.wwwapi/Data/DataContext.cs | 12 ++++++++- exercise.wwwapi/Endpoints/SecureApi.cs | 36 +++++++++++++++++++++++--- exercise.wwwapi/Models/Post.cs | 17 ++++++++++++ exercise.wwwapi/Program.cs | 1 + 4 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 exercise.wwwapi/Models/Post.cs diff --git a/exercise.wwwapi/Data/DataContext.cs b/exercise.wwwapi/Data/DataContext.cs index 5ba969f..647e734 100644 --- a/exercise.wwwapi/Data/DataContext.cs +++ b/exercise.wwwapi/Data/DataContext.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Numerics; using System.Data.SqlClient; +using System.Reflection.Metadata; namespace exercise.wwwapi.Data { @@ -20,11 +21,20 @@ public DataContext(DbContextOptions options) : base(options) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { //optionsBuilder.UseInMemoryDatabase(databaseName: "Database"); - // optionsBuilder.UseNpgsql(_connectionString); + optionsBuilder.UseNpgsql(_connectionString); optionsBuilder.UseLazyLoadingProxies(); + + + } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(b => b.postId); + } public DbSet Users { get; set; } + public DbSet Posts { get; set; } } } \ No newline at end of file diff --git a/exercise.wwwapi/Endpoints/SecureApi.cs b/exercise.wwwapi/Endpoints/SecureApi.cs index de9a821..457994c 100644 --- a/exercise.wwwapi/Endpoints/SecureApi.cs +++ b/exercise.wwwapi/Endpoints/SecureApi.cs @@ -14,8 +14,9 @@ public static class SecureApi public static void ConfigureSecureApi(this WebApplication app) { app.MapGet("message", GetMessage); - - + app.MapGet("posts", GetPosts); + app.MapPost("newpost", MakePost); + app.MapPut("updatepost", UpdatePost); } [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] @@ -25,5 +26,34 @@ private static async Task GetMessage(IRepository service, ClaimsP logger.LogDebug(new string('*', 1000)); return TypedResults.Ok(new { LoggedIn = true, UserId = user.UserRealId().ToString(), Email = $"{user.Email()}", Message = "Pulled the userid and email out of the claims" }); } + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + private static async Task GetPosts(IRepository service) + { + return TypedResults.Ok(service.GetAll()); + } + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + private static async Task MakePost(IRepository service, string title, string content ) + { + Post post = new Post { postTitle = title, content = content }; + service.Insert(post); + service.Save(); + return TypedResults.Ok(service.GetAll()); + } + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + private static async Task UpdatePost(IRepository service, int id, string title, string content) + { + + Post post = service.GetById(id); + post.postTitle = title; + post.content = content; + service.Save(); + return TypedResults.Ok(service.GetAll()); + } } -} \ No newline at end of file +} diff --git a/exercise.wwwapi/Models/Post.cs b/exercise.wwwapi/Models/Post.cs new file mode 100644 index 0000000..b5d74fe --- /dev/null +++ b/exercise.wwwapi/Models/Post.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace exercise.wwwapi.Models +{ + [Table("Post")] + [PrimaryKey("postId")] + public class Post + { + [Column("postId")] + public int postId { get; set; } + [Column("postTitle")] + public string postTitle { get; set; } + [Column("content")] + public string content { get; set; } + } +} diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 45cfa13..580777a 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -23,6 +23,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped, Repository>(); builder.Services.AddScoped>(); +builder.Services.AddScoped, Repository>(); builder.Services.AddDbContext(options => { options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); From 797786d5e0dd5928274ac4c0baa6134b74dc4520 Mon Sep 17 00:00:00 2001 From: Sander Rasmussen Date: Mon, 3 Feb 2025 16:04:11 +0100 Subject: [PATCH 3/4] fixed bugs, now a user can only edit his own posts --- exercise.wwwapi/Data/DataContext.cs | 15 ++++++++++- exercise.wwwapi/Endpoints/AuthApi.cs | 4 +-- exercise.wwwapi/Endpoints/SecureApi.cs | 36 ++++++++++++++++++-------- exercise.wwwapi/Models/Post.cs | 4 +++ exercise.wwwapi/Models/PostDTO.cs | 27 +++++++++++++++++++ exercise.wwwapi/Models/User.cs | 8 +++--- exercise.wwwapi/Models/UserDTO.cs | 9 +++++++ 7 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 exercise.wwwapi/Models/PostDTO.cs create mode 100644 exercise.wwwapi/Models/UserDTO.cs diff --git a/exercise.wwwapi/Data/DataContext.cs b/exercise.wwwapi/Data/DataContext.cs index 647e734..57ea2df 100644 --- a/exercise.wwwapi/Data/DataContext.cs +++ b/exercise.wwwapi/Data/DataContext.cs @@ -31,7 +31,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasKey(b => b.postId); - + modelBuilder.Entity() + .HasKey(b => b.userId); + + modelBuilder.Entity() + .HasOne(b => b.user) + .WithMany(b => b.Posts) + .HasForeignKey(b => b.userId); + + modelBuilder.Entity() + .HasMany(b => b.Posts) + .WithOne(b => b.user); + + + } public DbSet Users { get; set; } diff --git a/exercise.wwwapi/Endpoints/AuthApi.cs b/exercise.wwwapi/Endpoints/AuthApi.cs index dcb5ac3..3f25d25 100644 --- a/exercise.wwwapi/Endpoints/AuthApi.cs +++ b/exercise.wwwapi/Endpoints/AuthApi.cs @@ -57,7 +57,7 @@ private static async Task Login(UserRequestDto request, IRepository u.Username == request.Username).Any()) return Results.BadRequest(new Payload() { status = "User does not exist", data = request }); User user = service.GetAll().FirstOrDefault(u => u.Username == request.Username)!; - + if (!BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash)) { @@ -71,7 +71,7 @@ private static string CreateToken(User user, IConfigurationSettings config) { List claims = new List { - new Claim(ClaimTypes.Sid, user.Id.ToString()), + new Claim(ClaimTypes.Sid, user.userId.ToString()), new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.Email, user.Email), diff --git a/exercise.wwwapi/Endpoints/SecureApi.cs b/exercise.wwwapi/Endpoints/SecureApi.cs index 457994c..05b2ac8 100644 --- a/exercise.wwwapi/Endpoints/SecureApi.cs +++ b/exercise.wwwapi/Endpoints/SecureApi.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Win32; +using System.Diagnostics.Eventing.Reader; using System.Security.Claims; @@ -31,29 +32,42 @@ private static async Task GetMessage(IRepository service, ClaimsP [ProducesResponseType(StatusCodes.Status401Unauthorized)] private static async Task GetPosts(IRepository service) { - return TypedResults.Ok(service.GetAll()); + List posts = new List(); + service.GetAll().ToList().ForEach(post => posts.Add(new PostDTO(post)));// convert all posts to postDTO + + return TypedResults.Ok(posts); } [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - private static async Task MakePost(IRepository service, string title, string content ) + private static async Task MakePost(IRepository service,ClaimsPrincipal user, string title, string content ) { - Post post = new Post { postTitle = title, content = content }; + Post post = new Post { postTitle = title, content = content, userId=ClaimsPrincipalHelper.UserRealId(user) }; service.Insert(post); service.Save(); - return TypedResults.Ok(service.GetAll()); + return TypedResults.Ok(new PostDTO(post)); } [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - private static async Task UpdatePost(IRepository service, int id, string title, string content) + + private static async Task UpdatePost(IRepository service, int postId, ClaimsPrincipal user, string title, string content) { - - Post post = service.GetById(id); - post.postTitle = title; - post.content = content; - service.Save(); - return TypedResults.Ok(service.GetAll()); + + Post post = service.GetById(postId); + if (post.userId != ClaimsPrincipalHelper.UserRealId(user)) + { + return TypedResults.Unauthorized(); + } + + else + { + post.postTitle = title; + post.content = content; + service.Save(); + return TypedResults.Ok(); + } + } } } diff --git a/exercise.wwwapi/Models/Post.cs b/exercise.wwwapi/Models/Post.cs index b5d74fe..e0038c0 100644 --- a/exercise.wwwapi/Models/Post.cs +++ b/exercise.wwwapi/Models/Post.cs @@ -11,6 +11,10 @@ public class Post public int postId { get; set; } [Column("postTitle")] public string postTitle { get; set; } + [Column("userId")] + public int? userId { get; set; } + [Column("user")] + public virtual User user { get; set; } [Column("content")] public string content { get; set; } } diff --git a/exercise.wwwapi/Models/PostDTO.cs b/exercise.wwwapi/Models/PostDTO.cs new file mode 100644 index 0000000..b9730e2 --- /dev/null +++ b/exercise.wwwapi/Models/PostDTO.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.wwwapi.Models +{ + public class PostDTO + { + + + public string postTitle { get; set; } + + public int? userId { get; set; } + + public virtual string user { get; set; } + public int? postId { get; set; } + + public string content { get; set; } + + public PostDTO(Post post) + { + postTitle = post.postTitle; + userId = post.userId; + postId = post.postId; + content = post.content; + } + +} +} diff --git a/exercise.wwwapi/Models/User.cs b/exercise.wwwapi/Models/User.cs index bcbf3bf..be00dce 100644 --- a/exercise.wwwapi/Models/User.cs +++ b/exercise.wwwapi/Models/User.cs @@ -2,16 +2,18 @@ namespace exercise.wwwapi.Models { - [Table("users")] + [Table("User")] public class User { - [Column("id")] - public int Id { get; set; } + [Column("userid")] + public int userId { get; set; } [Column("username")] public string Username { get; set; } [Column("passwordhash")] public string PasswordHash { get; set; } [Column("email")] public string Email { get; set; } + [NotMapped] + public virtual List Posts { get; set; } } } \ No newline at end of file diff --git a/exercise.wwwapi/Models/UserDTO.cs b/exercise.wwwapi/Models/UserDTO.cs new file mode 100644 index 0000000..b68920f --- /dev/null +++ b/exercise.wwwapi/Models/UserDTO.cs @@ -0,0 +1,9 @@ +namespace exercise.wwwapi.Models +{ + public class UserDTO + { + public string username { get; set; } + public string email { get; set; } + } +} + From 8ab258f6875568593348712b9e518253101c002f Mon Sep 17 00:00:00 2001 From: Sander Rasmussen Date: Mon, 3 Feb 2025 18:00:40 +0100 Subject: [PATCH 4/4] done with extension --- exercise.wwwapi/Data/DataContext.cs | 25 +++++++++++++++++-- exercise.wwwapi/Endpoints/SecureApi.cs | 34 ++++++++++++++++++++++++++ exercise.wwwapi/Models/Comment.cs | 24 ++++++++++++++++++ exercise.wwwapi/Models/CommentDTO.cs | 21 ++++++++++++++++ exercise.wwwapi/Models/Post.cs | 2 ++ exercise.wwwapi/Models/PostDTO.cs | 6 +++++ exercise.wwwapi/Models/User.cs | 2 ++ 7 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 exercise.wwwapi/Models/Comment.cs create mode 100644 exercise.wwwapi/Models/CommentDTO.cs diff --git a/exercise.wwwapi/Data/DataContext.cs b/exercise.wwwapi/Data/DataContext.cs index 57ea2df..383a101 100644 --- a/exercise.wwwapi/Data/DataContext.cs +++ b/exercise.wwwapi/Data/DataContext.cs @@ -33,21 +33,42 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasKey(b => b.postId); modelBuilder.Entity() .HasKey(b => b.userId); + modelBuilder.Entity() + .HasKey(b => b.commentId); modelBuilder.Entity() .HasOne(b => b.user) .WithMany(b => b.Posts) .HasForeignKey(b => b.userId); + modelBuilder.Entity() + .HasMany(b => b.comments) + .WithOne(b => b.post); + + modelBuilder.Entity() .HasMany(b => b.Posts) .WithOne(b => b.user); - - + + modelBuilder.Entity() + .HasMany(b => b.Comments) + .WithOne(b => b.user); + + + modelBuilder.Entity() + .HasOne(b => b.user) + .WithMany(c => c.Comments) + .HasForeignKey(b => b.userId); + + modelBuilder.Entity() + .HasOne(b => b.post) + .WithMany(c => c.comments) + .HasForeignKey(b => b.postId); } public DbSet Users { get; set; } public DbSet Posts { get; set; } + public DbSet Comments { get; set; } } } \ No newline at end of file diff --git a/exercise.wwwapi/Endpoints/SecureApi.cs b/exercise.wwwapi/Endpoints/SecureApi.cs index 05b2ac8..86d786b 100644 --- a/exercise.wwwapi/Endpoints/SecureApi.cs +++ b/exercise.wwwapi/Endpoints/SecureApi.cs @@ -18,7 +18,41 @@ public static void ConfigureSecureApi(this WebApplication app) app.MapGet("posts", GetPosts); app.MapPost("newpost", MakePost); app.MapPut("updatepost", UpdatePost); + app.MapGet("postswithcomments", GetPostsWithComments); + app.MapPost("Post{postId}/comment{content}", CommentOnPost); + } + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public static async Task CommentOnPost(IRepository postRepo, IRepository commentRepo, int postId, ClaimsPrincipal user, string content ) + { + Post post = postRepo.GetById(postId); + Comment comment = new Comment + { + userId = ClaimsPrincipalHelper.UserRealId(user), + postId = postId, + content = content + }; + commentRepo.Insert(comment); + post.comments.Add(comment); + commentRepo.Save(); + postRepo.Save(); + return TypedResults.Ok(new CommentDTO(comment)); + } + + + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public static async Task GetPostsWithComments(IRepository repo) + { + List posts = repo.GetAll().ToList(); + List postDTOs = new List(); + posts.ForEach(x => postDTOs.Add(new PostDTO(x))); + return TypedResults.Ok(postDTOs); + } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] diff --git a/exercise.wwwapi/Models/Comment.cs b/exercise.wwwapi/Models/Comment.cs new file mode 100644 index 0000000..ad80907 --- /dev/null +++ b/exercise.wwwapi/Models/Comment.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.Drawing; +using Microsoft.EntityFrameworkCore; + +namespace exercise.wwwapi.Models +{ + [Table("Comment")] + [PrimaryKey("commentId")] + public class Comment + { + [Column("commentId")] + public int commentId { get; set; } + [Column("userId")] + public int? userId { get; set; } + [Column("content")] + public string content { get; set; } + [Column("postId")] + public int postId { get; set; } + [NotMapped] + public virtual Post post { get; set; } + [NotMapped] + public virtual User user { get; set; } + } +} diff --git a/exercise.wwwapi/Models/CommentDTO.cs b/exercise.wwwapi/Models/CommentDTO.cs new file mode 100644 index 0000000..dea1191 --- /dev/null +++ b/exercise.wwwapi/Models/CommentDTO.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.wwwapi.Models +{ + public class CommentDTO + { + public int commentId { get; set; } + + public string content { get; set; } + + public int? userId { get; set; } + + public CommentDTO(Comment comment ) + { + commentId = comment.commentId; + content = comment.content; + userId = comment.userId; + } + + } +} diff --git a/exercise.wwwapi/Models/Post.cs b/exercise.wwwapi/Models/Post.cs index e0038c0..02aa5c9 100644 --- a/exercise.wwwapi/Models/Post.cs +++ b/exercise.wwwapi/Models/Post.cs @@ -17,5 +17,7 @@ public class Post public virtual User user { get; set; } [Column("content")] public string content { get; set; } + [NotMapped] + public virtual List comments { get; set; } = new List(); } } diff --git a/exercise.wwwapi/Models/PostDTO.cs b/exercise.wwwapi/Models/PostDTO.cs index b9730e2..a401eb2 100644 --- a/exercise.wwwapi/Models/PostDTO.cs +++ b/exercise.wwwapi/Models/PostDTO.cs @@ -14,6 +14,7 @@ public class PostDTO public int? postId { get; set; } public string content { get; set; } + public List comments { get; set; } = new List(); public PostDTO(Post post) { @@ -21,6 +22,11 @@ public PostDTO(Post post) userId = post.userId; postId = post.postId; content = post.content; + if (post.comments.Any()) + { + post.comments.ForEach(x => comments.Add(new CommentDTO(x))); + } + } } diff --git a/exercise.wwwapi/Models/User.cs b/exercise.wwwapi/Models/User.cs index be00dce..7f121c3 100644 --- a/exercise.wwwapi/Models/User.cs +++ b/exercise.wwwapi/Models/User.cs @@ -15,5 +15,7 @@ public class User public string Email { get; set; } [NotMapped] public virtual List Posts { get; set; } + [NotMapped] + public virtual List Comments { get; set; } } } \ No newline at end of file