Compare commits

..

23 Commits

Author SHA1 Message Date
b0d66c9aa0 Bump version 2025-07-15 18:12:54 +02:00
6b23bfb39f Amend Upstream URL 2025-07-15 18:12:46 +02:00
202959dd9e Amend Upstream URL 2025-07-15 18:05:11 +02:00
53fc2ed8fb Amend Upstream URL 2025-07-15 17:51:54 +02:00
dfac93e1ff Amend Upstream URL 2025-07-15 17:51:02 +02:00
fc95c3fdfd Amend CORS and NGINX 2025-07-15 17:33:39 +02:00
8c9d38de35 Add missing item in Dockerfile 2025-07-15 16:21:19 +02:00
10d5edced9 Add missing item in Dockerfile 2025-07-15 16:18:48 +02:00
855996486b Bump version 2025-07-15 16:15:45 +02:00
9148bb3463 Amend CORS 2025-07-15 16:15:10 +02:00
edeffa7012 Bump version 2025-07-15 14:56:59 +02:00
3d244e5e83 Bump version 2025-07-15 14:53:59 +02:00
05f9255ef0 Amend API path 2025-07-15 14:53:43 +02:00
c942450e86 Amend API path 2025-07-15 14:45:22 +02:00
00759473f2 Correct Docker build CI/CD 2025-07-15 14:11:45 +02:00
d9acd72d7b Fix Dockerization 2025-07-15 14:09:42 +02:00
2639a5301a Add latest tag 2025-07-15 11:34:17 +02:00
460b7aee72 Add latest tag 2025-07-15 11:31:59 +02:00
3c393a1e0b Bump version 2025-07-15 11:27:03 +02:00
0189b0db15 Add Docker CI/CD 2025-07-15 11:18:30 +02:00
78b8c60329 Add Docker CI/CD 2025-07-15 11:11:39 +02:00
48bbc03a91 Add Docker CI/CD 2025-07-15 11:10:35 +02:00
d493800226 Add Docker CI/CD 2025-07-15 11:06:04 +02:00
14 changed files with 246 additions and 94 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.gitignore
openapitools.json
README.md
.vscode
.idea
.gitea

View File

@@ -1,4 +1,5 @@
name: Create and Push Release name: Create and Push Release
run-name: Create and Push Release
on: on:
workflow_dispatch: workflow_dispatch:
@@ -13,6 +14,9 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get version - name: Get version
id: get-version id: get-version
run: | run: |
@@ -26,14 +30,16 @@ jobs:
username: ${{ secrets.CI_SERVICE_ACCOUNT }} username: ${{ secrets.CI_SERVICE_ACCOUNT }}
password: ${{ secrets.CI_SERVICE_ACCOUNT_PASSWORD }} password: ${{ secrets.CI_SERVICE_ACCOUNT_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
- name: Build & Push Image - name: Build & Push Image
env: env:
TAG: ${{ steps.get-version.outputs.version }} TAG: ${{ steps.get-version.outputs.version }}
QUARKUS_CONTAINER_IMAGE_USERNAME: ${{ secrets.CI_SERVICE_ACCOUNT }} QUARKUS_CONTAINER_IMAGE_USERNAME: ${{ secrets.CI_SERVICE_ACCOUNT }}
QUARKUS_CONTAINER_IMAGE_PASSWORD: ${{ secrets.CI_SERVICE_ACCOUNT_PASSWORD }} QUARKUS_CONTAINER_IMAGE_PASSWORD: ${{ secrets.CI_SERVICE_ACCOUNT_PASSWORD }}
run: | run: |
docker build -t $REGISTRY_URL/$IMAGE_OWNER/$IMAGE_IMAGE:$TAG . docker build -f docker/Dockerfile \
docker push $REGISTRY_URL/$IMAGE_OWNER/$IMAGE_IMAGE:$TAG -t $REGISTRY_URL/$IMAGE_OWNER/$IMAGE_NAME:$TAG \
-t $REGISTRY_URL/$IMAGE_OWNER/$IMAGE_NAME:latest \
.
docker push $REGISTRY_URL/$IMAGE_OWNER/$IMAGE_NAME:$TAG
docker push $REGISTRY_URL/$IMAGE_OWNER/$IMAGE_NAME:latest

View File

@@ -1,29 +0,0 @@
# Stage 1: Build application
FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
# Stage 2: Prepare static web server
FROM alpine:3.19 AS server-prep
RUN wget -O /tmp/sws.tar.gz \
https://github.com/static-web-server/static-web-server/releases/download/v2.17.0/static-web-server-v2.17.0-x86_64-unknown-linux-musl.tar.gz
RUN tar -xzf /tmp/sws.tar.gz -C /tmp \
--strip-components=1
# Stage 3: Create runtime image
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder --chown=nonroot:nonroot /app/dist /app
COPY --from=server-prep --chown=nonroot:nonroot /tmp/static-web-server /usr/local/bin/
USER nonroot
WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/static-web-server"]
CMD ["--port", "8080", "--root", "/app", "--log-level", "warn"]

36
docker/Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
# Stage 1: Build application
FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Create runtime image
FROM nginxinc/nginx-unprivileged:1.25-alpine AS runtime
# Create writable directories
USER root
RUN mkdir -p /runtime-config && \
chown nginx:nginx /runtime-config && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/run
USER nginx
# Copy built assets
COPY --from=builder --chown=nginx:nginx /app/dist /usr/share/nginx/html
# Copy nginx config template
COPY --chown=nginx:nginx nginx.conf.template /etc/nginx/templates/
# Copy entrypoint script
COPY --chown=nginx:nginx docker/entrypoint.sh /entrypoint.sh
# Make entrypoint executable
USER root
RUN chmod +x /entrypoint.sh
USER nginx
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]

22
docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
set -e
: "${API_HOST:=http://localhost}"
: "${API_PORT:=7070}"
# Create runtime config file in writable location
cat > /runtime-config/config.js <<EOF
window.__APP_CONFIG__ = {
API_HOST: "${API_HOST}",
API_PORT: "${API_PORT}"
};
EOF
# Generate nginx config from template (if using)
if [ -f "/etc/nginx/templates/nginx.conf.template" ]; then
envsubst '${API_HOST} ${API_PORT}' \
< /etc/nginx/templates/nginx.conf.template \
> /etc/nginx/nginx.conf
fi
# Start Nginx
exec nginx -g "daemon off;"

View File

@@ -1,6 +1,7 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<script src="/config.js"></script>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />

88
nginx.conf.template Normal file
View File

@@ -0,0 +1,88 @@
# Main nginx configuration
worker_processes auto;
error_log /dev/stderr warn;
pid /tmp/nginx.pid; # Use writable location for PID
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
access_log /dev/stdout;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
absolute_redirect off;
map $http_origin $cors_origin {
default "";
"~*" $http_origin;
}
resolver 127.0.0.11 valid=10s ipv6=off;
upstream backend {
server ${API_HOST}:${API_PORT};
}
server {
listen 7070;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Handle client-side routing
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
# Security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options "DENY";
add_header Referrer-Policy "strict-origin-when-cross-origin";
}
# Serve config.js from the writable location
location = /config.js {
alias /runtime-config/config.js;
add_header Cache-Control "no-store, no-cache, must-revalidate";
access_log off;
}
location ^~ /api/ {
# Proxy to Quarkus
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Relay CORS headers from the backend
proxy_pass_header Access-Control-Allow-Origin;
proxy_pass_header Access-Control-Allow-Methods;
proxy_pass_header Access-Control-Allow-Headers;
proxy_pass_header Access-Control-Allow-Credentials;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
}
}

