Add account restrictions, appeals, and admin moderation tools
This commit is contained in:
65
shared/account-controls.ts
Normal file
65
shared/account-controls.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const accountStatuses = [
|
||||
"active",
|
||||
"deactivated",
|
||||
"banned",
|
||||
"deleting",
|
||||
] as const;
|
||||
export type AccountStatus = (typeof accountStatuses)[number];
|
||||
export const accountReason = z.string().trim().min(10).max(2000);
|
||||
export const accountActionInput = z
|
||||
.object({
|
||||
action: z.enum(["deactivate", "ban", "restore", "delete"]),
|
||||
reason: accountReason,
|
||||
confirmation: z.string().uuid().optional(),
|
||||
})
|
||||
.strict();
|
||||
export const accountAppealInput = z.object({ reason: accountReason }).strict();
|
||||
export const accountAppealReviewInput = z
|
||||
.object({
|
||||
decision: z.enum(["approve", "reject"]),
|
||||
reason: accountReason,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export function restrictionMessage(status: string) {
|
||||
return status === "banned"
|
||||
? "Your account is banned. Visit /ban/appeal to read the reason and appeal."
|
||||
: status === "deleting"
|
||||
? "Account deletion is pending. Visit /ban/appeal for account status."
|
||||
: "Your account is deactivated. Visit /ban/appeal for account status.";
|
||||
}
|
||||
|
||||
/** Restricted accounts retain only the pages needed to understand/appeal access. */
|
||||
export function restrictionPageAllowed(path: string) {
|
||||
return (
|
||||
path === "/ban/appeal" ||
|
||||
path === "/docs" ||
|
||||
path === "/log-in" ||
|
||||
path === "/auth/callback" ||
|
||||
path === "/auth/reset-password"
|
||||
);
|
||||
}
|
||||
|
||||
export type ManagedAccount = {
|
||||
id: string;
|
||||
display_name: string;
|
||||
role: string;
|
||||
account_status: AccountStatus;
|
||||
account_reason: string | null;
|
||||
account_changed_at: string | null;
|
||||
status_version: number;
|
||||
};
|
||||
export type AccountAppealRecord = {
|
||||
ban_reason?: string | null;
|
||||
id: string;
|
||||
owner_id: string;
|
||||
display_name: string;
|
||||
status_version: number;
|
||||
reason: string;
|
||||
state: "open" | "approved" | "rejected";
|
||||
created_at: string;
|
||||
review_reason: string | null;
|
||||
reviewed_at: string | null;
|
||||
};
|
||||
@@ -25,7 +25,7 @@ type ReportModerationFields = {
|
||||
type AppealRecord = {
|
||||
id: string;
|
||||
dataset_id: string;
|
||||
owner_id: string;
|
||||
owner_id: string | null;
|
||||
report_cycle: number;
|
||||
reason: string;
|
||||
state: string;
|
||||
@@ -35,6 +35,25 @@ type AppealRecord = {
|
||||
review_reason: string | null;
|
||||
};
|
||||
|
||||
type AccountStatusFields = {
|
||||
account_status: "active" | "deactivated" | "banned" | "deleting";
|
||||
account_reason: string | null;
|
||||
account_changed_at: string | null;
|
||||
account_changed_by: string | null;
|
||||
account_status_version: number;
|
||||
};
|
||||
type AccountAppealRecord = {
|
||||
id: string;
|
||||
owner_id: string;
|
||||
status_version: number;
|
||||
reason: string;
|
||||
state: "open" | "approved" | "rejected";
|
||||
created_at: string;
|
||||
reviewer_id: string | null;
|
||||
reviewed_at: string | null;
|
||||
review_reason: string | null;
|
||||
};
|
||||
|
||||
export type Database = {
|
||||
// Allows to automatically instantiate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
@@ -43,6 +62,37 @@ export type Database = {
|
||||
};
|
||||
public: {
|
||||
Tables: {
|
||||
account_appeals: {
|
||||
Row: AccountAppealRecord;
|
||||
Insert: Partial<AccountAppealRecord> &
|
||||
Pick<AccountAppealRecord, "owner_id" | "status_version" | "reason">;
|
||||
Update: Partial<AccountAppealRecord>;
|
||||
Relationships: [];
|
||||
};
|
||||
account_control_events: {
|
||||
Row: {
|
||||
id: string;
|
||||
owner_id: string | null;
|
||||
reviewer_id: string | null;
|
||||
action: string;
|
||||
previous_status: string;
|
||||
new_status: string;
|
||||
status_version: number;
|
||||
reason: string;
|
||||
created_at: string;
|
||||
};
|
||||
Insert: {
|
||||
owner_id?: string | null;
|
||||
reviewer_id?: string | null;
|
||||
action: string;
|
||||
previous_status: string;
|
||||
new_status: string;
|
||||
status_version: number;
|
||||
reason: string;
|
||||
};
|
||||
Update: { owner_id?: string | null; reviewer_id?: string | null };
|
||||
Relationships: [];
|
||||
};
|
||||
discord_announcements: {
|
||||
Row: import("./discord").DiscordAnnouncement;
|
||||
Insert: Partial<import("./discord").DiscordAnnouncement> &
|
||||
@@ -307,7 +357,7 @@ export type Database = {
|
||||
id: string;
|
||||
map_path: string | null;
|
||||
name: string;
|
||||
owner_id: string;
|
||||
owner_id: string | null;
|
||||
panorama_count: number;
|
||||
parent_dataset_id: string | null;
|
||||
rejection_reason: string | null;
|
||||
@@ -330,7 +380,7 @@ export type Database = {
|
||||
id?: string;
|
||||
map_path?: string | null;
|
||||
name: string;
|
||||
owner_id: string;
|
||||
owner_id: string | null;
|
||||
panorama_count?: number;
|
||||
parent_dataset_id?: string | null;
|
||||
rejection_reason?: string | null;
|
||||
@@ -353,7 +403,7 @@ export type Database = {
|
||||
id?: string;
|
||||
map_path?: string | null;
|
||||
name?: string;
|
||||
owner_id?: string;
|
||||
owner_id?: string | null;
|
||||
panorama_count?: number;
|
||||
parent_dataset_id?: string | null;
|
||||
rejection_reason?: string | null;
|
||||
@@ -489,7 +539,7 @@ export type Database = {
|
||||
Relationships: [];
|
||||
};
|
||||
profiles: {
|
||||
Row: {
|
||||
Row: AccountStatusFields & {
|
||||
glow_color: string;
|
||||
bug_finder: boolean;
|
||||
custom_prefix: string | null;
|
||||
@@ -500,7 +550,7 @@ export type Database = {
|
||||
id: string;
|
||||
role: string;
|
||||
};
|
||||
Insert: {
|
||||
Insert: Partial<AccountStatusFields> & {
|
||||
glow_color?: string;
|
||||
bug_finder?: boolean;
|
||||
custom_prefix?: string | null;
|
||||
@@ -511,7 +561,7 @@ export type Database = {
|
||||
id: string;
|
||||
role?: string;
|
||||
};
|
||||
Update: {
|
||||
Update: Partial<AccountStatusFields> & {
|
||||
glow_color?: string;
|
||||
bug_finder?: boolean;
|
||||
custom_prefix?: string | null;
|
||||
@@ -702,6 +752,28 @@ export type Database = {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Functions: {
|
||||
admin_account_action: {
|
||||
Args: {
|
||||
p_reviewer: string;
|
||||
p_user: string;
|
||||
p_action: string;
|
||||
p_reason: string;
|
||||
};
|
||||
Returns: Json;
|
||||
};
|
||||
submit_account_appeal: {
|
||||
Args: { p_owner: string; p_reason: string };
|
||||
Returns: string;
|
||||
};
|
||||
review_account_appeal: {
|
||||
Args: {
|
||||
p_reviewer: string;
|
||||
p_appeal: string;
|
||||
p_decision: string;
|
||||
p_reason: string;
|
||||
};
|
||||
Returns: undefined;
|
||||
};
|
||||
developer_keys_list: { Args: { p_owner: string }; Returns: Json };
|
||||
developer_key_create: {
|
||||
Args: {
|
||||
|
||||
@@ -884,5 +884,17 @@
|
||||
"Metrics access disabled. Existing scrape credentials no longer work.": "Доступ к метрикам отключён. Прежние данные доступа больше не работают.",
|
||||
"Copied to clipboard.": "Скопировано в буфер обмена.",
|
||||
"Could not copy. Select the text and copy it manually.": "Не удалось скопировать. Выделите текст и скопируйте его вручную.",
|
||||
"Status": "Статус сервисов"
|
||||
"Status": "Статус сервисов",
|
||||
"Admin & Moderation Tools": "Администрирование и модерация",
|
||||
"Admin & Moderation Tools.": "Администрирование и модерация.",
|
||||
"Manage the community, review maps and account appeals, and configure Admin settings.": "Управляйте сообществом, проверяйте карты и апелляции и меняйте настройки администратора.",
|
||||
"Moderators can review pending maps, updates and account ban appeals. Other controls require Admin permission.": "Модераторы могут проверять карты, обновления и апелляции блокировок аккаунтов. Остальные действия требуют прав администратора.",
|
||||
"Admin Tools": "Инструменты администратора",
|
||||
"Admin Tools.": "Инструменты администратора.",
|
||||
"Moderation Tools": "Инструменты модерации",
|
||||
"Moderation Tools.": "Инструменты модерации.",
|
||||
"Open Admin Tools": "Открыть инструменты администратора",
|
||||
"Open Moderation Tools": "Открыть инструменты модерации",
|
||||
"Review pending maps, map updates and account ban appeals.": "Рассматривайте новые карты, обновления карт и апелляции блокировок аккаунтов.",
|
||||
"Manage accounts, site settings, integrations and community reports.": "Управляйте аккаунтами, настройками сайта, интеграциями и жалобами сообщества."
|
||||
}
|
||||
|
||||
@@ -63,5 +63,10 @@
|
||||
"title": "Community and badges",
|
||||
"slug": "staff",
|
||||
"markdown": "# Community and badges\n\nMeet other explorers, recognize the people helping the community, and make your account feel like yours.\n\n## Join Discord\n\nUse the Discord button at the bottom of MineGeo, or [join the community here](https://discord.gg/Aer4ZugKWS).\n\nNewly approved maps may be announced there. Hosts can also share open multiplayer parties to the community's party channel. Open an invite to join while there is space.\n\n## What do the name badges mean?\n\n- **Moderator, with a shield:** someone who can check maps and approve them for public play.\n- **Admin, with a crown:** someone who can also handle community reports and other site management.\n- **Bug finder:** someone recognized for helping find a confirmed bug.\n- **Custom prefix:** extra text and an icon assigned by an Admin.\n\nA decorative custom prefix does not give someone moderation powers. Copying another player's name does not copy their badges.\n\n## Can I get a glow?\n\nSupport may award one for a confirmed bug report. Describe the real problem through **Report a bug**. A reward is granted by staff; it is not automatic for every report.\n\nIf you have an active glow, open **Settings → Username glow**, choose a color, and save it. You can keep an earned Bug finder badge even if the glow is later removed.\n\n## Be a good party member\n\nShare invites thoughtfully, keep chat friendly, and avoid spoiling the location before everyone has guessed. Report content that needs attention instead of starting an argument.\n\n> The real treasure was the friends who also guessed the wrong mountain. ⛰️"
|
||||
},
|
||||
{
|
||||
"slug": "account-bans",
|
||||
"title": "Account access and ban appeals",
|
||||
"markdown": "# Account access and ban appeals\n\nIf your account is restricted, sign in as usual. MineGeo will take you to **Account access & appeals**, where you can read the reason. You can also open **/ban/appeal** directly.\n\n## Deactivated or banned?\n\n- **Deactivated:** an Admin has temporarily disabled account access. An Admin must restore it; this does not use the ban appeal queue. Contact the team through the Discord community link in the footer.\n- **Banned:** normal account access is blocked. You can submit an appeal explaining why the decision should be reconsidered.\n- **Deletion pending:** an Admin has started removing the account. Access remains blocked while the action completes. Only an Admin can finish it.\n\nWhile restricted, you can read the docs, check this page, sign out, and use the sign-in or password recovery flow. You cannot play through that account, upload maps, use its API keys, or use staff tools. Existing upload and API keys are revoked and existing games and multiplayer participation are ended. Restoring access does not reactivate old keys; create new ones when needed.\n\n## Send an appeal\n\n1. Sign in to the banned account and open **/ban/appeal**.\n2. Read the ban reason. Explain what happened and why the ban should be reconsidered in **10–2000 characters**.\n3. Click **Send appeal**. You can send **one appeal for each ban**, so include the useful details before sending.\n4. Return to this page and click **Refresh status** to see the decision.\n\nBoth Moderators and Admins can review account ban appeals. An approval restores access for the ban being reviewed. A rejection keeps the ban. Staff leave a reason you can read on the same page. You cannot submit the same ban again after a rejection; contact the team if you have new information.\n\nIf an Admin changes the restriction while an appeal is waiting, the old appeal closes. An old approval cannot override a newer restriction.\n\n## Deleted accounts\n\nPermanent deletion removes sign-in, the profile, credentials and account appeals. It cannot be appealed or recovered through this page. Shared maps and community history can remain without an account owner; deletion does not erase the worlds other players use.\n\n## Staff controls\n\nOpen **Admin Tools** from the footer or go to **/admin**. This separate page is available only to Admins. Admins can search accounts, deactivate, ban, restore access, and permanently delete accounts. Every action needs a reason. Permanent deletion also requires typing the exact account ID. Admin accounts are protected from these controls.\n\nOpen **Moderation Tools** at **/moderation** to review maps and account ban appeals. Moderators and Admins can access this page. Moderators cannot deactivate, ban, delete, or directly restore arbitrary accounts."
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user