Latest commit

History

793 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Blueshell Website

Full-stack web application for ESA Blueshell — a student association platform for managing members, events, payments, and communications.

Built with Spring Boot 4 (Kotlin) backend and Vue.js 3 (TypeScript) frontend.


Architecture

Domain-Driven Design with a clean layered architecture:

LayerTechnology
Backend APISpring Boot 4 (Kotlin), Spring Security, Spring Data JPA
FrontendVue.js 3, TypeScript, Vuetify 3, Vite
DatabaseMariaDB 10.11
EmailStalwart SMTP relay (transactional)
SecretsHashiCorp Vault + Vault Secrets Operator
Auth / OIDCAPI issues tokens (Spring Authorization Server) for Headlamp, Vault
Reverse proxyTraefik v3 in k3s (Let's Encrypt DNS-01 via Cloudflare)
Orchestrationk3s (single node) + FluxCD GitOps
OSNixOS on Contabo VPS

Architecture decisions:docs/adr/ADR-INDEX.md


Development setup

Prerequisites

  • Docker + Docker Compose v2
  • Java 21 (optional — for running the API outside Docker)
  • Node.js + Yarn Berry (optional — for running the frontend outside Docker)
  • A GitHub token with read:packages — both images pull the Brevo and Discord clients from GitHub Packages, which authenticates even for public packages

A token for the image builds

The builds read the token from .secrets/ as a BuildKit secret, so it never lands in the image history. Write the files once — they are gitignored, and the compose files expect exactly these three names:

mkdir -p .secrets && chmod 700 .secrets
gh auth token > .secrets/github_token # Gradle → maven.pkg.github.com
gh auth token > .secrets/node_auth_token # Yarn → npm.pkg.github.com
gh api user -q .login > .secrets/github_actor # username the maven registry wants
chmod 600 .secrets/*

A classic PAT with read:packages works just as well as the gh token — paste it into the two token files instead. Compose refuses to start when a secret file is missing, so create all three even if you leave one empty.

The tokens are read at image build time only. The api image bakes Gradle's dependency cache in, so the running container starts bootRun --offline and never reaches for the registry. Rebuild the image (docker compose build api) when a dependency version changes; source edits hot-reload off the bind mount as before.

Start the dev environment

docker compose up -d

This starts:

ServiceURLNotes
APIhttp://localhost:8080Hot-reload via Gradle
Frontendhttp://localhost:3000Hot-reload via Vite
Swagger UIhttp://localhost:8080/swagger-uiSet SPRINGDOC_API_DOCS_ENABLED=true
MariaDBlocalhost:3307
Stalwarthttp://localhost:8085Dev MTA admin UI (SMTP :1025, IMAP :1143, admin admin/admin)

Reaching the api, and trying the site on a phone

The frontend reaches the api at the page's own origin under /api, the same shape production serves: http://localhost:3000/api from this machine, and http://<your-lan-ip>:3000/api from anything else on the network. Vite proxies /api and strips the prefix, mirroring the strip-api-prefix Traefik middleware, so no address is configured anywhere and a phone needs nothing but the URL:

ipconfig getifaddr en0 # then open http://<that>:3000 on the phone

The api's own port stays published, so http://localhost:8080 still answers directly for Swagger, curl and the debugger.

Two things stay laptop-only, and are supposed to: activation and password-reset links, and the email tracking pixel. Those are absolute URLs the api builds from FRONTEND_URL and APP_URL, and dev mail is read on the laptop anyway.

VITE_APP_URL still overrides the origin if you point the frontend at a deployed api — set a matching entry in security.cors.allowed-origins (services/api/src/main/resources/application-dev.yaml) when you do, since that request is cross-origin again.

Environment files

The compose files include sensible defaults. For production-like secrets, copy the examples:

cp services/api/.db.example.env services/api/.db.env

Run tests

./gradlew :services:api:test
./gradlew :services:api:integrationTest

Generate OpenAPI TypeScript client

./scripts/generate_openapi.sh

Remote debugging

The dev API container exposes JDWP on localhost:5005. IntelliJ: Remote JVM Debug → host: localhost, port: 5005.


Production deployment

Production runs on a single-node NixOS + k3s + FluxCD stack. Flux reconciles manifests from platform/cluster/flux/ against main; Keel polls ghcr.io/esa-blueshell/* for new :latest tags and rolls the matching Deployments. There is no CI deploy step — pushing to main is the deploy.

Runbook: platform/docs/runbook.md.

build-push.yml publishes api and frontend images to GHCR.


Services at a glance

ServiceImageInternal portDescription
apighcr.io/esa-blueshell/api8080Spring Boot REST API
frontendghcr.io/esa-blueshell/frontend3000Vue.js SPA
dbmariadb:10.113306Application database

Security

  • JWT authentication (Spring Security)
  • SQL injection prevention (JPA parameterized queries)
  • XSS protection (Vue template escaping + CSP headers)
  • CORS restricted to blueshell domains
  • TLS 1.2+ with Let's Encrypt certificates (auto-renewed)

API documentation

  • Development:http://localhost:8080/swagger-ui (set SPRINGDOC_API_DOCS_ENABLED=true)
  • Production: disabled (OpenAPI docs off in the prod profile)
  • OpenAPI spec:/api/v3/api-docs

Contributing

  1. Create a feature branch from main
  2. Make changes with hot reload in the dev environment
  3. Run tests: ./gradlew :services:api:test
  4. If API endpoints changed: ./scripts/generate_openapi.sh
  5. Open a pull request

Support

Questions or issues: board@blueshell.utwente.nl

About

No description, website, or topics provided.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

793 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Blueshell Website

Full-stack web application for ESA Blueshell — a student association platform for managing members, events, payments, and communications.

Built with Spring Boot 4 (Kotlin) backend and Vue.js 3 (TypeScript) frontend.


Architecture

Domain-Driven Design with a clean layered architecture:

LayerTechnology
Backend APISpring Boot 4 (Kotlin), Spring Security, Spring Data JPA
FrontendVue.js 3, TypeScript, Vuetify 3, Vite
DatabaseMariaDB 10.11
EmailStalwart SMTP relay (transactional)
SecretsHashiCorp Vault + Vault Secrets Operator
Auth / OIDCAPI issues tokens (Spring Authorization Server) for Headlamp, Vault
Reverse proxyTraefik v3 in k3s (Let's Encrypt DNS-01 via Cloudflare)
Orchestrationk3s (single node) + FluxCD GitOps
OSNixOS on Contabo VPS

Architecture decisions:docs/adr/ADR-INDEX.md


Development setup

Prerequisites

  • Docker + Docker Compose v2
  • Java 21 (optional — for running the API outside Docker)
  • Node.js + Yarn Berry (optional — for running the frontend outside Docker)
  • A GitHub token with read:packages — both images pull the Brevo and Discord clients from GitHub Packages, which authenticates even for public packages

A token for the image builds

The builds read the token from .secrets/ as a BuildKit secret, so it never lands in the image history. Write the files once — they are gitignored, and the compose files expect exactly these three names:

mkdir -p .secrets && chmod 700 .secrets
gh auth token > .secrets/github_token # Gradle → maven.pkg.github.com
gh auth token > .secrets/node_auth_token # Yarn → npm.pkg.github.com
gh api user -q .login > .secrets/github_actor # username the maven registry wants
chmod 600 .secrets/*

A classic PAT with read:packages works just as well as the gh token — paste it into the two token files instead. Compose refuses to start when a secret file is missing, so create all three even if you leave one empty.

The tokens are read at image build time only. The api image bakes Gradle's dependency cache in, so the running container starts bootRun --offline and never reaches for the registry. Rebuild the image (docker compose build api) when a dependency version changes; source edits hot-reload off the bind mount as before.

Start the dev environment

docker compose up -d

This starts:

ServiceURLNotes
APIhttp://localhost:8080Hot-reload via Gradle
Frontendhttp://localhost:3000Hot-reload via Vite
Swagger UIhttp://localhost:8080/swagger-uiSet SPRINGDOC_API_DOCS_ENABLED=true
MariaDBlocalhost:3307
Stalwarthttp://localhost:8085Dev MTA admin UI (SMTP :1025, IMAP :1143, admin admin/admin)

Reaching the api, and trying the site on a phone

The frontend reaches the api at the page's own origin under /api, the same shape production serves: http://localhost:3000/api from this machine, and http://<your-lan-ip>:3000/api from anything else on the network. Vite proxies /api and strips the prefix, mirroring the strip-api-prefix Traefik middleware, so no address is configured anywhere and a phone needs nothing but the URL:

ipconfig getifaddr en0 # then open http://<that>:3000 on the phone

The api's own port stays published, so http://localhost:8080 still answers directly for Swagger, curl and the debugger.

Two things stay laptop-only, and are supposed to: activation and password-reset links, and the email tracking pixel. Those are absolute URLs the api builds from FRONTEND_URL and APP_URL, and dev mail is read on the laptop anyway.

VITE_APP_URL still overrides the origin if you point the frontend at a deployed api — set a matching entry in security.cors.allowed-origins (services/api/src/main/resources/application-dev.yaml) when you do, since that request is cross-origin again.

Environment files

The compose files include sensible defaults. For production-like secrets, copy the examples:

cp services/api/.db.example.env services/api/.db.env

Run tests

./gradlew :services:api:test
./gradlew :services:api:integrationTest

Generate OpenAPI TypeScript client

./scripts/generate_openapi.sh

Remote debugging

The dev API container exposes JDWP on localhost:5005. IntelliJ: Remote JVM Debug → host: localhost, port: 5005.


Production deployment

Production runs on a single-node NixOS + k3s + FluxCD stack. Flux reconciles manifests from platform/cluster/flux/ against main; Keel polls ghcr.io/esa-blueshell/* for new :latest tags and rolls the matching Deployments. There is no CI deploy step — pushing to main is the deploy.

Runbook: platform/docs/runbook.md.

build-push.yml publishes api and frontend images to GHCR.


Services at a glance

ServiceImageInternal portDescription
apighcr.io/esa-blueshell/api8080Spring Boot REST API
frontendghcr.io/esa-blueshell/frontend3000Vue.js SPA
dbmariadb:10.113306Application database

Security

  • JWT authentication (Spring Security)
  • SQL injection prevention (JPA parameterized queries)
  • XSS protection (Vue template escaping + CSP headers)
  • CORS restricted to blueshell domains
  • TLS 1.2+ with Let's Encrypt certificates (auto-renewed)

API documentation

  • Development:http://localhost:8080/swagger-ui (set SPRINGDOC_API_DOCS_ENABLED=true)
  • Production: disabled (OpenAPI docs off in the prod profile)
  • OpenAPI spec:/api/v3/api-docs

Contributing

  1. Create a feature branch from main
  2. Make changes with hot reload in the dev environment
  3. Run tests: ./gradlew :services:api:test
  4. If API endpoints changed: ./scripts/generate_openapi.sh
  5. Open a pull request

Support

Questions or issues: board@blueshell.utwente.nl

About

No description, website, or topics provided.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

793 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Blueshell Website

Full-stack web application for ESA Blueshell — a student association platform for managing members, events, payments, and communications.

Built with Spring Boot 4 (Kotlin) backend and Vue.js 3 (TypeScript) frontend.


Architecture

Domain-Driven Design with a clean layered architecture:

LayerTechnology
Backend APISpring Boot 4 (Kotlin), Spring Security, Spring Data JPA
FrontendVue.js 3, TypeScript, Vuetify 3, Vite
DatabaseMariaDB 10.11
EmailStalwart SMTP relay (transactional)
SecretsHashiCorp Vault + Vault Secrets Operator
Auth / OIDCAPI issues tokens (Spring Authorization Server) for Headlamp, Vault
Reverse proxyTraefik v3 in k3s (Let's Encrypt DNS-01 via Cloudflare)
Orchestrationk3s (single node) + FluxCD GitOps
OSNixOS on Contabo VPS

Architecture decisions:docs/adr/ADR-INDEX.md


Development setup

Prerequisites

  • Docker + Docker Compose v2
  • Java 21 (optional — for running the API outside Docker)
  • Node.js + Yarn Berry (optional — for running the frontend outside Docker)
  • A GitHub token with read:packages — both images pull the Brevo and Discord clients from GitHub Packages, which authenticates even for public packages

A token for the image builds

The builds read the token from .secrets/ as a BuildKit secret, so it never lands in the image history. Write the files once — they are gitignored, and the compose files expect exactly these three names:

mkdir -p .secrets && chmod 700 .secrets
gh auth token > .secrets/github_token # Gradle → maven.pkg.github.com
gh auth token > .secrets/node_auth_token # Yarn → npm.pkg.github.com
gh api user -q .login > .secrets/github_actor # username the maven registry wants
chmod 600 .secrets/*

A classic PAT with read:packages works just as well as the gh token — paste it into the two token files instead. Compose refuses to start when a secret file is missing, so create all three even if you leave one empty.

The tokens are read at image build time only. The api image bakes Gradle's dependency cache in, so the running container starts bootRun --offline and never reaches for the registry. Rebuild the image (docker compose build api) when a dependency version changes; source edits hot-reload off the bind mount as before.

Start the dev environment

docker compose up -d

This starts:

ServiceURLNotes
APIhttp://localhost:8080Hot-reload via Gradle
Frontendhttp://localhost:3000Hot-reload via Vite
Swagger UIhttp://localhost:8080/swagger-uiSet SPRINGDOC_API_DOCS_ENABLED=true
MariaDBlocalhost:3307
Stalwarthttp://localhost:8085Dev MTA admin UI (SMTP :1025, IMAP :1143, admin admin/admin)

Reaching the api, and trying the site on a phone

The frontend reaches the api at the page's own origin under /api, the same shape production serves: http://localhost:3000/api from this machine, and http://<your-lan-ip>:3000/api from anything else on the network. Vite proxies /api and strips the prefix, mirroring the strip-api-prefix Traefik middleware, so no address is configured anywhere and a phone needs nothing but the URL:

ipconfig getifaddr en0 # then open http://<that>:3000 on the phone

The api's own port stays published, so http://localhost:8080 still answers directly for Swagger, curl and the debugger.

Two things stay laptop-only, and are supposed to: activation and password-reset links, and the email tracking pixel. Those are absolute URLs the api builds from FRONTEND_URL and APP_URL, and dev mail is read on the laptop anyway.

VITE_APP_URL still overrides the origin if you point the frontend at a deployed api — set a matching entry in security.cors.allowed-origins (services/api/src/main/resources/application-dev.yaml) when you do, since that request is cross-origin again.

Environment files

The compose files include sensible defaults. For production-like secrets, copy the examples:

cp services/api/.db.example.env services/api/.db.env

Run tests

./gradlew :services:api:test
./gradlew :services:api:integrationTest

Generate OpenAPI TypeScript client

./scripts/generate_openapi.sh

Remote debugging

The dev API container exposes JDWP on localhost:5005. IntelliJ: Remote JVM Debug → host: localhost, port: 5005.


Production deployment

Production runs on a single-node NixOS + k3s + FluxCD stack. Flux reconciles manifests from platform/cluster/flux/ against main; Keel polls ghcr.io/esa-blueshell/* for new :latest tags and rolls the matching Deployments. There is no CI deploy step — pushing to main is the deploy.

Runbook: platform/docs/runbook.md.

build-push.yml publishes api and frontend images to GHCR.


Services at a glance

ServiceImageInternal portDescription
apighcr.io/esa-blueshell/api8080Spring Boot REST API
frontendghcr.io/esa-blueshell/frontend3000Vue.js SPA
dbmariadb:10.113306Application database

Security

  • JWT authentication (Spring Security)
  • SQL injection prevention (JPA parameterized queries)
  • XSS protection (Vue template escaping + CSP headers)
  • CORS restricted to blueshell domains
  • TLS 1.2+ with Let's Encrypt certificates (auto-renewed)

API documentation

  • Development:http://localhost:8080/swagger-ui (set SPRINGDOC_API_DOCS_ENABLED=true)
  • Production: disabled (OpenAPI docs off in the prod profile)
  • OpenAPI spec:/api/v3/api-docs

Contributing

  1. Create a feature branch from main
  2. Make changes with hot reload in the dev environment
  3. Run tests: ./gradlew :services:api:test
  4. If API endpoints changed: ./scripts/generate_openapi.sh
  5. Open a pull request

Support

Questions or issues: board@blueshell.utwente.nl

About

No description, website, or topics provided.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

793 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Blueshell Website

Full-stack web application for ESA Blueshell — a student association platform for managing members, events, payments, and communications.

Built with Spring Boot 4 (Kotlin) backend and Vue.js 3 (TypeScript) frontend.


Architecture

Domain-Driven Design with a clean layered architecture:

LayerTechnology
Backend APISpring Boot 4 (Kotlin), Spring Security, Spring Data JPA
FrontendVue.js 3, TypeScript, Vuetify 3, Vite
DatabaseMariaDB 10.11
EmailStalwart SMTP relay (transactional)
SecretsHashiCorp Vault + Vault Secrets Operator
Auth / OIDCAPI issues tokens (Spring Authorization Server) for Headlamp, Vault
Reverse proxyTraefik v3 in k3s (Let's Encrypt DNS-01 via Cloudflare)
Orchestrationk3s (single node) + FluxCD GitOps
OSNixOS on Contabo VPS

Architecture decisions:docs/adr/ADR-INDEX.md


Development setup

Prerequisites

  • Docker + Docker Compose v2
  • Java 21 (optional — for running the API outside Docker)
  • Node.js + Yarn Berry (optional — for running the frontend outside Docker)
  • A GitHub token with read:packages — both images pull the Brevo and Discord clients from GitHub Packages, which authenticates even for public packages

A token for the image builds

The builds read the token from .secrets/ as a BuildKit secret, so it never lands in the image history. Write the files once — they are gitignored, and the compose files expect exactly these three names:

mkdir -p .secrets && chmod 700 .secrets
gh auth token > .secrets/github_token # Gradle → maven.pkg.github.com
gh auth token > .secrets/node_auth_token # Yarn → npm.pkg.github.com
gh api user -q .login > .secrets/github_actor # username the maven registry wants
chmod 600 .secrets/*

A classic PAT with read:packages works just as well as the gh token — paste it into the two token files instead. Compose refuses to start when a secret file is missing, so create all three even if you leave one empty.

The tokens are read at image build time only. The api image bakes Gradle's dependency cache in, so the running container starts bootRun --offline and never reaches for the registry. Rebuild the image (docker compose build api) when a dependency version changes; source edits hot-reload off the bind mount as before.

Start the dev environment

docker compose up -d

This starts:

ServiceURLNotes
APIhttp://localhost:8080Hot-reload via Gradle
Frontendhttp://localhost:3000Hot-reload via Vite
Swagger UIhttp://localhost:8080/swagger-uiSet SPRINGDOC_API_DOCS_ENABLED=true
MariaDBlocalhost:3307
Stalwarthttp://localhost:8085Dev MTA admin UI (SMTP :1025, IMAP :1143, admin admin/admin)

Reaching the api, and trying the site on a phone

The frontend reaches the api at the page's own origin under /api, the same shape production serves: http://localhost:3000/api from this machine, and http://<your-lan-ip>:3000/api from anything else on the network. Vite proxies /api and strips the prefix, mirroring the strip-api-prefix Traefik middleware, so no address is configured anywhere and a phone needs nothing but the URL:

ipconfig getifaddr en0 # then open http://<that>:3000 on the phone

The api's own port stays published, so http://localhost:8080 still answers directly for Swagger, curl and the debugger.

Two things stay laptop-only, and are supposed to: activation and password-reset links, and the email tracking pixel. Those are absolute URLs the api builds from FRONTEND_URL and APP_URL, and dev mail is read on the laptop anyway.

VITE_APP_URL still overrides the origin if you point the frontend at a deployed api — set a matching entry in security.cors.allowed-origins (services/api/src/main/resources/application-dev.yaml) when you do, since that request is cross-origin again.

Environment files

The compose files include sensible defaults. For production-like secrets, copy the examples:

cp services/api/.db.example.env services/api/.db.env

Run tests

./gradlew :services:api:test
./gradlew :services:api:integrationTest

Generate OpenAPI TypeScript client

./scripts/generate_openapi.sh

Remote debugging

The dev API container exposes JDWP on localhost:5005. IntelliJ: Remote JVM Debug → host: localhost, port: 5005.


Production deployment

Production runs on a single-node NixOS + k3s + FluxCD stack. Flux reconciles manifests from platform/cluster/flux/ against main; Keel polls ghcr.io/esa-blueshell/* for new :latest tags and rolls the matching Deployments. There is no CI deploy step — pushing to main is the deploy.

Runbook: platform/docs/runbook.md.

build-push.yml publishes api and frontend images to GHCR.


Services at a glance

ServiceImageInternal portDescription
apighcr.io/esa-blueshell/api8080Spring Boot REST API
frontendghcr.io/esa-blueshell/frontend3000Vue.js SPA
dbmariadb:10.113306Application database

Security

  • JWT authentication (Spring Security)
  • SQL injection prevention (JPA parameterized queries)
  • XSS protection (Vue template escaping + CSP headers)
  • CORS restricted to blueshell domains
  • TLS 1.2+ with Let's Encrypt certificates (auto-renewed)

API documentation

  • Development:http://localhost:8080/swagger-ui (set SPRINGDOC_API_DOCS_ENABLED=true)
  • Production: disabled (OpenAPI docs off in the prod profile)
  • OpenAPI spec:/api/v3/api-docs

Contributing

  1. Create a feature branch from main
  2. Make changes with hot reload in the dev environment
  3. Run tests: ./gradlew :services:api:test
  4. If API endpoints changed: ./scripts/generate_openapi.sh
  5. Open a pull request

Support

Questions or issues: board@blueshell.utwente.nl

About

No description, website, or topics provided.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

793 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Blueshell Website

Full-stack web application for ESA Blueshell — a student association platform for managing members, events, payments, and communications.

Built with Spring Boot 4 (Kotlin) backend and Vue.js 3 (TypeScript) frontend.


Architecture

Domain-Driven Design with a clean layered architecture:

LayerTechnology
Backend APISpring Boot 4 (Kotlin), Spring Security, Spring Data JPA
FrontendVue.js 3, TypeScript, Vuetify 3, Vite
DatabaseMariaDB 10.11
EmailStalwart SMTP relay (transactional)
SecretsHashiCorp Vault + Vault Secrets Operator
Auth / OIDCAPI issues tokens (Spring Authorization Server) for Headlamp, Vault
Reverse proxyTraefik v3 in k3s (Let's Encrypt DNS-01 via Cloudflare)
Orchestrationk3s (single node) + FluxCD GitOps
OSNixOS on Contabo VPS

Architecture decisions:docs/adr/ADR-INDEX.md


Development setup

Prerequisites

  • Docker + Docker Compose v2
  • Java 21 (optional — for running the API outside Docker)
  • Node.js + Yarn Berry (optional — for running the frontend outside Docker)
  • A GitHub token with read:packages — both images pull the Brevo and Discord clients from GitHub Packages, which authenticates even for public packages

A token for the image builds

The builds read the token from .secrets/ as a BuildKit secret, so it never lands in the image history. Write the files once — they are gitignored, and the compose files expect exactly these three names:

mkdir -p .secrets && chmod 700 .secrets
gh auth token > .secrets/github_token # Gradle → maven.pkg.github.com
gh auth token > .secrets/node_auth_token # Yarn → npm.pkg.github.com
gh api user -q .login > .secrets/github_actor # username the maven registry wants
chmod 600 .secrets/*

A classic PAT with read:packages works just as well as the gh token — paste it into the two token files instead. Compose refuses to start when a secret file is missing, so create all three even if you leave one empty.

The tokens are read at image build time only. The api image bakes Gradle's dependency cache in, so the running container starts bootRun --offline and never reaches for the registry. Rebuild the image (docker compose build api) when a dependency version changes; source edits hot-reload off the bind mount as before.

Start the dev environment

docker compose up -d

This starts:

ServiceURLNotes
APIhttp://localhost:8080Hot-reload via Gradle
Frontendhttp://localhost:3000Hot-reload via Vite
Swagger UIhttp://localhost:8080/swagger-uiSet SPRINGDOC_API_DOCS_ENABLED=true
MariaDBlocalhost:3307
Stalwarthttp://localhost:8085Dev MTA admin UI (SMTP :1025, IMAP :1143, admin admin/admin)

Reaching the api, and trying the site on a phone

The frontend reaches the api at the page's own origin under /api, the same shape production serves: http://localhost:3000/api from this machine, and http://<your-lan-ip>:3000/api from anything else on the network. Vite proxies /api and strips the prefix, mirroring the strip-api-prefix Traefik middleware, so no address is configured anywhere and a phone needs nothing but the URL:

ipconfig getifaddr en0 # then open http://<that>:3000 on the phone

The api's own port stays published, so http://localhost:8080 still answers directly for Swagger, curl and the debugger.

Two things stay laptop-only, and are supposed to: activation and password-reset links, and the email tracking pixel. Those are absolute URLs the api builds from FRONTEND_URL and APP_URL, and dev mail is read on the laptop anyway.

VITE_APP_URL still overrides the origin if you point the frontend at a deployed api — set a matching entry in security.cors.allowed-origins (services/api/src/main/resources/application-dev.yaml) when you do, since that request is cross-origin again.

Environment files

The compose files include sensible defaults. For production-like secrets, copy the examples:

cp services/api/.db.example.env services/api/.db.env

Run tests

./gradlew :services:api:test
./gradlew :services:api:integrationTest

Generate OpenAPI TypeScript client

./scripts/generate_openapi.sh

Remote debugging

The dev API container exposes JDWP on localhost:5005. IntelliJ: Remote JVM Debug → host: localhost, port: 5005.


Production deployment

Production runs on a single-node NixOS + k3s + FluxCD stack. Flux reconciles manifests from platform/cluster/flux/ against main; Keel polls ghcr.io/esa-blueshell/* for new :latest tags and rolls the matching Deployments. There is no CI deploy step — pushing to main is the deploy.

Runbook: platform/docs/runbook.md.

build-push.yml publishes api and frontend images to GHCR.


Services at a glance

ServiceImageInternal portDescription
apighcr.io/esa-blueshell/api8080Spring Boot REST API
frontendghcr.io/esa-blueshell/frontend3000Vue.js SPA
dbmariadb:10.113306Application database

Security

  • JWT authentication (Spring Security)
  • SQL injection prevention (JPA parameterized queries)
  • XSS protection (Vue template escaping + CSP headers)
  • CORS restricted to blueshell domains
  • TLS 1.2+ with Let's Encrypt certificates (auto-renewed)

API documentation

  • Development:http://localhost:8080/swagger-ui (set SPRINGDOC_API_DOCS_ENABLED=true)
  • Production: disabled (OpenAPI docs off in the prod profile)
  • OpenAPI spec:/api/v3/api-docs

Contributing

  1. Create a feature branch from main
  2. Make changes with hot reload in the dev environment
  3. Run tests: ./gradlew :services:api:test
  4. If API endpoints changed: ./scripts/generate_openapi.sh
  5. Open a pull request

Support

Questions or issues: board@blueshell.utwente.nl

About

No description, website, or topics provided.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

793 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Blueshell Website

Full-stack web application for ESA Blueshell — a student association platform for managing members, events, payments, and communications.

Built with Spring Boot 4 (Kotlin) backend and Vue.js 3 (TypeScript) frontend.


Architecture

Domain-Driven Design with a clean layered architecture:

LayerTechnology
Backend APISpring Boot 4 (Kotlin), Spring Security, Spring Data JPA
FrontendVue.js 3, TypeScript, Vuetify 3, Vite
DatabaseMariaDB 10.11
EmailStalwart SMTP relay (transactional)
SecretsHashiCorp Vault + Vault Secrets Operator
Auth / OIDCAPI issues tokens (Spring Authorization Server) for Headlamp, Vault
Reverse proxyTraefik v3 in k3s (Let's Encrypt DNS-01 via Cloudflare)
Orchestrationk3s (single node) + FluxCD GitOps
OSNixOS on Contabo VPS

Architecture decisions:docs/adr/ADR-INDEX.md


Development setup

Prerequisites

  • Docker + Docker Compose v2
  • Java 21 (optional — for running the API outside Docker)
  • Node.js + Yarn Berry (optional — for running the frontend outside Docker)
  • A GitHub token with read:packages — both images pull the Brevo and Discord clients from GitHub Packages, which authenticates even for public packages

A token for the image builds

The builds read the token from .secrets/ as a BuildKit secret, so it never lands in the image history. Write the files once — they are gitignored, and the compose files expect exactly these three names:

mkdir -p .secrets && chmod 700 .secrets
gh auth token > .secrets/github_token # Gradle → maven.pkg.github.com
gh auth token > .secrets/node_auth_token # Yarn → npm.pkg.github.com
gh api user -q .login > .secrets/github_actor # username the maven registry wants
chmod 600 .secrets/*

A classic PAT with read:packages works just as well as the gh token — paste it into the two token files instead. Compose refuses to start when a secret file is missing, so create all three even if you leave one empty.

The tokens are read at image build time only. The api image bakes Gradle's dependency cache in, so the running container starts bootRun --offline and never reaches for the registry. Rebuild the image (docker compose build api) when a dependency version changes; source edits hot-reload off the bind mount as before.

Start the dev environment

docker compose up -d

This starts:

ServiceURLNotes
APIhttp://localhost:8080Hot-reload via Gradle
Frontendhttp://localhost:3000Hot-reload via Vite
Swagger UIhttp://localhost:8080/swagger-uiSet SPRINGDOC_API_DOCS_ENABLED=true
MariaDBlocalhost:3307
Stalwarthttp://localhost:8085Dev MTA admin UI (SMTP :1025, IMAP :1143, admin admin/admin)

Reaching the api, and trying the site on a phone

The frontend reaches the api at the page's own origin under /api, the same shape production serves: http://localhost:3000/api from this machine, and http://<your-lan-ip>:3000/api from anything else on the network. Vite proxies /api and strips the prefix, mirroring the strip-api-prefix Traefik middleware, so no address is configured anywhere and a phone needs nothing but the URL:

ipconfig getifaddr en0 # then open http://<that>:3000 on the phone

The api's own port stays published, so http://localhost:8080 still answers directly for Swagger, curl and the debugger.

Two things stay laptop-only, and are supposed to: activation and password-reset links, and the email tracking pixel. Those are absolute URLs the api builds from FRONTEND_URL and APP_URL, and dev mail is read on the laptop anyway.

VITE_APP_URL still overrides the origin if you point the frontend at a deployed api — set a matching entry in security.cors.allowed-origins (services/api/src/main/resources/application-dev.yaml) when you do, since that request is cross-origin again.

Environment files

The compose files include sensible defaults. For production-like secrets, copy the examples:

cp services/api/.db.example.env services/api/.db.env

Run tests

./gradlew :services:api:test
./gradlew :services:api:integrationTest

Generate OpenAPI TypeScript client

./scripts/generate_openapi.sh

Remote debugging

The dev API container exposes JDWP on localhost:5005. IntelliJ: Remote JVM Debug → host: localhost, port: 5005.


Production deployment

Production runs on a single-node NixOS + k3s + FluxCD stack. Flux reconciles manifests from platform/cluster/flux/ against main; Keel polls ghcr.io/esa-blueshell/* for new :latest tags and rolls the matching Deployments. There is no CI deploy step — pushing to main is the deploy.

Runbook: platform/docs/runbook.md.

build-push.yml publishes api and frontend images to GHCR.


Services at a glance

ServiceImageInternal portDescription
apighcr.io/esa-blueshell/api8080Spring Boot REST API
frontendghcr.io/esa-blueshell/frontend3000Vue.js SPA
dbmariadb:10.113306Application database

Security

  • JWT authentication (Spring Security)
  • SQL injection prevention (JPA parameterized queries)
  • XSS protection (Vue template escaping + CSP headers)
  • CORS restricted to blueshell domains
  • TLS 1.2+ with Let's Encrypt certificates (auto-renewed)

API documentation

  • Development:http://localhost:8080/swagger-ui (set SPRINGDOC_API_DOCS_ENABLED=true)
  • Production: disabled (OpenAPI docs off in the prod profile)
  • OpenAPI spec:/api/v3/api-docs

Contributing

  1. Create a feature branch from main
  2. Make changes with hot reload in the dev environment
  3. Run tests: ./gradlew :services:api:test
  4. If API endpoints changed: ./scripts/generate_openapi.sh
  5. Open a pull request

Support

Questions or issues: board@blueshell.utwente.nl

About

No description, website, or topics provided.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

793 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Blueshell Website

Full-stack web application for ESA Blueshell — a student association platform for managing members, events, payments, and communications.

Built with Spring Boot 4 (Kotlin) backend and Vue.js 3 (TypeScript) frontend.


Architecture

Domain-Driven Design with a clean layered architecture:

LayerTechnology
Backend APISpring Boot 4 (Kotlin), Spring Security, Spring Data JPA
FrontendVue.js 3, TypeScript, Vuetify 3, Vite
DatabaseMariaDB 10.11
EmailStalwart SMTP relay (transactional)
SecretsHashiCorp Vault + Vault Secrets Operator
Auth / OIDCAPI issues tokens (Spring Authorization Server) for Headlamp, Vault
Reverse proxyTraefik v3 in k3s (Let's Encrypt DNS-01 via Cloudflare)
Orchestrationk3s (single node) + FluxCD GitOps
OSNixOS on Contabo VPS

Architecture decisions:docs/adr/ADR-INDEX.md


Development setup

Prerequisites

  • Docker + Docker Compose v2
  • Java 21 (optional — for running the API outside Docker)
  • Node.js + Yarn Berry (optional — for running the frontend outside Docker)
  • A GitHub token with read:packages — both images pull the Brevo and Discord clients from GitHub Packages, which authenticates even for public packages

A token for the image builds

The builds read the token from .secrets/ as a BuildKit secret, so it never lands in the image history. Write the files once — they are gitignored, and the compose files expect exactly these three names:

mkdir -p .secrets && chmod 700 .secrets
gh auth token > .secrets/github_token # Gradle → maven.pkg.github.com
gh auth token > .secrets/node_auth_token # Yarn → npm.pkg.github.com
gh api user -q .login > .secrets/github_actor # username the maven registry wants
chmod 600 .secrets/*

A classic PAT with read:packages works just as well as the gh token — paste it into the two token files instead. Compose refuses to start when a secret file is missing, so create all three even if you leave one empty.

The tokens are read at image build time only. The api image bakes Gradle's dependency cache in, so the running container starts bootRun --offline and never reaches for the registry. Rebuild the image (docker compose build api) when a dependency version changes; source edits hot-reload off the bind mount as before.

Start the dev environment

docker compose up -d

This starts:

ServiceURLNotes
APIhttp://localhost:8080Hot-reload via Gradle
Frontendhttp://localhost:3000Hot-reload via Vite
Swagger UIhttp://localhost:8080/swagger-uiSet SPRINGDOC_API_DOCS_ENABLED=true
MariaDBlocalhost:3307
Stalwarthttp://localhost:8085Dev MTA admin UI (SMTP :1025, IMAP :1143, admin admin/admin)

Reaching the api, and trying the site on a phone

The frontend reaches the api at the page's own origin under /api, the same shape production serves: http://localhost:3000/api from this machine, and http://<your-lan-ip>:3000/api from anything else on the network. Vite proxies /api and strips the prefix, mirroring the strip-api-prefix Traefik middleware, so no address is configured anywhere and a phone needs nothing but the URL:

ipconfig getifaddr en0 # then open http://<that>:3000 on the phone

The api's own port stays published, so http://localhost:8080 still answers directly for Swagger, curl and the debugger.

Two things stay laptop-only, and are supposed to: activation and password-reset links, and the email tracking pixel. Those are absolute URLs the api builds from FRONTEND_URL and APP_URL, and dev mail is read on the laptop anyway.

VITE_APP_URL still overrides the origin if you point the frontend at a deployed api — set a matching entry in security.cors.allowed-origins (services/api/src/main/resources/application-dev.yaml) when you do, since that request is cross-origin again.

Environment files

The compose files include sensible defaults. For production-like secrets, copy the examples:

cp services/api/.db.example.env services/api/.db.env

Run tests

./gradlew :services:api:test
./gradlew :services:api:integrationTest

Generate OpenAPI TypeScript client

./scripts/generate_openapi.sh

Remote debugging

The dev API container exposes JDWP on localhost:5005. IntelliJ: Remote JVM Debug → host: localhost, port: 5005.


Production deployment

Production runs on a single-node NixOS + k3s + FluxCD stack. Flux reconciles manifests from platform/cluster/flux/ against main; Keel polls ghcr.io/esa-blueshell/* for new :latest tags and rolls the matching Deployments. There is no CI deploy step — pushing to main is the deploy.

Runbook: platform/docs/runbook.md.

build-push.yml publishes api and frontend images to GHCR.


Services at a glance

ServiceImageInternal portDescription
apighcr.io/esa-blueshell/api8080Spring Boot REST API
frontendghcr.io/esa-blueshell/frontend3000Vue.js SPA
dbmariadb:10.113306Application database

Security

  • JWT authentication (Spring Security)
  • SQL injection prevention (JPA parameterized queries)
  • XSS protection (Vue template escaping + CSP headers)
  • CORS restricted to blueshell domains
  • TLS 1.2+ with Let's Encrypt certificates (auto-renewed)

API documentation

  • Development:http://localhost:8080/swagger-ui (set SPRINGDOC_API_DOCS_ENABLED=true)
  • Production: disabled (OpenAPI docs off in the prod profile)
  • OpenAPI spec:/api/v3/api-docs

Contributing

  1. Create a feature branch from main
  2. Make changes with hot reload in the dev environment
  3. Run tests: ./gradlew :services:api:test
  4. If API endpoints changed: ./scripts/generate_openapi.sh
  5. Open a pull request

Support

Questions or issues: board@blueshell.utwente.nl

About

No description, website, or topics provided.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

793 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Blueshell Website

Full-stack web application for ESA Blueshell — a student association platform for managing members, events, payments, and communications.

Built with Spring Boot 4 (Kotlin) backend and Vue.js 3 (TypeScript) frontend.


Architecture

Domain-Driven Design with a clean layered architecture:

LayerTechnology
Backend APISpring Boot 4 (Kotlin), Spring Security, Spring Data JPA
FrontendVue.js 3, TypeScript, Vuetify 3, Vite
DatabaseMariaDB 10.11
EmailStalwart SMTP relay (transactional)
SecretsHashiCorp Vault + Vault Secrets Operator
Auth / OIDCAPI issues tokens (Spring Authorization Server) for Headlamp, Vault
Reverse proxyTraefik v3 in k3s (Let's Encrypt DNS-01 via Cloudflare)
Orchestrationk3s (single node) + FluxCD GitOps
OSNixOS on Contabo VPS

Architecture decisions:docs/adr/ADR-INDEX.md


Development setup

Prerequisites

  • Docker + Docker Compose v2
  • Java 21 (optional — for running the API outside Docker)
  • Node.js + Yarn Berry (optional — for running the frontend outside Docker)
  • A GitHub token with read:packages — both images pull the Brevo and Discord clients from GitHub Packages, which authenticates even for public packages

A token for the image builds

The builds read the token from .secrets/ as a BuildKit secret, so it never lands in the image history. Write the files once — they are gitignored, and the compose files expect exactly these three names:

mkdir -p .secrets && chmod 700 .secrets
gh auth token > .secrets/github_token # Gradle → maven.pkg.github.com
gh auth token > .secrets/node_auth_token # Yarn → npm.pkg.github.com
gh api user -q .login > .secrets/github_actor # username the maven registry wants
chmod 600 .secrets/*

A classic PAT with read:packages works just as well as the gh token — paste it into the two token files instead. Compose refuses to start when a secret file is missing, so create all three even if you leave one empty.

The tokens are read at image build time only. The api image bakes Gradle's dependency cache in, so the running container starts bootRun --offline and never reaches for the registry. Rebuild the image (docker compose build api) when a dependency version changes; source edits hot-reload off the bind mount as before.

Start the dev environment

docker compose up -d

This starts:

ServiceURLNotes
APIhttp://localhost:8080Hot-reload via Gradle
Frontendhttp://localhost:3000Hot-reload via Vite
Swagger UIhttp://localhost:8080/swagger-uiSet SPRINGDOC_API_DOCS_ENABLED=true
MariaDBlocalhost:3307
Stalwarthttp://localhost:8085Dev MTA admin UI (SMTP :1025, IMAP :1143, admin admin/admin)

Reaching the api, and trying the site on a phone

The frontend reaches the api at the page's own origin under /api, the same shape production serves: http://localhost:3000/api from this machine, and http://<your-lan-ip>:3000/api from anything else on the network. Vite proxies /api and strips the prefix, mirroring the strip-api-prefix Traefik middleware, so no address is configured anywhere and a phone needs nothing but the URL:

ipconfig getifaddr en0 # then open http://<that>:3000 on the phone

The api's own port stays published, so http://localhost:8080 still answers directly for Swagger, curl and the debugger.

Two things stay laptop-only, and are supposed to: activation and password-reset links, and the email tracking pixel. Those are absolute URLs the api builds from FRONTEND_URL and APP_URL, and dev mail is read on the laptop anyway.

VITE_APP_URL still overrides the origin if you point the frontend at a deployed api — set a matching entry in security.cors.allowed-origins (services/api/src/main/resources/application-dev.yaml) when you do, since that request is cross-origin again.

Environment files

The compose files include sensible defaults. For production-like secrets, copy the examples:

cp services/api/.db.example.env services/api/.db.env

Run tests

./gradlew :services:api:test
./gradlew :services:api:integrationTest

Generate OpenAPI TypeScript client

./scripts/generate_openapi.sh

Remote debugging

The dev API container exposes JDWP on localhost:5005. IntelliJ: Remote JVM Debug → host: localhost, port: 5005.


Production deployment

Production runs on a single-node NixOS + k3s + FluxCD stack. Flux reconciles manifests from platform/cluster/flux/ against main; Keel polls ghcr.io/esa-blueshell/* for new :latest tags and rolls the matching Deployments. There is no CI deploy step — pushing to main is the deploy.

Runbook: platform/docs/runbook.md.

build-push.yml publishes api and frontend images to GHCR.


Services at a glance

ServiceImageInternal portDescription
apighcr.io/esa-blueshell/api8080Spring Boot REST API
frontendghcr.io/esa-blueshell/frontend3000Vue.js SPA
dbmariadb:10.113306Application database

Security

  • JWT authentication (Spring Security)
  • SQL injection prevention (JPA parameterized queries)
  • XSS protection (Vue template escaping + CSP headers)
  • CORS restricted to blueshell domains
  • TLS 1.2+ with Let's Encrypt certificates (auto-renewed)

API documentation

  • Development:http://localhost:8080/swagger-ui (set SPRINGDOC_API_DOCS_ENABLED=true)
  • Production: disabled (OpenAPI docs off in the prod profile)
  • OpenAPI spec:/api/v3/api-docs

Contributing

  1. Create a feature branch from main
  2. Make changes with hot reload in the dev environment
  3. Run tests: ./gradlew :services:api:test
  4. If API endpoints changed: ./scripts/generate_openapi.sh
  5. Open a pull request

Support

Questions or issues: board@blueshell.utwente.nl

About

No description, website, or topics provided.

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages