diff --git a/.idea/.idea.WardrobeManager/.idea/.gitignore b/.idea/.idea.WardrobeManager/.idea/.gitignore new file mode 100644 index 0000000..d8349db --- /dev/null +++ b/.idea/.idea.WardrobeManager/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.WardrobeManager.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.WardrobeManager/.idea/encodings.xml b/.idea/.idea.WardrobeManager/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.WardrobeManager/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.WardrobeManager/.idea/indexLayout.xml b/.idea/.idea.WardrobeManager/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.WardrobeManager/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.WardrobeManager/.idea/vcs.xml b/.idea/.idea.WardrobeManager/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.WardrobeManager/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Session.vim b/Session.vim new file mode 100644 index 0000000..e9c8bdb --- /dev/null +++ b/Session.vim @@ -0,0 +1,284 @@ +let SessionLoad = 1 +if &cp | set nocp | endif +let s:so_save = &g:so | let s:siso_save = &g:siso | setg so=0 siso=0 | setl so=-1 siso=-1 +let v:this_session=expand(":p") +silent only +silent tabonly +cd /mnt/FILEZ/Files/Documents/code/GITCLONE/my-stuff/WardrobeManager +if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == '' + let s:wipebuf = bufnr('%') +endif +let s:shortmess_save = &shortmess +if &shortmess =~ 'A' + set shortmess=aoOA +else + set shortmess=aoO +endif +badd +92 WardrobeManager.Api/Program.cs +badd +15 WardrobeManager.Shared/Models/User.cs +badd +1 .deepsource.toml +badd +51 WardrobeManager.Presentation/Program.cs +badd +48 WardrobeManager.Shared/Models/ServerClothingItem.cs +badd +1 WardrobeManager.Api/Database/DatabaseContext.cs +badd +1 WardrobeManager.Api/Database/Services/Implementation/ClothingItemService.cs +badd +48 WardrobeManager.Api/Database/Services/Implementation/UserService.cs +badd +1 WardrobeManager.Presentation/Pages/Dashboard.razor +badd +35 WardrobeManager.Presentation/Pages/Authenticated/Dashboard.razor +badd +85 WardrobeManager.Presentation/Pages/Authenticated/Clothing.razor +badd +5 WardrobeManager.Presentation/Pages/Authenticated/LoginInfo.razor +badd +1 WardrobeManager.Presentation/Pages/Public/Authentication.razor +badd +15 WardrobeManager.Presentation/Pages/Public/LoginInfo.razor +badd +13 WardrobeManager.Presentation/Pages/Public/Index.razor +badd +49 WardrobeManager.Presentation/Services/Implementation/ApiService.cs +badd +10 WardrobeManager.Api/Database/Services/Interfaces/IUserService.cs +badd +11 WardrobeManager.Presentation/Services/Interfaces/IApiService.cs +badd +1 WardrobeManager.Shared/Exceptions/UserNotFoundException.cs +badd +10 WardrobeManager.Api/Database/DatabaseInitializer.cs +argglobal +%argdel +$argadd WardrobeManager.Api/Program.cs +set stal=2 +tabnew +setlocal\ bufhidden=wipe +tabnew +setlocal\ bufhidden=wipe +tabnew +setlocal\ bufhidden=wipe +tabrewind +edit WardrobeManager.Api/Program.cs +argglobal +balt WardrobeManager.Api/Database/DatabaseInitializer.cs +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 100 - ((10 * winheight(0) + 22) / 45) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 100 +normal! 018| +tabnext +edit WardrobeManager.Presentation/Pages/Public/Index.razor +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +split +1wincmd k +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe '1resize ' . ((&lines * 27 + 24) / 48) +exe '2resize ' . ((&lines * 17 + 24) / 48) +argglobal +balt WardrobeManager.Presentation/Pages/Authenticated/Clothing.razor +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 13 - ((12 * winheight(0) + 13) / 27) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 13 +normal! 027| +wincmd w +argglobal +if bufexists(fnamemodify("WardrobeManager.Presentation/Services/Implementation/ApiService.cs", ":p")) | buffer WardrobeManager.Presentation/Services/Implementation/ApiService.cs | else | edit WardrobeManager.Presentation/Services/Implementation/ApiService.cs | endif +balt WardrobeManager.Presentation/Services/Interfaces/IApiService.cs +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 21 - ((3 * winheight(0) + 8) / 17) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 21 +normal! 021| +wincmd w +exe '1resize ' . ((&lines * 27 + 24) / 48) +exe '2resize ' . ((&lines * 17 + 24) / 48) +tabnext +edit WardrobeManager.Shared/Models/User.cs +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +split +1wincmd k +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe '1resize ' . ((&lines * 22 + 24) / 48) +exe '2resize ' . ((&lines * 22 + 24) / 48) +argglobal +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 16 - ((15 * winheight(0) + 11) / 22) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 16 +normal! 0 +wincmd w +argglobal +if bufexists(fnamemodify("WardrobeManager.Shared/Models/ServerClothingItem.cs", ":p")) | buffer WardrobeManager.Shared/Models/ServerClothingItem.cs | else | edit WardrobeManager.Shared/Models/ServerClothingItem.cs | endif +balt WardrobeManager.Api/Program.cs +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +silent! normal! zE +let &fdl = &fdl +let s:l = 25 - ((0 * winheight(0) + 11) / 22) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 25 +normal! 083| +wincmd w +exe '1resize ' . ((&lines * 22 + 24) / 48) +exe '2resize ' . ((&lines * 22 + 24) / 48) +tabnext +let s:save_splitbelow = &splitbelow +let s:save_splitright = &splitright +set splitbelow splitright +wincmd _ | wincmd | +split +wincmd _ | wincmd | +split +2wincmd k +wincmd w +wincmd w +let &splitbelow = s:save_splitbelow +let &splitright = s:save_splitright +wincmd t +let s:save_winminheight = &winminheight +let s:save_winminwidth = &winminwidth +set winminheight=0 +set winheight=1 +set winminwidth=0 +set winwidth=1 +exe '1resize ' . ((&lines * 15 + 24) / 48) +exe '2resize ' . ((&lines * 14 + 24) / 48) +exe '3resize ' . ((&lines * 16 + 24) / 48) +argglobal +terminal ++curwin ++cols=116 ++rows=15 +let s:term_buf_41 = bufnr() +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +let s:l = 4 - ((3 * winheight(0) + 7) / 15) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 4 +normal! 0 +wincmd w +argglobal +terminal ++curwin ++cols=116 ++rows=14 +let s:term_buf_42 = bufnr() +balt \!/bin/bash +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +let s:l = 34 - ((10 * winheight(0) + 7) / 14) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 34 +normal! 0 +wincmd w +argglobal +terminal ++curwin ++cols=116 ++rows=16 +let s:term_buf_43 = bufnr() +balt \!/bin/bash\ (1) +setlocal fdm=manual +setlocal fde=0 +setlocal fmr={{{,}}} +setlocal fdi=# +setlocal fdl=0 +setlocal fml=1 +setlocal fdn=20 +setlocal fen +let s:l = 4 - ((3 * winheight(0) + 8) / 16) +if s:l < 1 | let s:l = 1 | endif +keepjumps exe s:l +normal! zt +keepjumps 4 +normal! 030| +wincmd w +exe '1resize ' . ((&lines * 15 + 24) / 48) +exe '2resize ' . ((&lines * 14 + 24) / 48) +exe '3resize ' . ((&lines * 16 + 24) / 48) +tabnext 1 +set stal=1 +if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 + silent exe 'bwipe ' . s:wipebuf +endif +unlet! s:wipebuf +set winheight=1 winwidth=20 +let &shortmess = s:shortmess_save +let &winminheight = s:save_winminheight +let &winminwidth = s:save_winminwidth +let s:sx = expand(":p:r")."x.vim" +if filereadable(s:sx) + exe "source " . fnameescape(s:sx) +endif +let &g:so = s:so_save | let &g:siso = s:siso_save +doautoall SessionLoadPost +unlet SessionLoad +" vim: set ft=vim : diff --git a/WardrobeManager.Api/Database/DatabaseContext.cs b/WardrobeManager.Api/Database/DatabaseContext.cs index 3e06934..3be5568 100644 --- a/WardrobeManager.Api/Database/DatabaseContext.cs +++ b/WardrobeManager.Api/Database/DatabaseContext.cs @@ -3,7 +3,7 @@ namespace WardrobeManager.Api.Database; -public class DatabaseContext : DbContext, IDatabaseContext +public class DatabaseContext : DbContext { public DatabaseContext(DbContextOptions options) : base(options) @@ -11,10 +11,17 @@ public DatabaseContext(DbContextOptions options) : base(options } + public DbSet Users { get; set; } public DbSet ClothingItems { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasMany(e=> e.ServerClothingItems) + .WithOne(e=> e.User) + .HasForeignKey(e=> e.UserId) + .IsRequired(); } } diff --git a/WardrobeManager.Api/Database/DatabaseInitializer.cs b/WardrobeManager.Api/Database/DatabaseInitializer.cs index 20e48d2..180ad06 100644 --- a/WardrobeManager.Api/Database/DatabaseInitializer.cs +++ b/WardrobeManager.Api/Database/DatabaseInitializer.cs @@ -7,18 +7,6 @@ public class DatabaseInitializer { public static void Initialize(DatabaseContext context) { - if (context.ClothingItems.Count() > 0) { return; } - - - var clothingItems = new List - { - // Creating two base clothing items - new ServerClothingItem("Test Clothing 1", ClothingCategory.TShirt, null), - new ServerClothingItem("Test Clothing 2", ClothingCategory.Sweatpants, null) - }; - - - clothingItems.ForEach(d => context.ClothingItems.Add(d)); - context.SaveChanges(); + // nothing for now, users will be created through webapp } } diff --git a/WardrobeManager.Api/Database/IDatabaseContext.cs b/WardrobeManager.Api/Database/IDatabaseContext.cs deleted file mode 100644 index fa8bd21..0000000 --- a/WardrobeManager.Api/Database/IDatabaseContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using WardrobeManager.Shared.Models; - -namespace WardrobeManager.Api.Database; -public interface IDatabaseContext -{ - DbSet ClothingItems { get; set; } -} \ No newline at end of file diff --git a/WardrobeManager.Api/Database/Services/Implementation/ClothingItemService.cs b/WardrobeManager.Api/Database/Services/Implementation/ClothingItemService.cs index 458cf30..1019802 100644 --- a/WardrobeManager.Api/Database/Services/Implementation/ClothingItemService.cs +++ b/WardrobeManager.Api/Database/Services/Implementation/ClothingItemService.cs @@ -13,7 +13,7 @@ public ClothingItemService(DatabaseContext databaseContext) _databaseContext = databaseContext; } - public async Task CreateUser(string userId) + // public async Task CreateUser(string userId) public async Task> GetClothes() { diff --git a/WardrobeManager.Api/Database/Services/Implementation/UserService.cs b/WardrobeManager.Api/Database/Services/Implementation/UserService.cs new file mode 100644 index 0000000..302f0e8 --- /dev/null +++ b/WardrobeManager.Api/Database/Services/Implementation/UserService.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore; +using WardrobeManager.Shared.Models; +using WardrobeManager.Shared.Enums; +using WardrobeManager.Shared.Exceptions; +using WardrobeManager.Api.Database.Services.Interfaces; + +namespace WardrobeManager.Api.Database.Services.Implementation; + +public class UserService : IUserService +{ + private readonly DatabaseContext context; + + public UserService(DatabaseContext databaseContext) + { + context = databaseContext; + } + + public async Task CreateUser(string Auth0Id) { + // fail silently if user exists + if (!context.Users.Any(s => s.Auth0Id == Auth0Id)) { + var newUser = new User(Auth0Id); + newUser.ServerClothingItems = new List(); + + var sampleClothingItems = new List + { + new ServerClothingItem("Example T-Shirt", ClothingCategory.TShirt, null), + new ServerClothingItem("Example Pants", ClothingCategory.Jeans, null) + }; + newUser.ServerClothingItems = sampleClothingItems; + + await context.Users.AddAsync(newUser); + await context.SaveChangesAsync(); + } + } + + public async Task DoesUserExist(string Auth0Id) { + if (await context.Users.AnyAsync(user => user.Auth0Id == Auth0Id)) { + return true; + } + return false; + } + + public async Task GetUser(string Auth0Id) { + var user = await context.Users.SingleOrDefaultAsync(user => user.Auth0Id == Auth0Id); + + if (user == null) { + throw new UserNotFoundException(); + } + + return user; + } + + +} diff --git a/WardrobeManager.Api/Database/Services/Interfaces/IUserService.cs b/WardrobeManager.Api/Database/Services/Interfaces/IUserService.cs new file mode 100644 index 0000000..66a761c --- /dev/null +++ b/WardrobeManager.Api/Database/Services/Interfaces/IUserService.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; +using WardrobeManager.Shared.Models; +using WardrobeManager.Api.Database.Services.Interfaces; + +namespace WardrobeManager.Api.Database.Services.Interfaces; + +public interface IUserService +{ + public Task CreateUser(string Auth0Id); + public Task DoesUserExist(string Auth0Id); + public Task GetUser(string Auth0Id); +} diff --git a/WardrobeManager.Api/Migrations/20240720023611_AddedUserType.Designer.cs b/WardrobeManager.Api/Migrations/20240720023611_AddedUserType.Designer.cs new file mode 100644 index 0000000..edc8d3f --- /dev/null +++ b/WardrobeManager.Api/Migrations/20240720023611_AddedUserType.Designer.cs @@ -0,0 +1,106 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WardrobeManager.Api.Database; + +#nullable disable + +namespace WardrobeManager.Api.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240720023611_AddedUserType")] + partial class AddedUserType + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); + + modelBuilder.Entity("WardrobeManager.Shared.Models.ServerClothingItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DesiredTimesWornBeforeWash") + .HasColumnType("INTEGER"); + + b.Property("Favourited") + .HasColumnType("INTEGER"); + + b.Property("ImageGuid") + .HasColumnType("TEXT"); + + b.Property("LastWorn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("Season") + .HasColumnType("INTEGER"); + + b.Property("TimesWornSinceWash") + .HasColumnType("INTEGER"); + + b.Property("TimesWornTotal") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("ClothingItems"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Auth0Id") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.ServerClothingItem", b => + { + b.HasOne("WardrobeManager.Shared.Models.User", "Owner") + .WithMany("ServerClothingItems") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.User", b => + { + b.Navigation("ServerClothingItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WardrobeManager.Api/Migrations/20240720023611_AddedUserType.cs b/WardrobeManager.Api/Migrations/20240720023611_AddedUserType.cs new file mode 100644 index 0000000..e7b5b8f --- /dev/null +++ b/WardrobeManager.Api/Migrations/20240720023611_AddedUserType.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WardrobeManager.Api.Migrations +{ + /// + public partial class AddedUserType : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "OwnerId", + table: "ClothingItems", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Auth0Id = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ClothingItems_OwnerId", + table: "ClothingItems", + column: "OwnerId"); + + migrationBuilder.AddForeignKey( + name: "FK_ClothingItems_Users_OwnerId", + table: "ClothingItems", + column: "OwnerId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ClothingItems_Users_OwnerId", + table: "ClothingItems"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropIndex( + name: "IX_ClothingItems_OwnerId", + table: "ClothingItems"); + + migrationBuilder.DropColumn( + name: "OwnerId", + table: "ClothingItems"); + } + } +} diff --git a/WardrobeManager.Api/Migrations/20240720051434_addedRelationship.Designer.cs b/WardrobeManager.Api/Migrations/20240720051434_addedRelationship.Designer.cs new file mode 100644 index 0000000..e23efbc --- /dev/null +++ b/WardrobeManager.Api/Migrations/20240720051434_addedRelationship.Designer.cs @@ -0,0 +1,106 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WardrobeManager.Api.Database; + +#nullable disable + +namespace WardrobeManager.Api.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240720051434_addedRelationship")] + partial class addedRelationship + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); + + modelBuilder.Entity("WardrobeManager.Shared.Models.ServerClothingItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DesiredTimesWornBeforeWash") + .HasColumnType("INTEGER"); + + b.Property("Favourited") + .HasColumnType("INTEGER"); + + b.Property("ImageGuid") + .HasColumnType("TEXT"); + + b.Property("LastWorn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Season") + .HasColumnType("INTEGER"); + + b.Property("TimesWornSinceWash") + .HasColumnType("INTEGER"); + + b.Property("TimesWornTotal") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ClothingItems"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Auth0Id") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.ServerClothingItem", b => + { + b.HasOne("WardrobeManager.Shared.Models.User", "User") + .WithMany("ServerClothingItems") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.User", b => + { + b.Navigation("ServerClothingItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WardrobeManager.Api/Migrations/20240720051434_addedRelationship.cs b/WardrobeManager.Api/Migrations/20240720051434_addedRelationship.cs new file mode 100644 index 0000000..3472d16 --- /dev/null +++ b/WardrobeManager.Api/Migrations/20240720051434_addedRelationship.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WardrobeManager.Api.Migrations +{ + /// + public partial class addedRelationship : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ClothingItems_Users_OwnerId", + table: "ClothingItems"); + + migrationBuilder.RenameColumn( + name: "OwnerId", + table: "ClothingItems", + newName: "UserId"); + + migrationBuilder.RenameIndex( + name: "IX_ClothingItems_OwnerId", + table: "ClothingItems", + newName: "IX_ClothingItems_UserId"); + + migrationBuilder.AddForeignKey( + name: "FK_ClothingItems_Users_UserId", + table: "ClothingItems", + column: "UserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_ClothingItems_Users_UserId", + table: "ClothingItems"); + + migrationBuilder.RenameColumn( + name: "UserId", + table: "ClothingItems", + newName: "OwnerId"); + + migrationBuilder.RenameIndex( + name: "IX_ClothingItems_UserId", + table: "ClothingItems", + newName: "IX_ClothingItems_OwnerId"); + + migrationBuilder.AddForeignKey( + name: "FK_ClothingItems_Users_OwnerId", + table: "ClothingItems", + column: "OwnerId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/WardrobeManager.Api/Migrations/20240720052402_thin.Designer.cs b/WardrobeManager.Api/Migrations/20240720052402_thin.Designer.cs new file mode 100644 index 0000000..5f34585 --- /dev/null +++ b/WardrobeManager.Api/Migrations/20240720052402_thin.Designer.cs @@ -0,0 +1,106 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WardrobeManager.Api.Database; + +#nullable disable + +namespace WardrobeManager.Api.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240720052402_thin")] + partial class thin + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); + + modelBuilder.Entity("WardrobeManager.Shared.Models.ServerClothingItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DesiredTimesWornBeforeWash") + .HasColumnType("INTEGER"); + + b.Property("Favourited") + .HasColumnType("INTEGER"); + + b.Property("ImageGuid") + .HasColumnType("TEXT"); + + b.Property("LastWorn") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Season") + .HasColumnType("INTEGER"); + + b.Property("TimesWornSinceWash") + .HasColumnType("INTEGER"); + + b.Property("TimesWornTotal") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ClothingItems"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Auth0Id") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.ServerClothingItem", b => + { + b.HasOne("WardrobeManager.Shared.Models.User", "User") + .WithMany("ServerClothingItems") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.User", b => + { + b.Navigation("ServerClothingItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/WardrobeManager.Api/Migrations/20240720052402_thin.cs b/WardrobeManager.Api/Migrations/20240720052402_thin.cs new file mode 100644 index 0000000..ef784fc --- /dev/null +++ b/WardrobeManager.Api/Migrations/20240720052402_thin.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WardrobeManager.Api.Migrations +{ + /// + public partial class thin : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/WardrobeManager.Api/Migrations/DatabaseContextModelSnapshot.cs b/WardrobeManager.Api/Migrations/DatabaseContextModelSnapshot.cs index cbbd542..a04bd02 100644 --- a/WardrobeManager.Api/Migrations/DatabaseContextModelSnapshot.cs +++ b/WardrobeManager.Api/Migrations/DatabaseContextModelSnapshot.cs @@ -57,10 +57,46 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("TimesWornTotal") .HasColumnType("INTEGER"); + b.Property("UserId") + .HasColumnType("INTEGER"); + b.HasKey("Id"); + b.HasIndex("UserId"); + b.ToTable("ClothingItems"); }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Auth0Id") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.ServerClothingItem", b => + { + b.HasOne("WardrobeManager.Shared.Models.User", "User") + .WithMany("ServerClothingItems") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("WardrobeManager.Shared.Models.User", b => + { + b.Navigation("ServerClothingItems"); + }); #pragma warning restore 612, 618 } } diff --git a/WardrobeManager.Api/Program.cs b/WardrobeManager.Api/Program.cs index a553487..e9ba30a 100644 --- a/WardrobeManager.Api/Program.cs +++ b/WardrobeManager.Api/Program.cs @@ -5,6 +5,7 @@ using WardrobeManager.Shared.Models; using Microsoft.AspNetCore.Mvc; using WardrobeManager.Shared.Services.Interfaces; +using WardrobeManager.Shared.Exceptions; using WardrobeManager.Shared.Services.Implementation; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; @@ -15,8 +16,8 @@ // add db context builder.Services.AddDbContext(options => - options.UseSqlite("Data Source=database.dat") -); + options.UseSqlite("Data Source=database.dat") + ); // services added by me but created by microsft // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle @@ -28,23 +29,24 @@ //builder.WebHost.UseUrls("http://localhost:9865"); // custom services -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // auth0 var domain = $"https://{builder.Configuration["Auth0:Domain"]}/"; builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, c => - { - c.Authority = domain; - c.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters - { + { + c.Authority = domain; + c.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { ValidAudience = builder.Configuration["Auth0:Audience"], ValidIssuer = domain, - }; - }); + }; + }); //builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // .AddJwtBearer(options => // { @@ -54,11 +56,11 @@ builder.Services.AddAuthorization(o => -{ - o.AddPolicy("clothing:read-write", p => p. - RequireAuthenticatedUser(). - RequireClaim("scope", "clothing:read-write")); -}); + { + o.AddPolicy("clothing:read-write", p => p. + RequireAuthenticatedUser(). + RequireClaim("scope", "clothing:read-write")); + }); builder.Services.AddCors(); @@ -71,10 +73,10 @@ app.UseSwagger(); app.UseSwaggerUI(); app.UseCors(builder => builder - .AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader() - ); + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + ); } // auth0 @@ -84,140 +86,145 @@ app.UseHttpsRedirection(); -// Initialize DB (only run if db doesn't exist) -using var scope = app.Services.CreateScope(); -var context = scope.ServiceProvider.GetRequiredService(); -DatabaseInitializer.Initialize(context); -// ---------------------------------- -// All clothing items -// ---------------------------------- -app.MapGet("/ping", async Task (HttpContext context) => -{ +// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write?view=aspnetcore-8.0 +// middleware (convert into class when you clean up this file) +app.Use(async (HttpContext context, RequestDelegate next) => { - //var userId = context.User.FindFirst() - var userId = context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; + var userService = context.RequestServices.GetRequiredService(); - string resp = ""; - foreach (var iden in context.User.Identities) - { - resp += $"{iden.Name} - {iden.Label}\n"; - } - var f = context.User; + var Auth0Id = context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; - resp += $"{DateTime.UtcNow} Service is active - user: {userId}"; + // only create the user if the consumer of the api is authorized + if (Auth0Id != null) + { + await userService.CreateUser(Auth0Id); + } - return Results.Ok(resp); -}).RequireAuthorization(); + // pass along to controllers + context.Items["Auth0Id"] = Auth0Id; -app.MapGet("/createuser", async Task (HttpContext context) => -{ - var userId = context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; - - if (userId == null) - { - return Results.NotFound("No user ID"); - } + await next(context); + }); -}).RequireAuthorization(); +// Initialize DB (only run if db doesn't exist) +using var scope = app.Services.CreateScope(); +var context = scope.ServiceProvider.GetRequiredService(); +DatabaseInitializer.Initialize(context); +// ---------------------------------- +// Miscellaneous +// ---------------------------------- +app.MapGet("/ping", async Task (HttpContext context) => + { + return Results.Ok("WardrobeManager API is active"); + }); -app.MapGet("/clothing", [Authorize] async Task (IClothingItemService clothingItemService) => -{ - var clothes = await clothingItemService.GetClothes(); +// ---------------------------------- +// All clothing items +// ---------------------------------- +app.MapGet("/clothing", async Task (HttpContext context, IUserService userService, DatabaseContext con) => + { + // assuming user exists in db because of middleware + // assuming this exists because the user is logged in + var Auth0Id = context.Items["Auth0Id"].ToString(); + + var user = await userService.GetUser(Auth0Id); - return clothes.ToArray(); -}).WithName("Get Clothes").WithOpenApi(); + var clothes = await con.ClothingItems.Where(item => item.UserId == user.Id).ToListAsync(); + + return Results.Ok(clothes); + + }).RequireAuthorization(); // ---------------------------------- // GET, POST, PUT for one clothing item // ---------------------------------- -app.MapDelete("/clothingitem", [Authorize] async Task (int? Id, IClothingItemService clothingItemService) => -{ - if (Id == null || Id == 0) - { - return Results.BadRequest("You must specify a clothing ID"); - } +app.MapPost("/clothingitem", async Task ([FromBody] NewOrEditedClothingItem newItem, IClothingItemService clothingItemService, ISharedService sharedService, IFileService fileService) => + { + if (!sharedService.IsValid(newItem.ClothingItem)) { + return Results.BadRequest("Invalid clothing item"); + } - int properId = Id.Value; + var newClothingItem = new ServerClothingItem + ( + name: newItem.ClothingItem.Name, + category: newItem.ClothingItem.Category, + imageGuid: Guid.NewGuid() + ); - try - { - await clothingItemService.Delete(properId); - return Results.Ok("Delete"); - } - catch (Exception ex) - { - return Results.NotFound(ex); // change this later, don't wanna expose exception details - } -}).WithName("Delete clothing item").WithOpenApi(); + // decode and save file to place on disk with guid as name + // make new obj of server clothing item and and use guid from before + // return "Created" + await fileService.SaveImage(newClothingItem.ImageGuid, newItem.ImageBase64); + await clothingItemService.Add(newClothingItem); -app.MapPut("/clothingitem", [Authorize] async Task ([FromBody] NewOrEditedClothingItem editedItem, IClothingItemService clothingItemService, ISharedService sharedService, IFileService fileService) => -{ - if (!sharedService.IsValid(editedItem.ClothingItem)) - { - return Results.NotFound("Item cannot be updated. It can either not be found or the provided data is invalid."); - } + return Results.Created(); - if (editedItem.ImageBase64 != string.Empty) - { - // generate new guid since its a new image - editedItem.ClothingItem.ImageGuid = Guid.NewGuid(); - } + }).RequireAuthorization(); - // decode and save file to place on disk with guid as name - // make new obj of server clothing item and and use guid from before - await fileService.SaveImage(editedItem.ClothingItem.ImageGuid, editedItem.ImageBase64); - - await clothingItemService.Update(editedItem.ClothingItem); +app.MapPut("/clothingitem", async Task ([FromBody] NewOrEditedClothingItem editedItem, IClothingItemService clothingItemService, ISharedService sharedService, IFileService fileService) => + { + if (!sharedService.IsValid(editedItem.ClothingItem)) + { + return Results.NotFound("Item cannot be updated. It can either not be found or the provided data is invalid."); + } - return Results.Ok(); -}).WithName("Edit an existing clothing item").WithOpenApi(); -app.MapPost("/clothingitem", [Authorize] async Task ([FromBody] NewOrEditedClothingItem newItem, IClothingItemService clothingItemService, ISharedService sharedService, IFileService fileService) => -{ - if (!sharedService.IsValid(newItem.ClothingItem)) { - return Results.BadRequest("Invalid clothing item"); - } + if (editedItem.ImageBase64 != string.Empty) + { + // generate new guid since its a new image + editedItem.ClothingItem.ImageGuid = Guid.NewGuid(); + } - var newClothingItem = new ServerClothingItem - ( - name: newItem.ClothingItem.Name, - category: newItem.ClothingItem.Category, - imageGuid: Guid.NewGuid() - ); + // decode and save file to place on disk with guid as name + // make new obj of server clothing item and and use guid from before + await fileService.SaveImage(editedItem.ClothingItem.ImageGuid, editedItem.ImageBase64); - // decode and save file to place on disk with guid as name - // make new obj of server clothing item and and use guid from before - // return "Created" - await fileService.SaveImage(newClothingItem.ImageGuid, newItem.ImageBase64); + await clothingItemService.Update(editedItem.ClothingItem); - await clothingItemService.Add(newClothingItem); + return Results.Ok(); + }).RequireAuthorization(); +app.MapDelete("/clothingitem", async Task (int? Id, IClothingItemService clothingItemService) => + { + if (Id == null || Id == 0) + { + return Results.BadRequest("You must specify a clothing ID"); + } - return Results.Created(); + int properId = Id.Value; -}).WithName("Create Clothing Item").WithOpenApi(); + try + { + await clothingItemService.Delete(properId); + return Results.Ok("Delete"); + } + catch (Exception ex) + { + return Results.NotFound(ex); // change this later, don't wanna expose exception details + } + }).RequireAuthorization(); // ---------------------------------- // Images // ---------------------------------- app.MapGet("/img/{*imagePath}", [Authorize] async Task (string imagePath, IFileService fileService) => -{ - return Results.File(await fileService.GetImage(imagePath)); - - // should implement - //return Results.NotFound("Image not found"); + { + return Results.File(await fileService.GetImage(imagePath)); + + // should implement + //return Results.NotFound("Image not found"); -}).WithName("Get Image").WithOpenApi(); + }).WithName("Get Image").WithOpenApi(); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/WardrobeManager.Presentation/App.razor b/WardrobeManager.Presentation/App.razor index 5b92588..988f9e2 100644 --- a/WardrobeManager.Presentation/App.razor +++ b/WardrobeManager.Presentation/App.razor @@ -1,4 +1,6 @@ - +@using WardrobeManager.Presentation.Components.Public + + @@ -6,11 +8,9 @@

