Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 18 additions & 73 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
node_modules
# Dependencies
node_modules/
.pnpm-store/

# Nuxt build output
.nuxt/
.output/
.nitro/
.cache/
dist/

# TypeScript
tsconfig.tsbuildinfo
tsconfig.build.tsbuildinfo

Expand All @@ -13,92 +23,27 @@ tsconfig.build.tsbuildinfo
/logs
*.log

npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm
# Test coverage
coverage/

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# parcel-bundler cache (https://parceljs.org/)
.cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# Nuxt generate
dist

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless

# IDE / Editor
.idea

# Service worker
sw.*
*.swp

# macOS
.DS_Store

# Vim swap files
*.swp
# Nx
.nx
.output

# Claude
# Claude and agent tooling
.claude/
.playwright-mcp/
docs/
.superpowers/
docs/
113 changes: 86 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,89 @@ website, the administrators have access to an overview of the submitted projects
the data at the data center, as well as have full control over the individual containers running on their systems and
manage which projects/analyses have access to the various datasets.

## Required Environment Variables
It is a [Nuxt 4](https://nuxt.com) single-page application built with [PrimeVue 4](https://primevue.org),
Tailwind CSS 4 and Pinia. Authentication is handled by [@sidebase/nuxt-auth](https://auth.sidebase.io) (Auth.js) through
the server route `server/routes/flame/api/auth/[...].ts`.

This frontend requires the following environment variables to be set. The SSR framework used for this project (Nuxt)
in addition to the OIDC library (nuxt-oidc-auth) requires the keycloak-related environment variables to be precisely
named and explicitly defined in order to overwrite the default configuration during runtime
(i.e. when deployed in a container).
## Development

```dotenv
NUXT_PUBLIC_BASE_URL="http://localhost:3000" # URL of the website
NUXT_PUBLIC_HUB_ADAPTER_URL="http://urlForHubAdapterApi.de" # URL for hub adapter API
NUXT_PUBLIC_ORIGIN=$NUXT_PUBLIC_BASE_URL/api/auth # NO QUOTES! Relative path to sign page (you likely should not modify this)
Requires Node.js 24 and pnpm (version pinned in `package.json` via `packageManager`; `corepack enable` will pick it up).

```bash
pnpm install # also runs `nuxt prepare`
pnpm dev # http://localhost:3000, reads .env (see Environment Variables)
```

NUXT_IDP_CLIENT_ID=node-ui # Client ID defined in the IDP provided in NUXT_PUBLIC_IDP_ISSUER
NUXT_IDP_CLIENT_SECRET=xxx
NUXT_PUBLIC_IDP_PROVIDER="keycloak" # Can be either 'keycloak' or 'authentik'
NUXT_PUBLIC_IDP_ISSUER="http://my-keycloak.com/realms/flame"
| Command | Purpose |
|------------------|-------------------------------------------------------------------------------|
| `pnpm dev` | Start the dev server with hot reload |
| `pnpm build` | Production build into `.output/` (runs type checking) |
| `pnpm prod` | Serve the production build (`node .output/server/index.mjs`) |
| `pnpm test` | Run the Vitest suite in watch mode (`pnpm test --run` for a single pass) |
| `pnpm coverage` | Run the tests with coverage |
| `pnpm lint` | Lint with ESLint (`pnpm lint:fix` to apply automatic fixes) |
| `pnpm typecheck` | Type check the app, server and tests |
| `pnpm build-api` | Regenerate `app/services/Api.ts` from `app/services/hub_adapter_swagger.json` |

# If not using the same keycloak instance as the other FLAME Node services for user authentication, then this needs to be set
NUXT_PUBLIC_INTERNAL_KEYCLOAK_URL=""
### Project layout

NUXT_AUTH_SECRET=xxx # Unique key for encrypting JWT
```
app/
assets/ global CSS (main.css is the entry) and the PrimeVue theme preset
components/ UI components, auto-imported by file name
composables/ use* functions that rely on Vue/Nuxt context
layouts/ page shell (header, footer)
pages/ file-based routes
plugins/ Nuxt plugins (hub adapter $fetch client, node settings)
services/ generated hub adapter API types (do not edit Api.ts by hand)
stores/ Pinia stores
types/ shared app types
utils/ pure helper functions
server/ Nitro routes: auth handler and health endpoints
shared/types/ type augmentations shared by app and server
test/ Vitest specs mirroring app/, plus MSW mocks in test/mockapi
```

## Environment Variables

| EnvVar | Description | Default | Required |
|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------|--------------------------------|:--------:|
| NUXT_PUBLIC_BASE_URL | URL of the website | | x |
| NUXT_PUBLIC_HUB_ADAPTER_URL | URL for hub adapter API | | x |
| NUXT_PUBLIC_ORIGIN | Relative path to sign page (you likely should not modify this) | $NUXT_PUBLIC_BASE_URL/api/auth | |
| NUXT_IDP_CLIENT_SECRET | Client secret defined in the IDP provided in NUXT_PUBLIC_IDP_ISSUER | | x |
| NUXT_PUBLIC_IDP_PROVIDER | Can be either 'keycloak', 'auth0', 'authentik', 'onelogin', 'okta', or 'zitadel' | keycloak | |
| NUXT_PUBLIC_IDP_ISSUER | URI for the IDP used to identify itself using OpenID Connect (OIDC) protocol | | x |
| NUXT_PUBLIC_INTERNAL_KEYCLOAK_URL | If not using the same keycloak instance as the other FLAME Node services for user authentication, then this needs to be set | | |
| NUXT_AUTH_SECRET | Unique key for encrypting JWT, see [Setting a Secret below](#setting-a-secret) | | x |
Variables prefixed with `NUXT_` override the Nuxt runtime config, so all of them can be changed at container start
without rebuilding the image. The auth server route reads them directly from the environment.

```dotenv
NUXT_PUBLIC_BASE_URL="http://localhost:3000"
NUXT_PUBLIC_ORIGIN="http://localhost:3000/flame/api/auth"
NUXT_PUBLIC_HUB_ADAPTER_URL="http://localhost:5000"

NUXT_PUBLIC_IDP_PROVIDER="hub"
NUXT_PUBLIC_IDP_ISSUER="https://auth.example.org"
NUXT_IDP_CLIENT_ID="node-ui"
NUXT_IDP_CLIENT_SECRET="xxx"

NUXT_AUTH_SECRET="xxx" # see "Setting a Secret" below
```

### Required

| Variable | Description |
|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
| `NUXT_PUBLIC_BASE_URL` | Public URL of this website. Default: `http://localhost:3000` |
| `NUXT_PUBLIC_ORIGIN` | Full public URL of the auth endpoint, i.e. `$NUXT_PUBLIC_BASE_URL/flame/api/auth`. Default: `http://localhost:3000/flame/api/auth` |
| `NUXT_PUBLIC_HUB_ADAPTER_URL` | URL of the hub adapter API. Used by the browser and by the auth route. Default: `http://localhost:5000` |
| `NUXT_PUBLIC_IDP_PROVIDER` | Identity provider type: `hub`, `keycloak`, `authentik`, `onelogin`, `okta` or `zitadel`. Default: `hub` |
| `NUXT_PUBLIC_IDP_ISSUER` | OIDC issuer URL of the identity provider, e.g. `https://keycloak.example.org/realms/flame`. Default: `https://auth.hub.privateaim.net/realms/master` |
| `NUXT_IDP_CLIENT_SECRET` | Client secret of the OIDC client registered at the identity provider |
| `NUXT_AUTH_SECRET` | Secret used to sign and encrypt the session JWT, see [Setting a Secret](#setting-a-secret) |

### Optional

| Variable | Description | Default |
|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|
| `NUXT_IDP_CLIENT_ID` | Client ID of the OIDC client registered at the identity provider | `node-ui` |
| `NUXT_PUBLIC_INTERNAL_KEYCLOAK_URL` | Internal (cluster) URL of the identity provider used for server-side token, userinfo and JWKS requests when it differs from the public issuer. Not used with the `hub` provider | value of `NUXT_PUBLIC_IDP_ISSUER` |
| `NUXT_IDP_HEALTH_TIMEOUT_MS` | Timeout for the identity provider reachability check at `/flame/api/health`. Keep it below 3500, the Auth.js request timeout | `3000` |
| `NUXT_PUBLIC_VICTORIA_LOGS_URL` | URL of a VictoriaLogs instance. When set, a "VictoriaLogs" link appears in the user menu | |
| `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` | Outbound proxy for server-side requests to the identity provider and hub adapter | |
| `NUXT_HOST`, `NUXT_PORT` | Address and port the server listens on. The Docker images set `0.0.0.0` and `3000` | |

## Setting a Secret

Expand All @@ -46,4 +96,13 @@ sign/encrypt cookies, and to generate cryptographic keys. To quickly generate su

```bash
openssl rand -base64 32
```
```

## Docker

`prod.Dockerfile` builds the production image used by CI:

```bash
docker build -f prod.Dockerfile -t node-ui .
docker run --env-file .env -p 3000:3000 node-ui
```
42 changes: 4 additions & 38 deletions app/app.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
<script lang="ts" setup>
import MenuHeader from "~/components/header/MenuHeader.vue";
import Footer from "~/components/Footer.vue";
import type { ToastMessageOptions } from "primevue/toast";

const pauseToastTimer = () => {};

const toastPt = {
message: ({ props }: { props: { message: ToastMessageOptions } }) => {
const life = props.message.life;
Expand All @@ -16,39 +12,9 @@ const toastPt = {
</script>

<template>
<Toast
position="top-right"
:pt="toastPt"
:onMouseEnter="pauseToastTimer"
:onMouseLeave="pauseToastTimer"
/>
<Toast position="top-right" :pt="toastPt" />
<NuxtLoadingIndicator color="var(--p-primary-color)" />
<div class="app-container">
<MenuHeader />
<NuxtLayout>
<main class="page-content">
<NuxtPage />
</main>
</NuxtLayout>
<Footer />
</div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

<style>
.app-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}

.page-content {
flex: 1 0 auto;
padding: var(--page-padding-y) var(--page-padding-x);
}

@media (max-width: 768px) {
.page-content {
padding: 1rem;
}
}
</style>
8 changes: 4 additions & 4 deletions app/assets/css/main.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
@import "tailwindcss";
@import "./elements.css";
@import "./table.css";
@import "./preferences.css";
@import "./toast.css";

html {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
Expand All @@ -23,7 +27,3 @@ html {
gap: 1rem;
margin-bottom: 2rem;
}

.page-description {

}
Loading
Loading