Compare commits
20 Commits
5c84bbc35a
...
feature/ci
| Author | SHA1 | Date | |
|---|---|---|---|
| f4737f4dcf | |||
| 1d68f177de | |||
| ae5096bacd | |||
| a2608eac5b | |||
| 189069361f | |||
| 17817ea0a9 | |||
| a0c232e052 | |||
| 3e7b04df1c | |||
| bd7759cbcb | |||
| af0c49db77 | |||
| a0c994bc44 | |||
| 9758cba025 | |||
| fcd81969a8 | |||
| 0d4046d56c | |||
| 94d17a1266 | |||
| 326f3531b1 | |||
| 6eb6c09a4d | |||
| 1339a22ab8 | |||
| 465c4ca0c1 | |||
| d874c948e8 |
27
.gitea/workflows/backend-build.yml
Normal file
27
.gitea/workflows/backend-build.yml
Normal file
@ -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
|
||||
26
.gitea/workflows/frontend-build.yml
Normal file
26
.gitea/workflows/frontend-build.yml
Normal file
@ -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
|
||||
47
.gitea/workflows/pipeline.yml
Normal file
47
.gitea/workflows/pipeline.yml
Normal file
@ -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
|
||||
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'] },
|
||||
{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 },
|
||||
{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 }
|
||||
]
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
1363
usentrycoach.client/package-lock.json
generated
1363
usentrycoach.client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,10 +11,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
"react-dom": "^19.1.0",
|
||||
"zod": "^3.25.23"
|
||||
},
|
||||
"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 +27,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();
|
||||
|
||||
interface OpenAiToken {
|
||||
ephemeralToken: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
|
||||
export default function App() {
|
||||
if (!token)
|
||||
{
|
||||
return <Login setToken={setToken} />
|
||||
}
|
||||
|
||||
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/v4';
|
||||
|
||||
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/v4';
|
||||
|
||||
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,12 +1,14 @@
|
||||
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';
|
||||
import child_process from 'child_process';
|
||||
import { env } from 'process';
|
||||
|
||||
const isDevelopment = env.NODE_ENV !== 'production';
|
||||
|
||||
const baseFolder =
|
||||
env.APPDATA !== undefined && env.APPDATA !== ''
|
||||
? `${env.APPDATA}/ASP.NET/https`
|
||||
@ -16,11 +18,14 @@ 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)) {
|
||||
// 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', [
|
||||
'dev-certs',
|
||||
'https',
|
||||
@ -29,7 +34,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 +43,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,16 +69,12 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'^/ephemeral_token': {
|
||||
target,
|
||||
secure: false
|
||||
}
|
||||
},
|
||||
port: parseInt(env.DEV_SERVER_PORT || '54044'),
|
||||
https: {
|
||||
proxy: proxyConfig,
|
||||
port: parseInt(env.DEV_SERVER_PORT ?? '54044'),
|
||||
// This is only relevant for development anyway.
|
||||
https: isDevelopment ? {
|
||||
key: fs.readFileSync(keyFilePath),
|
||||
cert: fs.readFileSync(certFilePath),
|
||||
}
|
||||
} : undefined
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user