Skip to content

Commit 931b298

Browse files
kruscheclaude
andcommitted
Merge develop into chore/modularize-code
Resolves conflicts from develop (passkey #955, hide-expired-topics #1089, dependency updates, react-router v8) against the feature-package modularization. Resolution summary: - Imports: kept the modularized `@/` (client) and feature-package (server) import style; folded in develop's newly-added imports converted to the same style. - New passkey files added by develop landed in the old flat layout and were relocated into the modularized structure with `@/` imports: - components/PasskeyRegistrationPrompt -> core/components/PasskeyRegistrationPrompt - pages/SettingsPage/components/PasskeySettings -> core/admin/pages/SettingsPage/components/PasskeySettings - utils/passkey.ts (git-relocated) -> core/utils/passkey.ts - CollapsibleTopicElement: took develop's canonical #1089 behavior (`canApply = !!fullTopic && state === OPEN`). - BaseKeycloakIntegrationTest: kept in mock/ (consistent with BaseIntegrationTest) with modularized repository imports; TestContainerImages is same-package. Verified: client build/tsc/lint/tests green; server compiles + 864/864 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 parents 4ff25f4 + dcaea10 commit 931b298

61 files changed

Lines changed: 12643 additions & 5102 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Docker Compose environment file.
2+
#
3+
# This file is COMMITTED on purpose so the workspace sets up automatically: `docker compose up -d`
4+
# works out of the box with no manual steps (Docker Compose loads `.env` automatically).
5+
#
6+
# RULE: non-secret build/config values ONLY. Never commit credentials, tokens, or other secrets
7+
# here. Deployment secrets belong in an uncommitted env file (e.g. `.env.prod`, `.env.local`),
8+
# which stays gitignored. To override a value locally without committing, set it in your shell or
9+
# pass `--env-file`.
10+
11+
# PostgreSQL image tag. Single source of truth, read by both the docker-compose files and the
12+
# Gradle build (integration-test Testcontainers parse this file). All consumers fall back to the
13+
# same tag if this variable is unset, so the workspace still builds even without this file. The
14+
# e2e GitHub workflow uses its own repository variable — see docs/DATABASE.md.
15+
POSTGRES_IMAGE_TAG=18.4-alpine

.github/workflows/build_docker.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
server_image_tag: "${{ steps.output-tag.outputs.server_image_tag }}"
2222
steps:
2323
- name: Checkout
24-
uses: actions/checkout@v6
24+
uses: actions/checkout@v7
2525
with:
2626
ref: ${{ github.head_ref || github.ref }}
2727
fetch-depth: 1
@@ -95,7 +95,7 @@ jobs:
9595
client_image_tag: "${{ steps.output-tag.outputs.client_image_tag }}"
9696
steps:
9797
- name: Checkout
98-
uses: actions/checkout@v6
98+
uses: actions/checkout@v7
9999
with:
100100
ref: ${{ github.head_ref || github.ref }}
101101
fetch-depth: 1

.github/workflows/deploy_docker.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
url: '${{ vars.CLIENT_HOST }}'
4343
steps:
4444
- name: Checkout Code
45-
uses: actions/checkout@v6
45+
uses: actions/checkout@v7
4646

4747
- name: Create main.cf and master.cf File
4848
env:
@@ -110,6 +110,9 @@ jobs:
110110
KEYCLOAK_CLIENT_ID: ${{ vars.KEYCLOAK_CLIENT_ID }}
111111
KEYCLOAK_SERVICE_CLIENT_ID: ${{ vars.KEYCLOAK_SERVICE_CLIENT_ID }}
112112
KEYCLOAK_SERVICE_CLIENT_SECRET: ${{ secrets.KEYCLOAK_SERVICE_CLIENT_SECRET }}
113+
PASSKEY_RP_ID: ${{ vars.PASSKEY_RP_ID }}
114+
PASSKEY_RP_NAME: ${{ vars.PASSKEY_RP_NAME }}
115+
PASSKEY_PROMPT_APPS: ${{ vars.PASSKEY_PROMPT_APPS }}
113116
with:
114117
host: ${{ vars.VM_HOST }}
115118
username: ${{ vars.VM_USERNAME }}
@@ -118,7 +121,7 @@ jobs:
118121
proxy_username: ${{ vars.DEPLOYMENT_GATEWAY_USER }}
119122
proxy_key: ${{ secrets.DEPLOYMENT_GATEWAY_SSH_KEY }}
120123
proxy_port: ${{ vars.DEPLOYMENT_GATEWAY_PORT }}
121-
envs: SERVER_TAG,CLIENT_TAG,SPRING_DATASOURCE_DATABASE,SPRING_DATASOURCE_USERNAME,SPRING_DATASOURCE_PASSWORD,APP_HOSTNAME,SERVER_HOST,CLIENT_HOST,APPLICATION_TITLE,CHAIR_NAME,CHAIR_URL,ALLOW_SUGGESTED_TOPICS,THESIS_TYPES,STUDY_PROGRAMS,STUDY_DEGREES,GENDERS,LANGUAGES,CUSTOM_DATA,THESIS_FILES,MAIL_SENDER,KEYCLOAK_HOST,KEYCLOAK_REALM_NAME,KEYCLOAK_CLIENT_ID,KEYCLOAK_SERVICE_CLIENT_ID,KEYCLOAK_SERVICE_CLIENT_SECRET
124+
envs: SERVER_TAG,CLIENT_TAG,SPRING_DATASOURCE_DATABASE,SPRING_DATASOURCE_USERNAME,SPRING_DATASOURCE_PASSWORD,APP_HOSTNAME,SERVER_HOST,CLIENT_HOST,APPLICATION_TITLE,CHAIR_NAME,CHAIR_URL,ALLOW_SUGGESTED_TOPICS,THESIS_TYPES,STUDY_PROGRAMS,STUDY_DEGREES,GENDERS,LANGUAGES,CUSTOM_DATA,THESIS_FILES,MAIL_SENDER,KEYCLOAK_HOST,KEYCLOAK_REALM_NAME,KEYCLOAK_CLIENT_ID,KEYCLOAK_SERVICE_CLIENT_ID,KEYCLOAK_SERVICE_CLIENT_SECRET,PASSKEY_RP_ID,PASSKEY_RP_NAME,PASSKEY_PROMPT_APPS
122125
script: |
123126
rm -f .env.prod
124127
cat > .env.prod << ENVEOF
@@ -145,6 +148,9 @@ jobs:
145148
KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID}
146149
KEYCLOAK_SERVICE_CLIENT_ID=${KEYCLOAK_SERVICE_CLIENT_ID}
147150
KEYCLOAK_SERVICE_CLIENT_SECRET=${KEYCLOAK_SERVICE_CLIENT_SECRET}
151+
PASSKEY_RP_ID=${PASSKEY_RP_ID}
152+
PASSKEY_RP_NAME=${PASSKEY_RP_NAME}
153+
PASSKEY_PROMPT_APPS=${PASSKEY_PROMPT_APPS}
148154
SERVER_IMAGE_TAG=${SERVER_TAG:-latest}
149155
CLIENT_IMAGE_TAG=${CLIENT_TAG:-latest}
150156
ENVEOF

