4 Commits

Author SHA1 Message Date
cb0d2c80cf fix: readme discord 2026-03-19 10:48:40 +01:00
d593d3f0e2 fix: status app 2026-03-19 10:24:41 +01:00
66a6a8caf0 fix: status app 2026-03-19 09:37:55 +01:00
6aa85ac683 fix: systeme metrics chart 2026-03-19 09:29:28 +01:00
27 changed files with 332 additions and 159 deletions

View File

@@ -11,12 +11,12 @@ BACKUPS_REMOTE_ROOT=
BACKUPS_MAX_FILES= BACKUPS_MAX_FILES=
# Paramètres utilisés pour construire les commandes disque et backup # Paramètres utilisés pour construire les commandes disque et backup
DISK_REMOTE_HOST=malio-b DISK_REMOTE_HOST=
DISK_LOCAL_SCRIPT_DIR=/home/malio/Malio-ops/CheckStorage DISK_LOCAL_SCRIPT_DIR=
DISK_REMOTE_SCRIPT_DIR=/home/malio-b/Malio-ops/CheckStorage DISK_REMOTE_SCRIPT_DIR=
RECETTE_SCRIPTS_DIR=/home/malio/Malio-ops/RecetteScripts RECETTE_SCRIPTS_DIR=
VAULTWARDEN_SSH_HOST=bitwarden VAULTWARDEN_SSH_HOST=
VAULTWARDEN_SCRIPTS_DIR=/home/matt/vaultwarden/Malio-ops/BackupVaultWarden VAULTWARDEN_SCRIPTS_DIR=
# A quelle heure les backups doivent être effectués (format 24h) # A quelle heure les backups doivent être effectués (format 24h)
BACKUPS_HOUR=19 BACKUPS_HOUR=19

View File

@@ -21,4 +21,8 @@ jobs:
- run: npm ci - run: npm ci
- run: npm run lint
- run: npm run build
- run: npx semantic-release - run: npx semantic-release

View File

@@ -22,7 +22,6 @@ Sur Linux, installer Docker et nvm.
Suivre la documentation suivante : Suivre la documentation suivante :
https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux https://wiki.malio.fr/bookstack/books/environnement-de-dev/chapter/linux
### Installation du projet
Une fois les prérequis installés, cloner le dépôt puis installer les dépendances. Une fois les prérequis installés, cloner le dépôt puis installer les dépendances.
Les étapes ci-dessous sont celles qui sont réellement supportées par le depot. Les étapes ci-dessous sont celles qui sont réellement supportées par le depot.
@@ -63,11 +62,9 @@ Les variables visibles dans le depot sont :
- `BACKUPS_REMOTE_HOST` : hôte SSH cible pour les operations distantes - `BACKUPS_REMOTE_HOST` : hôte SSH cible pour les operations distantes
- `BACKUPS_REMOTE_ROOT` : dossier racine des sauvegardes sur l'hôte distant - `BACKUPS_REMOTE_ROOT` : dossier racine des sauvegardes sur l'hôte distant
- `BACKUPS_MAX_FILES` : nombre maximal de fichiers retournés par dossier de backup - `BACKUPS_MAX_FILES` : nombre maximal de fichiers retournés par dossier de backup
- `DISK_COMMAND_REMOTE` : commande shell utilisée pour la verification disque distante - `DISK_REMOTE_HOST` : commande shell utilisée pour la verification disque distante
- `DISK_COMMAND_LOCAL` : commande shell utilisée pour la verification disque locale - `DISK_REMOTE_SCRIPT_DIR` : dossier des scripts de vérification disque distante
- `BACKUP_SCRIPT_COMMAND_BACKUP_BDD_RECETTE` : commande a exécuter pour le script "Backup BDD recette" - `DISK_LOCAL_SCRIPT_DIR` : commande shell utilisée pour la verification disque locale
- `BACKUP_SCRIPT_COMMAND_CHECK_STATUT_RECETTE` : commande à exécuter pour le script "Check statut recette"
- `BACKUP_SCRIPT_COMMAND_BACKUP_VAULTWARDEN` : commande à exécuter pour le script "Backup vault warden"
- `BACKUPS_HOUR` : heure attendue des sauvegardes pour le contrôle de fraicheur - `BACKUPS_HOUR` : heure attendue des sauvegardes pour le contrôle de fraicheur
### 4. Installer les dépendances ### 4. Installer les dépendances
@@ -82,7 +79,7 @@ npm install
npm run dev npm run dev
``` ```
Par défaut, l'application Nuxt sera accessible sûr <http://localhost:3000>. Par défaut, l'application Nuxt sera accessible sur <http://localhost:3000>.
## Configuration necessaire ## Configuration necessaire
@@ -136,7 +133,7 @@ Usage :
- `npm run dev` : lance l'application en développement - `npm run dev` : lance l'application en développement
- `npm run build` : construit l'application pour la production - `npm run build` : construit l'application pour la production
- `npm run generate` : généré une sortie statique si ce mode est compatible avec votre usage - `npm run generate` : generee une sortie statique si ce mode est compatible avec votre usage
- `npm run preview` : prévisualisé le build Nuxt - `npm run preview` : prévisualisé le build Nuxt
- `npm run lint` : execute ESLint - `npm run lint` : execute ESLint
- `npm run lint:fix` : applique les corrections ESLint automatiques : collecte périodique CPU, mémoire et réseau - `npm run lint:fix` : applique les corrections ESLint automatiques

