Compare commits
5 Commits
66c30ac048
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dd467de5f | |||
| f7ea9cde45 | |||
| 2fcb2fd219 | |||
| 5ee02c34f0 | |||
| aea3aa47ae |
@@ -5,16 +5,7 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch: # manual re-deploy of the current main
|
||||||
inputs:
|
|
||||||
environment:
|
|
||||||
description: 'Deployment target'
|
|
||||||
required: true
|
|
||||||
default: 'development'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- production
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
HUGO_VERSION: '0.164.0'
|
HUGO_VERSION: '0.164.0'
|
||||||
@@ -36,35 +27,22 @@ jobs:
|
|||||||
sudo dpkg -i /tmp/hugo.deb
|
sudo dpkg -i /tmp/hugo.deb
|
||||||
hugo version
|
hugo version
|
||||||
|
|
||||||
- name: Resolve target
|
# baseURL comes from hugo.toml - that is the single place to change when
|
||||||
id: target
|
# the site moves to its final domain.
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] \
|
|
||||||
&& [ "${{ inputs.environment }}" = "production" ]; then
|
|
||||||
echo "base_url=https://motorradclub-giebelwald.de/" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "deploy_path=${{ secrets.DEPLOY_PATH_PROD }}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "name=production" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "base_url=https://mcg.luxick.de/" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "deploy_path=${{ secrets.DEPLOY_PATH_DEV }}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "name=development" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
hugo --gc --minify --baseURL "${{ steps.target.outputs.base_url }}"
|
hugo --gc --minify
|
||||||
|
|
||||||
# Pull requests are built to catch breakage, but never published.
|
# Pull requests are built to catch breakage, but never published.
|
||||||
- name: Deploy to ${{ steps.target.outputs.name }}
|
- name: Deploy
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
env:
|
env:
|
||||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
|
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
|
||||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||||
DEPLOY_PATH: ${{ steps.target.outputs.deploy_path }}
|
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,6 @@
|
|||||||
|
|
||||||
# Local scratch space
|
# Local scratch space
|
||||||
/.cache/
|
/.cache/
|
||||||
|
|
||||||
|
# Local deploy credentials
|
||||||
|
/scripts/deploy.env
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
Static website of the Motorradclub Giebelwald e.V., built with [Hugo](https://gohugo.io).
|
Static website of the Motorradclub Giebelwald e.V., built with [Hugo](https://gohugo.io).
|
||||||
|
|
||||||
- Development: <https://mcg.luxick.de>
|
Currently served at <https://mcg.luxick.de>. Once the site is signed off, the
|
||||||
- Production: <https://motorradclub-giebelwald.de>
|
main domain <https://motorradclub-giebelwald.de> will be pointed at the same
|
||||||
|
server and the old host retired — see [Changing the domain](#changing-the-domain).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -91,12 +92,37 @@ layouts/ baseof, home, list, page, termine
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
`.gitea/workflows/deploy.yml` builds with Hugo and rsyncs `public/` over SSH.
|
`.gitea/workflows/deploy.yml` builds with Hugo and rsyncs `public/` over SSH to
|
||||||
|
the VPS webroot.
|
||||||
|
|
||||||
- Push to `main` → deploys to **mcg.luxick.de**
|
- Push to `main` → builds and deploys
|
||||||
- Manual *Run workflow* with `environment: production` → deploys to
|
- *Run workflow* → manual re-deploy of the current `main`
|
||||||
**motorradclub-giebelwald.de**
|
- Pull requests are built to catch breakage, but never published
|
||||||
- Pull requests are built but never published
|
|
||||||
|
There is one deploy target. For development, run the site locally with
|
||||||
|
`hugo server`.
|
||||||
|
|
||||||
|
### Deploying from a workstation
|
||||||
|
|
||||||
|
`scripts/deploy.sh` does the same thing without CI — useful while the Gitea
|
||||||
|
runner is not set up. It needs `rsync`, so run it from WSL or another Linux
|
||||||
|
shell, not Git Bash or PowerShell.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp scripts/deploy.env.example scripts/deploy.env # then fill it in (gitignored)
|
||||||
|
./scripts/deploy.sh --dry-run # preview, upload nothing
|
||||||
|
./scripts/deploy.sh # build, preview, confirm, deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `--dry-run` | Stop after the preview |
|
||||||
|
| `--yes` | Skip the confirmation prompt |
|
||||||
|
| `--skip-build` | Deploy the existing `public/`, e.g. after building on Windows |
|
||||||
|
|
||||||
|
The script previews every change and reports how many files would be **deleted**
|
||||||
|
on the server before asking to continue. Keep the SSH key inside the WSL
|
||||||
|
filesystem — a key under `/mnt/c` cannot hold `0600`, and ssh will reject it.
|
||||||
|
|
||||||
### Required secrets
|
### Required secrets
|
||||||
|
|
||||||
@@ -105,12 +131,21 @@ Set these in the Gitea repository settings:
|
|||||||
| Secret | Example |
|
| Secret | Example |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `SSH_PRIVATE_KEY` | Deploy key with write access to the webroot |
|
| `SSH_PRIVATE_KEY` | Deploy key with write access to the webroot |
|
||||||
| `SSH_KNOWN_HOSTS` | Output of `ssh-keyscan <host>` |
|
| `SSH_KNOWN_HOSTS` | Verified output of `ssh-keyscan <host>` |
|
||||||
| `DEPLOY_HOST` | `server.example.de` |
|
| `DEPLOY_HOST` | `server.example.de` |
|
||||||
| `DEPLOY_USER` | `www-deploy` |
|
| `DEPLOY_USER` | `www-deploy` |
|
||||||
| `DEPLOY_PATH_DEV` | `/var/www/mcg-dev` |
|
| `DEPLOY_PATH` | `/var/www/mcg` |
|
||||||
| `DEPLOY_PATH_PROD` | `/var/www/mcg` |
|
|
||||||
|
|
||||||
rsync runs with `--delete`, so each `DEPLOY_PATH_*` must point at a directory
|
rsync runs with `--delete`, so `DEPLOY_PATH` must point at a directory owned
|
||||||
owned solely by this site. The workflow refuses to deploy if the path is unset,
|
solely by this site. The workflow refuses to deploy if the path is unset, too
|
||||||
too shallow, or if the build produced no `index.html`.
|
shallow, or if the build produced no `index.html`.
|
||||||
|
|
||||||
|
### Changing the domain
|
||||||
|
|
||||||
|
`baseURL` in `hugo.toml` is the only place the live domain is configured; it
|
||||||
|
feeds canonical URLs, `og:url`, the RSS feed and the sitemap. To cut over:
|
||||||
|
|
||||||
|
1. Point the DNS record at the VPS.
|
||||||
|
2. Add the domain to the nginx `server_name` and reissue the certificate.
|
||||||
|
3. Change `baseURL` in `hugo.toml` and push — the deploy rebuilds every
|
||||||
|
absolute URL.
|
||||||
|
|||||||
+7
-27
@@ -109,20 +109,14 @@ h3 { font-size: 1.2rem; margin: var(--space-3) 0 var(--space-1); }
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-toggle { position: absolute; opacity: 0; pointer-events: none; }
|
/* No hamburger: the five menu items need roughly 316px at the smallest gap, so
|
||||||
|
they fit on one line on any modern phone. The column gap scales with the
|
||||||
.nav-toggle-label {
|
viewport, and flex-wrap drops to a second row in the extreme case (very
|
||||||
display: none;
|
narrow screen or large text zoom) rather than hiding anything. */
|
||||||
padding: var(--space-2) 0;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-list {
|
.nav-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--space-3);
|
gap: 0 clamp(0.75rem, 0.2rem + 2.2vw, 1.5rem);
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -130,30 +124,16 @@ h3 { font-size: 1.2rem; margin: var(--space-3) 0 var(--space-1); }
|
|||||||
|
|
||||||
.nav-list a {
|
.nav-list a {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.85rem 0;
|
padding: 0.85rem 0; /* keeps a ~44px touch target */
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.95rem;
|
font-size: clamp(0.875rem, 0.83rem + 0.2vw, 0.95rem);
|
||||||
border-bottom: 3px solid transparent;
|
border-bottom: 3px solid transparent;
|
||||||
}
|
}
|
||||||
.nav-list a:hover { color: #fff; border-bottom-color: rgb(255 255 255 / 45%); }
|
.nav-list a:hover { color: #fff; border-bottom-color: rgb(255 255 255 / 45%); }
|
||||||
.nav-list a[aria-current="page"] { border-bottom-color: #fff; }
|
.nav-list a[aria-current="page"] { border-bottom-color: #fff; }
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.nav-toggle-label { display: block; }
|
|
||||||
.nav-list {
|
|
||||||
display: none;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0;
|
|
||||||
padding-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
.nav-toggle:checked ~ .nav-list { display: flex; }
|
|
||||||
.nav-list a { padding: 0.6rem 0; border-bottom: 0; }
|
|
||||||
.nav-list a[aria-current="page"] { text-decoration: underline; }
|
|
||||||
.nav-toggle:focus-visible ~ .nav-toggle-label { outline: 3px solid #fff; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------- main */
|
/* ----------------------------------------------------------------- main */
|
||||||
|
|
||||||
.site-main { padding-block: var(--space-4) var(--space-5); }
|
.site-main { padding-block: var(--space-4) var(--space-5); }
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 488 KiB |
@@ -1,3 +1,5 @@
|
|||||||
|
# The live domain. Change this one line when the site moves to
|
||||||
|
# https://motorradclub-giebelwald.de/ - it feeds canonical URLs, RSS and sitemap.
|
||||||
baseURL = 'https://mcg.luxick.de/'
|
baseURL = 'https://mcg.luxick.de/'
|
||||||
title = 'Motorradclub Giebelwald e.V.'
|
title = 'Motorradclub Giebelwald e.V.'
|
||||||
locale = 'de-DE'
|
locale = 'de-DE'
|
||||||
@@ -46,10 +48,6 @@ buildFuture = false
|
|||||||
name = 'Archiv'
|
name = 'Archiv'
|
||||||
pageRef = '/archiv'
|
pageRef = '/archiv'
|
||||||
weight = 40
|
weight = 40
|
||||||
[[menus.main]]
|
|
||||||
name = 'Impressum'
|
|
||||||
pageRef = '/impressum'
|
|
||||||
weight = 50
|
|
||||||
|
|
||||||
[outputs]
|
[outputs]
|
||||||
home = ['html', 'rss', 'sitemap']
|
home = ['html', 'rss', 'sitemap']
|
||||||
|
|||||||
@@ -22,10 +22,6 @@
|
|||||||
|
|
||||||
<nav class="site-nav" aria-label="Hauptnavigation">
|
<nav class="site-nav" aria-label="Hauptnavigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<input type="checkbox" id="nav-toggle" class="nav-toggle">
|
|
||||||
<label for="nav-toggle" class="nav-toggle-label">
|
|
||||||
<span aria-hidden="true">☰</span> Menü
|
|
||||||
</label>
|
|
||||||
<ul class="nav-list">
|
<ul class="nav-list">
|
||||||
<li><a href="{{ "/" | relURL }}"{{ if .IsHome }} aria-current="page"{{ end }}>Start</a></li>
|
<li><a href="{{ "/" | relURL }}"{{ if .IsHome }} aria-current="page"{{ end }}>Start</a></li>
|
||||||
{{ range site.Menus.main }}
|
{{ range site.Menus.main }}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Copy to scripts/deploy.env (gitignored) and fill in.
|
||||||
|
# Must match the values used by the Gitea Actions secrets.
|
||||||
|
|
||||||
|
DEPLOY_HOST=server.example.de
|
||||||
|
DEPLOY_USER=www-deploy
|
||||||
|
DEPLOY_PATH=/var/www/mcg
|
||||||
|
|
||||||
|
# Private half of the deploy key. Keep it inside the WSL filesystem —
|
||||||
|
# a key under /mnt/c or /mnt/d cannot hold 0600 and ssh will reject it.
|
||||||
|
SSH_KEY=$HOME/.ssh/mcg-deploy
|
||||||
Executable
+139
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Manual deploy from a workstation, for use until the Gitea runner is up.
|
||||||
|
# Mirrors .gitea/workflows/deploy.yml: same build, same rsync, same guards.
|
||||||
|
#
|
||||||
|
# ./scripts/deploy.sh build, preview the changes, confirm, deploy
|
||||||
|
# ./scripts/deploy.sh --dry-run stop after the preview
|
||||||
|
# ./scripts/deploy.sh --yes skip the confirmation prompt
|
||||||
|
# ./scripts/deploy.sh --skip-build deploy whatever is already in public/
|
||||||
|
#
|
||||||
|
# Configuration is read from scripts/deploy.env (gitignored); see
|
||||||
|
# scripts/deploy.env.example. Environment variables take precedence.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
DRY_RUN=0
|
||||||
|
ASSUME_YES=0
|
||||||
|
SKIP_BUILD=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dry-run) DRY_RUN=1 ;;
|
||||||
|
--yes|-y) ASSUME_YES=1 ;;
|
||||||
|
--skip-build) SKIP_BUILD=1 ;;
|
||||||
|
-h|--help) sed -n '3,13p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||||
|
*) echo "unknown option: $arg (try --help)" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
die() { echo "error: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- config ---
|
||||||
|
|
||||||
|
CONFIG="${DEPLOY_ENV_FILE:-scripts/deploy.env}"
|
||||||
|
if [ -f "$CONFIG" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$CONFIG"
|
||||||
|
elif [ -z "${DEPLOY_HOST:-}" ]; then
|
||||||
|
die "no $CONFIG and no DEPLOY_HOST in the environment.
|
||||||
|
Copy scripts/deploy.env.example to $CONFIG and fill it in."
|
||||||
|
fi
|
||||||
|
|
||||||
|
: "${DEPLOY_HOST:?set DEPLOY_HOST}"
|
||||||
|
: "${DEPLOY_USER:?set DEPLOY_USER}"
|
||||||
|
: "${DEPLOY_PATH:?set DEPLOY_PATH}"
|
||||||
|
SSH_KEY="${SSH_KEY:-$HOME/.ssh/mcg-deploy}"
|
||||||
|
|
||||||
|
# rsync runs with --delete, so the destination becomes an exact mirror of
|
||||||
|
# public/. Refuse to run against an unset, root or shallow path.
|
||||||
|
case "$DEPLOY_PATH" in
|
||||||
|
""|"/"|"/root"|"/home"|"/var"|"/etc"|"/usr"|"/srv"|"/opt")
|
||||||
|
die "DEPLOY_PATH is unsafe: '$DEPLOY_PATH'" ;;
|
||||||
|
esac
|
||||||
|
if [ "$(printf '%s' "$DEPLOY_PATH" | tr -cd '/' | wc -c)" -lt 2 ]; then
|
||||||
|
die "DEPLOY_PATH '$DEPLOY_PATH' is too shallow to delete into"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------- dependencies ---
|
||||||
|
|
||||||
|
for cmd in rsync ssh; do
|
||||||
|
command -v "$cmd" >/dev/null 2>&1 || die "'$cmd' is not installed.
|
||||||
|
Run this script from WSL or another Linux shell — Git Bash and
|
||||||
|
PowerShell do not ship rsync."
|
||||||
|
done
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- ssh key ---
|
||||||
|
|
||||||
|
[ -f "$SSH_KEY" ] || die "ssh key not found: $SSH_KEY"
|
||||||
|
|
||||||
|
case "$SSH_KEY" in
|
||||||
|
/mnt/*)
|
||||||
|
die "the ssh key is on the Windows filesystem ($SSH_KEY).
|
||||||
|
WSL cannot hold 0600 there, so ssh will reject the key. Copy it into
|
||||||
|
the WSL home directory instead:
|
||||||
|
cp '$SSH_KEY' ~/.ssh/mcg-deploy && chmod 600 ~/.ssh/mcg-deploy" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
perms="$(stat -c '%a' "$SSH_KEY" 2>/dev/null || echo '')"
|
||||||
|
if [ -n "$perms" ] && [ "$perms" != "600" ] && [ "$perms" != "400" ]; then
|
||||||
|
echo "==> Tightening permissions on $SSH_KEY (was $perms)"
|
||||||
|
chmod 600 "$SSH_KEY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- build ---
|
||||||
|
|
||||||
|
if [ "$SKIP_BUILD" -eq 0 ]; then
|
||||||
|
command -v hugo >/dev/null 2>&1 || die "hugo is not on PATH.
|
||||||
|
Either install Hugo 0.164.0 in WSL, or build on the Windows side and
|
||||||
|
re-run this script with --skip-build."
|
||||||
|
echo "==> Building"
|
||||||
|
rm -rf public
|
||||||
|
hugo --gc --minify
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f public/index.html ] || \
|
||||||
|
die "public/index.html missing - refusing to deploy an empty build"
|
||||||
|
|
||||||
|
# -------------------------------------------------------------- deploy ---
|
||||||
|
|
||||||
|
# No StrictHostKeyChecking override: ssh falls back to its default (ask), so it
|
||||||
|
# uses your personal ~/.ssh/known_hosts and prompts once on first connect. CI
|
||||||
|
# pins it to yes instead, because it has no prompt and a known_hosts secret.
|
||||||
|
SSH_CMD="ssh -i $SSH_KEY -o IdentitiesOnly=yes"
|
||||||
|
TARGET="${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/"
|
||||||
|
RSYNC_OPTS=(-az --delete --checksum)
|
||||||
|
|
||||||
|
preview="$(mktemp)"
|
||||||
|
trap 'rm -f "$preview"' EXIT
|
||||||
|
|
||||||
|
echo "==> Previewing changes against $TARGET"
|
||||||
|
rsync "${RSYNC_OPTS[@]}" --itemize-changes --dry-run \
|
||||||
|
-e "$SSH_CMD" public/ "$TARGET" | tee "$preview"
|
||||||
|
|
||||||
|
deletions="$(grep -c '^\*deleting' "$preview" || true)"
|
||||||
|
updates="$(grep -cv '^\*deleting' "$preview" || true)"
|
||||||
|
echo
|
||||||
|
echo "==> $updates file(s) to send, $deletions to delete on the server"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo "==> Dry run only, nothing was uploaded."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ASSUME_YES" -eq 0 ]; then
|
||||||
|
printf 'Continue? [y/N] '
|
||||||
|
read -r reply
|
||||||
|
case "$reply" in
|
||||||
|
[yY]|[yY][eE][sS]) ;;
|
||||||
|
*) echo "aborted"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Deploying"
|
||||||
|
rsync "${RSYNC_OPTS[@]}" --human-readable --stats \
|
||||||
|
-e "$SSH_CMD" public/ "$TARGET"
|
||||||
|
|
||||||
|
echo "==> Done"
|
||||||
Reference in New Issue
Block a user