.github/workflows/e2e_tests.yml

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313

1414
services:
1515
postgres:
16-
image: postgres:18.4-alpine
16+
image: postgres:${{ vars.POSTGRES_IMAGE_TAG || '18.4-alpine' }}
1717
env:
1818
POSTGRES_USER: thesis-management-postgres
1919
POSTGRES_PASSWORD: thesis-management-postgres
@@ -27,7 +27,7 @@ jobs:
2727
--health-retries 10
2828
2929
mailpit:
30-
image: axllent/mailpit:v1.30
30+
image: axllent/mailpit:v1.30.2
3131
ports:
3232
- 1125:1025
3333
- 8125:8025
@@ -39,26 +39,29 @@ jobs:
3939
4040
steps:
4141
- name: Checkout
42-
uses: actions/checkout@v6
42+
uses: actions/checkout@v7
4343
with:
4444
ref: ${{ github.head_ref || github.ref }}
4545
fetch-depth: 1
4646

47-
# Start Keycloak manually (service containers don't support custom commands)
47+
# Build + start Keycloak from the repo Dockerfile used in local compose.
4848
- name: Start Keycloak
4949
run: |
50+
docker build -t thesis-management-keycloak-ci -f keycloak/Dockerfile .
51+
5052
docker run -d --name keycloak \
5153
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
5254
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
5355
-p 8181:8080 \
54-
quay.io/keycloak/keycloak:26.5 \
55-
start-dev
56+
-v "$PWD/keycloak/thesis-management-realm.json:/opt/keycloak/data/import/thesis-management-realm.json:ro" \
57+
thesis-management-keycloak-ci \
58+
start-dev --import-realm
5659
5760
- name: Wait for Keycloak to be ready
5861
run: |
5962
echo "Waiting for Keycloak..."
6063
for i in $(seq 1 60); do
61-
if curl -sf http://localhost:8181/realms/master > /dev/null 2>&1; then
64+
if curl -sf http://localhost:8181/realms/thesis-management > /dev/null 2>&1; then
6265
echo "Keycloak is ready"
6366
break
6467
fi
@@ -70,20 +73,38 @@ jobs:
7073
sleep 2
7174
done
7275
73-
- name: Import Keycloak realm
76+
- name: Verify Keycloak passkey provider
7477
run: |
75-
# Get admin token
76-
TOKEN=$(curl -sf -X POST "http://localhost:8181/realms/master/protocol/openid-connect/token" \
77-
-H "Content-Type: application/x-www-form-urlencoded" \
78-
-d "username=admin&password=admin&grant_type=password&client_id=admin-cli" | jq -r '.access_token')
79-
80-
# Import realm
81-
curl -sf -X POST "http://localhost:8181/admin/realms" \
82-
-H "Authorization: Bearer $TOKEN" \
83-
-H "Content-Type: application/json" \
84-
-d @keycloak/thesis-management-realm.json
85-
86-
echo "Realm imported successfully"
78+
echo "Waiting for Keycloak passkey provider..."
79+
headers_file="$(mktemp)"
80+
body_file="$(mktemp)"
81+
for i in $(seq 1 60); do
82+
status="$(
83+
curl -sS \
84+
-D "$headers_file" \
85+
-o "$body_file" \
86+
-w "%{http_code}" \
87+
-H "Origin: http://localhost:3100" \
88+
http://localhost:8181/realms/thesis-management/passkey/thesis-management-app/health || true
89+
)"
90+
if [[ "$status" =~ ^2[0-9][0-9]$ ]] \
91+
&& grep -qi '^access-control-allow-origin: http://localhost:3100' "$headers_file" \
92+
&& grep -qi '^access-control-allow-credentials: true' "$headers_file"; then
93+
echo "Keycloak passkey provider is ready"
94+
break
95+
fi
96+
if [ "$i" -eq 60 ]; then
97+
echo "::error::Keycloak passkey provider did not become available with credentialed CORS for http://localhost:3100"
98+
echo "Last HTTP status: ${status:-none}"
99+
echo "Last response headers:"
100+
cat "$headers_file"
101+
echo "Last response body:"
102+
cat "$body_file"
103+
docker logs keycloak
104+
exit 1
105+
fi
106+
sleep 2
107+
done
87108
88109
# Set up Java for server
89110
- name: Set up JDK 25
@@ -118,7 +139,7 @@ jobs:
118139
- name: Set up Node.js
119140
uses: actions/setup-node@v6
120141
with:
121-
node-version: '24.16.0'
142+
node-version: '24.17.0'
122143
cache: 'pnpm'
123144
cache-dependency-path: client/pnpm-lock.yaml
124145

