5 Commits

Author SHA1 Message Date
efd69f63b0 Protected routes
Some checks failed
Build Backend and Frontend / Build & Test .NET Backend (push) Has been cancelled
Build Backend and Frontend / Build Frontend (push) Has been cancelled
2025-06-03 11:03:55 +02:00
2e93f3d04e Basic router 2025-06-03 09:37:03 +02:00
0ef2c73c71 Save before testing router 2025-06-02 18:22:34 +02:00
883252c049 Remove test db 2025-06-02 18:21:54 +02:00
9e7ec186cd Test Entity Framework 2025-05-27 14:17:47 +02:00
19 changed files with 1180 additions and 85 deletions

View File

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace USEntryCoach.Server.Data;
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : IdentityDbContext<IdentityUser>(options)
{
}

View File

@ -0,0 +1,277 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using USEntryCoach.Server.Data;
#nullable disable
namespace USEntryCoach.Server.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250527123305_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,223 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace USEntryCoach.Server.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: true),
SecurityStamp = table.Column<string>(type: "text", nullable: true),
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
PhoneNumber = table.Column<string>(type: "text", nullable: true),
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetRoleClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RoleId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserClaims",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<string>(type: "text", nullable: false),
ClaimType = table.Column<string>(type: "text", nullable: true),
ClaimValue = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
table.ForeignKey(
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserLogins",
columns: table => new
{
LoginProvider = table.Column<string>(type: "text", nullable: false),
ProviderKey = table.Column<string>(type: "text", nullable: false),
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
table.ForeignKey(
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<string>(type: "text", nullable: false),
RoleId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AspNetUserTokens",
columns: table => new
{
UserId = table.Column<string>(type: "text", nullable: false),
LoginProvider = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Value = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
table.ForeignKey(
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AspNetRoleClaims_RoleId",
table: "AspNetRoleClaims",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "RoleNameIndex",
table: "AspNetRoles",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_AspNetUserClaims_UserId",
table: "AspNetUserClaims",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserLogins_UserId",
table: "AspNetUserLogins",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_AspNetUserRoles_RoleId",
table: "AspNetUserRoles",
column: "RoleId");
migrationBuilder.CreateIndex(
name: "EmailIndex",
table: "AspNetUsers",
column: "NormalizedEmail");
migrationBuilder.CreateIndex(
name: "UserNameIndex",
table: "AspNetUsers",
column: "NormalizedUserName",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AspNetRoleClaims");
migrationBuilder.DropTable(
name: "AspNetUserClaims");
migrationBuilder.DropTable(
name: "AspNetUserLogins");
migrationBuilder.DropTable(
name: "AspNetUserRoles");
migrationBuilder.DropTable(
name: "AspNetUserTokens");
migrationBuilder.DropTable(
name: "AspNetRoles");
migrationBuilder.DropTable(
name: "AspNetUsers");
}
}
}

View File

@ -0,0 +1,274 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using USEntryCoach.Server.Data;
#nullable disable
namespace USEntryCoach.Server.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,8 +1,11 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using System.Text;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using USEntryCoach.Server.Data;
using USEntryCoach.Server.Services;
@ -20,28 +23,28 @@ builder.Services.AddOptionsWithValidateOnStart<AuthenticationSettings>().Bind(au
ApiSettings? apiSettings = apiSettingsSection.Get<ApiSettings>();
AuthenticationSettings? authSettings = authSettingsSection.Get<AuthenticationSettings>();
builder.Services.AddSingleton<TokenService>();
// Configure JWT token generation.
builder.Services.AddAuthentication(config =>
{
config.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
config.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(config =>
{
// TODO: Only for debug!
config.RequireHttpsMetadata = false;
config.SaveToken = true;
config.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(authSettings!.JwtGenerationSecretBytes),
ValidateIssuer = false,
ValidateAudience = false
};
});
//
// builder.Services.AddSingleton<TokenService>();
//
// // Configure JWT token generation.
// builder.Services.AddAuthentication(config =>
// {
// config.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
// config.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
// }).AddJwtBearer(config =>
// {
// // TODO: Only for debug!
// config.RequireHttpsMetadata = false;
//
// config.SaveToken = true;
// config.TokenValidationParameters = new TokenValidationParameters()
// {
// ValidateIssuerSigningKey = true,
// IssuerSigningKey = new SymmetricSecurityKey(authSettings!.JwtGenerationSecretBytes),
// ValidateIssuer = false,
// ValidateAudience = false
// };
// });
builder.Services.AddAuthorization(options =>
{
@ -53,14 +56,32 @@ builder.Services.AddAuthorization(options =>
});
});
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddAuthorization();
builder.Services.AddIdentityApiEndpoints<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
//
// builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
// .AddEntityFrameworkStores<ApplicationDbContext>();
//
// builder.Services.AddDbContext<BloggingContext>(options =>
// options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapIdentityApi<IdentityUser>();
//
// app.UseAuthentication();
// app.UseAuthorization();
app.UseDefaultFiles();
app.MapStaticAssets();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
@ -74,45 +95,45 @@ HttpClient client = new();
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiSettings.OpenAiToken}");
app.MapPost("/login", Login);
//app.MapPost("/login", Login);
app.MapGet("/hello", () => "Hello World!").RequireAuthorization("admin_greetings");
async Task<IResult> Login(LoginCredentials credentials, TokenService tokenService, HttpContext context)
{
// TODO: Check with database
User? user = null;
if (credentials is {Username:"developer", Password:"dev"})
{
user = new User()
{
Username = credentials.Username,
Password = credentials.Password,
Id = Guid.CreateVersion7(),
Role = UserRole.Developer
};
}
else if (credentials is {Username:"user", Password:"us"})
{
user = new User()
{
Username = credentials.Username,
Password = credentials.Password,
Id = Guid.CreateVersion7(),
Role = UserRole.User
};
}
if (user == null)
{
return Results.Unauthorized();
}
var token = tokenService.GenerateToken(user);
return Results.Ok(new { token });
}
//
// async Task<IResult> Login(LoginCredentials credentials, TokenService tokenService, HttpContext context)
// {
// // TODO: Check with database
// User? user = null;
//
// if (credentials is {Username:"developer", Password:"dev"})
// {
// user = new User()
// {
// Username = credentials.Username,
// Password = credentials.Password,
// Id = Guid.CreateVersion7(),
// Role = UserRole.Developer
// };
// }
// else if (credentials is {Username:"user", Password:"us"})
// {
// user = new User()
// {
// Username = credentials.Username,
// Password = credentials.Password,
// Id = Guid.CreateVersion7(),
// Role = UserRole.User
// };
// }
//
// if (user == null)
// {
// return Results.Unauthorized();
// }
//
// var token = tokenService.GenerateToken(user);
//
// return Results.Ok(new { token });
// }
app.MapGet("/developer", (ClaimsPrincipal user) =>
{

View File

@ -14,11 +14,17 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
<Version>9.*-*</Version>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
</ItemGroup>
<ItemGroup>
@ -27,4 +33,8 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project>

View File

@ -10,5 +10,8 @@
},
"Authentication": {
"JwtGenerationSecret": "Please provide a GUID (without dashes) as secret."
},
"ConnectionStrings": {
"Default": "User ID=us-entry-agent-dev;Password=example;Host=localhost;Port=5432;Database=us-entry-agent-dev;Pooling=true;MinPoolSize=0;MaxPoolSize=100;Connection Lifetime=0;"
}
}

View File

@ -12,5 +12,8 @@
"Authentication": {
"JwtGenerationSecret": "Please provide a GUID (without dashes) as secret.",
"JwtExpiryTime": "00:15:00"
},
"ConnectionStrings": {
"Default": "[NO_CONNECTION_STRING]"
}
}

View File

@ -8,8 +8,10 @@
"name": "usentrycoach.client",
"version": "0.0.0",
"dependencies": {
"classnames": "^2.5.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1",
"zod": "^3.25.23"
},
"devDependencies": {
@ -1781,6 +1783,11 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -1811,6 +1818,14 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2757,6 +2772,42 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.1.tgz",
"integrity": "sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ==",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.1.tgz",
"integrity": "sha512-vxU7ei//UfPYQ3iZvHuO1D/5fX3/JOqhNTbRR+WjSBWxf9bIvpWK+ftjmdfJHzPOuMQKe2fiEdG+dZX6E8uUpA==",
"dependencies": {
"react-router": "7.6.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -2855,6 +2906,11 @@
"node": ">=10"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"classnames": "^2.5.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1",
"zod": "^3.25.23"
},
"devDependencies": {

View File

@ -2,15 +2,36 @@ import './App.css';
import {ChatControl} from "./ChatClient/ChatControl.tsx";
import Login from './Components/Login.tsx';
import useLoginToken from "./Hooks/useLoginToken.tsx";
import Home from "./Components/Home.tsx";
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import ProtectedRoute from "./Components/ProtectedRoute.tsx";
export default function App()
{
const {token, setToken} = useLoginToken();
if (!token)
{
return <Login setToken={setToken} />
}
const Navigation = () => (
<nav>
<Link to="/">Home</Link>
<Link to="/login">Login</Link>
<Link to="/chat">Chat</Link>
</nav>
);
return <ChatControl/>
return (
<>
{/*<Navigation/>*/}
<BrowserRouter>
<Routes>
<Route index path="/" element={ <Home /> } />
<Route path="login" element={ <Login setToken={setToken} /> } />
<Route element={<ProtectedRoute user={token} setUser={setToken}/>}>
<Route path="chat" element={ <ChatControl /> } />
</Route>
<Route path="*" element={ <p>Deine Mamma ist so dick, sie hat diese Seite gefressen. </p> } />
</Routes>
</BrowserRouter>
</>
);
}

View File

@ -0,0 +1,19 @@
import { Link } from 'react-router-dom';
export default function Home()
{
return (
<>
<h1>Welcome to the US Entry Board dings bums Agent!</h1>
<Link to="/login">
Login
</Link>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
<p>
Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
</p>
</>
);
}

View File

@ -0,0 +1,93 @@
/*#login-container*/
/*{*/
/* top: 0;*/
/* left: 0;*/
/* position: absolute;*/
/* width: 100vw;*/
/* height: 100vh;*/
/* backdrop-filter: blur(5px);*/
/*}*/
form
{
/*position: absolute;*/
/*left: 50vw;*/
/*top: 50vh;*/
/*transform: translate(-50%, -50%);*/
/*width: 120%;*/
height: auto;
display: flex;
gap: 1rem;
flex-direction: column;
align-items: center;
padding: 0 25px 20px 25px;
border-radius: 10px;
border-color: #282828;
border-style: solid;
border-width: 2px;
background: #323232;
filter: drop-shadow(0px 0px 8px rgb(146, 97, 251));
}
.input-field
{
width: 100%;
}
.register-row
{
display: flex;
white-space-collapse: preserve;
}
.register-row a
{
cursor: pointer;
}
form input
{
padding: 0.4rem;
width: 100%;
box-sizing: border-box;
}
label
{
width: 100%;
}
label p
{
justify-self: start;
display: block;
margin: 0;
}
#button-login
{
width: 100%;
padding: 0.5em;
}
#login-error
{
background: #bf830e;
border-radius: 5px;
width: 0; /* Collapses element so it doesn't affect parent width */
min-width: 100%; /* Expands element to parent width */
}
#login-error p {
word-wrap: break-word;
overflow-wrap: break-word;
}
.button-back {
position: absolute;
top: 1.5em;
left: 1.5em;
width: 2em;
height: 2em;
padding: 0;
}

View File

@ -1,33 +1,64 @@
import { useState, type FormEvent } from 'react';
import { z } from 'zod/v4';
import './Login.css';
import { useNavigate } from "react-router";
export default function Login({ setToken } : {setToken: (token: string) => void})
{
const [username, setUsername] = useState<string>("");
const navigate = useNavigate();
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [loginError, setLoginError] = useState<string | null>(null);
const [waiting, setWaiting] = useState(false);
async function doLogin(event: FormEvent<HTMLFormElement>)
{
setWaiting(true);
event.preventDefault();
const token = await loginUser({username, password});
try
{
const token = await loginUser({email, password});
setToken(token);
}
catch (e)
{
console.error(e);
setLoginError("Ein fehler ist aufgetreten.");
}
setWaiting(false);
}
return (
<div>
<form className="login-form" onSubmit={e => void doLogin(e)} method="action">
<div id="login-container">
<form className="login-form" onSubmit={e => void doLogin(e)} method="action" inert={waiting}>
<button className="button-back" onClick={() => void navigate(-1)}></button>
<h2>
Login
</h2>
{
loginError ? (
<div id="login-error">
<p>{loginError}</p>
</div>) : <></>
}
<label>
Email:
<input type="text" name="username" placeholder="Username" required onChange={event => setUsername(event.target.value)} />
<p>Email:</p>
<input type="email" className="form-field" name="email" autoComplete="email" /*required*/
onChange={event => setEmail(event.target.value)} />
</label>
<label>
Password:
<input type="password" name="password" placeholder="Password" required onChange={event => setPassword(event.target.value)} />
<p>Password:</p>
<input type="password" name="password" autoComplete="current-password"
/*required*/ onChange={event => setPassword(event.target.value)} />
</label>
<button type="submit">Login</button>
<button className="button-submit" type="submit" id="button-login">{waiting ? "Please wait..." : "Login"}</button>
<div className="register-row">
New? <a>Register here!</a>
</div>
</form>
</div>
);
@ -37,10 +68,39 @@ const LoginResponse = z.object({
token: z.string()
});
async function loginUser(credentials : {username: string, password: string}): Promise<string>
async function loginUser(credentials : {email: string, password: string}): Promise<string>
{
const queryParams = new URLSearchParams({
useCookies: "true"
});
const url = `login?${queryParams.toString()}`;
// Get a session token for OpenAI Realtime API
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});
if (!response.ok)
{
throw new Error(response.statusText);
// 500: internal server error
}
//const responseJson:unknown = await response.json();
//const parsedToken = LoginResponse.parse(responseJson);
return "abc"; //parsedToken.token;
}
async function registerUser(credentials : {email: string, password: string}): Promise<string>
{
// Get a session token for OpenAI Realtime API
const response = await fetch('login', {
const response = await fetch('register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@ -0,0 +1,12 @@
import { Outlet } from "react-router-dom";
import Login from "./Login.tsx";
export default function ProtectedRoute({user, setUser} : {user: string | null, setUser: (user: string | null) => void})
{
if (user === null)
{
return <Login setToken={setUser}/>;
}
return <Outlet />;
}

View File

@ -15,7 +15,7 @@ export default function useLoginToken()
const [token, setToken] = useState<string | null>(getToken());
const saveToken = (token: string): void =>
const saveToken = (token: string | null): void =>
{
sessionStorage.setItem('token', JSON.stringify(token));
setToken(token);

View File

@ -43,12 +43,14 @@ button {
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
button:hover:not([disabled]) {
border-color: #646cff;
cursor: pointer;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
@ -66,3 +68,12 @@ button:focus-visible {
background-color: #f9f9f9;
}
}
.button-submit {
background-color: #208ddb;
}
.button-submit:hover:not([disabled]) {
background-color: #3c9be0;
border-color: #c7e3f6;
}

View File

@ -17,8 +17,8 @@
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
//"noUnusedLocals": true,
//"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true

View File

@ -44,7 +44,7 @@ const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_H
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7085';
// Define a list of all existing backend routes.
const backendRoutes = ['/login', '/ephemeral_token'];
const backendRoutes = ['/login', '/register', '/ephemeral_token'];
// For development, we have a node.js server running that delivers our frontend.
// We have to configure proxies for the backend calls, so that node.js will forward them to the backend.