Determining session state, please wait...

-

Sorry

-

You're not authorized to reach this page. You need to log in.

+
-
Not found @@ -19,4 +19,4 @@
-
\ No newline at end of file +
diff --git a/WardrobeManager.Presentation/Componenets/Public/NotAuthenticated.razor b/WardrobeManager.Presentation/Componenets/Public/NotAuthenticated.razor new file mode 100644 index 0000000..923372c --- /dev/null +++ b/WardrobeManager.Presentation/Componenets/Public/NotAuthenticated.razor @@ -0,0 +1,7 @@ +@namespace WardrobeManager.Presentation.Components.Public + + +
+

You are not authenticated

+

Plese log in here

+
diff --git a/WardrobeManager.Presentation/Layout/Footer.razor b/WardrobeManager.Presentation/Layout/Footer.razor index a4ca748..ec98d0f 100644 --- a/WardrobeManager.Presentation/Layout/Footer.razor +++ b/WardrobeManager.Presentation/Layout/Footer.razor @@ -1,9 +1,11 @@ -@inject NavigationManager Navigation +@namespace WardrobeManager.Presentation.Layout + +@inject NavigationManager Navigation @if (!IsAppPage()) {
-

Wardrobe Manager - Built By Musa Ahmed | Since 2024

+

Wardrobe Manager - Built By Musa Ahmed | Since 2024

} diff --git a/WardrobeManager.Presentation/Layout/MainLayout.razor b/WardrobeManager.Presentation/Layout/MainLayout.razor index 53981ee..f50a765 100644 --- a/WardrobeManager.Presentation/Layout/MainLayout.razor +++ b/WardrobeManager.Presentation/Layout/MainLayout.razor @@ -4,12 +4,12 @@
-
- @Body +
+ @Body
-
+
diff --git a/WardrobeManager.Presentation/Layout/NavMenu.razor b/WardrobeManager.Presentation/Layout/NavMenu.razor index 6593a97..20bfebc 100644 --- a/WardrobeManager.Presentation/Layout/NavMenu.razor +++ b/WardrobeManager.Presentation/Layout/NavMenu.razor @@ -1,4 +1,9 @@ -