Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5727f0f51 | ||
|
|
efd69f63b0 | ||
|
|
2e93f3d04e | ||
|
|
0ef2c73c71 | ||
|
|
883252c049 | ||
|
|
9e7ec186cd | ||
|
|
dffd31cd0f | ||
|
|
f4737f4dcf | ||
|
|
1d68f177de | ||
|
|
ae5096bacd | ||
|
|
a2608eac5b | ||
|
|
189069361f | ||
|
|
17817ea0a9 | ||
|
|
a0c232e052 | ||
|
|
3e7b04df1c | ||
|
|
bd7759cbcb | ||
|
|
af0c49db77 | ||
|
|
a0c994bc44 | ||
|
|
9758cba025 | ||
|
|
fcd81969a8 | ||
|
|
0d4046d56c | ||
|
|
94d17a1266 | ||
|
|
326f3531b1 | ||
|
|
6eb6c09a4d | ||
|
|
1339a22ab8 | ||
|
|
465c4ca0c1 |
@@ -0,0 +1,27 @@
|
|||||||
|
name: Build Backend
|
||||||
|
run-name: ${{ gitea.actor }} is testing out building the Backend
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend:
|
||||||
|
name: Build & Test .NET Backend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET SDK
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Build the project
|
||||||
|
run: dotnet build USEntryCoach.sln --no-restore --configuration Release
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: dotnet test USEntryCoach.sln --no-build --verbosity normal
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
name: Gitea Actions Demo
|
|
||||||
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Explore-Gitea-Actions:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
|
|
||||||
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
|
|
||||||
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
|
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
|
||||||
- name: List files in the repository
|
|
||||||
run: |
|
|
||||||
ls ${{ gitea.workspace }}
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Build Frontend
|
||||||
|
run-name: ${{ gitea.actor }} is testing out building the Frontend
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
frontend:
|
||||||
|
name: Build Frontend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: ./usentrycoach.client
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: NODE_ENV=production npm run build
|
||||||
|
working-directory: ./usentrycoach.client
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: Build Backend and Frontend
|
||||||
|
run-name: ${{ gitea.actor }} is testing out Building Website
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend:
|
||||||
|
name: Build & Test .NET Backend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET SDK
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Build the project
|
||||||
|
run: dotnet build USEntryCoach.sln --no-restore --configuration Release
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: dotnet test USEntryCoach.sln --no-build --verbosity normal
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
name: Build Frontend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: backend
|
||||||
|
steps:
|
||||||
|
- name: Clone the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: ./usentrycoach.client
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: npm run build
|
||||||
|
working-directory: ./usentrycoach.client
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Run Backend and Frontend" type="com.intellij.execution.configurations.multilaunch" factoryName="MultiLaunchConfiguration">
|
||||||
|
<rows>
|
||||||
|
<ExecutableRowSnapshot>
|
||||||
|
<option name="condition">
|
||||||
|
<ConditionSnapshot>
|
||||||
|
<option name="type" value="immediately" />
|
||||||
|
</ConditionSnapshot>
|
||||||
|
</option>
|
||||||
|
<option name="executable">
|
||||||
|
<ExecutableSnapshot>
|
||||||
|
<option name="id" value="buildSolution:0942b9" />
|
||||||
|
</ExecutableSnapshot>
|
||||||
|
</option>
|
||||||
|
</ExecutableRowSnapshot>
|
||||||
|
<ExecutableRowSnapshot>
|
||||||
|
<option name="condition">
|
||||||
|
<ConditionSnapshot>
|
||||||
|
<option name="type" value="afterPreviousFinished" />
|
||||||
|
</ConditionSnapshot>
|
||||||
|
</option>
|
||||||
|
<option name="executable">
|
||||||
|
<ExecutableSnapshot>
|
||||||
|
<option name="id" value="runConfig:.NET Launch Settings Profile.USEntryCoach.Server: https" />
|
||||||
|
</ExecutableSnapshot>
|
||||||
|
</option>
|
||||||
|
</ExecutableRowSnapshot>
|
||||||
|
<ExecutableRowSnapshot>
|
||||||
|
<option name="condition">
|
||||||
|
<ConditionSnapshot>
|
||||||
|
<attributes>
|
||||||
|
<entry key="port" value="7085" />
|
||||||
|
</attributes>
|
||||||
|
<option name="type" value="waitPortOpened" />
|
||||||
|
</ConditionSnapshot>
|
||||||
|
</option>
|
||||||
|
<option name="executable">
|
||||||
|
<ExecutableSnapshot>
|
||||||
|
<option name="id" value="runConfig:npm.dev" />
|
||||||
|
</ExecutableSnapshot>
|
||||||
|
</option>
|
||||||
|
</ExecutableRowSnapshot>
|
||||||
|
</rows>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -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)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace USEntryCoach.Server.Data;
|
||||||
|
|
||||||
|
class LoginCredentials
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace USEntryCoach.Server.Data;
|
||||||
|
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public UserRole Role { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace USEntryCoach.Server.Data;
|
||||||
|
|
||||||
|
public enum UserRole
|
||||||
|
{
|
||||||
|
Developer,
|
||||||
|
User
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+149
-11
@@ -1,16 +1,87 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json.Nodes;
|
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;
|
||||||
|
using USEntryCoach.Server.Settings;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
var apiSettingsSection = builder.Configuration.GetSection(ApiSettings.SectionName);
|
||||||
|
var authSettingsSection = builder.Configuration.GetSection(AuthenticationSettings.SectionName);
|
||||||
|
|
||||||
|
builder.Services.AddOptionsWithValidateOnStart<ApiSettings>().Bind(apiSettingsSection).ValidateDataAnnotations();
|
||||||
|
builder.Services.AddOptionsWithValidateOnStart<AuthenticationSettings>().Bind(authSettingsSection).ValidateDataAnnotations();
|
||||||
|
|
||||||
|
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.AddAuthorization(options =>
|
||||||
|
// {
|
||||||
|
// options.AddPolicy(nameof(UserRole.Developer), policy => policy.RequireRole(nameof(UserRole.Developer)));
|
||||||
|
// options.AddPolicy(nameof(UserRole.User), policy =>
|
||||||
|
// {
|
||||||
|
// // Also allow Developers to do anything a user can do.
|
||||||
|
// policy.RequireRole(nameof(UserRole.User), nameof(UserRole.Developer));
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
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();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.MapIdentityApi<IdentityUser>();
|
||||||
|
//
|
||||||
|
// app.UseAuthentication();
|
||||||
|
// app.UseAuthorization();
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
|
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
@@ -20,16 +91,85 @@ if (app.Environment.IsDevelopment())
|
|||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
HttpClient client = new();
|
HttpClient client = new();
|
||||||
string? apiKey = app.Configuration.GetValue<string>("API:OpenAI");
|
//string? apiKey = app.Configuration.GetValue<string>("API:OpenAI");
|
||||||
client.DefaultRequestHeaders.Clear();
|
client.DefaultRequestHeaders.Clear();
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
|
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiSettings.OpenAiToken}");
|
||||||
|
|
||||||
|
//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 });
|
||||||
|
// }
|
||||||
|
|
||||||
|
app.MapGet("/developer", (ClaimsPrincipal user) =>
|
||||||
|
{
|
||||||
|
Results.Ok(new { message = $"Authenticated as { user?.Identity?.Name }" });
|
||||||
|
}).RequireAuthorization(nameof(UserRole.Developer));
|
||||||
|
|
||||||
|
app.MapGet("/user", (ClaimsPrincipal user) =>
|
||||||
|
{
|
||||||
|
Results.Ok(new { message = $"Authenticated as { user?.Identity?.Name }" });
|
||||||
|
}).RequireAuthorization(nameof(UserRole.User));
|
||||||
|
|
||||||
|
app.MapGet("/auth/validate", async (HttpContext context, UserManager<IdentityUser> userManager) =>
|
||||||
|
{
|
||||||
|
if (!context.User.Identity?.IsAuthenticated ?? true)
|
||||||
|
{
|
||||||
|
return Results.Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
IdentityUser? user = await userManager.GetUserAsync(context.User);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
return Results.InternalServerError("User not found?!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok(new
|
||||||
|
{
|
||||||
|
user.Id,
|
||||||
|
user.Email
|
||||||
|
});
|
||||||
|
}).RequireAuthorization();
|
||||||
|
|
||||||
app.MapGet("/ephemeral_token", async () =>
|
app.MapGet("/ephemeral_token", async () =>
|
||||||
{
|
{
|
||||||
if (apiKey == null)
|
//if (apiKey == null)
|
||||||
throw new Exception("API key not set");
|
// throw new Exception("API key not set");
|
||||||
|
|
||||||
#pragma warning disable OPENAI002
|
|
||||||
|
|
||||||
var options = new
|
var options = new
|
||||||
{
|
{
|
||||||
@@ -44,8 +184,6 @@ app.MapGet("/ephemeral_token", async () =>
|
|||||||
//}//ConversationTurnDetectionOptions.CreateServerVoiceActivityTurnDetectionOptions(0.5f, TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(500), true)
|
//}//ConversationTurnDetectionOptions.CreateServerVoiceActivityTurnDetectionOptions(0.5f, TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(500), true)
|
||||||
};
|
};
|
||||||
|
|
||||||
#pragma warning restore OPENAI002
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
JsonContent content = JsonContent.Create(options);
|
JsonContent content = JsonContent.Create(options);
|
||||||
@@ -76,7 +214,7 @@ app.MapGet("/ephemeral_token", async () =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}).WithName("GetEphemeralToken");
|
}).WithName("GetEphemeralToken").RequireAuthorization(nameof(UserRole.User));
|
||||||
|
|
||||||
app.MapFallbackToFile("/index.html");
|
app.MapFallbackToFile("/index.html");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using USEntryCoach.Server.Data;
|
||||||
|
using USEntryCoach.Server.Settings;
|
||||||
|
|
||||||
|
namespace USEntryCoach.Server.Services;
|
||||||
|
|
||||||
|
public class TokenService(IOptions<AuthenticationSettings> authenticationSettings)
|
||||||
|
{
|
||||||
|
public string GenerateToken(User user)
|
||||||
|
{
|
||||||
|
JwtSecurityTokenHandler tokenHandler = new();
|
||||||
|
|
||||||
|
SecurityTokenDescriptor tokenDescriptor = new()
|
||||||
|
{
|
||||||
|
Subject = new ClaimsIdentity([
|
||||||
|
new Claim(ClaimTypes.Name, user.Username),
|
||||||
|
new Claim(ClaimTypes.Role, user.Role.ToString())
|
||||||
|
]),
|
||||||
|
Expires = DateTime.UtcNow.Add(authenticationSettings.Value.JwtExpiryTime),
|
||||||
|
SigningCredentials = new SigningCredentials(
|
||||||
|
new SymmetricSecurityKey(authenticationSettings.Value.JwtGenerationSecretBytes),
|
||||||
|
SecurityAlgorithms.HmacSha256Signature)
|
||||||
|
};
|
||||||
|
|
||||||
|
SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
|
||||||
|
return tokenHandler.WriteToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace USEntryCoach.Server.Settings;
|
||||||
|
|
||||||
|
public sealed class ApiSettings
|
||||||
|
{
|
||||||
|
public const string SectionName = "API";
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "OpenAI API token is required!", AllowEmptyStrings = false)]
|
||||||
|
public required string OpenAiToken { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AuthenticationSettings
|
||||||
|
{
|
||||||
|
public const string SectionName = "Authentication";
|
||||||
|
|
||||||
|
private string _jwtGenerationSecret = null!;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "JWT generation secret token is required!", AllowEmptyStrings = false)]
|
||||||
|
public required string JwtGenerationSecret
|
||||||
|
{
|
||||||
|
get => _jwtGenerationSecret;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_jwtGenerationSecret = value;
|
||||||
|
JwtGenerationSecretBytes = Encoding.UTF8.GetBytes(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] JwtGenerationSecretBytes { get; private set; }
|
||||||
|
|
||||||
|
const string JwtExpiryTimeMin = "00:00:30";
|
||||||
|
const string JwtExpiryTimeMax = "24:00:00";
|
||||||
|
|
||||||
|
[Range(typeof(TimeSpan), JwtExpiryTimeMin, JwtExpiryTimeMax,
|
||||||
|
ErrorMessage = $"JWT expiry time must be in the range from {JwtExpiryTimeMin} to {JwtExpiryTimeMax}.")]
|
||||||
|
public TimeSpan JwtExpiryTime { get; set; } = TimeSpan.FromMinutes(5);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,11 +13,18 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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.OpenApi" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
|
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
|
||||||
<Version>9.*-*</Version>
|
<Version>9.*-*</Version>
|
||||||
</PackageReference>
|
</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="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -26,4 +33,8 @@
|
|||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Migrations\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,5 +4,14 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"OpenAiToken": "Please set the key in secrets.json! NEVER HERE!!!"
|
||||||
|
},
|
||||||
|
"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;"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"API": {
|
"API": {
|
||||||
"OpenAI": "[NO_API_KEY]"
|
"OpenAiToken": "[NO_API_KEY]"
|
||||||
|
},
|
||||||
|
"Authentication": {
|
||||||
|
"JwtGenerationSecret": "Please provide a GUID (without dashes) as secret.",
|
||||||
|
"JwtExpiryTime": "00:15:00"
|
||||||
|
},
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Default": "[NO_CONNECTION_STRING]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Use postgres/example user/password credentials
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
restart: always
|
||||||
|
# set shared memory limit when using docker compose
|
||||||
|
shm_size: 128mb
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: us-entry-agent-dev
|
||||||
|
POSTGRES_PASSWORD: example
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
@@ -3,26 +3,56 @@ import globals from 'globals'
|
|||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
|
import stylistic from '@stylistic/eslint-plugin'
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ['dist'] },
|
{ignores: ['dist']},
|
||||||
{
|
{
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
],
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
'react-hooks': reactHooks,
|
'react-hooks': reactHooks,
|
||||||
'react-refresh': reactRefresh,
|
'react-refresh': reactRefresh,
|
||||||
|
//'stylistic/js': stylisticJs
|
||||||
|
stylistic
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
|
//'brace-style': ['error', 'allman'],
|
||||||
'react-refresh/only-export-components': [
|
'react-refresh/only-export-components': [
|
||||||
'warn',
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{allowConstantExport: true},
|
||||||
],
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", {
|
||||||
|
"vars": "all",
|
||||||
|
"args": "all",
|
||||||
|
"caughtErrors": "all",
|
||||||
|
"ignoreRestSiblings": false,
|
||||||
|
"reportUsedIgnorePattern": false
|
||||||
|
}],
|
||||||
|
"prefer-const": ["warn", {
|
||||||
|
"destructuring": "any",
|
||||||
|
"ignoreReadBeforeAssign": false
|
||||||
|
}],
|
||||||
|
|
||||||
|
// Formatting:
|
||||||
|
"stylistic/brace-style": [
|
||||||
|
'error',
|
||||||
|
'allman',
|
||||||
|
{ allowSingleLine: false }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Generated
+323
-1086
File diff suppressed because it is too large
Load Diff
@@ -10,11 +10,16 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.6.1",
|
||||||
|
"zod": "^3.25.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@eslint/js": "^9.25.0",
|
||||||
|
"@stylistic/eslint-plugin": "^4.2.0",
|
||||||
|
"@types/node": "^20",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
@@ -24,7 +29,6 @@
|
|||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.30.1",
|
"typescript-eslint": "^8.30.1",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5"
|
||||||
"@types/node": "^20"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+36
-411
@@ -1,415 +1,40 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import {ChatControl} from "./ChatClient/ChatControl.tsx";
|
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";
|
||||||
|
import {CookiesProvider} from "react-cookie";
|
||||||
|
|
||||||
interface Forecast {
|
export default function App()
|
||||||
date: string;
|
{
|
||||||
temperatureC: number;
|
const {token, setToken} = useLoginToken();
|
||||||
temperatureF: number;
|
|
||||||
summary: string;
|
const Navigation = () => (
|
||||||
|
<nav>
|
||||||
|
<Link to="/">Home</Link>
|
||||||
|
<Link to="/login">Login</Link>
|
||||||
|
<Link to="/chat">Chat</Link>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/*<Navigation/>*/}
|
||||||
|
|
||||||
|
<CookiesProvider defaultSetOptions={{ path: '/' }}>
|
||||||
|
<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>
|
||||||
|
</CookiesProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OpenAiToken {
|
|
||||||
ephemeralToken: string;
|
|
||||||
expiresAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
|
|
||||||
return <ChatControl/>
|
|
||||||
|
|
||||||
// const [isSessionActive, setIsSessionActive] = useState(false);
|
|
||||||
// const [events, setEvents] = useState([]);
|
|
||||||
// const [dataChannel, setDataChannel] = useState<RTCDataChannel | null>(null);
|
|
||||||
// const peerConnection = useRef<RTCPeerConnection>(null);
|
|
||||||
// const audioElement = useRef<HTMLAudioElement>(null);
|
|
||||||
//
|
|
||||||
// async function startSession() {
|
|
||||||
// // Get a session token for OpenAI Realtime API
|
|
||||||
// const tokenResponse = await fetch("/ephemeral_token");
|
|
||||||
// const data: OpenAiToken = await tokenResponse.json();
|
|
||||||
// const EPHEMERAL_KEY = data.ephemeralToken;
|
|
||||||
//
|
|
||||||
// // Create a peer connection
|
|
||||||
// const pc = new RTCPeerConnection();
|
|
||||||
//
|
|
||||||
// // Set up to play remote audio from the model
|
|
||||||
// audioElement.current = document.createElement("audio");
|
|
||||||
// audioElement.current.autoplay = true;
|
|
||||||
// pc.ontrack = (e) => {
|
|
||||||
// if (audioElement?.current !== null) {
|
|
||||||
// audioElement.current.srcObject = e.streams[0]
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// // Add local audio track for microphone input in the browser
|
|
||||||
// const ms = await navigator.mediaDevices.getUserMedia({
|
|
||||||
// audio: true,
|
|
||||||
// });
|
|
||||||
// pc.addTrack(ms.getTracks()[0]);
|
|
||||||
//
|
|
||||||
// // Set up data channel for sending and receiving events
|
|
||||||
// const dc = pc.createDataChannel("oai-events");
|
|
||||||
// setDataChannel(dc);
|
|
||||||
//
|
|
||||||
// // Start the session using the Session Description Protocol (SDP)
|
|
||||||
// const offer = await pc.createOffer();
|
|
||||||
// await pc.setLocalDescription(offer);
|
|
||||||
//
|
|
||||||
// const baseUrl = "https://api.openai.com/v1/realtime";
|
|
||||||
// const model = "gpt-4o-realtime-preview-2024-12-17";
|
|
||||||
// const sdpResponse = await fetch(`${baseUrl}?model=${model}`, {
|
|
||||||
// method: "POST",
|
|
||||||
// body: offer.sdp,
|
|
||||||
// headers: {
|
|
||||||
// Authorization: `Bearer ${EPHEMERAL_KEY}`,
|
|
||||||
// "Content-Type": "application/sdp",
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// const answer: RTCSessionDescriptionInit = {
|
|
||||||
// type: "answer",
|
|
||||||
// sdp: await sdpResponse.text(),
|
|
||||||
// };
|
|
||||||
// await pc.setRemoteDescription(answer);
|
|
||||||
//
|
|
||||||
// peerConnection.current = pc;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Stop current session, clean up peer connection and data channel
|
|
||||||
// function stopSession() {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// dataChannel.close();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (peerConnection?.current !== null)
|
|
||||||
// {
|
|
||||||
// peerConnection.current.getSenders().forEach((sender) => {
|
|
||||||
// if (sender.track) {
|
|
||||||
// sender.track.stop();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// if (peerConnection.current) {
|
|
||||||
// peerConnection.current.close();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// setIsSessionActive(false);
|
|
||||||
// setDataChannel(null);
|
|
||||||
// peerConnection.current = null;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Send a message to the model
|
|
||||||
// function sendClientEvent(message) {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// const timestamp = new Date().toLocaleTimeString();
|
|
||||||
// message.event_id = message.event_id || crypto.randomUUID();
|
|
||||||
//
|
|
||||||
// // send event before setting timestamp since the backend peer doesn't expect this field
|
|
||||||
// dataChannel.send(JSON.stringify(message));
|
|
||||||
//
|
|
||||||
// // if guard just in case the timestamp exists by miracle
|
|
||||||
// if (!message.timestamp) {
|
|
||||||
// message.timestamp = timestamp;
|
|
||||||
// }
|
|
||||||
// setEvents((prev) => [message, ...prev]);
|
|
||||||
// } else {
|
|
||||||
// console.error(
|
|
||||||
// "Failed to send message - no data channel available",
|
|
||||||
// message,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Send a text message to the model
|
|
||||||
// function sendTextMessage(message) {
|
|
||||||
// const event = {
|
|
||||||
// type: "conversation.item.create",
|
|
||||||
// item: {
|
|
||||||
// type: "message",
|
|
||||||
// role: "user",
|
|
||||||
// content: [
|
|
||||||
// {
|
|
||||||
// type: "input_text",
|
|
||||||
// text: message,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// sendClientEvent(event);
|
|
||||||
// sendClientEvent({ type: "response.create" });
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Attach event listeners to the data channel when a new one is created
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// // Append new server events to the list
|
|
||||||
// dataChannel.addEventListener("message", (e) => {
|
|
||||||
// const event = JSON.parse(e.data);
|
|
||||||
// if (!event.timestamp) {
|
|
||||||
// event.timestamp = new Date().toLocaleTimeString();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// setEvents((prev) => [event, ...prev]);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// // Set session active when the data channel is opened
|
|
||||||
// dataChannel.addEventListener("open", () => {
|
|
||||||
// setIsSessionActive(true);
|
|
||||||
// setEvents([]);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }, [dataChannel]);
|
|
||||||
//
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// <nav className="absolute top-0 left-0 right-0 h-16 flex items-center">
|
|
||||||
// <div className="flex items-center gap-4 w-full m-4 pb-2 border-0 border-b border-solid border-gray-200">
|
|
||||||
// <img style={{ width: "24px" }} src={logo} />
|
|
||||||
// <h1>realtime console</h1>
|
|
||||||
// </div>
|
|
||||||
// </nav>
|
|
||||||
// <main className="absolute top-16 left-0 right-0 bottom-0">
|
|
||||||
// <section className="absolute top-0 left-0 right-[380px] bottom-0 flex">
|
|
||||||
// <section className="absolute top-0 left-0 right-0 bottom-32 px-4 overflow-y-auto">
|
|
||||||
// <EventLog events={events} />
|
|
||||||
// </section>
|
|
||||||
// <section className="absolute h-32 left-0 right-0 bottom-0 p-4">
|
|
||||||
// <SessionControls
|
|
||||||
// startSession={startSession}
|
|
||||||
// stopSession={stopSession}
|
|
||||||
// sendClientEvent={sendClientEvent}
|
|
||||||
// sendTextMessage={sendTextMessage}
|
|
||||||
// events={events}
|
|
||||||
// isSessionActive={isSessionActive}
|
|
||||||
// />
|
|
||||||
// </section>
|
|
||||||
// </section>
|
|
||||||
// <section className="absolute top-0 w-[380px] right-0 bottom-0 p-4 pt-0 overflow-y-auto">
|
|
||||||
// <ToolPanel
|
|
||||||
// sendClientEvent={sendClientEvent}
|
|
||||||
// sendTextMessage={sendTextMessage}
|
|
||||||
// events={events}
|
|
||||||
// isSessionActive={isSessionActive}
|
|
||||||
// />
|
|
||||||
// </section>
|
|
||||||
// </main>
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
|
|
||||||
// export default function App() {
|
|
||||||
// const [openAiToken, setOpenAiToken] = useState<OpenAiToken | null>();
|
|
||||||
//
|
|
||||||
// useEffect(() => {
|
|
||||||
// get_the_fucking_token_already();
|
|
||||||
// }, []);
|
|
||||||
//
|
|
||||||
// const contents = openAiToken === undefined
|
|
||||||
// ? <p>Connecting to Open AI...</p>
|
|
||||||
// : <div>
|
|
||||||
// {
|
|
||||||
// openAiToken === null
|
|
||||||
// ? <p>Failed to connect to OpenAI, please reload the page.</p>
|
|
||||||
// : <p>
|
|
||||||
// {openAiToken.ephemeralToken}<br/>
|
|
||||||
// {new Date(openAiToken.expiresAt * 1000).toLocaleString()}
|
|
||||||
// </p>
|
|
||||||
// }
|
|
||||||
// </div>;
|
|
||||||
// return (
|
|
||||||
// <div>
|
|
||||||
// <h1 id="tableLabel">Weather forecast</h1>
|
|
||||||
// <p>This component demonstrates fetching data from the server.</p>
|
|
||||||
// {contents}
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// async function get_the_fucking_token_already() {
|
|
||||||
// const response = await fetch('ephemeral_token');
|
|
||||||
// if (response.ok) {
|
|
||||||
// const data = await response.json();
|
|
||||||
// setOpenAiToken(data);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// function Chatter({ openAiToken }: { openAiToken: OpenAiToken }) {
|
|
||||||
// const [isSessionActive, setIsSessionActive] = useState(false);
|
|
||||||
// const [events, setEvents] = useState([]);
|
|
||||||
// const [dataChannel, setDataChannel] = useState(null);
|
|
||||||
// const peerConnection = useRef(null);
|
|
||||||
// const audioElement = useRef(null);
|
|
||||||
//
|
|
||||||
// async function startSession() {
|
|
||||||
// // Get a session token for OpenAI Realtime API
|
|
||||||
// const tokenResponse = await fetch("/token");
|
|
||||||
// const data = await tokenResponse.json();
|
|
||||||
// const EPHEMERAL_KEY = data.client_secret.value;
|
|
||||||
//
|
|
||||||
// // Create a peer connection
|
|
||||||
// const pc = new RTCPeerConnection();
|
|
||||||
//
|
|
||||||
// // Set up to play remote audio from the model
|
|
||||||
// audioElement.current = document.createElement("audio");
|
|
||||||
// audioElement.current.autoplay = true;
|
|
||||||
// pc.ontrack = (e) => (audioElement.current.srcObject = e.streams[0]);
|
|
||||||
//
|
|
||||||
// // Add local audio track for microphone input in the browser
|
|
||||||
// const ms = await navigator.mediaDevices.getUserMedia({
|
|
||||||
// audio: true,
|
|
||||||
// });
|
|
||||||
// pc.addTrack(ms.getTracks()[0]);
|
|
||||||
//
|
|
||||||
// // Set up data channel for sending and receiving events
|
|
||||||
// const dc = pc.createDataChannel("oai-events");
|
|
||||||
// setDataChannel(dc);
|
|
||||||
//
|
|
||||||
// // Start the session using the Session Description Protocol (SDP)
|
|
||||||
// const offer = await pc.createOffer();
|
|
||||||
// await pc.setLocalDescription(offer);
|
|
||||||
//
|
|
||||||
// const baseUrl = "https://api.openai.com/v1/realtime";
|
|
||||||
// const model = "gpt-4o-realtime-preview-2024-12-17";
|
|
||||||
// const sdpResponse = await fetch(`${baseUrl}?model=${model}`, {
|
|
||||||
// method: "POST",
|
|
||||||
// body: offer.sdp,
|
|
||||||
// headers: {
|
|
||||||
// Authorization: `Bearer ${EPHEMERAL_KEY}`,
|
|
||||||
// "Content-Type": "application/sdp",
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// const answer = {
|
|
||||||
// type: "answer",
|
|
||||||
// sdp: await sdpResponse.text(),
|
|
||||||
// };
|
|
||||||
// await pc.setRemoteDescription(answer);
|
|
||||||
//
|
|
||||||
// peerConnection.current = pc;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Stop current session, clean up peer connection and data channel
|
|
||||||
// function stopSession() {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// dataChannel.close();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// peerConnection.current.getSenders().forEach((sender) => {
|
|
||||||
// if (sender.track) {
|
|
||||||
// sender.track.stop();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// if (peerConnection.current) {
|
|
||||||
// peerConnection.current.close();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// setIsSessionActive(false);
|
|
||||||
// setDataChannel(null);
|
|
||||||
// peerConnection.current = null;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Send a message to the model
|
|
||||||
// function sendClientEvent(message) {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// const timestamp = new Date().toLocaleTimeString();
|
|
||||||
// message.event_id = message.event_id || crypto.randomUUID();
|
|
||||||
//
|
|
||||||
// // send event before setting timestamp since the backend peer doesn't expect this field
|
|
||||||
// dataChannel.send(JSON.stringify(message));
|
|
||||||
//
|
|
||||||
// // if guard just in case the timestamp exists by miracle
|
|
||||||
// if (!message.timestamp) {
|
|
||||||
// message.timestamp = timestamp;
|
|
||||||
// }
|
|
||||||
// setEvents((prev) => [message, ...prev]);
|
|
||||||
// } else {
|
|
||||||
// console.error(
|
|
||||||
// "Failed to send message - no data channel available",
|
|
||||||
// message,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Send a text message to the model
|
|
||||||
// function sendTextMessage(message) {
|
|
||||||
// const event = {
|
|
||||||
// type: "conversation.item.create",
|
|
||||||
// item: {
|
|
||||||
// type: "message",
|
|
||||||
// role: "user",
|
|
||||||
// content: [
|
|
||||||
// {
|
|
||||||
// type: "input_text",
|
|
||||||
// text: message,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// sendClientEvent(event);
|
|
||||||
// sendClientEvent({ type: "response.create" });
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Attach event listeners to the data channel when a new one is created
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (dataChannel) {
|
|
||||||
// // Append new server events to the list
|
|
||||||
// dataChannel.addEventListener("message", (e) => {
|
|
||||||
// const event = JSON.parse(e.data);
|
|
||||||
// if (!event.timestamp) {
|
|
||||||
// event.timestamp = new Date().toLocaleTimeString();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// setEvents((prev) => [event, ...prev]);
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// // Set session active when the data channel is opened
|
|
||||||
// dataChannel.addEventListener("open", () => {
|
|
||||||
// setIsSessionActive(true);
|
|
||||||
// setEvents([]);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }, [dataChannel]);
|
|
||||||
//
|
|
||||||
// return (
|
|
||||||
// <>
|
|
||||||
// <nav className="absolute top-0 left-0 right-0 h-16 flex items-center">
|
|
||||||
// <div className="flex items-center gap-4 w-full m-4 pb-2 border-0 border-b border-solid border-gray-200">
|
|
||||||
// <img style={{ width: "24px" }} src={logo} />
|
|
||||||
// <h1>realtime console</h1>
|
|
||||||
// </div>
|
|
||||||
// </nav>
|
|
||||||
// <main className="absolute top-16 left-0 right-0 bottom-0">
|
|
||||||
// <section className="absolute top-0 left-0 right-[380px] bottom-0 flex">
|
|
||||||
// <section className="absolute top-0 left-0 right-0 bottom-32 px-4 overflow-y-auto">
|
|
||||||
// <EventLog events={events} />
|
|
||||||
// </section>
|
|
||||||
// <section className="absolute h-32 left-0 right-0 bottom-0 p-4">
|
|
||||||
// <SessionControls
|
|
||||||
// startSession={startSession}
|
|
||||||
// stopSession={stopSession}
|
|
||||||
// sendClientEvent={sendClientEvent}
|
|
||||||
// sendTextMessage={sendTextMessage}
|
|
||||||
// events={events}
|
|
||||||
// isSessionActive={isSessionActive}
|
|
||||||
// />
|
|
||||||
// </section>
|
|
||||||
// </section>
|
|
||||||
// <section className="absolute top-0 w-[380px] right-0 bottom-0 p-4 pt-0 overflow-y-auto">
|
|
||||||
// <ToolPanel
|
|
||||||
// sendClientEvent={sendClientEvent}
|
|
||||||
// sendTextMessage={sendTextMessage}
|
|
||||||
// events={events}
|
|
||||||
// isSessionActive={isSessionActive}
|
|
||||||
// />
|
|
||||||
// </section>
|
|
||||||
// </main>
|
|
||||||
// </>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import {useState, useRef, useEffect} from 'react';
|
import {useState, useRef, useEffect} from 'react';
|
||||||
|
import useLoginToken from "../Hooks/useLoginToken.tsx";
|
||||||
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
function SessionActive({stopSession} : {stopSession: () => void}) {
|
function SessionActive({stopSession} : {stopSession: () => void})
|
||||||
|
{
|
||||||
return <button onClick={stopSession}>Stop session</button>
|
return <button onClick={stopSession}>Stop session</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
function SessionStopped({startSession} : {startSession: () => void}) {
|
function SessionStopped({startSession} : {startSession: () => void})
|
||||||
|
{
|
||||||
return <button onClick={startSession}>Start session</button>
|
return <button onClick={startSession}>Start session</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,26 +19,37 @@ function SessionControl( { isSessionActive, startSession, stopSession }: { isSes
|
|||||||
: <SessionStopped startSession={startSession}/>);
|
: <SessionStopped startSession={startSession}/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OpenAiToken {
|
const EphemeralTokenResponse = z.object({
|
||||||
ephemeralToken: string;
|
ephemeralToken: z.string(),
|
||||||
expiresAt: number;
|
expiresAt: z.number(),
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export function ChatControl()
|
||||||
|
{
|
||||||
|
const { token } = useLoginToken();
|
||||||
|
|
||||||
export function ChatControl() {
|
|
||||||
const [isSessionActive, setSessionActive] = useState<boolean>(false);
|
const [isSessionActive, setSessionActive] = useState<boolean>(false);
|
||||||
const [dataChannel, setDataChannel] = useState<RTCDataChannel | null>(null);
|
const [dataChannel, setDataChannel] = useState<RTCDataChannel | null>(null);
|
||||||
const audioElement = useRef<HTMLAudioElement>(null);
|
const audioElement = useRef<HTMLAudioElement>(null);
|
||||||
const peerConnection = useRef<RTCPeerConnection>(null);
|
const peerConnection = useRef<RTCPeerConnection>(null);
|
||||||
|
|
||||||
async function StartSession() {
|
async function StartSession()
|
||||||
|
{
|
||||||
// Get a session token for OpenAI Realtime API
|
// Get a session token for OpenAI Realtime API
|
||||||
const response = await fetch('ephemeral_token');
|
const response = await fetch('ephemeral_token', {
|
||||||
if (!response.ok) {
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}asdasd`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok)
|
||||||
|
{
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: OpenAiToken = await response.json();
|
const responseJson:unknown = await response.json();
|
||||||
const ephemeralToken = data.ephemeralToken;
|
const parsedToken = EphemeralTokenResponse.parse(responseJson);
|
||||||
|
const ephemeralToken = parsedToken.ephemeralToken;
|
||||||
|
|
||||||
// Create a peer connection
|
// Create a peer connection
|
||||||
const pc = new RTCPeerConnection();
|
const pc = new RTCPeerConnection();
|
||||||
@@ -42,7 +57,8 @@ export function ChatControl() {
|
|||||||
// Set up to play remote audio from the model
|
// Set up to play remote audio from the model
|
||||||
audioElement.current = document.createElement("audio");
|
audioElement.current = document.createElement("audio");
|
||||||
audioElement.current.autoplay = true;
|
audioElement.current.autoplay = true;
|
||||||
pc.ontrack = (e) => {
|
pc.ontrack = (e) =>
|
||||||
|
{
|
||||||
if (audioElement.current !== null)
|
if (audioElement.current !== null)
|
||||||
{
|
{
|
||||||
audioElement.current.srcObject = e.streams[0];
|
audioElement.current.srcObject = e.streams[0];
|
||||||
@@ -82,8 +98,10 @@ export function ChatControl() {
|
|||||||
peerConnection.current = pc;
|
peerConnection.current = pc;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopSession() {
|
function stopSession()
|
||||||
if (dataChannel) {
|
{
|
||||||
|
if (dataChannel)
|
||||||
|
{
|
||||||
dataChannel.close();
|
dataChannel.close();
|
||||||
setDataChannel(null);
|
setDataChannel(null);
|
||||||
}
|
}
|
||||||
@@ -97,13 +115,16 @@ export function ChatControl() {
|
|||||||
setSessionActive(false);
|
setSessionActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() =>
|
||||||
if (dataChannel) {
|
{
|
||||||
dataChannel.addEventListener("open", () => {
|
if (dataChannel)
|
||||||
|
{
|
||||||
|
dataChannel.addEventListener("open", () =>
|
||||||
|
{
|
||||||
setSessionActive(true);
|
setSessionActive(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [dataChannel])
|
}, [dataChannel])
|
||||||
|
|
||||||
return <SessionControl isSessionActive={isSessionActive} startSession={StartSession} stopSession={stopSession}/>;
|
return <SessionControl isSessionActive={isSessionActive} startSession={() => void StartSession()} stopSession={stopSession}/>;
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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 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();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const token = await loginUser({email, password});
|
||||||
|
setToken(token);
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.error(e);
|
||||||
|
setLoginError("Ein fehler ist aufgetreten.");
|
||||||
|
}
|
||||||
|
|
||||||
|
setWaiting(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
<p>Email:</p>
|
||||||
|
<input type="email" className="form-field" name="email" autoComplete="email" /*required*/
|
||||||
|
onChange={event => setEmail(event.target.value)} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p>Password:</p>
|
||||||
|
<input type="password" name="password" autoComplete="current-password"
|
||||||
|
/*required*/ onChange={event => setPassword(event.target.value)} />
|
||||||
|
</label>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginResponse = z.object({
|
||||||
|
token: z.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('register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(credentials)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok)
|
||||||
|
{
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson:unknown = await response.json();
|
||||||
|
const parsedToken = LoginResponse.parse(responseJson);
|
||||||
|
|
||||||
|
return parsedToken.token;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import Login from "./Login.tsx";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
|
const AuthValidateResult = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
email: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function ProtectedRoute({user, setUser} : {user: string | null, setUser: (user: string | null) => void})
|
||||||
|
{
|
||||||
|
|
||||||
|
async function authenticateUser ()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
console.log("Checkineasdjkf")
|
||||||
|
const response = await fetch('/auth/validate', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok)
|
||||||
|
{
|
||||||
|
console.error(`Failed to validate session: ${response.status} ${response.statusText}`);
|
||||||
|
setUser(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson:unknown = await response.json();
|
||||||
|
const parsedUser = AuthValidateResult.parse(responseJson);
|
||||||
|
|
||||||
|
setUser(JSON.stringify(parsedUser));
|
||||||
|
|
||||||
|
console.log("User authorized!")
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.error(e);
|
||||||
|
setUser(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
void authenticateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user === null)
|
||||||
|
{
|
||||||
|
return <Login setToken={setUser}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A React hook that allows to access the session token (which might be in session or local storage).
|
||||||
|
* It also allows to set the token.
|
||||||
|
*/
|
||||||
|
export default function useLoginToken()
|
||||||
|
{
|
||||||
|
const getToken = () =>
|
||||||
|
{
|
||||||
|
const tokenJson = sessionStorage.getItem('token');
|
||||||
|
|
||||||
|
return tokenJson ? JSON.parse(tokenJson) as string : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [token, setToken] = useState<string | null>(getToken());
|
||||||
|
|
||||||
|
const saveToken = (token: string | null): void =>
|
||||||
|
{
|
||||||
|
sessionStorage.setItem('token', JSON.stringify(token));
|
||||||
|
setToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In React it is common for hooks to return arrays. However, returning an object allows for partial destruction
|
||||||
|
// (that is, user can grab only the values they need).
|
||||||
|
return { token, setToken: saveToken };
|
||||||
|
}
|
||||||
@@ -43,12 +43,14 @@ button {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
transition: border-color 0.25s;
|
||||||
}
|
}
|
||||||
button:hover {
|
|
||||||
|
button:hover:not([disabled]) {
|
||||||
border-color: #646cff;
|
border-color: #646cff;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus,
|
button:focus,
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
@@ -66,3 +68,12 @@ button:focus-visible {
|
|||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-submit {
|
||||||
|
background-color: #208ddb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-submit:hover:not([disabled]) {
|
||||||
|
background-color: #3c9be0;
|
||||||
|
border-color: #c7e3f6;
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
//"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
//"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url';
|
import { fileURLToPath, URL } from 'node:url';
|
||||||
|
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig, type ProxyOptions } from 'vite';
|
||||||
import plugin from '@vitejs/plugin-react';
|
import plugin from '@vitejs/plugin-react';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import child_process from 'child_process';
|
import child_process from 'child_process';
|
||||||
import { env } from 'process';
|
import { env } from 'process';
|
||||||
|
|
||||||
|
const isDevelopment = env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
const baseFolder =
|
const baseFolder =
|
||||||
env.APPDATA !== undefined && env.APPDATA !== ''
|
env.APPDATA !== undefined && env.APPDATA !== ''
|
||||||
? `${env.APPDATA}/ASP.NET/https`
|
? `${env.APPDATA}/ASP.NET/https`
|
||||||
@@ -16,11 +18,14 @@ const certificateName = "usentrycoach.client";
|
|||||||
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
|
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
|
||||||
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
|
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
|
||||||
|
|
||||||
if (!fs.existsSync(baseFolder)) {
|
if (!fs.existsSync(baseFolder))
|
||||||
|
{
|
||||||
fs.mkdirSync(baseFolder, { recursive: true });
|
fs.mkdirSync(baseFolder, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
// Generate dev certificate, if we are in development mode, and it doesn't exist yet.
|
||||||
|
if (isDevelopment && (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)))
|
||||||
|
{
|
||||||
if (0 !== child_process.spawnSync('dotnet', [
|
if (0 !== child_process.spawnSync('dotnet', [
|
||||||
'dev-certs',
|
'dev-certs',
|
||||||
'https',
|
'https',
|
||||||
@@ -29,7 +34,8 @@ if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
|||||||
'--format',
|
'--format',
|
||||||
'Pem',
|
'Pem',
|
||||||
'--no-password',
|
'--no-password',
|
||||||
], { stdio: 'inherit', }).status) {
|
], { stdio: 'inherit', }).status)
|
||||||
|
{
|
||||||
throw new Error("Could not create certificate.");
|
throw new Error("Could not create certificate.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +43,23 @@ if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
|||||||
const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
|
const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
|
||||||
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7085';
|
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7085';
|
||||||
|
|
||||||
|
// Define a list of all existing backend routes.
|
||||||
|
const backendRoutes = ['/login', '/register', '/ephemeral_token', '/auth/validate'];
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
const proxyConfig = backendRoutes.reduce((acc, path) =>
|
||||||
|
{
|
||||||
|
acc[path] = {
|
||||||
|
target: target,
|
||||||
|
// disable certificate verification because the development certificate is self-signed
|
||||||
|
secure: false,
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, ProxyOptions>);
|
||||||
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [plugin()],
|
plugins: [plugin()],
|
||||||
@@ -46,16 +69,12 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: proxyConfig,
|
||||||
'^/ephemeral_token': {
|
port: parseInt(env.DEV_SERVER_PORT ?? '54044'),
|
||||||
target,
|
// This is only relevant for development anyway.
|
||||||
secure: false
|
https: isDevelopment ? {
|
||||||
}
|
|
||||||
},
|
|
||||||
port: parseInt(env.DEV_SERVER_PORT || '54044'),
|
|
||||||
https: {
|
|
||||||
key: fs.readFileSync(keyFilePath),
|
key: fs.readFileSync(keyFilePath),
|
||||||
cert: fs.readFileSync(certFilePath),
|
cert: fs.readFileSync(certFilePath),
|
||||||
}
|
} : undefined
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user