Add account restrictions, appeals, and admin moderation tools
This commit is contained in:
30
app/admin/page.tsx
Normal file
30
app/admin/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import Link from "next/link";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { Moderation } from "@/components/Moderation";
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { staffPage } from "@/lib/staff-page";
|
||||
export const metadata = { title: "Admin Tools" };
|
||||
export default async function AdminPage() {
|
||||
const [{ t }] = await Promise.all([getI18n(), staffPage(true)]);
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content moderation-page">
|
||||
<span className="eyebrow">MINEGEO ADMIN</span>
|
||||
<h1>{t("Admin Tools.")}</h1>
|
||||
<p className="lead">
|
||||
{t(
|
||||
"Manage accounts, site settings, integrations and community reports.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<Link className="button" href="/moderation">
|
||||
{t("Open Moderation Tools")}
|
||||
</Link>
|
||||
</p>
|
||||
<Moderation mode="admin" />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { cookies } from "next/headers";
|
||||
import Link from "next/link";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { PasswordForm } from "@/components/PasswordForm";
|
||||
import { authClient, user } from "@/lib/supabase";
|
||||
import { authClient, rawUser } from "@/lib/supabase";
|
||||
import { recoveryCookie, verifyRecoveryGrant } from "@/lib/password-recovery";
|
||||
export async function generateMetadata() {
|
||||
const { t } = await getI18n();
|
||||
@@ -12,7 +12,7 @@ export async function generateMetadata() {
|
||||
export default async function ResetPassword() {
|
||||
const { t } = await getI18n();
|
||||
const [account, jar, client] = await Promise.all([
|
||||
user(),
|
||||
rawUser(),
|
||||
cookies(),
|
||||
authClient(),
|
||||
]);
|
||||
|
||||
67
app/ban/appeal/page.module.css
Normal file
67
app/ban/appeal/page.module.css
Normal file
@@ -0,0 +1,67 @@
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.heading h2,
|
||||
.heading h3 {
|
||||
margin: 0;
|
||||
}
|
||||
.heading {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.search {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: end;
|
||||
gap: 16px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
.search label {
|
||||
flex: 1 1 240px;
|
||||
min-width: 0;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
margin-top: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
.record {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: clamp(16px, 3vw, 24px);
|
||||
min-width: 0;
|
||||
}
|
||||
.record code,
|
||||
.record h3,
|
||||
.record p {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.record h3 {
|
||||
font-size: 22px;
|
||||
}
|
||||
.reason {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.confirm {
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 24px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.confirm label {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.page {
|
||||
max-width: 900px;
|
||||
}
|
||||
.page p {
|
||||
line-height: 1.8;
|
||||
}
|
||||
44
app/ban/appeal/page.tsx
Normal file
44
app/ban/appeal/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import Link from "next/link";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { AccountAppeal } from "@/components/AccountAppeal";
|
||||
import { rawUser } from "@/lib/supabase";
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import styles from "./page.module.css";
|
||||
export const metadata = { title: "Account access & appeals" };
|
||||
export const dynamic = "force-dynamic";
|
||||
export default async function AppealPage() {
|
||||
const [account, { locale }] = await Promise.all([rawUser(), getI18n()]);
|
||||
const ru = locale === "ru";
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className={`section page-content ${styles.page}`}>
|
||||
<span className="eyebrow">MINEGEO</span>
|
||||
<h1>{ru ? "Доступ и апелляции." : "Account access & appeals."}</h1>
|
||||
{account ? (
|
||||
<AccountAppeal />
|
||||
) : (
|
||||
<>
|
||||
<p className="lead">
|
||||
{ru
|
||||
? "Войдите в заблокированный аккаунт, чтобы прочитать причину и подать апелляцию."
|
||||
: "Log in to the affected account to see the reason and submit a ban appeal."}
|
||||
</p>
|
||||
<Link
|
||||
className="button primary"
|
||||
href="/log-in?next=%2Fban%2Fappeal"
|
||||
>
|
||||
{ru ? "Войти" : "Log in"}
|
||||
</Link>
|
||||
<p>
|
||||
<Link href="/docs?page=account-bans">
|
||||
{ru ? "Как работают апелляции" : "How appeals work"}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import "./community.css";
|
||||
import { themeInitScript } from "@/shared/theme";
|
||||
import { SiteSettingsProvider } from "@/components/SiteSettingsProvider";
|
||||
import { initialSiteSettings } from "@/lib/site-settings";
|
||||
import { user } from "@/lib/supabase";
|
||||
import { rawUser, accountState } from "@/lib/supabase";
|
||||
import { LegacyPasswordNotice } from "@/components/LegacyPasswordNotice";
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const { t } = await getI18n();
|
||||
@@ -41,7 +41,10 @@ export default async function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [{ locale, preference, detectedLocale }, settings, account] =
|
||||
await Promise.all([getI18n(), initialSiteSettings(), user()]);
|
||||
await Promise.all([getI18n(), initialSiteSettings(), rawUser()]);
|
||||
const active = account
|
||||
? (await accountState(account.id)) === "active"
|
||||
: false;
|
||||
return (
|
||||
<html lang={locale} data-theme="dark" suppressHydrationWarning>
|
||||
<head>
|
||||
@@ -56,7 +59,7 @@ export default async function RootLayout({
|
||||
<SiteSettingsProvider initialSettings={settings}>
|
||||
{children}
|
||||
<SiteCursor />
|
||||
<LegacyPasswordNotice enabled={!!account?.email} />
|
||||
<LegacyPasswordNotice enabled={active && !!account?.email} />
|
||||
</SiteSettingsProvider>
|
||||
</I18nProvider>
|
||||
<Analytics />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getI18n } from "@/lib/i18n";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { SignIn } from "@/components/SignIn";
|
||||
import { user } from "@/lib/supabase";
|
||||
import { rawUser, accountState } from "@/lib/supabase";
|
||||
import { authDestination } from "@/shared/account";
|
||||
export async function generateMetadata() {
|
||||
const { t } = await getI18n();
|
||||
@@ -15,8 +15,10 @@ export default async function LogIn({
|
||||
}) {
|
||||
const query = await searchParams;
|
||||
const next = authDestination(query.next);
|
||||
if (await user())
|
||||
redirect(next === "/auth/reset-password" ? "/settings" : next);
|
||||
const account = await rawUser();
|
||||
if (account && (await accountState(account.id)) !== "active")
|
||||
redirect("/ban/appeal");
|
||||
if (account) redirect(next === "/auth/reset-password" ? "/settings" : next);
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import { Moderation } from "@/components/Moderation";
|
||||
import Link from "next/link";
|
||||
import { staffPage } from "@/lib/staff-page";
|
||||
import { isAdministrator } from "@/shared/permissions";
|
||||
export const metadata = { title: "Moderation Tools" };
|
||||
export default async function Review() {
|
||||
const { t } = await getI18n();
|
||||
const [{ t }, staff] = await Promise.all([getI18n(), staffPage()]);
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="section page-content moderation-page">
|
||||
<span className="eyebrow">{t("THE COMMUNITY’S COMPASS")}</span>
|
||||
<h1>{t("Map moderation.")}</h1>
|
||||
<h1>{t("Moderation Tools.")}</h1>
|
||||
<p className="lead">
|
||||
{t(
|
||||
"Review map quality and coverage before a world enters the public catalog.",
|
||||
)}
|
||||
{t("Review pending maps, map updates and account ban appeals.")}
|
||||
</p>
|
||||
{isAdministrator(staff.role) && (
|
||||
<p>
|
||||
<Link className="button" href="/admin">
|
||||
{t("Open Admin Tools")}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
<Moderation />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
Reference in New Issue
Block a user