mirror of
https://github.com/BreizhHardware/ntfy_alerts.git
synced 2026-01-19 00:47:33 +01:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8550fb045c | ||
|
|
802081937f | ||
|
|
de60020b01 |
18
.github/dependabot.yaml
vendored
18
.github/dependabot.yaml
vendored
@@ -10,21 +10,3 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "dev"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "dev"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/.github/workflows"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "dev"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/web"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
target-branch: "dev"
|
||||
141
.github/workflows/build_pr.yml
vendored
141
.github/workflows/build_pr.yml
vendored
@@ -1,141 +0,0 @@
|
||||
name: Build et Push Docker PR Image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
build-binary:
|
||||
if: ${{ github.actor != 'dependabot[bot]' && !startsWith(github.ref, 'refs/heads/dependabot/') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Installer Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: x86_64-unknown-linux-musl
|
||||
override: true
|
||||
|
||||
- name: Installer cross
|
||||
run: cargo install cross
|
||||
|
||||
- name: Créer Cross.toml pour spécifier OpenSSL vendored
|
||||
run: |
|
||||
cat > Cross.toml << 'EOF'
|
||||
[build.env]
|
||||
passthrough = [
|
||||
"RUSTFLAGS",
|
||||
"OPENSSL_STATIC",
|
||||
"OPENSSL_NO_VENDOR"
|
||||
]
|
||||
EOF
|
||||
|
||||
- name: Construire avec cross et OpenSSL vendored
|
||||
env:
|
||||
OPENSSL_STATIC: 1
|
||||
RUSTFLAGS: "-C target-feature=+crt-static"
|
||||
OPENSSL_NO_VENDOR: 0
|
||||
run: |
|
||||
cross build --release --target x86_64-unknown-linux-musl --features vendored-openssl
|
||||
|
||||
- name: Préparer le binaire
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp target/x86_64-unknown-linux-musl/release/github-ntfy release/github-ntfy
|
||||
|
||||
- name: Upload binaire comme artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: github-ntfy-pr
|
||||
path: release/github-ntfy
|
||||
|
||||
build-frontend:
|
||||
if: ${{ github.actor != 'dependabot[bot]' && !startsWith(github.ref, 'refs/heads/dependabot/') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: '10.x'
|
||||
run_install: false
|
||||
|
||||
- name: Build Frontend (Nuxt)
|
||||
run: |
|
||||
cd web
|
||||
pnpm install
|
||||
pnpm generate
|
||||
|
||||
- name: Vérifier le contenu du répertoire output
|
||||
run: |
|
||||
ls -la web/.output/public || echo "Le répertoire .output n'existe pas!"
|
||||
|
||||
- name: Upload frontend comme artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nuxt-frontend-pr
|
||||
path: web/.output/public
|
||||
|
||||
docker-build-push:
|
||||
if: ${{ github.actor != 'dependabot[bot]' && !startsWith(github.ref, 'refs/heads/dependabot/') }}
|
||||
needs: [build-binary, build-frontend]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configurer Docker
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extraire le numéro de PR
|
||||
run: |
|
||||
PR_NUMBER=$(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }')
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
|
||||
|
||||
- name: Télécharger l'exécutable binaire
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: github-ntfy-pr
|
||||
path: binaries
|
||||
|
||||
- name: Télécharger le frontend
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nuxt-frontend-pr
|
||||
path: web/.output/public
|
||||
|
||||
- name: Préparer les fichiers pour Docker
|
||||
run: |
|
||||
chmod +x binaries/github-ntfy
|
||||
mkdir -p docker-build
|
||||
cp binaries/github-ntfy docker-build/
|
||||
mkdir -p docker-build/web-output/public
|
||||
cp -r web/.output/public/* docker-build/web-output/public/
|
||||
cp nginx.conf docker-build/
|
||||
cp entrypoint.sh docker-build/
|
||||
cp Dockerfile docker-build/
|
||||
chmod +x docker-build/entrypoint.sh
|
||||
|
||||
- name: Construire et pousser l'image Docker
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: docker-build
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/github-ntfy:pr-${{ env.PR_NUMBER }}
|
||||
file: docker-build/Dockerfile
|
||||
60
.github/workflows/create_dev.yml
vendored
60
.github/workflows/create_dev.yml
vendored
@@ -53,49 +53,15 @@ jobs:
|
||||
name: github-ntfy
|
||||
path: release/github-ntfy
|
||||
|
||||
build-frontend:
|
||||
if: ${{ github.actor != 'dependabot[bot]' && !startsWith(github.ref, 'refs/heads/dependabot/') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: '10.x'
|
||||
run_install: false
|
||||
|
||||
- name: Build Frontend (Nuxt)
|
||||
run: |
|
||||
cd web
|
||||
pnpm install
|
||||
pnpm generate
|
||||
|
||||
- name: Vérifier le contenu du répertoire output/public
|
||||
run: |
|
||||
ls -la web/.output/public || echo "Le répertoire .output/public n'existe pas!"
|
||||
|
||||
- name: Upload frontend comme artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nuxt-frontend
|
||||
path: web/.output/public # Cibler spécifiquement le répertoire public
|
||||
|
||||
docker-build-push:
|
||||
if: ${{ github.actor != 'dependabot[bot]' && !startsWith(github.ref, 'refs/heads/dependabot/') }}
|
||||
needs: [build-binary, build-frontend]
|
||||
needs: [build-binary]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configurer Docker Buildx
|
||||
- name: Configurer Docker
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login Docker Hub
|
||||
@@ -110,28 +76,14 @@ jobs:
|
||||
name: github-ntfy
|
||||
path: binaries
|
||||
|
||||
- name: Télécharger le frontend
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nuxt-frontend
|
||||
path: web/.output/public
|
||||
|
||||
- name: Préparer les fichiers pour Docker
|
||||
- name: Préparer le binaire pour Docker
|
||||
run: |
|
||||
chmod +x binaries/github-ntfy
|
||||
mkdir -p docker-build
|
||||
cp binaries/github-ntfy docker-build/
|
||||
mkdir -p docker-build/web-output/public
|
||||
cp -r web/.output/public/* docker-build/web-output/public/
|
||||
cp nginx.conf docker-build/
|
||||
cp entrypoint.sh docker-build/
|
||||
cp Dockerfile docker-build/
|
||||
chmod +x docker-build/entrypoint.sh
|
||||
|
||||
- name: Construire et pousser l'image Docker
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: docker-build
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_USERNAME }}/github-ntfy:dev
|
||||
file: docker-build/Dockerfile
|
||||
tags: breizhhardware/github-ntfy:dev
|
||||
file: Dockerfile
|
||||
113
.github/workflows/create_release.yml
vendored
113
.github/workflows/create_release.yml
vendored
@@ -29,44 +29,18 @@ jobs:
|
||||
MINOR=$(echo $VERSION | cut -d. -f2)
|
||||
PATCH=$(echo $VERSION | cut -d. -f3)
|
||||
|
||||
# Récupérer le dernier message de commit
|
||||
COMMIT_MSG=$(git log -1 --pretty=%B)
|
||||
|
||||
# Déterminer quel niveau de version doit être incrémenté
|
||||
if echo "$COMMIT_MSG" | grep -q "\[bump-major\]"; then
|
||||
echo "Incrémentation de la version majeure détectée dans le message de commit"
|
||||
MAJOR=$((MAJOR + 1))
|
||||
MINOR=0
|
||||
PATCH=0
|
||||
elif echo "$COMMIT_MSG" | grep -q "\[bump-minor\]"; then
|
||||
echo "Incrémentation de la version mineure détectée dans le message de commit"
|
||||
MINOR=$((MINOR + 1))
|
||||
PATCH=0
|
||||
elif echo "$COMMIT_MSG" | grep -q "\[version="; then
|
||||
# Format personnalisé: [version=X.Y.Z]
|
||||
CUSTOM_VERSION=$(echo "$COMMIT_MSG" | grep -o '\[version=[0-9]*\.[0-9]*\.[0-9]*\]' | sed 's/\[version=\(.*\)\]/\1/')
|
||||
if [ ! -z "$CUSTOM_VERSION" ]; then
|
||||
echo "Version personnalisée détectée: $CUSTOM_VERSION"
|
||||
MAJOR=$(echo $CUSTOM_VERSION | cut -d. -f1)
|
||||
MINOR=$(echo $CUSTOM_VERSION | cut -d. -f2)
|
||||
PATCH=$(echo $CUSTOM_VERSION | cut -d. -f3)
|
||||
else
|
||||
# Incrémentation de patch par défaut
|
||||
PATCH=$((PATCH + 1))
|
||||
fi
|
||||
else
|
||||
# Incrémentation de patch par défaut
|
||||
PATCH=$((PATCH + 1))
|
||||
fi
|
||||
# Incrémenter le patch
|
||||
PATCH=$((PATCH + 1))
|
||||
|
||||
# Nouvelle version
|
||||
NEW_VERSION="v$MAJOR.$MINOR.$PATCH"
|
||||
echo "Nouvelle version: $NEW_VERSION"
|
||||
echo "tag=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
build-binary:
|
||||
build-binaries:
|
||||
needs: version
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -111,42 +85,8 @@ jobs:
|
||||
name: github-ntfy
|
||||
path: release/github-ntfy
|
||||
|
||||
build-frontend:
|
||||
needs: version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: '10.x'
|
||||
run_install: false
|
||||
|
||||
- name: Build Frontend (Nuxt)
|
||||
run: |
|
||||
cd web
|
||||
pnpm install
|
||||
pnpm generate
|
||||
|
||||
- name: Vérifier le contenu du répertoire output
|
||||
run: |
|
||||
ls -la web/.output/public || echo "Le répertoire .output n'existe pas!"
|
||||
|
||||
- name: Upload frontend comme artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nuxt-frontend
|
||||
path: web/.output/public
|
||||
|
||||
docker-build-push:
|
||||
needs: [version, build-binary, build-frontend]
|
||||
needs: [version, build-binaries]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -161,73 +101,48 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Télécharger le binaire
|
||||
- name: Télécharger tous les binaires
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: github-ntfy
|
||||
path: binaries
|
||||
|
||||
- name: Télécharger le frontend
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nuxt-frontend
|
||||
path: web/.output/public
|
||||
|
||||
- name: Préparer les fichiers pour Docker
|
||||
- name: Préparer le binaire pour Docker
|
||||
run: |
|
||||
chmod +x binaries/github-ntfy
|
||||
mkdir -p docker-build
|
||||
cp binaries/github-ntfy docker-build/
|
||||
mkdir -p docker-build/web-output/public
|
||||
cp -r web/.output/public/* docker-build/web-output/public/
|
||||
cp nginx.conf docker-build/
|
||||
cp entrypoint.sh docker-build/
|
||||
cp Dockerfile docker-build/
|
||||
chmod +x docker-build/entrypoint.sh
|
||||
|
||||
# Construire et pousser l'image multi-architecture
|
||||
- name: Construire et pousser l'image Docker
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: docker-build
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
breizhhardware/github-ntfy:latest
|
||||
breizhhardware/github-ntfy:dev
|
||||
breizhhardware/github-ntfy:${{ needs.version.outputs.version }}
|
||||
file: docker-build/Dockerfile
|
||||
file: Dockerfile
|
||||
|
||||
create-release:
|
||||
needs: [version, build-binary, build-frontend]
|
||||
needs: [version, build-binaries]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Télécharger le binaire
|
||||
- name: Télécharger tous les binaires
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: github-ntfy
|
||||
path: binaries
|
||||
|
||||
- name: Télécharger le frontend
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nuxt-frontend
|
||||
path: web/.output/public
|
||||
|
||||
- name: Préparer les fichiers pour la release
|
||||
run: |
|
||||
mkdir -p release-artifacts
|
||||
cp binaries/github-ntfy release-artifacts/
|
||||
tar -czf release-artifacts/frontend.tar.gz -C web/.output/public .
|
||||
|
||||
- name: Créer une release GitHub
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ needs.version.outputs.version }}
|
||||
name: Release ${{ needs.version.outputs.version }}
|
||||
files: |
|
||||
release-artifacts/github-ntfy
|
||||
release-artifacts/frontend.tar.gz
|
||||
binaries/github-ntfy
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
|
||||
62
.github/workflows/dependabot-build.yml
vendored
62
.github/workflows/dependabot-build.yml
vendored
@@ -1,16 +1,15 @@
|
||||
name: Dependabot Build
|
||||
name: Dependabot Build Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ 'main', 'dev' ]
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- 'web/package.json'
|
||||
- 'web/pnpm-lock.yaml'
|
||||
branches: [dev]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
build-binary:
|
||||
build:
|
||||
if: ${{ startsWith(github.ref, 'refs/heads/dependabot/') || github.actor == 'dependabot[bot]' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -46,47 +45,8 @@ jobs:
|
||||
run: |
|
||||
cross build --release --target x86_64-unknown-linux-musl --features vendored-openssl
|
||||
|
||||
- name: Préparer le binaire
|
||||
- name: Afficher des informations de débogage
|
||||
run: |
|
||||
mkdir -p release
|
||||
cp target/x86_64-unknown-linux-musl/release/github-ntfy release/github-ntfy
|
||||
|
||||
- name: Upload binaire comme artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: github-ntfy-dependabot
|
||||
path: release/github-ntfy
|
||||
|
||||
build-frontend:
|
||||
if: ${{ github.actor == 'dependabot[bot]' || startsWith(github.ref, 'refs/heads/dependabot/') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: '10.x'
|
||||
run_install: false
|
||||
|
||||
- name: Build Frontend (Nuxt)
|
||||
run: |
|
||||
cd web
|
||||
pnpm install
|
||||
pnpm generate
|
||||
|
||||
- name: Vérifier le contenu du répertoire output
|
||||
run: |
|
||||
ls -la web/.output/public || echo "Le répertoire .output n'existe pas!"
|
||||
|
||||
- name: Upload frontend comme artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nuxt-frontend-dependabot
|
||||
path: web/.output/public
|
||||
echo "Acteur: ${{ github.actor }}"
|
||||
echo "Référence de la branche: ${{ github.head_ref }}"
|
||||
echo "Event name: ${{ github.event_name }}"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -413,4 +413,3 @@ target/*
|
||||
|
||||
binaries
|
||||
binaries/*
|
||||
|
||||
|
||||
133
Cargo.lock
generated
133
Cargo.lock
generated
@@ -130,19 +130,6 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"blowfish",
|
||||
"getrandom 0.3.3",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
@@ -158,16 +145,6 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blowfish"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.18.1"
|
||||
@@ -211,21 +188,10 @@ dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
@@ -495,13 +461,11 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
name = "github-ntfy"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
"dotenv",
|
||||
"env_logger",
|
||||
"log",
|
||||
"openssl",
|
||||
"rand 0.9.2",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
@@ -675,7 +639,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -752,7 +716,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -901,26 +865,6 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -991,9 +935,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.35.0"
|
||||
version = "0.34.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
|
||||
checksum = "91632f3b4fb6bd1d72aa3d78f41ffecfcf2b1a6648d8c241dbe7dbfaf4875e15"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
@@ -1320,18 +1264,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1341,17 +1275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.3",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1363,15 +1287,6 @@ dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.12"
|
||||
@@ -1412,9 +1327,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.22"
|
||||
version = "0.12.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
|
||||
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -1468,9 +1383,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.37.0"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
|
||||
checksum = "3de23c3319433716cf134eed225fe9986bc24f63bed9be9f20c329029e672dc7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
@@ -1610,9 +1525,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.142"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -1683,16 +1598,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -1808,22 +1713,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.47.1"
|
||||
version = "1.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
|
||||
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"io-uring",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"socket2 0.6.0",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1965,7 +1868,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"url",
|
||||
|
||||
@@ -13,14 +13,12 @@ vendored-openssl = ["openssl/vendored"]
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
reqwest = { version = "0.12", features = ["json", "blocking"] }
|
||||
rusqlite = { version = "0.37", features = ["bundled"] }
|
||||
rusqlite = { version = "0.36", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
dotenv = "0.15"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
chrono = "0.4"
|
||||
warp = "0.3"
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
rand = "0.9"
|
||||
bcrypt = "0.17"
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
15
Dockerfile
15
Dockerfile
@@ -1,32 +1,25 @@
|
||||
FROM alpine:3.22
|
||||
|
||||
# Copier le binaire
|
||||
COPY github-ntfy /usr/local/bin/github-ntfy
|
||||
COPY binaries/github-ntfy /usr/local/bin/github-ntfy
|
||||
|
||||
# Installer les dépendances
|
||||
RUN apk add --no-cache sqlite-libs openssl nginx nodejs npm && \
|
||||
RUN apk add --no-cache sqlite-libs openssl nginx && \
|
||||
chmod +x /usr/local/bin/github-ntfy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copier les fichiers web dans le répertoire attendu par nginx
|
||||
COPY web-output/public /var/www/html/
|
||||
COPY web/* /var/www/html/
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Copier le script d'entrée
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
# Créer le répertoire de données et définir les permissions
|
||||
# Créer le répertoire de données
|
||||
RUN mkdir -p /github-ntfy && chmod 755 /github-ntfy
|
||||
|
||||
# Variables d'environnement (optionnelles)
|
||||
ENV DB_PATH=/github-ntfy
|
||||
ENV RUST_LOG=info
|
||||
|
||||
# Volumes pour la persistance des données
|
||||
VOLUME ["/github-ntfy"]
|
||||
|
||||
EXPOSE 5000 80
|
||||
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
|
||||
18
README.md
18
README.md
@@ -1,6 +1,6 @@
|
||||
<h1 align="center">Welcome to ntfy_alerts 👋</h1>
|
||||
<p>
|
||||
<img alt="Version" src="https://img.shields.io/badge/version-2.1-blue.svg?cacheSeconds=2592000" />
|
||||
<img alt="Version" src="https://img.shields.io/badge/version-2.0-blue.svg?cacheSeconds=2592000" />
|
||||
<a href="#" target="_blank">
|
||||
<img alt="License: GPL--3" src="https://img.shields.io/badge/License-GPL--3-yellow.svg" />
|
||||
</a>
|
||||
@@ -22,6 +22,18 @@ services:
|
||||
github-ntfy:
|
||||
image: breizhhardware/github-ntfy:latest
|
||||
container_name: github-ntfy
|
||||
environment:
|
||||
- USERNAME=username # Required
|
||||
- PASSWORD=password # Required
|
||||
- NTFY_URL=ntfy_url # Required if ntfy is used
|
||||
- GHNTFY_TIMEOUT=timeout # Default is 3600 (1 hour)
|
||||
- GHNTFY_TOKEN= # Default is empty (Github token)
|
||||
- DOCKER_USERNAME= # Default is empty (Docker Hub username)
|
||||
- DOCKER_PASSWORD= # Default is empty (Docker Hub password)
|
||||
- GOTIFY_URL=gotify_url # Required if gotify is used
|
||||
- GOTIFY_TOKEN= # Required if gotify is used
|
||||
- DISCORD_WEBHOOK_URL= # Required if discord is used
|
||||
- SLACK_WEBHOOK_URL= # Required if Slack is used
|
||||
volumes:
|
||||
- /path/to/data:/data
|
||||
ports:
|
||||
@@ -60,9 +72,9 @@ The GitHub token (GHNTFY_TOKEN) needs to have the following permissions: repo, r
|
||||
|
||||
## TODO
|
||||
- [ ] Add support for multi achitecture Docker images
|
||||
- [x] Rework web interface
|
||||
- [ ] Rework web interface
|
||||
- [ ] Add support for more notification services (Telegram, Matrix, etc.)
|
||||
- [x] Add web oneboarding instead of using environment variables
|
||||
- [ ] Add web oneboarding instead of using environment variables
|
||||
|
||||
## Author
|
||||
👤 BreizhHardware
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Check if USERNAME and PASSWORD environment variables are defined
|
||||
if [ -n "$USERNAME" ] && [ -n "$PASSWORD" ]; then
|
||||
# Generate auth.txt file content from environment variables
|
||||
echo -n "$USERNAME:$PASSWORD" > /auth.txt
|
||||
echo "Authentication file generated from environment variables"
|
||||
else
|
||||
echo "USERNAME and/or PASSWORD variables not defined"
|
||||
echo "Authentication will be managed by the onboarding system via the web interface"
|
||||
fi
|
||||
# Génère le contenu du fichier auth.txt à partir des variables d'environnement
|
||||
echo -n "$USERNAME:$PASSWORD" | base64 > /auth.txt
|
||||
|
||||
# Set database directory permissions
|
||||
if [ -d "/github-ntfy" ]; then
|
||||
chmod -R 755 /github-ntfy
|
||||
echo "Permissions applied to data directory"
|
||||
fi
|
||||
|
||||
# Start nginx in the background
|
||||
echo "Starting Nginx..."
|
||||
# Démarrer nginx en arrière-plan
|
||||
nginx -g 'daemon off;' &
|
||||
|
||||
# Start the main application
|
||||
echo "Starting application..."
|
||||
# Exécute l'application Rust
|
||||
exec /usr/local/bin/github-ntfy
|
||||
75
nginx.conf
75
nginx.conf
@@ -6,52 +6,55 @@ http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# Ajout pour gérer les fichiers statiques correctement
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
# Ajout de cette variable pour préserver le port dans les redirections
|
||||
port_in_redirect off;
|
||||
absolute_redirect off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Configuration pour servir le frontend Nuxt statique
|
||||
location / {
|
||||
root /var/www/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Activer les options pour faciliter le débogage
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
add_header X-Frame-Options "DENY";
|
||||
add_header X-Served-By "nginx";
|
||||
root /var/www/html;
|
||||
index index.html;
|
||||
}
|
||||
|
||||
# Configuration groupée pour toutes les routes API
|
||||
location ~* ^/(app_github_repo|app_docker_repo|watched_repos|watched_docker_repos|delete_repo|delete_docker_repo|latest_updates|auth|settings|is_configured) {
|
||||
location /app_repo {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host:$server_port;
|
||||
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;
|
||||
}
|
||||
location /watched_repos {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
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;
|
||||
}
|
||||
location /delete_repo {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
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;
|
||||
}
|
||||
location /app_docker_repo {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
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;
|
||||
}
|
||||
location /watched_docker_repos {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
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;
|
||||
}
|
||||
location /delete_docker_repo {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
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 X-Forwarded-Port $server_port;
|
||||
|
||||
# Configuration importante pour les WebSockets si utilisés
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Augmenter les timeouts pour les requêtes longues
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# Ajouter des logs pour le débogage
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
access_log /var/log/nginx/access.log;
|
||||
}
|
||||
}
|
||||
|
||||
508
src/api.rs
508
src/api.rs
@@ -1,5 +1,5 @@
|
||||
use log::{error, info};
|
||||
use rusqlite::{Connection, params};
|
||||
use rusqlite::{Connection, Result as SqliteResult, params};
|
||||
use serde_json::json;
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
@@ -7,50 +7,26 @@ use tokio::sync::Mutex;
|
||||
use warp::{Filter, Reply, Rejection};
|
||||
use warp::http::StatusCode;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use chrono::Utc;
|
||||
use crate::database::{
|
||||
get_user_by_username, verify_password, create_user, create_session,
|
||||
get_session, delete_session, get_app_settings, update_app_settings
|
||||
};
|
||||
use crate::models::{UserLogin, UserRegistration, AuthResponse, ApiResponse, AppSettings};
|
||||
use warp::cors::Cors;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct RepoRequest {
|
||||
repo: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct UpdateInfo {
|
||||
date: String,
|
||||
repo: String,
|
||||
version: String,
|
||||
changelog: String,
|
||||
}
|
||||
|
||||
pub async fn start_api() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Open the database
|
||||
let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string());
|
||||
std::fs::create_dir_all(&db_path).ok();
|
||||
let repos_path = format!("{}/watched_repos.db", db_path);
|
||||
let versions_path = format!("{}/ghntfy_versions.db", db_path);
|
||||
|
||||
match Connection::open(&repos_path) {
|
||||
Ok(conn) => {
|
||||
info!("Database connection established successfully");
|
||||
let db = Arc::new(Mutex::new(conn));
|
||||
|
||||
let versions_conn = match Connection::open(&versions_path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Unable to open versions database: {}", e);
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
};
|
||||
|
||||
let versions_db = Arc::new(Mutex::new(versions_conn));
|
||||
|
||||
// Route definitions
|
||||
let add_github = warp::path("app_github_repo")
|
||||
let add_github = warp::path("app_repo")
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_db(db.clone()))
|
||||
@@ -84,55 +60,11 @@ pub async fn start_api() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(delete_docker_repo);
|
||||
|
||||
let get_updates = warp::path("latest_updates")
|
||||
.and(warp::get())
|
||||
.and(with_db(db.clone()))
|
||||
.and_then(get_latest_updates);
|
||||
|
||||
let login_route = warp::path("auth")
|
||||
.and(warp::path("login"))
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_db(versions_db.clone()))
|
||||
.and_then(login);
|
||||
|
||||
let register_route = warp::path("auth")
|
||||
.and(warp::path("register"))
|
||||
.and(warp::post())
|
||||
.and(warp::body::json())
|
||||
.and(with_db(versions_db.clone()))
|
||||
.and_then(register);
|
||||
|
||||
let logout_route = warp::path("auth")
|
||||
.and(warp::path("logout"))
|
||||
.and(warp::post())
|
||||
.and(with_auth())
|
||||
.and(with_db(versions_db.clone()))
|
||||
.and_then(logout);
|
||||
|
||||
let get_settings_route = warp::path("settings")
|
||||
.and(warp::get())
|
||||
.and(with_db(versions_db.clone()))
|
||||
.and(with_auth())
|
||||
.and_then(get_settings);
|
||||
|
||||
let update_settings_route = warp::path("settings")
|
||||
.and(warp::put())
|
||||
.and(warp::body::json())
|
||||
.and(with_db(versions_db.clone()))
|
||||
.and(with_auth())
|
||||
.and_then(update_settings);
|
||||
|
||||
let is_configured_route = warp::path("is_configured")
|
||||
.and(warp::get())
|
||||
.and(with_db(versions_db.clone()))
|
||||
.and_then(is_configured);
|
||||
|
||||
// Configure CORS
|
||||
let cors = warp::cors()
|
||||
.allow_any_origin()
|
||||
.allow_headers(vec!["Content-Type", "Authorization"])
|
||||
.allow_methods(vec!["GET", "POST", "PUT", "DELETE"]);
|
||||
.allow_headers(vec!["Content-Type"])
|
||||
.allow_methods(vec!["GET", "POST"]);
|
||||
|
||||
// Combine all routes with CORS
|
||||
let routes = add_github
|
||||
@@ -141,13 +73,6 @@ pub async fn start_api() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
|
||||
.or(get_docker)
|
||||
.or(delete_github)
|
||||
.or(delete_docker)
|
||||
.or(get_updates)
|
||||
.or(login_route)
|
||||
.or(register_route)
|
||||
.or(logout_route)
|
||||
.or(get_settings_route)
|
||||
.or(update_settings_route)
|
||||
.or(is_configured_route)
|
||||
.with(cors);
|
||||
|
||||
// Start the server
|
||||
@@ -166,27 +91,6 @@ fn with_db(db: Arc<Mutex<Connection>>) -> impl Filter<Extract = (Arc<Mutex<Conne
|
||||
warp::any().map(move || db.clone())
|
||||
}
|
||||
|
||||
fn with_auth() -> impl Filter<Extract = (String,), Error = warp::Rejection> + Clone {
|
||||
warp::header::<String>("Authorization")
|
||||
.map(|header: String| {
|
||||
if header.starts_with("Bearer ") {
|
||||
header[7..].to_string()
|
||||
} else {
|
||||
header
|
||||
}
|
||||
})
|
||||
.or_else(|_| async {
|
||||
Err(warp::reject::custom(AuthError::MissingToken))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AuthError {
|
||||
MissingToken,
|
||||
}
|
||||
|
||||
impl warp::reject::Reject for AuthError {}
|
||||
|
||||
async fn add_github_repo(body: RepoRequest, db: Arc<Mutex<Connection>>) -> Result<impl Reply, Rejection> {
|
||||
let repo = body.repo;
|
||||
|
||||
@@ -197,7 +101,7 @@ async fn add_github_repo(body: RepoRequest, db: Arc<Mutex<Connection>>) -> Resul
|
||||
));
|
||||
}
|
||||
|
||||
let db_guard = db.lock().await;
|
||||
let mut db_guard = db.lock().await;
|
||||
|
||||
// Check if repository already exists
|
||||
match db_guard.query_row(
|
||||
@@ -249,7 +153,7 @@ async fn add_docker_repo(body: RepoRequest, db: Arc<Mutex<Connection>>) -> Resul
|
||||
));
|
||||
}
|
||||
|
||||
let db_guard = db.lock().await;
|
||||
let mut db_guard = db.lock().await;
|
||||
|
||||
// Check if repository already exists
|
||||
match db_guard.query_row(
|
||||
@@ -387,7 +291,7 @@ async fn delete_github_repo(body: RepoRequest, db: Arc<Mutex<Connection>>) -> Re
|
||||
));
|
||||
}
|
||||
|
||||
let db_guard = db.lock().await;
|
||||
let mut db_guard = db.lock().await;
|
||||
|
||||
// Check if repository exists
|
||||
match db_guard.query_row(
|
||||
@@ -439,7 +343,7 @@ async fn delete_docker_repo(body: RepoRequest, db: Arc<Mutex<Connection>>) -> Re
|
||||
));
|
||||
}
|
||||
|
||||
let db_guard = db.lock().await;
|
||||
let mut db_guard = db.lock().await;
|
||||
|
||||
// Check if repository exists
|
||||
match db_guard.query_row(
|
||||
@@ -479,396 +383,4 @@ async fn delete_docker_repo(body: RepoRequest, db: Arc<Mutex<Connection>>) -> Re
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_latest_updates(db: Arc<Mutex<Connection>>) -> Result<impl Reply, Rejection> {
|
||||
let updates = {
|
||||
let _db_guard = db.lock().await;
|
||||
|
||||
let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string());
|
||||
let versions_path = format!("{}/ghntfy_versions.db", db_path);
|
||||
|
||||
match Connection::open(&versions_path) {
|
||||
Ok(versions_db) => {
|
||||
match versions_db.prepare("SELECT repo, version, changelog, datetime('now') as date FROM versions ORDER BY rowid DESC LIMIT 5") {
|
||||
Ok(mut stmt) => {
|
||||
let rows = match stmt.query_map([], |row| {
|
||||
Ok(UpdateInfo {
|
||||
repo: row.get(0)?,
|
||||
version: row.get(1)?,
|
||||
changelog: row.get(2)?,
|
||||
date: row.get(3)?,
|
||||
})
|
||||
}) {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
error!("Error executing query: {}", e);
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&json!({"error": format!("Database error: {}", e)})),
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let mut updates = Vec::new();
|
||||
for row in rows {
|
||||
if let Ok(update) = row {
|
||||
updates.push(update);
|
||||
}
|
||||
}
|
||||
|
||||
if updates.is_empty() {
|
||||
vec![
|
||||
UpdateInfo {
|
||||
date: Utc::now().to_rfc3339(),
|
||||
repo: "BreizhHardware/ntfy_alerts".to_string(),
|
||||
version: "2.0.2".to_string(),
|
||||
changelog: "- Aucune mise à jour trouvée dans la base de données\n- Ceci est une donnée d'exemple".to_string(),
|
||||
}
|
||||
]
|
||||
} else {
|
||||
updates
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error preparing query: {}", e);
|
||||
vec![
|
||||
UpdateInfo {
|
||||
date: Utc::now().to_rfc3339(),
|
||||
repo: "Erreur".to_string(),
|
||||
version: "N/A".to_string(),
|
||||
changelog: format!("- Erreur lors de la préparation de la requête: {}", e),
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error opening versions database: {}", e);
|
||||
vec![
|
||||
UpdateInfo {
|
||||
date: Utc::now().to_rfc3339(),
|
||||
repo: "Erreur".to_string(),
|
||||
version: "N/A".to_string(),
|
||||
changelog: format!("- Erreur lors de l'ouverture de la base de données: {}", e),
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&updates),
|
||||
StatusCode::OK
|
||||
))
|
||||
}
|
||||
|
||||
async fn login(login: UserLogin, db: Arc<Mutex<Connection>>) -> Result<impl Reply, Rejection> {
|
||||
let conn = db.lock().await;
|
||||
|
||||
match verify_password(&conn, &login.username, &login.password) {
|
||||
Ok(true) => {
|
||||
if let Ok(Some(user)) = get_user_by_username(&conn, &login.username) {
|
||||
if let Ok(token) = create_session(&conn, user.id) {
|
||||
let auth_response = AuthResponse {
|
||||
token,
|
||||
user: user.clone(),
|
||||
};
|
||||
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse {
|
||||
success: true,
|
||||
message: "Login successful".to_string(),
|
||||
data: Some(auth_response),
|
||||
}),
|
||||
StatusCode::OK,
|
||||
))
|
||||
} else {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Error creating session".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "User not found".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::NOT_FOUND,
|
||||
))
|
||||
}
|
||||
},
|
||||
Ok(false) => {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Incorrect username or password".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
))
|
||||
},
|
||||
Err(_) => {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Internal server error".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn register(registration: UserRegistration, db: Arc<Mutex<Connection>>) -> Result<impl Reply, Rejection> {
|
||||
let conn = db.lock().await;
|
||||
|
||||
// Check if a user already exists with this username
|
||||
if let Ok(Some(_)) = get_user_by_username(&conn, ®istration.username) {
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "A user with this name already exists".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::CONFLICT,
|
||||
));
|
||||
}
|
||||
|
||||
// Create the new user
|
||||
match create_user(&conn, ®istration.username, ®istration.password, registration.is_admin) {
|
||||
Ok(user_id) => {
|
||||
if let Ok(Some(user)) = get_user_by_username(&conn, ®istration.username) {
|
||||
if let Ok(token) = create_session(&conn, user_id) {
|
||||
let auth_response = AuthResponse {
|
||||
token,
|
||||
user,
|
||||
};
|
||||
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse {
|
||||
success: true,
|
||||
message: "Registration successful".to_string(),
|
||||
data: Some(auth_response),
|
||||
}),
|
||||
StatusCode::CREATED,
|
||||
))
|
||||
} else {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Error creating session".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Error retrieving user".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
))
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Error creating user".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn logout(token: String, db: Arc<Mutex<Connection>>) -> Result<impl Reply, Rejection> {
|
||||
let conn = db.lock().await;
|
||||
|
||||
match delete_session(&conn, &token) {
|
||||
Ok(_) => {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: true,
|
||||
message: "Logout successful".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::OK,
|
||||
))
|
||||
},
|
||||
Err(_) => {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Error during logout".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_settings(db: Arc<Mutex<Connection>>, token: String) -> Result<impl Reply, Rejection> {
|
||||
let conn = db.lock().await;
|
||||
|
||||
// Verify authentication
|
||||
if let Ok(Some(session)) = get_session(&conn, &token) {
|
||||
if session.expires_at < Utc::now() {
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Session expired".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
));
|
||||
}
|
||||
|
||||
// Retrieve settings
|
||||
match get_app_settings(&conn) {
|
||||
Ok(Some(settings)) => {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse {
|
||||
success: true,
|
||||
message: "Settings retrieved successfully".to_string(),
|
||||
data: Some(settings),
|
||||
}),
|
||||
StatusCode::OK,
|
||||
))
|
||||
},
|
||||
Ok(None) => {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "No settings found".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::NOT_FOUND,
|
||||
))
|
||||
},
|
||||
Err(_) => {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Error retrieving settings".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Unauthorized".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_settings(settings: AppSettings, db: Arc<Mutex<Connection>>, token: String) -> Result<impl Reply, Rejection> {
|
||||
let conn = db.lock().await;
|
||||
|
||||
// Verify authentication
|
||||
if let Ok(Some(session)) = get_session(&conn, &token) {
|
||||
if session.expires_at < Utc::now() {
|
||||
return Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Session expired".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
));
|
||||
}
|
||||
|
||||
// Update settings
|
||||
match update_app_settings(&conn, &settings) {
|
||||
Ok(_) => {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: true,
|
||||
message: "Settings updated successfully".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::OK,
|
||||
))
|
||||
},
|
||||
Err(_) => {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Error updating settings".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse::<()> {
|
||||
success: false,
|
||||
message: "Unauthorized".to_string(),
|
||||
data: None,
|
||||
}),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check if the application is configured
|
||||
async fn is_configured(db: Arc<Mutex<Connection>>) -> Result<impl Reply, Rejection> {
|
||||
let conn = db.lock().await;
|
||||
|
||||
// Check if at least one admin user exists
|
||||
let admin_exists = match conn.query_row(
|
||||
"SELECT COUNT(*) FROM users WHERE is_admin = 1",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0)
|
||||
) {
|
||||
Ok(count) => count > 0,
|
||||
Err(_) => false,
|
||||
};
|
||||
|
||||
// Check if settings are configured
|
||||
let settings_exist = match get_app_settings(&conn) {
|
||||
Ok(Some(settings)) => {
|
||||
// Check if at least one notification service is configured
|
||||
settings.ntfy_url.is_some() ||
|
||||
settings.discord_webhook_url.is_some() ||
|
||||
settings.slack_webhook_url.is_some() ||
|
||||
settings.gotify_url.is_some()
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::json(&ApiResponse {
|
||||
success: true,
|
||||
message: "Configuration status retrieved".to_string(),
|
||||
data: Some(json!({
|
||||
"configured": admin_exists && settings_exist,
|
||||
"admin_exists": admin_exists,
|
||||
"settings_exist": settings_exist
|
||||
})),
|
||||
}),
|
||||
StatusCode::OK,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
use dotenv::dotenv;
|
||||
use log::info;
|
||||
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use rusqlite::Connection;
|
||||
use crate::docker::create_dockerhub_token;
|
||||
use crate::database::get_app_settings;
|
||||
|
||||
// Configuration
|
||||
pub struct Config {
|
||||
@@ -60,49 +57,6 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_database(conn: &Connection) -> Self {
|
||||
// First, try to load from database
|
||||
if let Ok(Some(settings)) = get_app_settings(conn) {
|
||||
let docker_username = settings.docker_username;
|
||||
let docker_password = settings.docker_password.clone();
|
||||
|
||||
let docker_token = if let (Some(username), Some(password)) = (&docker_username, &docker_password) {
|
||||
create_dockerhub_token(username, password)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Read authentication file (for compatibility with the old system)
|
||||
let mut auth = String::new();
|
||||
if let Ok(mut file) = File::open("/auth.txt") {
|
||||
file.read_to_string(&mut auth).ok();
|
||||
auth = auth.trim().to_string();
|
||||
}
|
||||
|
||||
let timeout = settings.check_interval.unwrap_or(3600) as f64;
|
||||
|
||||
info!("Configuration loaded from database");
|
||||
|
||||
return Config {
|
||||
github_token: settings.github_token,
|
||||
docker_username,
|
||||
docker_password,
|
||||
docker_token,
|
||||
ntfy_url: settings.ntfy_url,
|
||||
gotify_url: settings.gotify_url,
|
||||
gotify_token: settings.gotify_token,
|
||||
discord_webhook_url: settings.discord_webhook_url,
|
||||
slack_webhook_url: settings.slack_webhook_url,
|
||||
auth,
|
||||
timeout,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to environment variables if database is not available
|
||||
info!("No configuration found in database, using environment variables");
|
||||
Self::from_env()
|
||||
}
|
||||
|
||||
pub fn github_headers(&self) -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
if let Some(token) = &self.github_token {
|
||||
|
||||
321
src/database.rs
321
src/database.rs
@@ -1,10 +1,7 @@
|
||||
use log::info;
|
||||
pub(crate) use rusqlite::{Connection, Result as SqliteResult, OpenFlags, Error as SqliteError};
|
||||
pub(crate) use rusqlite::{Connection, Result as SqliteResult, OpenFlags};
|
||||
use std::env;
|
||||
use chrono::Utc;
|
||||
use rand::Rng;
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
use crate::models::{User, Session, AppSettings};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn init_databases() -> SqliteResult<(Connection, Connection)> {
|
||||
let db_path = env::var("DB_PATH").unwrap_or_else(|_| "/github-ntfy".to_string());
|
||||
@@ -37,111 +34,6 @@ pub fn init_databases() -> SqliteResult<(Connection, Connection)> {
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS app_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
ntfy_url TEXT,
|
||||
github_token TEXT,
|
||||
docker_username TEXT,
|
||||
docker_password TEXT,
|
||||
gotify_url TEXT,
|
||||
gotify_token TEXT,
|
||||
discord_webhook_url TEXT,
|
||||
slack_webhook_url TEXT,
|
||||
check_interval INTEGER DEFAULT 3600,
|
||||
auth TEXT,
|
||||
last_updated TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
let admin_exists = conn
|
||||
.query_row("SELECT COUNT(*) FROM users WHERE is_admin = 1", [], |row| {
|
||||
row.get::<_, i64>(0)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
if admin_exists == 0 {
|
||||
if let (Ok(username), Ok(password)) = (env::var("USERNAME"), env::var("PASSWORD")) {
|
||||
if !username.is_empty() && !password.is_empty() {
|
||||
let hashed_password = hash(password, DEFAULT_COST).unwrap_or_else(|_| String::new());
|
||||
let now = Utc::now().to_rfc3339();
|
||||
if let Err(e) = conn.execute(
|
||||
"INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?, ?, 1, ?)",
|
||||
&[&username, &hashed_password, &now],
|
||||
) {
|
||||
info!("Erreur lors de la création de l'utilisateur admin: {}", e);
|
||||
} else {
|
||||
info!("Utilisateur admin créé avec succès depuis les variables d'environnement");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let settings_exist = conn
|
||||
.query_row("SELECT COUNT(*) FROM app_settings", [], |row| {
|
||||
row.get::<_, i64>(0)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
if settings_exist == 0 {
|
||||
let ntfy_url = env::var("NTFY_URL").ok();
|
||||
let github_token = env::var("GHNTFY_TOKEN").ok();
|
||||
let docker_username = env::var("DOCKER_USERNAME").ok();
|
||||
let docker_password = env::var("DOCKER_PASSWORD").ok();
|
||||
let gotify_url = env::var("GOTIFY_URL").ok();
|
||||
let gotify_token = env::var("GOTIFY_TOKEN").ok();
|
||||
let discord_webhook_url = env::var("DISCORD_WEBHOOK_URL").ok();
|
||||
let slack_webhook_url = env::var("SLACK_WEBHOOK_URL").ok();
|
||||
let check_interval = env::var("GHNTFY_TIMEOUT")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<i64>().ok())
|
||||
.unwrap_or(3600);
|
||||
let now = Utc::now().to_rfc3339();
|
||||
|
||||
if let Err(e) = conn.execute(
|
||||
"INSERT INTO app_settings (id, ntfy_url, github_token, docker_username, docker_password, gotify_url, gotify_token, discord_webhook_url, slack_webhook_url, check_interval, last_updated)
|
||||
VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
rusqlite::params![
|
||||
ntfy_url,
|
||||
github_token,
|
||||
docker_username,
|
||||
docker_password,
|
||||
gotify_url,
|
||||
gotify_token,
|
||||
discord_webhook_url,
|
||||
slack_webhook_url,
|
||||
check_interval,
|
||||
now
|
||||
],
|
||||
) {
|
||||
info!("Erreur lors de l'initialisation des paramètres: {}", e);
|
||||
} else {
|
||||
info!("Paramètres initialisés avec succès depuis les variables d'environnement");
|
||||
}
|
||||
}
|
||||
|
||||
let conn2 = Connection::open_with_flags(&repos_path, OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_URI)?;
|
||||
|
||||
info!("Database open at {}", repos_path);
|
||||
@@ -208,211 +100,4 @@ pub fn update_version(conn: &Connection, repo: &str, version: &str, changelog: O
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_user(conn: &Connection, username: &str, password: &str, is_admin: bool) -> SqliteResult<i64> {
|
||||
let hashed_password = hash(password, DEFAULT_COST).map_err(|e| {
|
||||
SqliteError::SqliteFailure(
|
||||
rusqlite::ffi::Error::new(1),
|
||||
Some(e.to_string())
|
||||
)
|
||||
})?;
|
||||
|
||||
let now = Utc::now().to_rfc3339();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO users (username, password_hash, is_admin, created_at) VALUES (?, ?, ?, ?)",
|
||||
&[username, &hashed_password, &(if is_admin { 1 } else { 0 }).to_string(), &now],
|
||||
)?;
|
||||
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub fn get_user_by_username(conn: &Connection, username: &str) -> SqliteResult<Option<User>> {
|
||||
let mut stmt = conn.prepare("SELECT id, username, password_hash, is_admin, created_at FROM users WHERE username = ?")?;
|
||||
let mut rows = stmt.query(&[username])?;
|
||||
|
||||
if let Some(row) = rows.next()? {
|
||||
let id = row.get(0)?;
|
||||
let username = row.get(1)?;
|
||||
let password_hash = row.get(2)?;
|
||||
let is_admin: i64 = row.get(3)?;
|
||||
let created_at_str: String = row.get(4)?;
|
||||
let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| {
|
||||
SqliteError::SqliteFailure(
|
||||
rusqlite::ffi::Error::new(1),
|
||||
Some(e.to_string())
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Some(User {
|
||||
id,
|
||||
username,
|
||||
password_hash,
|
||||
is_admin: is_admin == 1,
|
||||
created_at,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_password(conn: &Connection, username: &str, password: &str) -> SqliteResult<bool> {
|
||||
if let Some(user) = get_user_by_username(conn, username)? {
|
||||
Ok(verify(password, &user.password_hash).unwrap_or(false))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_session(conn: &Connection, user_id: i64) -> SqliteResult<String> {
|
||||
let token = generate_session_token();
|
||||
let expires_at = Utc::now() + chrono::Duration::days(7);
|
||||
let expires_at_str = expires_at.to_rfc3339();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)",
|
||||
&[&token, &user_id.to_string(), &expires_at_str],
|
||||
)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub fn get_session(conn: &Connection, token: &str) -> SqliteResult<Option<Session>> {
|
||||
let mut stmt = conn.prepare("SELECT token, user_id, expires_at FROM sessions WHERE token = ?")?;
|
||||
let mut rows = stmt.query(&[token])?;
|
||||
|
||||
if let Some(row) = rows.next()? {
|
||||
let token = row.get(0)?;
|
||||
let user_id = row.get(1)?;
|
||||
let expires_at_str: String = row.get(2)?;
|
||||
let expires_at = chrono::DateTime::parse_from_rfc3339(&expires_at_str)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| {
|
||||
SqliteError::SqliteFailure(
|
||||
rusqlite::ffi::Error::new(1),
|
||||
Some(e.to_string())
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Some(Session {
|
||||
token,
|
||||
user_id,
|
||||
expires_at,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_session(conn: &Connection, token: &str) -> SqliteResult<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM sessions WHERE token = ?",
|
||||
&[token],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_app_settings(conn: &Connection) -> SqliteResult<Option<AppSettings>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, ntfy_url, github_token, docker_username, docker_password,
|
||||
gotify_url, gotify_token, discord_webhook_url, slack_webhook_url,
|
||||
check_interval, auth, last_updated
|
||||
FROM app_settings
|
||||
WHERE id = 1"
|
||||
)?;
|
||||
|
||||
let mut rows = stmt.query([])?;
|
||||
|
||||
if let Some(row) = rows.next()? {
|
||||
let id = row.get(0)?;
|
||||
let ntfy_url = row.get(1)?;
|
||||
let github_token = row.get(2)?;
|
||||
let docker_username = row.get(3)?;
|
||||
let docker_password = row.get(4)?;
|
||||
let gotify_url = row.get(5)?;
|
||||
let gotify_token = row.get(6)?;
|
||||
let discord_webhook_url = row.get(7)?;
|
||||
let slack_webhook_url = row.get(8)?;
|
||||
let check_interval = row.get(9)?;
|
||||
let auth = row.get(10)?;
|
||||
let last_updated_str: String = row.get(11)?;
|
||||
let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.map_err(|e| {
|
||||
SqliteError::SqliteFailure(
|
||||
rusqlite::ffi::Error::new(1),
|
||||
Some(e.to_string())
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Some(AppSettings {
|
||||
id: Some(id),
|
||||
ntfy_url,
|
||||
github_token,
|
||||
docker_username,
|
||||
docker_password,
|
||||
gotify_url,
|
||||
gotify_token,
|
||||
discord_webhook_url,
|
||||
slack_webhook_url,
|
||||
check_interval,
|
||||
auth,
|
||||
last_updated,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_app_settings(conn: &Connection, settings: &AppSettings) -> SqliteResult<()> {
|
||||
let now = Utc::now().to_rfc3339();
|
||||
|
||||
conn.execute(
|
||||
"UPDATE app_settings
|
||||
SET ntfy_url = ?, github_token = ?, docker_username = ?, docker_password = ?,
|
||||
gotify_url = ?, gotify_token = ?, discord_webhook_url = ?, slack_webhook_url = ?,
|
||||
check_interval = ?, auth = ?, last_updated = ?
|
||||
WHERE id = 1",
|
||||
rusqlite::params![
|
||||
settings.ntfy_url,
|
||||
settings.github_token,
|
||||
settings.docker_username,
|
||||
settings.docker_password,
|
||||
settings.gotify_url,
|
||||
settings.gotify_token,
|
||||
settings.discord_webhook_url,
|
||||
settings.slack_webhook_url,
|
||||
settings.check_interval,
|
||||
settings.auth,
|
||||
now
|
||||
],
|
||||
)?;
|
||||
|
||||
// If auth credentials are provided, write them to the auth.txt file
|
||||
if let Some(auth) = &settings.auth {
|
||||
if !auth.is_empty() {
|
||||
if let Err(e) = std::fs::write("/auth.txt", auth) {
|
||||
log::error!("Error writing to auth.txt file: {}", e);
|
||||
} else {
|
||||
log::info!("Successfully updated auth.txt file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_session_token() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let token_bytes: Vec<u8> = (0..32).map(|_| rng.gen::<u8>()).collect();
|
||||
|
||||
// Convertir en hexadécimal
|
||||
token_bytes.iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect::<Vec<String>>()
|
||||
.join("")
|
||||
}
|
||||
}
|
||||
125
src/main.rs
125
src/main.rs
@@ -9,6 +9,7 @@ mod api;
|
||||
use log::{error, info};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::task;
|
||||
|
||||
// Function to start the API in a separate thread
|
||||
fn start_api() {
|
||||
@@ -27,120 +28,26 @@ fn start_api() {
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
|
||||
// Initialize databases
|
||||
let config = config::Config::from_env();
|
||||
let (conn_versions, conn_repos) = database::init_databases()?;
|
||||
|
||||
// Load environment variables
|
||||
let env_config = config::Config::from_env();
|
||||
|
||||
// Only update database with env vars if they are explicitly set
|
||||
// We check each field individually instead of overwriting everything
|
||||
let has_env_notification = env_config.ntfy_url.is_some() ||
|
||||
env_config.gotify_url.is_some() ||
|
||||
env_config.discord_webhook_url.is_some() ||
|
||||
env_config.slack_webhook_url.is_some();
|
||||
|
||||
if has_env_notification {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// First, ensure there's a record in the database
|
||||
conn_versions.execute(
|
||||
"INSERT OR IGNORE INTO app_settings (id, last_updated) VALUES (1, ?)",
|
||||
rusqlite::params![now],
|
||||
).map_err(|e| error!("Failed to initialize app settings: {}", e)).ok();
|
||||
|
||||
// Then update only the fields that are set in environment variables
|
||||
if let Some(ntfy_url) = &env_config.ntfy_url {
|
||||
conn_versions.execute(
|
||||
"UPDATE app_settings SET ntfy_url = ?, last_updated = ? WHERE id = 1",
|
||||
rusqlite::params![ntfy_url, now],
|
||||
).ok();
|
||||
}
|
||||
|
||||
if let Some(github_token) = &env_config.github_token {
|
||||
conn_versions.execute(
|
||||
"UPDATE app_settings SET github_token = ?, last_updated = ? WHERE id = 1",
|
||||
rusqlite::params![github_token, now],
|
||||
).ok();
|
||||
}
|
||||
|
||||
if let Some(docker_username) = &env_config.docker_username {
|
||||
conn_versions.execute(
|
||||
"UPDATE app_settings SET docker_username = ?, last_updated = ? WHERE id = 1",
|
||||
rusqlite::params![docker_username, now],
|
||||
).ok();
|
||||
}
|
||||
|
||||
if let Some(docker_password) = &env_config.docker_password {
|
||||
conn_versions.execute(
|
||||
"UPDATE app_settings SET docker_password = ?, last_updated = ? WHERE id = 1",
|
||||
rusqlite::params![docker_password, now],
|
||||
).ok();
|
||||
}
|
||||
|
||||
if let Some(gotify_url) = &env_config.gotify_url {
|
||||
conn_versions.execute(
|
||||
"UPDATE app_settings SET gotify_url = ?, last_updated = ? WHERE id = 1",
|
||||
rusqlite::params![gotify_url, now],
|
||||
).ok();
|
||||
}
|
||||
|
||||
if let Some(gotify_token) = &env_config.gotify_token {
|
||||
conn_versions.execute(
|
||||
"UPDATE app_settings SET gotify_token = ?, last_updated = ? WHERE id = 1",
|
||||
rusqlite::params![gotify_token, now],
|
||||
).ok();
|
||||
}
|
||||
|
||||
if let Some(discord_webhook_url) = &env_config.discord_webhook_url {
|
||||
conn_versions.execute(
|
||||
"UPDATE app_settings SET discord_webhook_url = ?, last_updated = ? WHERE id = 1",
|
||||
rusqlite::params![discord_webhook_url, now],
|
||||
).ok();
|
||||
}
|
||||
|
||||
if let Some(slack_webhook_url) = &env_config.slack_webhook_url {
|
||||
conn_versions.execute(
|
||||
"UPDATE app_settings SET slack_webhook_url = ?, last_updated = ? WHERE id = 1",
|
||||
rusqlite::params![slack_webhook_url, now],
|
||||
).ok();
|
||||
}
|
||||
|
||||
conn_versions.execute(
|
||||
"UPDATE app_settings SET check_interval = ?, last_updated = ? WHERE id = 1",
|
||||
rusqlite::params![env_config.timeout as i64, now],
|
||||
).ok();
|
||||
|
||||
info!("Configuration updated from environment variables (selective update)");
|
||||
}
|
||||
|
||||
// Load configuration from database, with fallback to environment variables
|
||||
let config = config::Config::from_database(&conn_versions);
|
||||
|
||||
// Check if configuration is complete
|
||||
let config_is_incomplete = config.auth.is_empty() || (config.ntfy_url.is_none() && config.gotify_url.is_none()
|
||||
&& config.discord_webhook_url.is_none() && config.slack_webhook_url.is_none());
|
||||
start_api();
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Now handle incomplete configuration
|
||||
if config_is_incomplete {
|
||||
info!("No notification service is configured.");
|
||||
info!("Please configure at least one notification service via the web interface or environment variables.");
|
||||
info!("Starting the REST API for configuration.");
|
||||
|
||||
// Start the REST API only if configuration is incomplete
|
||||
start_api();
|
||||
|
||||
// Continue running to allow configuration through the API
|
||||
loop {
|
||||
thread::sleep(Duration::from_secs(60));
|
||||
}
|
||||
if config.auth.is_empty() || (config.ntfy_url.is_none() && config.gotify_url.is_none()
|
||||
&& config.discord_webhook_url.is_none() && config.slack_webhook_url.is_none()) {
|
||||
error!("Incorrect configuration!");
|
||||
error!("auth: can be generated with the command: echo -n 'username:password' | base64");
|
||||
error!("NTFY_URL: URL of the ntfy server");
|
||||
error!("GOTIFY_URL: URL of the gotify server");
|
||||
error!("GOTIFY_TOKEN: Gotify token");
|
||||
error!("DISCORD_WEBHOOK_URL: Discord webhook URL");
|
||||
error!("SLACK_WEBHOOK_URL: Slack webhook URL");
|
||||
error!("GHNTFY_TIMEOUT: interval between checks");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Start the REST API only if configuration is complete
|
||||
start_api();
|
||||
|
||||
info!("Starting version monitoring...");
|
||||
|
||||
loop {
|
||||
@@ -150,8 +57,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let github_releases = github::get_latest_releases(&github_repos, &client, config.github_headers()).await;
|
||||
let docker_releases = docker::get_latest_docker_releases(&docker_repos, &client, config.docker_headers()).await;
|
||||
|
||||
let _ = notifications::send_notifications(github_releases, docker_releases, &config, &conn_versions).await;
|
||||
notifications::send_notifications(github_releases, docker_releases, &config, &conn_versions).await;
|
||||
|
||||
tokio::time::sleep(Duration::from_secs_f64(config.timeout)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
// Structures for GitHub data
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
@@ -14,7 +13,6 @@ pub struct GithubRelease {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GithubReleaseInfo {
|
||||
pub repo: String,
|
||||
#[allow(dead_code)]
|
||||
pub name: String,
|
||||
pub tag_name: String,
|
||||
pub html_url: String,
|
||||
@@ -37,67 +35,8 @@ pub struct DockerReleaseInfo {
|
||||
pub published_at: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct NotifiedRelease {
|
||||
pub repo: String,
|
||||
pub tag_name: String,
|
||||
pub notified_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
pub is_admin: bool,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserLogin {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserRegistration {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Session {
|
||||
pub token: String,
|
||||
pub user_id: i64,
|
||||
pub expires_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AppSettings {
|
||||
pub id: Option<i64>,
|
||||
pub ntfy_url: Option<String>,
|
||||
pub github_token: Option<String>,
|
||||
pub docker_username: Option<String>,
|
||||
pub docker_password: Option<String>,
|
||||
pub gotify_url: Option<String>,
|
||||
pub gotify_token: Option<String>,
|
||||
pub discord_webhook_url: Option<String>,
|
||||
pub slack_webhook_url: Option<String>,
|
||||
pub check_interval: Option<i64>,
|
||||
pub auth: Option<String>,
|
||||
pub last_updated: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AuthResponse {
|
||||
pub token: String,
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub data: Option<T>,
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ pub async fn send_to_slack(release: DockerReleaseInfo, webhook_url: &str) {
|
||||
slack::send_docker_notification(&release, webhook_url).await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn send_notifications(releases: &[DockerReleaseInfo], config: &Config) {
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ pub async fn send_to_slack(release: GithubReleaseInfo, webhook_url: &str) {
|
||||
slack::send_github_notification(&release, webhook_url).await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn send_notifications(releases: &[GithubReleaseInfo], config: &Config) {
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use log::{error, info};
|
||||
use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
|
||||
use crate::models::{GithubReleaseInfo, DockerReleaseInfo};
|
||||
|
||||
pub async fn send_github_notification(release: &GithubReleaseInfo, auth: &str, ntfy_url: &str) {
|
||||
|
||||
24
web/.gitignore
vendored
24
web/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -1,75 +0,0 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
19
web/app.vue
19
web/app.vue
@@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-900 text-gray-200">
|
||||
<UContainer>
|
||||
<AppHeader />
|
||||
<main class="py-8">
|
||||
<NuxtPage />
|
||||
</main>
|
||||
<AppFooter />
|
||||
</UContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// No script content provided in the original code
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* No style content provided in the original code */
|
||||
</style>
|
||||
@@ -1,2 +0,0 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@nuxt/ui';
|
||||
@@ -1,6 +0,0 @@
|
||||
<template>
|
||||
<footer class="text-center py-6 bg-emerald-950 rounded-t-lg mt-4">
|
||||
<p class="text-sm">I know this web interface is simple, but I'm improving!</p>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<header class="py-6 bg-emerald-950 shadow-lg rounded-b-lg mb-4">
|
||||
<div class="container mx-auto px-4 flex justify-between items-center">
|
||||
<NuxtLink to="/" class="text-white hover:text-gray-200 transition-colors duration-200">
|
||||
<h1 class="text-4xl font-bold tracking-wide">Github Ntfy</h1>
|
||||
</NuxtLink>
|
||||
|
||||
<div v-if="auth.isAuthenticated" class="flex space-x-3">
|
||||
<UButton
|
||||
to="/settings"
|
||||
variant="ghost"
|
||||
color="white"
|
||||
icon="i-heroicons-cog-6-tooth"
|
||||
size="sm"
|
||||
>
|
||||
Settings
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
@click="handleLogout"
|
||||
variant="ghost"
|
||||
color="white"
|
||||
icon="i-heroicons-arrow-right-on-rectangle"
|
||||
size="sm"
|
||||
>
|
||||
Logout
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const handleLogout = async () => {
|
||||
await auth.logout();
|
||||
router.push('/login');
|
||||
};
|
||||
</script>
|
||||
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<UCard class="bg-emerald-950 shadow-lg">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold">Add a Docker Repo</h2>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="addDockerRepo">
|
||||
<UFormGroup label="Name of the Docker Repo" name="dockerRepo">
|
||||
<div class="flex items-center">
|
||||
<UBadge class="mr-2 py-2.5 px-3 bg-gray-700 text-gray-400">hub.docker.com/r/</UBadge>
|
||||
<UInput
|
||||
v-model="dockerRepoName"
|
||||
placeholder="breizhhardware/github-ntfy"
|
||||
class="flex-1 bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
<div class="flex justify-end gap-4 mt-4">
|
||||
<UButton color="gray" variant="ghost" @click="dockerRepoName = ''">Cancel</UButton>
|
||||
<UButton type="submit" color="green" variant="solid">Save</UButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Watched Docker Repositories</h3>
|
||||
<UList v-if="watchedDockerRepos.length" class="space-y-2">
|
||||
<UListItem v-for="repo in watchedDockerRepos" :key="repo" class="flex justify-between items-center">
|
||||
<span>{{ repo }}</span>
|
||||
<UButton
|
||||
color="red"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-x-mark"
|
||||
size="xs"
|
||||
@click="removeDockerRepo(repo)"
|
||||
/>
|
||||
</UListItem>
|
||||
</UList>
|
||||
<p v-else class="text-gray-400 italic">No Docker repositories being watched</p>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const dockerRepoName = ref('')
|
||||
const watchedDockerRepos = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
refreshWatchedDockerRepos()
|
||||
})
|
||||
|
||||
async function addDockerRepo() {
|
||||
if (!dockerRepoName.value) return
|
||||
|
||||
try {
|
||||
const response = await fetch('/app_docker_repo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ repo: dockerRepoName.value })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
dockerRepoName.value = ''
|
||||
await refreshWatchedDockerRepos()
|
||||
} else {
|
||||
throw new Error('Failed to add Docker repository')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWatchedDockerRepos() {
|
||||
try {
|
||||
const response = await fetch('/watched_docker_repos')
|
||||
if (response.ok) {
|
||||
watchedDockerRepos.value = await response.json()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching watched Docker repos:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDockerRepo(repo) {
|
||||
try {
|
||||
const response = await fetch('/delete_docker_repo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ repo })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
await refreshWatchedDockerRepos()
|
||||
} else {
|
||||
throw new Error('Failed to remove Docker repository')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<UCard class="bg-emerald-950 shadow-lg">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold">Add a Github Repo</h2>
|
||||
</template>
|
||||
|
||||
<form @submit.prevent="addRepo">
|
||||
<UFormGroup label="Name of the Github Repo" name="repo">
|
||||
<div class="flex items-center">
|
||||
<UBadge class="mr-2 py-2.5 px-3 bg-gray-700 text-gray-400">github.com/</UBadge>
|
||||
<UInput
|
||||
v-model="repoName"
|
||||
placeholder="BreizhHardware/ntfy_alerts"
|
||||
class="flex-1 bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
</UFormGroup>
|
||||
<div class="flex justify-end gap-4 mt-4">
|
||||
<UButton color="gray" variant="ghost" @click="repoName = ''">Cancel</UButton>
|
||||
<UButton type="submit" color="green" variant="solid">Save</UButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Watched Github Repositories</h3>
|
||||
<UList v-if="watchedRepos.length" class="space-y-2">
|
||||
<UListItem v-for="repo in watchedRepos" :key="repo" class="flex justify-between items-center">
|
||||
<span>{{ repo }}</span>
|
||||
<UButton
|
||||
color="red"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-x-mark"
|
||||
size="xs"
|
||||
@click="removeRepo(repo)"
|
||||
/>
|
||||
</UListItem>
|
||||
</UList>
|
||||
<p v-else class="text-gray-400 italic">No repositories being watched</p>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const repoName = ref('')
|
||||
const watchedRepos = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
refreshWatchedRepos()
|
||||
})
|
||||
|
||||
async function addRepo() {
|
||||
if (!repoName.value) return
|
||||
|
||||
try {
|
||||
const response = await fetch('/app_github_repo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ repo: repoName.value })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
repoName.value = ''
|
||||
await refreshWatchedRepos()
|
||||
} else {
|
||||
throw new Error('Failed to add repository')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWatchedRepos() {
|
||||
try {
|
||||
const response = await fetch('/watched_repos')
|
||||
if (response.ok) {
|
||||
watchedRepos.value = await response.json()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching watched repos:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRepo(repo) {
|
||||
try {
|
||||
const response = await fetch('/delete_repo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ repo })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
await refreshWatchedRepos()
|
||||
} else {
|
||||
throw new Error('Failed to remove repository')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<UCard class="bg-gray-800 shadow-lg mb-8">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold">Latest Updates</h2>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-for="(update, index) in latestUpdates" :key="index" class="border border-gray-700 rounded-md overflow-hidden">
|
||||
<button
|
||||
@click="toggleChangelog(index)"
|
||||
class="w-full flex justify-between items-center px-4 py-3 bg-gray-700 hover:bg-gray-600 transition-colors text-left"
|
||||
>
|
||||
<div>
|
||||
<span class="font-medium">{{ update.repo }} - {{ update.version }}</span>
|
||||
<div class="text-sm text-gray-400">{{ update.date }}</div>
|
||||
</div>
|
||||
<UIcon :name="openStates[index] ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'" class="text-gray-400" />
|
||||
</button>
|
||||
<div
|
||||
v-show="openStates[index]"
|
||||
class="p-4 bg-gray-800 prose prose-invert max-w-none transition-all"
|
||||
v-html="renderedChangelogs[index]"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { marked } from 'marked';
|
||||
|
||||
const latestUpdates = ref([]);
|
||||
const renderedChangelogs = ref([]);
|
||||
const openStates = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await fetch('/latest_updates');
|
||||
if (response.ok) {
|
||||
latestUpdates.value = await response.json();
|
||||
renderedChangelogs.value = latestUpdates.value.map(update =>
|
||||
marked(update.changelog)
|
||||
);
|
||||
openStates.value = Array(latestUpdates.value.length).fill(false);
|
||||
} else {
|
||||
console.error('Erreur lors de la récupération des mises à jour');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleChangelog(index) {
|
||||
openStates.value[index] = !openStates.value[index];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.prose h1, .prose h2, .prose h3 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.prose ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5em;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: #60a5fa;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
border-left: 4px solid #4b5563;
|
||||
padding-left: 1em;
|
||||
font-style: italic;
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,154 +0,0 @@
|
||||
// Composable for managing authentication
|
||||
export const useAuth = () => {
|
||||
const user = useState('user', () => null);
|
||||
const token = useState('token', () => null);
|
||||
const isFirstLogin = useState('isFirstLogin', () => false);
|
||||
|
||||
// Initialize authentication state from localStorage
|
||||
onMounted(() => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const storedUser = localStorage.getItem('user');
|
||||
|
||||
if (storedToken && storedUser) {
|
||||
token.value = storedToken;
|
||||
user.value = JSON.parse(storedUser);
|
||||
}
|
||||
});
|
||||
|
||||
// Login function
|
||||
const login = async (username, password) => {
|
||||
try {
|
||||
const response = await fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Login failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success || !data.data) {
|
||||
throw new Error(data.message || 'Login failed');
|
||||
}
|
||||
|
||||
// Store authentication information
|
||||
token.value = data.data.token;
|
||||
user.value = data.data.user;
|
||||
|
||||
localStorage.setItem('token', data.data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.data.user));
|
||||
|
||||
// Check if this is the first login
|
||||
const configResponse = await fetch('/is_configured');
|
||||
if (configResponse.ok) {
|
||||
const configData = await configResponse.json();
|
||||
isFirstLogin.value = !configData.data.settings_exist;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Registration function
|
||||
const register = async (username, password, isAdmin = false, isPending = false) => {
|
||||
try {
|
||||
const response = await fetch('/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
is_admin: isAdmin,
|
||||
is_pending: isPending
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Registration failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success || !data.data) {
|
||||
throw new Error(data.message || 'Registration failed');
|
||||
}
|
||||
|
||||
// If registration is pending, don't store auth info
|
||||
if (isPending) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Store authentication information
|
||||
token.value = data.data.token;
|
||||
user.value = data.data.user;
|
||||
|
||||
localStorage.setItem('token', data.data.token);
|
||||
localStorage.setItem('user', JSON.stringify(data.data.user));
|
||||
|
||||
// By default, consider a new registration needs onboarding
|
||||
isFirstLogin.value = true;
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Logout function
|
||||
const logout = async () => {
|
||||
try {
|
||||
if (token.value) {
|
||||
await fetch('/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': token.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
} finally {
|
||||
// Clean up local authentication data
|
||||
token.value = null;
|
||||
user.value = null;
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
};
|
||||
|
||||
// Check if user is authenticated
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value);
|
||||
|
||||
// Check if user is admin
|
||||
const isAdmin = computed(() => isAuthenticated.value && user.value?.is_admin);
|
||||
|
||||
// Get token for authenticated requests
|
||||
const getAuthHeader = () => {
|
||||
return token.value ? { Authorization: token.value } : {};
|
||||
};
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
isFirstLogin,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
getAuthHeader,
|
||||
};
|
||||
};
|
||||
69
web/index.html
Normal file
69
web/index.html
Normal file
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Github-Ntfy Add a Repo</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="script.js" defer></script>
|
||||
</head>
|
||||
<body class="bg-[#1b2124] text-gray-200">
|
||||
<header class="text-center py-8 bg-[#23453d] shadow-lg">
|
||||
<h1 class="text-5xl font-bold tracking-wide text-white">Github-Ntfy</h1>
|
||||
</header>
|
||||
|
||||
<main class="flex flex-wrap justify-center gap-8 py-12">
|
||||
<!-- Github Repo Section -->
|
||||
<section class="bg-[#23453d] rounded-lg shadow-lg p-6 w-full max-w-lg">
|
||||
<h2 class="text-2xl font-semibold mb-4">Add a Github Repo</h2>
|
||||
<form id="addRepoForm" class="space-y-6">
|
||||
<div>
|
||||
<label for="repo" class="block text-sm font-medium">Name of the Github Repo</label>
|
||||
<div class="mt-2 flex items-center border rounded-md bg-gray-700">
|
||||
<span class="px-3 text-gray-400">github.com/</span>
|
||||
<input type="text" name="repo" id="repo" autocomplete="repo" class="flex-1 py-2 px-3 bg-transparent focus:outline-none" placeholder="BreizhHardware/ntfy_alerts">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-4">
|
||||
<button type="button" class="px-4 py-2 text-gray-400 hover:text-white">Cancel</button>
|
||||
<button type="submit" class="px-4 py-2 bg-green-700 hover:bg-green-600 text-white font-semibold rounded-md">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-8">
|
||||
<h3 class="text-lg font-semibold mb-2">Watched Github Repositories</h3>
|
||||
<ul id="watchedReposList" class="space-y-2">
|
||||
<!-- Dynamically populated with JavaScript -->
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Docker Repo Section -->
|
||||
<section class="bg-[#23453d] rounded-lg shadow-lg p-6 w-full max-w-lg">
|
||||
<h2 class="text-2xl font-semibold mb-4">Add a Docker Repo</h2>
|
||||
<form id="addDockerRepoForm" class="space-y-6">
|
||||
<div>
|
||||
<label for="dockerRepo" class="block text-sm font-medium">Name of the Docker Repo</label>
|
||||
<div class="mt-2 flex items-center border rounded-md bg-gray-700">
|
||||
<span class="px-3 text-gray-400">hub.docker.com/r/</span>
|
||||
<input type="text" name="dockerRepo" id="dockerRepo" autocomplete="dockerRepo" class="flex-1 py-2 px-3 bg-transparent focus:outline-none" placeholder="breizhhardware/github-ntfy">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-4">
|
||||
<button type="button" class="px-4 py-2 text-gray-400 hover:text-white">Cancel</button>
|
||||
<button type="submit" class="px-4 py-2 bg-green-700 hover:bg-green-600 text-white font-semibold rounded-md">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-8">
|
||||
<h3 class="text-lg font-semibold mb-2">Watched Docker Repositories</h3>
|
||||
<ul id="watchedDockerReposList" class="space-y-2">
|
||||
<!-- Dynamically populated with JavaScript -->
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="text-center py-6 bg-[#23453d]">
|
||||
<p class="text-sm">I know this web interface is simple, but I'm improving!</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,22 +0,0 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-05-15',
|
||||
devtools: { enabled: true },
|
||||
modules: [
|
||||
'@nuxt/ui'
|
||||
],
|
||||
ui: {
|
||||
global: true,
|
||||
icons: ['heroicons']
|
||||
},
|
||||
css: ['~/assets/css/main.css'],
|
||||
postcss: {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
'~/plugins/auth.js'
|
||||
]
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/icon": "1.14.0",
|
||||
"@nuxt/ui": "3.1.3",
|
||||
"marked": "^15.0.12",
|
||||
"nuxt": "^3.17.5",
|
||||
"typescript": "^5.8.3",
|
||||
"vue": "^3.5.16",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/tailwindcss": "7.0.0-beta.0",
|
||||
"@tailwindcss/postcss": "^4.1.10",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.10"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Section des dernières mises à jour -->
|
||||
<LatestUpdates />
|
||||
|
||||
<!-- Section des dépôts GitHub et Docker -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<GithubRepoSection />
|
||||
<DockerRepoSection />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,91 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center min-h-screen bg-gray-900">
|
||||
<div class="w-full max-w-md p-8 space-y-8 bg-gray-800 rounded-lg shadow-lg">
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl font-bold text-white">Login</h1>
|
||||
<p class="mt-2 text-sm text-gray-400">Sign in to manage your notifications</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="mt-8 space-y-6">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-400">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
required
|
||||
class="block w-full px-3 py-2 mt-1 text-white placeholder-gray-500 bg-gray-700 border border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-400">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
required
|
||||
class="block w-full px-3 py-2 mt-1 text-white placeholder-gray-500 bg-gray-700 border border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="p-3 text-sm text-red-500 bg-red-100 rounded-md">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<UButton
|
||||
type="submit"
|
||||
color="primary"
|
||||
block
|
||||
:loading="loading"
|
||||
>
|
||||
Login
|
||||
</UButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<p class="text-sm text-gray-400">
|
||||
First time?
|
||||
<NuxtLink to="/onboarding" class="font-medium text-indigo-400 hover:text-indigo-300">
|
||||
Setup your application
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
const error = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
async function handleLogin() {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
|
||||
await auth.login(form.username, form.password);
|
||||
|
||||
// Redirect to main page or configuration page if needed
|
||||
if (auth.isFirstLogin.value) {
|
||||
router.push('/onboarding');
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message || 'An error occurred during login';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,489 +0,0 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-900 p-6">
|
||||
<div class="max-w-3xl mx-auto bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
||||
<div class="p-6 border-b border-gray-700">
|
||||
<h1 class="text-2xl font-bold text-white">Application Setup</h1>
|
||||
<p class="mt-2 text-gray-400">Configure your application and create an administrator account</p>
|
||||
</div>
|
||||
|
||||
<UStepper v-model="step" :items="steps" class="p-6">
|
||||
<template #item="{ item }">
|
||||
<h2 class="text-lg font-medium">{{ item.title }}</h2>
|
||||
<p class="text-sm text-gray-400">{{ item.description }}</p>
|
||||
</template>
|
||||
</UStepper>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Step 1: Create Administrator Account -->
|
||||
<div v-if="step === 0" class="space-y-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-white mb-4">Create Administrator Account</h3>
|
||||
<p class="text-sm text-gray-400 mb-6">This account will have full access to manage the application</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-400">Username</label>
|
||||
<UInput
|
||||
id="username"
|
||||
v-model="adminUser.username"
|
||||
placeholder="admin"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-400">Password</label>
|
||||
<UInput
|
||||
id="password"
|
||||
v-model="adminUser.password"
|
||||
type="password"
|
||||
placeholder="********"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-medium text-gray-400">Confirm Password</label>
|
||||
<UInput
|
||||
id="confirmPassword"
|
||||
v-model="adminUser.confirmPassword"
|
||||
type="password"
|
||||
placeholder="********"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Main notification service -->
|
||||
<div v-if="step === 1" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Main notification service</label>
|
||||
<USelect
|
||||
v-model="selectedService"
|
||||
:items="notificationServices"
|
||||
placeholder="Select a notification service"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- NTFY Configuration -->
|
||||
<div v-if="selectedService === 'ntfy'" class="space-y-4">
|
||||
<div>
|
||||
<label for="ntfy_url" class="block text-sm font-medium text-gray-400">NTFY URL</label>
|
||||
<UInput
|
||||
id="ntfy_url"
|
||||
v-model="settings.ntfy_url"
|
||||
placeholder="https://ntfy.sh/your-topic"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ntfy_username" class="block text-sm font-medium text-gray-400">NTFY Username</label>
|
||||
<UInput
|
||||
id="ntfy_username"
|
||||
v-model="settings.ntfy_username"
|
||||
placeholder="username"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ntfy_password" class="block text-sm font-medium text-gray-400">NTFY Password</label>
|
||||
<UInput
|
||||
id="ntfy_password"
|
||||
v-model="settings.ntfy_password"
|
||||
type="password"
|
||||
placeholder="********"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Username and password will be used to generate the auth.txt file
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discord Configuration -->
|
||||
<div v-if="selectedService === 'discord'" class="space-y-4">
|
||||
<div>
|
||||
<label for="discord_webhook" class="block text-sm font-medium text-gray-400">Discord Webhook URL</label>
|
||||
<UInput
|
||||
id="discord_webhook"
|
||||
v-model="settings.discord_webhook_url"
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slack Configuration -->
|
||||
<div v-if="selectedService === 'slack'" class="space-y-4">
|
||||
<div>
|
||||
<label for="slack_webhook" class="block text-sm font-medium text-gray-400">Slack Webhook URL</label>
|
||||
<UInput
|
||||
id="slack_webhook"
|
||||
v-model="settings.slack_webhook_url"
|
||||
placeholder="https://hooks.slack.com/services/..."
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gotify Configuration -->
|
||||
<div v-if="selectedService === 'gotify'" class="space-y-4">
|
||||
<div>
|
||||
<label for="gotify_url" class="block text-sm font-medium text-gray-400">Gotify URL</label>
|
||||
<UInput
|
||||
id="gotify_url"
|
||||
v-model="settings.gotify_url"
|
||||
placeholder="https://gotify.example.com"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="gotify_token" class="block text-sm font-medium text-gray-400">Gotify Token</label>
|
||||
<UInput
|
||||
id="gotify_token"
|
||||
v-model="settings.gotify_token"
|
||||
placeholder="Axxxxxxxxx.xxxxx"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: GitHub Settings -->
|
||||
<div v-if="step === 2" class="space-y-6">
|
||||
<div>
|
||||
<label for="github_token" class="block text-sm font-medium text-gray-400">GitHub Token (optional)</label>
|
||||
<UInput
|
||||
id="github_token"
|
||||
v-model="settings.github_token"
|
||||
placeholder="ghp_xxxxxxxxxxxxxxxx"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
A GitHub token helps avoid API rate limits for private repositories
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Docker Hub Settings -->
|
||||
<div v-if="step === 3" class="space-y-6">
|
||||
<div>
|
||||
<label for="docker_username" class="block text-sm font-medium text-gray-400">Docker Hub Username (optional)</label>
|
||||
<UInput
|
||||
id="docker_username"
|
||||
v-model="settings.docker_username"
|
||||
placeholder="username"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="docker_password" class="block text-sm font-medium text-gray-400">Docker Hub Password (optionnel)</label>
|
||||
<UInput
|
||||
id="docker_password"
|
||||
v-model="settings.docker_password"
|
||||
type="password"
|
||||
placeholder="********"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Docker Hub credentials allow access to private images
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Advanced Settings -->
|
||||
<div v-if="step === 4" class="space-y-6">
|
||||
<div>
|
||||
<label for="check_interval" class="block text-sm font-medium text-gray-400">Check Interval (seconds)</label>
|
||||
<UInput
|
||||
id="check_interval"
|
||||
v-model="settings.check_interval"
|
||||
type="number"
|
||||
min="60"
|
||||
placeholder="3600"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Default interval is 3600 seconds (1 hour)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mt-6 p-3 text-sm text-red-500 bg-red-100 rounded-md">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-8">
|
||||
<UButton
|
||||
v-if="step > 0"
|
||||
@click="step--"
|
||||
color="gray"
|
||||
>
|
||||
Previous
|
||||
</UButton>
|
||||
<div v-else></div>
|
||||
|
||||
<UButton
|
||||
v-if="step < steps.length - 1"
|
||||
@click="nextStep"
|
||||
color="primary"
|
||||
>
|
||||
Next
|
||||
</UButton>
|
||||
<UButton
|
||||
v-else
|
||||
@click="saveSettings"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
>
|
||||
Complete Setup
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Check if admin exists and redirect accordingly
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Check if admin exists
|
||||
const response = await fetch('/is_configured');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const adminExists = data.data && data.data.admin_exists;
|
||||
|
||||
// If admin exists, redirect to login or dashboard
|
||||
// This ensures onboarding can only be done once
|
||||
if (adminExists) {
|
||||
if (auth.isAuthenticated.value) {
|
||||
router.push('/');
|
||||
} else {
|
||||
router.push('/login');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only load existing settings if we're continuing with onboarding
|
||||
// (only happens when no admin exists yet)
|
||||
await loadExistingSettings();
|
||||
} catch (err) {
|
||||
console.error('Error checking configuration:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Admin user creation data
|
||||
const adminUser = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
// Onboarding steps
|
||||
const steps = [
|
||||
{ title: 'Create Admin', description: 'Create your administrator account' },
|
||||
{ title: 'Notification Service', description: 'Choose your main notification service' },
|
||||
{ title: 'GitHub Settings', description: 'Configure options for GitHub' },
|
||||
{ title: 'Docker Hub Settings', description: 'Configure options for Docker Hub' },
|
||||
{ title: 'Advanced Settings', description: 'Configure additional options' }
|
||||
];
|
||||
|
||||
const step = ref(0);
|
||||
const selectedService = ref(null);
|
||||
const error = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
// List of available notification services
|
||||
const notificationServices = [
|
||||
{ label: 'NTFY', value: 'ntfy' },
|
||||
{ label: 'Discord', value: 'discord' },
|
||||
{ label: 'Slack', value: 'slack' },
|
||||
{ label: 'Gotify', value: 'gotify' }
|
||||
];
|
||||
|
||||
// Application settings
|
||||
const settings = reactive({
|
||||
ntfy_url: '',
|
||||
ntfy_username: '',
|
||||
ntfy_password: '',
|
||||
github_token: '',
|
||||
docker_username: '',
|
||||
docker_password: '',
|
||||
gotify_url: '',
|
||||
gotify_token: '',
|
||||
discord_webhook_url: '',
|
||||
slack_webhook_url: '',
|
||||
check_interval: 3600
|
||||
});
|
||||
|
||||
// Function to proceed to next step
|
||||
async function nextStep() {
|
||||
// Validate current step
|
||||
if (step.value === 0) {
|
||||
// Validate admin user creation
|
||||
if (!adminUser.username) {
|
||||
error.value = 'Please enter a username';
|
||||
return;
|
||||
}
|
||||
if (!adminUser.password) {
|
||||
error.value = 'Please enter a password';
|
||||
return;
|
||||
}
|
||||
if (adminUser.password !== adminUser.confirmPassword) {
|
||||
error.value = 'Passwords do not match';
|
||||
return;
|
||||
}
|
||||
|
||||
// Create admin user
|
||||
try {
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
|
||||
// Register admin user
|
||||
await auth.register(adminUser.username, adminUser.password, true);
|
||||
|
||||
// Continue to next step
|
||||
loading.value = false;
|
||||
step.value++;
|
||||
return;
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Error creating admin user';
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (step.value === 1) {
|
||||
if (!selectedService.value) {
|
||||
error.value = 'Please select a notification service';
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate selected service
|
||||
if (selectedService.value === 'ntfy' && !settings.ntfy_url) {
|
||||
error.value = 'Please enter the NTFY URL';
|
||||
return;
|
||||
} else if (selectedService.value === 'ntfy' && (!settings.ntfy_username || !settings.ntfy_password)) {
|
||||
error.value = 'Please enter both NTFY username and password';
|
||||
return;
|
||||
} else if (selectedService.value === 'discord' && !settings.discord_webhook_url) {
|
||||
error.value = 'Please enter the Discord webhook URL';
|
||||
return;
|
||||
} else if (selectedService.value === 'slack' && !settings.slack_webhook_url) {
|
||||
error.value = 'Please enter the Slack webhook URL';
|
||||
return;
|
||||
} else if (selectedService.value === 'gotify' && (!settings.gotify_url || !settings.gotify_token)) {
|
||||
error.value = 'Please enter both Gotify URL and token';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset error and proceed to next step
|
||||
error.value = '';
|
||||
step.value++;
|
||||
}
|
||||
|
||||
// Function to save settings
|
||||
async function saveSettings() {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
// Prepare settings
|
||||
const now = new Date().toISOString();
|
||||
const settingsData = {
|
||||
...settings,
|
||||
last_updated: now
|
||||
};
|
||||
|
||||
// Format NTFY auth if credentials are provided
|
||||
if (selectedService.value === 'ntfy' && settings.ntfy_username && settings.ntfy_password) {
|
||||
// Create auth string in the format expected by the backend
|
||||
const authString = `${settings.ntfy_username}:${settings.ntfy_password}`;
|
||||
settingsData.auth = authString;
|
||||
}
|
||||
|
||||
// Send settings to server
|
||||
const response = await fetch('/settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': auth.token.value
|
||||
},
|
||||
body: JSON.stringify(settingsData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.message || 'Error saving settings');
|
||||
}
|
||||
|
||||
// Redirect to main page
|
||||
router.push('/');
|
||||
} catch (err) {
|
||||
error.value = err.message || 'An error occurred while saving settings';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to load existing settings
|
||||
async function loadExistingSettings() {
|
||||
try {
|
||||
if (!auth.isAuthenticated.value) return;
|
||||
|
||||
const response = await fetch('/settings', {
|
||||
headers: {
|
||||
'Authorization': auth.token.value
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.data) {
|
||||
// Populate settings with existing values
|
||||
const existingSettings = data.data;
|
||||
|
||||
// Update notification service selection
|
||||
if (existingSettings.ntfy_url) {
|
||||
selectedService.value = 'ntfy';
|
||||
settings.ntfy_url = existingSettings.ntfy_url;
|
||||
|
||||
// Parse auth string if it exists (format: username:password)
|
||||
if (existingSettings.auth) {
|
||||
const authParts = existingSettings.auth.split(':');
|
||||
if (authParts.length === 2) {
|
||||
settings.ntfy_username = authParts[0];
|
||||
settings.ntfy_password = authParts[1];
|
||||
}
|
||||
}
|
||||
} else if (existingSettings.discord_webhook_url) {
|
||||
selectedService.value = 'discord';
|
||||
settings.discord_webhook_url = existingSettings.discord_webhook_url;
|
||||
} else if (existingSettings.slack_webhook_url) {
|
||||
selectedService.value = 'slack';
|
||||
settings.slack_webhook_url = existingSettings.slack_webhook_url;
|
||||
} else if (existingSettings.gotify_url) {
|
||||
selectedService.value = 'gotify';
|
||||
settings.gotify_url = existingSettings.gotify_url;
|
||||
settings.gotify_token = existingSettings.gotify_token;
|
||||
}
|
||||
|
||||
// Update other settings
|
||||
settings.github_token = existingSettings.github_token || '';
|
||||
settings.docker_username = existingSettings.docker_username || '';
|
||||
settings.docker_password = existingSettings.docker_password || '';
|
||||
settings.check_interval = existingSettings.check_interval || 3600;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading existing settings:', err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,294 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppHeader />
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-2xl font-bold text-white mb-8">Settings</h1>
|
||||
|
||||
<UCard class="mb-8">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold">Notification Services</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- NTFY -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-2">NTFY</h3>
|
||||
<div class="space-y-2">
|
||||
<UInput
|
||||
v-model="settings.ntfy_url"
|
||||
label="NTFY URL"
|
||||
placeholder="https://ntfy.sh/your-topic"
|
||||
class="w-full"
|
||||
/>
|
||||
<UInput
|
||||
v-model="settings.ntfy_username"
|
||||
label="NTFY Username"
|
||||
placeholder="username"
|
||||
class="w-full"
|
||||
/>
|
||||
<UInput
|
||||
v-model="settings.ntfy_password"
|
||||
label="NTFY Password"
|
||||
type="password"
|
||||
placeholder="********"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Username and password will be used to generate the auth.txt file
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discord -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-2">Discord</h3>
|
||||
<UInput
|
||||
v-model="settings.discord_webhook_url"
|
||||
label="Discord Webhook URL"
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Slack -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-2">Slack</h3>
|
||||
<UInput
|
||||
v-model="settings.slack_webhook_url"
|
||||
label="Slack Webhook URL"
|
||||
placeholder="https://hooks.slack.com/services/..."
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Gotify -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-2">Gotify</h3>
|
||||
<div class="space-y-2">
|
||||
<UInput
|
||||
v-model="settings.gotify_url"
|
||||
label="Gotify URL"
|
||||
placeholder="https://gotify.example.com"
|
||||
class="w-full"
|
||||
/>
|
||||
<UInput
|
||||
v-model="settings.gotify_token"
|
||||
label="Gotify Token"
|
||||
placeholder="Axxxxxxxxx.xxxxx"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="mb-8">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold">GitHub</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<UInput
|
||||
v-model="settings.github_token"
|
||||
label="GitHub Token (optional)"
|
||||
placeholder="ghp_xxxxxxxxxxxxxxxx"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
A GitHub token helps avoid API rate limits for private repositories
|
||||
</p>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="mb-8">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold">Docker Hub</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<UInput
|
||||
v-model="settings.docker_username"
|
||||
label="Docker Hub Username (optional)"
|
||||
placeholder="username"
|
||||
class="w-full"
|
||||
/>
|
||||
<UInput
|
||||
v-model="settings.docker_password"
|
||||
label="Docker Hub Password (optional)"
|
||||
type="password"
|
||||
placeholder="********"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Docker Hub credentials allow access to private images
|
||||
</p>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="mb-8">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold">Advanced Settings</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<UInput
|
||||
v-model="settings.check_interval"
|
||||
label="Check Interval (seconds)"
|
||||
type="number"
|
||||
min="60"
|
||||
placeholder="3600"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Default interval is 3600 seconds (1 hour)
|
||||
</p>
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<div v-if="error" class="p-3 mb-6 text-sm text-red-500 bg-red-100 rounded-md">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-if="success" class="p-3 mb-6 text-sm text-green-500 bg-green-100 rounded-md">
|
||||
{{ success }}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<UButton
|
||||
@click="saveSettings"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
>
|
||||
Save Changes
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AppFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const auth = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
// Check if user is authenticated
|
||||
onMounted(async () => {
|
||||
if (!auth.isAuthenticated.value) {
|
||||
return router.push('/login');
|
||||
}
|
||||
|
||||
// Load current settings
|
||||
await loadSettings();
|
||||
});
|
||||
|
||||
const settings = reactive({
|
||||
ntfy_url: '',
|
||||
ntfy_username: '',
|
||||
ntfy_password: '',
|
||||
github_token: '',
|
||||
docker_username: '',
|
||||
docker_password: '',
|
||||
gotify_url: '',
|
||||
gotify_token: '',
|
||||
discord_webhook_url: '',
|
||||
slack_webhook_url: '',
|
||||
check_interval: 3600
|
||||
});
|
||||
|
||||
const error = ref('');
|
||||
const success = ref('');
|
||||
const loading = ref(false);
|
||||
|
||||
// Load current settings
|
||||
async function loadSettings() {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const response = await fetch('/settings', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': auth.token.value
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.message || 'Error loading settings');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
// Update settings with loaded values
|
||||
Object.assign(settings, data.data);
|
||||
|
||||
// Parse NTFY auth string if it exists
|
||||
if (data.data.auth) {
|
||||
const authParts = data.data.auth.split(':');
|
||||
if (authParts.length === 2) {
|
||||
settings.ntfy_username = authParts[0];
|
||||
settings.ntfy_password = authParts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message || 'An error occurred while loading settings';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to save settings
|
||||
async function saveSettings() {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
|
||||
// Prepare settings
|
||||
const now = new Date().toISOString();
|
||||
const settingsData = {
|
||||
...settings,
|
||||
last_updated: now
|
||||
};
|
||||
|
||||
// Format NTFY auth if credentials are provided
|
||||
if (settings.ntfy_url && settings.ntfy_username && settings.ntfy_password) {
|
||||
// Create auth string in the format expected by the backend
|
||||
const authString = `${settings.ntfy_username}:${settings.ntfy_password}`;
|
||||
settingsData.auth = authString;
|
||||
}
|
||||
|
||||
// Send settings to server
|
||||
const response = await fetch('/settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': auth.token.value
|
||||
},
|
||||
body: JSON.stringify(settingsData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.message || 'Error saving settings');
|
||||
}
|
||||
|
||||
success.value = 'Settings updated successfully';
|
||||
} catch (err) {
|
||||
error.value = err.message || 'An error occurred while saving settings';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,24 +0,0 @@
|
||||
// Authentication verification plugin
|
||||
export default defineNuxtPlugin(() => {
|
||||
console.log('Authentication plugin loaded');
|
||||
|
||||
addRouteMiddleware('auth', (to) => {
|
||||
console.log('Auth middleware executed for route:', to.path);
|
||||
|
||||
if (to.path === '/login' || to.path === '/onboarding') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.client) {
|
||||
const token = localStorage.getItem('token');
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
console.log('Authentication check:', !!token, !!user);
|
||||
|
||||
if (!token || !user) {
|
||||
console.log('Redirecting to /login');
|
||||
return navigateTo('/login');
|
||||
}
|
||||
}
|
||||
}, { global: true });
|
||||
});
|
||||
8486
web/pnpm-lock.yaml
generated
8486
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,2 +0,0 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
158
web/script.js
Normal file
158
web/script.js
Normal file
@@ -0,0 +1,158 @@
|
||||
document.getElementById('addRepoForm').addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
let repoName = document.getElementById('repo').value;
|
||||
fetch('/app_repo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({repo: repoName})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
// Si la requête s'est bien déroulée, actualiser la liste des dépôts surveillés
|
||||
refreshWatchedRepos();
|
||||
} else {
|
||||
throw new Error('Erreur lors de l\'ajout du dépôt');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('addDockerRepoForm').addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
let repoName = document.getElementById('dockerRepo').value;
|
||||
fetch('/app_docker_repo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({repo: repoName})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
// Si la requête s'est bien déroulée, actualiser la liste des dépôts surveillés
|
||||
refreshWatchedRepos();
|
||||
} else {
|
||||
throw new Error('Erreur lors de l\'ajout du dépôt');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
|
||||
function refreshWatchedRepos() {
|
||||
fetch('/watched_repos')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const watchedReposList = document.getElementById('watchedReposList');
|
||||
// Vider la liste actuelle
|
||||
watchedReposList.innerHTML = '';
|
||||
// Ajouter chaque dépôt surveillé à la liste
|
||||
data.forEach(repo => {
|
||||
const listItem = document.createElement('li');
|
||||
const repoName = document.createElement('span');
|
||||
repoName.textContent = repo;
|
||||
repoName.className = 'repo-name';
|
||||
listItem.appendChild(repoName);
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.textContent = ' X';
|
||||
deleteButton.className = 'delete-btn text-red-500 ml-2';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
// Remove the repo from the watched repos
|
||||
// This is a placeholder. Replace it with your actual code to remove the repo from the watched repos.
|
||||
removeRepoFromWatchedRepos(repo);
|
||||
|
||||
// Remove the repo from the DOM
|
||||
listItem.remove();
|
||||
});
|
||||
listItem.appendChild(deleteButton);
|
||||
|
||||
watchedReposList.appendChild(listItem);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
|
||||
fetch('/watched_docker_repos')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const watchedDockerReposList = document.getElementById('watchedDockerReposList');
|
||||
// Vider la liste actuelle
|
||||
watchedDockerReposList.innerHTML = '';
|
||||
// Ajouter chaque dépôt surveillé à la liste
|
||||
data.forEach(repo => {
|
||||
const listItem = document.createElement('li');
|
||||
const repoName = document.createElement('span');
|
||||
repoName.textContent = repo;
|
||||
repoName.className = 'repo-name';
|
||||
listItem.appendChild(repoName);
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.textContent = ' X';
|
||||
deleteButton.className = 'delete-btn text-red-500 ml-2';
|
||||
deleteButton.addEventListener('click', () => {
|
||||
// Remove the repo from the watched repos
|
||||
// This is a placeholder. Replace it with your actual code to remove the repo from the watched repos.
|
||||
removeDockerRepoFromWatchedRepos(repo);
|
||||
|
||||
// Remove the repo from the DOM
|
||||
listItem.remove();
|
||||
});
|
||||
listItem.appendChild(deleteButton);
|
||||
|
||||
watchedDockerReposList.appendChild(listItem);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function removeRepoFromWatchedRepos(repo) {
|
||||
fetch('/delete_repo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({repo: repo})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur lors de la suppression du dépôt');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function removeDockerRepoFromWatchedRepos(repo) {
|
||||
fetch('/delete_docker_repo', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({repo: repo})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur lors de la suppression du dépôt');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Appeler la fonction pour charger les dépôts surveillés au chargement de la page
|
||||
refreshWatchedRepos();
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./components/**/*.{js,vue,ts}",
|
||||
"./layouts/**/*.vue",
|
||||
"./pages/**/*.vue",
|
||||
"./plugins/**/*.{js,ts}",
|
||||
"./app.vue",
|
||||
"./node_modules/@nuxt/ui/dist/**/*.{mjs,js,vue}"
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'emerald-950': '#23453d'
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
Reference in New Issue
Block a user