Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions exercise.wwwapi/Configuration/ConfigurationSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@


using Microsoft.Extensions.Configuration;

namespace exercise.wwwapi.Configuration
{
public class ConfigurationSettings : IConfigurationSettings
{
private readonly IConfiguration _configuration;

public ConfigurationSettings()
{
_configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
}

public string GetValue(string key)
{
return _configuration.GetValue<string>(key)!;
}
}
}

9 changes: 9 additions & 0 deletions exercise.wwwapi/Configuration/IConfigurationSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace exercise.wwwapi.Configuration
{
public interface IConfigurationSettings
{
string GetValue(string key);


}
}
7 changes: 7 additions & 0 deletions exercise.wwwapi/DTO/BlogPostDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace exercise.wwwapi.DTO
{
public class BlogPostDTO
{
public string Text { get; set; }
}
}
11 changes: 11 additions & 0 deletions exercise.wwwapi/DTO/UserDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace exercise.wwwapi.DTO
{
public class UserDTO
{
public int Id { get; set; }
public string Username { get; set; }
public string PasswordHash { get; set; }
public string Email { get; set; }

}
}
10 changes: 10 additions & 0 deletions exercise.wwwapi/DTO/UserRequestDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace exercise.wwwapi.DTO
{
public class UserRequestDTO
{
public required string Username { get; set; }
public required string Password { get; set; }
public required string Email { get; set; }
}

}
9 changes: 9 additions & 0 deletions exercise.wwwapi/DTO/UserResponseDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace exercise.wwwapi.DTO
{
public class UserResponseDTO
{
public string Username { get; set; }
public string PasswordHash { get; set; }
public string Email { get; set; }
}
}
34 changes: 34 additions & 0 deletions exercise.wwwapi/Data/DataContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using exercise.wwwapi.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using System.Diagnostics;



namespace exercise.wwwapi.Data
{
public class DataContext : DbContext
{
private string _connectionString;
public DataContext(DbContextOptions<DataContext> options) : base(options)
{
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
_connectionString = configuration.GetValue<string>("ConnectionStrings:DefaultConnectionString")!;
this.Database.EnsureCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(_connectionString);
optionsBuilder.LogTo(message => Debug.WriteLine(message));
}


protected override void OnModelCreating(ModelBuilder modelBuilder)
{

}
public DbSet<BlogPost> BlogPosts { get; set; }
public DbSet<User> Users { get; set; }

}
}
78 changes: 78 additions & 0 deletions exercise.wwwapi/Endpoints/AuthApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using exercise.wwwapi.Configuration;
using exercise.wwwapi.DTO;
using exercise.wwwapi.Helper;
using exercise.wwwapi.Helpers;
using exercise.wwwapi.Models;
using exercise.wwwapi.Repository;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Linq;


