27 lines
5.6 KiB
TypeScript
27 lines
5.6 KiB
TypeScript
import { redirect } from "next/navigation";
|
||
import { getSession } from "@/lib/auth";
|
||
import { db } from "@/lib/db";
|
||
import { checkRuntimeConfig } from "@/lib/config";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
export default async function AdminPage({ searchParams }: { searchParams: Promise<{ updated?: string; error?: string }> }) {
|
||
const user = await getSession(); if (!user || user.role !== "admin") redirect("/"); const query = await searchParams;
|
||
const failures = db.prepare("SELECT j.id,j.kind,j.trigger,j.status,j.attempts,j.last_error,j.created_at,j.finished_at,s.id AS source_id,s.name FROM sync_jobs j JOIN sources s ON s.id=j.source_id WHERE j.status='failed' OR s.sync_status='error' ORDER BY COALESCE(j.finished_at,j.created_at) DESC LIMIT 100").all() as any[];
|
||
const reports = db.prepare("SELECT r.id,r.reason,r.created_at,p.id AS post_id,p.content,u.username FROM reports r JOIN posts p ON p.id=r.post_id LEFT JOIN users u ON u.id=r.reporter_id WHERE r.resolved=0 ORDER BY r.created_at LIMIT 100").all() as any[];
|
||
const users = db.prepare("SELECT id,username,role,disabled,created_at FROM users ORDER BY created_at DESC LIMIT 100").all() as any[];
|
||
const errors = db.prepare("SELECT scope,message,created_at FROM error_events ORDER BY id DESC LIMIT 30").all() as any[];
|
||
const audits = db.prepare("SELECT a.action,a.target_type,a.target_id,a.metadata_json,a.created_at,u.username FROM audit_events a LEFT JOIN users u ON u.id=a.actor_user_id ORDER BY a.id DESC LIMIT 50").all() as any[];
|
||
const sources = db.prepare("SELECT s.id,s.name,s.base_url,s.sync_status,s.last_synced_at,s.is_enabled,count(sm.user_id) AS member_count FROM sources s LEFT JOIN source_members sm ON sm.source_id=s.id GROUP BY s.id ORDER BY s.id DESC").all() as any[];
|
||
const config = checkRuntimeConfig();
|
||
return <><h1>管理員</h1>{query.updated && <p>管理操作已完成。</p>}{query.error && <p className="error">管理操作未完成。</p>}
|
||
<section className="card"><h2>待審核檢舉</h2>{reports.length ? <ul className="job-list">{reports.map((report) => <li key={report.id}><strong>貼文 #{report.post_id}</strong> · 檢舉者 @{report.username || "已刪除使用者"}<br />{report.reason}<br /><span className="meta">{report.content.slice(0, 180)} · {new Date(report.created_at + "Z").toLocaleString("zh-TW")}</span><div className="row"><form action="/api/admin/moderate" method="post"><input type="hidden" name="action" value="hide-post" /><input type="hidden" name="id" value={report.post_id} /><button className="danger">隱藏貼文並結案</button></form><form action="/api/admin/moderate" method="post"><input type="hidden" name="action" value="resolve-report" /><input type="hidden" name="id" value={report.id} /><button>保留貼文並結案</button></form></div></li>)}</ul> : <p className="muted">沒有待審核檢舉。</p>}</section>
|
||
<section className="card"><h2>使用者</h2><ul className="job-list">{users.map((account) => <li key={account.id}><strong>@{account.username}</strong> · <span className="tag">{account.role}</span> · {account.disabled ? "已停權" : "正常"}<div className="row">{account.id !== user.id && <form action="/api/admin/moderate" method="post"><input type="hidden" name="action" value={account.disabled ? "enable-user" : "disable-user"} /><input type="hidden" name="id" value={account.id} /><button className={account.disabled ? "" : "danger"}>{account.disabled ? "解除停權" : "停權"}</button></form>}<form action="/api/admin/moderate" method="post"><input type="hidden" name="action" value="reset-password" /><input type="hidden" name="id" value={account.id} /><input name="password" type="password" minLength={10} required placeholder="管理員重設密碼" /><button>重設密碼</button></form></div></li>)}</ul></section>
|
||
<section className="card"><h2>失敗或異常工作</h2>{failures.length ? <ul className="job-list">{failures.map((item) => <li key={`${item.id}-${item.source_id}`}><strong>{item.name}</strong>(來源 #{item.source_id}) · {item.kind || "source"} · <span className="tag">{item.status || "error"}</span><br /><span className="error">{item.last_error || "來源處於錯誤狀態"}</span></li>)}</ul> : <p className="muted">沒有同步異常。</p>}</section>
|
||
<section className="card"><h2>最近系統錯誤</h2>{errors.length ? <ul className="job-list">{errors.map((error, index) => <li key={index}><strong>{error.scope}</strong> · <span className="error">{error.message}</span><br /><span className="meta">{new Date(error.created_at + "Z").toLocaleString("zh-TW")}</span></li>)}</ul> : <p className="muted">尚無記錄。</p>}</section>
|
||
<section className="card"><h2>稽核紀錄</h2>{audits.length ? <ul className="job-list">{audits.map((item, index) => <li key={index}><strong>{item.action}</strong> · @{item.username || "system"} · {item.target_type} #{item.target_id || "—"}<br /><span className="meta">{new Date(item.created_at + "Z").toLocaleString("zh-TW")}</span></li>)}</ul> : <p className="muted">尚無稽核紀錄。</p>}</section>
|
||
<section className="card"><h2>所有來源</h2><ul className="job-list">{sources.map((source) => <li key={source.id}><strong>{source.name}</strong> · <span className="tag">{source.is_enabled ? source.sync_status : "disabled"}</span> · 成員 {source.member_count}<br /><span className="meta">#{source.id} · {source.base_url} · 上次同步:{source.last_synced_at || "尚未完成"}</span></li>)}</ul></section>
|
||
<section className="card"><h2>正式環境設定</h2>{config.ok ? <p>設定檢查通過。</p> : <ul>{config.errors.map((error) => <li className="error" key={error}>{error}</li>)}</ul>}</section>
|
||
</>;
|
||
}
|