diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml new file mode 100644 index 0000000..53c75fe --- /dev/null +++ b/.forgejo/workflows/build.yaml @@ -0,0 +1,109 @@ +on: + push: + workflow_dispatch: + inputs: + build: + description: "Build package and container" + required: true + default: false + type: boolean + +jobs: + build: + runs-on: container + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get the version from git + id: version + run: echo "version=$(git describe --dirty --tags)" >> $GITHUB_OUTPUT + + - name: Check if the package should be built + id: builder + env: + RUN: ${{ toJSON(inputs.build || !contains(steps.version.outputs.version, '-')) }} + run: | + echo "run=$RUN" >> $GITHUB_OUTPUT + echo "Run build: $RUN" + + - name: Info - version and if the image container should be built + env: + VERSION: ${{ steps.version.outputs.version }} + RUN: ${{ steps.builder.outputs.run }} + FORCE: ${{ toJSON(inputs.build) }} + run: | + echo "Version $VERSION, force (manual input): $FORCE, run the build: $RUN" + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + version: 10 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - name: Update version.json from git describe + run: pnpm run version + + - name: Set the version in package.json + env: + VERSION: ${{ steps.version.outputs.version }} + run: sed "s/0.0.0/${VERSION}/" -i package.json + + - name: Build package (transpile ts => js) + run: pnpm run build --base /oidc-test-web + + - name: Set registry token for pnpm" + env: + LOCAL_NPM_TOKEN: ${{ secrets.LOCAL_NPM_TOKEN }} + run: pnpm set "//code.philo.ydns.eu/api/packages/philorg/npm/:_authToken=${LOCAL_NPM_TOKEN}" + + - name: Publish + if: fromJSON(steps.builder.outputs.run) + run: pnpm publish --no-git-checks + continue-on-error: true + + - name: Build container + if: fromJSON(steps.builder.outputs.run) + uses: actions/buildah-build@v1 + with: + image: oidc-vue-test + oci: true + labels: oidc-vue-test + tags: latest ${{ steps.version.outputs.version }} + containerfiles: | + ./Containerfile + build-args: | + APP_VERSION=${{ steps.version.outputs.version }} + + - name: Workaround for bug of podman-login + if: fromJSON(steps.builder.outputs.run) + run: | + mkdir -p $HOME/.docker + echo "{ \"auths\": {} }" > $HOME/.docker/config.json + + - name: Log in to container registry (with another workaround) + if: fromJSON(steps.builder.outputs.run) + uses: actions/podman-login@v1 + with: + registry: ${{ vars.REGISTRY }} + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_PASSWORD }} + auth_file_path: /tmp/auth.json + + - name: Push the image to the registry + if: fromJSON(steps.builder.outputs.run) + uses: actions/push-to-registry@v2 + with: + registry: "docker://${{ vars.REGISTRY }}/${{ vars.ORGANISATION }}" + image: oidc-vue-test + tags: latest ${{ steps.version.outputs.version }} diff --git a/.gitignore b/.gitignore index 3659f1a..eff8b6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules/* dist/* +settings.json +.env diff --git a/Containerfile b/Containerfile index e9815a2..f5612bd 100644 --- a/Containerfile +++ b/Containerfile @@ -1,3 +1,5 @@ FROM docker.io/nginx:alpine -COPY ./dist /usr/share/nginx/html +COPY ./dist /usr/share/nginx/html/oidc-test-web + +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 3f71700..271f522 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,126 @@ -# Test app for OIDC, OAuth2 - pure web client version written with Vue3 +# Test app for OIDC, OAuth2 - web app version written with Vue3 Small web app for experimenting a web app with a Keycloak auth server. -## Deployment +It is a sibling of the server version [oidc-test](philorg/oidc-fastapi-test), +which acts also as a resource server. -In a container: +Live demo: : + +- configured with a test realm on a private Keycloak instance +- 2 users are defined: foo (foofoo) and bar (barbar). + +**Note**: decoding tokens requires the use of cryto extension, +that web browsers allow only with a secured connection (https). + +## Configuration + +The app expects that a `settings.json` file is available on the server +at the app's base url. + +For example: + +```json +{ + "keycloakUri": "https://keycloak.your.domain", + "realm": "test", + "authProvider": "keycloak", + "sso": false, + "clientId": "oidc-test-web", + "tokenSandbox": true, + "resourceServerUrl": "https://someserver.your.domain/resourceBaseUrl", + "resourceScopes": [ + "get:time", + "get:bs" + ], + "resourceProviders": { + "resourceProvider1": { + "name": "Third party 1", + "baseUrl": "https://otherserver.your.domain/resources/", + "verifySSL": true, + "resources": { + "public": { + "name": "A public resource", + "url": "resource/public" + }, + "bs": { + "name": "A secured resource, eg by scope", + "url": "resource/secured1" + }, + "time": { + "name": "Another secured resource, eg by role", + "url": "resource/secured2" + } + } + } + } +} +``` + +## Build + +For generating a `dist` directory ready to be copied to a web server +static data tree, it's a straightforward: + +```sh +pnpm run build +``` + +Eventually specify a `base url` (eg. accessible from `https://for.example.com/oidc-test-web`): ```sh pnpm run build --base oidc-test-web -podman run -it --rm -p 8874:80 -v ./dist:/usr/share/nginx/html/oidc-test-web docker.io/nginx:alpine +``` + +## Deployment + +Examples of deployment are presented below. + +- Using the nginx default container, from the development source tree: + +```sh +podman run -it --rm -p 8874:80 -v ./dist:/usr/share/nginx/html/oidc-test-web -v ./settings.json:/usr/share/nginx/html/oidc-test-web/settings.json docker.io/nginx:alpine +``` + +- The build is packaged in a provided container (see *pakcages*), serving with the `/oidc-test-web` base url: + +```sh +podman run -it --rm -p 8874:80 -v ./settings.json:/usr/share/nginx/html/oidc-test-web/settings.json code.philo.ydns.eu/philorg/oidc-vue-test:latest +``` + +- A *quadlet* *systemd* service, in `~/.config/containers/systemd/oidc-vue-test.container`: + +```systemd +[Container] +ContainerName=oidc-vue-test +Image=code.philo.ydns.eu/philorg/oidc-vue-test:latest +Mount=type=bind,source=/path/to/settings.json,destination=/usr/share/nginx/html/oidc-test-web/settings.json +PublishPort=8874:80 + +[Service] +Restart=always +RestartSec=5 + +[Unit] +After=podman-user-wait-network-online.service + +[Install] +WantedBy=default.target +``` + +Run with: + +```sh +systemctl --user daemon-reload +systemcrl --user start oidc-vue-test ``` ## Frontend +YMMV, easy with *Caddy*: + ```Caddyfile -handle /iodc-test-web { +handle /oidc-test-web { reverse-proxy hostname.domainame:8874 } redir /oidc-test-web /oidc-test-web/ diff --git a/example_settings.json b/example_settings.json new file mode 100644 index 0000000..3c1f9f5 --- /dev/null +++ b/example_settings.json @@ -0,0 +1,13 @@ +{ + "keycloakUri": "https://philo.ydns.eu/auth", + "realm": "test", + "authProvider": "keycloak", + "sso": false, + "clientId": "oidc-test-web", + "resourceServerUrl": "https://philo.ydns.eu/oidc-test/resource", + "resourceScopes": [ + "get:time", + "get:bs" + ], + "tokenSandbox": true +} diff --git a/index.html b/index.html index e2443c1..fc972fe 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,11 @@ - Axios interceptor example + OIDC test web +
- + diff --git a/package.json b/package.json index 7aae403..360dd30 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { - "name": "typescript", + "name": "oidc-test-web", "version": "0.0.0", - "private": true, "type": "module", "scripts": { "dev": "vite --port 3000", "build": "run-p type-check \"build-only {@}\" --", "preview": "vite preview", "build-only": "vite build", - "type-check": "vue-tsc --build" + "type-check": "vue-tsc --build", + "version": "echo \"{\\\"version\\\":\\\"$(git describe --tags --dirty --always)\\\"}\" > src/version.json" }, "dependencies": { - "@dsb-norge/vue-keycloak-js": "3.0.1", + "@dsb-norge/vue-keycloak-js": "^3.0.1", "axios": "1.7.9", + "keycloak-js": "^26.1.0", "vue": "3.5.13" }, "devDependencies": { @@ -25,5 +26,8 @@ "vite": "6.0.7", "vite-plugin-vue-devtools": "7.7.0", "vue-tsc": "2.2.0" + }, + "publishConfig": { + "registry": "http://code.philo.ydns.eu/api/packages/philorg/npm/" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e9f286..356bbae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,14 @@ importers: .: dependencies: '@dsb-norge/vue-keycloak-js': - specifier: 3.0.0 - version: 3.0.0(vue@3.5.13(typescript@5.6.3)) + specifier: ^3.0.1 + version: 3.0.1(vue@3.5.13(typescript@5.6.3)) axios: specifier: 1.7.9 version: 1.7.9 + keycloak-js: + specifier: ^26.1.0 + version: 26.1.0 vue: specifier: 3.5.13 version: 3.5.13(typescript@5.6.3) @@ -191,8 +194,8 @@ packages: resolution: {integrity: sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==} engines: {node: '>=6.9.0'} - '@dsb-norge/vue-keycloak-js@3.0.0': - resolution: {integrity: sha512-PcANEYbCRWtvMLg9wPpGuBDR5wrvfC4nkOawGsF79zm6RQ6IfxGIpxWHoZSXEcRaYdPchkT4gCZ+dyp8wb7ccA==} + '@dsb-norge/vue-keycloak-js@3.0.1': + resolution: {integrity: sha512-uJ4deVw4Vyp2FrG0JjYAy8NE6zIlIIl/92mQDlSGH+9kc758hBdrCdZSD3aVzv/Lwh07tpOXsx4jXzbVQkPfkA==} peerDependencies: vue: '>=3.0.0' @@ -803,6 +806,9 @@ packages: keycloak-js@26.0.7: resolution: {integrity: sha512-vKCk1ISMiouYRLjMzi5fKp58RnYxKEBS29BoDgVfYwVW94IXchj9jLqBvIet31VD1v79l3WaWT+WMX7fH8O4wA==} + keycloak-js@26.1.0: + resolution: {integrity: sha512-3CTelLNADK6sIxGHCQmKlT3ezcIp8O3Iimmg+ybS78RHy+HAUkkoBaW/YuHGdYkfEDMBlrqD3u+CQ4vLsrmyFA==} + kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -1298,7 +1304,7 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@dsb-norge/vue-keycloak-js@3.0.0(vue@3.5.13(typescript@5.6.3))': + '@dsb-norge/vue-keycloak-js@3.0.1(vue@3.5.13(typescript@5.6.3))': dependencies: keycloak-js: 26.0.7 vue: 3.5.13(typescript@5.6.3) @@ -1825,6 +1831,8 @@ snapshots: keycloak-js@26.0.7: {} + keycloak-js@26.1.0: {} + kolorist@1.8.0: {} lru-cache@5.1.1: diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..8d4804f --- /dev/null +++ b/public/styles.css @@ -0,0 +1,229 @@ +body { + font-family: Arial, Helvetica, sans-serif; + background-color: floralwhite; + margin: 0; + font-family: system-ui; + text-align: center; +} +h1 { + background-color: #f786867d; + margin: 0 0 0.2em 0; + box-shadow: 0px 0.2em 0.2em #f786867d; + text-shadow: 0 0 2px #00000080; + font-weight: 200; +} +p { + margin: 0.2em; +} +hr { + margin: 0.2em; +} +.hidden { + display: none; +} +.center { + text-align: center; +} +.error { + color: darkred; +} +.content { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} +.user-info { + padding: 0.5em; + display: flex; + gap: 0.5em; + flex-direction: column; + width: fit-content; + align-items: center; + margin: 5px auto; + box-shadow: 0px 0px 10px lightgreen; + background-color: lightgreen; + border-radius: 8px; +} +.user-info * { + flex: 2 1 auto; + margin: 0; +} +.user-info .picture { + max-width: 3em; + max-height: 3em +} +.user-info a.logout { + border: 2px solid darkkhaki; + padding: 3px 6px; + text-decoration: none; + color: black; +} +.user-info a.logout:hover { + background-color: orange; +} +.debug-auth { + font-size: 90%; + background-color: #d8bebc75; + padding: 6px; +} +.debug-auth * { + margin: 0; +} +.debug-auth p { + border-bottom: 1px solid black; +} +.debug-auth ul { + padding: 0; + list-style: none; +} +.debug-auth p, .debug-auth .key { + font-weight: bold; +} +.content { + text-align: left; +} +.hasResponseStatus { + background-color: #88888840; +} +.hasResponseStatus.status-200 { + background-color: #00ff0040; +} +.hasResponseStatus.status-401 { + background-color: #ff000040; +} +.hasResponseStatus.status-403 { + background-color: #ff990040; +} +.hasResponseStatus.status-404 { + background-color: #ffCC0040; +} +.hasResponseStatus.status-503 { + background-color: #ffA88050; +} + +.role, .scope { + padding: 3px 6px; + margin: 3px; + border-radius: 6px; +} + +.role { + background-color: #44228840; +} + +.scope { + background-color: #8888FF80; +} + + +/* For home */ + +.login-box { + background-color: antiquewhite; + margin: 0.5em auto; + width: fit-content; + box-shadow: 0 0 10px #49759b88; + border-radius: 8px; +} +.login-box .description { + font-style: italic; + font-weight: bold; + background-color: #f7c7867d; + padding: 6px; + margin: 0; + border-radius: 8px 8px 0 0; +} +.providers { + justify-content: center; + padding: 0.8em; +} +.providers .provider { + min-height: 2em; +} +.providers .provider a.link { + text-decoration: none; + max-height: 2em; +} +.providers .provider .link div { + background-color: #f7c7867d; + border-radius: 8px; + padding: 6px; + text-align: center; + color: black; + font-weight: bold; + cursor: pointer; +} +.providers .provider .hint { + font-size: 80%; + max-width: 13em; +} +.providers .error { + padding: 3px 6px; + font-weight: bold; + flex: 1 1 auto; +} +.content .links-to-check { + display: flex; + justify-content: center; + gap: 0.5em; + flex-flow: wrap; +} +.content .links-to-check button { + color: black; + padding: 5px 10px; + text-decoration: none; + border-radius: 8px; + border: none; + cursor: pointer; +} +.content .links-to-check span { + margin: auto; +} + +.token { + overflow-wrap: anywhere; + font-family: monospace; +} + +.resource { + padding: 0.5em; + display: flex; + gap: 0.5em; + flex-direction: column; + width: fit-content; + align-items: center; + margin: 5px auto; + box-shadow: 0px 0px 10px #90c3eeA0; + background-color: #90c3eeA0; + border-radius: 8px; +} + +.resources { + display: flex; +} + +.resource { + text-align: center; +} + +.token-info { + margin: 0 1em; +} + +.key { + font-weight: bold; +} + +.token .key, .token .value { + display: inline; +} +.token .value { + padding-left: 1em; +} + +.msg { + text-align: center; + font-weight: bold; +} diff --git a/src/App.vue b/src/App.vue index 40cefaf..006f88b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,40 +1,100 @@ - - diff --git a/src/ResourceButton.vue b/src/ResourceButton.vue new file mode 100644 index 0000000..8c787cc --- /dev/null +++ b/src/ResourceButton.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/ResourceResponse.vue b/src/ResourceResponse.vue new file mode 100644 index 0000000..63f9f24 --- /dev/null +++ b/src/ResourceResponse.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/TokenView.vue b/src/TokenView.vue new file mode 100644 index 0000000..b8287bf --- /dev/null +++ b/src/TokenView.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/UserInfo.vue b/src/UserInfo.vue new file mode 100644 index 0000000..cfc1c31 --- /dev/null +++ b/src/UserInfo.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/main.ts b/src/main.ts index 48d8916..459b1b7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,37 +1,120 @@ import { createApp } from 'vue' import Keycloak from "keycloak-js" -import VueKeycloakJs, { useKeycloak } from '@dsb-norge/vue-keycloak-js' -import axios from 'axios' -import App from './App.vue' +import VueKeycloakJs from '@dsb-norge/vue-keycloak-js' +import axios, { Axios, type AxiosInstance } from 'axios' +import App from '@/App.vue' -export const HTTP = axios.create({ - baseURL: '/', - timeout: 10_000 -}) +export interface Resource { + name: string + default_resource_id: string + role_required: string + scope_required: string +} -function initializeTokenInterceptor() { - HTTP.interceptors.request.use(config => { - const keycloak = useKeycloak() - if (keycloak.authenticated) { - config.headers.Authorization = `Bearer ${keycloak.token}` +export interface Resources { + [name: string]: Resource +} + +interface ResourceProvider { + id: string + name: string + baseUrl: string + verifySSL: boolean + resources: Resources +} + +export interface ResourceProviders { + [name: string]: ResourceProvider +} + +interface Settings { + keycloakUri: string + realm: string + clientId: string + sso: boolean + resourceServerUrl: string + resourceScopes: string[] + authProvider: string + tokenSandbox: boolean + resourceProviders: ResourceProviders +} + +interface AxiosResourceProviders { + [name: string]: AxiosInstance +} + +export let settings: Settings +export let authServer: AxiosInstance +export let resourceServer: AxiosInstance +export let axiosResourceProviders: AxiosResourceProviders = {} + +// The settings.json file is expected at the server's base url +axios.get("settings.json").then().then( + resp => { + settings = resp.data + resourceServer = axios.create({ + baseURL: settings.resourceServerUrl, + timeout: 10000 + }) + authServer = axios.create({ + baseURL: '/', + timeout: 10000 + }) + app.use(VueKeycloakJs, { + config: { + url: settings.keycloakUri, + realm: settings.realm, + clientId: settings.clientId + }, + init: { + onLoad: settings.sso ? "check-sso" : "login-required", + scope: "openid email profile " + settings.resourceScopes.join(" ") + }, + onReady(keycloak: Keycloak) { + initializeTokenInterceptor(keycloak) + app.mount("#app") + }, + }) + } +) + +function initializeTokenInterceptor(keycloak: Keycloak) { + Object.entries(settings.resourceProviders).forEach( + ([id, resourceProvider]) => { + const rp = axios.create({ + baseURL: resourceProvider.baseUrl, + timeout: 10000 + }) + rp.interceptors.request.use(axiosSettings => { + if (keycloak.authenticated) { + axiosSettings.headers.Authorization = `Bearer ${keycloak.token}` + axiosSettings.headers.auth_provider = settings.authProvider + } + return axiosSettings + }, error => { + return Promise.reject(error) + }) + axiosResourceProviders[id] = rp } - return config + ) + authServer.interceptors.request.use(axiosSettings => { + if (keycloak.authenticated) { + axiosSettings.headers.Authorization = `Bearer ${keycloak.token}` + axiosSettings.headers.auth_provider = settings.authProvider + } + return axiosSettings + }, error => { + return Promise.reject(error) + }) + resourceServer.interceptors.request.use(axiosSettings => { + if (keycloak.authenticated) { + axiosSettings.headers.Authorization = `Bearer ${keycloak.token}` + axiosSettings.headers.auth_provider = settings.authProvider + } + return axiosSettings }, error => { return Promise.reject(error) }) } -createApp(App) - .use(VueKeycloakJs, { - config: { - url: 'https://philo.ydns.eu/auth/', - realm: 'test', - clientId: 'oidc-test-web', - }, - onReady(kk: Keycloak, vkk) { - //console.log(kk, vkk) - console.log(kk.idTokenParsed) - initializeTokenInterceptor() - }, - }) - .mount('#app') +const app = createApp(App) diff --git a/tsconfig.app.json b/tsconfig.app.json index 913b8f2..8bd162f 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,12 +1,20 @@ { "extends": "@vue/tsconfig/tsconfig.dom.json", - "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], - "exclude": ["src/**/__tests__/*"], + "include": [ + "env.d.ts", + "src/**/*", + "src/**/*.vue", + "config.ts" + ], + "exclude": [ + "src/**/__tests__/*" + ], "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } } } diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..50976ed --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ], + "compilerOptions": { + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": [ + "node", + "vite/client" + ] + } +} diff --git a/vite.config.ts b/vite.config.ts index 2e3ef22..9b20b48 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,29 +1,44 @@ +// +// Webcrypto is required for docoding the tokens, and thus https is compulsary. +// +// Read an environment file (.env) at the project's root, +// with 2 variables VITE_SSL_KEY and VITE_SSL_CERT defining the location +// of the ssl key and cert +// + import { fileURLToPath, URL } from 'node:url' -import { defineConfig } from 'vite' +import { defineConfig, UserConfig, loadEnv } from 'vite' import fs from 'fs'; import path from 'path'; import vue from '@vitejs/plugin-vue' -import vueDevTools from 'vite-plugin-vue-devtools' +//import vueDevTools from 'vite-plugin-vue-devtools' -// https://vite.dev/config/ -export default defineConfig({ +// Read the env file +const env = loadEnv("dev", ".") + +let baseSettings: UserConfig = { plugins: [ vue(), - vueDevTools(), + //vueDevTools(), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }, }, - server: { +} + +if (env.VITE_SSL_CERT && env.VITE_SSL_KEY) { + baseSettings['server'] = { + hmr: false, https: { - // key: fs.readFileSync(path.resolve(__dirname, 'localhost-key.pem')), - // cert: fs.readFileSync(path.resolve(__dirname, 'localhost.pem')), - key: fs.readFileSync(path.resolve("/home/phil", 'tiptop+4-key.pem')), - cert: fs.readFileSync(path.resolve("/home/phil", 'tiptop+4.pem')), - }, + key: fs.readFileSync(path.resolve(env.VITE_SSL_KEY)), + cert: fs.readFileSync(path.resolve(env.VITE_SSL_CERT)) + } } -}) +} + +// https://vite.dev/config/ +export default defineConfig(baseSettings)