diff --git a/.env.prod.example b/.env.prod.example index 8ea6bbb..8463c89 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -1,14 +1,14 @@ -# Copy to `.env.prod` on the deploy host (CT502) and fill with real secrets. -# `.env.prod` is gitignored — never commit real credentials. - -# Database -POSTGRES_DB=xip -POSTGRES_USER=xip -POSTGRES_PASSWORD=change-me-to-a-strong-secret - -# Public origin (baked into the frontend build + used by the WS URL) -PUBLIC_URL=https://xip.kerboul.me - -# Paywall: "true" = open bar (everything free for everyone), "false" = paywall on -# (free only on localhost, per the README). -XIP_OPEN_BAR=true +# Copy to `.env.prod` on the deploy host (CT502) and fill with real secrets. +# `.env.prod` is gitignored — never commit real credentials. + +# Database +POSTGRES_DB=xip +POSTGRES_USER=xip +POSTGRES_PASSWORD=change-me-to-a-strong-secret + +# Public origin (baked into the frontend build + used by the WS URL) +PUBLIC_URL=https://xip.kerboul.me + +# Paywall: "true" = open bar (everything free for everyone), "false" = paywall on +# (free only on localhost, per the README). +XIP_OPEN_BAR=true diff --git a/.gitattributes b/.gitattributes index 23ef490..252ee15 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ -# Shell scripts must keep LF endings or they break with "bad interpreter" on Linux. -*.sh text eol=lf -docker-entrypoint.sh text eol=lf +# Shell scripts must keep LF endings or they break with "bad interpreter" on Linux. +*.sh text eol=lf +docker-entrypoint.sh text eol=lf diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 0183fc2..1c41fe5 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,35 +1,35 @@ -name: Deploy XIP - -# Auto-deploy on every push to main. The runner SSHes into the xip-app CT -# (Echelon CT502) and runs scripts/deploy.sh, which pulls + rebuilds the stack. -on: - push: - branches: [main] - workflow_dispatch: - -# Serialize deploys: never run two deploys against the CT at the same time -# (concurrent `docker compose up --build` on the same project races and fails). -concurrency: - group: deploy-xip-prod - cancel-in-progress: false - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Deploy over SSH to xip-app - env: - # Secrets via env (not inlined in the script) so the multi-line key - # keeps its newlines and never breaks shell quoting. - DEPLOY_HOST: ${{ secrets.XIP_DEPLOY_HOST }} - DEPLOY_USER: ${{ secrets.XIP_DEPLOY_USER }} - DEPLOY_KEY: ${{ secrets.XIP_DEPLOY_KEY }} - run: | - set -e - command -v ssh >/dev/null 2>&1 || (apt-get update && apt-get install -y --no-install-recommends openssh-client) - mkdir -p ~/.ssh - printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/id_ed25519 - chmod 600 ~/.ssh/id_ed25519 - ssh -i ~/.ssh/id_ed25519 \ - -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - "$DEPLOY_USER@$DEPLOY_HOST" 'bash /opt/xip/scripts/deploy.sh' +name: Deploy XIP + +# Auto-deploy on every push to main. The runner SSHes into the xip-app CT +# (Echelon CT502) and runs scripts/deploy.sh, which pulls + rebuilds the stack. +on: + push: + branches: [main] + workflow_dispatch: + +# Serialize deploys: never run two deploys against the CT at the same time +# (concurrent `docker compose up --build` on the same project races and fails). +concurrency: + group: deploy-xip-prod + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Deploy over SSH to xip-app + env: + # Secrets via env (not inlined in the script) so the multi-line key + # keeps its newlines and never breaks shell quoting. + DEPLOY_HOST: ${{ secrets.XIP_DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.XIP_DEPLOY_USER }} + DEPLOY_KEY: ${{ secrets.XIP_DEPLOY_KEY }} + run: | + set -e + command -v ssh >/dev/null 2>&1 || (apt-get update && apt-get install -y --no-install-recommends openssh-client) + mkdir -p ~/.ssh + printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh -i ~/.ssh/id_ed25519 \ + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "$DEPLOY_USER@$DEPLOY_HOST" 'bash /opt/xip/scripts/deploy.sh' diff --git a/.gitignore b/.gitignore index 5dc5fe9..5aef152 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -node_modules/ -dist/ -.env -.env.local -.env.prod -*.log -.DS_Store -Thumbs.db +node_modules/ +dist/ +.env +.env.local +.env.prod +*.log +.DS_Store +Thumbs.db diff --git a/DEPLOY.md b/DEPLOY.md index 65433eb..1cad01a 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,64 +1,64 @@ -# Déploiement XIP - -Production : **https://xip.kerboul.me** — déploiement continu sur push `main`. - -## Architecture (pattern Vireli, cluster SENTINEL) - -``` -Cloudflare (*.kerboul.me) ─► VPS WireGuard ─► Traefik (CT102, Cerberus) - │ Host(`xip.kerboul.me`) → http://192.168.1.242:80 - ▼ - CT502 « xip-app » (Echelon, Docker host) - ┌───────────────────────────────────────┐ - │ web (nginx:80) │ - │ ├── / → SPA Vue (statique) │ - │ ├── /api/ → backend:3000 │ - │ └── /ws → backend:3000 (WS) │ - │ backend (bun:3000, Hono + Prisma) │ - │ postgres:16 redis:7 │ - └───────────────────────────────────────┘ -``` - -Origine unique : le front (buildé avec `VITE_API_URL=https://xip.kerboul.me`) -appelle `/api` et `wss://xip.kerboul.me/ws`, nginx proxifie vers le backend. -Traefik termine le TLS (Let's Encrypt, DNS challenge Cloudflare). - -## CI/CD (Gitea Actions) - -`.gitea/workflows/deploy.yml` se déclenche sur push `main` (+ `workflow_dispatch`). -Le runner (CT121) se connecte en SSH au CT502 et exécute `scripts/deploy.sh` -(`git reset --hard origin/main` + `docker compose up -d --build`). - -Migrations Prisma + seed (idempotent) tournent au démarrage du conteneur backend -(`backend/docker-entrypoint.sh`). - -### Secrets du repo (Gitea → Settings → Actions → Secrets) -| Secret | Rôle | -|--------|------| -| `XIP_DEPLOY_HOST` | IP du CT502 (192.168.1.242) | -| `XIP_DEPLOY_USER` | utilisateur de déploiement (`deploy`) | -| `XIP_DEPLOY_KEY` | clé privée SSH autorisée sur le CT502 | - -## Fichiers - -| Fichier | Rôle | -|---------|------| -| `docker-compose.prod.yml` | stack prod (postgres, redis, backend, web) | -| `backend/Dockerfile` + `docker-entrypoint.sh` | image backend, migrate+seed au boot | -| `frontend/Dockerfile` | build Vite → nginx | -| `deploy/nginx.conf` | reverse proxy single-origin | -| `scripts/deploy.sh` | script de (re)déploiement sur le CT | -| `.env.prod` (non commité) | secrets : voir `.env.prod.example` | - -## Paywall - -`XIP_OPEN_BAR=true` (dans `.env.prod`) = **open bar** : toutes les fonctionnalités -payantes gratuites pour tout le monde. Mettre `false` pour réactiver le paywall -(gratuit uniquement en localhost). Logique centralisée dans `backend/src/lib/ip.ts` -(`isFree()`). - -## Redéploiement manuel - -```bash -ssh deploy@192.168.1.242 'bash /opt/xip/scripts/deploy.sh' -``` +# Déploiement XIP + +Production : **https://xip.kerboul.me** — déploiement continu sur push `main`. + +## Architecture (pattern Vireli, cluster SENTINEL) + +``` +Cloudflare (*.kerboul.me) ─► VPS WireGuard ─► Traefik (CT102, Cerberus) + │ Host(`xip.kerboul.me`) → http://192.168.1.242:80 + ▼ + CT502 « xip-app » (Echelon, Docker host) + ┌───────────────────────────────────────┐ + │ web (nginx:80) │ + │ ├── / → SPA Vue (statique) │ + │ ├── /api/ → backend:3000 │ + │ └── /ws → backend:3000 (WS) │ + │ backend (bun:3000, Hono + Prisma) │ + │ postgres:16 redis:7 │ + └───────────────────────────────────────┘ +``` + +Origine unique : le front (buildé avec `VITE_API_URL=https://xip.kerboul.me`) +appelle `/api` et `wss://xip.kerboul.me/ws`, nginx proxifie vers le backend. +Traefik termine le TLS (Let's Encrypt, DNS challenge Cloudflare). + +## CI/CD (Gitea Actions) + +`.gitea/workflows/deploy.yml` se déclenche sur push `main` (+ `workflow_dispatch`). +Le runner (CT121) se connecte en SSH au CT502 et exécute `scripts/deploy.sh` +(`git reset --hard origin/main` + `docker compose up -d --build`). + +Migrations Prisma + seed (idempotent) tournent au démarrage du conteneur backend +(`backend/docker-entrypoint.sh`). + +### Secrets du repo (Gitea → Settings → Actions → Secrets) +| Secret | Rôle | +|--------|------| +| `XIP_DEPLOY_HOST` | IP du CT502 (192.168.1.242) | +| `XIP_DEPLOY_USER` | utilisateur de déploiement (`deploy`) | +| `XIP_DEPLOY_KEY` | clé privée SSH autorisée sur le CT502 | + +## Fichiers + +| Fichier | Rôle | +|---------|------| +| `docker-compose.prod.yml` | stack prod (postgres, redis, backend, web) | +| `backend/Dockerfile` + `docker-entrypoint.sh` | image backend, migrate+seed au boot | +| `frontend/Dockerfile` | build Vite → nginx | +| `deploy/nginx.conf` | reverse proxy single-origin | +| `scripts/deploy.sh` | script de (re)déploiement sur le CT | +| `.env.prod` (non commité) | secrets : voir `.env.prod.example` | + +## Paywall + +`XIP_OPEN_BAR=true` (dans `.env.prod`) = **open bar** : toutes les fonctionnalités +payantes gratuites pour tout le monde. Mettre `false` pour réactiver le paywall +(gratuit uniquement en localhost). Logique centralisée dans `backend/src/lib/ip.ts` +(`isFree()`). + +## Redéploiement manuel + +```bash +ssh deploy@192.168.1.242 'bash /opt/xip/scripts/deploy.sh' +``` diff --git a/LICENSE b/LICENSE index 7a3094a..e007a57 100644 --- a/LICENSE +++ b/LICENSE @@ -1,11 +1,11 @@ -DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE -Version 2, December 2004 - -Copyright (C) 2004 Sam Hocevar - -Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. - -DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. You just DO WHAT THE FUCK YOU WANT TO. +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed. + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md index c8b0394..9cc52ac 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,110 @@ -# XIP +# XIP — Réseau social « sans modération » -Réseau social à consommer sans modération +SPA satirique : un chat public en temps réel où **ton pseudo = ton adresse IP**, +noyé sous les pubs et le merchandising. Catalogue de messages distant, liste +perso de favoris, statistiques dérivées, marketplace à crédits fictifs, thèmes +(dont WhatsApp). -## Concept +🌐 **Application déployée : https://xip.kerboul.me** -Faire un réseau social open sans contrôles ni modération. -Pas de compte, Pseudo = IP. -Merchandising à fond. -Envahit par des Pubs. +--- + +## Stack + +| Couche | Technologies | +|--------|--------------| +| Frontend | Vue 3 (` - - + + + + + + XIP + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json index c420332..1a0ccca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,21 +4,24 @@ "private": true, "scripts": { "dev": "vite", - "build": "vite build", + "build": "vue-tsc && vite build", + "preview": "vite preview", "typecheck": "vue-tsc --noEmit", - "preview": "vite preview" + "test": "vitest run", + "test:cov": "vitest run --coverage" }, "dependencies": { - "@ionic/vue": "^8.3.0", - "@ionic/vue-router": "^8.3.0", - "ionicons": "^7.4.0", "vue": "^3.5.0", "vue-router": "^4.4.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.1.0", + "@vitest/coverage-v8": "^2.1.0", + "@vue/test-utils": "^2.4.6", + "happy-dom": "^15.0.0", "typescript": "^5.6.0", "vite": "^5.4.0", + "vitest": "^2.1.0", "vue-tsc": "^2.1.0" } } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d96ca47..48e3313 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,8 +1,72 @@ + - + diff --git a/frontend/src/components/AdBand.vue b/frontend/src/components/AdBand.vue index 3c73836..8592590 100644 --- a/frontend/src/components/AdBand.vue +++ b/frontend/src/components/AdBand.vue @@ -1,163 +1,163 @@ - - - - - - + + + + + + diff --git a/frontend/src/components/AnimatedNumber.vue b/frontend/src/components/AnimatedNumber.vue index 47ce285..04cd0b2 100644 --- a/frontend/src/components/AnimatedNumber.vue +++ b/frontend/src/components/AnimatedNumber.vue @@ -1,50 +1,50 @@ - - - - + + + + diff --git a/frontend/src/components/ChatHeader.vue b/frontend/src/components/ChatHeader.vue index 7baf082..3d96d82 100644 --- a/frontend/src/components/ChatHeader.vue +++ b/frontend/src/components/ChatHeader.vue @@ -1,133 +1,133 @@ - - - - - - + + + + + + diff --git a/frontend/src/components/FavButton.vue b/frontend/src/components/FavButton.vue new file mode 100644 index 0000000..104e125 --- /dev/null +++ b/frontend/src/components/FavButton.vue @@ -0,0 +1,41 @@ + + + + + + diff --git a/frontend/src/components/InlineCasinoAd.vue b/frontend/src/components/InlineCasinoAd.vue index af5db4b..1227618 100644 --- a/frontend/src/components/InlineCasinoAd.vue +++ b/frontend/src/components/InlineCasinoAd.vue @@ -1,152 +1,152 @@ - - - - - - + + + + + + diff --git a/frontend/src/components/MessageAttachments.vue b/frontend/src/components/MessageAttachments.vue index 439fc71..dafce69 100644 --- a/frontend/src/components/MessageAttachments.vue +++ b/frontend/src/components/MessageAttachments.vue @@ -1,80 +1,80 @@ - - - - - - + + + + + + diff --git a/frontend/src/components/MessageItem.vue b/frontend/src/components/MessageItem.vue index 0a578f4..247d5ad 100644 --- a/frontend/src/components/MessageItem.vue +++ b/frontend/src/components/MessageItem.vue @@ -1,238 +1,240 @@ - - - - - - + + + + + + diff --git a/frontend/src/components/MessageItemBubble.vue b/frontend/src/components/MessageItemBubble.vue index 7ca9cd3..47d7125 100644 --- a/frontend/src/components/MessageItemBubble.vue +++ b/frontend/src/components/MessageItemBubble.vue @@ -1,154 +1,159 @@ - - - - - - + + + + + + diff --git a/frontend/src/components/MessageItemCompact.vue b/frontend/src/components/MessageItemCompact.vue index c908b77..9985169 100644 --- a/frontend/src/components/MessageItemCompact.vue +++ b/frontend/src/components/MessageItemCompact.vue @@ -23,6 +23,7 @@ type="button" @click="$emit('reply', { id: message.id, authorIp: message.authorIp })" >↩ + @@ -50,6 +51,7 @@ import type { Message } from '@/composables/useMessages'; import { useMessageItem } from '@/composables/useMessageItem'; import RichContent from './RichContent.vue'; import MessageAttachments from './MessageAttachments.vue'; +import FavButton from './FavButton.vue'; defineProps<{ message: Message; myIp?: string }>(); defineEmits<{ reply: [payload: { id: string; authorIp: string }] }>(); diff --git a/frontend/src/components/MessageList.vue b/frontend/src/components/MessageList.vue index dd4dfa1..3ec9572 100644 --- a/frontend/src/components/MessageList.vue +++ b/frontend/src/components/MessageList.vue @@ -1,98 +1,98 @@ - - - - - - + + + + + + diff --git a/frontend/src/components/Modal.vue b/frontend/src/components/Modal.vue new file mode 100644 index 0000000..1e897cf --- /dev/null +++ b/frontend/src/components/Modal.vue @@ -0,0 +1,64 @@ + + + + + + diff --git a/frontend/src/components/RichContent.vue b/frontend/src/components/RichContent.vue index b9f5f2a..2058530 100644 --- a/frontend/src/components/RichContent.vue +++ b/frontend/src/components/RichContent.vue @@ -1,108 +1,108 @@ - -