Compare commits
4 Commits
986b2056cd
...
d450175871
| Author | SHA1 | Date | |
|---|---|---|---|
| d450175871 | |||
|
|
6fea2749e0 | ||
|
|
18da725567 | ||
|
|
4d5fb43ebc |
@@ -3,7 +3,7 @@ name: Code Analysis and Production Deploy
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ main, dev ]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -31,6 +31,10 @@ jobs:
|
||||
node -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)
|
||||
run: |
|
||||
# Try to get the latest release asset URL
|
||||
@@ -118,4 +122,35 @@ jobs:
|
||||
-o BatchMode=yes \
|
||||
-p "${{ vars.PROD_PORT }}" \
|
||||
"${{ vars.PROD_USER }}@${{ vars.PROD_HOST }}" \
|
||||
"bash -lc 'cd /var/www/harheimertc && ./deploy-production.sh'"
|
||||
"bash -lc 'cd /var/www/harheimertc && git fetch origin main && git checkout -B main origin/main && git reset --hard origin/main && ./deploy-production.sh'"
|
||||
|
||||
deploy-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: analyze
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
||||
steps:
|
||||
- name: Prepare SSH
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
printf "%s" "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -p "${{ vars.PROD_PORT }}" "${{ vars.PROD_HOST }}" >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Test SSH connection
|
||||
run: |
|
||||
ssh -i ~/.ssh/id_ed25519 \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o BatchMode=yes \
|
||||
-p "${{ vars.PROD_PORT }}" \
|
||||
"${{ vars.PROD_USER }}@${{ vars.PROD_HOST }}" \
|
||||
"echo SSH OK"
|
||||
|
||||
- name: Run test deployment script
|
||||
run: |
|
||||
ssh -i ~/.ssh/id_ed25519 \
|
||||
-o BatchMode=yes \
|
||||
-p "${{ vars.PROD_PORT }}" \
|
||||
"${{ vars.PROD_USER }}@${{ vars.PROD_HOST }}" \
|
||||
"bash -lc 'cd /var/www/harheimertc.test && git fetch origin dev && git checkout -B dev origin/dev && git reset --hard origin/dev && ./deploy-test.sh'"
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
© {{ currentYear }} Harheimer TC 1954 e.V.
|
||||
</p>
|
||||
<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
|
||||
to="/impressum"
|
||||
class="text-gray-400 hover:text-primary-400 transition-colors"
|
||||
@@ -89,7 +96,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { User, ChevronUp } from 'lucide-vue-next'
|
||||
|
||||
@@ -97,11 +104,26 @@ const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const currentYear = new Date().getFullYear()
|
||||
const isMemberMenuOpen = ref(false)
|
||||
const appVersion = ref('')
|
||||
|
||||
// Reactive auth state from store
|
||||
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
||||
// 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 = () => {
|
||||
isMemberMenuOpen.value = !isMemberMenuOpen.value
|
||||
}
|
||||
@@ -116,6 +138,10 @@ onMounted(() => {
|
||||
authStore.checkAuth()
|
||||
})
|
||||
|
||||
watch(isLoggedIn, () => {
|
||||
loadAppVersion()
|
||||
}, { immediate: true })
|
||||
|
||||
// Close menu when clicking outside
|
||||
const handleClickOutside = (event) => {
|
||||
if (!event.target.closest('.relative')) {
|
||||
|
||||
@@ -208,7 +208,9 @@ git clean -fd \
|
||||
|
||||
# Pull latest changes
|
||||
echo " Pulling latest changes..."
|
||||
if ! git pull --ff-only; then
|
||||
git fetch origin main
|
||||
git checkout -B main origin/main
|
||||
if ! git reset --hard origin/main; then
|
||||
echo "ERROR: git pull fehlgeschlagen."
|
||||
echo ""
|
||||
echo "Häufige Ursache: SSH-Key für den aktuellen User fehlt."
|
||||
|
||||
@@ -67,6 +67,77 @@ has_tracked_files_under() {
|
||||
git ls-files "$prefix" | head -n 1 | grep -q .
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
if [ -f "package-lock.json" ]; then
|
||||
echo " Running: npm ci"
|
||||
npm ci
|
||||
else
|
||||
echo " WARNING: package-lock.json fehlt. Führe npm install aus..."
|
||||
npm install
|
||||
fi
|
||||
}
|
||||
|
||||
use_project_node() {
|
||||
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||
if [ -s "$NVM_DIR/nvm.sh" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$NVM_DIR/nvm.sh"
|
||||
if [ -f ".nvmrc" ]; then
|
||||
echo " Using Node version from .nvmrc..."
|
||||
nvm use
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_node_version() {
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "ERROR: Node.js ist nicht im PATH."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local node_version
|
||||
node_version="$(node -p 'process.versions.node')"
|
||||
if ! node -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit(major > 22 || (major === 22 && minor >= 12) ? 0 : 1)' >/dev/null 2>&1; then
|
||||
echo "ERROR: Node.js >= 22.12.0 wird benötigt, aktuell ist $node_version aktiv."
|
||||
echo "Bitte Node 22 installieren/aktivieren, z.B.:"
|
||||
echo " nvm install 22"
|
||||
echo " nvm alias default 22"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Node.js $node_version"
|
||||
}
|
||||
|
||||
sync_public_documents_to_build() {
|
||||
if [ ! -d "public/documents" ]; then
|
||||
echo " No public/documents directory to sync"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -d ".output/public" ]; then
|
||||
echo "ERROR: .output/public fehlt, kann public/documents nicht synchronisieren."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p ".output/public/documents"
|
||||
cp -a "public/documents/." ".output/public/documents/"
|
||||
echo " ✓ public/documents -> .output/public/documents synchronisiert"
|
||||
|
||||
local template_pdf="beitrittserklärung_template.pdf"
|
||||
if [ -f "public/documents/$template_pdf" ]; then
|
||||
local source_size output_size
|
||||
source_size=$(stat -f%z "public/documents/$template_pdf" 2>/dev/null || stat -c%s "public/documents/$template_pdf" 2>/dev/null || echo "0")
|
||||
output_size=$(stat -f%z ".output/public/documents/$template_pdf" 2>/dev/null || stat -c%s ".output/public/documents/$template_pdf" 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$source_size" != "$output_size" ] || [ "$source_size" = "0" ]; then
|
||||
echo "ERROR: .output/public/documents/$template_pdf stimmt nicht mit public/documents überein (Source: $source_size, Output: $output_size)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " ✓ $template_pdf im Build verifiziert ($output_size bytes)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "0. Ensuring persistent data directories (recommended)..."
|
||||
# IMPORTANT: Only symlink server/data if it's not tracked by git.
|
||||
if has_tracked_files_under "server/data"; then
|
||||
@@ -143,7 +214,9 @@ git clean -fd \
|
||||
|
||||
# Pull latest changes
|
||||
echo " Pulling latest changes..."
|
||||
if ! git pull --ff-only; then
|
||||
git fetch origin dev
|
||||
git checkout -B dev origin/dev
|
||||
if ! git reset --hard origin/dev; then
|
||||
echo "ERROR: git pull fehlgeschlagen."
|
||||
echo ""
|
||||
echo "Häufige Ursache: SSH-Key für den aktuellen User fehlt."
|
||||
@@ -170,7 +243,9 @@ fi
|
||||
# 3. Install dependencies
|
||||
echo ""
|
||||
echo "3. Installing dependencies..."
|
||||
npm install
|
||||
use_project_node
|
||||
ensure_node_version
|
||||
install_dependencies
|
||||
|
||||
# 4. Remove old build (but keep data!)
|
||||
echo ""
|
||||
@@ -204,7 +279,7 @@ fi
|
||||
if [ ! -d "node_modules" ]; then
|
||||
echo ""
|
||||
echo "WARNING: node_modules fehlt. Installiere Dependencies..."
|
||||
npm install
|
||||
install_dependencies
|
||||
fi
|
||||
|
||||
# 5. Build
|
||||
@@ -217,7 +292,7 @@ echo " (This may take a few minutes...)"
|
||||
echo " Checking dependencies..."
|
||||
if [ ! -f "node_modules/.package-lock.json" ] && [ ! -f "package-lock.json" ]; then
|
||||
echo " WARNING: package-lock.json fehlt. Führe npm install aus..."
|
||||
npm install
|
||||
install_dependencies
|
||||
fi
|
||||
|
||||
# Build mit expliziter Fehlerbehandlung und Output-Capture
|
||||
@@ -234,6 +309,10 @@ if [ "$BUILD_EXIT_CODE" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Synchronizing public documents into build output..."
|
||||
sync_public_documents_to_build
|
||||
|
||||
# Prüfe auf Warnungen im Build-Output, die auf Probleme hinweisen
|
||||
if echo "$BUILD_OUTPUT" | grep -qi "error\|failed\|missing"; then
|
||||
echo ""
|
||||
|
||||
@@ -10,7 +10,8 @@ try {
|
||||
// Helper function to create env object
|
||||
function createEnv(port) {
|
||||
return {
|
||||
NODE_ENV: 'production',
|
||||
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||
APP_ENV: process.env.APP_ENV || 'test',
|
||||
PORT: port,
|
||||
// Secrets/Config (loaded from .env above, if present)
|
||||
ENCRYPTION_KEY: process.env.ENCRYPTION_KEY,
|
||||
|
||||
25
scripts/check-package-version-changed.sh
Executable file
25
scripts/check-package-version-changed.sh
Executable 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"
|
||||
25
server/api/app/version.get.js
Normal file
25
server/api/app/version.get.js
Normal 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()
|
||||
}
|
||||
})
|
||||
@@ -24,6 +24,12 @@ async function loadConfig() {
|
||||
}
|
||||
|
||||
async function collectRecipients(config) {
|
||||
const isProduction = process.env.NODE_ENV === 'production' && process.env.APP_ENV !== 'test'
|
||||
|
||||
if (!isProduction) {
|
||||
return ['tsschulz@tsschulz.de']
|
||||
}
|
||||
|
||||
const recipients = []
|
||||
|
||||
// Vorstand
|
||||
|
||||
@@ -41,7 +41,7 @@ async function loadConfig() {
|
||||
* @returns {Array<string>} Email addresses
|
||||
*/
|
||||
function getEmailRecipients(data, config) {
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const isProduction = process.env.NODE_ENV === 'production' && process.env.APP_ENV !== 'test'
|
||||
|
||||
if (!isProduction) {
|
||||
return ['tsschulz@tsschulz.de']
|
||||
@@ -215,4 +215,4 @@ export async function sendRegistrationNotification(data) {
|
||||
console.error('sendRegistrationNotification failed:', error.message || error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createEvent, mockSuccessReadBody } from './setup'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
vi.mock('../server/utils/auth.js', () => {
|
||||
return {
|
||||
@@ -60,8 +61,14 @@ import logoutHandler from '../server/api/auth/logout.post.js'
|
||||
import registerHandler from '../server/api/auth/register.post.js'
|
||||
import resetPasswordHandler from '../server/api/auth/reset-password.post.js'
|
||||
import statusHandler from '../server/api/auth/status.get.js'
|
||||
import versionHandler from '../server/api/app/version.get.js'
|
||||
|
||||
describe('Auth API Endpoints', () => {
|
||||
afterEach(() => {
|
||||
delete process.env.NODE_ENV
|
||||
delete process.env.APP_ENV
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Setze SMTP-Credentials für Tests
|
||||
process.env.SMTP_USER = 'test@example.com'
|
||||
@@ -171,6 +178,30 @@ describe('Auth API Endpoints', () => {
|
||||
})
|
||||
expect(nodemailer.default.createTransport).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('benachrichtigt in Testumgebung nicht die Vorstand-Empfänger', async () => {
|
||||
process.env.NODE_ENV = 'production'
|
||||
process.env.APP_ENV = 'test'
|
||||
|
||||
const event = createEvent()
|
||||
mockSuccessReadBody({
|
||||
name: 'Max',
|
||||
email: 'max@example.com',
|
||||
password: '12345678',
|
||||
phone: '123',
|
||||
geburtsdatum: '2000-01-01'
|
||||
})
|
||||
authUtils.readUsers.mockResolvedValue([])
|
||||
authUtils.hashPassword.mockResolvedValue('hashed')
|
||||
authUtils.writeUsers.mockResolvedValue(true)
|
||||
|
||||
await registerHandler(event)
|
||||
|
||||
const transporter = nodemailer.default.createTransport.mock.results[0].value
|
||||
expect(transporter.sendMail).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
||||
to: 'tsschulz@tsschulz.de'
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/auth/reset-password', () => {
|
||||
@@ -212,4 +243,22 @@ describe('Auth API Endpoints', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createEvent, mockSuccessReadBody } from './setup'
|
||||
import fsPromises from 'fs/promises'
|
||||
import { promises as fs } from 'fs'
|
||||
@@ -26,6 +26,11 @@ import termineHandler from '../server/api/termine.get.js'
|
||||
import spielplaeneHandler from '../server/api/spielplaene.get.js'
|
||||
|
||||
describe('Öffentliche API-Endpunkte', () => {
|
||||
afterEach(() => {
|
||||
delete process.env.NODE_ENV
|
||||
delete process.env.APP_ENV
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Setze SMTP-Credentials für Tests
|
||||
process.env.SMTP_USER = 'test@example.com'
|
||||
@@ -58,6 +63,21 @@ describe('Öffentliche API-Endpunkte', () => {
|
||||
expect(response.success).toBe(true)
|
||||
expect(nodemailer.default.createTransport).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sendet in Testumgebung nicht an Vorstand-Empfänger', async () => {
|
||||
process.env.NODE_ENV = 'production'
|
||||
process.env.APP_ENV = 'test'
|
||||
|
||||
const event = createEvent()
|
||||
mockSuccessReadBody({ name: 'Max', email: 'max@example.com', subject: 'Frage', message: 'Hallo' })
|
||||
|
||||
await contactHandler(event)
|
||||
|
||||
const transporter = nodemailer.default.createTransport.mock.results[0].value
|
||||
expect(transporter.sendMail).toHaveBeenCalledWith(expect.objectContaining({
|
||||
to: 'tsschulz@tsschulz.de'
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/galerie', () => {
|
||||
|
||||
Reference in New Issue
Block a user