diff --git a/Project.App/Project.Api/Migrations/20251024170113_ImplementBlackjack.Designer.cs b/Project.App/Project.Api/Migrations/20251024170113_ImplementBlackjack.Designer.cs
new file mode 100644
index 0000000..31bc64a
--- /dev/null
+++ b/Project.App/Project.Api/Migrations/20251024170113_ImplementBlackjack.Designer.cs
@@ -0,0 +1,247 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Project.Api.Data;
+
+#nullable disable
+
+namespace Project.Api.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20251024170113_ImplementBlackjack")]
+ partial class ImplementBlackjack
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Project.Api.Models.Hand", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Bet")
+ .HasColumnType("bigint");
+
+ b.Property("HandNumber")
+ .HasColumnType("int");
+
+ b.Property("Order")
+ .HasColumnType("int");
+
+ b.Property("RoomPlayerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoomPlayerId");
+
+ b.ToTable("Hands");
+ });
+
+ modelBuilder.Entity("Project.Api.Models.Room", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("DeckId")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Description")
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("EndedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("GameConfig")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("GameMode")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("GameState")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("HostId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("IsActive")
+ .HasColumnType("bit");
+
+ b.Property("IsPublic")
+ .HasColumnType("bit");
+
+ b.Property("MaxPlayers")
+ .HasColumnType("int");
+
+ b.Property("MinPlayers")
+ .HasColumnType("int");
+
+ b.Property("Round")
+ .HasColumnType("int");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("rowversion");
+
+ b.Property("StartedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("State")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("HostId");
+
+ b.ToTable("Rooms");
+ });
+
+ modelBuilder.Entity("Project.Api.Models.RoomPlayer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Balance")
+ .HasColumnType("bigint");
+
+ b.Property("BalanceDelta")
+ .HasColumnType("bigint");
+
+ b.Property("Role")
+ .HasColumnType("int");
+
+ b.Property("RoomId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("RoomId", "UserId")
+ .IsUnique()
+ .HasDatabaseName("IX_RoomPlayer_RoomId_UserId_Unique");
+
+ b.ToTable("RoomPlayers");
+ });
+
+ modelBuilder.Entity("Project.Api.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AvatarUrl")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("Balance")
+ .HasColumnType("float");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Email")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Project.Api.Models.Hand", b =>
+ {
+ b.HasOne("Project.Api.Models.RoomPlayer", "RoomPlayer")
+ .WithMany("Hands")
+ .HasForeignKey("RoomPlayerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("RoomPlayer");
+ });
+
+ modelBuilder.Entity("Project.Api.Models.Room", b =>
+ {
+ b.HasOne("Project.Api.Models.User", "Host")
+ .WithMany()
+ .HasForeignKey("HostId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Host");
+ });
+
+ modelBuilder.Entity("Project.Api.Models.RoomPlayer", b =>
+ {
+ b.HasOne("Project.Api.Models.Room", "Room")
+ .WithMany("RoomPlayers")
+ .HasForeignKey("RoomId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Project.Api.Models.User", "User")
+ .WithMany("RoomPlayers")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Room");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Project.Api.Models.Room", b =>
+ {
+ b.Navigation("RoomPlayers");
+ });
+
+ modelBuilder.Entity("Project.Api.Models.RoomPlayer", b =>
+ {
+ b.Navigation("Hands");
+ });
+
+ modelBuilder.Entity("Project.Api.Models.User", b =>
+ {
+ b.Navigation("RoomPlayers");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Project.App/Project.Api/Migrations/20251024170113_ImplementBlackjack.cs b/Project.App/Project.Api/Migrations/20251024170113_ImplementBlackjack.cs
new file mode 100644
index 0000000..4627c7e
--- /dev/null
+++ b/Project.App/Project.Api/Migrations/20251024170113_ImplementBlackjack.cs
@@ -0,0 +1,145 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Project.Api.Migrations
+{
+ ///
+ public partial class ImplementBlackjack : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_RoomPlayers_RoomId",
+ table: "RoomPlayers");
+
+ migrationBuilder.AlterColumn(
+ name: "StartedAt",
+ table: "Rooms",
+ type: "datetimeoffset",
+ nullable: true,
+ oldClrType: typeof(DateTime),
+ oldType: "datetime2",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "EndedAt",
+ table: "Rooms",
+ type: "datetimeoffset",
+ nullable: true,
+ oldClrType: typeof(DateTime),
+ oldType: "datetime2",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "DeckId",
+ table: "Rooms",
+ type: "nvarchar(max)",
+ nullable: true,
+ oldClrType: typeof(int),
+ oldType: "int");
+
+ migrationBuilder.AlterColumn(
+ name: "CreatedAt",
+ table: "Rooms",
+ type: "datetimeoffset",
+ nullable: false,
+ oldClrType: typeof(DateTime),
+ oldType: "datetime2");
+
+ migrationBuilder.AlterColumn(
+ name: "Bet",
+ table: "Hands",
+ type: "bigint",
+ nullable: false,
+ oldClrType: typeof(int),
+ oldType: "int");
+
+ migrationBuilder.AddColumn(
+ name: "HandNumber",
+ table: "Hands",
+ type: "int",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Users_Email",
+ table: "Users",
+ column: "Email",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_RoomPlayer_RoomId_UserId_Unique",
+ table: "RoomPlayers",
+ columns: new[] { "RoomId", "UserId" },
+ unique: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropIndex(
+ name: "IX_Users_Email",
+ table: "Users");
+
+ migrationBuilder.DropIndex(
+ name: "IX_RoomPlayer_RoomId_UserId_Unique",
+ table: "RoomPlayers");
+
+ migrationBuilder.DropColumn(
+ name: "HandNumber",
+ table: "Hands");
+
+ migrationBuilder.AlterColumn(
+ name: "StartedAt",
+ table: "Rooms",
+ type: "datetime2",
+ nullable: true,
+ oldClrType: typeof(DateTimeOffset),
+ oldType: "datetimeoffset",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "EndedAt",
+ table: "Rooms",
+ type: "datetime2",
+ nullable: true,
+ oldClrType: typeof(DateTimeOffset),
+ oldType: "datetimeoffset",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "DeckId",
+ table: "Rooms",
+ type: "int",
+ nullable: false,
+ defaultValue: 0,
+ oldClrType: typeof(string),
+ oldType: "nvarchar(max)",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "CreatedAt",
+ table: "Rooms",
+ type: "datetime2",
+ nullable: false,
+ oldClrType: typeof(DateTimeOffset),
+ oldType: "datetimeoffset");
+
+ migrationBuilder.AlterColumn(
+ name: "Bet",
+ table: "Hands",
+ type: "int",
+ nullable: false,
+ oldClrType: typeof(long),
+ oldType: "bigint");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_RoomPlayers_RoomId",
+ table: "RoomPlayers",
+ column: "RoomId");
+ }
+ }
+}
diff --git a/Project.App/Project.Api/Migrations/AppDbContextModelSnapshot.cs b/Project.App/Project.Api/Migrations/AppDbContextModelSnapshot.cs
index 28a7d4a..29cafb4 100644
--- a/Project.App/Project.Api/Migrations/AppDbContextModelSnapshot.cs
+++ b/Project.App/Project.Api/Migrations/AppDbContextModelSnapshot.cs
@@ -28,7 +28,10 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
- b.Property("Bet")
+ b.Property("Bet")
+ .HasColumnType("bigint");
+
+ b.Property("HandNumber")
.HasColumnType("int");
b.Property("Order")
@@ -50,18 +53,18 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
- b.Property("CreatedAt")
- .HasColumnType("datetime2");
+ b.Property("CreatedAt")
+ .HasColumnType("datetimeoffset");
- b.Property("DeckId")
- .HasColumnType("int");
+ b.Property("DeckId")
+ .HasColumnType("nvarchar(max)");
b.Property("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
- b.Property("EndedAt")
- .HasColumnType("datetime2");
+ b.Property("EndedAt")
+ .HasColumnType("datetimeoffset");
b.Property("GameConfig")
.IsRequired()
@@ -100,8 +103,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("rowversion");
- b.Property("StartedAt")
- .HasColumnType("datetime2");
+ b.Property("StartedAt")
+ .HasColumnType("datetimeoffset");
b.Property("State")
.IsRequired()
@@ -140,10 +143,12 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasKey("Id");
- b.HasIndex("RoomId");
-
b.HasIndex("UserId");
+ b.HasIndex("RoomId", "UserId")
+ .IsUnique()
+ .HasDatabaseName("IX_RoomPlayer_RoomId_UserId_Unique");
+
b.ToTable("RoomPlayers");
});
@@ -172,6 +177,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.HasKey("Id");
+ b.HasIndex("Email")
+ .IsUnique();
+
b.ToTable("Users");
});
diff --git a/Project.App/Project.Api/Program.cs b/Project.App/Project.Api/Program.cs
index 647300f..619fcba 100644
--- a/Project.App/Project.Api/Program.cs
+++ b/Project.App/Project.Api/Program.cs
@@ -29,6 +29,9 @@ public static void Main(string[] args)
reloadOnChange: true
);
+ // env files should override adminsettings.json
+ builder.Configuration.AddEnvironmentVariables();
+
// configure Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
@@ -235,7 +238,7 @@ IWebHostEnvironment environment
>();
// fetch the full user JSON from Google's userinfo endpoint
- var request = new System.Net.Http.HttpRequestMessage(
+ var request = new HttpRequestMessage(
System.Net.Http.HttpMethod.Get,
ctx.Options.UserInformationEndpoint
);
diff --git a/Project.App/Project.Client/Dockerfile b/Project.App/Project.Client/Dockerfile
index b79be7f..093678d 100644
--- a/Project.App/Project.Client/Dockerfile
+++ b/Project.App/Project.Client/Dockerfile
@@ -13,34 +13,35 @@ COPY . .
# build
RUN npm run build
-FROM nginx:alpine AS run
+# --- run frontend from nginx server (static) ---
+# FROM nginx:alpine AS run
-# copy nginx config (if anything other than the default is required)
-# COPY nginx.conf /etc/nginx/conf.d/default.conf
+# # copy nginx config (if anything other than the default is required)
+# # COPY nginx.conf /etc/nginx/conf.d/default.conf
-# copy built files from build
-COPY --from=build /src/out /usr/share/nginx/html
+# # copy built files from build
+# COPY --from=build /src/out /usr/share/nginx/html
-# according to richard this does nothing :/
-EXPOSE 80
+# # according to richard this does nothing :/
+# EXPOSE 80
-# run (in the foreground)! hopefully.
-CMD ["nginx", "-g", "daemon off;"]
+# # run (in the foreground)! hopefully.
+# CMD ["nginx", "-g", "daemon off;"]
# --- run frontend from node server (if SSR is required) ---
-# FROM node:24-alpine AS run
+FROM node:24-alpine AS run
-# # set working directory
-# WORKDIR /app
+# set working directory
+WORKDIR /app
-# # copy built files from build
-# COPY --from=build /src/.next ./.next
-# # copy static files
-# COPY --from=build /src/public ./public
+# copy built files from build
+COPY --from=build /src/.next ./.next
+# copy static files
+COPY --from=build /src/public ./public
-# # install dependencies (without dev deps)
-# COPY package.json package-lock.json ./
-# RUN npm ci --omit=dev
+# install dependencies (without dev deps)
+COPY package.json package-lock.json ./
+RUN npm ci --omit=dev
-# # run! (on node server)
-# CMD ["npm", "run", "start"]
+# run! (on node server)
+CMD ["npm", "run", "start"]
diff --git a/compose.yml b/compose.yml
index 870b679..f656a70 100644
--- a/compose.yml
+++ b/compose.yml
@@ -4,20 +4,32 @@ services:
build:
context: ./Project.App/Project.Client
ports:
- - 8080:80
+ - 3000:3000
depends_on:
- backend
-
+
backend:
build:
context: ./Project.App/Project.Api
environment:
# binds to ConnectionStrings:DefaultConnection
- ConnectionStrings__DefaultConnection=Server=db;Database=DevitoDB;User Id=SA;Password=ThisIsTheBestPassword!!1!;TrustServerCertificate=True;
+ - ASPNETCORE_ENVIRONMENT=Development
+ - ASPNETCORE_URLS=https://+:7069;http://+:5131
+ - ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx
+ - ASPNETCORE_Kestrel__Certificates__Default__Password=devcertpass
ports:
- - 5131:5131
+ - 5131:5131 # http
+ - 7069:7069 # https
depends_on:
- db
+ volumes:
+ # dev certificate directory (assumes windows machine)
+ # to create the certificate:
+ # > `dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p devcertpass`
+ # > `dotnet dev-certs https --trust`
+ # see: https://learn.microsoft.com/en-us/aspnet/core/security/docker-https?view=aspnetcore-9.0
+ - ${USERPROFILE}/.aspnet/https:/https/
db:
image: mcr.microsoft.com/mssql/server:2022-latest
@@ -27,7 +39,7 @@ services:
- SA_PASSWORD=ThisIsTheBestPassword!!1!
- MSSQL_PID=Developer # set to developer cuz we're developing :)
- MSSQL_DATABASE=DevitoDB
- ports:
+ ports:
- 1433:1433
volumes:
- db_data:/var/opt/mssql