Update Convex with no payload to be just like convex with payload but without payload
This commit is contained in:
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENVIRONMENT="${1:-staging}"
|
||||
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: build-next-app [dev|staging]" >&2; exit 2; }
|
||||
ENV_FILE="${CI_ENV_FILE:-}"
|
||||
cleanup() { [[ -n "$ENV_FILE" && "$ENV_FILE" != "${CI_ENV_FILE:-}" ]] && rm -f "$ENV_FILE" || true; }
|
||||
trap cleanup EXIT
|
||||
if [[ -z "$ENV_FILE" && -z "${CI:-}" ]]; then ENV_FILE="$(mktemp)"; sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"; fi
|
||||
args=(); [[ -z "$ENV_FILE" ]] || args+=(--env-file "$ENV_FILE")
|
||||
docker compose "${args[@]}" -f "$ROOT_DIR/docker/compose.yml" build convexmonorepo-next
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
||||
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
|
||||
STATE_FILE="$ROOT_DIR/.local/dev.generated.env"
|
||||
WIPE=false
|
||||
[ "${1:-}" = "--wipe" ] && WIPE=true
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
|
||||
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
|
||||
else echo "Docker or Podman not found; nothing to stop." >&2; exit 0; fi
|
||||
|
||||
ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-local.XXXXXX.env")"
|
||||
trap 'rm -f "$ENV_FILE"' EXIT
|
||||
sh "$ROOT_DIR/scripts/export-env" dev > "$ENV_FILE"
|
||||
|
||||
if [ "$WIPE" = true ]; then
|
||||
"$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" down -v
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
tmp="${STATE_FILE}.tmp"
|
||||
grep -v '^CONVEX_SELF_HOSTED_ADMIN_KEY=' "$STATE_FILE" > "$tmp" || true
|
||||
mv "$tmp" "$STATE_FILE"
|
||||
fi
|
||||
echo "Local stack and Convex data volume removed; generated admin key cleared."
|
||||
else
|
||||
"$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" down
|
||||
echo "Local stack stopped; Convex data preserved."
|
||||
fi
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
||||
COMPOSE_FILE="$ROOT_DIR/docker/compose.local.yml"
|
||||
STATE_FILE="$ROOT_DIR/.local/dev.generated.env"
|
||||
ENV_FILE=""
|
||||
|
||||
info() { printf '▶ %s\n' "$*"; }
|
||||
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then RUNTIME=docker
|
||||
elif command -v podman >/dev/null 2>&1; then RUNTIME=podman
|
||||
else die "Docker or Podman is required."; fi
|
||||
"$RUNTIME" info >/dev/null 2>&1 || die "$RUNTIME is not usable."
|
||||
|
||||
mkdir -p "$ROOT_DIR/.local"
|
||||
cleanup() { [ -z "$ENV_FILE" ] || rm -f "$ENV_FILE"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
refresh_env() {
|
||||
local next
|
||||
next="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-local.XXXXXX.env")"
|
||||
sh "$ROOT_DIR/scripts/export-env" dev > "$next" || { rm -f "$next"; die "Unable to export Infisical dev."; }
|
||||
[ -z "$ENV_FILE" ] || rm -f "$ENV_FILE"
|
||||
ENV_FILE="$next"
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
}
|
||||
dc() { "$RUNTIME" compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
|
||||
upsert_state() {
|
||||
local key="$1" value="$2" tmp escaped
|
||||
tmp="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-state.XXXXXX.env")"
|
||||
[ ! -f "$STATE_FILE" ] || grep -v "^${key}=" "$STATE_FILE" > "$tmp" || true
|
||||
escaped="$(printf '%s' "$value" | sed "s/'/'\\\\''/g")"
|
||||
printf "%s='%s'\n" "$key" "$escaped" >> "$tmp"
|
||||
mv "$tmp" "$STATE_FILE"
|
||||
}
|
||||
|
||||
refresh_env
|
||||
info "Starting local Convex and dashboard"
|
||||
dc up -d
|
||||
|
||||
info "Waiting for Convex at http://localhost:${BACKEND_PORT:-3210}"
|
||||
for i in $(seq 1 60); do
|
||||
curl -fs "http://localhost:${BACKEND_PORT:-3210}/version" >/dev/null 2>&1 && break
|
||||
[ "$i" -lt 60 ] || die "Convex did not become ready."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ -z "${CONVEX_SELF_HOSTED_ADMIN_KEY:-}" ]; then
|
||||
admin_key="$(dc exec -T convex-backend ./generate_admin_key.sh 2>/dev/null | grep -E '.+\|.+' | tail -n1 | tr -d '\r')"
|
||||
[ -n "$admin_key" ] || die "Unable to generate the Convex admin key."
|
||||
upsert_state CONVEX_SELF_HOSTED_ADMIN_KEY "$admin_key"
|
||||
refresh_env
|
||||
info "Generated the machine-local Convex admin key"
|
||||
fi
|
||||
|
||||
info "Deploying Convex schema and functions"
|
||||
(cd "$ROOT_DIR/packages/backend" && bun run setup)
|
||||
|
||||
convex_env_names="$(
|
||||
sh "$ROOT_DIR/scripts/with-env" dev -- bash -c \
|
||||
'cd packages/backend && bunx convex env list' 2>/dev/null \
|
||||
| sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)=.*/\1/p'
|
||||
)"
|
||||
if ! printf '%s\n' "$convex_env_names" | grep -qx 'JWT_PRIVATE_KEY' \
|
||||
|| ! printf '%s\n' "$convex_env_names" | grep -qx 'JWKS' \
|
||||
|| ! printf '%s\n' "$convex_env_names" | grep -qx 'SITE_URL'; then
|
||||
info "Configuring local Convex Auth keys"
|
||||
auth_keys="$(node "$ROOT_DIR/scripts/generate-convex-auth-keys.mjs")"
|
||||
jwt="$(printf '%s\n' "$auth_keys" | sed -n 's/^JWT_PRIVATE_KEY="\(.*\)"$/\1/p')"
|
||||
jwks="$(printf '%s\n' "$auth_keys" | sed -n 's/^JWKS=//p')"
|
||||
JWT_VAL="$jwt" JWKS_VAL="$jwks" sh "$ROOT_DIR/scripts/with-env" dev -- bash -c '
|
||||
cd packages/backend
|
||||
bunx convex env set "JWT_PRIVATE_KEY=$JWT_VAL" >/dev/null
|
||||
bunx convex env set "JWKS=$JWKS_VAL" >/dev/null
|
||||
bunx convex env set "SITE_URL=http://localhost:3000" >/dev/null
|
||||
'
|
||||
fi
|
||||
|
||||
printf '\nLocal stack ready:\n App: http://localhost:3000\n Convex: http://localhost:%s\n Dashboard: http://localhost:%s\n' "${BACKEND_PORT:-3210}" "${DASHBOARD_PORT:-6791}"
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMPOSE_FILE="$ROOT_DIR/docker/compose.yml"
|
||||
ENVIRONMENT="${1:-staging}"
|
||||
if [[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]]; then shift || true; else ENVIRONMENT=staging; fi
|
||||
|
||||
ENV_FILE="${CI_ENV_FILE:-}"
|
||||
cleanup() { [[ -n "$ENV_FILE" && "$ENV_FILE" != "${CI_ENV_FILE:-}" ]] && rm -f "$ENV_FILE" || true; }
|
||||
trap cleanup EXIT
|
||||
if [[ -z "$ENV_FILE" && -z "${CI:-}" ]]; then
|
||||
ENV_FILE="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-compose.XXXXXX.env")"
|
||||
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
|
||||
fi
|
||||
|
||||
args=()
|
||||
[[ -z "$ENV_FILE" ]] || args+=(--env-file "$ENV_FILE")
|
||||
translated=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in backend) translated+=(convexmonorepo-backend) ;; dashboard) translated+=(convexmonorepo-dashboard) ;; next) translated+=(convexmonorepo-next) ;; *) translated+=("$arg") ;; esac
|
||||
done
|
||||
set +e
|
||||
docker compose "${args[@]}" -f "$COMPOSE_FILE" "${translated[@]}"
|
||||
status=$?
|
||||
set -e
|
||||
exit "$status"
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${CI:-}" ]; then echo "CI detected; skipping local e2e."; exit 0; fi
|
||||
if [ -n "${SKIP_E2E:-}" ]; then echo "SKIP_E2E set; skipping local e2e."; exit 0; fi
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
bash "$ROOT_DIR/scripts/db/up"
|
||||
echo "Local-stack smoke checks passed; no seeded browser flow is configured."
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
[ "$#" -eq 1 ] || { echo "usage: export-env <dev|staging>" >&2; exit 2; }
|
||||
ENVIRONMENT="$1"
|
||||
case "$ENVIRONMENT" in dev|staging) ;; *) echo "export-env: expected dev or staging" >&2; exit 2 ;; esac
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env"
|
||||
|
||||
if [ -n "${CI:-}" ]; then
|
||||
echo "export-env: refusing to export secrets in CI; use injected variables or CI_ENV_FILE." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ -f "$ROOT_DIR/.infisical.json" ] || { echo "export-env: run 'infisical init' in this repository." >&2; exit 1; }
|
||||
command -v infisical >/dev/null 2>&1 || { echo "export-env: Infisical CLI is required." >&2; exit 1; }
|
||||
|
||||
(cd "$ROOT_DIR" && infisical export --env="$ENVIRONMENT" --format=dotenv --silent) || {
|
||||
echo "export-env: failed to export '$ENVIRONMENT'; check login and project access." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
printf '\n'
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
case "$line" in ''|'#'*) printf '%s\n' "$line"; continue ;; esac
|
||||
key=${line%%=*}
|
||||
value=${line#*=}
|
||||
case "$value" in \'*\') value=${value#\'}; value=${value%\'} ;; \"*\") value=${value#\"}; value=${value%\"} ;; esac
|
||||
escaped=$(printf '%s' "$value" | sed "s/'/'\\\\''/g")
|
||||
printf "%s='%s'\n" "$key" "$escaped"
|
||||
done < "$STATE_FILE"
|
||||
fi
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENVIRONMENT="${1:-staging}"
|
||||
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: generate-convex-admin-key [dev|staging]" >&2; exit 2; }
|
||||
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
|
||||
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
|
||||
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" exec convexmonorepo-backend ./generate_admin_key.sh
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
import { exportJWK, exportPKCS8, generateKeyPair } from 'jose';
|
||||
|
||||
const keys = await generateKeyPair('RS256', {
|
||||
extractable: true,
|
||||
});
|
||||
const privateKey = await exportPKCS8(keys.privateKey);
|
||||
const publicKey = await exportJWK(keys.publicKey);
|
||||
const jwks = JSON.stringify({ keys: [{ use: 'sig', ...publicKey }] });
|
||||
|
||||
process.stdout.write(
|
||||
`JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, ' ')}"`,
|
||||
);
|
||||
process.stdout.write('\n');
|
||||
process.stdout.write(`JWKS=${jwks}`);
|
||||
process.stdout.write('\n');
|
||||
@@ -0,0 +1,102 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const usesendDir = path.join(__dirname, '..', 'node_modules', 'usesend-js');
|
||||
|
||||
const ensureReplacement = (content, searchValue, replaceValue, filePath) => {
|
||||
if (content.includes(replaceValue)) {
|
||||
return content;
|
||||
}
|
||||
|
||||
if (!content.includes(searchValue)) {
|
||||
throw new Error(`Expected snippet not found in ${filePath}`);
|
||||
}
|
||||
|
||||
return content.replace(searchValue, replaceValue);
|
||||
};
|
||||
|
||||
const patchFile = async (relativePath, replacements) => {
|
||||
const filePath = path.join(usesendDir, relativePath);
|
||||
let content = await readFile(filePath, 'utf8');
|
||||
|
||||
for (const [searchValue, replaceValue] of replacements) {
|
||||
content = ensureReplacement(
|
||||
content,
|
||||
searchValue,
|
||||
replaceValue,
|
||||
relativePath,
|
||||
);
|
||||
}
|
||||
|
||||
await writeFile(filePath, content);
|
||||
};
|
||||
|
||||
const patchUseSend = async () => {
|
||||
const packageJsonPath = path.join(usesendDir, 'package.json');
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
||||
|
||||
if (packageJson.version !== '1.6.3') {
|
||||
console.log(
|
||||
`Skipping UseSend patch for version ${packageJson.version ?? 'unknown'}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeHelper = `function getNodeCrypto() {
|
||||
const builtinModuleLoader = globalThis.process?.getBuiltinModule;
|
||||
if (typeof builtinModuleLoader === "function") {
|
||||
const nodeCrypto = builtinModuleLoader("node:crypto");
|
||||
if (nodeCrypto) {
|
||||
return nodeCrypto;
|
||||
}
|
||||
}
|
||||
throw new WebhookVerificationError(
|
||||
"UNSUPPORTED_RUNTIME",
|
||||
"Webhook verification requires a Node.js runtime with node:crypto support"
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
await patchFile('dist/index.mjs', [
|
||||
['import { createHmac, timingSafeEqual } from "crypto";\n', ''],
|
||||
[
|
||||
'function computeSignature(secret, timestamp, body) {\n',
|
||||
`${runtimeHelper}function computeSignature(secret, timestamp, body) {\n`,
|
||||
],
|
||||
[
|
||||
' const hmac = createHmac("sha256", secret);\n',
|
||||
' const { createHmac } = getNodeCrypto();\n const hmac = createHmac("sha256", secret);\n',
|
||||
],
|
||||
[
|
||||
'function safeEqual(a, b) {\n',
|
||||
'function safeEqual(a, b) {\n const { timingSafeEqual } = getNodeCrypto();\n',
|
||||
],
|
||||
]);
|
||||
|
||||
await patchFile('dist/index.js', [
|
||||
['var import_crypto = require("crypto");\n', ''],
|
||||
[
|
||||
'function computeSignature(secret, timestamp, body) {\n',
|
||||
`${runtimeHelper}function computeSignature(secret, timestamp, body) {\n`,
|
||||
],
|
||||
[
|
||||
' const hmac = (0, import_crypto.createHmac)("sha256", secret);\n',
|
||||
' const { createHmac } = getNodeCrypto();\n const hmac = createHmac("sha256", secret);\n',
|
||||
],
|
||||
[
|
||||
'function safeEqual(a, b) {\n',
|
||||
'function safeEqual(a, b) {\n const { timingSafeEqual } = getNodeCrypto();\n',
|
||||
],
|
||||
[
|
||||
' return (0, import_crypto.timingSafeEqual)(aBuf, bBuf);\n',
|
||||
' return timingSafeEqual(aBuf, bBuf);\n',
|
||||
],
|
||||
]);
|
||||
|
||||
console.log('Patched usesend-js 1.6.3 for Convex-compatible bundling.');
|
||||
};
|
||||
|
||||
await patchUseSend();
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENVIRONMENT="${1:-staging}"
|
||||
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: update-convex [dev|staging]" >&2; exit 2; }
|
||||
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
|
||||
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
|
||||
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" pull convexmonorepo-backend convexmonorepo-dashboard
|
||||
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" up -d convexmonorepo-backend convexmonorepo-dashboard
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENVIRONMENT="${1:-staging}"
|
||||
[[ "$ENVIRONMENT" == dev || "$ENVIRONMENT" == staging ]] || { echo "usage: update-next-app [dev|staging]" >&2; exit 2; }
|
||||
ENV_FILE="$(mktemp)"; trap 'rm -f "$ENV_FILE"' EXIT
|
||||
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$ENV_FILE"
|
||||
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" build convexmonorepo-next
|
||||
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker/compose.yml" up -d convexmonorepo-next
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: with-env <dev|staging> -- <command> [args...]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
ENVIRONMENT="$1"
|
||||
shift
|
||||
[ "${1:-}" = "--" ] && shift
|
||||
[ "$#" -gt 0 ] || { echo "with-env: no command given" >&2; exit 2; }
|
||||
|
||||
case "$ENVIRONMENT" in dev|staging) ;; *) echo "with-env: expected dev or staging" >&2; exit 2 ;; esac
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
STATE_FILE="$ROOT_DIR/.local/$ENVIRONMENT.generated.env"
|
||||
|
||||
if [ -n "${CI:-}" ]; then
|
||||
export WITH_ENV_SOURCE=ci WITH_ENV_ENVIRONMENT="$ENVIRONMENT" WITH_ENV_STATE_FILE="$STATE_FILE"
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
command -v infisical >/dev/null 2>&1 || {
|
||||
echo "with-env: install Infisical, run 'infisical login', and link this repo with 'infisical init'." >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$ROOT_DIR/.infisical.json" ] || { echo "with-env: .infisical.json is missing." >&2; exit 1; }
|
||||
|
||||
TMP_ENV="$(mktemp "${TMPDIR:-/tmp}/convex-monorepo-$ENVIRONMENT.XXXXXX.env")"
|
||||
trap 'rm -f "$TMP_ENV"' EXIT INT TERM HUP
|
||||
sh "$ROOT_DIR/scripts/export-env" "$ENVIRONMENT" > "$TMP_ENV"
|
||||
|
||||
export WITH_ENV_SOURCE=infisical WITH_ENV_ENVIRONMENT="$ENVIRONMENT" WITH_ENV_STATE_FILE="$STATE_FILE"
|
||||
set +e
|
||||
bunx dotenv -e "$TMP_ENV" -- "$@"
|
||||
status=$?
|
||||
set -e
|
||||
exit "$status"
|
||||
Reference in New Issue
Block a user