parent
15657df008
commit
68aa1e72a0
@ -0,0 +1,4 @@
|
||||
|
||||
# Docker Container Persistant files
|
||||
Postgres/pg-db_data/*
|
||||
Postgres/pg-admin_data/*
|
@ -0,0 +1,3 @@
|
||||
POSTGRES_PASSWORD=nqA3UV3CliLLHpLL
|
||||
PGADMIN_DEFAULT_EMAIL=admin@admin.com
|
||||
PGADMIN_DEFAULT_PASSWORD=3254
|
@ -0,0 +1,33 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
database:
|
||||
container_name: activeallocator-pg-db
|
||||
image: 'postgres:15'
|
||||
ports:
|
||||
- 15432:5432
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
- postgres-network
|
||||
volumes:
|
||||
- ./pg-db_data/:/var/lib/postgresql/data/
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
|
||||
management_interface:
|
||||
container_name: activeallocator-pg-admin
|
||||
image: 'dpage/pgadmin4:7.1'
|
||||
ports:
|
||||
- 15433:80
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- database
|
||||
networks:
|
||||
- postgres-network
|
||||
volumes:
|
||||
- ./pg-admin_data/:/var/lib/pgadmin/
|
||||
|
||||
networks:
|
||||
postgres-network:
|
||||
driver: bridge
|
@ -0,0 +1,3 @@
|
||||
-- Create the DB if it doesn't already exist
|
||||
SELECT 'CREATE DATABASE AADB'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'AADB')\gexec
|
@ -1,3 +1,67 @@
|
||||
# ActiveAllocator
|
||||
# Active Allocator - React & .NET Core API
|
||||
|
||||
An API and Web App for managing budgets with allocations (buckets of funds based on history) and interfacing with Banks, and FireFlyIII.
|
||||
An API and Web App for managing budgets with allocations (buckets of funds based on history) and interfacing with Banks, and FireFlyIII.
|
||||
|
||||
I started with [this](https://travis.media/how-to-create-react-app-net-api-vscode/) tutorial.
|
||||
[Here](https://jasonwatmore.com/post/2022/03/15/net-6-crud-api-example-and-tutorial#user-service-cs) is another one that is handy for good structure.
|
||||
|
||||
Reference for adding JWT authentication is [here](https://jasonwatmore.com/net-7-csharp-jwt-authentication-tutorial-without-aspnet-core-identity#program-cs).
|
||||
|
||||
[Docker Repository Usage](https://www.ionos.com/digitalguide/server/know-how/setting-up-a-docker-repository/)
|
||||
|
||||
## Dev Environment Setup
|
||||
|
||||
On Archlinux install the following to use dotnet: ```sudo pacman -Sy dotnet-sdk dotnet-runtime aspnet-runtime```.
|
||||
|
||||
For Archlinux install docker with ```sudo pacman -Sy docker docker-compose```.
|
||||
|
||||
Then run ```systemctl start docker.service``` and ```systemctl enable docker.service``` to start and enable on boot the docker engine.
|
||||
|
||||
Install React dependencies if needed - ```npm install``` in the ClientApp directory.
|
||||
|
||||
### Download 'Reverse Engineering' Tools
|
||||
|
||||
```
|
||||
dotnet tool install --global dotnet-ef
|
||||
dotnet tool install --global dotnet-aspnet-codegenerator
|
||||
```
|
||||
|
||||
Confirm it is working with ```dotnet ef```.
|
||||
|
||||
## Scaffolding
|
||||
|
||||
1. Create Entity (or Model) for object.
|
||||
2. Add ```DbSet<object>``` entry in your DbContext.
|
||||
3. Migration
|
||||
4. Endpoints (if needed)
|
||||
|
||||
To add Migration for new entity:
|
||||
```
|
||||
dotnet ef migrations add InitialCreate
|
||||
dotnet ef database update
|
||||
```
|
||||
|
||||
For generating API endpoints for an entity called ```Note```:
|
||||
```
|
||||
$HOME/.dotnet/tools/dotnet-aspnet-codegenerator controller -name NotesController -async -api -m Note -dc MyDBContext --relativeFolderPath Controllers
|
||||
```
|
||||
|
||||
## Deploying
|
||||
|
||||
### Create Docker Image
|
||||
|
||||
Running ```sudo dotnet publish``` will create a docker image targetting linux x64 with the "DefaultContainer" profile and image name of "cavaliercraftsmen.site". Edit the .csproj file to change the version of the image.
|
||||
|
||||
To create a docker image explicitly run ```sudo dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer -p:ContainerImageName=cavaliercraftsmen.site``` in the same folder as the dotnet solution. Because we added the ```Microsoft.NET.Build.Containers``` package, we don't actually need to use the Dockerfile.
|
||||
|
||||
### Pushing Docker Image
|
||||
|
||||
You will have to be logged in. ```sudo docker login gitea.veritablevalor.com```
|
||||
|
||||
First tag ```sudo docker tag <image_id> gitea.veritablevalor.com/blizliam/cavaliercraftsmen.site:latest``` (check that the version is right).
|
||||
|
||||
Then push ```sudo docker push gitea.veritablevalor.com/blizliam/cavaliercraftsmen.site:latest```.
|
||||
|
||||
## Using API
|
||||
|
||||
API should be something like - https://localhost:7260/swagger
|
@ -0,0 +1,5 @@
|
||||
namespace active_allocator.Authorization;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class AllowAnonymousAttribute : Attribute
|
||||
{ }
|
@ -0,0 +1,22 @@
|
||||
namespace active_allocator.Authorization;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using active_allocator.Entities;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
|
||||
{
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
// skip authorization if action is decorated with [AllowAnonymous] attribute
|
||||
var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any();
|
||||
if (allowAnonymous)
|
||||
return;
|
||||
|
||||
// authorization
|
||||
var user = (User?)context.HttpContext.Items["User"];
|
||||
if (user == null)
|
||||
context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
namespace active_allocator.Authorization;
|
||||
|
||||
using active_allocator.Services;
|
||||
|
||||
public class JwtMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public JwtMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
|
||||
{
|
||||
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
|
||||
var userId = jwtUtils.ValidateToken(token);
|
||||
if (userId != null)
|
||||
{
|
||||
// attach user to context on successful jwt validation
|
||||
context.Items["User"] = userService.GetById(userId.Value);
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
namespace active_allocator.Authorization;
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using active_allocator.Entities;
|
||||
using active_allocator.Helpers;
|
||||
|
||||
public interface IJwtUtils
|
||||
{
|
||||
public string GenerateToken(User user);
|
||||
public int? ValidateToken(string token);
|
||||
}
|
||||
|
||||
public class JwtUtils : IJwtUtils
|
||||
{
|
||||
private readonly AppSettings _appSettings;
|
||||
|
||||
public JwtUtils(IOptions<AppSettings> appSettings)
|
||||
{
|
||||
_appSettings = appSettings.Value;
|
||||
}
|
||||
|
||||
public string GenerateToken(User user)
|
||||
{
|
||||
// generate token that is valid for 7 days
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }),
|
||||
Expires = DateTime.UtcNow.AddDays(7),
|
||||
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
|
||||
};
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
|
||||
public int? ValidateToken(string token)
|
||||
{
|
||||
if (token == null)
|
||||
return null;
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
|
||||
try
|
||||
{
|
||||
tokenHandler.ValidateToken(token, new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = false,
|
||||
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
|
||||
ClockSkew = TimeSpan.Zero
|
||||
}, out SecurityToken validatedToken);
|
||||
|
||||
var jwtToken = (JwtSecurityToken)validatedToken;
|
||||
var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);
|
||||
|
||||
// return user id from JWT token if validation successful
|
||||
return userId;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// return null if validation fails
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
namespace active_allocator.Controllers;
|
||||
|
||||
using AutoMapper;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using active_allocator.Models.Users;
|
||||
using active_allocator.Services;
|
||||
using active_allocator.Authorization;
|
||||
using active_allocator.Helpers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private IUserService _userService;
|
||||
private IMapper _mapper;
|
||||
private readonly AppSettings _appSettings;
|
||||
|
||||
public UsersController(
|
||||
IUserService userService,
|
||||
IMapper mapper,
|
||||
IOptions<AppSettings> appSettings)
|
||||
{
|
||||
_userService = userService;
|
||||
_mapper = mapper;
|
||||
_appSettings = appSettings.Value;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("authenticate")]
|
||||
public IActionResult Authenticate(AuthenticateRequest model)
|
||||
{
|
||||
var response = _userService.Authenticate(model);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("register")]
|
||||
public IActionResult Register(RegisterRequest model)
|
||||
{
|
||||
_userService.Register(model);
|
||||
return Ok(new { message = "Registration successful" });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult GetAll()
|
||||
{
|
||||
var users = _userService.GetAll();
|
||||
return Ok(users);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public IActionResult GetById(int id)
|
||||
{
|
||||
var user = _userService.GetById(id);
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public IActionResult Update(int id, [FromBody]UpdateRequest model)
|
||||
{
|
||||
_userService.Update(id, model);
|
||||
return Ok(new { message = "User updated" });
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public IActionResult Delete(int id)
|
||||
{
|
||||
_userService.Delete(id);
|
||||
return Ok(new { message = "User deleted" });
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace active_allocator.Entities;
|
||||
|
||||
public enum Role
|
||||
{
|
||||
Admin,
|
||||
User
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace active_allocator.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string Email { get; set; }
|
||||
public Role Role { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string PasswordHash { get; set; }
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
namespace active_allocator.Helpers;
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
// custom exception class for throwing application specific exceptions (e.g. for validation)
|
||||
// that can be caught and handled within the application
|
||||
public class AppException : Exception
|
||||
{
|
||||
public AppException() : base() {}
|
||||
|
||||
public AppException(string message) : base(message) { }
|
||||
|
||||
public AppException(string message, params object[] args)
|
||||
: base(String.Format(CultureInfo.CurrentCulture, message, args))
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace active_allocator.Helpers;
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public required string Secret { get; set; }
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
namespace active_allocator.Helpers;
|
||||
|
||||
using AutoMapper;
|
||||
using active_allocator.Entities;
|
||||
using active_allocator.Models.Users;
|
||||
|
||||
public class AutoMapperProfile : Profile
|
||||
{
|
||||
public AutoMapperProfile()
|
||||
{
|
||||
// User -> AuthenticateResponse
|
||||
CreateMap<User, AuthenticateResponse>();
|
||||
|
||||
// RegisterRequest -> User
|
||||
CreateMap<RegisterRequest, User>();
|
||||
|
||||
// UpdateRequest -> User
|
||||
CreateMap<UpdateRequest, User>()
|
||||
.ForAllMembers(x => x.Condition(
|
||||
(src, dest, prop) =>
|
||||
{
|
||||
// ignore both null & empty string properties
|
||||
if (prop == null) return false;
|
||||
if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
|
||||
|
||||
// ignore null role
|
||||
if (x.DestinationMember.Name == "Role" && src.Role == null) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
namespace active_allocator.Helpers;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using active_allocator.Entities;
|
||||
|
||||
public class DataContext : DbContext
|
||||
{
|
||||
//protected readonly IConfiguration Configuration;
|
||||
|
||||
public DataContext(DbContextOptions<DataContext> options) : base(options)
|
||||
{
|
||||
//Configuration = configuration;
|
||||
}
|
||||
|
||||
/*protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||
{
|
||||
// in memory database used for simplicity, change to a real db for production applications
|
||||
options.UseInMemoryDatabase("TestDb");
|
||||
}*/
|
||||
|
||||
public DbSet<User> Users { get; set; }
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
namespace active_allocator.Helpers;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public class ErrorHandlerMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ErrorHandlerMiddleware(RequestDelegate next, ILogger<ErrorHandlerMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
var response = context.Response;
|
||||
response.ContentType = "application/json";
|
||||
|
||||
switch (error)
|
||||
{
|
||||
case AppException e:
|
||||
// custom application error
|
||||
response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
break;
|
||||
case KeyNotFoundException e:
|
||||
// not found error
|
||||
response.StatusCode = (int)HttpStatusCode.NotFound;
|
||||
break;
|
||||
default:
|
||||
// unhandled error
|
||||
_logger.LogError(error, error.Message);
|
||||
response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
break;
|
||||
}
|
||||
|
||||
var result = JsonSerializer.Serialize(new { message = error?.Message });
|
||||
await response.WriteAsync(result);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
namespace active_allocator.Models.Users;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
public class AuthenticateRequest
|
||||
{
|
||||
[Required]
|
||||
public string Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Password { get; set; }
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
namespace active_allocator.Models.Users;
|
||||
|
||||
public class AuthenticateResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Token { get; set; }
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
namespace active_allocator.Models.Users;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using active_allocator.Entities;
|
||||
|
||||
public class RegisterRequest
|
||||
{
|
||||
[Required]
|
||||
public string Username { get; set; }
|
||||
|
||||
[Required]
|
||||
public string FirstName { get; set; }
|
||||
|
||||
[Required]
|
||||
public string LastName { get; set; }
|
||||
|
||||
[Required]
|
||||
[EnumDataType(typeof(Role))]
|
||||
public string Role { get; set; }
|
||||
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[MinLength(6)]
|
||||
public string Password { get; set; }
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
namespace active_allocator.Models.Users;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using active_allocator.Entities;
|
||||
|
||||
public class UpdateRequest
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
|
||||
[EnumDataType(typeof(Role))]
|
||||
public string Role { get; set; }
|
||||
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
// treat empty string as null for password fields to
|
||||
// make them optional in front end apps
|
||||
private string _password;
|
||||
[MinLength(6)]
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
set => _password = replaceEmptyWithNull(value);
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
private string replaceEmptyWithNull(string value)
|
||||
{
|
||||
// replace empty string with null to make field optional
|
||||
return string.IsNullOrEmpty(value) ? null : value;
|
||||
}
|
||||
}
|
@ -1,27 +1,62 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
using active_allocator.Authorization;
|
||||
using active_allocator.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
// Add services to the container.
|
||||
using active_allocator.Services;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
builder.Services.AddControllersWithViews();
|
||||
internal class Program
|
||||
{
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
var app = builder.Build();
|
||||
// Add services to the container.
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
builder.Services.AddControllersWithViews();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "AA API", Version = "v1" });
|
||||
});
|
||||
|
||||
builder.Services.AddAutoMapper(typeof(Program));
|
||||
builder.Services.AddDbContext<DataContext>(opt =>
|
||||
opt.UseNpgsql(builder.Configuration.GetConnectionString("PSQLConnection")));
|
||||
|
||||
// Configure strongly typed settings object
|
||||
builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));
|
||||
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
builder.Services.AddScoped<IJwtUtils, JwtUtils>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.UseRouting();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
// custom jwt auth middleware
|
||||
app.UseMiddleware<JwtMiddleware>();
|
||||
|
||||
// global error handler
|
||||
app.UseMiddleware<ErrorHandlerMiddleware>();
|
||||
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller}/{action=Index}/{id?}");
|
||||
app.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller}/{action=Index}/{id?}");
|
||||
|
||||
app.MapFallbackToFile("index.html");
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
app.Run();
|
||||
app.Run();
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
namespace active_allocator.Services;
|
||||
|
||||
using AutoMapper;
|
||||
using BCrypt.Net;
|
||||
using active_allocator.Authorization;
|
||||
using active_allocator.Entities;
|
||||
using active_allocator.Helpers;
|
||||
using active_allocator.Models.Users;
|
||||
|
||||
public interface IUserService
|
||||
{
|
||||
AuthenticateResponse Authenticate(AuthenticateRequest model);
|
||||
void Register(RegisterRequest model);
|
||||
IEnumerable<User> GetAll();
|
||||
User GetById(int id);
|
||||
void Update(int id, UpdateRequest model);
|
||||
void Delete(int id);
|
||||
}
|
||||
|
||||
public class UserService : IUserService
|
||||
{
|
||||
private DataContext _context;
|
||||
private IJwtUtils _jwtUtils;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public UserService(
|
||||
DataContext context,
|
||||
IMapper mapper,
|
||||
IJwtUtils jwtUtils)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
_jwtUtils = jwtUtils;
|
||||
}
|
||||
|
||||
public AuthenticateResponse Authenticate(AuthenticateRequest model)
|
||||
{
|
||||
var user = _context.Users.SingleOrDefault(x => x.Username == model.Username);
|
||||
|
||||
// validate
|
||||
if (user == null || !BCrypt.Verify(model.Password, user.PasswordHash))
|
||||
throw new AppException("Username or password is incorrect");
|
||||
|
||||
// authentication successful
|
||||
var response = _mapper.Map<AuthenticateResponse>(user);
|
||||
response.Token = _jwtUtils.GenerateToken(user);
|
||||
return response;
|
||||
}
|
||||
|
||||
public void Register(RegisterRequest model)
|
||||
{
|
||||
// validate username
|
||||
if (_context.Users.Any(x => x.Username == model.Username))
|
||||
throw new AppException("Username '" + model.Username + "' is already taken");
|
||||
|
||||
// validate email
|
||||
if (_context.Users.Any(x => x.Email == model.Email))
|
||||
throw new AppException("Email '" + model.Email + "' is already taken");
|
||||
|
||||
// map model to new user object
|
||||
var user = _mapper.Map<User>(model);
|
||||
|
||||
// hash password
|
||||
user.PasswordHash = BCrypt.HashPassword(model.Password);
|
||||
|
||||
// save user
|
||||
_context.Users.Add(user);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
public IEnumerable<User> GetAll()
|
||||
{
|
||||
return _context.Users;
|
||||
}
|
||||
|
||||
public User GetById(int id)
|
||||
{
|
||||
return getUser(id);
|
||||
}
|
||||
|
||||
public void Update(int id, UpdateRequest model)
|
||||
{
|
||||
var user = getUser(id);
|
||||
|
||||
// validate
|
||||
if (model.Email != user.Email && _context.Users.Any(x => x.Email == model.Email))
|
||||
throw new AppException("User with the email '" + model.Email + "' already exists");
|
||||
|
||||
// hash password if it was entered
|
||||
if (!string.IsNullOrEmpty(model.Password))
|
||||
user.PasswordHash = BCrypt.HashPassword(model.Password);
|
||||
|
||||
// copy model to user and save
|
||||
_mapper.Map(model, user);
|
||||
_context.Users.Update(user);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
{
|
||||
var user = getUser(id);
|
||||
_context.Users.Remove(user);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
// helper methods
|
||||
|
||||
private User getUser(int id)
|
||||
{
|
||||
var user = _context.Users.Find(id);
|
||||
if (user == null) throw new KeyNotFoundException("User not found");
|
||||
return user;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
activeallocator-site:
|
||||
image: activeallocator:1.0.0
|
||||
container_name: activeallocator
|
||||
#build:
|
||||
#context: .
|
||||
#dockerfile: MoviesAPI/Dockerfile
|
||||
ports:
|
||||
- 80:80
|
Loading…
Reference in new issue