diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..0d8b22a --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -0,0 +1,108 @@ +name: Release Build +run-name: Build and Release by ${{ gitea.actor }} + +on: + push: + branches: + - main + +jobs: + build-docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build and Push Server + run: | + cd server + docker build -t ${{ vars.REGISTRY_URL }}/zen-kanji-server:latest . + docker push ${{ vars.REGISTRY_URL }}/zen-kanji-server:latest + + - name: Build and Push Client + run: | + cd client + docker build \ + --target production-stage \ + --build-arg VITE_API_URL=${{ vars.VITE_API_URL }} \ + -t ${{ vars.REGISTRY_URL }}/zen-kanji-client:latest . + docker push ${{ vars.REGISTRY_URL }}/zen-kanji-client:latest + + build-android-and-release: + runs-on: ubuntu-latest + needs: build-docker + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: "temurin" + + - name: Setup Android SDK (Manual) + run: | + export ANDROID_HOME=$HOME/android-sdk + CMDLINE_VERSION=11076708 + + mkdir -p $ANDROID_HOME/cmdline-tools + + echo "Downloading SDK..." + wget -q https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_VERSION}_latest.zip -O cmdline-tools.zip + + unzip -q cmdline-tools.zip + mv cmdline-tools $ANDROID_HOME/cmdline-tools/latest + rm cmdline-tools.zip + + yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses || true + + echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV + echo "ANDROID_SDK_ROOT=$ANDROID_HOME" >> $GITHUB_ENV + echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> $GITHUB_PATH + echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH + + - name: Install and Build Web App + working-directory: client + env: + VITE_API_URL: ${{ vars.VITE_API_URL }} + run: | + npm ci + npm run build:android + + - name: Sync Capacitor to Android + working-directory: client + run: npx cap sync android + + - name: Decode Keystore + run: | + cd client/android/app + echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > my-release-key.jks + + - name: Build Release APK + working-directory: client/android + env: + RELEASE_KEY_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_RELEASE_KEY_PASSWORD }} + RELEASE_KEY_ALIAS: ${{ secrets.ORG_GRADLE_PROJECT_RELEASE_KEY_ALIAS }} + run: | + chmod +x gradlew + ./gradlew assembleRelease + + - name: Create Release + uses: https://gitea.com/actions/gitea-release-action@v1 + with: + tag_name: v1.0.${{ gitea.run_number }} + name: Release v1.0.${{ gitea.run_number }} + body: | + Automated release for commit ${{ gitea.sha }}. + + **Docker Images:** + - Server: `${{ vars.REGISTRY }}/${{ vars.SERVER_IMAGE }}:latest` + - Client: `${{ vars.REGISTRY }}/${{ vars.CLIENT_IMAGE }}:latest` + files: | + client/android/app/build/outputs/apk/release/*.apk + api_key: ${{ secrets.GITHUB_TOKEN }} diff --git a/client/.env.android b/client/.env.android deleted file mode 100644 index e1418f3..0000000 --- a/client/.env.android +++ /dev/null @@ -1,2 +0,0 @@ -CAP_ENV=dev -VITE_API_URL=https://zenkanji-api.crylia.de diff --git a/client/.gitignore b/client/.gitignore index 0401cea..fa52c07 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -1,5 +1,3 @@ node_modules .env .env.android -my-release-key.jks -gradle.properties diff --git a/client/Dockerfile b/client/Dockerfile index 3e7e8cc..4d81fa8 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,15 +1,31 @@ -FROM node:24-alpine AS dev-stage +# Stage 1: Build the Application +FROM node:20-alpine AS build-stage WORKDIR /app +ARG VITE_API_URL +ENV VITE_API_URL=$VITE_API_URL + COPY package*.json ./ - RUN npm ci +COPY . . +RUN npm run build + +FROM node:20-alpine AS dev-stage +WORKDIR /app +COPY package*.json ./ +RUN npm ci COPY . . EXPOSE 5173 CMD ["npm", "run", "dev", "--", "--host"] -FROM dev-stage AS build-stage -ARG VITE_API_URL -ENV VITE_API_URL=$VITE_API_URL -RUN npm run build +FROM nginx:alpine AS production-stage +RUN mkdir -p /run/nginx + +COPY --from=build-stage /app/dist /usr/share/nginx/html + +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/client/android/app/build.gradle b/client/android/app/build.gradle index 266006c..1d78df7 100644 --- a/client/android/app/build.gradle +++ b/client/android/app/build.gradle @@ -16,12 +16,28 @@ android { } signingConfigs { release { + if (file("my-release-key.jks").exists()) { storeFile file("my-release-key.jks") - storePassword RELEASE_KEY_PASSWORD - keyAlias "my-key-alias" - keyPassword RELEASE_KEY_PASSWORD + if (project.hasProperty("RELEASE_KEY_PASSWORD")) { + storePassword RELEASE_KEY_PASSWORD + keyPassword RELEASE_KEY_PASSWORD + } else if (System.getenv("RELEASE_KEY_PASSWORD") != null) { + storePassword System.getenv("RELEASE_KEY_PASSWORD") + keyPassword System.getenv("RELEASE_KEY_PASSWORD") + } else { + storePassword "missing_password" + keyPassword "missing_password" + } + if (project.hasProperty("RELEASE_KEY_ALIAS")) { + keyAlias RELEASE_KEY_ALIAS + } else if (System.getenv("RELEASE_KEY_ALIAS") != null) { + keyAlias System.getenv("RELEASE_KEY_ALIAS") + } else { + keyAlias "my-key-alias" + } } } + } buildTypes { release { signingConfig signingConfigs.release diff --git a/client/android/gradle.properties b/client/android/gradle.properties new file mode 100644 index 0000000..5e2afb4 --- /dev/null +++ b/client/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1536m +android.useAndroidX=true diff --git a/client/index.html b/client/index.html index 2d09bbf..515ee18 100644 --- a/client/index.html +++ b/client/index.html @@ -5,7 +5,7 @@ Zen Kanji - + diff --git a/client/nginx.conf b/client/nginx.conf new file mode 100644 index 0000000..90b2cc3 --- /dev/null +++ b/client/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, no-transform"; + } +} diff --git a/client/vite.config.js b/client/vite.config.js index de3ed39..9759d43 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -1,29 +1,33 @@ import { fileURLToPath, URL } from 'node:url'; -import { defineConfig } from 'vite'; +import { defineConfig, loadEnv } from 'vite'; import vue from '@vitejs/plugin-vue'; import vueDevTools from 'vite-plugin-vue-devtools'; -export default defineConfig({ - plugins: [ - vue(), - vueDevTools(), - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, - server: { - allowedHosts: [ - 'zenkanji.crylia.de', +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + + return { + plugins: [ + vue(), + vueDevTools(), ], - host: true, - port: 5173, - strictPort: true, - hmr: { - host: 'zenkanji.crylia.de', - protocol: 'wss', - clientPort: 443, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, }, - }, + server: { + allowedHosts: [ + 'localhost', + ], + host: true, + port: 5173, + strictPort: true, + hmr: { + host: 'localhost', + protocol: 'ws', + clientPort: 5173, + }, + }, + }; }); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..fb948c9 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,33 @@ +services: + mongo: + image: mongo:6 + container_name: zen_mongo + restart: always + ports: + - "27017:27017" + volumes: + - mongo-data:/data/db + networks: + - zen-network + + server: + depends_on: + - mongo + environment: + - MONGO_URI=mongodb://mongo:27017/zenkanji + volumes: + - ./server:/app + - /app/node_modules + ports: + - "3000:3000" + command: npm run dev + + client: + build: + target: dev-stage + ports: + - "5173:5173" + volumes: + - ./client:/app + - /app/node_modules + command: npm run dev -- --host diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index fa20eb0..1f888eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,47 +1,23 @@ services: - mongo: - image: mongo:6 - container_name: zen_mongo - restart: always - ports: - - "27017:27017" - volumes: - - mongo-data:/data/db - networks: - - zen-network - server: - build: ./server + build: + context: ./server container_name: zen_server - restart: always - ports: - - "3000:3000" env_file: - - .env - depends_on: - - mongo + - ./server/.env networks: - zen-network - volumes: - - ./server:/app - - /app/node_modules client: build: context: ./client - target: dev-stage container_name: zen_client - ports: - - "5173:5173" env_file: - - .env + - ./client/.env depends_on: - server networks: - zen-network - volumes: - - ./client:/app - - /app/node_modules volumes: mongo-data: diff --git a/server/server.js b/server/server.js index 14d769e..f64cabe 100644 --- a/server/server.js +++ b/server/server.js @@ -11,29 +11,32 @@ const fastify = Fastify({ logger: true }); await connectDB(); const allowedOrigins = [ - 'http://192.168.0.26:5169', - 'http://192.168.0.26:5173', - 'http://localhost:5173', - 'http://localhost', + process.env.SERVER_EXT_ACCESS, + process.env.SERVER_INT_ACCESS, 'https://localhost', 'capacitor://localhost', 'https://10.0.2.2:5173', - 'https://zenkanji.crylia.de' -]; - -if (process.env.CORS_ORIGINS) { - const prodOrigins = process.env.CORS_ORIGINS.split(','); - allowedOrigins.push(...prodOrigins); -} + 'http://localhost:5173' +].filter(Boolean).map(uri => uri.replace(/\/$/, '')); await fastify.register(cors, { - origin: allowedOrigins, - methods: ['GET', 'POST', 'PUT', 'DELETE'], + origin: (origin, cb) => { + if (!origin) return cb(null, true); + + if (allowedOrigins.includes(origin)) { + return cb(null, true); + } + + console.log(`CORS BLOCKED: Browser sent "${origin}". Allowed list:`, allowedOrigins); + + cb(new Error("Not allowed by CORS")); + }, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], credentials: true }); await fastify.register(jwt, { - secret: JWT_SECRET + secret: process.env.JWT_SECRET }); fastify.decorate('authenticate', async function (req, reply) { @@ -67,7 +70,6 @@ await fastify.register(routes); const start = async () => { try { await fastify.listen({ port: PORT, host: '0.0.0.0' }); - console.log(`Server running at http://localhost:${PORT}`); } catch (err) { fastify.log.error(err); process.exit(1); diff --git a/server/src/config/constants.js b/server/src/config/constants.js index d07dfca..5d18238 100644 --- a/server/src/config/constants.js +++ b/server/src/config/constants.js @@ -1,4 +1,3 @@ -export const PORT = process.env.PORT || 3000; -export const MONGO_URI = process.env.MONGO_URI || 'mongodb://mongo:27017/zenkanji' || 'mongodb://192.168.0.26:27017/zenkanji'; -export const SRS_TIMINGS_HOURS = [0, 2, 4, 8, 23, 47]; +export const PORT = 3000; +export const MONGO_URI = process.env.MONGO_URI export const JWT_SECRET = process.env.JWT_SECRET;