19 Commits

Author SHA1 Message Date
WorkClub Automation
9cb80e4517 fix(auth): restore keycloak sign-in for NodePort access
All checks were successful
CI Pipeline / Backend Build & Test (push) Successful in 58s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 28s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
Trust external host for Auth.js, provide missing frontend auth env/secrets, and submit a proper CSRF-backed sign-in POST so browser login reaches Keycloak reliably.
2026-03-13 06:52:18 +01:00
WorkClub Automation
d4f09295be feat(k8s): expose workclub services via LAN NodePorts
Expose frontend, API, and Keycloak on stable NodePorts and align app/keycloak external URLs for local-network browser access.
2026-03-13 06:33:50 +01:00
WorkClub Automation
eaa163afa4 fix(k8s): stabilize keycloak rollout and align CD deploy manifests
Update Keycloak probe/realm import behavior and authority config so auth services start reliably on the dev cluster, while keeping CD deployment steps aligned with the actual Kubernetes overlay behavior.
2026-03-13 06:25:07 +01:00
WorkClub Automation
7272358746 fix(k8s): extreme probe timeouts for RPi and final Keycloak 26 admin fix
All checks were successful
CI Pipeline / Backend Build & Test (push) Successful in 51s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 28s
CI Pipeline / Infrastructure Validation (push) Successful in 3s
2026-03-10 22:22:36 +01:00
WorkClub Automation
9b1ceb1fb4 fix(k8s): fix image names, keycloak 26 envs, and bump resource limits for RPi
All checks were successful
CI Pipeline / Backend Build & Test (push) Successful in 52s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 42s
CI Pipeline / Infrastructure Validation (push) Successful in 5s
2026-03-10 22:16:31 +01:00
WorkClub Automation
90ae752652 fix(k8s): enable keycloak health endpoints and increase probe delays
All checks were successful
CI Pipeline / Backend Build & Test (push) Successful in 1m2s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 29s
CI Pipeline / Infrastructure Validation (push) Successful in 3s
2026-03-10 22:07:02 +01:00
WorkClub Automation
3c41f0e40c fix(k8s): use args instead of command for keycloak to allow default entrypoint
All checks were successful
CI Pipeline / Backend Build & Test (push) Successful in 1m19s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 26s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
2026-03-10 22:02:48 +01:00
WorkClub Automation
fce8b28114 fix(cd): force delete postgres statefulset to allow storage changes
All checks were successful
CI Pipeline / Backend Build & Test (push) Successful in 57s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 34s
CI Pipeline / Infrastructure Validation (push) Successful in 5s
2026-03-10 21:54:26 +01:00
WorkClub Automation
b204f6aa32 fix(k8s): register secrets and postgres-patch in dev kustomization
Some checks failed
CI Pipeline / Frontend Lint, Test & Build (push) Has been cancelled
CI Pipeline / Infrastructure Validation (push) Has been cancelled
CI Pipeline / Backend Build & Test (push) Has been cancelled
2026-03-10 21:42:31 +01:00
WorkClub Automation
0a4d99b65b fix(k8s): add dev secrets and use emptyDir for postgres on storage-less cluster
Some checks failed
CI Pipeline / Frontend Lint, Test & Build (push) Has been cancelled
CI Pipeline / Infrastructure Validation (push) Has been cancelled
CI Pipeline / Backend Build & Test (push) Has been cancelled
2026-03-10 21:18:19 +01:00
WorkClub Automation
c9841d6cfc fix(cd): ensure workclub-dev namespace exists before deployment
All checks were successful
CI Pipeline / Backend Build & Test (push) Successful in 59s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 26s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
2026-03-10 20:40:29 +01:00
WorkClub Automation
641a6d0af0 fix(cd): use dynamic KUBECONFIG path and enhanced context diagnostics
Some checks failed
CI Pipeline / Frontend Lint, Test & Build (push) Has been cancelled
CI Pipeline / Infrastructure Validation (push) Has been cancelled
CI Pipeline / Backend Build & Test (push) Has been cancelled
2026-03-10 20:38:21 +01:00
WorkClub Automation
b1c351e936 fix(cd): use printf for robust KUBECONFIG writing and add diagnostics
Some checks failed
CI Pipeline / Frontend Lint, Test & Build (push) Has been cancelled
CI Pipeline / Infrastructure Validation (push) Has been cancelled
CI Pipeline / Backend Build & Test (push) Has been cancelled
2026-03-10 20:35:12 +01:00
WorkClub Automation
df625f3b3a Next try fixing the deployment pipeline
Some checks failed
CI Pipeline / Frontend Lint, Test & Build (push) Has been cancelled
CI Pipeline / Infrastructure Validation (push) Has been cancelled
CI Pipeline / Backend Build & Test (push) Has been cancelled
2026-03-10 20:32:48 +01:00
WorkClub Automation
b028c06636 Fix for Deployment, install kubectl
Some checks failed
CI Pipeline / Frontend Lint, Test & Build (push) Has been cancelled
CI Pipeline / Infrastructure Validation (push) Has been cancelled
CI Pipeline / Backend Build & Test (push) Has been cancelled
2026-03-10 20:29:28 +01:00
WorkClub Automation
9f4bea36fe fix(cd): use robust manual kubectl setup to avoid base64 truncated input error
Some checks failed
CI Pipeline / Backend Build & Test (push) Failing after 13s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 27s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
2026-03-10 20:25:10 +01:00
WorkClub Automation
c5b3fbe4cb Added Kubernetes Cluster Deployment
Some checks failed
CI Pipeline / Backend Build & Test (push) Failing after 55s
CI Pipeline / Frontend Lint, Test & Build (push) Failing after 33s
CI Pipeline / Infrastructure Validation (push) Successful in 9s
2026-03-10 19:58:55 +01:00
WorkClub Automation
4f6d0ae6df chore: remove old screenshot images
All checks were successful
CI Pipeline / Backend Build & Test (push) Successful in 1m1s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 29s
CI Pipeline / Infrastructure Validation (push) Successful in 4s
2026-03-09 17:31:51 +01:00
c6981324d6 Merge pull request 'fix(backend): resolve shift signup by looking up Member via ExternalUserId' (#3) from fix/shift-signup-external-user-lookup into main
All checks were successful
CI Pipeline / Backend Build & Test (push) Successful in 49s
CI Pipeline / Frontend Lint, Test & Build (push) Successful in 28s
CI Pipeline / Infrastructure Validation (push) Successful in 5s
Reviewed-on: #3
2026-03-09 15:56:12 +01:00
21 changed files with 592 additions and 60 deletions

View File

@@ -0,0 +1,97 @@
name: CD Deployment - Kubernetes
on:
workflow_run:
workflows: ["CD Bootstrap - Release Image Publish"]
types: [completed]
branches: [main, develop]
workflow_dispatch:
inputs:
image_tag:
description: 'Image tag to deploy (e.g., latest, dev)'
required: true
default: 'dev'
type: string
jobs:
deploy:
name: Deploy to Kubernetes
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install kubectl
run: |
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
- name: Install Kustomize
run: |
curl -Lo kustomize.tar.gz https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.4.1/kustomize_v5.4.1_linux_amd64.tar.gz
tar -xzf kustomize.tar.gz
chmod +x kustomize
sudo mv kustomize /usr/local/bin/
- name: Set Image Tag
run: |
IMAGE_TAG="${{ github.event.inputs.image_tag }}"
if [[ -z "$IMAGE_TAG" ]]; then
IMAGE_TAG="dev" # Default for auto-trigger
fi
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
- name: Kustomize Edit Image Tag
working-directory: ./infra/k8s/overlays/dev
run: |
kustomize edit set image workclub-api=192.168.241.13:8080/workclub-api:$IMAGE_TAG
kustomize edit set image workclub-frontend=192.168.241.13:8080/workclub-frontend:$IMAGE_TAG
- name: Deploy to Kubernetes
run: |
set -euo pipefail
export KUBECONFIG=$HOME/.kube/config
mkdir -p $HOME/.kube
if echo "${{ secrets.KUBECONFIG }}" | grep -q "apiVersion"; then
echo "Detected plain text KUBECONFIG"
printf '%s' "${{ secrets.KUBECONFIG }}" > $KUBECONFIG
else
echo "Detected base64 KUBECONFIG"
# Handle potential newlines/wrapping in the secret
printf '%s' "${{ secrets.KUBECONFIG }}" | base64 -d > $KUBECONFIG
fi
chmod 600 $KUBECONFIG
kubectl --kubeconfig="$KUBECONFIG" config view >/dev/null
# Diagnostics
echo "Kubeconfig path: $KUBECONFIG"
echo "Kubeconfig size: $(wc -c < $KUBECONFIG) bytes"
echo "Available contexts:"
kubectl --kubeconfig="$KUBECONFIG" config get-contexts
if ! grep -q "current-context" $KUBECONFIG; then
echo "Warning: current-context missing, attempting to fix..."
FIRST_CONTEXT=$(kubectl --kubeconfig="$KUBECONFIG" config get-contexts -o name | head -n 1)
if [ -n "$FIRST_CONTEXT" ]; then
kubectl --kubeconfig="$KUBECONFIG" config use-context "$FIRST_CONTEXT"
fi
fi
echo "Current context: $(kubectl --kubeconfig="$KUBECONFIG" config current-context)"
# Ensure target namespace exists
kubectl --kubeconfig="$KUBECONFIG" create namespace workclub-dev --dry-run=client -o yaml | kubectl --kubeconfig="$KUBECONFIG" apply -f -
# Apply manifests (non-destructive by default; avoid DB state churn)
kubectl --kubeconfig="$KUBECONFIG" config view --minify # Verification of context
kustomize build --load-restrictor LoadRestrictionsNone infra/k8s/overlays/dev | kubectl --kubeconfig="$KUBECONFIG" apply -f -
# Rollout verification
kubectl --kubeconfig="$KUBECONFIG" rollout status statefulset/workclub-postgres -n workclub-dev --timeout=300s
kubectl --kubeconfig="$KUBECONFIG" rollout status deployment/workclub-keycloak -n workclub-dev --timeout=600s
kubectl --kubeconfig="$KUBECONFIG" rollout status deployment/workclub-api -n workclub-dev --timeout=300s
kubectl --kubeconfig="$KUBECONFIG" rollout status deployment/workclub-frontend -n workclub-dev --timeout=300s

View File

@@ -12,6 +12,7 @@
> - Docker Compose for local development (hot reload, Keycloak, PostgreSQL)
> - Kubernetes manifests (Kustomize base + dev overlay)
> - Gitea CI pipeline (`.gitea/workflows/ci.yml`) for backend/frontend/infrastructure validation
> - Gitea CD bootstrap + deployment pipelines (`.gitea/workflows/cd-bootstrap.yml`, `.gitea/workflows/cd-deploy.yml`)
> - Comprehensive TDD test suite (xUnit + Testcontainers, Vitest + RTL, Playwright E2E)
> - Seed data for development (2 clubs, 5 users, sample tasks + shifts)
>
@@ -36,7 +37,7 @@ Build a multi-tenant internet application for managing work items over several m
- **Testing**: TDD approach (tests first).
- **Notifications**: None for MVP.
- **CI extension**: Add Gitea-hosted CI pipeline for this repository.
- **Pipeline scope**: CI-only (build/test/lint/manifest validation), no auto-deploy in this iteration.
- **Pipeline scope (updated)**: CI + CD. CI handles build/test/lint/manifest validation; CD bootstrap publishes multi-arch images; CD deploy applies Kubernetes manifests.
**Research Findings**:
- **Finbuckle.MultiTenant**: ClaimStrategy + HeaderStrategy fallback is production-proven (fullstackhero/dotnet-starter-kit pattern).
@@ -73,6 +74,8 @@ Deliver a working multi-tenant club work management application where authentica
- `/docker-compose.yml` — Local dev stack (PostgreSQL, Keycloak, .NET API, Next.js)
- `/infra/k8s/` — Kustomize manifests (base + dev overlay)
- `/.gitea/workflows/ci.yml` — Gitea Actions CI pipeline (parallel backend/frontend/infra checks)
- `/.gitea/workflows/cd-bootstrap.yml` — Gitea Actions CD bootstrap workflow (manual multi-arch image publish)
- `/.gitea/workflows/cd-deploy.yml` — Gitea Actions CD deployment workflow (Kubernetes deploy with Kustomize overlay)
- PostgreSQL database with RLS policies on all tenant-scoped tables
- Keycloak realm configuration with test users and club memberships
- Seed data for development
@@ -106,6 +109,8 @@ Deliver a working multi-tenant club work management application where authentica
- TDD: all backend features have tests BEFORE implementation
- Gitea-hosted CI pipeline for this repository (`code.hal9000.damnserver.com/MasterMito/work-club-manager`)
- CI jobs run in parallel (backend, frontend, infrastructure validation)
- Gitea-hosted CD bootstrap workflow for private registry image publication (`workclub-api`, `workclub-frontend`)
- Gitea-hosted CD deployment workflow for Kubernetes dev namespace rollout (`workclub-dev`)
### Must NOT Have (Guardrails)
- **No CQRS/MediatR** — Direct service injection from controllers/endpoints
@@ -124,7 +129,7 @@ Deliver a working multi-tenant club work management application where authentica
- **No in-memory database for tests** — Real PostgreSQL via Testcontainers
- **No billing, subscriptions, or analytics dashboard**
- **No mobile app**
- **No automatic deployment in this CI extension** — CD remains out-of-scope for this append
- **No single-step build-and-deploy coupling** — keep image bootstrap and cluster deployment as separate workflows
---
@@ -193,11 +198,11 @@ Wave 5 (After Wave 4 — polish + Docker):
├── Task 24: Frontend Dockerfiles (dev + prod standalone) (depends: 18) [quick]
└── Task 25: Kustomize dev overlay + resource limits + health checks (depends: 6, 23, 24) [unspecified-high]
Wave 6 (After Wave 5 — E2E + integration):
Wave 6 (After Wave 5 — E2E + CI/CD integration):
├── Task 26: Playwright E2E tests — auth flow + club switching (depends: 21, 22) [unspecified-high]
├── Task 27: Playwright E2E tests — task management flow (depends: 19, 22) [unspecified-high]
├── Task 28: Playwright E2E tests — shift sign-up flow (depends: 20, 22) [unspecified-high]
└── Task 29: Gitea CI workflow (backend + frontend + infra checks) (depends: 12, 17, 23, 24, 25) [unspecified-high]
└── Task 29: Gitea CI/CD workflows (CI checks + image bootstrap + Kubernetes deploy) (depends: 12, 17, 23, 24, 25) [unspecified-high]
Wave FINAL (After ALL tasks — independent review, 4 parallel):
├── Task F1: Plan compliance audit (oracle)
@@ -2525,34 +2530,37 @@ Max Concurrent: 6 (Wave 1)
- Files: `frontend/tests/e2e/shifts.spec.ts`
- Pre-commit: `bunx playwright test tests/e2e/shifts.spec.ts`
- [x] 29. Gitea CI Pipeline — Backend + Frontend + Infra Validation
- [x] 29. Gitea CI/CD PipelinesCI Validation + Image Bootstrap + Kubernetes Deploy
**What to do**:
- Create `.gitea/workflows/ci.yml` for repository `code.hal9000.damnserver.com/MasterMito/work-club-manager`
- Configure triggers:
- Maintain `.gitea/workflows/ci.yml` for repository `code.hal9000.damnserver.com/MasterMito/work-club-manager`
- Maintain `.gitea/workflows/cd-bootstrap.yml` for manual multi-arch image publishing to private registry
- Maintain `.gitea/workflows/cd-deploy.yml` for Kubernetes deployment using Kustomize overlays
- Configure CI triggers:
- `push` on `main` and feature branches
- `pull_request` targeting `main`
- `workflow_dispatch` for manual reruns
- Structure pipeline into parallel jobs (fail-fast disabled so all diagnostics are visible):
- CI workflow structure (parallel validation jobs):
- `backend-ci`: setup .NET 10 SDK, restore, build, run backend unit/integration tests
- `frontend-ci`: setup Bun, install deps, run lint, type-check, unit tests, production build
- `infra-ci`: validate Docker Compose and Kustomize manifests
- Add path filters so docs-only changes skip heavy jobs when possible
- Add dependency caching:
- NuGet cache keyed by `**/*.csproj` + lock/context
- Bun cache keyed by `bun.lockb`
- Add artifact upload on failure:
- `backend-test-results` (trx/log output)
- `frontend-test-results` (vitest output)
- `infra-validation-output`
- CD bootstrap workflow behavior:
- Manual trigger with `image_tag` + build flags
- Buildx multi-arch build (`linux/amd64,linux/arm64`) for `workclub-api` and `workclub-frontend`
- Push image tags to `192.168.241.13:8080` and emit task-31/task-32/task-33 evidence artifacts
- CD deploy workflow behavior:
- Triggered by successful bootstrap (`workflow_run`) or manual dispatch (`image_tag` input)
- Install kubectl + kustomize on runner
- Run `kustomize edit set image` in `infra/k8s/overlays/dev`
- Apply manifests with `kubectl apply -k infra/k8s/overlays/dev`
- Ensure namespace `workclub-dev` exists and perform deployment diagnostics
- Enforce branch protection expectation in plan notes:
- Required checks: `backend-ci`, `frontend-ci`, `infra-ci`
- Keep CD out-of-scope in this append (no image push, no deploy steps)
**Must NOT do**:
- Do NOT add deployment jobs (Kubernetes apply/helm/kustomize deploy)
- Do NOT add secrets for registry push in this CI-only iteration
- Do NOT couple CI workflow to release-tag deployment behavior
- Do NOT collapse bootstrap and deployment into one opaque pipeline stage
- Do NOT bypass image-tag pinning in deployment
- Do NOT remove CI validation gates (`backend-ci`, `frontend-ci`, `infra-ci`)
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
@@ -2568,10 +2576,13 @@ Max Concurrent: 6 (Wave 1)
**References**:
**Pattern References**:
- `.gitea/workflows/ci.yml` — Source of truth for CI checks
- `.gitea/workflows/cd-bootstrap.yml` — Source of truth for image publish bootstrap
- `.gitea/workflows/cd-deploy.yml` — Source of truth for deployment apply logic
- `docker-compose.yml` — Source of truth for `docker compose config` validation
- `infra/k8s/base/kustomization.yaml` and `infra/k8s/overlays/dev/kustomization.yaml` — Kustomize build inputs used by infra-ci job
- `infra/k8s/base/kustomization.yaml` and `infra/k8s/overlays/dev/kustomization.yaml` — Kustomize build/apply inputs
- `backend/WorkClub.sln` — Backend restore/build/test entrypoint for .NET job
- `frontend/package.json` + `frontend/bun.lockb` — Frontend scripts and cache key anchor
- `frontend/package.json` + `frontend/bun.lock` — Frontend scripts and cache key anchor
**External References**:
- Gitea Actions docs: workflow syntax and trigger model (`.gitea/workflows/*.yml`)
@@ -2596,6 +2607,18 @@ Max Concurrent: 6 (Wave 1)
Failure Indicators: Missing job, skipped required job, or non-success conclusion
Evidence: .sisyphus/evidence/task-29-gitea-ci-success.json
Scenario: CD bootstrap and deploy workflows are present and wired
Tool: Bash
Preconditions: Repository contains workflow files
Steps:
1. Assert `.gitea/workflows/cd-bootstrap.yml` exists
2. Assert `.gitea/workflows/cd-deploy.yml` exists
3. Grep bootstrap workflow for buildx multi-arch publish step
4. Grep deploy workflow for `workflow_run`, `kustomize edit set image`, and `kubectl apply -k`
Expected Result: Both CD workflows exist with expected bootstrap and deploy steps
Failure Indicators: Missing file, missing trigger, or missing deploy commands
Evidence: .sisyphus/evidence/task-29-gitea-cd-workflows.txt
Scenario: Pipeline fails on intentional backend break
Tool: Bash (git + Gitea API)
Preconditions: Temporary branch available, ability to push test commit
@@ -2611,8 +2634,8 @@ Max Concurrent: 6 (Wave 1)
```
**Commit**: YES
- Message: `ci(gitea): add parallel CI workflow for backend, frontend, and infra validation`
- Files: `.gitea/workflows/ci.yml`
- Message: `ci(cd): add CI validation plus bootstrap and Kubernetes deployment workflows`
- Files: `.gitea/workflows/ci.yml`, `.gitea/workflows/cd-bootstrap.yml`, `.gitea/workflows/cd-deploy.yml`
- Pre-commit: `docker compose config && kustomize build infra/k8s/overlays/dev > /dev/null`
---
@@ -2662,7 +2685,7 @@ Max Concurrent: 6 (Wave 1)
| 4 | T18-T21 | `feat(ui): add layout, club-switcher, login, task and shift pages` | frontend/src/app/**/*.tsx, frontend/src/components/**/*.tsx | `bun run build && bun run test` |
| 5 | T22-T25 | `infra(deploy): add full Docker Compose stack, Dockerfiles, and Kustomize dev overlay` | docker-compose.yml, **/Dockerfile*, infra/k8s/overlays/dev/**/*.yaml | `docker compose config && kustomize build infra/k8s/overlays/dev` |
| 6 | T26-T28 | `test(e2e): add Playwright E2E tests for auth, tasks, and shifts` | frontend/tests/e2e/**/*.spec.ts | `bunx playwright test` |
| 6 | T29 | `ci(gitea): add parallel CI workflow for backend, frontend, and infra validation` | .gitea/workflows/ci.yml | `docker compose config && kustomize build infra/k8s/overlays/dev > /dev/null` |
| 6 | T29 | `ci(cd): add CI validation plus bootstrap and Kubernetes deployment workflows` | .gitea/workflows/ci.yml, .gitea/workflows/cd-bootstrap.yml, .gitea/workflows/cd-deploy.yml | `docker compose config && kustomize build infra/k8s/overlays/dev > /dev/null` |
---
@@ -2699,6 +2722,12 @@ kustomize build infra/k8s/overlays/dev > /dev/null # Expected: Exit 0
# CI workflow file present and includes required jobs
grep -E "backend-ci|frontend-ci|infra-ci" .gitea/workflows/ci.yml # Expected: all 3 job names present
# CD bootstrap workflow present with multi-arch publish
grep -E "buildx|linux/amd64,linux/arm64|workclub-api|workclub-frontend" .gitea/workflows/cd-bootstrap.yml
# CD deploy workflow present with deploy trigger and apply step
grep -E "workflow_run|kustomize edit set image|kubectl apply -k" .gitea/workflows/cd-deploy.yml
```
### Final Checklist
@@ -2710,6 +2739,7 @@ grep -E "backend-ci|frontend-ci|infra-ci" .gitea/workflows/ci.yml # Expected: a
- [x] Docker Compose stack starts clean and healthy
- [x] Kustomize manifests build without errors
- [x] Gitea CI workflow exists and references backend-ci/frontend-ci/infra-ci
- [x] Gitea CD bootstrap and deploy workflows exist and are wired to image publish/deploy steps
- [x] RLS isolation proven at database level
- [x] Cross-tenant access returns 403
- [x] Task state machine rejects invalid transitions (422)

View File

@@ -62,6 +62,22 @@ public class SeedDataService
");
// Create admin bypass policies (idempotent)
await context.Database.ExecuteSqlRawAsync(@"
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_admin') THEN
CREATE ROLE app_admin;
END IF;
END
$$;
GRANT app_admin TO app;
GRANT USAGE ON SCHEMA public TO app_admin;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_admin;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO app_admin;
ALTER DEFAULT PRIVILEGES FOR ROLE app IN SCHEMA public GRANT ALL ON TABLES TO app_admin;
ALTER DEFAULT PRIVILEGES FOR ROLE app IN SCHEMA public GRANT ALL ON SEQUENCES TO app_admin;
");
await context.Database.ExecuteSqlRawAsync(@"
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='clubs' AND policyname='bypass_rls_policy') THEN

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

View File

@@ -1,7 +1,7 @@
'use client';
import { useEffect, Suspense } from 'react';
import { signIn, signOut, useSession } from 'next-auth/react';
import { signOut, useSession } from 'next-auth/react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -18,8 +18,33 @@ function LoginContent() {
}
}, [status, router]);
const handleSignIn = () => {
signIn('keycloak', { callbackUrl: '/dashboard' });
const handleSignIn = async () => {
const csrfResponse = await fetch('/api/auth/csrf');
const csrfPayload = await csrfResponse.json() as { csrfToken?: string };
if (!csrfPayload.csrfToken) {
window.location.href = '/api/auth/signin?callbackUrl=%2Fdashboard';
return;
}
const form = document.createElement('form');
form.method = 'POST';
form.action = '/api/auth/signin/keycloak';
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfToken';
csrfInput.value = csrfPayload.csrfToken;
form.appendChild(csrfInput);
const callbackInput = document.createElement('input');
callbackInput.type = 'hidden';
callbackInput.name = 'callbackUrl';
callbackInput.value = `${window.location.origin}/dashboard`;
form.appendChild(callbackInput);
document.body.appendChild(form);
form.submit();
};
const handleSwitchAccount = () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -18,7 +18,7 @@ spec:
spec:
containers:
- name: api
image: workclub-api:latest
image: 192.168.241.13:8080/workclub-api:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
@@ -28,10 +28,10 @@ spec:
httpGet:
path: /health/startup
port: http
initialDelaySeconds: 5
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
failureThreshold: 60
livenessProbe:
httpGet:
path: /health/live
@@ -44,10 +44,10 @@ spec:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 10
initialDelaySeconds: 60
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 2
failureThreshold: 10
resources:
requests:
@@ -55,7 +55,7 @@ spec:
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
memory: 768Mi
env:
- name: ASPNETCORE_ENVIRONMENT
@@ -67,8 +67,13 @@ spec:
secretKeyRef:
name: workclub-secrets
key: database-connection-string
- name: Keycloak__Url
- name: Keycloak__Authority
valueFrom:
configMapKeyRef:
name: workclub-config
key: keycloak-url
key: keycloak-authority
- name: Keycloak__Audience
valueFrom:
configMapKeyRef:
name: workclub-config
key: keycloak-audience

View File

@@ -6,11 +6,12 @@ metadata:
app: workclub-api
component: backend
spec:
type: ClusterIP
type: NodePort
selector:
app: workclub-api
ports:
- name: http
port: 80
targetPort: 8080
nodePort: 30081
protocol: TCP

View File

@@ -6,9 +6,11 @@ metadata:
app: workclub
data:
log-level: "Information"
cors-origins: "http://localhost:3000"
api-base-url: "http://workclub-api"
keycloak-url: "http://workclub-keycloak"
cors-origins: "http://localhost:3000,http://192.168.240.200:30080"
api-base-url: "http://192.168.240.200:30081"
keycloak-url: "http://192.168.240.200:30082"
keycloak-authority: "http://192.168.240.200:30082/realms/workclub"
keycloak-audience: "workclub-api"
keycloak-realm: "workclub"
# Database configuration
@@ -39,3 +41,18 @@ data:
\c workclub
GRANT ALL PRIVILEGES ON SCHEMA public TO app;
ALTER SCHEMA public OWNER TO app;
-- App admin role for RLS bypass policies used by API startup seed
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_admin') THEN
CREATE ROLE app_admin;
END IF;
END
$$;
GRANT app_admin TO app WITH INHERIT FALSE, SET TRUE;
GRANT USAGE ON SCHEMA public TO app_admin;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_admin;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO app_admin;
ALTER DEFAULT PRIVILEGES FOR ROLE app IN SCHEMA public GRANT ALL ON TABLES TO app_admin;
ALTER DEFAULT PRIVILEGES FOR ROLE app IN SCHEMA public GRANT ALL ON SEQUENCES TO app_admin;

View File

@@ -18,7 +18,7 @@ spec:
spec:
containers:
- name: frontend
image: workclub-frontend:latest
image: 192.168.241.13:8080/workclub-frontend:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
@@ -62,3 +62,31 @@ spec:
configMapKeyRef:
name: workclub-config
key: keycloak-url
- name: NEXT_PUBLIC_KEYCLOAK_ISSUER
valueFrom:
configMapKeyRef:
name: workclub-config
key: keycloak-authority
- name: NEXTAUTH_URL
value: "http://192.168.240.200:30080"
- name: AUTH_TRUST_HOST
value: "true"
- name: NEXTAUTH_SECRET
valueFrom:
secretKeyRef:
name: workclub-secrets
key: nextauth-secret
- name: KEYCLOAK_CLIENT_ID
value: "workclub-app"
- name: KEYCLOAK_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: workclub-secrets
key: keycloak-client-secret
- name: KEYCLOAK_ISSUER
valueFrom:
configMapKeyRef:
name: workclub-config
key: keycloak-authority
- name: KEYCLOAK_ISSUER_INTERNAL
value: "http://workclub-keycloak/realms/workclub"

View File

@@ -6,11 +6,12 @@ metadata:
app: workclub-frontend
component: frontend
spec:
type: ClusterIP
type: NodePort
selector:
app: workclub-frontend
ports:
- name: http
port: 80
targetPort: 3000
nodePort: 30080
protocol: TCP

View File

@@ -7,6 +7,9 @@ metadata:
component: auth
spec:
replicas: 1
strategy:
type: Recreate
progressDeadlineSeconds: 1800
selector:
matchLabels:
app: workclub-keycloak
@@ -20,36 +23,48 @@ spec:
- name: keycloak
image: quay.io/keycloak/keycloak:26.1
imagePullPolicy: IfNotPresent
command:
- start
args:
- start-dev
- --import-realm
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: management
containerPort: 9000
protocol: TCP
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 10
periodSeconds: 10
port: management
initialDelaySeconds: 240
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 2
failureThreshold: 10
startupProbe:
httpGet:
path: /health/ready
port: management
initialDelaySeconds: 60
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 120
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 20
periodSeconds: 15
port: management
initialDelaySeconds: 420
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
failureThreshold: 5
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
memory: 1024Mi
env:
- name: KC_DB
value: postgres
@@ -66,9 +81,12 @@ spec:
secretKeyRef:
name: workclub-secrets
key: keycloak-db-password
- name: KEYCLOAK_ADMIN
value: admin
- name: KEYCLOAK_ADMIN_PASSWORD
- name: KC_BOOTSTRAP_ADMIN_USERNAME
valueFrom:
secretKeyRef:
name: workclub-secrets
key: keycloak-admin-username
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: workclub-secrets
@@ -79,3 +97,13 @@ spec:
value: "edge"
- name: KC_HTTP_ENABLED
value: "true"
- name: KC_HEALTH_ENABLED
value: "true"
volumeMounts:
- name: keycloak-realm-import
mountPath: /opt/keycloak/data/import
readOnly: true
volumes:
- name: keycloak-realm-import
configMap:
name: keycloak-realm-import

View File

@@ -0,0 +1,248 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: keycloak-realm-import
labels:
app: workclub-keycloak
data:
realm-export.json: |
{
"realm": "workclub",
"enabled": true,
"displayName": "Work Club Manager",
"registrationAllowed": false,
"rememberMe": true,
"verifyEmail": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false,
"bruteForceProtected": true,
"clients": [
{
"clientId": "workclub-api",
"name": "Work Club API",
"enabled": true,
"protocol": "openid-connect",
"clientAuthenticatorType": "client-secret",
"secret": "dev-secret-workclub-api-change-in-production",
"redirectUris": [],
"webOrigins": [],
"publicClient": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"standardFlowEnabled": false,
"implicitFlowEnabled": false,
"fullScopeAllowed": true,
"protocolMappers": [
{
"name": "audience-workclub-api",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "workclub-api",
"id.token.claim": "false",
"access.token.claim": "true"
}
},
{
"name": "clubs-claim",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "clubs",
"claim.name": "clubs",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
},
{
"clientId": "workclub-app",
"name": "Work Club Frontend",
"enabled": true,
"protocol": "openid-connect",
"publicClient": true,
"redirectUris": [
"http://localhost:3000/*",
"http://localhost:3001/*",
"http://workclub-frontend/*",
"http://192.168.240.200:30080/*"
],
"webOrigins": [
"http://localhost:3000",
"http://localhost:3001",
"http://workclub-frontend",
"http://192.168.240.200:30080"
],
"directAccessGrantsEnabled": true,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"fullScopeAllowed": true,
"protocolMappers": [
{
"name": "audience-workclub-api",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "workclub-api",
"id.token.claim": "false",
"access.token.claim": "true"
}
},
{
"name": "clubs-claim",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "clubs",
"claim.name": "clubs",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
}
],
"roles": {
"realm": [
{
"name": "admin",
"description": "Club admin"
},
{
"name": "manager",
"description": "Club manager"
},
{
"name": "member",
"description": "Club member"
},
{
"name": "viewer",
"description": "Club viewer"
}
]
},
"users": [
{
"username": "admin@test.com",
"enabled": true,
"email": "admin@test.com",
"firstName": "Admin",
"lastName": "User",
"credentials": [
{
"type": "password",
"value": "testpass123",
"temporary": false
}
],
"realmRoles": [
"admin"
],
"attributes": {
"clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,Admin,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda,Member"
]
}
},
{
"username": "manager@test.com",
"enabled": true,
"email": "manager@test.com",
"firstName": "Manager",
"lastName": "User",
"credentials": [
{
"type": "password",
"value": "testpass123",
"temporary": false
}
],
"realmRoles": [
"manager"
],
"attributes": {
"clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,Manager"
]
}
},
{
"username": "member1@test.com",
"enabled": true,
"email": "member1@test.com",
"firstName": "Member",
"lastName": "One",
"credentials": [
{
"type": "password",
"value": "testpass123",
"temporary": false
}
],
"realmRoles": [
"member"
],
"attributes": {
"clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,Member,3b4afcfa-1352-8fc7-b497-8ab52a0d5fda,Member"
]
}
},
{
"username": "member2@test.com",
"enabled": true,
"email": "member2@test.com",
"firstName": "Member",
"lastName": "Two",
"credentials": [
{
"type": "password",
"value": "testpass123",
"temporary": false
}
],
"realmRoles": [
"member"
],
"attributes": {
"clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,Member"
]
}
},
{
"username": "viewer@test.com",
"enabled": true,
"email": "viewer@test.com",
"firstName": "Viewer",
"lastName": "User",
"credentials": [
{
"type": "password",
"value": "testpass123",
"temporary": false
}
],
"realmRoles": [
"viewer"
],
"attributes": {
"clubs": [
"64e05b5e-ef45-81d7-f2e8-3d14bd197383,Viewer"
]
}
}
]
}