@@ -153,7 +174,7 @@ jobs:
153174

154175
- name: Serve client static bundle
155176
working-directory: ./client
156-
run: pnpm dlx serve@14 -s build -l 3100 --no-clipboard --no-port-switching &
177+
run: pnpm dlx serve@14 -s build -l 3100 -c ../serve.e2e.json --no-clipboard --no-port-switching &
157178

158179
# Wait for both services to be ready
159180
- name: Wait for server to be ready

.github/workflows/refresh_sbom.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ jobs:
116116
fi
117117
118118
- name: Checkout PR branch (read-only, no token persisted)
119-
uses: actions/checkout@v6
119+
uses: actions/checkout@v7
120120
with:
121121
# pull_request_target defaults to the base ref; we want the PR head.
122122
ref: ${{ github.event.pull_request.head.sha }}
@@ -177,7 +177,7 @@ jobs:
177177
if: steps.scopes.outputs.client == 'true'
178178
uses: actions/setup-node@v6
179179
with:
180-
node-version: '24.16.0'
180+
node-version: '24.17.0'
181181

182182
- name: Regenerate client SBOM
183183
if: steps.scopes.outputs.client == 'true'

.github/workflows/run_tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- name: Checkout
14-
uses: actions/checkout@v6
14+
uses: actions/checkout@v7
1515
with:
1616
ref: ${{ github.head_ref || github.ref }}
1717
fetch-depth: 1
@@ -57,7 +57,7 @@ jobs:
5757
- name: Set up Node.js
5858
uses: actions/setup-node@v6
5959
with:
60-
node-version: '24.16.0'
60+
node-version: '24.17.0'
6161
cache: 'pnpm'
6262
cache-dependency-path: 'client/pnpm-lock.yaml'
6363

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.env*
2+
!.env
23

34
db_backups
45
uploads

client.Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:24.16.0-alpine AS build
1+
FROM node:24.17.0-alpine AS build
22
WORKDIR /app
33
ENV CI=1
44
ENV HUSKY=0

client/e2e/admin/data-export.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,15 @@ test.describe('Data Export - Route Protection (authenticated)', () => {
131131
test.describe('Data Export - Route Protection (unauthenticated)', () => {
132132
test.use({ storageState: { cookies: [], origins: [] } })
133133

134-
test('unauthenticated users are redirected from data export page', async ({ page }) => {
134+
test('unauthenticated users see the login modal on data export page', async ({ page }) => {
135135
await page.goto('/data-export', { waitUntil: 'domcontentloaded', timeout: 30_000 })
136136

137-
// Should redirect to login — the Data Export heading should not be visible
137+
await expect(page).toHaveURL(/\/data-export/)
138+
const loginModal = page.getByRole('dialog', { name: 'Login' })
139+
await expect(loginModal).toBeVisible({ timeout: 30_000 })
140+
await expect(loginModal.getByRole('button', { name: 'Login' })).toBeVisible()
141+
142+
// Protected page content should not be visible while unauthenticated.
138143
await expect(page.getByRole('heading', { name: 'Data Export' })).not.toBeVisible({
139144
timeout: 15_000,
140145
})

0 commit comments

Comments
 (0)