From a800b3b9f18ae3e8ab030c30c5d7b6504f2a5ebb Mon Sep 17 00:00:00 2001 From: ivarlovlie Date: Sat, 1 Aug 2020 20:14:34 +0200 Subject: Initial commit --- src/server/Controllers/AccountController.cs | 76 +++++++++++ src/server/Controllers/BaseController.cs | 27 ++++ src/server/Controllers/TransactionsController.cs | 78 +++++++++++ src/server/Controllers/UsersController.cs | 6 + src/server/Dough.csproj | 16 +++ .../Migrations/20200729090558_Initial.Designer.cs | 143 +++++++++++++++++++++ src/server/Migrations/20200729090558_Initial.cs | 97 ++++++++++++++ .../Migrations/MainDbContextModelSnapshot.cs | 141 ++++++++++++++++++++ src/server/Models/Database/BaseModel.cs | 25 ++++ src/server/Models/Database/Category.cs | 7 + src/server/Models/Database/MainDbContext.cs | 34 +++++ src/server/Models/Database/Payee.cs | 7 + src/server/Models/Database/Transaction.cs | 43 +++++++ src/server/Models/Database/User.cs | 31 +++++ src/server/Models/DbSetOverrides.cs | 25 ++++ .../Models/Exceptions/ModelValidationException.cs | 31 +++++ src/server/Models/Results/ErrorResult.cs | 14 ++ src/server/Program.cs | 55 ++++++++ src/server/Properties/launchSettings.json | 12 ++ src/server/Startup.cs | 67 ++++++++++ src/server/Utilities/ExtensionMethods.cs | 54 ++++++++ src/server/appsettings.json | 10 ++ src/server/database.sqlite | Bin 0 -> 45056 bytes 23 files changed, 999 insertions(+) create mode 100644 src/server/Controllers/AccountController.cs create mode 100644 src/server/Controllers/BaseController.cs create mode 100644 src/server/Controllers/TransactionsController.cs create mode 100644 src/server/Controllers/UsersController.cs create mode 100644 src/server/Dough.csproj create mode 100644 src/server/Migrations/20200729090558_Initial.Designer.cs create mode 100644 src/server/Migrations/20200729090558_Initial.cs create mode 100644 src/server/Migrations/MainDbContextModelSnapshot.cs create mode 100644 src/server/Models/Database/BaseModel.cs create mode 100644 src/server/Models/Database/Category.cs create mode 100644 src/server/Models/Database/MainDbContext.cs create mode 100644 src/server/Models/Database/Payee.cs create mode 100644 src/server/Models/Database/Transaction.cs create mode 100644 src/server/Models/Database/User.cs create mode 100644 src/server/Models/DbSetOverrides.cs create mode 100644 src/server/Models/Exceptions/ModelValidationException.cs create mode 100644 src/server/Models/Results/ErrorResult.cs create mode 100644 src/server/Program.cs create mode 100644 src/server/Properties/launchSettings.json create mode 100644 src/server/Startup.cs create mode 100644 src/server/Utilities/ExtensionMethods.cs create mode 100644 src/server/appsettings.json create mode 100644 src/server/database.sqlite (limited to 'src/server') diff --git a/src/server/Controllers/AccountController.cs b/src/server/Controllers/AccountController.cs new file mode 100644 index 0000000..58bb7b6 --- /dev/null +++ b/src/server/Controllers/AccountController.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Dough.Models; +using Dough.Models.Database; +using Dough.Models.Results; +using Dough.Utilities; + +namespace Dough.Controllers +{ + public class AccountController : BaseController + { + private readonly MainDbContext _context; + + public AccountController(MainDbContext context) + { + _context = context; + } + + [HttpPost("login")] + public async Task Login(string username, string password) + { + var user = _context.Users.SingleByNameOrDefault(username); + if (user == default) + return BadRequest(new ErrorResult("Ugyldig brukernavn eller passord", + "Verifiser at passord og brukernavn er riktig og prøv igjen")); + + if (!user.VerifyPassword(password)) + return BadRequest(new ErrorResult("Ugyldig brukernavn eller passord", + "Verifiser at passord og brukernavn er riktig")); + + var claims = new List + { + new Claim(ClaimTypes.Name, user.Username), + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) + }; + + var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + + var authenticationProperties = new AuthenticationProperties + { + IsPersistent = false, + IssuedUtc = DateTime.UtcNow, + AllowRefresh = true, + ExpiresUtc = DateTime.UtcNow.AddDays(7), + }; + + await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, + claimsPrincipal, + authenticationProperties); + + return Ok(); + } + + [HttpGet("logout")] + public async Task Logout(string continueTo = default) + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + if (continueTo.IsPresent() && continueTo.IsValidUrl()) return Redirect(continueTo); + return Ok(); + } + + [Authorize] + [HttpGet("me")] + public ActionResult GetClaimsForUser() + { + return Ok(LoggedInUser); + } + } +} diff --git a/src/server/Controllers/BaseController.cs b/src/server/Controllers/BaseController.cs new file mode 100644 index 0000000..33f1e4b --- /dev/null +++ b/src/server/Controllers/BaseController.cs @@ -0,0 +1,27 @@ +using System; +using System.Security.Claims; +using Microsoft.AspNetCore.Mvc; +using Dough.Models.Database; +using Dough.Utilities; + +namespace Dough.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class BaseController : ControllerBase + { + public LoggedInUserModel LoggedInUser => new LoggedInUserModel + { + Id = User.GetClaimValueOrDefault(ClaimTypes.NameIdentifier)?.ToGuidOrDefault() ?? default, + Username = User.GetClaimValueOrDefault(ClaimTypes.Name), + SessionStart = User.GetClaimValueOrDefault(ClaimTypes.AuthenticationInstant).ToDateTimeOrDefault() + }; + + public class LoggedInUserModel + { + public Guid Id { get; set; } + public string Username { get; set; } + public DateTime SessionStart { get; set; } + } + } +} diff --git a/src/server/Controllers/TransactionsController.cs b/src/server/Controllers/TransactionsController.cs new file mode 100644 index 0000000..8441beb --- /dev/null +++ b/src/server/Controllers/TransactionsController.cs @@ -0,0 +1,78 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Dough.Models.Database; +using Dough.Models; +using Dough.Models.Exceptions; +using Dough.Models.Results; + +namespace Dough.Controllers +{ + public class TransactionsController : BaseController + { + private readonly MainDbContext _context; + + public TransactionsController(MainDbContext context) + { + _context = context; + } + + [HttpGet] + public ActionResult GetTransactions() + { + var transactions = _context.Transactions.Where(c => !c.Hidden); + return Ok(transactions.ToList()); + } + + [HttpPost("add")] + public ActionResult AddTransaction(Transaction data) + { + data.SetBaseProperties(); + try + { + data.Validate(); + } + catch (Exception e) + { + Console.WriteLine(e); + if (e is ModelValidationException mve) + return BadRequest(mve.ErrorResult); + return BadRequest(new ErrorResult()); + } + + _context.Transactions.Add(data); + _context.SaveChanges(); + return Ok(); + } + + [HttpPost("update")] + public ActionResult UpdateTransaction(Transaction data) + { + try + { + data.Validate(); + } + catch (Exception e) + { + Console.WriteLine(e); + if (e is ModelValidationException mve) + return BadRequest(mve.ErrorResult); + return BadRequest(new ErrorResult()); + } + + var transaction = _context.Transactions.SingleOrDefault(data.Id); + transaction.Update(data); + _context.SaveChanges(); + return Ok(data); + } + + [HttpDelete("delete")] + public ActionResult DeleteTransaction(Transaction data) + { + if (data.Id == default) return BadRequest(new ErrorResult()); + _context.Transactions.Remove(data); + _context.SaveChanges(); + return Ok(); + } + } +} diff --git a/src/server/Controllers/UsersController.cs b/src/server/Controllers/UsersController.cs new file mode 100644 index 0000000..bef8cc2 --- /dev/null +++ b/src/server/Controllers/UsersController.cs @@ -0,0 +1,6 @@ +namespace Dough.Controllers +{ + public class UsersController : BaseController + { + } +} diff --git a/src/server/Dough.csproj b/src/server/Dough.csproj new file mode 100644 index 0000000..ae3d56a --- /dev/null +++ b/src/server/Dough.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + diff --git a/src/server/Migrations/20200729090558_Initial.Designer.cs b/src/server/Migrations/20200729090558_Initial.Designer.cs new file mode 100644 index 0000000..9657f66 --- /dev/null +++ b/src/server/Migrations/20200729090558_Initial.Designer.cs @@ -0,0 +1,143 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Dough.Models.Database; + +namespace Dough.Migrations +{ + [DbContext(typeof(MainDbContext))] + [Migration("20200729090558_Initial")] + partial class Initial + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.6"); + + modelBuilder.Entity("MoneyManager.Models.Database.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Hidden") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("MoneyManager.Models.Database.Payee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Hidden") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Payees"); + }); + + modelBuilder.Entity("MoneyManager.Models.Database.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("CategoryId") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Hidden") + .HasColumnType("INTEGER"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("PayeeId") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("MoneyManager.Models.Database.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Hidden") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + + b.HasData( + new + { + Id = new Guid("193053d0-4292-4dc5-baae-59a920b64891"), + Created = new DateTime(2020, 7, 29, 9, 5, 57, 914, DateTimeKind.Utc).AddTicks(3427), + Hidden = false, + Password = "$2b$10$RFdcYLeqporq94pUIOoJGOPnhUbpV7R4e.2Iz8ot02N2PqeCpDCA6", + Username = "ivar" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/server/Migrations/20200729090558_Initial.cs b/src/server/Migrations/20200729090558_Initial.cs new file mode 100644 index 0000000..5dc0b40 --- /dev/null +++ b/src/server/Migrations/20200729090558_Initial.cs @@ -0,0 +1,97 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Dough.Migrations +{ + public partial class Initial : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(nullable: false), + Created = table.Column(nullable: false), + CreatedBy = table.Column(nullable: true), + Hidden = table.Column(nullable: false), + Name = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Payees", + columns: table => new + { + Id = table.Column(nullable: false), + Created = table.Column(nullable: false), + CreatedBy = table.Column(nullable: true), + Hidden = table.Column(nullable: false), + Name = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Payees", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Transactions", + columns: table => new + { + Id = table.Column(nullable: false), + Created = table.Column(nullable: false), + CreatedBy = table.Column(nullable: true), + Hidden = table.Column(nullable: false), + Tags = table.Column(nullable: true), + Note = table.Column(nullable: true), + Date = table.Column(nullable: false), + Amount = table.Column(nullable: false), + PayeeId = table.Column(nullable: false), + CategoryId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Transactions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(nullable: false), + Created = table.Column(nullable: false), + CreatedBy = table.Column(nullable: true), + Hidden = table.Column(nullable: false), + Password = table.Column(nullable: true), + Username = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.InsertData( + table: "Users", + columns: new[] { "Id", "Created", "CreatedBy", "Hidden", "Password", "Username" }, + values: new object[] { new Guid("193053d0-4292-4dc5-baae-59a920b64891"), new DateTime(2020, 7, 29, 9, 5, 57, 914, DateTimeKind.Utc).AddTicks(3427), null, false, "$2b$10$RFdcYLeqporq94pUIOoJGOPnhUbpV7R4e.2Iz8ot02N2PqeCpDCA6", "ivar" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "Payees"); + + migrationBuilder.DropTable( + name: "Transactions"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/src/server/Migrations/MainDbContextModelSnapshot.cs b/src/server/Migrations/MainDbContextModelSnapshot.cs new file mode 100644 index 0000000..ad883c5 --- /dev/null +++ b/src/server/Migrations/MainDbContextModelSnapshot.cs @@ -0,0 +1,141 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Dough.Models.Database; + +namespace Dough.Migrations +{ + [DbContext(typeof(MainDbContext))] + partial class MainDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.6"); + + modelBuilder.Entity("MoneyManager.Models.Database.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Hidden") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("MoneyManager.Models.Database.Payee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Hidden") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Payees"); + }); + + modelBuilder.Entity("MoneyManager.Models.Database.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("CategoryId") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Hidden") + .HasColumnType("INTEGER"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("PayeeId") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Transactions"); + }); + + modelBuilder.Entity("MoneyManager.Models.Database.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Hidden") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + + b.HasData( + new + { + Id = new Guid("193053d0-4292-4dc5-baae-59a920b64891"), + Created = new DateTime(2020, 7, 29, 9, 5, 57, 914, DateTimeKind.Utc).AddTicks(3427), + Hidden = false, + Password = "$2b$10$RFdcYLeqporq94pUIOoJGOPnhUbpV7R4e.2Iz8ot02N2PqeCpDCA6", + Username = "ivar" + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/server/Models/Database/BaseModel.cs b/src/server/Models/Database/BaseModel.cs new file mode 100644 index 0000000..32c2f8f --- /dev/null +++ b/src/server/Models/Database/BaseModel.cs @@ -0,0 +1,25 @@ +using System; + +namespace Dough.Models.Database +{ + public class BaseModel + { + public Guid Id { get; set; } + public DateTime Created { get; set; } + public Guid? CreatedBy { get; set; } + public bool Hidden { get; set; } + + public void SetBaseProperties(User actor = default) + { + Id = Guid.NewGuid(); + Created = DateTime.UtcNow; + CreatedBy = actor?.Id; + } + + protected void Update(BaseModel data) + { + Hidden = data.Hidden; + CreatedBy = data.CreatedBy; + } + } +} diff --git a/src/server/Models/Database/Category.cs b/src/server/Models/Database/Category.cs new file mode 100644 index 0000000..10ef226 --- /dev/null +++ b/src/server/Models/Database/Category.cs @@ -0,0 +1,7 @@ +namespace Dough.Models.Database +{ + public class Category : BaseModel + { + public string Name { get; set; } + } +} diff --git a/src/server/Models/Database/MainDbContext.cs b/src/server/Models/Database/MainDbContext.cs new file mode 100644 index 0000000..88b1585 --- /dev/null +++ b/src/server/Models/Database/MainDbContext.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; + +namespace Dough.Models.Database +{ + public class MainDbContext : DbContext + { + + public MainDbContext(DbContextOptions options) : base(options) { + + } + + public DbSet Transactions { get; set; } + public DbSet Users { get; set; } + public DbSet Categories { get; set; } + public DbSet Payees { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("Transactions"); + var initUser = new User("ivar"); + initUser.SetBaseProperties(); + initUser.HashAndSetPassword("ivar123"); + modelBuilder.Entity().HasData(new List + { + initUser + }); + modelBuilder.Entity().ToTable("Users"); + modelBuilder.Entity().ToTable("Categories"); + modelBuilder.Entity().ToTable("Payees"); + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/src/server/Models/Database/Payee.cs b/src/server/Models/Database/Payee.cs new file mode 100644 index 0000000..b801d5f --- /dev/null +++ b/src/server/Models/Database/Payee.cs @@ -0,0 +1,7 @@ +namespace Dough.Models.Database +{ + public class Payee : BaseModel + { + public string Name { get; set; } + } +} diff --git a/src/server/Models/Database/Transaction.cs b/src/server/Models/Database/Transaction.cs new file mode 100644 index 0000000..a81626b --- /dev/null +++ b/src/server/Models/Database/Transaction.cs @@ -0,0 +1,43 @@ +using System; +using Dough.Models.Exceptions; + +namespace Dough.Models.Database +{ + public class Transaction : BaseModel + { + public string Tags { get; set; } + public string Note { get; set; } + public DateTime Date { get; set; } + public double Amount { get; set; } + public Guid PayeeId { get; set; } + public Guid CategoryId { get; set; } + + public void Validate() + { + if (PayeeId == default) + { + var validationException = new ModelValidationException("PayeeId is invalid"); + validationException.ErrorResult.Title = "Mottaker er ugyldig"; + throw validationException; + } + + if (CategoryId == default) + { + var validationException = new ModelValidationException("CategoryId is invalid"); + validationException.ErrorResult.Title = "Kategori er ugyldig"; + throw validationException; + } + } + + public void Update(Transaction data) + { + Amount = data.Amount; + Date = data.Date; + Note = data.Note; + Tags = data.Tags; + CategoryId = data.CategoryId; + PayeeId = data.PayeeId; + base.Update(data); + } + } +} diff --git a/src/server/Models/Database/User.cs b/src/server/Models/Database/User.cs new file mode 100644 index 0000000..479c15c --- /dev/null +++ b/src/server/Models/Database/User.cs @@ -0,0 +1,31 @@ +using System; + +namespace Dough.Models.Database +{ + public class User : BaseModel + { + + public User(string username = default) + { + Username = username; + } + public string Password { get; set; } + public string Username { get; set; } + + public void Update(User data) + { + Username = data.Username; + base.Update(data); + } + + public void HashAndSetPassword(string password) + { + Password = BCrypt.Net.BCrypt.HashPassword(password); + } + + public bool VerifyPassword(string password) + { + return BCrypt.Net.BCrypt.Verify(password, Password); + } + } +} diff --git a/src/server/Models/DbSetOverrides.cs b/src/server/Models/DbSetOverrides.cs new file mode 100644 index 0000000..24622bc --- /dev/null +++ b/src/server/Models/DbSetOverrides.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Dough.Models.Database; + +namespace Dough.Models +{ + public static class DbSetOverrides + { + public static T SingleOrDefault(this DbSet entity, Guid id, + bool includeHidden = false) where T : BaseModel + { + if (includeHidden) + return entity.SingleOrDefault(c => c.Id == id); + return entity.SingleOrDefault(c => c.Id == id && !c.Hidden); + } + + public static User SingleByNameOrDefault(this DbSet users, string username, bool includeHidden = false) + { + if (includeHidden) + return users.SingleOrDefault(c => c.Username == username); + return users.SingleOrDefault(c => c.Username == username && !c.Hidden); + } + } +} diff --git a/src/server/Models/Exceptions/ModelValidationException.cs b/src/server/Models/Exceptions/ModelValidationException.cs new file mode 100644 index 0000000..3156229 --- /dev/null +++ b/src/server/Models/Exceptions/ModelValidationException.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.Serialization; +using Dough.Models.Results; + +namespace Dough.Models.Exceptions +{ + [Serializable] + public class ModelValidationException : Exception + { + public ErrorResult ErrorResult { get; set; } + + public ModelValidationException() + { + ErrorResult = new ErrorResult(); + } + + public ModelValidationException(string message) : base(message) + { + } + + public ModelValidationException(string message, Exception inner) : base(message, inner) + { + } + + protected ModelValidationException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/server/Models/Results/ErrorResult.cs b/src/server/Models/Results/ErrorResult.cs new file mode 100644 index 0000000..8d4504f --- /dev/null +++ b/src/server/Models/Results/ErrorResult.cs @@ -0,0 +1,14 @@ +namespace Dough.Models.Results +{ + public class ErrorResult + { + public ErrorResult(string title = default, string message = default) + { + Title = title; + Message = message; + } + + public string Title { get; set; } = "En feil oppstod"; + public string Message { get; set; } = "Vennligst prøv igjen snart"; + } +} diff --git a/src/server/Program.cs b/src/server/Program.cs new file mode 100644 index 0000000..04a9447 --- /dev/null +++ b/src/server/Program.cs @@ -0,0 +1,55 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; + +namespace Dough +{ + public class Program + { + public static int Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) + .MinimumLevel.Override("System", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.File( + @"/var/log/money-manager.txt", + fileSizeLimitBytes: 4_000_000, + rollOnFileSizeLimit: true, + shared: true, + flushToDiskInterval: TimeSpan.FromSeconds(1)) + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code) + .CreateLogger(); + + try + { + Log.Information("Starting host..."); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly."); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSerilog() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/server/Properties/launchSettings.json b/src/server/Properties/launchSettings.json new file mode 100644 index 0000000..2b071f3 --- /dev/null +++ b/src/server/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "MoneyManager": { + "commandName": "Project", + "applicationUrl": "https://localhost:5002;http://localhost:5001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/server/Startup.cs b/src/server/Startup.cs new file mode 100644 index 0000000..ad98370 --- /dev/null +++ b/src/server/Startup.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Dough.Models; +using Dough.Utilities; +using Dough.Models.Database; + +namespace Dough +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + private const string MainCorsPolicy = "MainCorsPolicy"; + + public void ConfigureServices(IServiceCollection services) + { + + services.AddCors(options => + { + options.AddPolicy(MainCorsPolicy, builder => + { + builder + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials() + .WithOrigins("http://localhost:8080"); + }); + }); + + services.AddDbContext(options => { + options.UseSqlite("Data Source=database.sqlite"); + }); + + services.AddControllers(); + + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => + { + options.LoginPath = "/api/account/login"; + options.SlidingExpiration = true; + options.LogoutPath = "/api/account/logout"; + }); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + app.UseDeveloperExceptionPage(); + + app.UseCors(MainCorsPolicy); + app.UseRouting(); + app.UseStatusCodePages(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + } +} diff --git a/src/server/Utilities/ExtensionMethods.cs b/src/server/Utilities/ExtensionMethods.cs new file mode 100644 index 0000000..d6bce94 --- /dev/null +++ b/src/server/Utilities/ExtensionMethods.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; + +namespace Dough.Utilities +{ + public static class ExtentionMethods + { + public static Guid ToGuidOrDefault(this string value) + { + if (value.IsMissing()) return default; + try + { + return new Guid(value); + } + catch (Exception e) + { + Console.WriteLine(e); + return default; + } + } + + public static DateTime ToDateTimeOrDefault(this string value) + { + if (value.IsMissing()) return default; + try + { + return DateTime.Parse(value); + } + catch (Exception e) + { + Console.WriteLine(e); + return default; + } + } + + public static bool IsMissing(this string value) => string.IsNullOrWhiteSpace(value); + public static bool IsPresent(this string value) => !value.IsMissing(); + + public static bool IsValidUrl(this string value) + { + return Uri.TryCreate(value, UriKind.Absolute, out var uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + } + + public static string GetClaimValueOrDefault(this IPrincipal currentPrincipal, string claimName) + { + var identity = currentPrincipal.Identity as ClaimsIdentity; + var claim = identity?.Claims.SingleOrDefault(c => c.Type == claimName); + return claim?.Value; + } + } +} diff --git a/src/server/appsettings.json b/src/server/appsettings.json new file mode 100644 index 0000000..81ff877 --- /dev/null +++ b/src/server/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/server/database.sqlite b/src/server/database.sqlite new file mode 100644 index 0000000..f86d443 Binary files /dev/null and b/src/server/database.sqlite differ -- cgit v1.3