Slice 1: scaffolding + propose-to-super-draft vertical
Brings the §1 bot wrapper, the §4 cache (webhook + reconciler), the §5 schema (six numbered migrations), Gitea OAuth + §6 user provisioning, the §7 catalog left pane, and the propose-to-merge vertical: propose modal opens an idea PR against the meta repo, an owner merges from the pending-idea view, the cache picks it up via webhook or reconciler sweep, and the catalog renders the new super-draft. Per §1 the bot is the only Git writer; every commit, branch creation, and PR merge carries the §6.5 On-behalf-of: trailer and an `actions` audit row. Per §4 the cache is never written from a user action — it's webhook+reconciler only. Covered by `backend/tests/test_propose_vertical.py` against an in-process Gitea simulator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
-- §5 / §6: users, permission events, and audit log.
|
||||
--
|
||||
-- The users table is the app-owned canonical account record. Per §6.1,
|
||||
-- role is one of owner / admin / contributor; anonymous is the absence
|
||||
-- of a row (or the absence of a session). The §6.2 app-wide write-mute
|
||||
-- lives here as `muted`, structurally distinct from the §15.6 per-RFC
|
||||
-- mute (on watches) and the §15.8 per-user mute (notification_user_mutes).
|
||||
--
|
||||
-- Per §15, the per-user notification preferences are inlined for
|
||||
-- proximity. The watched-RFC-churn category has no column per §15.4 —
|
||||
-- it is permanently off and surfaces in settings as a disabled toggle.
|
||||
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
gitea_id INTEGER UNIQUE NOT NULL,
|
||||
gitea_login TEXT UNIQUE NOT NULL,
|
||||
email TEXT,
|
||||
display_name TEXT NOT NULL,
|
||||
avatar_url TEXT,
|
||||
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'contributor')),
|
||||
muted INTEGER NOT NULL DEFAULT 0, -- §6.2 app-wide write-mute
|
||||
email_personal_direct INTEGER NOT NULL DEFAULT 1, -- §15.4 default on
|
||||
email_watched_structural INTEGER NOT NULL DEFAULT 0, -- §15.4 default off
|
||||
email_admin_actionable INTEGER NOT NULL DEFAULT 1, -- §15.4 default on for admins/owners; ignored for contributors
|
||||
digest_cadence TEXT NOT NULL DEFAULT 'weekly' CHECK (digest_cadence IN ('off', 'weekly', 'daily')), -- §15.5
|
||||
notification_quiet_hours_start TEXT, -- §15.8 ISO-8601 local time HH:MM
|
||||
notification_quiet_hours_end TEXT,
|
||||
notification_quiet_hours_timezone TEXT, -- IANA tz name
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_role ON users (role);
|
||||
|
||||
-- §6.5: permission-change audit. Append-only. Every mute, role grant,
|
||||
-- or capability override produces a row here.
|
||||
CREATE TABLE permission_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
subject_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
event_kind TEXT NOT NULL, -- e.g. role_changed, muted, restored
|
||||
details TEXT, -- JSON blob with before/after, reason, etc.
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_permission_events_subject ON permission_events (subject_user_id, created_at);
|
||||
|
||||
-- §5: append-only action log. Every state transition, every graduation,
|
||||
-- every grant change. Includes the on-behalf-of trailer per §6.5 so the
|
||||
-- audit log and the Git log carry the same accountability.
|
||||
CREATE TABLE actions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
on_behalf_of TEXT NOT NULL, -- the gitea_login the bot acted on behalf of
|
||||
action_kind TEXT NOT NULL, -- propose_rfc, merge_proposal, graduate, etc.
|
||||
rfc_slug TEXT,
|
||||
branch_name TEXT,
|
||||
pr_number INTEGER,
|
||||
bot_commit_sha TEXT,
|
||||
details TEXT, -- JSON blob with kind-specific extras
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_actions_rfc ON actions (rfc_slug, created_at);
|
||||
CREATE INDEX idx_actions_actor ON actions (actor_user_id, created_at);
|
||||
@@ -0,0 +1,82 @@
|
||||
-- §4: the metadata cache. Reconstructible from Gitea at any time by the
|
||||
-- §4.1 reconciler; never written from user actions, only from webhook
|
||||
-- handlers and reconciler sweeps. Body content is cached for main-branch
|
||||
-- reads (§4 #3); branch bodies are not.
|
||||
--
|
||||
-- These tables are not in §5's "canonical app tables" list because they
|
||||
-- are cache, not truth — but they are required for the left-pane render
|
||||
-- path and for serving super-draft and main-branch bodies without a
|
||||
-- Gitea round-trip on every navigation.
|
||||
|
||||
-- One row per meta-repo rfcs/<slug>.md entry. Mirrors §2.1 frontmatter
|
||||
-- plus the cached body for super-draft preview (graduated entries have
|
||||
-- frontmatter-only bodies per §13.3 step 3, but the field is reused).
|
||||
CREATE TABLE cached_rfcs (
|
||||
slug TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
state TEXT NOT NULL CHECK (state IN ('super-draft', 'active', 'withdrawn')),
|
||||
rfc_id TEXT, -- 'RFC-NNNN' or NULL until graduated
|
||||
repo TEXT, -- 'org/repo' or NULL until graduated
|
||||
proposed_by TEXT, -- gitea login or email
|
||||
proposed_at TEXT,
|
||||
graduated_at TEXT,
|
||||
graduated_by TEXT,
|
||||
owners_json TEXT NOT NULL DEFAULT '[]',
|
||||
arbiters_json TEXT NOT NULL DEFAULT '[]',
|
||||
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||
body TEXT, -- super-draft body or main RFC.md body
|
||||
body_sha TEXT, -- the commit sha the body was fetched at
|
||||
last_main_commit_at TEXT, -- §7.1's "Recently active" sort
|
||||
last_entry_commit_at TEXT, -- last meta-repo commit touching this entry
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_cached_rfcs_state ON cached_rfcs (state);
|
||||
CREATE INDEX idx_cached_rfcs_last_active ON cached_rfcs (
|
||||
COALESCE(last_main_commit_at, last_entry_commit_at) DESC
|
||||
);
|
||||
|
||||
-- One row per branch the bot knows about on either a per-RFC repo
|
||||
-- (rfc_slug, state='active'') or on the meta repo as a super-draft edit
|
||||
-- branch (rfc_slug, state='super-draft', branch_name like 'edit/<slug>/...').
|
||||
-- §11.5: closed branches stay; deleted branches keep their metadata row
|
||||
-- per §12 ("branch removed from Gitea, row remains").
|
||||
CREATE TABLE cached_branches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc_slug TEXT NOT NULL,
|
||||
branch_name TEXT NOT NULL,
|
||||
head_sha TEXT,
|
||||
state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'closed', 'deleted')),
|
||||
pinned INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_commit_at TEXT,
|
||||
closed_at TEXT,
|
||||
UNIQUE (rfc_slug, branch_name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_cached_branches_rfc ON cached_branches (rfc_slug, state);
|
||||
|
||||
-- One row per PR the bot knows about. Includes meta-repo idea PRs (rfc_slug
|
||||
-- carries the proposed slug, see §5 super-draft scoping note) and meta-repo
|
||||
-- body-edit PRs and per-RFC-repo PRs. The pr_kind disambiguates.
|
||||
CREATE TABLE cached_prs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc_slug TEXT NOT NULL,
|
||||
pr_kind TEXT NOT NULL CHECK (pr_kind IN ('idea', 'meta_body_edit', 'meta_metadata', 'meta_claim', 'rfc_branch')),
|
||||
repo TEXT NOT NULL, -- 'org/repo' the PR lives on
|
||||
pr_number INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
state TEXT NOT NULL CHECK (state IN ('open', 'merged', 'closed', 'withdrawn')),
|
||||
opened_by TEXT, -- gitea login (resolved from On-behalf-of trailer where present)
|
||||
opened_at TEXT,
|
||||
merged_at TEXT,
|
||||
closed_at TEXT,
|
||||
head_branch TEXT,
|
||||
base_branch TEXT NOT NULL DEFAULT 'main',
|
||||
head_sha TEXT,
|
||||
UNIQUE (repo, pr_number)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_cached_prs_rfc ON cached_prs (rfc_slug, state);
|
||||
CREATE INDEX idx_cached_prs_kind ON cached_prs (pr_kind, state);
|
||||
@@ -0,0 +1,38 @@
|
||||
-- §5 / §6.4 / §11.1: per-branch visibility and contribute settings.
|
||||
-- These rows are app data, not cache. They describe what the app permits
|
||||
-- for a given branch; the bot enforces them before acting. Absence of a
|
||||
-- row means defaults: read_public=1, contribute_mode='just-me'.
|
||||
|
||||
CREATE TABLE branch_visibility (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc_slug TEXT NOT NULL,
|
||||
branch_name TEXT NOT NULL,
|
||||
read_public INTEGER NOT NULL DEFAULT 1,
|
||||
contribute_mode TEXT NOT NULL DEFAULT 'just-me' CHECK (contribute_mode IN ('just-me', 'specific', 'any-contributor')),
|
||||
UNIQUE (rfc_slug, branch_name)
|
||||
);
|
||||
|
||||
CREATE TABLE branch_contribute_grants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc_slug TEXT NOT NULL,
|
||||
branch_name TEXT NOT NULL,
|
||||
grantee_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
granted_by INTEGER NOT NULL REFERENCES users(id) ON DELETE SET NULL,
|
||||
granted_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (rfc_slug, branch_name, grantee_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_grants_lookup ON branch_contribute_grants (rfc_slug, branch_name);
|
||||
CREATE INDEX idx_grants_grantee ON branch_contribute_grants (grantee_user_id);
|
||||
|
||||
-- §5 / §7.2: starred RFCs pin to the top of the current sort order.
|
||||
CREATE TABLE stars (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
rfc_slug TEXT NOT NULL,
|
||||
starred_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (user_id, rfc_slug)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_stars_user ON stars (user_id);
|
||||
CREATE INDEX idx_stars_rfc ON stars (rfc_slug);
|
||||
@@ -0,0 +1,73 @@
|
||||
-- §5: threads, thread_messages, changes — the conversation and revision
|
||||
-- substrate.
|
||||
--
|
||||
-- Per the §5 super-draft scoping note, rows with rfc_slug pointing at a
|
||||
-- super-draft entry use branch_name to name a meta-repo branch rather
|
||||
-- than a per-RFC-repo branch. The schema is identical either way; the
|
||||
-- interpretation flows from the entry's state in cached_rfcs.
|
||||
--
|
||||
-- Threads on a pending-idea PR (§9.3) carry the proposed slug as rfc_slug
|
||||
-- pre-merge — slugs are reserved during the idea PR per §9.1's uniqueness
|
||||
-- check — and surface under the super-draft on merge with no data movement.
|
||||
|
||||
CREATE TABLE threads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc_slug TEXT NOT NULL,
|
||||
branch_name TEXT, -- NULL = scoped to the RFC's main view
|
||||
anchor_kind TEXT NOT NULL CHECK (anchor_kind IN ('whole-doc', 'range', 'paragraph')),
|
||||
anchor_payload TEXT, -- JSON: ProseMirror range or paragraph id
|
||||
thread_kind TEXT NOT NULL CHECK (thread_kind IN ('chat', 'flag', 'review')),
|
||||
label TEXT, -- short summary, or full flag content
|
||||
state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'resolved', 'stale')),
|
||||
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
resolved_at TEXT,
|
||||
resolved_by INTEGER REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_threads_scope ON threads (rfc_slug, branch_name, state);
|
||||
CREATE INDEX idx_threads_kind ON threads (thread_kind, state);
|
||||
|
||||
-- §5: chat content. Only chat-kind threads have rows here unless a flag
|
||||
-- has been converted to a chat (§8.13). System-author messages (role='system',
|
||||
-- author_user_id=NULL) carry the §10.6 manual-edit-flush markers, the §9.3
|
||||
-- decline-comment record, and similar system-narration entries.
|
||||
CREATE TABLE thread_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
thread_id INTEGER NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
|
||||
author_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
model_id TEXT, -- set when role='assistant'
|
||||
text TEXT NOT NULL,
|
||||
quote TEXT, -- optional selection the user attached
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_thread_messages_thread ON thread_messages (thread_id, created_at);
|
||||
CREATE INDEX idx_thread_messages_author ON thread_messages (author_user_id, created_at);
|
||||
|
||||
-- §5 / §8.6 / §8.9 / §8.11: structured proposed edits. AI-proposed (parsed
|
||||
-- from <change> blocks per the §18 carryover) or manually authored.
|
||||
-- stale_since is orthogonal to state: a stale AI proposal stays 'pending'
|
||||
-- until the contributor acts on the staleness warning per §8.11.
|
||||
CREATE TABLE changes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc_slug TEXT NOT NULL,
|
||||
branch_name TEXT NOT NULL,
|
||||
thread_id INTEGER REFERENCES threads(id) ON DELETE SET NULL,
|
||||
source_message_id INTEGER REFERENCES thread_messages(id) ON DELETE SET NULL,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('ai', 'manual')),
|
||||
state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'accepted', 'declined')),
|
||||
original TEXT NOT NULL,
|
||||
proposed TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
was_edited_before_accept INTEGER NOT NULL DEFAULT 0,
|
||||
stale_since TEXT,
|
||||
acted_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
acted_at TEXT,
|
||||
commit_sha TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_changes_scope ON changes (rfc_slug, branch_name, state);
|
||||
CREATE INDEX idx_changes_thread ON changes (thread_id);
|
||||
@@ -0,0 +1,45 @@
|
||||
-- §5 / §10.3 / §15.6 / §15.7: the freshness cursors and the watch model.
|
||||
--
|
||||
-- Two cursor families per §15.7: per-event read state lives on notifications
|
||||
-- (added in 006); per-scope freshness lives on pr_seen and branch_chat_seen.
|
||||
-- They serve different jobs and are reconciled by the visit-advances-cursor
|
||||
-- reconciler in §15.7.
|
||||
|
||||
CREATE TABLE pr_seen (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
rfc_slug TEXT NOT NULL,
|
||||
pr_number INTEGER NOT NULL,
|
||||
last_seen_commit_sha TEXT,
|
||||
last_seen_message_id INTEGER REFERENCES thread_messages(id) ON DELETE SET NULL,
|
||||
seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (user_id, rfc_slug, pr_number)
|
||||
);
|
||||
|
||||
CREATE TABLE branch_chat_seen (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
rfc_slug TEXT NOT NULL,
|
||||
branch_name TEXT NOT NULL,
|
||||
last_seen_message_id INTEGER REFERENCES thread_messages(id) ON DELETE SET NULL,
|
||||
seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (user_id, rfc_slug, branch_name)
|
||||
);
|
||||
|
||||
-- §15.6: the watch model. Three states; auto-rules upgrade but never
|
||||
-- downgrade; explicit settings exempt from the 90-day decay. Per-RFC
|
||||
-- mute lives here as state='muted'.
|
||||
CREATE TABLE watches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
rfc_slug TEXT NOT NULL,
|
||||
state TEXT NOT NULL CHECK (state IN ('watching', 'following', 'muted')),
|
||||
set_by TEXT NOT NULL CHECK (set_by IN ('auto', 'explicit')),
|
||||
set_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_participation_at TEXT, -- 90-day decay key per §15.6
|
||||
UNIQUE (user_id, rfc_slug)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_watches_user ON watches (user_id);
|
||||
CREATE INDEX idx_watches_rfc ON watches (rfc_slug);
|
||||
CREATE INDEX idx_watches_decay ON watches (state, last_participation_at);
|
||||
@@ -0,0 +1,57 @@
|
||||
-- §5 / §15: the notification substrate. Per §15.7, per-row read_at is what
|
||||
-- the inbox needs because triage is per-event. Per §15.9, system-generated
|
||||
-- events carry actor_user_id = NULL; the bot account does not appear here.
|
||||
--
|
||||
-- Fan-out is at signal-generation time per §15.7: each recipient gets their
|
||||
-- own row. This trades storage for query simplicity at the inbox surface,
|
||||
-- and the §15.5 digest's exclusion rules need per-recipient timestamps
|
||||
-- (email_sent_at, digest_included_at) anyway.
|
||||
|
||||
CREATE TABLE notifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
recipient_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
event_kind TEXT NOT NULL, -- §15.1 enum, extensible
|
||||
rfc_slug TEXT,
|
||||
branch_name TEXT,
|
||||
pr_number INTEGER,
|
||||
thread_id INTEGER REFERENCES threads(id) ON DELETE SET NULL,
|
||||
change_id INTEGER REFERENCES changes(id) ON DELETE SET NULL,
|
||||
actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, -- NULL = system per §15.9
|
||||
payload TEXT NOT NULL DEFAULT '{}', -- JSON: rendered row text + extras
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
read_at TEXT, -- §15.7 per-event triage
|
||||
email_sent_at TEXT, -- §15.5 exclusion rule 1
|
||||
digest_included_at TEXT -- §15.5 exclusion rule 3 audit
|
||||
);
|
||||
|
||||
CREATE INDEX idx_notifications_inbox ON notifications (recipient_user_id, read_at, created_at);
|
||||
CREATE INDEX idx_notifications_scope ON notifications (rfc_slug, branch_name, pr_number);
|
||||
CREATE INDEX idx_notifications_digest ON notifications (recipient_user_id, digest_included_at);
|
||||
|
||||
-- §15.5: per-recipient digest emissions. The period_start / period_end
|
||||
-- pair makes the event-window dedup queryable at audit time.
|
||||
CREATE TABLE notification_digests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
recipient_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
sent_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
period_start TEXT NOT NULL,
|
||||
period_end TEXT NOT NULL,
|
||||
signal_ids_included TEXT NOT NULL DEFAULT '[]' -- JSON array of notification ids
|
||||
);
|
||||
|
||||
CREATE INDEX idx_digests_recipient ON notification_digests (recipient_user_id, sent_at);
|
||||
|
||||
-- §15.8: per-user notification mute. Notification-volume only; never
|
||||
-- gates content visibility. The §6.2 clarification reads: an admin or
|
||||
-- arbiter exercising authority on an RFC cannot mute participants on
|
||||
-- that RFC. Enforcement of the role-exemption is in the API layer; the
|
||||
-- schema just stores the mute.
|
||||
CREATE TABLE notification_user_mutes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
muter_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
muted_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
muted_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (muter_user_id, muted_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_mutes_muter ON notification_user_mutes (muter_user_id);
|
||||
Reference in New Issue
Block a user