Compare commits
16 Commits
6e09214182
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 301cbf9d06 | |||
| d1025134cd | |||
| 8766e534df | |||
| 31e480d3de | |||
| c773169ff6 | |||
| f8560068dd | |||
| ccae7bf73c | |||
| fc3181ee3b | |||
| acf09db63e | |||
| 7a7e0bd185 | |||
| 11d26dfe8b | |||
| a540a57efc | |||
| 37cf88a06e | |||
| eb3ffed163 | |||
| 091d00b5c2 | |||
| 10fb66c470 |
+449
-19
@@ -2,15 +2,268 @@ name: Deploy
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
GIT_HOST: git.jakach.ch
|
||||||
|
GIT_REPO: jakach/jakach-login
|
||||||
|
GIT_BRANCH: main
|
||||||
|
|
||||||
|
APP_NAME: auth
|
||||||
|
APP_DOMAIN: auth.jakach.ch
|
||||||
|
APP_PORT: 447
|
||||||
|
|
||||||
|
SECURITY_SCAN_ENABLED: ${{ vars.SECURITY_SCAN_ENABLED }}
|
||||||
|
CODE_SCAN_ENABLED: ${{ vars.CODE_SCAN_ENABLED }}
|
||||||
|
TRIVY_SEVERITY: HIGH,CRITICAL
|
||||||
|
TRIVY_IMAGE_SCANNERS: vuln
|
||||||
|
TRIVY_VEX: repo
|
||||||
|
TRIVY_FS_SCANNERS: vuln,misconfig,secret
|
||||||
|
SEMGREP_CONFIG: p/default
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
security_scan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
GIT_USER: ${{ vars.GIT_USER }}
|
||||||
|
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||||
|
TRIVY_REGISTRY_USER: ${{ vars.TRIVY_REGISTRY_USER }}
|
||||||
|
TRIVY_REGISTRY_PASSWORD: ${{ secrets.TRIVY_REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Scan Docker images for vulnerabilities
|
||||||
|
run: |
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
case "${SECURITY_SCAN_ENABLED:-true}" in
|
||||||
|
false|False|FALSE|0|no|No|NO|off|Off|OFF)
|
||||||
|
echo "Security scan disabled by SECURITY_SCAN_ENABLED=${SECURITY_SCAN_ENABLED}"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
: "${GIT_USER:?GIT_USER is required}"
|
||||||
|
: "${GIT_TOKEN:?GIT_TOKEN is required}"
|
||||||
|
|
||||||
|
if command -v apk >/dev/null 2>&1; then
|
||||||
|
apk add --no-cache ca-certificates curl git
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y ca-certificates curl git
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
dnf install -y ca-certificates curl git
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
yum install -y ca-certificates curl git
|
||||||
|
else
|
||||||
|
echo "Unsupported package manager"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v trivy >/dev/null 2>&1; then
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
|
||||||
|
| sh -s -- -b "$HOME/.local/bin"
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_URL="https://${GIT_USER}:${GIT_TOKEN}@${GIT_HOST}/${GIT_REPO}"
|
||||||
|
|
||||||
|
git clone \
|
||||||
|
--branch "$GIT_BRANCH" \
|
||||||
|
"$REPO_URL" \
|
||||||
|
source
|
||||||
|
|
||||||
|
cd source
|
||||||
|
|
||||||
|
COMPOSE_FILE=""
|
||||||
|
|
||||||
|
for file in docker-compose.yml compose.yml compose.yaml; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
COMPOSE_FILE="$file"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$COMPOSE_FILE" ]; then
|
||||||
|
echo "No docker compose file found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
docker compose -f "$COMPOSE_FILE" config --images > images.txt
|
||||||
|
else
|
||||||
|
awk '
|
||||||
|
/^[[:space:]]*image:[[:space:]]*/ {
|
||||||
|
sub(/^[[:space:]]*image:[[:space:]]*/, "")
|
||||||
|
gsub(/["\047]/, "")
|
||||||
|
print
|
||||||
|
}
|
||||||
|
' "$COMPOSE_FILE" > images.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
sort -u images.txt -o images.txt
|
||||||
|
|
||||||
|
if [ ! -s images.txt ]; then
|
||||||
|
echo "No Docker images found to scan"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${TRIVY_REGISTRY_USER:-}" ] || [ -n "${TRIVY_REGISTRY_PASSWORD:-}" ]; then
|
||||||
|
: "${TRIVY_REGISTRY_USER:?TRIVY_REGISTRY_USER is required when TRIVY_REGISTRY_PASSWORD is set}"
|
||||||
|
: "${TRIVY_REGISTRY_PASSWORD:?TRIVY_REGISTRY_PASSWORD is required when TRIVY_REGISTRY_USER is set}"
|
||||||
|
echo "Using registry credentials from TRIVY_REGISTRY_USER/TRIVY_REGISTRY_PASSWORD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TRIVY_IGNORE_ARGS=""
|
||||||
|
|
||||||
|
if [ -f cve_blacklist.txt ]; then
|
||||||
|
awk '
|
||||||
|
/^[[:space:]]*($|#)/ {
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{
|
||||||
|
print $1
|
||||||
|
}
|
||||||
|
' cve_blacklist.txt > .trivyignore
|
||||||
|
|
||||||
|
if [ -s .trivyignore ]; then
|
||||||
|
TRIVY_IGNORE_ARGS="--ignorefile .trivyignore"
|
||||||
|
echo "Using CVE blacklist from cve_blacklist.txt"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Scanning Docker images for ${TRIVY_SEVERITY} vulnerabilities:"
|
||||||
|
cat images.txt
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
|
||||||
|
while IFS= read -r image; do
|
||||||
|
[ -n "$image" ] || continue
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Scanning ${image}"
|
||||||
|
|
||||||
|
if [ -n "${TRIVY_REGISTRY_USER:-}" ]; then
|
||||||
|
if ! trivy \
|
||||||
|
image \
|
||||||
|
--username "${TRIVY_REGISTRY_USER}" \
|
||||||
|
--password "${TRIVY_REGISTRY_PASSWORD}" \
|
||||||
|
--scanners "${TRIVY_IMAGE_SCANNERS}" \
|
||||||
|
--vex "${TRIVY_VEX}" \
|
||||||
|
--exit-code 1 \
|
||||||
|
--severity "${TRIVY_SEVERITY}" \
|
||||||
|
--ignore-unfixed \
|
||||||
|
${TRIVY_IGNORE_ARGS} \
|
||||||
|
--no-progress \
|
||||||
|
"${image}"; then
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
elif ! trivy \
|
||||||
|
image \
|
||||||
|
--scanners "${TRIVY_IMAGE_SCANNERS}" \
|
||||||
|
--vex "${TRIVY_VEX}" \
|
||||||
|
--exit-code 1 \
|
||||||
|
--severity "${TRIVY_SEVERITY}" \
|
||||||
|
--ignore-unfixed \
|
||||||
|
${TRIVY_IGNORE_ARGS} \
|
||||||
|
--no-progress \
|
||||||
|
"${image}"; then
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done < images.txt
|
||||||
|
|
||||||
|
if [ "$failed" -ne 0 ]; then
|
||||||
|
echo "WARNING: One or more Docker image scans failed or found ${TRIVY_SEVERITY} vulnerabilities. Deployment stopped."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "No high or critical vulnerabilities found"
|
||||||
|
|
||||||
|
code_scan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
GIT_USER: ${{ vars.GIT_USER }}
|
||||||
|
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Scan source code
|
||||||
|
run: |
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
case "${CODE_SCAN_ENABLED:-true}" in
|
||||||
|
false|False|FALSE|0|no|No|NO|off|Off|OFF)
|
||||||
|
echo "Code scan disabled by CODE_SCAN_ENABLED=${CODE_SCAN_ENABLED}"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
: "${GIT_USER:?GIT_USER is required}"
|
||||||
|
: "${GIT_TOKEN:?GIT_TOKEN is required}"
|
||||||
|
|
||||||
|
if command -v apk >/dev/null 2>&1; then
|
||||||
|
apk add --no-cache ca-certificates curl git python3 py3-pip
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y ca-certificates curl git python3 python3-pip python3-venv
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
dnf install -y ca-certificates curl git python3 python3-pip
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
yum install -y ca-certificates curl git python3 python3-pip
|
||||||
|
else
|
||||||
|
echo "Unsupported package manager"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v semgrep >/dev/null 2>&1; then
|
||||||
|
python3 -m venv "$HOME/.semgrep-venv"
|
||||||
|
. "$HOME/.semgrep-venv/bin/activate"
|
||||||
|
python3 -m pip install --upgrade pip
|
||||||
|
python3 -m pip install semgrep
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v trivy >/dev/null 2>&1; then
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
|
||||||
|
| sh -s -- -b "$HOME/.local/bin"
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_URL="https://${GIT_USER}:${GIT_TOKEN}@${GIT_HOST}/${GIT_REPO}"
|
||||||
|
|
||||||
|
git clone \
|
||||||
|
--branch "$GIT_BRANCH" \
|
||||||
|
"$REPO_URL" \
|
||||||
|
source
|
||||||
|
|
||||||
|
cd source
|
||||||
|
|
||||||
|
semgrep scan \
|
||||||
|
--config "${SEMGREP_CONFIG}" \
|
||||||
|
--error \
|
||||||
|
--metrics=off
|
||||||
|
|
||||||
|
trivy fs \
|
||||||
|
--scanners "${TRIVY_FS_SCANNERS}" \
|
||||||
|
--exit-code 1 \
|
||||||
|
--severity "${TRIVY_SEVERITY}" \
|
||||||
|
--ignore-unfixed \
|
||||||
|
--no-progress \
|
||||||
|
.
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- security_scan
|
||||||
|
- code_scan
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install SSH client
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if command -v apk >/dev/null 2>&1; then
|
if command -v apk >/dev/null 2>&1; then
|
||||||
apk add --no-cache openssh-client git bash
|
apk add --no-cache openssh-client git bash
|
||||||
elif command -v apt-get >/dev/null 2>&1; then
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
@@ -21,55 +274,232 @@ jobs:
|
|||||||
elif command -v yum >/dev/null 2>&1; then
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
yum install -y openssh-clients git bash
|
yum install -y openssh-clients git bash
|
||||||
else
|
else
|
||||||
echo "No supported package manager found"
|
echo "Unsupported package manager"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
- name: Run deploy
|
|
||||||
|
- name: Deploy application
|
||||||
env:
|
env:
|
||||||
SSH_KEY: ${{ secrets.SSH_KEY }}
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
||||||
SSH_USER: ${{ vars.SSH_USER }}
|
SSH_USER: ${{ vars.SSH_USER }}
|
||||||
SSH_IP: ${{ vars.SSH_IP }}
|
SSH_IP: ${{ vars.SSH_IP }}
|
||||||
|
|
||||||
GIT_USER: ${{ vars.GIT_USER }}
|
GIT_USER: ${{ vars.GIT_USER }}
|
||||||
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||||
APP_DIR: /home/deploy/my-app
|
|
||||||
GIT_REPO: Jakach/my-app.git
|
|
||||||
GIT_BRANCH: main
|
|
||||||
run: |
|
run: |
|
||||||
cat > deploy.sh <<'EOF'
|
cat > deploy.sh <<'OUTER_EOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
: "${SSH_KEY:?SSH_KEY is required}"
|
: "${SSH_KEY:?SSH_KEY is required}"
|
||||||
: "${SSH_USER:?SSH_USER is required}"
|
: "${SSH_USER:?SSH_USER is required}"
|
||||||
: "${SSH_IP:?SSH_IP is required}"
|
: "${SSH_IP:?SSH_IP is required}"
|
||||||
: "${GIT_USER:?GIT_USER is required}"
|
: "${GIT_USER:?GIT_USER is required}"
|
||||||
: "${GIT_TOKEN:?GIT_TOKEN is required}"
|
: "${GIT_TOKEN:?GIT_TOKEN is required}"
|
||||||
|
|
||||||
APP_DIR="/srv/systems/jakach-login"
|
REPO_NAME="$(basename "$GIT_REPO")"
|
||||||
GIT_HOST="${GIT_HOST:-git.jakach.ch}"
|
APP_DIR="/srv/systems/${REPO_NAME}"
|
||||||
GIT_REPO="jakach/jakach-login.git"
|
|
||||||
GIT_BRANCH="${GIT_BRANCH:-main}"
|
|
||||||
|
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
chmod 700 ~/.ssh
|
chmod 700 ~/.ssh
|
||||||
|
|
||||||
printf '%s\n' "$SSH_KEY" | tr -d '\r' > ~/.ssh/deploy_key
|
printf '%s\n' "$SSH_KEY" | tr -d '\r' > ~/.ssh/deploy_key
|
||||||
chmod 600 ~/.ssh/deploy_key
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
|
||||||
ssh-keyscan -H "$SSH_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
|
ssh-keyscan -H "$SSH_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||||
|
|
||||||
ssh -i ~/.ssh/deploy_key \
|
ssh \
|
||||||
|
-i ~/.ssh/deploy_key \
|
||||||
-o StrictHostKeyChecking=yes \
|
-o StrictHostKeyChecking=yes \
|
||||||
-o IdentitiesOnly=yes \
|
-o IdentitiesOnly=yes \
|
||||||
"$SSH_USER@$SSH_IP" \
|
"$SSH_USER@$SSH_IP" \
|
||||||
"export APP_DIR='$APP_DIR' GIT_HOST='$GIT_HOST' GIT_REPO='$GIT_REPO' GIT_BRANCH='$GIT_BRANCH' GIT_USER='$GIT_USER' GIT_TOKEN='$GIT_TOKEN'; bash -s" <<'REMOTE'
|
"
|
||||||
|
export \
|
||||||
|
APP_NAME='${APP_NAME}' \
|
||||||
|
APP_DOMAIN='${APP_DOMAIN}' \
|
||||||
|
APP_PORT='${APP_PORT}' \
|
||||||
|
APP_DIR='${APP_DIR}' \
|
||||||
|
GIT_HOST='${GIT_HOST}' \
|
||||||
|
GIT_REPO='${GIT_REPO}' \
|
||||||
|
GIT_BRANCH='${GIT_BRANCH}' \
|
||||||
|
GIT_USER='${GIT_USER}' \
|
||||||
|
GIT_TOKEN='${GIT_TOKEN}';
|
||||||
|
bash -s
|
||||||
|
" <<'REMOTE_EOF'
|
||||||
|
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
REPO_URL="https://${GIT_USER}:${GIT_TOKEN}@${GIT_HOST}/${GIT_REPO}"
|
||||||
|
|
||||||
|
PROXY_DIR="/srv/systems/proxy"
|
||||||
|
NGINX_CONF="${PROXY_DIR}/nginx_conf/nginx.conf"
|
||||||
|
GET_CERT_SCRIPT="${PROXY_DIR}/get_cert.sh"
|
||||||
|
RENEW_SCRIPT="${PROXY_DIR}/renew_cert.sh"
|
||||||
|
FIRST_DEPLOY=0
|
||||||
|
PROXY_RESTART_REQUIRED=0
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Clone repository if missing
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
if [ ! -d "$APP_DIR/.git" ]; then
|
||||||
|
echo "Repository missing, cloning..."
|
||||||
|
FIRST_DEPLOY=1
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$APP_DIR")"
|
||||||
|
|
||||||
|
git clone \
|
||||||
|
--branch "$GIT_BRANCH" \
|
||||||
|
"$REPO_URL" \
|
||||||
|
"$APP_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Update repository
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
cd "$APP_DIR"
|
cd "$APP_DIR"
|
||||||
git remote set-url origin "https://${GIT_USER}:${GIT_TOKEN}@${GIT_HOST}/${GIT_REPO}"
|
|
||||||
|
git remote set-url origin "$REPO_URL"
|
||||||
|
|
||||||
git fetch origin "$GIT_BRANCH"
|
git fetch origin "$GIT_BRANCH"
|
||||||
|
|
||||||
git checkout "$GIT_BRANCH"
|
git checkout "$GIT_BRANCH"
|
||||||
|
|
||||||
git pull origin "$GIT_BRANCH"
|
git pull origin "$GIT_BRANCH"
|
||||||
docker compose down
|
|
||||||
docker compose up -d --build
|
# --------------------------------------------------
|
||||||
REMOTE
|
# Create nginx reverse proxy entry
|
||||||
EOF
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
if [ "$FIRST_DEPLOY" -eq 1 ]; then
|
||||||
|
|
||||||
|
if ! grep -q "proxy_pass http://192.168.1.109:${APP_PORT}/;" "$NGINX_CONF"; then
|
||||||
|
echo "Creating nginx entry..."
|
||||||
|
PROXY_RESTART_REQUIRED=1
|
||||||
|
|
||||||
|
cat >> "$NGINX_CONF" <<NGINXEOF
|
||||||
|
|
||||||
|
# ---------------------
|
||||||
|
# ${APP_NAME} Service
|
||||||
|
# ---------------------
|
||||||
|
server {
|
||||||
|
server_tokens off;
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name ${APP_DOMAIN};
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/${APP_NAME}.fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/${APP_NAME}.privkey.pem;
|
||||||
|
|
||||||
|
if (\$allowed_country = no) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://192.168.1.109:${APP_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NGINXEOF
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Nginx entry already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Add domain to get_cert.sh
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
if ! grep -q "\"${APP_DOMAIN}\"" "$GET_CERT_SCRIPT"; then
|
||||||
|
echo "Adding domain to get_cert.sh"
|
||||||
|
|
||||||
|
sed -i "/DOMAINS=(/a\ \"${APP_DOMAIN}\"" "$GET_CERT_SCRIPT"
|
||||||
|
PROXY_RESTART_REQUIRED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Create certificate if missing
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
if [ ! -d "/etc/letsencrypt/live/${APP_DOMAIN}" ]; then
|
||||||
|
echo "Creating certificate..."
|
||||||
|
|
||||||
|
sudo certbot certonly \
|
||||||
|
--standalone \
|
||||||
|
--non-interactive \
|
||||||
|
--agree-tos \
|
||||||
|
-m admin@jakach.ch \
|
||||||
|
-d "${APP_DOMAIN}"
|
||||||
|
PROXY_RESTART_REQUIRED=1
|
||||||
|
else
|
||||||
|
echo "Certificate already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Update renew_cert.sh
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
if ! grep -q "${APP_DOMAIN}/privkey.pem" "$RENEW_SCRIPT"; then
|
||||||
|
echo "Updating renew_cert.sh"
|
||||||
|
|
||||||
|
cat >> "$RENEW_SCRIPT" <<CERTEOF
|
||||||
|
|
||||||
|
cp /etc/letsencrypt/live/${APP_DOMAIN}/privkey.pem certs/${APP_NAME}.privkey.pem
|
||||||
|
cp /etc/letsencrypt/live/${APP_DOMAIN}/fullchain.pem certs/${APP_NAME}.fullchain.pem
|
||||||
|
|
||||||
|
CERTEOF
|
||||||
|
PROXY_RESTART_REQUIRED=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod +x "$RENEW_SCRIPT"
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Make renew non-interactive
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
sed -i 's/certbot renew$/certbot renew -n/' "$RENEW_SCRIPT" || true
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Renew certs + restart proxy
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
if [ "$PROXY_RESTART_REQUIRED" -eq 1 ]; then
|
||||||
|
cd "$PROXY_DIR"
|
||||||
|
|
||||||
|
sudo bash /srv/systems/proxy/renew_cert.sh
|
||||||
|
|
||||||
|
docker compose down || true
|
||||||
|
docker compose up -d --build
|
||||||
|
else
|
||||||
|
echo "Proxy already configured, skipping certificate renewal and proxy restart"
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Existing deployment, skipping proxy setup, certificate renewal and proxy restart"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Deploy app
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
|
||||||
|
if [ -f docker-compose.yml ] || [ -f compose.yml ] || [ -f compose.yaml ]; then
|
||||||
|
echo "Deploying docker stack..."
|
||||||
|
|
||||||
|
docker compose down || true
|
||||||
|
docker compose up -d --build
|
||||||
|
else
|
||||||
|
echo "No docker compose file found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deployment complete"
|
||||||
|
|
||||||
|
REMOTE_EOF
|
||||||
|
OUTER_EOF
|
||||||
|
|
||||||
chmod +x deploy.sh
|
chmod +x deploy.sh
|
||||||
./deploy.sh
|
./deploy.sh
|
||||||
@@ -14,19 +14,18 @@ Using Jakach Login is straightforward:
|
|||||||
|
|
||||||
1. **Clone the repository:**
|
1. **Clone the repository:**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/jakani24/jakach-login
|
git clone https://git.jakach.ch/jakani24/jakach-login
|
||||||
```
|
```
|
||||||
2. **Create the `certs/` folder and set up SSL certificates:**
|
|
||||||
```bash
|
|
||||||
mkdir certs/
|
|
||||||
```
|
|
||||||
- Generate certificates (e.g., using [Let's Encrypt](https://letsencrypt.org/getting-started/#with-shell-access)).
|
|
||||||
|
|
||||||
3. **Create a Docker volume for database storage:**
|
3. **Create a Docker volume for database storage:**
|
||||||
```bash
|
```bash
|
||||||
docker volume create jakach-login-db-storage
|
docker volume create jakach-login-db-storage
|
||||||
```
|
```
|
||||||
4. **Run the system using Docker Compose:**
|
4. **Authenticate to Docker Hardened Images:**
|
||||||
|
```bash
|
||||||
|
docker login dhi.io
|
||||||
|
```
|
||||||
|
5. **Run the system using Docker Compose:**
|
||||||
```bash
|
```bash
|
||||||
docker-compose up
|
docker-compose up
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
|
ServerTokens Prod
|
||||||
|
ServerSignature Off
|
||||||
|
TraceEnable Off
|
||||||
|
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
ServerName auth.jakach.ch
|
ServerName auth.jakach.ch
|
||||||
DocumentRoot /var/www/html
|
DocumentRoot /var/www/html
|
||||||
|
|
||||||
|
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
Header always set X-Frame-Options "DENY"
|
||||||
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), publickey-credentials-get=(self)"
|
||||||
|
Header always set Content-Security-Policy "base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests"
|
||||||
|
|
||||||
<Directory /var/www/html>
|
<Directory /var/www/html>
|
||||||
Options FollowSymLinks
|
Options FollowSymLinks
|
||||||
AllowOverride All
|
AllowOverride All
|
||||||
|
|||||||
@@ -818,8 +818,25 @@ function updatePasswordStrength() {
|
|||||||
data.domains.forEach(d => {
|
data.domains.forEach(d => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'list-group-item d-flex justify-content-between align-items-center';
|
item.className = 'list-group-item d-flex justify-content-between align-items-center';
|
||||||
item.innerHTML = '<span><strong>' + d.domain + '</strong><br><small class="text-muted">Approved: ' + d.confirmed_at + '</small></span>' +
|
|
||||||
'<button class="btn btn-sm btn-outline-danger" onclick="removeDomain(' + d.id + ')">Revoke</button>';
|
const details = document.createElement('span');
|
||||||
|
const domain = document.createElement('strong');
|
||||||
|
domain.textContent = d.domain;
|
||||||
|
const approvedAt = document.createElement('small');
|
||||||
|
approvedAt.className = 'text-muted';
|
||||||
|
approvedAt.textContent = 'Approved: ' + d.confirmed_at;
|
||||||
|
details.appendChild(domain);
|
||||||
|
details.appendChild(document.createElement('br'));
|
||||||
|
details.appendChild(approvedAt);
|
||||||
|
|
||||||
|
const revokeButton = document.createElement('button');
|
||||||
|
revokeButton.type = 'button';
|
||||||
|
revokeButton.className = 'btn btn-sm btn-outline-danger';
|
||||||
|
revokeButton.textContent = 'Revoke';
|
||||||
|
revokeButton.addEventListener('click', () => removeDomain(Number(d.id)));
|
||||||
|
|
||||||
|
item.appendChild(details);
|
||||||
|
item.appendChild(revokeButton);
|
||||||
list.appendChild(item);
|
list.appendChild(item);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -881,9 +898,30 @@ function updatePasswordStrength() {
|
|||||||
'sessions_revoked': 'Sessions revoked',
|
'sessions_revoked': 'Sessions revoked',
|
||||||
};
|
};
|
||||||
const label = actionLabels[e.action] || e.action;
|
const label = actionLabels[e.action] || e.action;
|
||||||
item.innerHTML = '<div class="d-flex w-100 justify-content-between"><strong>' + label + '</strong><small class="text-muted">' + e.created_at + '</small></div>' +
|
|
||||||
'<small class="text-muted">' + (e.ip ? e.ip + ' · ' : '') + (e.user_agent ? e.user_agent.substring(0, 60) + '...' : '') + '</small>' +
|
const header = document.createElement('div');
|
||||||
(e.details ? '<br><small>' + e.details + '</small>' : '');
|
header.className = 'd-flex w-100 justify-content-between';
|
||||||
|
const action = document.createElement('strong');
|
||||||
|
action.textContent = label;
|
||||||
|
const createdAt = document.createElement('small');
|
||||||
|
createdAt.className = 'text-muted';
|
||||||
|
createdAt.textContent = e.created_at;
|
||||||
|
header.appendChild(action);
|
||||||
|
header.appendChild(createdAt);
|
||||||
|
|
||||||
|
const metadata = document.createElement('small');
|
||||||
|
metadata.className = 'text-muted';
|
||||||
|
metadata.textContent = (e.ip ? e.ip + ' - ' : '') + (e.user_agent ? e.user_agent.substring(0, 60) + '...' : '');
|
||||||
|
|
||||||
|
item.appendChild(header);
|
||||||
|
item.appendChild(metadata);
|
||||||
|
|
||||||
|
if (e.details) {
|
||||||
|
const details = document.createElement('small');
|
||||||
|
details.textContent = e.details;
|
||||||
|
item.appendChild(document.createElement('br'));
|
||||||
|
item.appendChild(details);
|
||||||
|
}
|
||||||
list.appendChild(item);
|
list.appendChild(item);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -904,7 +942,11 @@ function updatePasswordStrength() {
|
|||||||
data.sessions.forEach(s => {
|
data.sessions.forEach(s => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'list-group-item d-flex justify-content-between align-items-center';
|
item.className = 'list-group-item d-flex justify-content-between align-items-center';
|
||||||
item.innerHTML = '<span><strong>' + (s.user_agent || 'Unknown device') + '</strong></span>';
|
const device = document.createElement('span');
|
||||||
|
const deviceName = document.createElement('strong');
|
||||||
|
deviceName.textContent = s.user_agent || 'Unknown device';
|
||||||
|
device.appendChild(deviceName);
|
||||||
|
item.appendChild(device);
|
||||||
list.appendChild(item);
|
list.appendChild(item);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ if ($method === 'GET') {
|
|||||||
$result = mysqli_stmt_get_result($stmt);
|
$result = mysqli_stmt_get_result($stmt);
|
||||||
$domains = [];
|
$domains = [];
|
||||||
while ($row = mysqli_fetch_assoc($result)) {
|
while ($row = mysqli_fetch_assoc($result)) {
|
||||||
|
$domain = normalize_redirect_host($row['domain'] ?? '');
|
||||||
|
if ($domain === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$row['domain'] = $domain;
|
||||||
|
$row['id'] = (int) $row['id'];
|
||||||
$domains[] = $row;
|
$domains[] = $row;
|
||||||
}
|
}
|
||||||
mysqli_stmt_close($stmt);
|
mysqli_stmt_close($stmt);
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ secure_session_start();
|
|||||||
require_same_origin_request();
|
require_same_origin_request();
|
||||||
require_csrf_token();
|
require_csrf_token();
|
||||||
|
|
||||||
|
function print_json_response($data): void
|
||||||
|
{
|
||||||
|
print(htmlentities(json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT), ENT_NOQUOTES, 'UTF-8'));
|
||||||
|
}
|
||||||
|
|
||||||
// Assuming you've already established a database connection here
|
// Assuming you've already established a database connection here
|
||||||
include "../../config/config.php";
|
include "../../config/config.php";
|
||||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE);
|
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE);
|
||||||
@@ -104,7 +109,7 @@ try {
|
|||||||
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment);
|
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment);
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
print(json_encode($createArgs));
|
print_json_response($createArgs);
|
||||||
|
|
||||||
// save challange to session. you have to deliver it to processGet later.
|
// save challange to session. you have to deliver it to processGet later.
|
||||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||||
@@ -138,7 +143,7 @@ try {
|
|||||||
$getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification);
|
$getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification);
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
print(json_encode($getArgs));
|
print_json_response($getArgs);
|
||||||
|
|
||||||
// save challange to session. you have to deliver it to processGet later.
|
// save challange to session. you have to deliver it to processGet later.
|
||||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize and validate the input
|
// Sanitize and validate the input
|
||||||
$name = preg_replace("/[^a-zA-Z0-9_]/", "", $data['name']); // Allow only letters, numbers, and underscores
|
$name = strtolower(preg_replace("/[^a-zA-Z0-9_]/", "", $data['name']));
|
||||||
$email = trim((string) $data['email']);
|
$email = trim((string) $data['email']);
|
||||||
if ($email !== "" && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
if ($email !== "" && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
@@ -52,7 +52,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
]);
|
]);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
$telegram_id = htmlspecialchars($data['telegram_id'], ENT_QUOTES, 'UTF-8'); // Escape special characters
|
$telegram_id = htmlspecialchars($data['telegram_id'], ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
//check if username is allready taken
|
//check if username is allready taken
|
||||||
$id_check=0;
|
$id_check=0;
|
||||||
@@ -63,8 +63,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
mysqli_stmt_store_result($stmt);
|
mysqli_stmt_store_result($stmt);
|
||||||
mysqli_stmt_bind_result($stmt, $id_check);
|
mysqli_stmt_bind_result($stmt, $id_check);
|
||||||
mysqli_stmt_fetch($stmt);
|
mysqli_stmt_fetch($stmt);
|
||||||
if(mysqli_stmt_num_rows($stmt) > 0 && $username!==$name){
|
if((mysqli_stmt_num_rows($stmt) > 0 && $username!==$name) || $name === ""){
|
||||||
//this username is allready taken
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Username allready taken. Please choose another username.'
|
'message' => 'Username allready taken. Please choose another username.'
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ include "../utils/security.php";
|
|||||||
secure_session_start();
|
secure_session_start();
|
||||||
require_same_origin_request();
|
require_same_origin_request();
|
||||||
require_csrf_token();
|
require_csrf_token();
|
||||||
|
|
||||||
|
function print_json_response($data): void
|
||||||
|
{
|
||||||
|
print(htmlentities(json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT), ENT_NOQUOTES, 'UTF-8'));
|
||||||
|
}
|
||||||
|
|
||||||
include "../../config/config.php";
|
include "../../config/config.php";
|
||||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE);
|
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE);
|
||||||
if ($conn->connect_error) {
|
if ($conn->connect_error) {
|
||||||
@@ -90,7 +96,7 @@ try {
|
|||||||
// Get create arguments
|
// Get create arguments
|
||||||
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification);
|
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification);
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
print(json_encode($createArgs));
|
print_json_response($createArgs);
|
||||||
|
|
||||||
// Save challenge to session or somewhere else if needed
|
// Save challenge to session or somewhere else if needed
|
||||||
} else if ($fn === 'getGetArgs') {
|
} else if ($fn === 'getGetArgs') {
|
||||||
@@ -120,7 +126,7 @@ try {
|
|||||||
$getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification);
|
$getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification);
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
print(json_encode($getArgs));
|
print_json_response($getArgs);
|
||||||
|
|
||||||
// save challange to session. you have to deliver it to processGet later.
|
// save challange to session. you have to deliver it to processGet later.
|
||||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$send_to = normalize_redirect_target($_SESSION["end_url"] ?? "/account/");
|
||||||
$domain = $input['domain'] ?? '';
|
$domain = is_external_domain($send_to);
|
||||||
|
|
||||||
if ($domain === '' || !isset($_SESSION['id'])) {
|
if ($domain === null || !isset($_SESSION['id'])) {
|
||||||
echo json_encode(['success' => false, 'message' => 'Missing domain or not logged in.']);
|
echo json_encode(['success' => false, 'message' => 'Missing external domain or not logged in.']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ if (!$user_found) {
|
|||||||
}
|
}
|
||||||
//send telegram message
|
//send telegram message
|
||||||
$device = $_SERVER['HTTP_USER_AGENT'] ?? "";
|
$device = $_SERVER['HTTP_USER_AGENT'] ?? "";
|
||||||
//$ip=$_SERVER["REMOTE_ADDR"];
|
|
||||||
$forwarded_for = $_SERVER["HTTP_X_FORWARDED_FOR"] ?? $_SERVER["REMOTE_ADDR"] ?? "";
|
$forwarded_for = $_SERVER["HTTP_X_FORWARDED_FOR"] ?? $_SERVER["REMOTE_ADDR"] ?? "";
|
||||||
$ip=trim(explode(",",$forwarded_for)[0]);
|
$ip=trim(explode(",",$forwarded_for)[0]);
|
||||||
$location=get_location_from_ip($ip);
|
$location=get_location_from_ip($ip);
|
||||||
@@ -44,15 +43,20 @@ $token=bin2hex(random_bytes(128));
|
|||||||
$token_hash=auth_token_hash($token);
|
$token_hash=auth_token_hash($token);
|
||||||
$link="https://auth.jakach.ch/login/reset_pw.php?token=$token";
|
$link="https://auth.jakach.ch/login/reset_pw.php?token=$token";
|
||||||
|
|
||||||
|
$tg_device = str_replace(['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'], ['\\_', '\\*', '\\[', '\\]', '\\(', '\\)', '\\~', '\\`', '\\>', '\\#', '\\+', '\\-', '\\=', '\\|', '\\{', '\\}', '\\.', '\\!'], $device);
|
||||||
|
$tg_username = str_replace(['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'], ['\\_', '\\*', '\\[', '\\]', '\\(', '\\)', '\\~', '\\`', '\\>', '\\#', '\\+', '\\-', '\\=', '\\|', '\\{', '\\}', '\\.', '\\!'], $_SESSION["username"]);
|
||||||
|
$tg_ip = str_replace(['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'], ['\\_', '\\*', '\\[', '\\]', '\\(', '\\)', '\\~', '\\`', '\\>', '\\#', '\\+', '\\-', '\\=', '\\|', '\\{', '\\}', '\\.', '\\!'], $ip);
|
||||||
|
$tg_location = str_replace(['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'], ['\\_', '\\*', '\\[', '\\]', '\\(', '\\)', '\\~', '\\`', '\\>', '\\#', '\\+', '\\-', '\\=', '\\|', '\\{', '\\}', '\\.', '\\!'], ($location["country"] ?? "").", ".($location["state"] ?? "").", ".($location["city"] ?? ""));
|
||||||
|
|
||||||
$message = "*Password reset token*\n\n"
|
$message = "*Password reset token*\n\n"
|
||||||
. "You have requested the reset of your password here is your reset link.\n\n"
|
. "You have requested the reset of your password here is your reset link.\n\n"
|
||||||
. "*Link*: [click here]($link)\n\n"
|
. "*Link*: [click here]($link)\n\n"
|
||||||
. "*Details of this request:*\n"
|
. "*Details of this request:*\n"
|
||||||
. "• *Date&Time*: $date\n"
|
. "• *Date&Time*: $date\n"
|
||||||
. "• *Device&Browser*: $device\n"
|
. "• *Device&Browser*: $tg_device\n"
|
||||||
. "*Location*: ".$location["country"].", ".$location["state"].", ".$location["city"]."\n"
|
. "*Location*: $tg_location\n"
|
||||||
. "• *Account*: ".$_SESSION["username"]."\n"
|
. "• *Account*: $tg_username\n"
|
||||||
. "• *IP*: $ip\n\n"
|
. "• *IP*: $tg_ip\n\n"
|
||||||
."If this was you, you can reset your password. If this was not you somebody else tried to reset your password!\n"
|
."If this was you, you can reset your password. If this was not you somebody else tried to reset your password!\n"
|
||||||
. "*Thank you for using Jakach login!*";
|
. "*Thank you for using Jakach login!*";
|
||||||
|
|
||||||
@@ -78,6 +82,12 @@ curl_close($ch);
|
|||||||
//send mail
|
//send mail
|
||||||
if(!empty($mail)){
|
if(!empty($mail)){
|
||||||
$loc=$location["country"].", ".$location["state"].", ".$location["city"];
|
$loc=$location["country"].", ".$location["state"].", ".$location["city"];
|
||||||
|
$html_username = htmlspecialchars($username, ENT_QUOTES, 'UTF-8');
|
||||||
|
$html_device = htmlspecialchars($device, ENT_QUOTES, 'UTF-8');
|
||||||
|
$html_ip = htmlspecialchars($ip, ENT_QUOTES, 'UTF-8');
|
||||||
|
$html_loc = htmlspecialchars($loc, ENT_QUOTES, 'UTF-8');
|
||||||
|
$html_mail = htmlspecialchars($mail, ENT_QUOTES, 'UTF-8');
|
||||||
|
$html_link = htmlspecialchars($link, ENT_QUOTES, 'UTF-8');
|
||||||
$content = '
|
$content = '
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
@@ -89,433 +99,67 @@ if(!empty($mail)){
|
|||||||
<meta name="supported-color-schemes" content="light dark" />
|
<meta name="supported-color-schemes" content="light dark" />
|
||||||
<title></title>
|
<title></title>
|
||||||
<style type="text/css" rel="stylesheet" media="all">
|
<style type="text/css" rel="stylesheet" media="all">
|
||||||
/* Base ------------------------------ */
|
|
||||||
|
|
||||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||||
body {
|
body { width:100% !important; height:100%; margin:0; -webkit-text-size-adjust:none; }
|
||||||
width: 100% !important;
|
a { color:#3869D4; }
|
||||||
height: 100%;
|
a img { border:none; }
|
||||||
margin: 0;
|
td { word-break:break-word; }
|
||||||
-webkit-text-size-adjust: none;
|
.preheader { display:none !important; visibility:hidden; mso-hide:all; font-size:1px; line-height:1px; max-height:0; max-width:0; opacity:0; overflow:hidden; }
|
||||||
}
|
body, td, th { font-family:"Nunito Sans",Helvetica,Arial,sans-serif; }
|
||||||
|
h1 { margin-top:0; color:#333333; font-size:22px; font-weight:bold; text-align:left; }
|
||||||
a {
|
h2 { margin-top:0; color:#333333; font-size:16px; font-weight:bold; text-align:left; }
|
||||||
color: #3869D4;
|
h3 { margin-top:0; color:#333333; font-size:14px; font-weight:bold; text-align:left; }
|
||||||
}
|
td, th { font-size:16px; }
|
||||||
|
p, ul, ol, blockquote { margin:.4em 0 1.1875em; font-size:16px; line-height:1.625; }
|
||||||
a img {
|
p.sub { font-size:13px; }
|
||||||
border: none;
|
.align-right { text-align:right; }
|
||||||
}
|
.align-left { text-align:left; }
|
||||||
|
.align-center { text-align:center; }
|
||||||
td {
|
.u-margin-bottom-none { margin-bottom:0; }
|
||||||
word-break: break-word;
|
.button { background-color:#3869D4; border-top:10px solid #3869D4; border-right:18px solid #3869D4; border-bottom:10px solid #3869D4; border-left:18px solid #3869D4; display:inline-block; color:#FFF; text-decoration:none; border-radius:3px; box-shadow:0 2px 3px rgba(0,0,0,0.16); -webkit-text-size-adjust:none; box-sizing:border-box; }
|
||||||
}
|
.button--green { background-color:#22BC66; border-top:10px solid #22BC66; border-right:18px solid #22BC66; border-bottom:10px solid #22BC66; border-left:18px solid #22BC66; }
|
||||||
|
.button--red { background-color:#FF6136; border-top:10px solid #FF6136; border-right:18px solid #FF6136; border-bottom:10px solid #FF6136; border-left:18px solid #FF6136; }
|
||||||
.preheader {
|
@media only screen and (max-width:500px) { .button { width:100% !important; text-align:center !important; } }
|
||||||
display: none !important;
|
.attributes { margin:0 0 21px; }
|
||||||
visibility: hidden;
|
.attributes_content { background-color:#F4F4F7; padding:16px; }
|
||||||
mso-hide: all;
|
.attributes_item { padding:0; }
|
||||||
font-size: 1px;
|
.related { width:100%; margin:0; padding:25px 0 0 0; -premailer-width:100%; -premailer-cellpadding:0; -premailer-cellspacing:0; }
|
||||||
line-height: 1px;
|
.related_item { padding:10px 0; color:#CBCCCF; font-size:15px; line-height:18px; }
|
||||||
max-height: 0;
|
.related_item-title { display:block; margin:.5em 0 0; }
|
||||||
max-width: 0;
|
.related_item-thumb { display:block; padding-bottom:10px; }
|
||||||
opacity: 0;
|
.related_heading { border-top:1px solid #CBCCCF; text-align:center; padding:25px 0 10px; }
|
||||||
overflow: hidden;
|
.discount { width:100%; margin:0; padding:24px; -premailer-width:100%; -premailer-cellpadding:0; -premailer-cellspacing:0; background-color:#F4F4F7; border:2px dashed #CBCCCF; }
|
||||||
}
|
.discount_heading { text-align:center; }
|
||||||
/* Type ------------------------------ */
|
.discount_body { text-align:center; font-size:15px; }
|
||||||
|
.social { width:auto; }
|
||||||
body,
|
.social td { padding:0; width:auto; }
|
||||||
td,
|
.social_icon { height:20px; margin:0 8px 10px 8px; padding:0; }
|
||||||
th {
|
.purchase { width:100%; margin:0; padding:35px 0; -premailer-width:100%; -premailer-cellpadding:0; -premailer-cellspacing:0; }
|
||||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
.purchase_content { width:100%; margin:0; padding:25px 0 0 0; -premailer-width:100%; -premailer-cellpadding:0; -premailer-cellspacing:0; }
|
||||||
}
|
.purchase_item { padding:10px 0; color:#51545E; font-size:15px; line-height:18px; }
|
||||||
|
.purchase_heading { padding-bottom:8px; border-bottom:1px solid #EAEAEC; }
|
||||||
h1 {
|
.purchase_heading p { margin:0; color:#85878E; font-size:12px; }
|
||||||
margin-top: 0;
|
.purchase_footer { padding-top:15px; border-top:1px solid #EAEAEC; }
|
||||||
color: #333333;
|
.purchase_total { margin:0; text-align:right; font-weight:bold; color:#333333; }
|
||||||
font-size: 22px;
|
.purchase_total--label { padding:0 15px 0 0; }
|
||||||
font-weight: bold;
|
body { background-color:#F2F4F6; color:#51545E; }
|
||||||
text-align: left;
|
p { color:#51545E; }
|
||||||
}
|
.email-wrapper { width:100%; margin:0; padding:0; -premailer-width:100%; -premailer-cellpadding:0; -premailer-cellspacing:0; background-color:#F2F4F6; }
|
||||||
|
.email-content { width:100%; margin:0; padding:0; -premailer-width:100%; -premailer-cellpadding:0; -premailer-cellspacing:0; }
|
||||||
h2 {
|
.email-masthead { padding:25px 0; text-align:center; }
|
||||||
margin-top: 0;
|
.email-masthead_logo { width:94px; }
|
||||||
color: #333333;
|
.email-masthead_name { font-size:16px; font-weight:bold; color:#A8AAAF; text-decoration:none; text-shadow:0 1px 0 white; }
|
||||||
font-size: 16px;
|
.email-body { width:100%; margin:0; padding:0; -premailer-width:100%; -premailer-cellpadding:0; -premailer-cellspacing:0; }
|
||||||
font-weight: bold;
|
.email-body_inner { width:570px; margin:0 auto; padding:0; -premailer-width:570px; -premailer-cellpadding:0; -premailer-cellspacing:0; background-color:#FFFFFF; }
|
||||||
text-align: left;
|
.email-footer { width:570px; margin:0 auto; padding:0; -premailer-width:570px; -premailer-cellpadding:0; -premailer-cellspacing:0; text-align:center; }
|
||||||
}
|
.email-footer p { color:#A8AAAF; }
|
||||||
|
.body-action { width:100%; margin:30px auto; padding:0; -premailer-width:100%; -premailer-cellpadding:0; -premailer-cellspacing:0; text-align:center; }
|
||||||
h3 {
|
.body-sub { margin-top:25px; padding-top:25px; border-top:1px solid #EAEAEC; }
|
||||||
margin-top: 0;
|
.content-cell { padding:45px; }
|
||||||
color: #333333;
|
@media only screen and (max-width:600px) { .email-body_inner, .email-footer { width:100% !important; } }
|
||||||
font-size: 14px;
|
@media (prefers-color-scheme:dark) { body,.email-body,.email-body_inner,.email-content,.email-wrapper,.email-masthead,.email-footer { background-color:#333333 !important; color:#FFF !important; } p,ul,ol,blockquote,h1,h2,h3,span,.purchase_item { color:#FFF !important; } .attributes_content,.discount { background-color:#222 !important; } .email-masthead_name { text-shadow:none !important; } }
|
||||||
font-weight: bold;
|
:root { color-scheme:light dark; supported-color-schemes:light dark; }
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
|
||||||
ul,
|
|
||||||
ol,
|
|
||||||
blockquote {
|
|
||||||
margin: .4em 0 1.1875em;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.625;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.sub {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
/* Utilities ------------------------------ */
|
|
||||||
|
|
||||||
.align-right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-left {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-margin-bottom-none {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
/* Buttons ------------------------------ */
|
|
||||||
|
|
||||||
.button {
|
|
||||||
background-color: #3869D4;
|
|
||||||
border-top: 10px solid #3869D4;
|
|
||||||
border-right: 18px solid #3869D4;
|
|
||||||
border-bottom: 10px solid #3869D4;
|
|
||||||
border-left: 18px solid #3869D4;
|
|
||||||
display: inline-block;
|
|
||||||
color: #FFF;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
|
||||||
-webkit-text-size-adjust: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button--green {
|
|
||||||
background-color: #22BC66;
|
|
||||||
border-top: 10px solid #22BC66;
|
|
||||||
border-right: 18px solid #22BC66;
|
|
||||||
border-bottom: 10px solid #22BC66;
|
|
||||||
border-left: 18px solid #22BC66;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button--red {
|
|
||||||
background-color: #FF6136;
|
|
||||||
border-top: 10px solid #FF6136;
|
|
||||||
border-right: 18px solid #FF6136;
|
|
||||||
border-bottom: 10px solid #FF6136;
|
|
||||||
border-left: 18px solid #FF6136;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 500px) {
|
|
||||||
.button {
|
|
||||||
width: 100% !important;
|
|
||||||
text-align: center !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* Attribute list ------------------------------ */
|
|
||||||
|
|
||||||
.attributes {
|
|
||||||
margin: 0 0 21px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes_content {
|
|
||||||
background-color: #F4F4F7;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attributes_item {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
/* Related Items ------------------------------ */
|
|
||||||
|
|
||||||
.related {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 25px 0 0 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related_item {
|
|
||||||
padding: 10px 0;
|
|
||||||
color: #CBCCCF;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related_item-title {
|
|
||||||
display: block;
|
|
||||||
margin: .5em 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related_item-thumb {
|
|
||||||
display: block;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.related_heading {
|
|
||||||
border-top: 1px solid #CBCCCF;
|
|
||||||
text-align: center;
|
|
||||||
padding: 25px 0 10px;
|
|
||||||
}
|
|
||||||
/* Discount Code ------------------------------ */
|
|
||||||
|
|
||||||
.discount {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 24px;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
background-color: #F4F4F7;
|
|
||||||
border: 2px dashed #CBCCCF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discount_heading {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discount_body {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
/* Social Icons ------------------------------ */
|
|
||||||
|
|
||||||
.social {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social td {
|
|
||||||
padding: 0;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social_icon {
|
|
||||||
height: 20px;
|
|
||||||
margin: 0 8px 10px 8px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
/* Data table ------------------------------ */
|
|
||||||
|
|
||||||
.purchase {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 35px 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_content {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 25px 0 0 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_item {
|
|
||||||
padding: 10px 0;
|
|
||||||
color: #51545E;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_heading {
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 1px solid #EAEAEC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_heading p {
|
|
||||||
margin: 0;
|
|
||||||
color: #85878E;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_footer {
|
|
||||||
padding-top: 15px;
|
|
||||||
border-top: 1px solid #EAEAEC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_total {
|
|
||||||
margin: 0;
|
|
||||||
text-align: right;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase_total--label {
|
|
||||||
padding: 0 15px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #F2F4F6;
|
|
||||||
color: #51545E;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #51545E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
background-color: #F2F4F6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-content {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
/* Masthead ----------------------- */
|
|
||||||
|
|
||||||
.email-masthead {
|
|
||||||
padding: 25px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-masthead_logo {
|
|
||||||
width: 94px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-masthead_name {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #A8AAAF;
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow: 0 1px 0 white;
|
|
||||||
}
|
|
||||||
/* Body ------------------------------ */
|
|
||||||
|
|
||||||
.email-body {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-body_inner {
|
|
||||||
width: 570px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 570px;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-footer {
|
|
||||||
width: 570px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 570px;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-footer p {
|
|
||||||
color: #A8AAAF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-action {
|
|
||||||
width: 100%;
|
|
||||||
margin: 30px auto;
|
|
||||||
padding: 0;
|
|
||||||
-premailer-width: 100%;
|
|
||||||
-premailer-cellpadding: 0;
|
|
||||||
-premailer-cellspacing: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-sub {
|
|
||||||
margin-top: 25px;
|
|
||||||
padding-top: 25px;
|
|
||||||
border-top: 1px solid #EAEAEC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-cell {
|
|
||||||
padding: 45px;
|
|
||||||
}
|
|
||||||
/*Media Queries ------------------------------ */
|
|
||||||
|
|
||||||
@media only screen and (max-width: 600px) {
|
|
||||||
.email-body_inner,
|
|
||||||
.email-footer {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body,
|
|
||||||
.email-body,
|
|
||||||
.email-body_inner,
|
|
||||||
.email-content,
|
|
||||||
.email-wrapper,
|
|
||||||
.email-masthead,
|
|
||||||
.email-footer {
|
|
||||||
background-color: #333333 !important;
|
|
||||||
color: #FFF !important;
|
|
||||||
}
|
|
||||||
p,
|
|
||||||
ul,
|
|
||||||
ol,
|
|
||||||
blockquote,
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
span,
|
|
||||||
.purchase_item {
|
|
||||||
color: #FFF !important;
|
|
||||||
}
|
|
||||||
.attributes_content,
|
|
||||||
.discount {
|
|
||||||
background-color: #222 !important;
|
|
||||||
}
|
|
||||||
.email-masthead_name {
|
|
||||||
text-shadow: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
supported-color-schemes: light dark;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<!--[if mso]>
|
|
||||||
<style type="text/css">
|
|
||||||
.f-fallback {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<![endif]-->
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<span class="preheader">Use this link to reset your password. The link is only valid for 12 hours.</span>
|
<span class="preheader">Use this link to reset your password. The link is only valid for 12 hours.</span>
|
||||||
@@ -525,31 +169,24 @@ if(!empty($mail)){
|
|||||||
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="email-masthead">
|
<td class="email-masthead">
|
||||||
<a href="https://auth.jakach.ch" class="f-fallback email-masthead_name">
|
<a href="https://auth.jakach.ch" class="f-fallback email-masthead_name">Jakach Login</a>
|
||||||
Jakach Login
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Email Body -->
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
|
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
|
||||||
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
<!-- Body content -->
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="content-cell">
|
<td class="content-cell">
|
||||||
<div class="f-fallback">
|
<div class="f-fallback">
|
||||||
<h1>Hi '.$username.',</h1>
|
<h1>Hi '.$html_username.',</h1>
|
||||||
<p>You recently requested to reset your password for your Jakach login account. Use the button below to reset it. <strong>This password reset is only valid for the next 12 hours.</strong></p>
|
<p>You recently requested to reset your password for your Jakach login account. Use the button below to reset it. <strong>This password reset is only valid for the next 12 hours.</strong></p>
|
||||||
<!-- Action -->
|
|
||||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<!-- Border based button
|
|
||||||
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
|
||||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="'.$link.'" class="f-fallback button button--green" target="_blank">Reset your password</a>
|
<a href="'.$html_link.'" class="f-fallback button button--green" target="_blank">Reset your password</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -559,19 +196,17 @@ if(!empty($mail)){
|
|||||||
<p><strong>Request Details:</strong></p>
|
<p><strong>Request Details:</strong></p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Date & Time:</strong> '.$date.'</li>
|
<li><strong>Date & Time:</strong> '.$date.'</li>
|
||||||
<li><strong>Device & Browser:</strong> '.$device.'</li>
|
<li><strong>Device & Browser:</strong> '.$html_device.'</li>
|
||||||
<li><strong>Account:</strong> '.$mail.'</li>
|
<li><strong>Account:</strong> '.$html_mail.'</li>
|
||||||
<li><strong>IP Address:</strong> '.$ip.'</li>
|
<li><strong>IP Address:</strong> '.$html_ip.'</li>
|
||||||
<li><strong>Location:</strong> '.$loc.'</li>
|
<li><strong>Location:</strong> '.$html_loc.'</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Thanks,
|
<p>Thanks,<br>The Jakach login team</p>
|
||||||
<br>The Jakach login team</p>
|
|
||||||
<!-- Sub copy -->
|
|
||||||
<table class="body-sub" role="presentation">
|
<table class="body-sub" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<p class="f-fallback sub">If you’re having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
<p class="f-fallback sub">If you are having trouble with the button above, copy and paste the URL below into your web browser.</p>
|
||||||
<p class="f-fallback sub">'.$link.'</p>
|
<p class="f-fallback sub">'.$html_link.'</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -586,10 +221,7 @@ if(!empty($mail)){
|
|||||||
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="content-cell" align="center">
|
<td class="content-cell" align="center">
|
||||||
<p class="f-fallback sub align-center">
|
<p class="f-fallback sub align-center">Jakach.ch<br>CH-Switzerland.</p>
|
||||||
Jakach.ch
|
|
||||||
<br>CH-Switzerland.
|
|
||||||
</p>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -16,21 +16,32 @@ if ($conn->connect_error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$search = trim($_GET['search'] ?? '');
|
$search = trim($_GET['search'] ?? '');
|
||||||
$sort = $_GET['sort'] ?? 'id';
|
$sort = $_GET['sort'] ?? '';
|
||||||
$order = strtoupper($_GET['order'] ?? 'ASC') === 'DESC' ? 'DESC' : 'ASC';
|
$order = strtoupper($_GET['order'] ?? 'ASC') === 'DESC' ? 'DESC' : 'ASC';
|
||||||
|
|
||||||
$allowedSorts = ['id', 'username'];
|
|
||||||
if (!in_array($sort, $allowedSorts)) {
|
|
||||||
$sort = 'id';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== '') {
|
||||||
$query = "SELECT id, username FROM users WHERE username LIKE ? ORDER BY $sort $order";
|
if ($sort === 'username') {
|
||||||
|
$query = $order === 'DESC'
|
||||||
|
? "SELECT id, username FROM users WHERE username LIKE ? ORDER BY username DESC"
|
||||||
|
: "SELECT id, username FROM users WHERE username LIKE ? ORDER BY username ASC";
|
||||||
|
} else {
|
||||||
|
$query = $order === 'DESC'
|
||||||
|
? "SELECT id, username FROM users WHERE username LIKE ? ORDER BY id DESC"
|
||||||
|
: "SELECT id, username FROM users WHERE username LIKE ? ORDER BY id ASC";
|
||||||
|
}
|
||||||
$stmt = $conn->prepare($query);
|
$stmt = $conn->prepare($query);
|
||||||
$like = '%' . $search . '%';
|
$like = '%' . $search . '%';
|
||||||
$stmt->bind_param('s', $like);
|
$stmt->bind_param('s', $like);
|
||||||
} else {
|
} else {
|
||||||
$query = "SELECT id, username FROM users ORDER BY $sort $order";
|
if ($sort === 'username') {
|
||||||
|
$query = $order === 'DESC'
|
||||||
|
? "SELECT id, username FROM users ORDER BY username DESC"
|
||||||
|
: "SELECT id, username FROM users ORDER BY username ASC";
|
||||||
|
} else {
|
||||||
|
$query = $order === 'DESC'
|
||||||
|
? "SELECT id, username FROM users ORDER BY id DESC"
|
||||||
|
: "SELECT id, username FROM users ORDER BY id ASC";
|
||||||
|
}
|
||||||
$stmt = $conn->prepare($query);
|
$stmt = $conn->prepare($query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
function secure_cookie_options(array $overrides = []): array
|
||||||
|
{
|
||||||
|
return array_merge([
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => true,
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
], $overrides);
|
||||||
|
}
|
||||||
|
|
||||||
function secure_session_start(): void
|
function secure_session_start(): void
|
||||||
{
|
{
|
||||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
session_set_cookie_params([
|
session_set_cookie_params(secure_cookie_options([
|
||||||
'lifetime' => 0,
|
'lifetime' => 0,
|
||||||
'path' => '/',
|
|
||||||
'domain' => '',
|
'domain' => '',
|
||||||
'secure' => true,
|
]));
|
||||||
'httponly' => true,
|
|
||||||
'samesite' => 'Lax',
|
|
||||||
]);
|
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
@@ -242,13 +248,9 @@ function clear_rate_limit(mysqli $conn, string $bucket, string $identifier = '')
|
|||||||
|
|
||||||
function set_secure_cookie(string $name, string $value, int $expires): void
|
function set_secure_cookie(string $name, string $value, int $expires): void
|
||||||
{
|
{
|
||||||
setcookie($name, $value, [
|
setcookie($name, $value, secure_cookie_options([
|
||||||
'expires' => $expires,
|
'expires' => $expires,
|
||||||
'path' => '/',
|
]));
|
||||||
'secure' => true,
|
|
||||||
'httponly' => true,
|
|
||||||
'samesite' => 'Lax',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function delete_cookie(string $name): void
|
function delete_cookie(string $name): void
|
||||||
@@ -280,6 +282,10 @@ function normalize_redirect_target(?string $target): string
|
|||||||
return '/account/';
|
return '/account/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (normalize_redirect_host($parts['host']) === null) {
|
||||||
|
return '/account/';
|
||||||
|
}
|
||||||
|
|
||||||
return $target;
|
return $target;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,12 +314,11 @@ function is_external_domain(string $url): ?string
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$host = parse_url($url, PHP_URL_HOST);
|
$host = normalize_redirect_host((string) parse_url($url, PHP_URL_HOST));
|
||||||
if ($host === null || $host === '') {
|
if ($host === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$host = strtolower($host);
|
|
||||||
if ($host === 'auth.jakach.ch' || str_ends_with($host, '.jakach.ch')) {
|
if ($host === 'auth.jakach.ch' || str_ends_with($host, '.jakach.ch')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -321,4 +326,23 @@ function is_external_domain(string $url): ?string
|
|||||||
return $host;
|
return $host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalize_redirect_host(string $host): ?string
|
||||||
|
{
|
||||||
|
$host = rtrim(strtolower(trim($host)), '.');
|
||||||
|
|
||||||
|
if ($host === '' || strlen($host) > 253 || preg_match('/[\x00-\x20\x7f<>"\'`]/', $host)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
||||||
|
return $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $host;
|
||||||
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
services:
|
services:
|
||||||
jakach-login-db:
|
jakach-login-db:
|
||||||
image: yobasystems/alpine-mariadb:latest
|
image: dhi.io/mariadb:12
|
||||||
container_name: jakach-login-db
|
container_name: jakach-login-db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: 1234
|
MARIADB_ROOT_PASSWORD: 1234
|
||||||
networks:
|
networks:
|
||||||
jakach-login-network:
|
jakach-login-network:
|
||||||
ipv4_address: 192.168.5.2
|
ipv4_address: 192.168.5.2
|
||||||
|
|||||||
+2
-2
@@ -7,8 +7,8 @@ RUN apt-get update && \
|
|||||||
pecl install redis && \
|
pecl install redis && \
|
||||||
docker-php-ext-enable redis
|
docker-php-ext-enable redis
|
||||||
|
|
||||||
# Enable SSL module for Apache
|
# Enable Apache modules
|
||||||
RUN a2enmod ssl
|
RUN a2enmod ssl headers
|
||||||
|
|
||||||
# Restart Apache to apply changes
|
# Restart Apache to apply changes
|
||||||
RUN service apache2 restart
|
RUN service apache2 restart
|
||||||
|
|||||||
Reference in New Issue
Block a user