diff --git a/.gitignore b/.gitignore index e51f3af2e2..a437d1c790 100644 --- a/.gitignore +++ b/.gitignore @@ -149,3 +149,7 @@ static/ data/**/*.md5 .DS_Store + +localcert.pem +localcert_key.pem +*.pem diff --git a/.vscode/launch.json b/.vscode/launch.json index 5a83dfd713..a67b501486 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Python: Quart", + "name": "Backend (HTTP)", "type": "debugpy", "request": "launch", "module": "quart", @@ -26,12 +26,42 @@ "envFile": "${input:dotEnvFilePath}", }, { - "name": "Frontend: watch", + "name": "Backend (HTTPS)", + "type": "debugpy", + "request": "launch", + "module": "quart", + "cwd": "${workspaceFolder}/app/backend", + "python": "${workspaceFolder}/.venv/bin/python", + "env": { + "QUART_APP": "main:app", + "QUART_ENV": "development", + "QUART_DEBUG": "0" + }, + "args": [ + "run", + "--no-reload", + "--port=50505", + "--certfile=../../localcert.pem", + "--keyfile=../../localcert_key.pem" + ], + "console": "integratedTerminal", + "justMyCode": false, + "envFile": "${input:dotEnvFilePath}", + }, + { + "name": "Frontend (HTTP)", "type": "node-terminal", "request": "launch", "command": "npm run dev", "cwd": "${workspaceFolder}/app/frontend", }, + { + "name": "Frontend (HTTPS)", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev-https", + "cwd": "${workspaceFolder}/app/frontend", + }, { "name": "Python: Debug Tests", "type": "debugpy", diff --git a/app/frontend/package-lock.json b/app/frontend/package-lock.json index 7c4117e085..5287c27e0d 100644 --- a/app/frontend/package-lock.json +++ b/app/frontend/package-lock.json @@ -26,6 +26,7 @@ "devDependencies": { "@types/dom-speech-recognition": "^0.0.4", "@types/dompurify": "^3.0.4", + "@types/node": "^20.14.11", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-syntax-highlighter": "^15.5.13", @@ -2640,6 +2641,15 @@ "@types/unist": "^2" } }, + "node_modules/@types/node": { + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/prop-types": { "version": "15.7.5", "license": "MIT" @@ -3556,6 +3566,12 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -5444,6 +5460,15 @@ "@types/unist": "^2" } }, + "@types/node": { + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, "@types/prop-types": { "version": "15.7.5" }, @@ -6010,6 +6035,12 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==" }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", diff --git a/app/frontend/package.json b/app/frontend/package.json index a55c9c2e10..a497dc5bab 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -8,34 +8,36 @@ }, "scripts": { "dev": "vite --host 127.0.0.1", + "dev-https": "vite --host 127.0.0.1 --config vite.https.ts", "build": "tsc && vite build", "preview": "vite preview" }, "dependencies": { - "@azure/msal-react": "^2.0.6", "@azure/msal-browser": "^3.17.0", + "@azure/msal-react": "^2.0.6", "@fluentui/react": "^8.112.5", "@fluentui/react-components": "^9.37.3", "@fluentui/react-icons": "^2.0.221", "@react-spring/web": "^9.7.3", - "marked": "^13.0.0", "dompurify": "^3.0.6", + "marked": "^13.0.0", + "ndjson-readablestream": "^1.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.23.1", - "ndjson-readablestream": "^1.2.0", "react-syntax-highlighter": "^15.5.0", "scheduler": "^0.20.2" }, "devDependencies": { + "@types/dom-speech-recognition": "^0.0.4", "@types/dompurify": "^3.0.4", + "@types/node": "^20.14.11", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.3.1", "prettier": "^3.0.3", "typescript": "^5.5.3", - "@types/react-syntax-highlighter": "^15.5.13", - "@types/dom-speech-recognition": "^0.0.4", "vite": "^4.5.3" } } diff --git a/app/frontend/tsconfig.json b/app/frontend/tsconfig.json index 39a545e919..430f2eff0b 100644 --- a/app/frontend/tsconfig.json +++ b/app/frontend/tsconfig.json @@ -15,7 +15,7 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "types": ["vite/client"] + "types": ["vite/client", "node"] }, "include": ["src"] } diff --git a/app/frontend/vite.common.ts b/app/frontend/vite.common.ts new file mode 100644 index 0000000000..3f27bf26bc --- /dev/null +++ b/app/frontend/vite.common.ts @@ -0,0 +1,27 @@ +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +const commonConfig = { + plugins: [react()], + build: { + outDir: "../backend/static", + emptyOutDir: true, + sourcemap: true, + rollupOptions: { + output: { + manualChunks: id => { + if (id.includes("@fluentui/react-icons")) { + return "fluentui-icons"; + } else if (id.includes("@fluentui/react")) { + return "fluentui-react"; + } else if (id.includes("node_modules")) { + return "vendor"; + } + } + } + }, + target: "esnext" + } +}; + +export default commonConfig; diff --git a/app/frontend/vite.config.ts b/app/frontend/vite.config.ts index 7fe15e7533..94fc80fea4 100644 --- a/app/frontend/vite.config.ts +++ b/app/frontend/vite.config.ts @@ -1,28 +1,8 @@ import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import commonConfig from "./vite.config.ts"; -// https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], - build: { - outDir: "../backend/static", - emptyOutDir: true, - sourcemap: true, - rollupOptions: { - output: { - manualChunks: id => { - if (id.includes("@fluentui/react-icons")) { - return "fluentui-icons"; - } else if (id.includes("@fluentui/react")) { - return "fluentui-react"; - } else if (id.includes("node_modules")) { - return "vendor"; - } - } - } - }, - target: "esnext" - }, + ...commonConfig, server: { proxy: { "/content/": "http://localhost:50505", diff --git a/app/frontend/vite.https.ts b/app/frontend/vite.https.ts new file mode 100644 index 0000000000..de79ff3172 --- /dev/null +++ b/app/frontend/vite.https.ts @@ -0,0 +1,27 @@ +import fs from "fs"; + +import { defineConfig } from "vite"; + +import commonConfig from "./vite.common.ts"; + +export default defineConfig({ + ...commonConfig, + server: { + proxy: { + "/content/": "https://localhost:50505", + "/auth_setup": "https://localhost:50505", + "/.auth/me": "https://localhost:50505", + "/ask": "https://localhost:50505", + "/chat": "https://localhost:50505", + "/speech": "https://localhost:50505", + "/config": "https://localhost:50505", + "/upload": "https://localhost:50505", + "/delete_uploaded": "https://localhost:50505", + "/list_uploaded": "https://localhost:50505" + }, + https: { + cert: fs.readFileSync("../../localcert.pem"), + key: fs.readFileSync("../../localcert_key.pem") + } + } +}); diff --git a/app/start.sh b/app/start.sh index ec7d64067a..5a1a81e41a 100755 --- a/app/start.sh +++ b/app/start.sh @@ -59,7 +59,13 @@ cd ../backend port=50505 host=localhost -../../.venv/bin/python -m quart --app main:app run --port "$port" --host "$host" --reload + +# optionally add certfile and keyfile args if AZURE_USE_AUTHENTICATION = "true" +if [ "$AZURE_USE_AUTHENTICATION" = "true" ]; then + certArgs="--certfile ../../localcert.pem --keyfile ../../localcert_key.pem" +fi + +../../.venv/bin/python -m quart --app main:app run --port "$port" --host "$host" --reload $certArgs if [ $? -ne 0 ]; then echo "Failed to start backend" exit $? diff --git a/docs/localdev.md b/docs/localdev.md index cb717b9d1b..f161a2fd35 100644 --- a/docs/localdev.md +++ b/docs/localdev.md @@ -40,6 +40,34 @@ Navigate to the URL shown in the terminal (in this case, `http://localhost:5173/ Then, whenever you make changes to frontend files, the changes will be automatically reloaded, without any browser refresh needed. +## Accessing localhost over HTTPS + +If you are using the user authentication feature of the app, you should only access your localhost server over HTTPS. That requires generated locally trusted certificates and then running the localhost with access to those certificates. + +### Generating certificates + +To generate certificates, you can use the `mkcert` tool. Follow the instructions on the [mkcert GitHub page](https://github.com/FiloSottile/mkcert) to install it on your operating system. + +Once installed, run the following command from the repository root: + +```shell +mkcert --key-file localcert_key.pem --cert-file localcert.pem 127.0.0.1 localhost +``` + +This will generate two files: `localcert_key.pem` and `localcert.pem`. These files will be used to run the localhost server with HTTPS. + +### Running the localhost server with HTTPS + +If you are running the localhost server by running `./start.ps1` or `./start.sh`, then it will automatically pass in the certificates to the server when `AZURE_USE_AUTHENTICATION` is true, using the `certfile` and `keyfile` arguments. + +To run the frontend server with HTTPS, you can use the following command: + +```shell +npm run dev-https +``` + +If you are using the VS Code debugger to run the local server, then use "Backend (HTTPS)" and "Frontend (HTTPS)", which are configured to use the certificates. + ## Using a local OpenAI-compatible API You may want to save costs by developing against a local LLM server, such as diff --git a/scripts/auth_init.py b/scripts/auth_init.py index e638f40f73..0786f48411 100644 --- a/scripts/auth_init.py +++ b/scripts/auth_init.py @@ -134,7 +134,14 @@ def client_app(server_app_id: str, server_app: Application, identifier: int) -> redirect_uris=["http://localhost:50505/.auth/login/aad/callback"], implicit_grant_settings=ImplicitGrantSettings(enable_id_token_issuance=True), ), - spa=SpaApplication(redirect_uris=["http://localhost:50505/redirect", "http://localhost:5173/redirect"]), + spa=SpaApplication( + redirect_uris=[ + "http://localhost:50505/redirect", + "https://localhost:50505/redirect", + "http://localhost:5173/redirect", + "https://localhost:5173/redirect", + ] + ), required_resource_access=[ RequiredResourceAccess( resource_app_id=server_app_id, diff --git a/scripts/auth_update.py b/scripts/auth_update.py index cb6cd48968..c4f2186785 100755 --- a/scripts/auth_update.py +++ b/scripts/auth_update.py @@ -34,7 +34,9 @@ async def main(): spa=SpaApplication( redirect_uris=[ "http://localhost:50505/redirect", + "https://localhost:50505/redirect", "http://localhost:5173/redirect", + "https://localhost:5173/redirect", f"{uri}/redirect", ] ),