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 |
+448
-18
@@ -2,15 +2,268 @@ name: Deploy
|
||||
|
||||
on:
|
||||
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:
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- security_scan
|
||||
- code_scan
|
||||
|
||||
steps:
|
||||
- name: Install SSH client
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
set -e
|
||||
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache openssh-client git bash
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
@@ -21,55 +274,232 @@ jobs:
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y openssh-clients git bash
|
||||
else
|
||||
echo "No supported package manager found"
|
||||
echo "Unsupported package manager"
|
||||
exit 1
|
||||
fi
|
||||
- name: Run deploy
|
||||
|
||||
- name: Deploy application
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SSH_KEY }}
|
||||
SSH_USER: ${{ vars.SSH_USER }}
|
||||
SSH_IP: ${{ vars.SSH_IP }}
|
||||
|
||||
GIT_USER: ${{ vars.GIT_USER }}
|
||||
GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
APP_DIR: /home/deploy/my-app
|
||||
GIT_REPO: Jakach/my-app.git
|
||||
GIT_BRANCH: main
|
||||
|
||||
run: |
|
||||
cat > deploy.sh <<'EOF'
|
||||
cat > deploy.sh <<'OUTER_EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
: "${SSH_KEY:?SSH_KEY is required}"
|
||||
: "${SSH_USER:?SSH_USER is required}"
|
||||
: "${SSH_IP:?SSH_IP is required}"
|
||||
: "${GIT_USER:?GIT_USER is required}"
|
||||
: "${GIT_TOKEN:?GIT_TOKEN is required}"
|
||||
|
||||
APP_DIR="/srv/systems/jakach-login"
|
||||
GIT_HOST="${GIT_HOST:-git.jakach.ch}"
|
||||
GIT_REPO="jakach/jakach-login.git"
|
||||
GIT_BRANCH="${GIT_BRANCH:-main}"
|
||||
REPO_NAME="$(basename "$GIT_REPO")"
|
||||
APP_DIR="/srv/systems/${REPO_NAME}"
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
|
||||
printf '%s\n' "$SSH_KEY" | tr -d '\r' > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
|
||||
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 IdentitiesOnly=yes \
|
||||
"$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
|
||||
|
||||
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"
|
||||
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 checkout "$GIT_BRANCH"
|
||||
|
||||
git pull origin "$GIT_BRANCH"
|
||||
docker compose down
|
||||
|
||||
# --------------------------------------------------
|
||||
# Create nginx reverse proxy entry
|
||||
# --------------------------------------------------
|
||||
|
||||
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
|
||||
REMOTE
|
||||
EOF
|
||||
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
|
||||
./deploy.sh
|
||||
@@ -14,19 +14,18 @@ Using Jakach Login is straightforward:
|
||||
|
||||
1. **Clone the repository:**
|
||||
```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:**
|
||||
```bash
|
||||
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
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
ServerTokens Prod
|
||||
ServerSignature Off
|
||||
TraceEnable Off
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName auth.jakach.ch
|
||||
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>
|
||||
Options FollowSymLinks
|
||||
AllowOverride All
|
||||
|
||||
@@ -818,8 +818,25 @@ function updatePasswordStrength() {
|
||||
data.domains.forEach(d => {
|
||||
const item = document.createElement('div');
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -881,9 +898,30 @@ function updatePasswordStrength() {
|
||||
'sessions_revoked': 'Sessions revoked',
|
||||
};
|
||||
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>' +
|
||||
(e.details ? '<br><small>' + e.details + '</small>' : '');
|
||||
|
||||
const header = document.createElement('div');
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -904,7 +942,11 @@ function updatePasswordStrength() {
|
||||
data.sessions.forEach(s => {
|
||||
const item = document.createElement('div');
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,12 @@ if ($method === 'GET') {
|
||||
$result = mysqli_stmt_get_result($stmt);
|
||||
$domains = [];
|
||||
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;
|
||||
}
|
||||
mysqli_stmt_close($stmt);
|
||||
|
||||
@@ -10,6 +10,11 @@ secure_session_start();
|
||||
require_same_origin_request();
|
||||
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
|
||||
include "../../config/config.php";
|
||||
$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);
|
||||
|
||||
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.
|
||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||
@@ -138,7 +143,7 @@ try {
|
||||
$getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification);
|
||||
|
||||
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.
|
||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||
|
||||
@@ -43,7 +43,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
// 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']);
|
||||
if ($email !== "" && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
echo json_encode([
|
||||
@@ -52,7 +52,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
]);
|
||||
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
|
||||
$id_check=0;
|
||||
@@ -63,8 +63,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
mysqli_stmt_store_result($stmt);
|
||||
mysqli_stmt_bind_result($stmt, $id_check);
|
||||
mysqli_stmt_fetch($stmt);
|
||||
if(mysqli_stmt_num_rows($stmt) > 0 && $username!==$name){
|
||||
//this username is allready taken
|
||||
if((mysqli_stmt_num_rows($stmt) > 0 && $username!==$name) || $name === ""){
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Username allready taken. Please choose another username.'
|
||||
|
||||
@@ -7,6 +7,12 @@ include "../utils/security.php";
|
||||
secure_session_start();
|
||||
require_same_origin_request();
|
||||
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";
|
||||
$conn = new mysqli($DB_SERVERNAME, $DB_USERNAME, $DB_PASSWORD,$DB_DATABASE);
|
||||
if ($conn->connect_error) {
|
||||
@@ -90,7 +96,7 @@ try {
|
||||
// Get create arguments
|
||||
$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification);
|
||||
header('Content-Type: application/json');
|
||||
print(json_encode($createArgs));
|
||||
print_json_response($createArgs);
|
||||
|
||||
// Save challenge to session or somewhere else if needed
|
||||
} else if ($fn === 'getGetArgs') {
|
||||
@@ -120,7 +126,7 @@ try {
|
||||
$getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification);
|
||||
|
||||
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.
|
||||
$_SESSION['challenge'] = $WebAuthn->getChallenge();
|
||||
|
||||
@@ -8,11 +8,11 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
exit;
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$domain = $input['domain'] ?? '';
|
||||
$send_to = normalize_redirect_target($_SESSION["end_url"] ?? "/account/");
|
||||
$domain = is_external_domain($send_to);
|
||||
|
||||
if ($domain === '' || !isset($_SESSION['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Missing domain or not logged in.']);
|
||||
if ($domain === null || !isset($_SESSION['id'])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Missing external domain or not logged in.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ if (!$user_found) {
|
||||
}
|
||||
//send telegram message
|
||||
$device = $_SERVER['HTTP_USER_AGENT'] ?? "";
|
||||
//$ip=$_SERVER["REMOTE_ADDR"];
|
||||
$forwarded_for = $_SERVER["HTTP_X_FORWARDED_FOR"] ?? $_SERVER["REMOTE_ADDR"] ?? "";
|
||||
$ip=trim(explode(",",$forwarded_for)[0]);
|
||||
$location=get_location_from_ip($ip);
|
||||
@@ -44,15 +43,20 @@ $token=bin2hex(random_bytes(128));
|
||||
$token_hash=auth_token_hash($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"
|
||||
. "You have requested the reset of your password here is your reset link.\n\n"
|
||||
. "*Link*: [click here]($link)\n\n"
|
||||
. "*Details of this request:*\n"
|
||||
. "• *Date&Time*: $date\n"
|
||||
. "• *Device&Browser*: $device\n"
|
||||
. "*Location*: ".$location["country"].", ".$location["state"].", ".$location["city"]."\n"
|
||||
. "• *Account*: ".$_SESSION["username"]."\n"
|
||||
. "• *IP*: $ip\n\n"
|
||||
. "• *Device&Browser*: $tg_device\n"
|
||||
. "*Location*: $tg_location\n"
|
||||
. "• *Account*: $tg_username\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"
|
||||
. "*Thank you for using Jakach login!*";
|
||||
|
||||
@@ -78,6 +82,12 @@ curl_close($ch);
|
||||
//send mail
|
||||
if(!empty($mail)){
|
||||
$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 = '
|
||||
<!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">
|
||||
@@ -89,433 +99,67 @@ if(!empty($mail)){
|
||||
<meta name="supported-color-schemes" content="light dark" />
|
||||
<title></title>
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
/* Base ------------------------------ */
|
||||
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
/* Type ------------------------------ */
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
body { width:100% !important; height:100%; margin:0; -webkit-text-size-adjust:none; }
|
||||
a { color:#3869D4; }
|
||||
a img { border:none; }
|
||||
td { word-break:break-word; }
|
||||
.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; }
|
||||
h2 { margin-top:0; color:#333333; font-size:16px; font-weight:bold; text-align:left; }
|
||||
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; }
|
||||
p.sub { font-size:13px; }
|
||||
.align-right { text-align:right; }
|
||||
.align-left { text-align:left; }
|
||||
.align-center { text-align:center; }
|
||||
.u-margin-bottom-none { margin-bottom:0; }
|
||||
.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; } }
|
||||
.attributes { margin:0 0 21px; }
|
||||
.attributes_content { background-color:#F4F4F7; padding:16px; }
|
||||
.attributes_item { padding:0; }
|
||||
.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 { 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 { width:auto; }
|
||||
.social td { padding:0; width:auto; }
|
||||
.social_icon { height:20px; margin:0 8px 10px 8px; padding:0; }
|
||||
.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; }
|
||||
.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; }
|
||||
.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 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>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
.f-fallback {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
<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">
|
||||
<tr>
|
||||
<td class="email-masthead">
|
||||
<a href="https://auth.jakach.ch" class="f-fallback email-masthead_name">
|
||||
Jakach Login
|
||||
</a>
|
||||
<a href="https://auth.jakach.ch" class="f-fallback email-masthead_name">Jakach Login</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Email Body -->
|
||||
<tr>
|
||||
<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">
|
||||
<!-- Body content -->
|
||||
<tr>
|
||||
<td class="content-cell">
|
||||
<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>
|
||||
<!-- Action -->
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<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">
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -559,19 +196,17 @@ if(!empty($mail)){
|
||||
<p><strong>Request Details:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Date & Time:</strong> '.$date.'</li>
|
||||
<li><strong>Device & Browser:</strong> '.$device.'</li>
|
||||
<li><strong>Account:</strong> '.$mail.'</li>
|
||||
<li><strong>IP Address:</strong> '.$ip.'</li>
|
||||
<li><strong>Location:</strong> '.$loc.'</li>
|
||||
<li><strong>Device & Browser:</strong> '.$html_device.'</li>
|
||||
<li><strong>Account:</strong> '.$html_mail.'</li>
|
||||
<li><strong>IP Address:</strong> '.$html_ip.'</li>
|
||||
<li><strong>Location:</strong> '.$html_loc.'</li>
|
||||
</ul>
|
||||
<p>Thanks,
|
||||
<br>The Jakach login team</p>
|
||||
<!-- Sub copy -->
|
||||
<p>Thanks,<br>The Jakach login team</p>
|
||||
<table class="body-sub" role="presentation">
|
||||
<tr>
|
||||
<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">'.$link.'</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">'.$html_link.'</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -586,10 +221,7 @@ if(!empty($mail)){
|
||||
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell" align="center">
|
||||
<p class="f-fallback sub align-center">
|
||||
Jakach.ch
|
||||
<br>CH-Switzerland.
|
||||
</p>
|
||||
<p class="f-fallback sub align-center">Jakach.ch<br>CH-Switzerland.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -16,21 +16,32 @@ if ($conn->connect_error) {
|
||||
}
|
||||
|
||||
$search = trim($_GET['search'] ?? '');
|
||||
$sort = $_GET['sort'] ?? 'id';
|
||||
$sort = $_GET['sort'] ?? '';
|
||||
$order = strtoupper($_GET['order'] ?? 'ASC') === 'DESC' ? 'DESC' : 'ASC';
|
||||
|
||||
$allowedSorts = ['id', 'username'];
|
||||
if (!in_array($sort, $allowedSorts)) {
|
||||
$sort = 'id';
|
||||
}
|
||||
|
||||
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);
|
||||
$like = '%' . $search . '%';
|
||||
$stmt->bind_param('s', $like);
|
||||
} 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
<?php
|
||||
|
||||
function secure_cookie_options(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
function secure_session_start(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
session_set_cookie_params([
|
||||
session_set_cookie_params(secure_cookie_options([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
]));
|
||||
|
||||
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
|
||||
{
|
||||
setcookie($name, $value, [
|
||||
setcookie($name, $value, secure_cookie_options([
|
||||
'expires' => $expires,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
]));
|
||||
}
|
||||
|
||||
function delete_cookie(string $name): void
|
||||
@@ -280,6 +282,10 @@ function normalize_redirect_target(?string $target): string
|
||||
return '/account/';
|
||||
}
|
||||
|
||||
if (normalize_redirect_host($parts['host']) === null) {
|
||||
return '/account/';
|
||||
}
|
||||
|
||||
return $target;
|
||||
}
|
||||
|
||||
@@ -308,12 +314,11 @@ function is_external_domain(string $url): ?string
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = parse_url($url, PHP_URL_HOST);
|
||||
if ($host === null || $host === '') {
|
||||
$host = normalize_redirect_host((string) parse_url($url, PHP_URL_HOST));
|
||||
if ($host === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$host = strtolower($host);
|
||||
if ($host === 'auth.jakach.ch' || str_ends_with($host, '.jakach.ch')) {
|
||||
return null;
|
||||
}
|
||||
@@ -321,4 +326,23 @@ function is_external_domain(string $url): ?string
|
||||
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:
|
||||
jakach-login-db:
|
||||
image: yobasystems/alpine-mariadb:latest
|
||||
image: dhi.io/mariadb:12
|
||||
container_name: jakach-login-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: 1234
|
||||
MARIADB_ROOT_PASSWORD: 1234
|
||||
networks:
|
||||
jakach-login-network:
|
||||
ipv4_address: 192.168.5.2
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ RUN apt-get update && \
|
||||
pecl install redis && \
|
||||
docker-php-ext-enable redis
|
||||
|
||||
# Enable SSL module for Apache
|
||||
RUN a2enmod ssl
|
||||
# Enable Apache modules
|
||||
RUN a2enmod ssl headers
|
||||
|
||||
# Restart Apache to apply changes
|
||||
RUN service apache2 restart
|
||||
|
||||
Reference in New Issue
Block a user