View File

@@ -123,7 +123,10 @@
background-clip: text; background-clip: text;
} }
} }
* {
scrollbar-width: thin;
scrollbar-color: rgb(var(--m-border)) rgb(var(--m-bg));
}
@keyframes fade-in-up { @keyframes fade-in-up {
from { from {
opacity: 0; opacity: 0;

View File

@@ -31,7 +31,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"
import { Icon as IconifyIcon } from "@iconify/vue" import { Icon as IconifyIcon } from "@iconify/vue"
import backupOptions from "~/server/config/backup-options.json" import backupOptions from "~/server/config/backup-options.json"

View File

@@ -1,8 +1,14 @@
<script setup> <script setup lang="ts">
import {Icon as IconifyIcon} from "@iconify/vue" import {Icon as IconifyIcon} from "@iconify/vue"
import { apiFetch } from "~/composables/useApiAuth" import { apiFetch } from "~/composables/useApiAuth"
const { data: messages, error } = await useFetch('/api/discord/messages', { interface DiscordMessage {
id: string
content: string
author: { username: string }
}
const { data: messages, error } = await useFetch<DiscordMessage[]>('/api/discord/messages', {
$fetch: apiFetch, $fetch: apiFetch,
server: false server: false
}) })

View File

@@ -44,7 +44,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref} from "vue"
import {Icon as IconifyIcon} from "@iconify/vue" import {Icon as IconifyIcon} from "@iconify/vue"
import { apiRequest } from "~/composables/useApiAuth" import { apiRequest } from "~/composables/useApiAuth"
@@ -66,9 +65,14 @@ async function testDownload() {
if (!res.ok) { if (!res.ok) {
throw new Error(`HTTP ${res.status}`) throw new Error(`HTTP ${res.status}`)
} }
const blob = await res.blob()
const end = performance.now() const end = performance.now()
const size = blob.size const reader = res.body!.getReader()
let size = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
size += value.length
}
const seconds = (end - start) / 1000 const seconds = (end - start) / 1000
download.value = Math.round((size * 8) / seconds / 1000000) download.value = Math.round((size * 8) / seconds / 1000000)
} }
@@ -88,7 +92,7 @@ async function testUpload() {
async function testPing() { async function testPing() {
const start = performance.now() const start = performance.now()
const response = await fetch('/api/ping') const response = await apiRequest('/api/ping')
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP ${response.status}`) throw new Error(`HTTP ${response.status}`)
} }

View File

@@ -47,7 +47,6 @@
<script setup lang="ts"> <script setup lang="ts">
import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue" import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue"
import TextSkeleton from "~/components/skeleton/TextSkeleton.vue" import TextSkeleton from "~/components/skeleton/TextSkeleton.vue"
import {onBeforeUnmount, onMounted, ref} from "vue"
import { apiFetch } from "~/composables/useApiAuth" import { apiFetch } from "~/composables/useApiAuth"
interface StatusRow { interface StatusRow {

View File

@@ -50,9 +50,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import CircleSkeleton from "~/components/skeleton/CircleSkeleton.vue"
import TextSkeleton from "~/components/skeleton/TextSkeleton.vue"
import {onBeforeUnmount, onMounted, ref} from "vue"
import { apiFetch } from "~/composables/useApiAuth" import { apiFetch } from "~/composables/useApiAuth"
interface StatusRow { interface StatusRow {
@@ -69,6 +66,10 @@ interface StatusResponse {
results: StatusRow[] results: StatusRow[]
} }
interface BackupScriptRunResponse {
ok: boolean
}
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
endpoint?: string endpoint?: string
@@ -83,7 +84,8 @@ const props = withDefaults(
const rows = ref<StatusRow[]>([]) const rows = ref<StatusRow[]>([])
const loading = ref(true) const loading = ref(true)
const initialized = ref(false) const initialized = ref(false)
let timer: ReturnType<typeof setInterval> | null = null let statusTimer: ReturnType<typeof setInterval> | null = null
let scriptTimer: ReturnType<typeof setInterval> | null = null
const statusLabel = (row: StatusRow) => { const statusLabel = (row: StatusRow) => {
if (row.ok) return "OK" if (row.ok) return "OK"
@@ -130,15 +132,34 @@ const checkStatus = async () => {
} }
} }
const runStatusScript = async () => {
try {
await apiFetch<BackupScriptRunResponse>("/api/backup-script", {
method: "POST",
body: { key: "check-statut-recette" }
})
await checkStatus()
} catch (error) {
console.error("Erreur execution check statut recette:", error)
}
}
onMounted(() => { onMounted(() => {
checkStatus() checkStatus()
timer = setInterval(checkStatus, props.refreshMs) runStatusScript()
statusTimer = setInterval(checkStatus, props.refreshMs)
scriptTimer = setInterval(runStatusScript, 5 * 60 * 1000)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (timer) { if (statusTimer) {
clearInterval(timer) clearInterval(statusTimer)
timer = null statusTimer = null
}
if (scriptTimer) {
clearInterval(scriptTimer)
scriptTimer = null
} }
}) })
</script> </script>

View File

@@ -139,7 +139,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue"
import type { SystemMetrics } from "~/types/system" import type { SystemMetrics } from "~/types/system"
type MetricKey = "cpu" | "ram" type MetricKey = "cpu" | "ram"

View File

@@ -45,7 +45,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed} from "vue"
import type { SystemMetrics } from "~/types/system"; import type { SystemMetrics } from "~/types/system";
const props = defineProps<{ const props = defineProps<{

View File

@@ -1,50 +1,3 @@
function toHeadersObject(headers?: HeadersInit): Record<string, string> {
if (!headers) {
return {}
}
if (headers instanceof Headers) {
return Object.fromEntries(headers.entries())
}
if (Array.isArray(headers)) {
return Object.fromEntries(headers)
}
return { ...headers }
}
function getDownloadFileName(contentDisposition: string | null, fallback: string) {
if (!contentDisposition) {
return fallback
}
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)
if (utf8Match?.[1]) {
return decodeURIComponent(utf8Match[1])
}
const asciiMatch = contentDisposition.match(/filename="([^"]+)"/i)
if (asciiMatch?.[1]) {
return asciiMatch[1]
}
return fallback
}
export function useApiAuthHeader() {
const runtimeConfig = useRuntimeConfig()
const token = runtimeConfig.public.apiSecretKey
if (!token) {
return {}
}
// Tous les appels frontend vers /api/* reutilisent ce header commun.
return {
}
}
export const apiFetch = $fetch.create({}) export const apiFetch = $fetch.create({})
export function apiRequest(input: RequestInfo | URL, init: RequestInit = {}) { export function apiRequest(input: RequestInfo | URL, init: RequestInit = {}) {
@@ -52,38 +5,16 @@ export function apiRequest(input: RequestInfo | URL, init: RequestInit = {}) {
} }
export async function downloadApiFile(url: string, fileNameFallback: string) { export async function downloadApiFile(url: string, fileNameFallback: string) {
// Les telechargements passent aussi par fetch pour pouvoir recuperer
// le contenu et le nom de fichier renvoye par l'API.
const response = await apiRequest(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const blob = await response.blob()
const objectUrl = URL.createObjectURL(blob)
const fileName = getDownloadFileName(
response.headers.get("content-disposition"),
fileNameFallback
)
const link = document.createElement("a") const link = document.createElement("a")
link.href = objectUrl link.href = url
link.download = fileName link.download = fileNameFallback
link.style.display = "none" link.style.display = "none"
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
link.remove() link.remove()
URL.revokeObjectURL(objectUrl)
} }
export function withApiAuth(init: RequestInit = {}) { export function withApiAuth(init: RequestInit = {}) {
// Fusionne le header d'auth avec d'eventuels headers deja fournis. return { ...init }
return {
...init,
headers: {
...useApiAuthHeader(),
...toHeadersObject(init.headers)
}
}
} }

View File

@@ -50,7 +50,7 @@
<div class="sidebar-divider"/> <div class="sidebar-divider"/>
<div class="status-card"> <div class="status-card">
<p class="status-label">Environnement</p> <p class="status-label">Environnement</p>
<p class="status-value">Production</p> <p class="status-value">{{ environmentLabel }}</p>
<p class="status-description"> <p class="status-description">
Acces rapide au monitoring, aux sauvegardes et aux cartes systeme. Acces rapide au monitoring, aux sauvegardes et aux cartes systeme.
</p> </p>
@@ -123,7 +123,7 @@
<div class="sidebar-divider"/> <div class="sidebar-divider"/>
<div class="status-card"> <div class="status-card">
<p class="status-label">Environnement</p> <p class="status-label">Environnement</p>
<p class="status-value">Production</p> <p class="status-value">{{ environmentLabel }}</p>
<p class="status-description"> <p class="status-description">
Navigation rapide vers les vues principales de supervision. Navigation rapide vers les vues principales de supervision.
</p> </p>
@@ -153,6 +153,7 @@ const {
public: {appVersion} public: {appVersion}
} = useRuntimeConfig() } = useRuntimeConfig()
const isMenuOpen = ref(false) const isMenuOpen = ref(false)
const environmentLabel = import.meta.dev ? "Developpement" : "Production"
const navItems = [ const navItems = [
{ {
to: "/", to: "/",
@@ -169,6 +170,13 @@ const navItems = [
icon: "mdi:database-arrow-up-outline" icon: "mdi:database-arrow-up-outline"
} }
] ]
onMounted(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") isMenuOpen.value = false
}
document.addEventListener("keydown", handler)
onBeforeUnmount(() => document.removeEventListener("keydown", handler))
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -20,7 +20,7 @@ const getRepoVersion = () => {
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: "2025-07-15", compatibilityDate: "2025-07-15",
devtools: { enabled: true }, devtools: { enabled: process.env.NODE_ENV !== "production" },
css: ["~/assets/css/main.css"], css: ["~/assets/css/main.css"],
app: { app: {
head: { head: {
@@ -35,12 +35,12 @@ export default defineNuxtConfig({
} }
}, },
runtimeConfig: { runtimeConfig: {
authCookieSecure: process.env.AUTH_COOKIE_SECURE === "true",
apiSecretKey: process.env.API_SECRET_KEY, apiSecretKey: process.env.API_SECRET_KEY,
discordBotToken: process.env.DISCORD_BOT_TOKEN, discordBotToken: process.env.DISCORD_BOT_TOKEN,
discordChannelId: process.env.DISCORD_CHANNEL_ID, discordChannelId: process.env.DISCORD_CHANNEL_ID,
public: { public: {
appVersion: getRepoVersion(), appVersion: getRepoVersion()
apiKey: process.env.API_SECRET_KEY
} }
}, },
vite: { vite: {

200
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,10 +16,10 @@
}, },
"dependencies": { "dependencies": {
"@iconify/vue": "^5.0.0", "@iconify/vue": "^5.0.0",
"@nuxt/eslint": "^1.15.2",
"nuxt": "^4.3.1" "nuxt": "^4.3.1"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint": "^1.15.2",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",

View File

@@ -1,5 +1,4 @@
<template> <template>
<NuxtLayout name="default">
<div class="dashboard-container"> <div class="dashboard-container">
<header class="dashboard-header"> <header class="dashboard-header">
<div class="header-copy"> <div class="header-copy">
@@ -105,16 +104,11 @@
</section> </section>
</div> </div>
</div> </div>
</NuxtLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"
import BackupRun from "~/components/BackupRun.vue"
import { apiFetch, downloadApiFile } from "~/composables/useApiAuth" import { apiFetch, downloadApiFile } from "~/composables/useApiAuth"
definePageMeta({ layout: false })
type ScriptResult = { type ScriptResult = {
key: string | null key: string | null
label: string label: string

View File

@@ -1,5 +1,4 @@
<template> <template>
<NuxtLayout name="default">
<div class="dashboard-container"> <div class="dashboard-container">
<header class="dashboard-header"> <header class="dashboard-header">
<div> <div>
@@ -62,15 +61,12 @@
</div> </div>
</div> </div>
</div> </div>
</NuxtLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { apiFetch } from "~/composables/useApiAuth" import { apiFetch } from "~/composables/useApiAuth"
import type { SystemMetrics } from "~/types/system"; import type { SystemMetrics } from "~/types/system";
definePageMeta({layout: false})
type DiskSourceResult = { type DiskSourceResult = {
key: string key: string
label: string label: string
@@ -192,7 +188,7 @@ const runScript = async () => {
const loadSystemMetrics = async () => { const loadSystemMetrics = async () => {
try { try {
systemMetrics.value = await $fetch<SystemMetrics>("/api/system") systemMetrics.value = await apiFetch<SystemMetrics>("/api/system")
} catch { } catch {
systemMetrics.value = null systemMetrics.value = null
} finally { } finally {

View File

@@ -30,27 +30,23 @@ function parseLines(output: string): string[] {
.filter(Boolean) .filter(Boolean)
} }
function quoteDir(pathValue: string) {
return shellQuote(pathValue)
}
async function listRemoteFiles(remoteDir: string): Promise<string[]> { async function listRemoteFiles(remoteDir: string): Promise<string[]> {
const output = await runSsh( const output = await runSsh(
`cd ${quoteDir(remoteDir)} && ls -1A | sort -r | head -n ${MAX_FILES_PER_FOLDER}` `cd ${shellQuote(remoteDir)} && ls -1A | sort -r | head -n ${MAX_FILES_PER_FOLDER}`
) )
return parseLines(output) return parseLines(output)
} }
async function listRemoteDirs(remoteRoot: string): Promise<string[]> { async function listRemoteDirs(remoteRoot: string): Promise<string[]> {
const output = await runSsh( const output = await runSsh(
`cd ${quoteDir(remoteRoot)} && for d in */; do [ -d "$d" ] && printf '%s\n' "\${d%/}"; done` `cd ${shellQuote(remoteRoot)} && for d in */; do [ -d "$d" ] && printf '%s\n' "\${d%/}"; done`
) )
return parseLines(output) return parseLines(output)
} }
async function getLatestRemoteFile(remoteDir: string): Promise<string | null> { async function getLatestRemoteFile(remoteDir: string): Promise<string | null> {
const output = await runSsh( const output = await runSsh(
`cd ${quoteDir(remoteDir)} && ls -1A | sort -r | head -n 1` `cd ${shellQuote(remoteDir)} && ls -1A | sort -r | head -n 1`
) )
const files = parseLines(output) const files = parseLines(output)
return files[0] || null return files[0] || null

View File

@@ -4,10 +4,9 @@ import {
resolveFolderRemoteDir resolveFolderRemoteDir
} from "../utils/ssh.ts" } from "../utils/ssh.ts"
import {process} from "std-env";
import backupOptions from "../config/backup-options.json" import backupOptions from "../config/backup-options.json"
export const BACKUP_HOUR = process.env.BACKUPS_HOUR export const BACKUP_HOUR = Number(process.env.BACKUPS_HOUR) || 19
type BackupTarget = { type BackupTarget = {
name: string name: string
@@ -141,7 +140,7 @@ export default defineEventHandler(async () => {
latestBackupAt: null, latestBackupAt: null,
backupDate: null, backupDate: null,
expectedBackupDate: expectedDateKey, expectedBackupDate: expectedDateKey,
error: error instanceof Error ? error.message : String(error) error: "Erreur lors de la verification"
} }
} }
}) })

View File

@@ -0,0 +1,29 @@
# Créer un bot discord
Allez sur le portail des développeurs Discord : https://discord.com/developers/applications
1. Cliquez sur "New Application" et donnez un nom à votre application.
2. Dans le menu de gauche, cliquez sur "Bot" puis sur "Add Bot".
3. Vous pouvez personnaliser votre bot en lui donnant un nom, une image de profil, etc.
4. Sous la section "Token", cliquez sur "Copy" pour copier le token de votre bot. Gardez ce token secret, car il permet à quiconque de contrôler votre bot.
5. Dans L'onglet "Installation", on peut décocher "Installation pour un utlisateur" si vous voulez installer le bot que sur des serveurs.
6. Tout en bas dans "Paramètres d'installation par défaut", cliquez sur "applications.commands" et sélectionnez "bot" pour donner les permissions nécessaires à votre bot.
7. Ensuite donner les permissions que vous souhaitez à votre bot en cochant les cases correspondantes. Pour l'utilisation pour Supervisor cocher "Voir les anciens messages"
8. Enregistrez les modifications.
9. Puis vous pouvez copier le d'installation et le copier pour inviter le bot au discord

View File

@@ -20,7 +20,11 @@ export default defineEventHandler(async (event) => {
} }
) )
return messages return (messages as any[]).map((m) => ({
id: m.id,
content: m.content,
author: { username: m.author?.username ?? "Inconnu" }
}))
} catch (error) { } catch (error) {
console.error("Erreur Discord messages:", error) console.error("Erreur Discord messages:", error)

View File

@@ -3,7 +3,8 @@ import {
shellQuote, shellQuote,
resolveFolderRemoteDir, resolveFolderRemoteDir,
REMOTE_HOST, REMOTE_HOST,
isSafeFolder isSafeFolder,
isSafeFile
} from "../utils/ssh.ts" } from "../utils/ssh.ts"
import { spawn } from "node:child_process" import { spawn } from "node:child_process"
@@ -29,6 +30,9 @@ export default defineEventHandler(async (event) => {
if (folderNames.length === 0) { if (folderNames.length === 0) {
throw createError({ statusCode: 400, statusMessage: "Paramètre folders invalide" }) throw createError({ statusCode: 400, statusMessage: "Paramètre folders invalide" })
} }
if (!REMOTE_HOST) {
throw createError({ statusCode: 503, statusMessage: "Service non configure" })
}
if (folderNames.some((folder) => !isSafeFolder(folder))) { if (folderNames.some((folder) => !isSafeFolder(folder))) {
throw createError({ statusCode: 400, statusMessage: "Paramètre folders invalide" }) throw createError({ statusCode: 400, statusMessage: "Paramètre folders invalide" })
@@ -44,10 +48,7 @@ export default defineEventHandler(async (event) => {
} }
const fileName = await getLatestRemoteFile(remoteDir) const fileName = await getLatestRemoteFile(remoteDir)
if (!fileName || !isSafeFolder(fileName)) { if (!fileName || !isSafeFile(fileName)) {
continue
}
if (!fileName) {
continue continue
} }

View File

@@ -18,6 +18,10 @@ export default defineEventHandler(async (event) => {
const folderName = typeof folder === "string" ? folder : null const folderName = typeof folder === "string" ? folder : null
const fileName = typeof file === "string" ? file : null const fileName = typeof file === "string" ? file : null
if (!REMOTE_HOST) {
throw createError({ statusCode: 503, statusMessage: "Service non configure" })
}
if (!folderName || !fileName) { if (!folderName || !fileName) {
throw createError({ statusCode: 400, statusMessage: "Paramètres manquants" }) throw createError({ statusCode: 400, statusMessage: "Paramètres manquants" })
} }

View File

@@ -4,9 +4,11 @@ export default defineEventHandler(async (event) => {
let received = 0 let received = 0
for await (const chunk of req) { for await (const chunk of req) {
if (received > MAX_UPLOAD_BYTES) throw createError({ statusCode: 413, statusMessage: "Fichier trop volumineux" })
received += chunk.length received += chunk.length
if (received > MAX_UPLOAD_BYTES) {
event.node.res.destroy()
throw createError({statusCode: 413, statusMessage: "Fichier trop volumineux"})
}
} }
return {received} return {received}
}) })

View File

@@ -22,7 +22,7 @@ export default defineEventHandler((event) => {
return return
} }
const secureCookie = process.env.AUTH_COOKIE_SECURE === "true" const secureCookie = runtimeConfig.authCookieSecure
setCookie(event, "api_auth_token", expectedToken, { setCookie(event, "api_auth_token", expectedToken, {
httpOnly: true, httpOnly: true,

View File

@@ -1,3 +1,5 @@
import { shellQuote } from "./ssh"
export type BackupScript = { export type BackupScript = {
key: string key: string
label: string label: string
@@ -32,10 +34,10 @@ const getDefaultBackupScriptCommands = (): Record<string, string> => {
process.env.VAULTWARDEN_SCRIPTS_DIR || "/home/matt/vaultwarden/Malio-ops/BackupVaultWarden" process.env.VAULTWARDEN_SCRIPTS_DIR || "/home/matt/vaultwarden/Malio-ops/BackupVaultWarden"
return { return {
"backup-bdd-recette": `cd ${recetteScriptsDir} && bash backup-bdd-recette.sh`, "backup-bdd-recette": `cd ${shellQuote(recetteScriptsDir)} && bash backup-bdd-recette.sh`,
"check-statut-recette": `cd ${recetteScriptsDir} && bash check-statut-recette.sh`, "check-statut-recette": `cd ${shellQuote(recetteScriptsDir)} && bash check-statut-recette.sh`,
"backup-vaultwarden": "backup-vaultwarden":
`ssh ${vaultwardenHost} "cd ${vaultwardenScriptsDir} && bash backup-vaultwarden.sh"` `ssh ${shellQuote(vaultwardenHost)} "cd ${shellQuote(vaultwardenScriptsDir)} && bash backup-vaultwarden.sh"`
} }
} }