aboutsummaryrefslogtreecommitdiffstats
path: root/src/server
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2020-08-01 20:14:34 +0200
committerivarlovlie <git@ivarlovlie.no>2020-08-01 20:14:34 +0200
commita800b3b9f18ae3e8ab030c30c5d7b6504f2a5ebb (patch)
tree68ffcdb7a2b42418ff1c4818d0b2cd5af41d5fa2 /src/server
downloaddough-a800b3b9f18ae3e8ab030c30c5d7b6504f2a5ebb.tar.xz
dough-a800b3b9f18ae3e8ab030c30c5d7b6504f2a5ebb.zip
Initial commit
Diffstat (limited to 'src/server')
-rw-r--r--src/server/Controllers/AccountController.cs76
-rw-r--r--src/server/Controllers/BaseController.cs27
-rw-r--r--src/server/Controllers/TransactionsController.cs78
-rw-r--r--src/server/Controllers/UsersController.cs6
-rw-r--r--src/server/Dough.csproj16
-rw-r--r--src/server/Migrations/20200729090558_Initial.Designer.cs143
-rw-r--r--src/server/Migrations/20200729090558_Initial.cs97
-rw-r--r--src/server/Migrations/MainDbContextModelSnapshot.cs141
-rw-r--r--src/server/Models/Database/BaseModel.cs25
-rw-r--r--src/server/Models/Database/Category.cs7
-rw-r--r--src/server/Models/Database/MainDbContext.cs34
-rw-r--r--src/server/Models/Database/Payee.cs7
-rw-r--r--src/server/Models/Database/Transaction.cs43
-rw-r--r--src/server/Models/Database/User.cs31
-rw-r--r--src/server/Models/DbSetOverrides.cs25
-rw-r--r--src/server/Models/Exceptions/ModelValidationException.cs31
-rw-r--r--src/server/Models/Results/ErrorResult.cs14
-rw-r--r--src/server/Program.cs55
-rw-r--r--src/server/Properties/launchSettings.json12
-rw-r--r--src/server/Startup.cs67
-rw-r--r--src/server/Utilities/ExtensionMethods.cs54
-rw-r--r--src/server/appsettings.json10
-rw-r--r--src/server/database.sqlitebin0 -> 45056 bytes
23 files changed, 999 insertions, 0 deletions
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<ActionResult> 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<Claim>
+ {
+ 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<ActionResult> 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 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="BCrypt.Net-Core" Version="1.6.0" />
+ <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.6" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.6" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.6" />
+ </ItemGroup>
+
+
+</Project>
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 @@
+// <auto-generated />
+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<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Hidden")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Categories");
+ });
+
+ modelBuilder.Entity("MoneyManager.Models.Database.Payee", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Hidden")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Payees");
+ });
+
+ modelBuilder.Entity("MoneyManager.Models.Database.Transaction", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<double>("Amount")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("CategoryId")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Date")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Hidden")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Note")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PayeeId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Transactions");
+ });
+
+ modelBuilder.Entity("MoneyManager.Models.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Hidden")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("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<Guid>(nullable: false),
+ Created = table.Column<DateTime>(nullable: false),
+ CreatedBy = table.Column<Guid>(nullable: true),
+ Hidden = table.Column<bool>(nullable: false),
+ Name = table.Column<string>(nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Categories", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Payees",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(nullable: false),
+ Created = table.Column<DateTime>(nullable: false),
+ CreatedBy = table.Column<Guid>(nullable: true),
+ Hidden = table.Column<bool>(nullable: false),
+ Name = table.Column<string>(nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Payees", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Transactions",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(nullable: false),
+ Created = table.Column<DateTime>(nullable: false),
+ CreatedBy = table.Column<Guid>(nullable: true),
+ Hidden = table.Column<bool>(nullable: false),
+ Tags = table.Column<string>(nullable: true),
+ Note = table.Column<string>(nullable: true),
+ Date = table.Column<DateTime>(nullable: false),
+ Amount = table.Column<double>(nullable: false),
+ PayeeId = table.Column<Guid>(nullable: false),
+ CategoryId = table.Column<Guid>(nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Transactions", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Users",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(nullable: false),
+ Created = table.Column<DateTime>(nullable: false),
+ CreatedBy = table.Column<Guid>(nullable: true),
+ Hidden = table.Column<bool>(nullable: false),
+ Password = table.Column<string>(nullable: true),
+ Username = table.Column<string>(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 @@
+// <auto-generated />
+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<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Hidden")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Categories");
+ });
+
+ modelBuilder.Entity("MoneyManager.Models.Database.Payee", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Hidden")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Payees");
+ });
+
+ modelBuilder.Entity("MoneyManager.Models.Database.Transaction", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<double>("Amount")
+ .HasColumnType("REAL");
+
+ b.Property<Guid>("CategoryId")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Date")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Hidden")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Note")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid>("PayeeId")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("Tags")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Transactions");
+ });
+
+ modelBuilder.Entity("MoneyManager.Models.Database.User", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("TEXT");
+
+ b.Property<Guid?>("CreatedBy")
+ .HasColumnType("TEXT");
+
+ b.Property<bool>("Hidden")
+ .HasColumnType("INTEGER");
+
+ b.Property<string>("Password")
+ .HasColumnType("TEXT");
+
+ b.Property<string>("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<MainDbContext> options) : base(options) {
+
+ }
+
+ public DbSet<Transaction> Transactions { get; set; }
+ public DbSet<User> Users { get; set; }
+ public DbSet<Category> Categories { get; set; }
+ public DbSet<Payee> Payees { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity<Transaction>().ToTable("Transactions");
+ var initUser = new User("ivar");
+ initUser.SetBaseProperties();
+ initUser.HashAndSetPassword("ivar123");
+ modelBuilder.Entity<User>().HasData(new List<User>
+ {
+ initUser
+ });
+ modelBuilder.Entity<User>().ToTable("Users");
+ modelBuilder.Entity<Category>().ToTable("Categories");
+ modelBuilder.Entity<Payee>().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<T>(this DbSet<T> 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<User> 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<Startup>();
+ });
+ }
+}
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<MainDbContext>(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
--- /dev/null
+++ b/src/server/database.sqlite
Binary files differ