Initial infra setup: Terraform, Ansible, backup roles
This commit is contained in:
7
ansible/gitea.yml
Normal file
7
ansible/gitea.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Set up Gitea
|
||||
hosts: gitea
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- gitea
|
||||
12
ansible/group_vars/gitea/vault.yml.example
Normal file
12
ansible/group_vars/gitea/vault.yml.example
Normal file
@@ -0,0 +1,12 @@
|
||||
# Copy to vault.yml and encrypt with: ansible-vault encrypt vault.yml
|
||||
# Reference in playbook with: ansible-playbook --ask-vault-pass gitea.yml
|
||||
---
|
||||
cf_origin_cert: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
<paste cert from Cloudflare here>
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
cf_origin_key: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
<paste key from Cloudflare here>
|
||||
-----END PRIVATE KEY-----
|
||||
6
ansible/group_vars/infra/vars.yml
Normal file
6
ansible/group_vars/infra/vars.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
gitea_domain: "git.lunarfront.tech"
|
||||
gitea_instance_url: "https://git.lunarfront.tech"
|
||||
vaultwarden_domain: "vault.lunarfront.tech"
|
||||
vaultwarden_signups_allowed: false
|
||||
gitea_db_type: postgres
|
||||
10
ansible/infra.yml
Normal file
10
ansible/infra.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: Set up infra server
|
||||
hosts: infra
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- gitea
|
||||
- gitea-runner
|
||||
- vaultwarden
|
||||
- backup
|
||||
2
ansible/inventory.ini.example
Normal file
2
ansible/inventory.ini.example
Normal file
@@ -0,0 +1,2 @@
|
||||
[gitea]
|
||||
<GITEA_IP> ansible_user=root ansible_ssh_private_key_file=~/.ssh/id_ed25519
|
||||
14
ansible/roles/backup/defaults/main.yml
Normal file
14
ansible/roles/backup/defaults/main.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
backup_bucket: "lunarfront-infra"
|
||||
backup_spaces_region: "nyc3"
|
||||
backup_schedule: "0 3 * * *" # 3am daily
|
||||
|
||||
backup_dirs:
|
||||
- src: /var/lib/gitea/data
|
||||
dest: backups/gitea
|
||||
- src: /var/lib/vaultwarden/data
|
||||
dest: backups/vaultwarden
|
||||
|
||||
# Set in vault
|
||||
spaces_access_key: ""
|
||||
spaces_secret_key: ""
|
||||
38
ansible/roles/backup/tasks/main.yml
Normal file
38
ansible/roles/backup/tasks/main.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
- name: Install rclone
|
||||
apt:
|
||||
name: rclone
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Create rclone config directory
|
||||
file:
|
||||
path: /root/.config/rclone
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Deploy rclone config
|
||||
template:
|
||||
src: rclone.conf.j2
|
||||
dest: /root/.config/rclone/rclone.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
|
||||
- name: Deploy backup script
|
||||
template:
|
||||
src: backup.sh.j2
|
||||
dest: /usr/local/bin/backup.sh
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Schedule daily backup cron job
|
||||
cron:
|
||||
name: "lunarfront backup"
|
||||
job: "/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1"
|
||||
minute: "0"
|
||||
hour: "3"
|
||||
user: root
|
||||
34
ansible/roles/backup/templates/backup.sh.j2
Normal file
34
ansible/roles/backup/templates/backup.sh.j2
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
MONTH=$(date +%Y-%m)
|
||||
DAY_OF_MONTH=$(date +%d)
|
||||
|
||||
echo "[$(date)] Starting backup"
|
||||
|
||||
{% for item in backup_dirs %}
|
||||
echo "[$(date)] Backing up {{ item.src }}"
|
||||
|
||||
# Daily backup — kept for 30 days
|
||||
rclone sync {{ item.src }} spaces://{{ backup_bucket }}/{{ item.dest }}/daily/${DATE} \
|
||||
--s3-acl private
|
||||
|
||||
# Monthly backup on the 1st — kept for 12 months
|
||||
if [ "${DAY_OF_MONTH}" = "01" ]; then
|
||||
rclone sync {{ item.src }} spaces://{{ backup_bucket }}/{{ item.dest }}/monthly/${MONTH} \
|
||||
--s3-acl private
|
||||
echo "[$(date)] Monthly backup complete for {{ item.src }}"
|
||||
fi
|
||||
|
||||
# Prune daily backups older than 30 days
|
||||
rclone delete spaces://{{ backup_bucket }}/{{ item.dest }}/daily \
|
||||
--min-age 30d --s3-acl private
|
||||
|
||||
# Prune monthly backups older than 12 months
|
||||
rclone delete spaces://{{ backup_bucket }}/{{ item.dest }}/monthly \
|
||||
--min-age 365d --s3-acl private
|
||||
|
||||
{% endfor %}
|
||||
|
||||
echo "[$(date)] Backup complete"
|
||||
7
ansible/roles/backup/templates/rclone.conf.j2
Normal file
7
ansible/roles/backup/templates/rclone.conf.j2
Normal file
@@ -0,0 +1,7 @@
|
||||
[spaces]
|
||||
type = s3
|
||||
provider = DigitalOcean
|
||||
access_key_id = {{ spaces_access_key }}
|
||||
secret_access_key = {{ spaces_secret_key }}
|
||||
endpoint = nyc3.digitaloceanspaces.com
|
||||
acl = private
|
||||
10
ansible/roles/gitea-runner/defaults/main.yml
Normal file
10
ansible/roles/gitea-runner/defaults/main.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
gitea_runner_version: "0.2.11"
|
||||
gitea_runner_data_dir: /var/lib/gitea-runner
|
||||
gitea_instance_url: "https://git.example.com"
|
||||
|
||||
# Generate in Gitea: Site Admin → Actions → Runners → Create new runner
|
||||
gitea_runner_token: ""
|
||||
|
||||
gitea_runner_name: "{{ inventory_hostname }}"
|
||||
gitea_runner_labels: "ubuntu-latest:docker://node:20"
|
||||
6
ansible/roles/gitea-runner/handlers/main.yml
Normal file
6
ansible/roles/gitea-runner/handlers/main.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Restart gitea-runner
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ gitea_runner_data_dir }}"
|
||||
state: present
|
||||
recreate: always
|
||||
27
ansible/roles/gitea-runner/tasks/main.yml
Normal file
27
ansible/roles/gitea-runner/tasks/main.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
- name: Create gitea-runner data directory
|
||||
file:
|
||||
path: "{{ gitea_runner_data_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Deploy docker-compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ gitea_runner_data_dir }}/docker-compose.yml"
|
||||
mode: "0600"
|
||||
notify: Restart gitea-runner
|
||||
|
||||
- name: Deploy runner config
|
||||
template:
|
||||
src: config.yml.j2
|
||||
dest: "{{ gitea_runner_data_dir }}/config.yml"
|
||||
mode: "0600"
|
||||
notify: Restart gitea-runner
|
||||
|
||||
- name: Start gitea-runner
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ gitea_runner_data_dir }}"
|
||||
state: present
|
||||
17
ansible/roles/gitea-runner/templates/config.yml.j2
Normal file
17
ansible/roles/gitea-runner/templates/config.yml.j2
Normal file
@@ -0,0 +1,17 @@
|
||||
log:
|
||||
level: info
|
||||
|
||||
runner:
|
||||
file: /data/.runner
|
||||
capacity: 2 # max concurrent jobs — lower if droplet is under load
|
||||
labels:
|
||||
- "ubuntu-latest:docker://node:20"
|
||||
- "ubuntu-22.04:docker://node:20"
|
||||
|
||||
cache:
|
||||
enabled: true
|
||||
dir: /data/cache
|
||||
|
||||
container:
|
||||
network: bridge
|
||||
force_pull: false # reuse cached images to speed up builds
|
||||
14
ansible/roles/gitea-runner/templates/docker-compose.yml.j2
Normal file
14
ansible/roles/gitea-runner/templates/docker-compose.yml.j2
Normal file
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
gitea-runner:
|
||||
image: gitea/act_runner:{{ gitea_runner_version }}
|
||||
container_name: gitea-runner
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- {{ gitea_runner_data_dir }}/config.yml:/config.yml
|
||||
- {{ gitea_runner_data_dir }}/data:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock # allows runner to spin up job containers
|
||||
environment:
|
||||
CONFIG_FILE: /config.yml
|
||||
GITEA_INSTANCE_URL: "{{ gitea_instance_url }}"
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN: "{{ gitea_runner_token }}"
|
||||
GITEA_RUNNER_NAME: "{{ gitea_runner_name }}"
|
||||
10
ansible/roles/gitea/defaults/main.yml
Normal file
10
ansible/roles/gitea/defaults/main.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
gitea_version: "1.22.3"
|
||||
gitea_domain: "git.example.com"
|
||||
gitea_http_port: 3000
|
||||
gitea_ssh_port: 2222
|
||||
gitea_data_dir: /var/lib/gitea
|
||||
|
||||
# Cloudflare Origin Certificate
|
||||
cf_origin_cert: ""
|
||||
cf_origin_key: ""
|
||||
11
ansible/roles/gitea/handlers/main.yml
Normal file
11
ansible/roles/gitea/handlers/main.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Restart gitea
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ gitea_data_dir }}"
|
||||
state: present
|
||||
recreate: always
|
||||
|
||||
- name: Reload nginx
|
||||
systemd:
|
||||
name: nginx
|
||||
state: reloaded
|
||||
88
ansible/roles/gitea/tasks/main.yml
Normal file
88
ansible/roles/gitea/tasks/main.yml
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
- name: Install dependencies
|
||||
apt:
|
||||
name: [nginx, docker.io, docker-compose-v2]
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Enable and start Docker
|
||||
systemd:
|
||||
name: docker
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Create gitea data directory
|
||||
file:
|
||||
path: "{{ gitea_data_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Deploy docker-compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ gitea_data_dir }}/docker-compose.yml"
|
||||
mode: "0600"
|
||||
notify: Restart gitea
|
||||
|
||||
- name: Start gitea
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ gitea_data_dir }}"
|
||||
state: present
|
||||
|
||||
# ─── Cloudflare Origin Certificate ───────────────────────────────────────────
|
||||
|
||||
- name: Create SSL directory
|
||||
file:
|
||||
path: /etc/nginx/ssl
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Install Cloudflare origin certificate
|
||||
copy:
|
||||
content: "{{ cf_origin_cert }}"
|
||||
dest: /etc/nginx/ssl/cf-origin.pem
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Install Cloudflare origin key
|
||||
copy:
|
||||
content: "{{ cf_origin_key }}"
|
||||
dest: /etc/nginx/ssl/cf-origin.key
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
notify: Reload nginx
|
||||
|
||||
# ─── nginx ────────────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Deploy nginx config
|
||||
template:
|
||||
src: nginx.conf.j2
|
||||
dest: /etc/nginx/sites-available/gitea
|
||||
mode: "0644"
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Enable nginx site
|
||||
file:
|
||||
src: /etc/nginx/sites-available/gitea
|
||||
dest: /etc/nginx/sites-enabled/gitea
|
||||
state: link
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Remove nginx default site
|
||||
file:
|
||||
path: /etc/nginx/sites-enabled/default
|
||||
state: absent
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Ensure nginx is started
|
||||
systemd:
|
||||
name: nginx
|
||||
enabled: true
|
||||
state: started
|
||||
29
ansible/roles/gitea/templates/docker-compose.yml.j2
Normal file
29
ansible/roles/gitea/templates/docker-compose.yml.j2
Normal file
@@ -0,0 +1,29 @@
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:{{ gitea_version }}
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- {{ gitea_data_dir }}/data:/data
|
||||
environment:
|
||||
USER_UID: 1000
|
||||
USER_GID: 1000
|
||||
GITEA__server__DOMAIN: "{{ gitea_domain }}"
|
||||
GITEA__server__ROOT_URL: "https://{{ gitea_domain }}/"
|
||||
GITEA__server__HTTP_PORT: 3000
|
||||
GITEA__server__SSH_DOMAIN: "{{ gitea_domain }}"
|
||||
GITEA__server__SSH_PORT: "{{ gitea_ssh_port }}"
|
||||
GITEA__server__SSH_LISTEN_PORT: 2222
|
||||
GITEA__server__START_SSH_SERVER: "true"
|
||||
GITEA__service__DISABLE_REGISTRATION: "true"
|
||||
{% if gitea_db_url is defined and gitea_db_url %}
|
||||
GITEA__database__DB_TYPE: "postgres"
|
||||
GITEA__database__HOST: "{{ gitea_db_host }}"
|
||||
GITEA__database__NAME: "gitea"
|
||||
GITEA__database__USER: "{{ gitea_db_user }}"
|
||||
GITEA__database__PASSWD: "{{ gitea_db_password }}"
|
||||
GITEA__database__SSL_MODE: "require"
|
||||
{% endif %}
|
||||
ports:
|
||||
- "127.0.0.1:{{ gitea_http_port }}:3000"
|
||||
- "{{ gitea_ssh_port }}:2222"
|
||||
24
ansible/roles/gitea/templates/nginx.conf.j2
Normal file
24
ansible/roles/gitea/templates/nginx.conf.j2
Normal file
@@ -0,0 +1,24 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ gitea_domain }};
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name {{ gitea_domain }};
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/cf-origin.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/cf-origin.key;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:{{ gitea_http_port }};
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
18
ansible/roles/vaultwarden/defaults/main.yml
Normal file
18
ansible/roles/vaultwarden/defaults/main.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
vaultwarden_domain: "vault.example.com"
|
||||
vaultwarden_port: 8080
|
||||
vaultwarden_data_dir: /var/lib/vaultwarden
|
||||
|
||||
# Set to your postgres connection string or leave as default for SQLite
|
||||
vaultwarden_database_url: "" # e.g. postgresql://user:pass@host/vaultwarden
|
||||
|
||||
# Restrict signups after first admin account is created
|
||||
vaultwarden_signups_allowed: "true"
|
||||
|
||||
# Admin token — set this to a strong random string
|
||||
# Generate with: openssl rand -base64 48
|
||||
vaultwarden_admin_token: ""
|
||||
|
||||
# Cloudflare Origin Certificate
|
||||
cf_origin_cert: ""
|
||||
cf_origin_key: ""
|
||||
11
ansible/roles/vaultwarden/handlers/main.yml
Normal file
11
ansible/roles/vaultwarden/handlers/main.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Restart vaultwarden
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ vaultwarden_data_dir }}"
|
||||
state: present
|
||||
recreate: always
|
||||
|
||||
- name: Reload nginx
|
||||
systemd:
|
||||
name: nginx
|
||||
state: reloaded
|
||||
76
ansible/roles/vaultwarden/tasks/main.yml
Normal file
76
ansible/roles/vaultwarden/tasks/main.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
- name: Install dependencies
|
||||
apt:
|
||||
name: [docker.io, docker-compose-v2]
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Enable and start Docker
|
||||
systemd:
|
||||
name: docker
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Create vaultwarden data directory
|
||||
file:
|
||||
path: "{{ vaultwarden_data_dir }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Deploy docker-compose file
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ vaultwarden_data_dir }}/docker-compose.yml"
|
||||
mode: "0600"
|
||||
notify: Restart vaultwarden
|
||||
|
||||
- name: Start vaultwarden
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ vaultwarden_data_dir }}"
|
||||
state: present
|
||||
|
||||
# ─── Cloudflare Origin Certificate ───────────────────────────────────────────
|
||||
|
||||
- name: Create SSL directory
|
||||
file:
|
||||
path: /etc/nginx/ssl
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Install Cloudflare origin certificate
|
||||
copy:
|
||||
content: "{{ cf_origin_cert }}"
|
||||
dest: /etc/nginx/ssl/cf-origin.pem
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Install Cloudflare origin key
|
||||
copy:
|
||||
content: "{{ cf_origin_key }}"
|
||||
dest: /etc/nginx/ssl/cf-origin.key
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
notify: Reload nginx
|
||||
|
||||
# ─── nginx ────────────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Deploy nginx config
|
||||
template:
|
||||
src: nginx.conf.j2
|
||||
dest: /etc/nginx/sites-available/vaultwarden
|
||||
mode: "0644"
|
||||
notify: Reload nginx
|
||||
|
||||
- name: Enable nginx site
|
||||
file:
|
||||
src: /etc/nginx/sites-available/vaultwarden
|
||||
dest: /etc/nginx/sites-enabled/vaultwarden
|
||||
state: link
|
||||
notify: Reload nginx
|
||||
16
ansible/roles/vaultwarden/templates/docker-compose.yml.j2
Normal file
16
ansible/roles/vaultwarden/templates/docker-compose.yml.j2
Normal file
@@ -0,0 +1,16 @@
|
||||
services:
|
||||
vaultwarden:
|
||||
image: vaultwarden/server:latest
|
||||
container_name: vaultwarden
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- {{ vaultwarden_data_dir }}/data:/data
|
||||
environment:
|
||||
DOMAIN: "https://{{ vaultwarden_domain }}"
|
||||
SIGNUPS_ALLOWED: "{{ vaultwarden_signups_allowed | lower }}"
|
||||
ADMIN_TOKEN: "{{ vaultwarden_admin_token }}"
|
||||
{% if vaultwarden_database_url %}
|
||||
DATABASE_URL: "{{ vaultwarden_database_url }}"
|
||||
{% endif %}
|
||||
ports:
|
||||
- "127.0.0.1:{{ vaultwarden_port }}:80"
|
||||
27
ansible/roles/vaultwarden/templates/nginx.conf.j2
Normal file
27
ansible/roles/vaultwarden/templates/nginx.conf.j2
Normal file
@@ -0,0 +1,27 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{ vaultwarden_domain }};
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name {{ vaultwarden_domain }};
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/cf-origin.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/cf-origin.key;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
# Required for Bitwarden web vault
|
||||
client_max_body_size 525m;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:{{ vaultwarden_port }};
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user