Compare commits
3 Commits
main
...
17817ea0a9
| Author | SHA1 | Date | |
|---|---|---|---|
| 17817ea0a9 | |||
| a0c232e052 | |||
| 3e7b04df1c |
46
.run/Run Backend and Frontend.run.xml
Normal file
46
.run/Run Backend and Frontend.run.xml
Normal file
@ -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>
|
||||
7
USEntryCoach.Server/Data/LoginCredentials.cs
Normal file
7
USEntryCoach.Server/Data/LoginCredentials.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace USEntryCoach.Server.Data;
|
||||
|
||||
class LoginCredentials
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
9
USEntryCoach.Server/Data/User.cs
Normal file
9
USEntryCoach.Server/Data/User.cs
Normal file
@ -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; }
|
||||
}
|
||||
7
USEntryCoach.Server/Data/UserRole.cs
Normal file
7
USEntryCoach.Server/Data/UserRole.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace USEntryCoach.Server.Data;
|
||||
|
||||
public enum UserRole
|
||||
{
|
||||
Developer,
|
||||
User
|
||||
}
|
||||
@ -1,13 +1,68 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using USEntryCoach.Server.Data;
|
||||
using USEntryCoach.Server.Services;
|
||||
|
||||
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.AddSingleton<TokenService>();
|
||||
|
||||
// Configure JWT token generation.
|
||||
builder.Services.AddAuthentication(config =>
|
||||
{
|
||||
config.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
config.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer(config =>
|
||||
{
|
||||
// TODO: Use Microsoft.Extensions.Options?
|
||||
// var myOptions = new MyOptions(); // Your settings class
|
||||
// builder.Configuration.GetSection("MySection").Bind(myOptions);
|
||||
// myOptions now has the values
|
||||
//
|
||||
// TODO: This is identical in TokenService
|
||||
string? secretToken = builder.Configuration.GetValue<string>("Authentication:Secret");
|
||||
|
||||
if (secretToken == null)
|
||||
{
|
||||
throw new Exception("No Authentication Secret Token set! Please define a value for \"Authentication:SecretToken\" in appsettings.json.");
|
||||
}
|
||||
|
||||
byte[] secretKey = Encoding.ASCII.GetBytes(secretToken);
|
||||
|
||||
// TODO: Only for debug!
|
||||
config.RequireHttpsMetadata = false;
|
||||
config.SaveToken = true;
|
||||
config.TokenValidationParameters = new TokenValidationParameters()
|
||||
{
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(secretKey),
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.MapStaticAssets();
|
||||
|
||||
@ -24,13 +79,61 @@ string? apiKey = app.Configuration.GetValue<string>("API:OpenAI");
|
||||
client.DefaultRequestHeaders.Clear();
|
||||
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
|
||||
|
||||
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("/ephemeral_token", async () =>
|
||||
{
|
||||
if (apiKey == null)
|
||||
throw new Exception("API key not set");
|
||||
|
||||
#pragma warning disable OPENAI002
|
||||
|
||||
var options = new
|
||||
{
|
||||
model = "gpt-4o-mini-realtime-preview",
|
||||
@ -44,8 +147,6 @@ app.MapGet("/ephemeral_token", async () =>
|
||||
//}//ConversationTurnDetectionOptions.CreateServerVoiceActivityTurnDetectionOptions(0.5f, TimeSpan.FromMilliseconds(300), TimeSpan.FromMilliseconds(500), true)
|
||||
};
|
||||
|
||||
#pragma warning restore OPENAI002
|
||||
|
||||
try
|
||||
{
|
||||
JsonContent content = JsonContent.Create(options);
|
||||
@ -76,7 +177,7 @@ app.MapGet("/ephemeral_token", async () =>
|
||||
}
|
||||
|
||||
return null;
|
||||
}).WithName("GetEphemeralToken");
|
||||
}).WithName("GetEphemeralToken").RequireAuthorization(nameof(UserRole.User));
|
||||
|
||||
app.MapFallbackToFile("/index.html");
|
||||
|
||||
|
||||
56
USEntryCoach.Server/Services/TokenService.cs
Normal file
56
USEntryCoach.Server/Services/TokenService.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using USEntryCoach.Server.Data;
|
||||
|
||||
namespace USEntryCoach.Server.Services;
|
||||
|
||||
public class TokenService
|
||||
{
|
||||
private byte[] _secretToken;
|
||||
private double _jwtExpiryMinutes;
|
||||
private const double DefaultJwtExpiryMinutes = 15;
|
||||
|
||||
public TokenService(IConfiguration configuration)
|
||||
{
|
||||
string? secretToken = configuration.GetValue<string>("Authentication:Secret");
|
||||
|
||||
if (secretToken == null)
|
||||
{
|
||||
throw new Exception("No Authentication Secret Token set! Please define a value for \"Authentication:SecretToken\" in appsettings.json.");
|
||||
}
|
||||
|
||||
_secretToken = Encoding.ASCII.GetBytes(secretToken);
|
||||
|
||||
double? jwtExpiryMinutes = configuration.GetValue<double?>("Authentication:JwtExpiryMinutes");
|
||||
|
||||
if (jwtExpiryMinutes == null)
|
||||
{
|
||||
// TODO: Use logger
|
||||
Console.WriteLine($"Warning: No expiry time for jwt session tokens defined. Using {DefaultJwtExpiryMinutes} minutes.");
|
||||
}
|
||||
|
||||
_jwtExpiryMinutes = jwtExpiryMinutes ?? DefaultJwtExpiryMinutes;
|
||||
}
|
||||
|
||||
public string GenerateToken(User user)
|
||||
{
|
||||
JwtSecurityTokenHandler tokenHandler = new();
|
||||
|
||||
SecurityTokenDescriptor tokenDescriptor = new()
|
||||
{
|
||||
Subject = new ClaimsIdentity([
|
||||
new Claim(ClaimTypes.Name, user.Username),
|
||||
//new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new Claim(ClaimTypes.Role, user.Role.ToString())
|
||||
]),
|
||||
Expires = DateTime.UtcNow.AddMinutes(_jwtExpiryMinutes),
|
||||
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(_secretToken), SecurityAlgorithms.HmacSha256Signature)
|
||||
};
|
||||
|
||||
SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
|
||||
<Version>9.*-*</Version>
|
||||
|
||||
@ -4,5 +4,11 @@
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"API": {
|
||||
"OpenAI": "Please set the key in secrets.json! NEVER HERE!!!"
|
||||
},
|
||||
"Authentication": {
|
||||
"Secret": "Please provide a GUID (without dashes) as secret."
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,26 +3,56 @@ import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import stylistic from '@stylistic/eslint-plugin'
|
||||
|
||||
export default tseslint.config(
|
||||
{ignores: ['dist']},
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
//'stylistic/js': stylisticJs
|
||||
stylistic
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
//'brace-style': ['error', 'allman'],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{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 }
|
||||
]
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
677
usentrycoach.client/package-lock.json
generated
677
usentrycoach.client/package-lock.json
generated
@ -13,6 +13,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@stylistic/eslint-plugin": "^4.2.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
@ -302,390 +303,6 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
|
||||
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
|
||||
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
|
||||
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
|
||||
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
|
||||
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
|
||||
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
|
||||
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
|
||||
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
|
||||
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
|
||||
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
|
||||
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
|
||||
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
|
||||
@ -1007,253 +624,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
|
||||
"integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
|
||||
"integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
|
||||
"integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
|
||||
"integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
|
||||
"integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
|
||||
"integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
|
||||
"integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
|
||||
"integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
|
||||
"integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
|
||||
"integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
|
||||
"integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
|
||||
"integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
|
||||
"integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
|
||||
"integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.40.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
|
||||
@ -1267,6 +637,37 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-4.2.0.tgz",
|
||||
"integrity": "sha512-8hXezgz7jexGHdo5WN6JBEIPHCSFyyU4vgbxevu4YLVS5vl+sxqAAGyXSzfNDyR6xMNSH5H1x67nsXcYMOHtZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^8.23.0",
|
||||
"eslint-visitor-keys": "^4.2.0",
|
||||
"espree": "^10.3.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"picomatch": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@ -2489,20 +1890,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@stylistic/eslint-plugin": "^4.2.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
@ -24,7 +26,6 @@
|
||||
"globals": "^16.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5",
|
||||
"@types/node": "^20"
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
@ -1,415 +1,16 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import './App.css';
|
||||
import {ChatControl} from "./ChatClient/ChatControl.tsx";
|
||||
import Login from './Components/Login.tsx';
|
||||
import useLoginToken from "./Hooks/useLoginToken.tsx";
|
||||
|
||||
interface Forecast {
|
||||
date: string;
|
||||
temperatureC: number;
|
||||
temperatureF: number;
|
||||
summary: string;
|
||||
export default function App()
|
||||
{
|
||||
const {token, setToken} = useLoginToken();
|
||||
|
||||
if (!token)
|
||||
{
|
||||
return <Login setToken={setToken} />
|
||||
}
|
||||
|
||||
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 useLoginToken from "../Hooks/useLoginToken.tsx";
|
||||
import { z } from 'zod';
|
||||
|
||||
function SessionActive({stopSession} : {stopSession: () => void}) {
|
||||
function SessionActive({stopSession} : {stopSession: () => void})
|
||||
{
|
||||
return <button onClick={stopSession}>Stop session</button>
|
||||
}
|
||||
|
||||
function SessionStopped({startSession} : {startSession: () => void}) {
|
||||
function SessionStopped({startSession} : {startSession: () => void})
|
||||
{
|
||||
return <button onClick={startSession}>Start session</button>
|
||||
}
|
||||
|
||||
@ -15,26 +19,37 @@ function SessionControl( { isSessionActive, startSession, stopSession }: { isSes
|
||||
: <SessionStopped startSession={startSession}/>);
|
||||
}
|
||||
|
||||
interface OpenAiToken {
|
||||
ephemeralToken: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
const EphemeralTokenResponse = z.object({
|
||||
ephemeralToken: z.string(),
|
||||
expiresAt: z.number(),
|
||||
});
|
||||
|
||||
export function ChatControl()
|
||||
{
|
||||
const { token } = useLoginToken();
|
||||
|
||||
export function ChatControl() {
|
||||
const [isSessionActive, setSessionActive] = useState<boolean>(false);
|
||||
const [dataChannel, setDataChannel] = useState<RTCDataChannel | null>(null);
|
||||
const audioElement = useRef<HTMLAudioElement>(null);
|
||||
const peerConnection = useRef<RTCPeerConnection>(null);
|
||||
|
||||
async function StartSession() {
|
||||
async function StartSession()
|
||||
{
|
||||
// Get a session token for OpenAI Realtime API
|
||||
const response = await fetch('ephemeral_token');
|
||||
if (!response.ok) {
|
||||
const response = await fetch('ephemeral_token', {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}asdasd`
|
||||
},
|
||||
});
|
||||
if (!response.ok)
|
||||
{
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const data: OpenAiToken = await response.json();
|
||||
const ephemeralToken = data.ephemeralToken;
|
||||
const responseJson:unknown = await response.json();
|
||||
const parsedToken = EphemeralTokenResponse.parse(responseJson);
|
||||
const ephemeralToken = parsedToken.ephemeralToken;
|
||||
|
||||
// Create a peer connection
|
||||
const pc = new RTCPeerConnection();
|
||||
@ -42,7 +57,8 @@ export function ChatControl() {
|
||||
// Set up to play remote audio from the model
|
||||
audioElement.current = document.createElement("audio");
|
||||
audioElement.current.autoplay = true;
|
||||
pc.ontrack = (e) => {
|
||||
pc.ontrack = (e) =>
|
||||
{
|
||||
if (audioElement.current !== null)
|
||||
{
|
||||
audioElement.current.srcObject = e.streams[0];
|
||||
@ -82,8 +98,10 @@ export function ChatControl() {
|
||||
peerConnection.current = pc;
|
||||
}
|
||||
|
||||
function stopSession() {
|
||||
if (dataChannel) {
|
||||
function stopSession()
|
||||
{
|
||||
if (dataChannel)
|
||||
{
|
||||
dataChannel.close();
|
||||
setDataChannel(null);
|
||||
}
|
||||
@ -97,13 +115,16 @@ export function ChatControl() {
|
||||
setSessionActive(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dataChannel) {
|
||||
dataChannel.addEventListener("open", () => {
|
||||
useEffect(() =>
|
||||
{
|
||||
if (dataChannel)
|
||||
{
|
||||
dataChannel.addEventListener("open", () =>
|
||||
{
|
||||
setSessionActive(true);
|
||||
});
|
||||
}
|
||||
}, [dataChannel])
|
||||
|
||||
return <SessionControl isSessionActive={isSessionActive} startSession={StartSession} stopSession={stopSession}/>;
|
||||
return <SessionControl isSessionActive={isSessionActive} startSession={() => void StartSession()} stopSession={stopSession}/>;
|
||||
}
|
||||
60
usentrycoach.client/src/Components/Login.tsx
Normal file
60
usentrycoach.client/src/Components/Login.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
export default function Login({ setToken } : {setToken: (token: string) => void})
|
||||
{
|
||||
const [username, setUsername] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
|
||||
async function doLogin(event: FormEvent<HTMLFormElement>)
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
const token = await loginUser({username, password});
|
||||
|
||||
setToken(token);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form className="login-form" onSubmit={e => void doLogin(e)} method="action">
|
||||
<label>
|
||||
Email:
|
||||
<input type="text" name="username" placeholder="Username" required onChange={event => setUsername(event.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Password:
|
||||
<input type="password" name="password" placeholder="Password" required onChange={event => setPassword(event.target.value)} />
|
||||
</label>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const LoginResponse = z.object({
|
||||
token: z.string()
|
||||
});
|
||||
|
||||
async function loginUser(credentials : {username: string, password: string}): Promise<string>
|
||||
{
|
||||
// Get a session token for OpenAI Realtime API
|
||||
const response = await fetch('login', {
|
||||
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;
|
||||
}
|
||||
27
usentrycoach.client/src/Hooks/useLoginToken.tsx
Normal file
27
usentrycoach.client/src/Hooks/useLoginToken.tsx
Normal file
@ -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): 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 };
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig, type ProxyOptions } from 'vite';
|
||||
import plugin from '@vitejs/plugin-react';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@ -16,11 +16,13 @@ const certificateName = "usentrycoach.client";
|
||||
const certFilePath = path.join(baseFolder, `${certificateName}.pem`);
|
||||
const keyFilePath = path.join(baseFolder, `${certificateName}.key`);
|
||||
|
||||
if (!fs.existsSync(baseFolder)) {
|
||||
if (!fs.existsSync(baseFolder))
|
||||
{
|
||||
fs.mkdirSync(baseFolder, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath))
|
||||
{
|
||||
if (0 !== child_process.spawnSync('dotnet', [
|
||||
'dev-certs',
|
||||
'https',
|
||||
@ -29,7 +31,8 @@ if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
||||
'--format',
|
||||
'Pem',
|
||||
'--no-password',
|
||||
], { stdio: 'inherit', }).status) {
|
||||
], { stdio: 'inherit', }).status)
|
||||
{
|
||||
throw new Error("Could not create certificate.");
|
||||
}
|
||||
}
|
||||
@ -37,6 +40,23 @@ if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
||||
const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
|
||||
env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7085';
|
||||
|
||||
// Define a list of all existing backend routes.
|
||||
const backendRoutes = ['/login', '/ephemeral_token'];
|
||||
|
||||
// For development, we have a node.js server running that delivers our frontend.
|
||||
// We have to configure proxies for the backend calls, so that node.js will forward them to the backend.
|
||||
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/
|
||||
export default defineConfig({
|
||||
plugins: [plugin()],
|
||||
@ -46,13 +66,8 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'^/ephemeral_token': {
|
||||
target,
|
||||
secure: false
|
||||
}
|
||||
},
|
||||
port: parseInt(env.DEV_SERVER_PORT || '54044'),
|
||||
proxy: proxyConfig,
|
||||
port: parseInt(env.DEV_SERVER_PORT ?? '54044'),
|
||||
https: {
|
||||
key: fs.readFileSync(keyFilePath),
|
||||
cert: fs.readFileSync(certFilePath),
|
||||
|
||||
Reference in New Issue
Block a user