8
package-lock.json generated
View File

@@ -1,15 +1,16 @@
{ {
"name": "dex-ui-vue", "name": "dex-ui-vue",
"version": "0.0.0", "version": "0.0.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "dex-ui-vue", "name": "dex-ui-vue",
"version": "0.0.0", "version": "0.0.1",
"dependencies": { "dependencies": {
"@primeuix/themes": "^1.2.1", "@primeuix/themes": "^1.2.1",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@types/node": "^24.0.14",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",
"axios": "^1.10.0", "axios": "^1.10.0",
"oidc-client-ts": "^3.3.0", "oidc-client-ts": "^3.3.0",
@@ -23,7 +24,6 @@
}, },
"devDependencies": { "devDependencies": {
"@openapitools/openapi-generator-cli": "^2.20.2", "@openapitools/openapi-generator-cli": "^2.20.2",
"@types/node": "^24.0.14",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"typescript": "~5.7.2", "typescript": "~5.7.2",
@@ -1386,7 +1386,6 @@
"version": "24.0.14", "version": "24.0.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz",
"integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.8.0" "undici-types": "~7.8.0"
@@ -4546,7 +4545,6 @@
"version": "7.8.0", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/universalify": { "node_modules/universalify": {

View File

@@ -1,7 +1,7 @@
{ {
"name": "dex-ui-vue", "name": "dex-ui-vue",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.7",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@primeuix/themes": "^1.2.1", "@primeuix/themes": "^1.2.1",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@types/node": "^24.0.14",
"@vueuse/core": "^13.5.0", "@vueuse/core": "^13.5.0",
"axios": "^1.10.0", "axios": "^1.10.0",
"oidc-client-ts": "^3.3.0", "oidc-client-ts": "^3.3.0",
@@ -25,7 +26,6 @@
}, },
"devDependencies": { "devDependencies": {
"@openapitools/openapi-generator-cli": "^2.20.2", "@openapitools/openapi-generator-cli": "^2.20.2",
"@types/node": "^24.0.14",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"typescript": "~5.7.2", "typescript": "~5.7.2",

View File

@@ -0,0 +1,4 @@
window.__RUNTIME_CONFIG__ = {
API_HOST: "__API_HOST__",
API_PORT: "__API_PORT__"
};

View File

@@ -1,27 +0,0 @@
import axios from "axios";
import {userManager} from "../stores/auth.ts";
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8080'
})
axiosInstance.interceptors.request.use(async (config) => {
const user = await userManager.getUser()
if (user?.access_token) {
config.headers.Authorization = `Bearer ${user.access_token}`
}
return config
})
// Handle token expiration
axiosInstance.interceptors.response.use(
response => response,
async (error) => {
if (error.response?.status === 401) {
await userManager.signinRedirect()
}
return Promise.reject(error)
}
)
export default axiosInstance;

View File

@@ -10,7 +10,8 @@ import {
AccordionContent, AccordionContent,
AccordionHeader, AccordionHeader,
AccordionPanel, AccordionPanel,
DatePicker, Fluid, DatePicker,
Fluid,
ToastService ToastService
} from "primevue"; } from "primevue";
import {createPinia} from "pinia"; import {createPinia} from "pinia";
@@ -22,10 +23,14 @@ import DecksView from "./views/deck/DecksView.vue";
import DeckView from "./views/deck/DeckView.vue"; import DeckView from "./views/deck/DeckView.vue";
import Callback from "./views/Callback.vue"; import Callback from "./views/Callback.vue";
import {useAuthStore} from "./stores/auth.ts"; import {useAuthStore} from "./stores/auth.ts";
import axiosInstance from "./api";
import SetsView from "./views/set/SetsView.vue"; import SetsView from "./views/set/SetsView.vue";
import JobsView from "./views/JobsView.vue"; import JobsView from "./views/JobsView.vue";
import {definePreset} from "@primeuix/themes"; import {definePreset} from "@primeuix/themes";
import {getApiUrl, initConfig} from "@/util/config.ts";
import axios from "axios";
// Initialize configuration from window object
initConfig((window as any).__APP_CONFIG__ || {})
export const DeckServiceKey = Symbol("deckServiceKey") export const DeckServiceKey = Symbol("deckServiceKey")
export const CardServiceKey = Symbol("cardServiceKey") export const CardServiceKey = Symbol("cardServiceKey")
@@ -134,11 +139,45 @@ router.beforeEach(async (to) => {
app.use(router); app.use(router);
app.use(ToastService) app.use(ToastService)
const deckService: DeckService = new DeckService(undefined, "http://localhost:8080", axiosInstance) let apiUrl = getApiUrl();
const cardService: CardService = new CardService(undefined, "http://localhost:8080", axiosInstance)
const setService: SetService = new SetService(undefined, "http://localhost:8080", axiosInstance) if (!apiUrl) {
const cardPrintService: CardPrintService = new CardPrintService(undefined, "http://localhost:8080", axiosInstance) apiUrl = `http://${apiUrl}`
const jobService: JobService = new JobService(undefined, "http://localhost:8080", axiosInstance) } else {
apiUrl = ''
}
const axiosInstance = axios.create({
baseURL: apiUrl,
headers: {
'Content-Type': 'application/json'
}
})
axiosInstance.interceptors.request.use(async (config) => {
const user = await userManager.getUser()
if (user?.access_token) {
config.headers.Authorization = `Bearer ${user.access_token}`
}
return config
})
// Handle token expiration
axiosInstance.interceptors.response.use(
response => response,
async (error) => {
if (error.response?.status === 401) {
await userManager.signinRedirect()
}
return Promise.reject(error)
}
)
const deckService: DeckService = new DeckService(undefined, undefined, axiosInstance)
const cardService: CardService = new CardService(undefined, undefined, axiosInstance)
const setService: SetService = new SetService(undefined, undefined, axiosInstance)
const cardPrintService: CardPrintService = new CardPrintService(undefined, undefined, axiosInstance)
const jobService: JobService = new JobService(undefined, undefined, axiosInstance)
app.provide(DeckServiceKey, deckService) app.provide(DeckServiceKey, deckService)
app.provide(CardServiceKey, cardService) app.provide(CardServiceKey, cardService)

View File

@@ -1,19 +0,0 @@
import darkAttribute from "/src/assets/DARK.svg"
import divineAttribute from "/src/assets/DIVINE.svg"
import earthAttribute from "/src/assets/EARTH.svg"
import fireAttribute from "/src/assets/FIRE.svg"
import laughAttribute from "/src/assets/LAUGH.svg"
import lightAttribute from "/src/assets/LIGHT.svg"
import waterAttribute from "/src/assets/WATER.svg"
import windAttribute from "/src/assets/WIND.svg"
export {
darkAttribute,
divineAttribute,
earthAttribute,
fireAttribute,
laughAttribute,
lightAttribute,
waterAttribute,
windAttribute
}

27
src/util/config.ts Normal file
View File

@@ -0,0 +1,27 @@
export interface AppConfig {
API_HOST: string;
API_PORT: number;
}
let runtimeConfig: AppConfig = {
API_HOST: import.meta.env.VITE_API_HOST || '',
API_PORT: import.meta.env.VITE_API_PORT || 8080
};
export function initConfig(config: Partial<AppConfig>) {
runtimeConfig = {
...runtimeConfig,
...config
};
}
export function getConfig(): AppConfig {
return runtimeConfig;
}
export function getApiUrl() {
if (!runtimeConfig.API_HOST) {
return null;
}
return `${runtimeConfig.API_HOST}:${runtimeConfig.API_PORT}`;
}