namespace exercise.wwwapi.Endpoints
{
public static class AuthApi
{
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public static async Task<IResult> Register(UserRequestDTO request, IRepository<User> repository)
{
var users= await repository.GetAll();
if (users.Any(u => u.Username == request.Username))
return Results.Conflict(new Payload<UserRequestDTO> { status = "Username already exists!", data = request });

string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password);

var user = new User
{
Username = request.Username,
PasswordHash = passwordHash,
Email = request.Email
};

repository.Insert(user);

return Results.Ok(new Payload<string> { data = "Created Account" });
}

[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public static async Task<IResult> Login(UserRequestDTO request, IRepository<User> repository, IConfigurationSettings config)
{
var users = await repository.GetAll();
User? user = users.FirstOrDefault(u => u.Username == request.Username);
if (user == null)
return Results.BadRequest(new Payload<UserRequestDTO> { status = "User does not exist", data = request });

if (!BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
return Results.BadRequest(new Payload<UserRequestDTO> { status = "Wrong Password", data = request });

string token = CreateToken(user, config);
return Results.Ok(new Payload<string> { data = token });
}

private static string CreateToken(User user, IConfigurationSettings config)
{
List<Claim> claims = new List<Claim>
{
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
);

return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}

106 changes: 106 additions & 0 deletions exercise.wwwapi/Endpoints/SecureApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using exercise.wwwapi.DTO;
using exercise.wwwapi.Helper;
using exercise.wwwapi.Helpers;
using exercise.wwwapi.Models;
using exercise.wwwapi.Repository;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

namespace exercise.wwwapi.Endpoints
{
public static class SecureApi
{
public static void ConfigureUserEndpoint(this WebApplication app)
{
var userGroup = app.MapGroup("users");
userGroup.MapGet("/", GetUsers);

var postGroup = app.MapGroup("posts");
postGroup.MapGet("/", GetPosts);
postGroup.MapPost("/", CreatePost);
postGroup.MapPut("/{id}", UpdatePost);

app.MapPost("/register", AuthApi.Register);
app.MapPost("/login", AuthApi.Login);
}

[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
private static async Task<IResult> GetUsers(IRepository<User> repository, ClaimsPrincipal user)
{
var users = await repository.GetAll();
return TypedResults.Ok(users);
}

[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
private static async Task<IResult> GetPosts(IRepository<BlogPost> repository, ClaimsPrincipal user)
{
var posts = await repository.GetAll();
return TypedResults.Ok(posts);
}

[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
private static async Task<IResult> CreatePost(IRepository<BlogPost> postRepo, IRepository<User> userRepo, BlogPostDTO postDTO, ClaimsPrincipal user)
{
int? userId = user.UserRealId();

var users = await userRepo.GetAll();
User? author = users.FirstOrDefault(u => u.Id == userId);

if (author == null)
return Results.BadRequest(new Payload<string> { status = "User not found" });

BlogPost post = new BlogPost
{
Text = postDTO.Text,
AuthorId = userId.Value,
Author = author
};

postRepo.Insert(post);

return TypedResults.Ok(new Payload<string> { data = "Post Created" });
}

[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public static async Task<IResult> UpdatePost(
IRepository<BlogPost> postRepo,
IRepository<User> userRepo,
BlogPostDTO postDTO,
ClaimsPrincipal user,
int id)
{
int? userId = user.UserRealId();
if (userId == null)
return Results.Unauthorized();

// Hent posten fra databasen (bruk AsNoTracking for å unngå problemer med ChangeTracker)
BlogPost? existingPost = (await postRepo.GetAll()).FirstOrDefault(p => p.Id == id);

if (existingPost == null)
return Results.NotFound(new Payload<string> { status = "Post not found" });

if (existingPost.AuthorId != userId)
return Results.Forbid(); // HTTP 403 hvis brukeren ikke er eieren av innlegget

existingPost.Text = postDTO.Text;

// Bruk den oppdaterte Update-metoden som er async
await postRepo.Update(existingPost);

return TypedResults.Ok(new Payload<string> { data = "Post Updated Successfully" });
}
}

}

35 changes: 35 additions & 0 deletions exercise.wwwapi/Helper/ClaimsPrincipalHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

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<Claim> 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;
}

}
}

11 changes: 11 additions & 0 deletions exercise.wwwapi/Helper/Payload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations.Schema;

namespace exercise.wwwapi.Helper
{
[NotMapped]
public class Payload<T> where T : class
{
public string status { get; set; } = "success";
public T data { get; set; }
}
}
20 changes: 20 additions & 0 deletions exercise.wwwapi/Models/BlogPost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace exercise.wwwapi.Models
{
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public class BlogPost
{

public int Id { get; set; }


public string Text { get; set; }


public int AuthorId { get; set; }

public User Author { get; set; }
}
}

23 changes: 23 additions & 0 deletions exercise.wwwapi/Models/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations.Schema;

namespace exercise.wwwapi.Models
{
[Table("users")]
public class User
{
[Column("id")]
public int Id { get; set; } // Endret fra int til Guid

[Column("username")]
public string Username { get; set; }

[Column("passwordhash")]
public string PasswordHash { get; set; }

[Column("email")]
public string Email { get; set; }


}
}

Loading