Add app version display in Footer and implement version API endpoint
Some checks failed
Code Analysis and Production Deploy / analyze (push) Successful in 2m49s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 1m54s
Code Analysis and Production Deploy / analyze (pull_request) Failing after 11s
Code Analysis and Production Deploy / deploy-production (pull_request) Has been skipped
Code Analysis and Production Deploy / deploy-test (pull_request) Has been skipped

- Updated Footer.vue to show the application version for logged-in users.
- Added a new API endpoint to return the application version from package.json.
- Enhanced code-analysis.yml to require package version changes for main PRs.
This commit is contained in:
Torsten Schulz (local)
2026-04-16 13:16:53 +02:00
parent 18da725567
commit 6fea2749e0
5 changed files with 101 additions and 1 deletions

View File

@@ -31,6 +31,10 @@ jobs:
node -v node -v
npm -v npm -v
- name: Require package version change for main PRs
if: github.event_name == 'pull_request' && github.base_ref == 'main'
run: scripts/check-package-version-changed.sh origin/main
- name: gitleaks (Secrets Scanning) - name: gitleaks (Secrets Scanning)
run: | run: |
# Try to get the latest release asset URL # Try to get the latest release asset URL

View File

@@ -6,6 +6,13 @@
© {{ currentYear }} Harheimer TC 1954 e.V. © {{ currentYear }} Harheimer TC 1954 e.V.
</p> </p>
<div class="flex items-center space-x-6 text-sm relative"> <div class="flex items-center space-x-6 text-sm relative">
<span
v-if="isLoggedIn && appVersion"
class="text-xs text-gray-600"
title="Version"
>
v{{ appVersion }}
</span>
<NuxtLink <NuxtLink
to="/impressum" to="/impressum"
class="text-gray-400 hover:text-primary-400 transition-colors" class="text-gray-400 hover:text-primary-400 transition-colors"
@@ -89,7 +96,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { User, ChevronUp } from 'lucide-vue-next' import { User, ChevronUp } from 'lucide-vue-next'
@@ -97,11 +104,26 @@ const router = useRouter()
const authStore = useAuthStore() const authStore = useAuthStore()
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear()
const isMemberMenuOpen = ref(false) const isMemberMenuOpen = ref(false)
const appVersion = ref('')
// Reactive auth state from store // Reactive auth state from store
const isLoggedIn = computed(() => authStore.isLoggedIn) const isLoggedIn = computed(() => authStore.isLoggedIn)
// const isAdmin = computed(() => authStore.isAdmin) // const isAdmin = computed(() => authStore.isAdmin)
const loadAppVersion = async () => {
if (!isLoggedIn.value) {
appVersion.value = ''
return
}
try {
const response = await $fetch('/api/app/version')
appVersion.value = response.version || ''
} catch (_error) {
appVersion.value = ''
}
}
const toggleMemberMenu = () => { const toggleMemberMenu = () => {
isMemberMenuOpen.value = !isMemberMenuOpen.value isMemberMenuOpen.value = !isMemberMenuOpen.value
} }
@@ -116,6 +138,10 @@ onMounted(() => {
authStore.checkAuth() authStore.checkAuth()
}) })
watch(isLoggedIn, () => {
loadAppVersion()
}, { immediate: true })
// Close menu when clicking outside // Close menu when clicking outside
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if (!event.target.closest('.relative')) { if (!event.target.closest('.relative')) {

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_REF="${1:-origin/main}"
BASE_BRANCH="${BASE_REF#origin/}"
git fetch --no-tags --depth=1 origin "$BASE_BRANCH"
current_version="$(node -e 'const fs = require("fs"); const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); process.stdout.write(String(pkg.version || ""));')"
base_version="$(git show "$BASE_REF:package.json" | node -e 'let input = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", chunk => input += chunk); process.stdin.on("end", () => { const pkg = JSON.parse(input); process.stdout.write(String(pkg.version || "")); });')"
if [ -z "$current_version" ]; then
echo "ERROR: package.json enthält kein version-Feld."
exit 1
fi
if [ "$current_version" = "$base_version" ]; then
echo "ERROR: package.json version wurde nicht geändert."
echo "Base ($BASE_REF): $base_version"
echo "Current: $current_version"
echo "Bitte version in package.json erhöhen, bevor nach main gemerged wird."
exit 1
fi
echo "package.json version changed: $base_version -> $current_version"

View File

@@ -0,0 +1,25 @@
import { promises as fs } from 'fs'
import path from 'path'
import { getUserFromToken } from '../../utils/auth.js'
async function readPackageVersion() {
const packageJsonPath = path.join(process.cwd(), 'package.json')
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'))
return String(packageJson.version || '')
}
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth_token')
const user = token ? await getUserFromToken(token) : null
if (!user) {
throw createError({
statusCode: 401,
statusMessage: 'Nicht authentifiziert'
})
}
return {
version: await readPackageVersion()
}
})

View File

@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createEvent, mockSuccessReadBody } from './setup' import { createEvent, mockSuccessReadBody } from './setup'
import { readFileSync } from 'fs'
vi.mock('../server/utils/auth.js', () => { vi.mock('../server/utils/auth.js', () => {
return { return {
@@ -60,6 +61,7 @@ import logoutHandler from '../server/api/auth/logout.post.js'
import registerHandler from '../server/api/auth/register.post.js' import registerHandler from '../server/api/auth/register.post.js'
import resetPasswordHandler from '../server/api/auth/reset-password.post.js' import resetPasswordHandler from '../server/api/auth/reset-password.post.js'
import statusHandler from '../server/api/auth/status.get.js' import statusHandler from '../server/api/auth/status.get.js'
import versionHandler from '../server/api/app/version.get.js'
describe('Auth API Endpoints', () => { describe('Auth API Endpoints', () => {
afterEach(() => { afterEach(() => {
@@ -241,4 +243,22 @@ describe('Auth API Endpoints', () => {
expect(response.user).toMatchObject({ id: '1' }) expect(response.user).toMatchObject({ id: '1' })
}) })
}) })
describe('GET /api/app/version', () => {
it('verlangt Login', async () => {
const event = createEvent()
await expect(versionHandler(event)).rejects.toMatchObject({ statusCode: 401 })
})
it('liefert eingeloggten Benutzern die package.json-Version', async () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
authUtils.getUserFromToken.mockResolvedValue({ id: '1', email: 'user@example.com', roles: ['mitglied'] })
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
const response = await versionHandler(event)
expect(response.version).toBe(packageJson.version)
})
})
}) })