Compare commits
6 Commits
d1d49ecc49
...
v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
8587b9577a
|
|||
|
9911eaf812
|
|||
|
cc3996b384
|
|||
|
1b946789a4
|
|||
|
fbe10a0288
|
|||
|
bfafd63b33
|
@@ -28,6 +28,21 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 26
|
||||
cache: 'npm'
|
||||
cache-dependency-path: ./frontend/package-lock.json
|
||||
|
||||
- name: Install Dependencies
|
||||
working-directory: ./frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Run Vite Build
|
||||
working-directory: ./frontend
|
||||
run: npm run build
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
|
||||
+10
@@ -1,15 +1,25 @@
|
||||
# Frontend build stage
|
||||
FROM node:22-alpine AS frontend-builder
|
||||
WORKDIR /app
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm ci
|
||||
COPY frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
# Build stage
|
||||
FROM golang:latest AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
COPY --from=frontend-builder /app/dist ./frontend/dist
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -v -o outfit-voting-abi26 ./cmd/
|
||||
|
||||
# Final stage
|
||||
FROM debian:bookworm-slim
|
||||
WORKDIR /root/
|
||||
EXPOSE 4000
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /app/outfit-voting-abi26 /usr/local/bin/outfit-voting-abi26
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
services:
|
||||
outfit-voting-abi26:
|
||||
image: tueem/outfit-voting-abi26:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DB_HOST: "HOST"
|
||||
DB_PORT: "PORT"
|
||||
DB_NAME: "NAME"
|
||||
DB_USER: "USER"
|
||||
DB_PASS: "PASS"
|
||||
OIDC_ISSUER: "OIDC_ISSUER"
|
||||
OIDC_CLIENTID: "OIDC_CLIENTID"
|
||||
OIDC_SECRET: "OIDC_SECRET"
|
||||
OIDC_REDIRECT: "OIDC_REDIRECT"
|
||||
SIGNINGKEY: "SIGNINGKEY"
|
||||
IMPRINTURL: "IMPRINTURL"
|
||||
networks:
|
||||
- web-network
|
||||
- backend
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.myapp.rule=Host(`example.com`)"
|
||||
- "traefik.http.routers.myapp.entrypoints=websecure"
|
||||
- "traefik.http.routers.myapp.tls=true"
|
||||
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.myapp.loadbalancer.server.port=4000"
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
shm_size: 128mb
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "PASS"
|
||||
volumes:
|
||||
- db:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U myuser -d mydatabase"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
networks:
|
||||
- backend
|
||||
traefik:
|
||||
image: traefik:v3.1
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
ports:
|
||||
- "80:80" # HTTP port
|
||||
- "443:443" # HTTPS port
|
||||
command:
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
|
||||
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
|
||||
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
|
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./traefik-ssl:/etc/traefik/acme
|
||||
networks:
|
||||
- web-network
|
||||
|
||||
networks:
|
||||
web-network:
|
||||
backend:
|
||||
|
||||
volumes:
|
||||
db:
|
||||
|
||||
@@ -42,4 +42,8 @@ if [ -n "$SIGNINGKEY" ]; then
|
||||
CMD="$CMD --signingKey $SIGNINGKEY"
|
||||
fi
|
||||
|
||||
if [ -n "$IMPRINTURL" ]; then
|
||||
CMD="$CMD --imprinturl $IMPRINTURL"
|
||||
fi
|
||||
|
||||
eval exec $CMD "$@"
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Outfit-Voting Abi26</title>
|
||||
</head>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB |
@@ -134,6 +134,36 @@ export default function AdminPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteCategory = (cat: string) => {
|
||||
const catIds = options.filter(o => o.Category === cat).map(o => o.ID);
|
||||
if (catIds.length === 0) return;
|
||||
openDialog(
|
||||
"Kategorie leeren?",
|
||||
`Möchtest du wirklich alle ${catIds.length} Einträge in der Kategorie "${cat}" unwiderruflich löschen?`,
|
||||
async () => {
|
||||
await api.bulkDeleteVoteEntries(catIds);
|
||||
setSelectedIds(new Set([...selectedIds].filter(id => !catIds.includes(id))));
|
||||
loadData();
|
||||
closeDialog();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteAll = () => {
|
||||
if (options.length === 0) return;
|
||||
openDialog(
|
||||
"Alle Einträge löschen?",
|
||||
`ACHTUNG: Möchtest du wirklich ALLE ${options.length} Einträge unwiderruflich löschen?`,
|
||||
async () => {
|
||||
const allIds = options.map(o => o.ID);
|
||||
await api.bulkDeleteVoteEntries(allIds);
|
||||
setSelectedIds(new Set());
|
||||
loadData();
|
||||
closeDialog();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const saveEdit = async (id: string) => {
|
||||
await api.setVoteCount(id, editVotes);
|
||||
setEditId(null);
|
||||
@@ -205,13 +235,19 @@ export default function AdminPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedIds.size > 0 && (
|
||||
{selectedIds.size > 0 ? (
|
||||
<div className="bulk-actions panel">
|
||||
<span>{selectedIds.size} Einträge ausgewählt</span>
|
||||
<button className="btn-danger" onClick={handleBulkDelete}>
|
||||
<Trash2 size={18} /> Ausgewählte Löschen
|
||||
</button>
|
||||
</div>
|
||||
) : options.length > 0 && (
|
||||
<div className="bulk-actions panel" style={{ justifyContent: 'flex-end' }}>
|
||||
<button className="btn-danger" onClick={handleDeleteAll}>
|
||||
<Trash2 size={18} /> Alle Kategorien & Einträge löschen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="admin-tables">
|
||||
@@ -245,6 +281,7 @@ export default function AdminPage() {
|
||||
style={{ padding: '0.4rem 0.8rem', borderRadius: 'var(--radius-m)', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--panel-border)', fontSize: '0.9rem', width: '160px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<button className="btn-icon add-to-cat" onClick={() => {
|
||||
setBulkCategory(cat);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
@@ -252,6 +289,10 @@ export default function AdminPage() {
|
||||
}}>
|
||||
<Plus size={16} /> Hier hinzufügen
|
||||
</button>
|
||||
<button className="btn-icon delete" onClick={() => handleDeleteCategory(cat)}>
|
||||
<Trash2 size={16} /> Alle löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
|
||||
Reference in New Issue
Block a user