This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
docker/
|
||||||
|
.dockerignore
|
||||||
|
CNAME
|
||||||
|
Dockerfile
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
240
.github/workflows/release_build.yml
vendored
Normal file
240
.github/workflows/release_build.yml
vendored
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
name: Build, Push, Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '28 5 * * *'
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Sync Repo"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Build & Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 📥 Checkout code with full history and tags
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check if any tags exist
|
||||||
|
id: check_tags_exist
|
||||||
|
run: |
|
||||||
|
git fetch --tags
|
||||||
|
TAG_COUNT=$(git tag | wc -l)
|
||||||
|
if [ "$TAG_COUNT" -eq 0 ]; then
|
||||||
|
echo "has_tags=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "latest_tag=v0.0.0" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "has_tags=true" >> "$GITHUB_OUTPUT"
|
||||||
|
LATEST_TAG=$(git describe --tags --abbrev=0)
|
||||||
|
echo "latest_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check if meaningful commits exist since latest tag
|
||||||
|
id: check_commits
|
||||||
|
run: |
|
||||||
|
if [ "${{ steps.check_tags_exist.outputs.has_tags }}" = "false" ]; then
|
||||||
|
# No tags exist, so we should create first release
|
||||||
|
echo "commit_count=1" >> "$GITHUB_OUTPUT"
|
||||||
|
CHANGED_FILES=$(git ls-files | grep -v '^manifest.json$' || true)
|
||||||
|
if [ -n "$CHANGED_FILES" ]; then
|
||||||
|
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
printf '%s\n' "$CHANGED_FILES" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "changed_files=Initial release" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
LATEST_TAG="${{ steps.check_tags_exist.outputs.latest_tag }}"
|
||||||
|
CHANGED_FILES="$(git diff --name-only "${LATEST_TAG}..HEAD" | grep -v '^manifest.json$' || true)"
|
||||||
|
if [ -n "$CHANGED_FILES" ]; then
|
||||||
|
echo "commit_count=1" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
printf '%s\n' "$CHANGED_FILES" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "commit_count=0" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Get latest release tag (from GitHub API)
|
||||||
|
id: get_latest_release
|
||||||
|
run: |
|
||||||
|
LATEST_RELEASE_TAG=$(curl -sL -H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/latest" | jq -r .tag_name)
|
||||||
|
if [ -z "$LATEST_RELEASE_TAG" ] || [ "$LATEST_RELEASE_TAG" = "null" ]; then
|
||||||
|
LATEST_RELEASE_TAG="v1.0.0"
|
||||||
|
fi
|
||||||
|
echo "latest_release_tag=$LATEST_RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "latest_release_version=${LATEST_RELEASE_TAG#v}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Sync manifest.json to last release version if behind (only when no meaningful commits)
|
||||||
|
# -------------------------------
|
||||||
|
- name: 🛠 Ensure manifest.json matches latest release version
|
||||||
|
if: steps.check_commits.outputs.commit_count == '0'
|
||||||
|
run: |
|
||||||
|
if [ -f manifest.json ]; then
|
||||||
|
MANIFEST_VERSION=$(jq -r '.version // empty' manifest.json)
|
||||||
|
else
|
||||||
|
MANIFEST_VERSION=""
|
||||||
|
fi
|
||||||
|
LATEST_RELEASE_VERSION="${{ steps.get_latest_release.outputs.latest_release_version }}"
|
||||||
|
PYTHON_CODE="from packaging import version; \
|
||||||
|
print(version.parse('$LATEST_RELEASE_VERSION') > version.parse('$MANIFEST_VERSION') if '$MANIFEST_VERSION' else True)"
|
||||||
|
NEED_UPDATE=$(python3 -c "$PYTHON_CODE")
|
||||||
|
if [ "$NEED_UPDATE" = "True" ]; then
|
||||||
|
echo "Updating manifest.json to version $LATEST_RELEASE_VERSION (sync with release)"
|
||||||
|
jq --arg v "$LATEST_RELEASE_VERSION" '.version = $v' manifest.json > tmp.json && mv tmp.json manifest.json
|
||||||
|
git config user.name "github-actions"
|
||||||
|
git config user.email "github-actions@github.com"
|
||||||
|
git add manifest.json
|
||||||
|
git commit -m "Sync manifest.json to release $LATEST_RELEASE_VERSION [🔄]" || echo "Nothing to commit"
|
||||||
|
git push origin main || true
|
||||||
|
else
|
||||||
|
echo "Manifest.json is already up-to-date with the latest release."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------------------------------
|
||||||
|
# Continue normal workflow if commits exist
|
||||||
|
# -------------------------------
|
||||||
|
- name: 📃 Get list of changed files (Markdown bullet list)
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0'
|
||||||
|
id: changed_files
|
||||||
|
run: |
|
||||||
|
BULLET_LIST="$(printf '%s\n' "${{ steps.check_commits.outputs.changed_files }}" | sed 's/^/- /')"
|
||||||
|
echo "CHANGED<<EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
printf '%s\n' "$BULLET_LIST" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||||
|
COUNT="$(printf '%s\n' "${{ steps.check_commits.outputs.changed_files }}" | wc -l)"
|
||||||
|
echo "COUNT=$COUNT" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Get manifest version
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0'
|
||||||
|
id: get_manifest_version
|
||||||
|
run: |
|
||||||
|
if [ -f manifest.json ]; then
|
||||||
|
MANIFEST_VERSION=$(jq -r '.version // empty' manifest.json)
|
||||||
|
if [ -z "$MANIFEST_VERSION" ] || [ "$MANIFEST_VERSION" = "null" ]; then
|
||||||
|
MANIFEST_VERSION="1.0.0"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
MANIFEST_VERSION="1.0.0"
|
||||||
|
fi
|
||||||
|
echo "manifest_version=$MANIFEST_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Pick base version
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0'
|
||||||
|
id: pick_base_version
|
||||||
|
run: |
|
||||||
|
LATEST_RELEASE="${{ steps.get_latest_release.outputs.latest_release_version }}"
|
||||||
|
MANIFEST="${{ steps.get_manifest_version.outputs.manifest_version }}"
|
||||||
|
BASE_VERSION=$(python3 -c "from packaging import version; \
|
||||||
|
print(str(max(version.parse('$LATEST_RELEASE'), version.parse('$MANIFEST'))))")
|
||||||
|
echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: 🔢 Determine version
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0'
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
BASE_VERSION="${{ steps.pick_base_version.outputs.base_version }}"
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"
|
||||||
|
COUNT="${{ steps.changed_files.outputs.COUNT }}"
|
||||||
|
if [ "$COUNT" -ge 5 ]; then
|
||||||
|
MAJOR=$((MAJOR + 1))
|
||||||
|
MINOR=0
|
||||||
|
PATCH=0
|
||||||
|
elif [ "$COUNT" -ge 3 ]; then
|
||||||
|
MINOR=$((MINOR + 1))
|
||||||
|
PATCH=0
|
||||||
|
else
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
fi
|
||||||
|
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
REPO_NAME="$(basename "$GITHUB_REPOSITORY")"
|
||||||
|
ZIP_NAME="${REPO_NAME}-${NEW_VERSION}.zip"
|
||||||
|
echo "VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ZIP_NAME=$ZIP_NAME" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "REPO_NAME=$REPO_NAME" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: 🛠 Update or create manifest.json
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.VERSION }}"
|
||||||
|
AUTHOR="Ivan Carlos"
|
||||||
|
VERSION_FILE="manifest.json"
|
||||||
|
if [ -f "$VERSION_FILE" ]; then
|
||||||
|
jq --arg v "$VERSION" --arg a "$AUTHOR" \
|
||||||
|
'.version = $v | .author = $a' "$VERSION_FILE" > tmp.json && mv tmp.json "$VERSION_FILE"
|
||||||
|
else
|
||||||
|
echo "{ \"version\": \"$VERSION\", \"author\": \"$AUTHOR\" }" > "$VERSION_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 💾 Commit and push updated manifest.json
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0'
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions"
|
||||||
|
git config user.email "github-actions@github.com"
|
||||||
|
git add manifest.json
|
||||||
|
git commit -m "Update manifest version to ${{ steps.version.outputs.VERSION }} [▶️]" || echo "Nothing to commit"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
- name: 📦 Create ZIP package (excluding certain files)
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0'
|
||||||
|
run: |
|
||||||
|
ZIP_NAME="${{ steps.version.outputs.ZIP_NAME }}"
|
||||||
|
zip -r "$ZIP_NAME" . -x ".git/*" ".github/*" "docker/*" ".dockerignore" "CNAME" "Dockerfile" "README.md" "LICENSE"
|
||||||
|
|
||||||
|
- name: 🚀 Create GitHub Release
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0'
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: "v${{ steps.version.outputs.VERSION }}"
|
||||||
|
name: "${{ steps.version.outputs.REPO_NAME }} v${{ steps.version.outputs.VERSION }}"
|
||||||
|
body: |
|
||||||
|
### Changelog
|
||||||
|
Files changed in this release:
|
||||||
|
${{ steps.changed_files.outputs.CHANGED }}
|
||||||
|
files: ${{ steps.version.outputs.ZIP_NAME }}
|
||||||
|
|
||||||
|
# ----- Docker steps -----
|
||||||
|
- name: 🔍 Check if Dockerfile exists
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0'
|
||||||
|
id: dockerfile_check
|
||||||
|
run: |
|
||||||
|
if [ -f Dockerfile ]; then
|
||||||
|
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 🛠 Set up Docker Buildx
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0' && steps.dockerfile_check.outputs.exists == 'true'
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: 🔐 Login to GitHub Container Registry
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0' && steps.dockerfile_check.outputs.exists == 'true'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: 🐳 Build and Push Docker image
|
||||||
|
if: steps.check_commits.outputs.commit_count != '0' && steps.dockerfile_check.outputs.exists == 'true'
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ghcr.io/${{ github.repository }}:latest
|
||||||
42
.github/workflows/sync_repo.yml
vendored
Normal file
42
.github/workflows/sync_repo.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: Sync Repo
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '34 3 * * *' # 03:34 UTC == 00:34 BRT
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
download-aws-sdk:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: write # needed to dispatch another workflow
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download AWS SDK PHAR
|
||||||
|
run: |
|
||||||
|
mkdir -p vendor
|
||||||
|
wget https://github.com/aws/aws-sdk-php/releases/latest/download/aws.phar -O vendor/aws.phar
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
id: commit_step
|
||||||
|
run: |
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@github.com"
|
||||||
|
git add vendor/aws.phar
|
||||||
|
|
||||||
|
# If there are changes, commit & push; set output flag accordingly
|
||||||
|
if git diff --quiet && git diff --staged --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
echo "changes_committed=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
git commit -m "Update AWS SDK PHAR [▶️]"
|
||||||
|
git push origin HEAD:main
|
||||||
|
echo "changes_committed=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
78
.github/workflows/update_readme.yml
vendored
Normal file
78
.github/workflows/update_readme.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
name: Update README
|
||||||
|
|
||||||
|
# Allow GitHub Actions to commit and push changes
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 4 * * *' # Every day at 4 AM UTC
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-readme:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
SOURCE_REPO: ivancarlosti/.github
|
||||||
|
SOURCE_BRANCH: main
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout current repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Checkout source README template
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: ${{ env.SOURCE_REPO }}
|
||||||
|
ref: ${{ env.SOURCE_BRANCH }}
|
||||||
|
path: source_readme
|
||||||
|
|
||||||
|
- name: Update README.md (buttons and footer)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
REPO_NAME="${GITHUB_REPOSITORY##*/}"
|
||||||
|
|
||||||
|
# --- Extract buttons block from source ---
|
||||||
|
BUTTONS=$(awk '/<!-- buttons -->/{flag=1;next}/<!-- endbuttons -->/{flag=0}flag' source_readme/README.md)
|
||||||
|
BUTTONS_UPDATED=$(echo "$BUTTONS" | sed "s/\.github/${REPO_NAME}/g")
|
||||||
|
|
||||||
|
# --- Extract footer block from source (everything from <!-- footer --> onward) ---
|
||||||
|
FOOTER=$(awk '/<!-- footer -->/{flag=1}flag' source_readme/README.md)
|
||||||
|
|
||||||
|
# --- Replace buttons section in README.md ---
|
||||||
|
UPDATED=$(awk -v buttons="$BUTTONS_UPDATED" '
|
||||||
|
BEGIN { skip=0 }
|
||||||
|
/<!-- buttons -->/ {
|
||||||
|
print
|
||||||
|
print buttons
|
||||||
|
skip=1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/<!-- endbuttons -->/ && skip {
|
||||||
|
print
|
||||||
|
skip=0
|
||||||
|
next
|
||||||
|
}
|
||||||
|
!skip { print }
|
||||||
|
' README.md)
|
||||||
|
|
||||||
|
# --- Replace everything after <!-- footer --> with FOOTER ---
|
||||||
|
echo "$UPDATED" | awk -v footer="$FOOTER" '
|
||||||
|
/<!-- footer -->/ {
|
||||||
|
print footer
|
||||||
|
found=1
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
' > README.tmp && mv README.tmp README.md
|
||||||
|
|
||||||
|
- name: Remove source_readme from git index
|
||||||
|
run: git rm --cached -r source_readme || true
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
uses: stefanzweifel/git-auto-commit-action@v5
|
||||||
|
with:
|
||||||
|
file_pattern: README.md
|
||||||
|
commit_message: "Sync README from template [▶️]"
|
||||||
|
branch: ${{ github.ref_name }}
|
||||||
49
Dockerfile
Normal file
49
Dockerfile
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
FROM php:8.4-fpm-alpine
|
||||||
|
|
||||||
|
# Install Nginx and MariaDB client; install PHP extensions (mysqli) and clean up
|
||||||
|
RUN apk add --no-cache --update nginx \
|
||||||
|
&& docker-php-ext-install mysqli \
|
||||||
|
&& rm -rf /var/cache/apk/* /tmp/*
|
||||||
|
|
||||||
|
# Copy your application code
|
||||||
|
COPY . /var/www/html/
|
||||||
|
|
||||||
|
# Create nginx.conf directly in the Docker build
|
||||||
|
RUN printf '%s\n' \
|
||||||
|
'worker_processes auto;' \
|
||||||
|
'' \
|
||||||
|
'events { worker_connections 1024; }' \
|
||||||
|
'' \
|
||||||
|
'http {' \
|
||||||
|
' include mime.types;' \
|
||||||
|
' default_type application/octet-stream;' \
|
||||||
|
'' \
|
||||||
|
' sendfile on;' \
|
||||||
|
'' \
|
||||||
|
' server {' \
|
||||||
|
' listen 80;' \
|
||||||
|
' server_name localhost;' \
|
||||||
|
' root /var/www/html/public;' \
|
||||||
|
'' \
|
||||||
|
' index index.php index.html;' \
|
||||||
|
'' \
|
||||||
|
' location / {' \
|
||||||
|
' try_files $uri $uri/ /index.php?$query_string;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location ~ \.php$ {' \
|
||||||
|
' include fastcgi_params;' \
|
||||||
|
' fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;' \
|
||||||
|
' fastcgi_index index.php;' \
|
||||||
|
' fastcgi_pass 127.0.0.1:9000;' \
|
||||||
|
' }' \
|
||||||
|
' }' \
|
||||||
|
'}' \
|
||||||
|
> /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Make sure Nginx and PHP-FPM can access/serve project files
|
||||||
|
RUN chown -R www-data:www-data /var/www/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD php-fpm -D && nginx -g 'daemon off;'
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Ivan Carlos de Almeida
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
123
README.md
123
README.md
@@ -1,2 +1,123 @@
|
|||||||
# ddnsonroute53
|
# DDNS on Route53
|
||||||
|
Router friendly client to update AWS Route53 entries for Dynamic DNS funtionality
|
||||||
|
|
||||||
|
<!-- buttons -->
|
||||||
|
[](https://github.com/ivancarlosti/ddnsonroute53/stargazers)
|
||||||
|
[](https://github.com/sponsors/ivancarlosti)
|
||||||
|
[](https://github.com/sponsors/ivancarlosti)
|
||||||
|
[](https://github.com/ivancarlosti/ddnsonroute53/pulse)
|
||||||
|
[](https://github.com/ivancarlosti/ddnsonroute53/issues)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://github.com/ivancarlosti/ddnsonroute53/commits)
|
||||||
|
[](https://github.com/ivancarlosti/ddnsonroute53/security)
|
||||||
|
[](https://github.com/ivancarlosti/ddnsonroute53?tab=coc-ov-file)
|
||||||
|
[][sponsor]
|
||||||
|
<!-- endbuttons -->
|
||||||
|
|
||||||
|
## Requirement:
|
||||||
|
|
||||||
|
* [Docker Compose](https://docs.docker.com/engine/install/)
|
||||||
|
* MySQL/MariaDB
|
||||||
|
* Keycloak for SSO
|
||||||
|
|
||||||
|
## Hosting instructions (Docker Compose with PHP + nginx + mysqli):
|
||||||
|
|
||||||
|
* Download `/docker` files on your server, example:
|
||||||
|
```
|
||||||
|
curl -o .env https://raw.githubusercontent.com/ivancarlosti/ddnsonroute53/main/docker/.env
|
||||||
|
curl -o docker-compose.yml https://raw.githubusercontent.com/ivancarlosti/ddnsonroute53/main/docker/docker-compose.yml
|
||||||
|
```
|
||||||
|
* Edit both `.env`, `docker-compose.yml` files
|
||||||
|
* This Docker Compose contains only PHP + nginx + mysqli as webserver, you can use a reverse proxy for SSL, default exposed port is `5666`, an external MySQL/MariaDB is required
|
||||||
|
* Start Docker Compose, example:
|
||||||
|
```
|
||||||
|
docker compose pull && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service setup:
|
||||||
|
|
||||||
|
* Run `setup.php` on browser to set username and password
|
||||||
|
* Run `index.php` on browser to login
|
||||||
|
* Access `Manage AWS Credentials` menu and fill required fields
|
||||||
|
* Access `Manage DDNS Entries` to add, edit and delete DDNS entries
|
||||||
|
* (optional) Access `Manage Users` to create new users, add/edit reCAPTCHA keys
|
||||||
|
* (optional) Access `View All Logs` to check last 30 days of entries created and/or updated
|
||||||
|
|
||||||
|
IAM required policy, remember to update `YOURZONEID` value to related domain zone ID and `subdomain.example.com.` to your domain or subdomain for service usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"route53:ChangeResourceRecordSets",
|
||||||
|
"route53:ListResourceRecordSets"
|
||||||
|
],
|
||||||
|
"Resource": "arn:aws:route53:::hostedzone/YOURZONEID",
|
||||||
|
"Condition": {
|
||||||
|
"ForAllValues:StringLike": {
|
||||||
|
"route53:ChangeResourceRecordSetsNormalizedRecordNames": [
|
||||||
|
"*.subdomain.example.com."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": "route53:GetChange",
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## DDNS cURL update format:
|
||||||
|
|
||||||
|
To simplify the process, `[FQDN]` is also used as login for basic auth purposes.
|
||||||
|
|
||||||
|
Example: `https://[FQDN]:[PASSWORD]@subdomain.example.com/update.php?hostname=[FQDN]&myip=[IP]`
|
||||||
|
|
||||||
|
### Tested DDNS Custom URL:
|
||||||
|
|
||||||
|
TP-Link Omada Update URL:
|
||||||
|
* `https://[USERNAME]:[PASSWORD]@subdomain.example.com/update.php?hostname=[DOMAIN]&myip=[IP]`
|
||||||
|
|
||||||
|
## To Do:
|
||||||
|
|
||||||
|
* HTML beautification
|
||||||
|
* Build releases using Compose to populate AWS SDK dinamically
|
||||||
|
|
||||||
|
## Hosting note:
|
||||||
|
|
||||||
|
Using PHP with the Suhosin patch is not recommended, but is common on some Ubuntu and Debian distributions. To modify `suhosin.ini`, add the following line.
|
||||||
|
|
||||||
|
```
|
||||||
|
suhosin.executor.include.whitelist = phar
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- footer -->
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧑💻 Consulting and technical support
|
||||||
|
* For personal support and queries, please submit a new issue to have it addressed.
|
||||||
|
* For commercial related questions, please [**contact me**][ivancarlos] for consulting costs.
|
||||||
|
|
||||||
|
## 🩷 Project support
|
||||||
|
| If you found this project helpful, consider |
|
||||||
|
| :---: |
|
||||||
|
[**buying me a coffee**][buymeacoffee], [**donate by paypal**][paypal], [**sponsor this project**][sponsor] or just [**leave a star**](../..)⭐
|
||||||
|
|Thanks for your support, it is much appreciated!|
|
||||||
|
|
||||||
|
[cc]: https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-code-of-conduct-to-your-project
|
||||||
|
[contributing]: https://docs.github.com/en/articles/setting-guidelines-for-repository-contributors
|
||||||
|
[security]: https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository
|
||||||
|
[support]: https://docs.github.com/en/articles/adding-support-resources-to-your-project
|
||||||
|
[it]: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
|
||||||
|
[prt]: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository
|
||||||
|
[funding]: https://docs.github.com/en/articles/displaying-a-sponsor-button-in-your-repository
|
||||||
|
[ivancarlos]: https://ivancarlos.it
|
||||||
|
[buymeacoffee]: https://www.buymeacoffee.com/ivancarlos
|
||||||
|
[paypal]: https://icc.gg/donate
|
||||||
|
[sponsor]: https://github.com/sponsors/ivancarlosti
|
||||||
|
|||||||
23
dbconfig.php
Normal file
23
dbconfig.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
// dbconfig.php
|
||||||
|
define('DB_SERVER', getenv('DB_SERVER') ?: 'localhost');
|
||||||
|
define('DB_NAME', getenv('DB_NAME') ?: 'dns');
|
||||||
|
define('DB_USERNAME', getenv('DB_USERNAME') ?: 'dns');
|
||||||
|
define('DB_PASSWORD', getenv('DB_PASSWORD') ?: 'password');
|
||||||
|
|
||||||
|
// Keycloak Configuration
|
||||||
|
define('KEYCLOAK_BASE_URL', getenv('KEYCLOAK_BASE_URL') ?: 'https://keycloak.example.com/auth');
|
||||||
|
define('KEYCLOAK_REALM', getenv('KEYCLOAK_REALM') ?: 'myrealm');
|
||||||
|
define('KEYCLOAK_CLIENT_ID', getenv('KEYCLOAK_CLIENT_ID') ?: 'myclient');
|
||||||
|
define('KEYCLOAK_CLIENT_SECRET', getenv('KEYCLOAK_CLIENT_SECRET') ?: 'mysecret');
|
||||||
|
define('KEYCLOAK_REDIRECT_URI', getenv('KEYCLOAK_REDIRECT_URI') ?: 'http://localhost/index.php');
|
||||||
|
|
||||||
|
|
||||||
|
// Create the connection (EXACTLY as in your original)
|
||||||
|
$link = @new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||||
|
|
||||||
|
// Optional: Add error logging if connection fails
|
||||||
|
if ($link->connect_error) {
|
||||||
|
error_log("Database connection failed: " . $link->connect_error);
|
||||||
|
}
|
||||||
|
?>
|
||||||
12
docker/.env
Normal file
12
docker/.env
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Database Settings
|
||||||
|
DB_SERVER=host.docker.internal
|
||||||
|
DB_NAME=YOURDBNAME
|
||||||
|
DB_USERNAME=YOURDBUSERNAME
|
||||||
|
DB_PASSWORD=YOURDBPASSWORD
|
||||||
|
|
||||||
|
# Keycloak Settings
|
||||||
|
KEYCLOAK_BASE_URL=https://sso.example.com
|
||||||
|
KEYCLOAK_REALM=YOURSSORealm
|
||||||
|
KEYCLOAK_CLIENT_ID=ddnsonroute53
|
||||||
|
KEYCLOAK_CLIENT_SECRET=YOURKEYCLOAKCLIENTSECRET
|
||||||
|
KEYCLOAK_REDIRECT_URI=https://ddnsonroute53.example.com/index.php
|
||||||
22
docker/docker-compose.yml
Normal file
22
docker/docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: ddnsonroute53
|
||||||
|
|
||||||
|
services:
|
||||||
|
ddnsonroute53:
|
||||||
|
image: ghcr.io/ivancarlosti/ddnsonroute53:latest
|
||||||
|
env_file: .env
|
||||||
|
container_name: ddnsonroute53
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5666:80" # expose port 5666 to host
|
||||||
|
environment:
|
||||||
|
- DB_SERVER=${DB_SERVER}
|
||||||
|
- DB_NAME=${DB_NAME}
|
||||||
|
- DB_USERNAME=${DB_USERNAME}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
- KEYCLOAK_BASE_URL=${KEYCLOAK_BASE_URL}
|
||||||
|
- KEYCLOAK_REALM=${KEYCLOAK_REALM}
|
||||||
|
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID}
|
||||||
|
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET}
|
||||||
|
- KEYCLOAK_REDIRECT_URI=${KEYCLOAK_REDIRECT_URI}
|
||||||
|
extra_hosts:
|
||||||
|
- host.docker.internal:host-gateway
|
||||||
4
manifest.json
Normal file
4
manifest.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"version": "3.0.5",
|
||||||
|
"author": "Ivan Carlos"
|
||||||
|
}
|
||||||
42
public/dashboard.php
Normal file
42
public/dashboard.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if(!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true){
|
||||||
|
header('location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current domain of the hosted page
|
||||||
|
$domain = $_SERVER['HTTP_HOST']; // This will automatically get the domain of the hosted page
|
||||||
|
|
||||||
|
// Construct the cURL command
|
||||||
|
$curlCommand = "https://[USERNAME]:[PASSWORD]@$domain/update.php?hostname=[DOMAIN]&myip=[IP]";
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="card">
|
||||||
|
<h1>Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!</h1>
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<a href="manage_users.php" class="btn">Manage Users</a>
|
||||||
|
<a href="manage_aws.php" class="btn">Manage AWS Credentials</a>
|
||||||
|
<a href="manage_ddns.php" class="btn">Manage DDNS Entries</a>
|
||||||
|
<a href="view_logs.php" class="btn">View All Logs</a>
|
||||||
|
<a href="index.php?logout=true" class="btn btn-danger">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>DDNS Update cURL Command</h2>
|
||||||
|
<p>Use the following cURL command to update your DDNS entry:</p>
|
||||||
|
<pre style="background: rgba(0,0,0,0.3); padding: 1rem; border-radius: 0.5rem; overflow-x: auto;"><?php echo htmlspecialchars($curlCommand); ?></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
145
public/index.php
Normal file
145
public/index.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
error_reporting(0);
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
|
||||||
|
function handleFatalError()
|
||||||
|
{
|
||||||
|
$error = error_get_last();
|
||||||
|
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||||||
|
die("A fatal error occurred. Please check your configuration.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
register_shutdown_function('handleFatalError');
|
||||||
|
|
||||||
|
include '../dbconfig.php';
|
||||||
|
|
||||||
|
if ($link === null || $link->connect_error) {
|
||||||
|
die("Database connection failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check table existence
|
||||||
|
function tableExists($link, $tableName)
|
||||||
|
{
|
||||||
|
$sql = "SHOW TABLES LIKE '$tableName'";
|
||||||
|
$result = $link->query($sql);
|
||||||
|
return $result->num_rows > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tableExists($link, 'users')) {
|
||||||
|
header('Location: setup.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Keycloak SSO Logic ---
|
||||||
|
|
||||||
|
// 1. Handle Logout
|
||||||
|
if (isset($_GET['logout'])) {
|
||||||
|
session_destroy();
|
||||||
|
$logoutUrl = KEYCLOAK_BASE_URL . "/realms/" . KEYCLOAK_REALM . "/protocol/openid-connect/logout?redirect_uri=" . urlencode(KEYCLOAK_REDIRECT_URI);
|
||||||
|
header("Location: $logoutUrl");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Handle OAuth Callback
|
||||||
|
if (isset($_GET['code'])) {
|
||||||
|
$code = $_GET['code'];
|
||||||
|
$tokenUrl = KEYCLOAK_BASE_URL . "/realms/" . KEYCLOAK_REALM . "/protocol/openid-connect/token";
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'grant_type' => 'authorization_code',
|
||||||
|
'client_id' => KEYCLOAK_CLIENT_ID,
|
||||||
|
'client_secret' => KEYCLOAK_CLIENT_SECRET,
|
||||||
|
'code' => $code,
|
||||||
|
'redirect_uri' => KEYCLOAK_REDIRECT_URI
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $tokenUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, 1);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$tokenData = json_decode($response, true);
|
||||||
|
|
||||||
|
if (isset($tokenData['access_token'])) {
|
||||||
|
// Get User Info
|
||||||
|
$userInfoUrl = KEYCLOAK_BASE_URL . "/realms/" . KEYCLOAK_REALM . "/protocol/openid-connect/userinfo";
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, $userInfoUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $tokenData['access_token']]);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
$userInfoResponse = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$userInfo = json_decode($userInfoResponse, true);
|
||||||
|
$email = $userInfo['email'] ?? $userInfo['preferred_username'] ?? null;
|
||||||
|
|
||||||
|
if ($email) {
|
||||||
|
// Check if user exists in local DB
|
||||||
|
$stmt = $link->prepare("SELECT id, username FROM users WHERE username = ?");
|
||||||
|
$stmt->bind_param("s", $email);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
if ($result->num_rows === 1) {
|
||||||
|
$user = $result->fetch_assoc();
|
||||||
|
$_SESSION['loggedin'] = true;
|
||||||
|
$_SESSION['id'] = $user['id'];
|
||||||
|
$_SESSION['username'] = $user['username'];
|
||||||
|
header("Location: dashboard.php");
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$error = "Access Denied: User not found in local database.";
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
} else {
|
||||||
|
$error = "Could not retrieve email from identity provider.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$error = "Failed to authenticate with SSO.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Generate Login URL
|
||||||
|
$loginUrl = KEYCLOAK_BASE_URL . "/realms/" . KEYCLOAK_REALM . "/protocol/openid-connect/auth" .
|
||||||
|
"?client_id=" . KEYCLOAK_CLIENT_ID .
|
||||||
|
"&response_type=code" .
|
||||||
|
"&redirect_uri=" . urlencode(KEYCLOAK_REDIRECT_URI) .
|
||||||
|
"&scope=openid email profile";
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - DDNS Manager</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="card login-card text-center">
|
||||||
|
<h1>DDNS Manager</h1>
|
||||||
|
<p class="mb-4" style="color: #94a3b8;">Secure Access Control</p>
|
||||||
|
|
||||||
|
<?php if (isset($error)): ?>
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<?php echo htmlspecialchars($error); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<a href="<?php echo htmlspecialchars($loginUrl); ?>" class="btn"
|
||||||
|
style="width: 100%; box-sizing: border-box;">
|
||||||
|
Login with SSO
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
126
public/manage_aws.php
Normal file
126
public/manage_aws.php
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||||
|
header('location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
include '../dbconfig.php';
|
||||||
|
|
||||||
|
// Handle form submission to update AWS credentials
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||||
|
$region = $_POST['region'];
|
||||||
|
$access_key_id = $_POST['access_key_id'];
|
||||||
|
$secret_access_key = $_POST['secret_access_key'];
|
||||||
|
$hosted_zone_id = $_POST['hosted_zone_id'];
|
||||||
|
$approved_fqdn = $_POST['approved_fqdn'];
|
||||||
|
|
||||||
|
// Check if there's already data in the table
|
||||||
|
$check_sql = "SELECT id FROM aws_credentials LIMIT 1";
|
||||||
|
$check_result = $link->query($check_sql);
|
||||||
|
|
||||||
|
if ($check_result->num_rows > 0) {
|
||||||
|
// Update existing record
|
||||||
|
$sql = "UPDATE aws_credentials SET region = ?, access_key_id = ?, secret_access_key = ?, hosted_zone_id = ?, approved_fqdn = ? WHERE id = 1";
|
||||||
|
} else {
|
||||||
|
// Insert new record
|
||||||
|
$sql = "INSERT INTO aws_credentials (region, access_key_id, secret_access_key, hosted_zone_id, approved_fqdn) VALUES (?, ?, ?, ?, ?)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stmt = $link->prepare($sql)) {
|
||||||
|
$stmt->bind_param("sssss", $region, $access_key_id, $secret_access_key, $hosted_zone_id, $approved_fqdn);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
$success = "AWS credentials updated successfully!";
|
||||||
|
} else {
|
||||||
|
$error = "Error updating AWS credentials: " . $stmt->error;
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
} else {
|
||||||
|
$error = "Error preparing SQL statement: " . $link->error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch current AWS credentials from the database
|
||||||
|
$sql = "SELECT region, access_key_id, secret_access_key, hosted_zone_id, approved_fqdn FROM aws_credentials LIMIT 1";
|
||||||
|
$current_credentials = [];
|
||||||
|
if ($result = $link->query($sql)) {
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
$current_credentials = $result->fetch_assoc();
|
||||||
|
}
|
||||||
|
$result->free();
|
||||||
|
}
|
||||||
|
|
||||||
|
$link->close();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Manage AWS Credentials</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Manage AWS Credentials</h1>
|
||||||
|
|
||||||
|
<?php if (isset($error)): ?>
|
||||||
|
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (isset($success)): ?>
|
||||||
|
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<?php if (!empty($current_credentials)): ?>
|
||||||
|
<h2>Current AWS Credentials</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Region:</strong> <?php echo htmlspecialchars($current_credentials['region']); ?></li>
|
||||||
|
<li><strong>Access Key ID:</strong>
|
||||||
|
<?php echo htmlspecialchars($current_credentials['access_key_id']); ?></li>
|
||||||
|
<li><strong>Secret Access Key:</strong>
|
||||||
|
<?php echo htmlspecialchars($current_credentials['secret_access_key']); ?></li>
|
||||||
|
<li><strong>Hosted Zone ID:</strong>
|
||||||
|
<?php echo htmlspecialchars($current_credentials['hosted_zone_id']); ?></li>
|
||||||
|
<li><strong>Approved FQDN:</strong>
|
||||||
|
<?php echo htmlspecialchars($current_credentials['approved_fqdn']); ?></li>
|
||||||
|
</ul>
|
||||||
|
<?php else: ?>
|
||||||
|
<p>No AWS credentials found in the database.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Update AWS Credentials</h2>
|
||||||
|
<form method="post">
|
||||||
|
<label>Region:</label>
|
||||||
|
<input type="text" name="region"
|
||||||
|
value="<?php echo htmlspecialchars($current_credentials['region'] ?? ''); ?>" required>
|
||||||
|
|
||||||
|
<label>Access Key ID:</label>
|
||||||
|
<input type="text" name="access_key_id"
|
||||||
|
value="<?php echo htmlspecialchars($current_credentials['access_key_id'] ?? ''); ?>" required>
|
||||||
|
|
||||||
|
<label>Secret Access Key:</label>
|
||||||
|
<input type="text" name="secret_access_key"
|
||||||
|
value="<?php echo htmlspecialchars($current_credentials['secret_access_key'] ?? ''); ?>" required>
|
||||||
|
|
||||||
|
<label>Hosted Zone ID:</label>
|
||||||
|
<input type="text" name="hosted_zone_id"
|
||||||
|
value="<?php echo htmlspecialchars($current_credentials['hosted_zone_id'] ?? ''); ?>" required>
|
||||||
|
|
||||||
|
<label>Approved FQDN:</label>
|
||||||
|
<input type="text" name="approved_fqdn"
|
||||||
|
value="<?php echo htmlspecialchars($current_credentials['approved_fqdn'] ?? ''); ?>" required>
|
||||||
|
|
||||||
|
<input type="submit" value="Update Credentials">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="dashboard.php">Back to Dashboard</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
397
public/manage_ddns.php
Normal file
397
public/manage_ddns.php
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||||
|
header('location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
include '../dbconfig.php';
|
||||||
|
require '../vendor/aws.phar';
|
||||||
|
|
||||||
|
use Aws\Route53\Route53Client;
|
||||||
|
use Aws\Exception\AwsException;
|
||||||
|
|
||||||
|
// Clean up logs older than 30 days
|
||||||
|
$cleanup_sql = "CALL CleanupOldLogs()";
|
||||||
|
if ($cleanup_stmt = $link->prepare($cleanup_sql)) {
|
||||||
|
$cleanup_stmt->execute();
|
||||||
|
$cleanup_stmt->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the approved FQDN from the database
|
||||||
|
$approved_fqdn = '';
|
||||||
|
$aws_sql = "SELECT approved_fqdn, region, access_key_id, secret_access_key, hosted_zone_id FROM aws_credentials LIMIT 1";
|
||||||
|
if ($aws_result = $link->query($aws_sql)) {
|
||||||
|
if ($aws_result->num_rows > 0) {
|
||||||
|
$row = $aws_result->fetch_assoc();
|
||||||
|
$approved_fqdn = $row['approved_fqdn'];
|
||||||
|
$region = $row['region'];
|
||||||
|
$access_key_id = $row['access_key_id'];
|
||||||
|
$secret_access_key = $row['secret_access_key'];
|
||||||
|
$hosted_zone_id = $row['hosted_zone_id'];
|
||||||
|
} else {
|
||||||
|
die("No AWS credentials found in the database.");
|
||||||
|
}
|
||||||
|
$aws_result->free();
|
||||||
|
} else {
|
||||||
|
die("Error fetching AWS credentials: " . $link->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the Route53 client
|
||||||
|
try {
|
||||||
|
$route53 = new Route53Client([
|
||||||
|
'version' => 'latest',
|
||||||
|
'region' => $region,
|
||||||
|
'credentials' => [
|
||||||
|
'key' => $access_key_id,
|
||||||
|
'secret' => $secret_access_key,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
} catch (AwsException $e) {
|
||||||
|
die("Error initializing Route53 client: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission to add a new DDNS entry
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['add_ddns'])) {
|
||||||
|
$ddns_fqdn = $_POST['ddns_fqdn'];
|
||||||
|
$ddns_password = $_POST['ddns_password'];
|
||||||
|
$initial_ip = $_POST['initial_ip'];
|
||||||
|
$ttl = $_POST['ttl'];
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (empty($ddns_fqdn) || empty($ddns_password) || empty($initial_ip) || empty($ttl)) {
|
||||||
|
$error = "DDNS FQDN, password, initial IP, and TTL are required.";
|
||||||
|
} elseif (!filter_var($initial_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
$error = "Invalid IPv4 address.";
|
||||||
|
} else {
|
||||||
|
// Check if the DDNS FQDN is a subdomain of the approved FQDN
|
||||||
|
if (strpos($ddns_fqdn, $approved_fqdn) === false || !preg_match('/^[a-zA-Z0-9-]+\.' . preg_quote($approved_fqdn, '/') . '$/', $ddns_fqdn)) {
|
||||||
|
$error = "DDNS FQDN must be a subdomain of $approved_fqdn.";
|
||||||
|
} else {
|
||||||
|
// Check if the DDNS entry already exists
|
||||||
|
$check_sql = "SELECT id FROM ddns_entries WHERE ddns_fqdn = ?";
|
||||||
|
if ($check_stmt = $link->prepare($check_sql)) {
|
||||||
|
$check_stmt->bind_param("s", $ddns_fqdn);
|
||||||
|
$check_stmt->execute();
|
||||||
|
$check_stmt->store_result();
|
||||||
|
|
||||||
|
if ($check_stmt->num_rows > 0) {
|
||||||
|
$error = "DDNS entry with this FQDN already exists.";
|
||||||
|
} else {
|
||||||
|
// Prepare the DNS record
|
||||||
|
$changeBatch = [
|
||||||
|
'Changes' => [
|
||||||
|
[
|
||||||
|
'Action' => 'UPSERT',
|
||||||
|
'ResourceRecordSet' => [
|
||||||
|
'Name' => $ddns_fqdn . '.',
|
||||||
|
'Type' => 'A',
|
||||||
|
'TTL' => (int)$ttl,
|
||||||
|
'ResourceRecords' => [
|
||||||
|
[
|
||||||
|
'Value' => $initial_ip,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create the DNS record in Route53
|
||||||
|
$result = $route53->changeResourceRecordSets([
|
||||||
|
'HostedZoneId' => $hosted_zone_id,
|
||||||
|
'ChangeBatch' => $changeBatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Insert the new DDNS entry into the database
|
||||||
|
$insert_sql = "INSERT INTO ddns_entries (ddns_fqdn, ddns_password, last_ipv4, ttl) VALUES (?, ?, ?, ?)";
|
||||||
|
if ($insert_stmt = $link->prepare($insert_sql)) {
|
||||||
|
$insert_stmt->bind_param("sssi", $ddns_fqdn, $ddns_password, $initial_ip, $ttl);
|
||||||
|
if ($insert_stmt->execute()) {
|
||||||
|
$ddns_entry_id = $insert_stmt->insert_id;
|
||||||
|
|
||||||
|
// Log the action
|
||||||
|
$action = 'add';
|
||||||
|
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||||
|
$details = "Added DDNS entry with FQDN: $ddns_fqdn, Initial IP: $initial_ip, TTL: $ttl";
|
||||||
|
$log_sql = "INSERT INTO ddns_logs (ddns_entry_id, action, ip_address, details) VALUES (?, ?, ?, ?)";
|
||||||
|
if ($log_stmt = $link->prepare($log_sql)) {
|
||||||
|
$log_stmt->bind_param("isss", $ddns_entry_id, $action, $ip_address, $details);
|
||||||
|
$log_stmt->execute();
|
||||||
|
$log_stmt->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = "DDNS entry '$ddns_fqdn' added successfully!";
|
||||||
|
} else {
|
||||||
|
$error = "Error adding DDNS entry: " . $insert_stmt->error;
|
||||||
|
}
|
||||||
|
$insert_stmt->close();
|
||||||
|
}
|
||||||
|
} catch (AwsException $e) {
|
||||||
|
$error = "Error updating Route53: " . $e->getAwsErrorMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$check_stmt->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IP and TTL update for a DDNS entry
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['update_ip'])) {
|
||||||
|
$ddns_id = $_POST['ddns_id'];
|
||||||
|
$new_ip = $_POST['new_ip'];
|
||||||
|
$new_ttl = $_POST['new_ttl'];
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (empty($new_ip) || empty($new_ttl)) {
|
||||||
|
$error = "IP and TTL are required.";
|
||||||
|
} elseif (!filter_var($new_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
$error = "Invalid IPv4 address.";
|
||||||
|
} else {
|
||||||
|
// Fetch the DDNS entry
|
||||||
|
$fetch_sql = "SELECT ddns_fqdn FROM ddns_entries WHERE id = ?";
|
||||||
|
if ($fetch_stmt = $link->prepare($fetch_sql)) {
|
||||||
|
$fetch_stmt->bind_param("i", $ddns_id);
|
||||||
|
$fetch_stmt->execute();
|
||||||
|
$fetch_stmt->store_result();
|
||||||
|
$fetch_stmt->bind_result($ddns_fqdn);
|
||||||
|
$fetch_stmt->fetch();
|
||||||
|
$fetch_stmt->close();
|
||||||
|
|
||||||
|
// Prepare the DNS record update
|
||||||
|
$changeBatch = [
|
||||||
|
'Changes' => [
|
||||||
|
[
|
||||||
|
'Action' => 'UPSERT',
|
||||||
|
'ResourceRecordSet' => [
|
||||||
|
'Name' => $ddns_fqdn . '.',
|
||||||
|
'Type' => 'A',
|
||||||
|
'TTL' => (int)$new_ttl,
|
||||||
|
'ResourceRecords' => [
|
||||||
|
[
|
||||||
|
'Value' => $new_ip,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update the DNS record in Route53
|
||||||
|
$result = $route53->changeResourceRecordSets([
|
||||||
|
'HostedZoneId' => $hosted_zone_id,
|
||||||
|
'ChangeBatch' => $changeBatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update the IP and TTL in the database
|
||||||
|
$update_sql = "UPDATE ddns_entries SET last_ipv4 = ?, ttl = ?, last_update = NOW() WHERE id = ?";
|
||||||
|
if ($update_stmt = $link->prepare($update_sql)) {
|
||||||
|
$update_stmt->bind_param("sii", $new_ip, $new_ttl, $ddns_id);
|
||||||
|
if ($update_stmt->execute()) {
|
||||||
|
// Log the action
|
||||||
|
$action = 'update';
|
||||||
|
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||||
|
$details = "Updated IP: $new_ip, TTL: $new_ttl";
|
||||||
|
$log_sql = "INSERT INTO ddns_logs (ddns_entry_id, action, ip_address, details) VALUES (?, ?, ?, ?)";
|
||||||
|
if ($log_stmt = $link->prepare($log_sql)) {
|
||||||
|
$log_stmt->bind_param("isss", $ddns_id, $action, $ip_address, $details);
|
||||||
|
$log_stmt->execute();
|
||||||
|
$log_stmt->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = "IP and TTL updated successfully for '$ddns_fqdn'!";
|
||||||
|
} else {
|
||||||
|
$error = "Error updating IP and TTL: " . $update_stmt->error;
|
||||||
|
}
|
||||||
|
$update_stmt->close();
|
||||||
|
}
|
||||||
|
} catch (AwsException $e) {
|
||||||
|
$error = "Error updating Route53: " . $e->getAwsErrorMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle DDNS entry deletion
|
||||||
|
if (isset($_GET['delete'])) {
|
||||||
|
$ddns_id = $_GET['delete'];
|
||||||
|
|
||||||
|
// Fetch the DDNS entry to get the FQDN and last IP
|
||||||
|
$fetch_sql = "SELECT ddns_fqdn, last_ipv4, ttl FROM ddns_entries WHERE id = ?";
|
||||||
|
if ($fetch_stmt = $link->prepare($fetch_sql)) {
|
||||||
|
$fetch_stmt->bind_param("i", $ddns_id);
|
||||||
|
$fetch_stmt->execute();
|
||||||
|
$fetch_stmt->store_result();
|
||||||
|
$fetch_stmt->bind_result($ddns_fqdn, $last_ipv4, $ttl);
|
||||||
|
$fetch_stmt->fetch();
|
||||||
|
$fetch_stmt->close();
|
||||||
|
|
||||||
|
// Prepare the DNS record deletion
|
||||||
|
$changeBatch = [
|
||||||
|
'Changes' => [
|
||||||
|
[
|
||||||
|
'Action' => 'DELETE',
|
||||||
|
'ResourceRecordSet' => [
|
||||||
|
'Name' => $ddns_fqdn . '.',
|
||||||
|
'Type' => 'A',
|
||||||
|
'TTL' => (int)$ttl,
|
||||||
|
'ResourceRecords' => [
|
||||||
|
[
|
||||||
|
'Value' => $last_ipv4,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete the DNS record in Route53
|
||||||
|
$result = $route53->changeResourceRecordSets([
|
||||||
|
'HostedZoneId' => $hosted_zone_id,
|
||||||
|
'ChangeBatch' => $changeBatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete the DDNS entry from the database
|
||||||
|
$delete_sql = "DELETE FROM ddns_entries WHERE id = ?";
|
||||||
|
if ($delete_stmt = $link->prepare($delete_sql)) {
|
||||||
|
$delete_stmt->bind_param("i", $ddns_id);
|
||||||
|
if ($delete_stmt->execute()) {
|
||||||
|
$success = "DDNS entry deleted successfully and Route53 record removed!";
|
||||||
|
} else {
|
||||||
|
$error = "Error deleting DDNS entry: " . $delete_stmt->error;
|
||||||
|
}
|
||||||
|
$delete_stmt->close();
|
||||||
|
}
|
||||||
|
} catch (AwsException $e) {
|
||||||
|
$error = "Error updating Route53: " . $e->getAwsErrorMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all DDNS entries from the database
|
||||||
|
$sql = "SELECT id, ddns_fqdn, ddns_password, last_ipv4, ttl, last_update FROM ddns_entries";
|
||||||
|
$ddns_entries = [];
|
||||||
|
if ($result = $link->query($sql)) {
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$ddns_entries[] = $row;
|
||||||
|
}
|
||||||
|
$result->free();
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Manage DDNS Entries</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="table_sort.js"></script>
|
||||||
|
<style>
|
||||||
|
th.sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
th.sortable:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
th.sortable::after {
|
||||||
|
content: '↕';
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
th.sortable.asc::after {
|
||||||
|
content: '↑';
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
th.sortable.desc::after {
|
||||||
|
content: '↓';
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Manage DDNS Entries</h1>
|
||||||
|
|
||||||
|
<?php if (isset($error)): ?>
|
||||||
|
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (isset($success)): ?>
|
||||||
|
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Add New DDNS Entry</h2>
|
||||||
|
<form method="post">
|
||||||
|
<label>DDNS FQDN:</label>
|
||||||
|
<input type="text" name="ddns_fqdn" required placeholder="subdomain.<?php echo htmlspecialchars($approved_fqdn); ?>">
|
||||||
|
|
||||||
|
<label>DDNS Password:</label>
|
||||||
|
<input type="password" name="ddns_password" required>
|
||||||
|
|
||||||
|
<label>Initial IP:</label>
|
||||||
|
<input type="text" name="initial_ip" required value="<?php echo $_SERVER['REMOTE_ADDR']; ?>">
|
||||||
|
|
||||||
|
<label>TTL (Time to Live):</label>
|
||||||
|
<input type="number" name="ttl" min="1" required value="300">
|
||||||
|
|
||||||
|
<input type="submit" name="add_ddns" value="Add DDNS Entry">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>DDNS Entries</h2>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="ddnsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="sortable" data-type="string">FQDN</th>
|
||||||
|
<th class="sortable" data-type="string">Password</th>
|
||||||
|
<th class="sortable" data-type="string">Last IPv4</th>
|
||||||
|
<th class="sortable" data-type="number">TTL</th>
|
||||||
|
<th class="sortable" data-type="string">Last Update</th>
|
||||||
|
<th>Update IP/TTL</th>
|
||||||
|
<th>Logs</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($ddns_entries as $entry): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo htmlspecialchars($entry['ddns_fqdn']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($entry['ddns_password']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($entry['last_ipv4']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($entry['ttl']); ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($entry['last_update']); ?></td>
|
||||||
|
<td>
|
||||||
|
<form method="post" style="display:inline; max-width: none;">
|
||||||
|
<input type="hidden" name="ddns_id" value="<?php echo $entry['id']; ?>">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="text" name="new_ip" placeholder="New IP" required style="width: 120px;">
|
||||||
|
<input type="number" name="new_ttl" placeholder="TTL" min="1" required style="width: 80px;">
|
||||||
|
<input type="submit" name="update_ip" value="Update" style="padding: 0.5rem;">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="view_logs.php?ddns_id=<?php echo $entry['id']; ?>" class="btn" style="padding: 0.25rem 0.5rem; font-size: 0.875rem;">Logs</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="manage_ddns.php?delete=<?php echo $entry['id']; ?>" onclick="return confirm('Are you sure you want to delete this DDNS entry?');" class="btn btn-danger" style="padding: 0.25rem 0.5rem; font-size: 0.875rem;">Delete</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="dashboard.php">Back to Dashboard</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
158
public/manage_users.php
Normal file
158
public/manage_users.php
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||||
|
header('location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
include '../dbconfig.php';
|
||||||
|
|
||||||
|
// Fetch the logged-in user's ID and username
|
||||||
|
$logged_in_user_id = $_SESSION['id'];
|
||||||
|
$logged_in_username = $_SESSION['username'];
|
||||||
|
|
||||||
|
// Handle form submission to add a new user (admin-only feature)
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['add_user'])) {
|
||||||
|
$username = $_POST['username'];
|
||||||
|
// Password is now optional for SSO users
|
||||||
|
$password = !empty($_POST['password']) ? $_POST['password'] : null;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (empty($username)) {
|
||||||
|
$error = "Username (Email) is required.";
|
||||||
|
} else {
|
||||||
|
// Check if the username is a valid email address
|
||||||
|
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$error = "Invalid email address.";
|
||||||
|
} else {
|
||||||
|
// Check if the user already exists
|
||||||
|
$check_sql = "SELECT id FROM users WHERE username = ?";
|
||||||
|
if ($check_stmt = $link->prepare($check_sql)) {
|
||||||
|
$check_stmt->bind_param("s", $username);
|
||||||
|
$check_stmt->execute();
|
||||||
|
$check_stmt->store_result();
|
||||||
|
|
||||||
|
if ($check_stmt->num_rows > 0) {
|
||||||
|
$error = "User with this email already exists.";
|
||||||
|
} else {
|
||||||
|
// Insert the new user
|
||||||
|
$password_hash = $password ? password_hash($password, PASSWORD_DEFAULT) : null;
|
||||||
|
$insert_sql = "INSERT INTO users (username, password_hash) VALUES (?, ?)";
|
||||||
|
if ($insert_stmt = $link->prepare($insert_sql)) {
|
||||||
|
$insert_stmt->bind_param("ss", $username, $password_hash);
|
||||||
|
if ($insert_stmt->execute()) {
|
||||||
|
$success = "User '$username' added successfully!";
|
||||||
|
} else {
|
||||||
|
$error = "Error adding user: " . $insert_stmt->error;
|
||||||
|
}
|
||||||
|
$insert_stmt->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$check_stmt->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle user deletion (admin-only feature)
|
||||||
|
if (isset($_GET['delete'])) {
|
||||||
|
$user_id = $_GET['delete'];
|
||||||
|
|
||||||
|
// Prevent the logged-in user from deleting themselves
|
||||||
|
if ($user_id == $logged_in_user_id) {
|
||||||
|
$error = "You cannot delete your own account.";
|
||||||
|
} else {
|
||||||
|
$sql = "DELETE FROM users WHERE id = ?";
|
||||||
|
if ($stmt = $link->prepare($sql)) {
|
||||||
|
$stmt->bind_param("i", $user_id);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
$success = "User deleted successfully!";
|
||||||
|
} else {
|
||||||
|
$error = "Error deleting user: " . $stmt->error;
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all users from the database
|
||||||
|
$sql = "SELECT id, username, password_hash FROM users";
|
||||||
|
$users = [];
|
||||||
|
if ($result = $link->query($sql)) {
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$users[] = $row;
|
||||||
|
}
|
||||||
|
$result->free();
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Manage Users</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Manage Users</h1>
|
||||||
|
|
||||||
|
<?php if (isset($error)): ?>
|
||||||
|
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (isset($success)): ?>
|
||||||
|
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Add New User</h2>
|
||||||
|
<p>Add a user to allow them to login via Keycloak SSO. Password is optional and only needed if you plan to support local login (legacy).</p>
|
||||||
|
<form method="post">
|
||||||
|
<label>Email address:</label>
|
||||||
|
<input type="email" name="username" required placeholder="user@example.com">
|
||||||
|
|
||||||
|
<label>Password (Optional):</label>
|
||||||
|
<input type="password" name="password" placeholder="Leave empty for SSO-only users">
|
||||||
|
|
||||||
|
<input type="submit" name="add_user" value="Add User">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>User List</h2>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Email address</th>
|
||||||
|
<th>Auth Type</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($users as $user): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo $user['id']; ?></td>
|
||||||
|
<td><?php echo htmlspecialchars($user['username']); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php echo $user['password_hash'] ? 'Password + SSO' : 'SSO Only'; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if ($user['id'] != $logged_in_user_id): ?>
|
||||||
|
<a href="manage_users.php?delete=<?php echo $user['id']; ?>" onclick="return confirm('Are you sure you want to delete this user?');" class="btn btn-danger" style="padding: 0.25rem 0.5rem; font-size: 0.875rem;">Delete</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<em>Current User</em>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="dashboard.php">Back to Dashboard</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
141
public/setup.php
Normal file
141
public/setup.php
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
error_reporting(0);
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
|
||||||
|
function handleFatalError()
|
||||||
|
{
|
||||||
|
$error = error_get_last();
|
||||||
|
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||||||
|
die("A fatal error occurred. Please check your configuration and try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register_shutdown_function('handleFatalError');
|
||||||
|
|
||||||
|
if (!file_exists('../dbconfig.php')) {
|
||||||
|
die("The database configuration file (dbconfig.php) is missing. Please create it with the correct database credentials.");
|
||||||
|
}
|
||||||
|
|
||||||
|
include '../dbconfig.php';
|
||||||
|
|
||||||
|
if ($link === null || $link->connect_error) {
|
||||||
|
die("Database connection failed. Please check the dbconfig.php file and ensure the database credentials are correct.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$tables = [];
|
||||||
|
$result = $link->query("SHOW TABLES");
|
||||||
|
if ($result) {
|
||||||
|
while ($row = $result->fetch_row()) {
|
||||||
|
$tables[] = $row[0];
|
||||||
|
}
|
||||||
|
$result->free();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($tables)) {
|
||||||
|
die("An installation already exists in this database. Please clean up the database or update the dbconfig.php file to use a new database.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['create_admin'])) {
|
||||||
|
$username = $_POST['username'];
|
||||||
|
$password = $_POST['password'];
|
||||||
|
|
||||||
|
if (empty($username) || empty($password)) {
|
||||||
|
echo "Username and password are required.";
|
||||||
|
} elseif (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
echo "Username must be a valid email address.";
|
||||||
|
} else {
|
||||||
|
$password_hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
|
||||||
|
$create_tables_sql = [
|
||||||
|
"CREATE TABLE users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NULL
|
||||||
|
)",
|
||||||
|
"CREATE TABLE aws_credentials (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
region VARCHAR(50) NOT NULL,
|
||||||
|
access_key_id VARCHAR(255) NOT NULL,
|
||||||
|
secret_access_key VARCHAR(255) NOT NULL,
|
||||||
|
hosted_zone_id VARCHAR(255) NOT NULL,
|
||||||
|
approved_fqdn VARCHAR(255) NOT NULL
|
||||||
|
)",
|
||||||
|
"CREATE TABLE ddns_entries (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
ddns_fqdn VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
ddns_password VARCHAR(255) NOT NULL,
|
||||||
|
last_ipv4 VARCHAR(15),
|
||||||
|
ttl INT NOT NULL DEFAULT 300,
|
||||||
|
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
)",
|
||||||
|
"CREATE TABLE ddns_logs (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
ddns_entry_id INT NOT NULL,
|
||||||
|
action VARCHAR(50) NOT NULL,
|
||||||
|
ip_address VARCHAR(15),
|
||||||
|
details TEXT,
|
||||||
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (ddns_entry_id) REFERENCES ddns_entries(id) ON DELETE CASCADE
|
||||||
|
)",
|
||||||
|
"CREATE TABLE recaptcha_keys (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
site_key VARCHAR(255) NOT NULL,
|
||||||
|
secret_key VARCHAR(255) NOT NULL
|
||||||
|
)",
|
||||||
|
"CREATE PROCEDURE CleanupOldLogs()
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM ddns_logs WHERE timestamp < NOW() - INTERVAL 30 DAY;
|
||||||
|
END"
|
||||||
|
];
|
||||||
|
|
||||||
|
$success = true;
|
||||||
|
foreach ($create_tables_sql as $sql) {
|
||||||
|
if (!$link->query($sql)) {
|
||||||
|
$success = false;
|
||||||
|
echo "Error creating table or procedure: " . $link->error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$insert_sql = "INSERT INTO users (username, password_hash) VALUES (?, ?)";
|
||||||
|
if ($stmt = $link->prepare($insert_sql)) {
|
||||||
|
$stmt->bind_param("ss", $username, $password_hash);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
echo "First admin user created successfully! You can now log in.";
|
||||||
|
echo '<p><a href="index.php">Go to Login Page</a></p>';
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
echo "Error creating admin user: " . $stmt->error;
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Setup</title>
|
||||||
|
<title>Setup</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Setup</h1>
|
||||||
|
<p>Welcome to the setup wizard. This script will help you prepare a new installation.</p>
|
||||||
|
|
||||||
|
<?php if (empty($tables)): ?>
|
||||||
|
<h2>Create First Admin User</h2>
|
||||||
|
<form method="post">
|
||||||
|
<label>Email address:</label>
|
||||||
|
<input type="email" name="username" required><br>
|
||||||
|
<label>Password:</label>
|
||||||
|
<input type="password" name="password" required><br>
|
||||||
|
<input type="submit" name="create_admin" value="Create Admin User">
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
244
public/style.css
Normal file
244
public/style.css
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/* Modern Dark Theme with Glassmorphism */
|
||||||
|
:root {
|
||||||
|
--bg-color: #0f172a;
|
||||||
|
--text-color: #e2e8f0;
|
||||||
|
--primary-color: #6366f1;
|
||||||
|
--primary-hover: #4f46e5;
|
||||||
|
--secondary-color: #10b981;
|
||||||
|
--surface-color: rgba(30, 41, 59, 0.7);
|
||||||
|
--border-color: rgba(148, 163, 184, 0.1);
|
||||||
|
--error-color: #ef4444;
|
||||||
|
--glass-bg: rgba(30, 41, 59, 0.6);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%),
|
||||||
|
radial-gradient(at 100% 0%, rgba(16, 185, 129, 0.15) 0px, transparent 50%);
|
||||||
|
background-attachment: fixed;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: #f8fafc;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.container {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards / Glassmorphism */
|
||||||
|
.card {
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="password"],
|
||||||
|
input[type="number"],
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
background: rgba(15, 23, 42, 0.6);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
button,
|
||||||
|
input[type="submit"],
|
||||||
|
.btn {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s, transform 0.1s;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover,
|
||||||
|
input[type="submit"]:hover,
|
||||||
|
.btn:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active,
|
||||||
|
input[type="submit"]:active,
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables */
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 1rem;
|
||||||
|
background: rgba(15, 23, 42, 0.3);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: rgba(30, 41, 59, 0.8);
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts */
|
||||||
|
.alert {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
border-color: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: rgba(16, 185, 129, 0.1);
|
||||||
|
border-color: rgba(16, 185, 129, 0.2);
|
||||||
|
color: #6ee7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login Page Specific */
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsiveness */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stack buttons on very small screens if needed, or keep wrapped */
|
||||||
|
.flex > .btn {
|
||||||
|
flex: 1 1 auto; /* Allow buttons to grow and fill width */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Wrapper class to be added in PHP */
|
||||||
|
.table-responsive {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure table doesn't force width */
|
||||||
|
table {
|
||||||
|
min-width: 600px; /* Force scroll if content is squished */
|
||||||
|
}
|
||||||
|
}
|
||||||
48
public/table_sort.js
Normal file
48
public/table_sort.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const table = document.getElementById('ddnsTable');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
const headers = table.querySelectorAll('th.sortable');
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const type = header.dataset.type || 'string';
|
||||||
|
const isAscending = header.classList.contains('asc');
|
||||||
|
|
||||||
|
// Reset all headers
|
||||||
|
headers.forEach(h => h.classList.remove('asc', 'desc'));
|
||||||
|
|
||||||
|
// Toggle sort order
|
||||||
|
if (!isAscending) {
|
||||||
|
header.classList.add('asc');
|
||||||
|
} else {
|
||||||
|
header.classList.add('desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||||
|
|
||||||
|
const sortedRows = rows.sort((a, b) => {
|
||||||
|
// Find the cell corresponding to the header index
|
||||||
|
// Note: We need to account for the fact that headers might not match columns 1:1 if there are colspans,
|
||||||
|
// but here it seems straightforward. However, we should find the index of the header among all ths in the thead
|
||||||
|
// to match the td index.
|
||||||
|
const allHeaders = Array.from(header.parentElement.children);
|
||||||
|
const colIndex = allHeaders.indexOf(header);
|
||||||
|
|
||||||
|
const aText = a.children[colIndex].textContent.trim();
|
||||||
|
const bText = b.children[colIndex].textContent.trim();
|
||||||
|
|
||||||
|
if (type === 'number') {
|
||||||
|
return isAscending ? bText - aText : aText - bText;
|
||||||
|
} else {
|
||||||
|
return isAscending ? bText.localeCompare(aText) : aText.localeCompare(bText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-append rows
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
sortedRows.forEach(row => tbody.appendChild(row));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
120
public/update.php
Normal file
120
public/update.php
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
include '../dbconfig.php';
|
||||||
|
require '../vendor/aws.phar';
|
||||||
|
|
||||||
|
use Aws\Route53\Route53Client;
|
||||||
|
use Aws\Exception\AwsException;
|
||||||
|
|
||||||
|
// Clean up logs older than 30 days
|
||||||
|
$cleanup_sql = "CALL CleanupOldLogs()";
|
||||||
|
if ($cleanup_stmt = $link->prepare($cleanup_sql)) {
|
||||||
|
$cleanup_stmt->execute();
|
||||||
|
$cleanup_stmt->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the hostname and IP from the request
|
||||||
|
$ddns_fqdn = $_GET['hostname'];
|
||||||
|
$myip = $_GET['myip'];
|
||||||
|
$ddns_password = $_SERVER['PHP_AUTH_PW'];
|
||||||
|
|
||||||
|
// Validate the request
|
||||||
|
if (empty($ddns_fqdn) || empty($myip) || empty($ddns_password)) {
|
||||||
|
die("badauth");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the DDNS entry from the database
|
||||||
|
$sql = "SELECT id, ddns_fqdn, ddns_password, last_ipv4, ttl FROM ddns_entries WHERE ddns_fqdn = ? AND ddns_password = ?";
|
||||||
|
if ($stmt = $link->prepare($sql)) {
|
||||||
|
$stmt->bind_param("ss", $ddns_fqdn, $ddns_password);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->store_result();
|
||||||
|
|
||||||
|
if ($stmt->num_rows == 1) {
|
||||||
|
$stmt->bind_result($id, $ddns_fqdn, $ddns_password, $last_ipv4, $ttl);
|
||||||
|
$stmt->fetch();
|
||||||
|
|
||||||
|
// Check if the IP has changed
|
||||||
|
if ($last_ipv4 !== $myip) {
|
||||||
|
// Fetch AWS credentials from the database
|
||||||
|
$aws_sql = "SELECT region, access_key_id, secret_access_key, hosted_zone_id FROM aws_credentials LIMIT 1";
|
||||||
|
if ($aws_stmt = $link->prepare($aws_sql)) {
|
||||||
|
$aws_stmt->execute();
|
||||||
|
$aws_stmt->store_result();
|
||||||
|
$aws_stmt->bind_result($region, $access_key_id, $secret_access_key, $hosted_zone_id);
|
||||||
|
$aws_stmt->fetch();
|
||||||
|
$aws_stmt->close();
|
||||||
|
|
||||||
|
// Initialize the Route53 client
|
||||||
|
$route53 = new Route53Client([
|
||||||
|
'version' => 'latest',
|
||||||
|
'region' => $region,
|
||||||
|
'credentials' => [
|
||||||
|
'key' => $access_key_id,
|
||||||
|
'secret' => $secret_access_key,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Prepare the DNS record update
|
||||||
|
$changeBatch = [
|
||||||
|
'Changes' => [
|
||||||
|
[
|
||||||
|
'Action' => 'UPSERT',
|
||||||
|
'ResourceRecordSet' => [
|
||||||
|
'Name' => $ddns_fqdn,
|
||||||
|
'Type' => 'A',
|
||||||
|
'TTL' => $ttl,
|
||||||
|
'ResourceRecords' => [
|
||||||
|
[
|
||||||
|
'Value' => $myip,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update the DNS record in Route53
|
||||||
|
$result = $route53->changeResourceRecordSets([
|
||||||
|
'HostedZoneId' => $hosted_zone_id,
|
||||||
|
'ChangeBatch' => $changeBatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update the database with the new IP
|
||||||
|
$update_sql = "UPDATE ddns_entries SET last_ipv4 = ?, last_update = NOW() WHERE id = ?";
|
||||||
|
if ($update_stmt = $link->prepare($update_sql)) {
|
||||||
|
$update_stmt->bind_param("si", $myip, $id);
|
||||||
|
$update_stmt->execute();
|
||||||
|
$update_stmt->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the action
|
||||||
|
$action = 'update';
|
||||||
|
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||||
|
$details = "Updated IP: $myip";
|
||||||
|
$log_sql = "INSERT INTO ddns_logs (ddns_entry_id, action, ip_address, details) VALUES (?, ?, ?, ?)";
|
||||||
|
if ($log_stmt = $link->prepare($log_sql)) {
|
||||||
|
$log_stmt->bind_param("isss", $id, $action, $ip_address, $details);
|
||||||
|
$log_stmt->execute();
|
||||||
|
$log_stmt->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "good"; // Success
|
||||||
|
} catch (AwsException $e) {
|
||||||
|
echo "dnserror"; // DNS update failed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "badauth"; // AWS credentials not found
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "nochg"; // IP hasn't changed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "badauth"; // Invalid DDNS credentials
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
} else {
|
||||||
|
echo "badauth"; // Database error
|
||||||
|
}
|
||||||
|
$link->close();
|
||||||
|
?>
|
||||||
141
public/view_logs.php
Normal file
141
public/view_logs.php
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||||
|
header('location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
include '../dbconfig.php';
|
||||||
|
|
||||||
|
// Clean up logs older than 30 days
|
||||||
|
$cleanup_sql = "CALL CleanupOldLogs()";
|
||||||
|
if ($cleanup_stmt = $link->prepare($cleanup_sql)) {
|
||||||
|
$cleanup_stmt->execute();
|
||||||
|
$cleanup_stmt->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize variables
|
||||||
|
$ddns_id = isset($_GET['ddns_id']) ? (int) $_GET['ddns_id'] : null;
|
||||||
|
$where_clause = "";
|
||||||
|
$params = [];
|
||||||
|
$types = "";
|
||||||
|
|
||||||
|
// Build WHERE clause if ddns_id is specified
|
||||||
|
if ($ddns_id !== null) {
|
||||||
|
$where_clause = " WHERE l.ddns_entry_id = ?";
|
||||||
|
$params[] = $ddns_id;
|
||||||
|
$types = "i";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination setup
|
||||||
|
$per_page = 20;
|
||||||
|
$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
|
||||||
|
$offset = ($page - 1) * $per_page;
|
||||||
|
|
||||||
|
// Main query with conditional filtering
|
||||||
|
$query = "SELECT l.*, d.ddns_fqdn
|
||||||
|
FROM ddns_logs l
|
||||||
|
LEFT JOIN ddns_entries d ON l.ddns_entry_id = d.id
|
||||||
|
$where_clause
|
||||||
|
ORDER BY l.timestamp DESC
|
||||||
|
LIMIT ?, ?";
|
||||||
|
|
||||||
|
// Count query with same filtering
|
||||||
|
$count_query = "SELECT COUNT(*) as total
|
||||||
|
FROM ddns_logs l
|
||||||
|
$where_clause";
|
||||||
|
|
||||||
|
// Prepare and execute count query
|
||||||
|
$count_stmt = $link->prepare($count_query);
|
||||||
|
if ($ddns_id !== null) {
|
||||||
|
$count_stmt->bind_param($types, ...$params);
|
||||||
|
}
|
||||||
|
$count_stmt->execute();
|
||||||
|
$total = $count_stmt->get_result()->fetch_assoc()['total'];
|
||||||
|
$count_stmt->close();
|
||||||
|
|
||||||
|
// Calculate total pages
|
||||||
|
$pages = ceil($total / $per_page);
|
||||||
|
|
||||||
|
// Prepare main query
|
||||||
|
$stmt = $link->prepare($query);
|
||||||
|
if ($ddns_id !== null) {
|
||||||
|
$params[] = $offset;
|
||||||
|
$params[] = $per_page;
|
||||||
|
$stmt->bind_param($types . "ii", ...$params);
|
||||||
|
} else {
|
||||||
|
$stmt->bind_param("ii", $offset, $per_page);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
$logs = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||||
|
$stmt->close();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><?= $ddns_id ? "Logs for DDNS #$ddns_id" : "All DDNS Logs" ?></title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1><?= $ddns_id ? "Logs for DDNS Entry #$ddns_id" : "All DDNS Logs" ?></h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<!-- Logs Table -->
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>FQDN</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>Details</th>
|
||||||
|
<th>Timestamp</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($logs as $log): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= htmlspecialchars($log['ddns_fqdn'] ?? 'N/A') ?></td>
|
||||||
|
<td><?= htmlspecialchars($log['action']) ?></td>
|
||||||
|
<td><?= htmlspecialchars($log['ip_address']) ?></td>
|
||||||
|
<td><?= htmlspecialchars($log['details']) ?></td>
|
||||||
|
<td><?= htmlspecialchars($log['timestamp']) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="flex gap-2 mt-4" style="justify-content: center;">
|
||||||
|
<?php if ($page > 1): ?>
|
||||||
|
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $page - 1])) ?>" class="btn">Previous</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php for ($i = 1; $i <= $pages; $i++): ?>
|
||||||
|
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $i])) ?>"
|
||||||
|
class="btn <?= $i == $page ? 'btn-primary' : '' ?>"
|
||||||
|
style="<?= $i == $page ? 'background-color: var(--primary-hover);' : '' ?>"><?= $i ?></a>
|
||||||
|
<?php endfor; ?>
|
||||||
|
|
||||||
|
<?php if ($page < $pages): ?>
|
||||||
|
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $page + 1])) ?>" class="btn">Next</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-4">
|
||||||
|
<a href="<?= $ddns_id ? 'manage_ddns.php' : 'dashboard.php' ?>">
|
||||||
|
Back to <?= $ddns_id ? 'DDNS Management' : 'Dashboard' ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
BIN
vendor/aws.phar
vendored
Normal file
BIN
vendor/aws.phar
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user