2026-04-14 18:05:32 +08:00

221 lines
7.9 KiB
TypeScript

import { getRequestOrigin } from "@/lib/request-origin";
import {
buildSubscriptionUrl,
describeRemainingTraffic,
} from "@/lib/subscription";
import {
formatBytes,
listUsers,
toDatetimeLocalValue,
} from "@/lib/store";
import {
createUserAction,
deleteUserAction,
resetUserPasswordAction,
updateUserAction,
} from "./actions";
function getUserPills(user: {
enabled: number;
is_admin: number;
expires_at: string | null;
}) {
return [
{
label: user.enabled ? "已启用" : "已禁用",
className: `status-pill ${user.enabled ? "status-pill--ok" : "status-pill--danger"}`,
},
user.is_admin
? { label: "管理员", className: "status-pill status-pill--ink" }
: null,
user.expires_at
? { label: user.expires_at, className: "status-pill status-pill--neutral" }
: { label: "长期有效", className: "status-pill status-pill--warm" },
].filter((pill): pill is { label: string; className: string } => Boolean(pill));
}
export default async function UsersPage() {
const [users, origin] = await Promise.all([listUsers(), getRequestOrigin()]);
return (
<main className="page-stack">
<section className="hero-panel">
<div className="hero-copy">
<span className="eyebrow">USERS</span>
<h2></h2>
<p className="page-lede">
管理用户名:密码`auth_id`
</p>
<div className="badge-row">
<span className="status-pill status-pill--ok"></span>
<span className="status-pill status-pill--neutral"></span>
</div>
</div>
<div className="hero-aside">
<div className="stat-tile">
<span></span>
<strong>{users.length}</strong>
</div>
<div className="stat-tile">
<span></span>
<strong>{users.filter((user) => user.enabled).length}</strong>
</div>
</div>
</section>
<section className="panel-card">
<div className="section-title">
<h3></h3>
</div>
<form action={createUserAction} className="form-grid">
<label className="field">
<span></span>
<input name="username" required />
</label>
<label className="field">
<span></span>
<input name="password" type="password" required />
</label>
<label className="field">
<span></span>
<input name="expires_at" type="datetime-local" />
</label>
<label className="field">
<span> GiB</span>
<input name="traffic_limit_gib" type="number" min="0" step="0.1" />
</label>
<label className="field full-span">
<span></span>
<textarea name="notes" rows={3} />
</label>
<label className="checkbox">
<input name="enabled" type="checkbox" defaultChecked />
<span></span>
</label>
<button className="primary-button" type="submit">
</button>
</form>
</section>
<section className="stack-lg">
{users.map((user) => (
<article key={user.id} className="panel-card entity-card">
<div className="section-title section-title--top">
<div>
<h3>{user.username}</h3>
<p className="muted mono">auth_id: {user.auth_id}</p>
</div>
<div className="align-right stack-tight">
<div className="badge-row badge-row--end">
{getUserPills(user).map((pill) => (
<span key={pill.label} className={pill.className}>
{pill.label}
</span>
))}
</div>
<p className="muted">
{formatBytes(user.used_tx_bytes + user.used_rx_bytes)} / 线{" "}
{user.online_connections}
</p>
</div>
</div>
<div className="summary-grid">
<div className="metric-inline">
<span></span>
<strong>{formatBytes(user.used_tx_bytes + user.used_rx_bytes)}</strong>
</div>
<div className="metric-inline">
<span></span>
<strong>{describeRemainingTraffic(user)}</strong>
</div>
<div className="metric-inline">
<span>线</span>
<strong>{user.online_connections}</strong>
</div>
</div>
<form action={updateUserAction} className="form-grid compact">
<input name="id" type="hidden" value={user.id} />
<label className="field">
<span></span>
<input name="username" defaultValue={user.username} required />
</label>
<label className="field">
<span></span>
<input
name="expires_at"
type="datetime-local"
defaultValue={toDatetimeLocalValue(user.expires_at)}
/>
</label>
<label className="field">
<span> GiB</span>
<input
name="traffic_limit_gib"
type="number"
min="0"
step="0.1"
defaultValue={
user.traffic_limit_bytes && !user.is_admin
? (user.traffic_limit_bytes / 1024 / 1024 / 1024).toFixed(2)
: ""
}
disabled={Boolean(user.is_admin)}
/>
</label>
<label className="field">
<span></span>
<input
name="subscription_label"
defaultValue={user.subscription_label || user.username}
/>
</label>
<label className="field full-span">
<span></span>
<textarea name="notes" rows={3} defaultValue={user.notes} />
</label>
<label className="checkbox">
<input
name="enabled"
type="checkbox"
defaultChecked={Boolean(user.enabled)}
/>
<span></span>
</label>
<button className="ghost-button" type="submit">
</button>
</form>
<div className="instruction-block">
<strong></strong>
<p className="muted mono break-all">
{user.subscription_token
? buildSubscriptionUrl(user.subscription_token, origin)
: "暂不可用"}
</p>
</div>
<div className="action-row">
<form action={resetUserPasswordAction} className="inline-form">
<input name="id" type="hidden" value={user.id} />
<input name="password" type="password" placeholder="新密码" required />
<button className="ghost-button" type="submit">
</button>
</form>
{user.is_admin ? null : (
<form action={deleteUserAction}>
<input name="id" type="hidden" value={user.id} />
<button className="danger-button" type="submit">
</button>
</form>
)}
</div>
</article>
))}
</section>
</main>
);
}