View File

@@ -6,11 +6,12 @@ metadata:
app: workclub-keycloak
component: auth
spec:
type: ClusterIP
type: NodePort
selector:
app: workclub-keycloak
ports:
- name: http
port: 80
targetPort: 8080
nodePort: 30082
protocol: TCP

View File

@@ -9,6 +9,10 @@ resources:
- postgres-statefulset.yaml
- postgres-service.yaml
- keycloak-deployment.yaml
- keycloak-realm-import-configmap.yaml
- keycloak-service.yaml
- configmap.yaml
- ingress.yaml
generatorOptions:
disableNameSuffixHash: true

View File

@@ -3,6 +3,7 @@ kind: Kustomization
resources:
- ../../base
- secrets.yaml
namespace: workclub-dev
@@ -10,9 +11,11 @@ commonLabels:
environment: development
images:
- name: workclub-api
- name: 192.168.241.13:8080/workclub-api
newName: 192.168.241.13:8080/workclub-api
newTag: dev
- name: workclub-frontend
- name: 192.168.241.13:8080/workclub-frontend
newName: 192.168.241.13:8080/workclub-frontend
newTag: dev
replicas:
@@ -30,3 +33,7 @@ patches:
target:
kind: Deployment
name: workclub-frontend
- path: patches/postgres-patch.yaml
target:
kind: StatefulSet
name: workclub-postgres

View File

@@ -0,0 +1,11 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: workclub-postgres
spec:
template:
spec:
volumes:
- name: postgres-data
emptyDir: {}
volumeClaimTemplates: [] # This removes the VCT from the base

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Secret
metadata:
name: workclub-secrets
type: Opaque
stringData:
database-connection-string: "Host=workclub-postgres;Database=workclub;Username=app;Password=devpassword"
postgres-password: "devpassword"
keycloak-db-password: "keycloakpass"
keycloak-admin-username: "admin"
keycloak-admin-password: "adminpassword"
keycloak-client-secret: "dev-secret-workclub-api-change-in-production"
nextauth-secret: "dev-secret-change-in-production-use-openssl-rand-base64-32"