fix: honor licensed user limits

This commit is contained in:
ShukeBta
2026-05-30 21:48:09 +08:00
parent e08b827861
commit de3f1f3bb4
8 changed files with 243 additions and 60 deletions
+4 -1
View File
@@ -12,6 +12,8 @@ export interface LicenseActivation {
device_name?: string
plan?: string
max_activations?: number
max_users?: number | null
unlimited_users?: boolean
/** ISO8601 — null means perpetual */
expires_at?: string | null
valid: boolean
@@ -25,7 +27,8 @@ export interface LicenseStatus {
/** Whether a license is currently active */
active: boolean
activation?: LicenseActivation
max_users?: number
max_users?: number | null
unlimited_users?: boolean
/** Error or status message */
message?: string
}
+31 -6
View File
@@ -4,6 +4,7 @@ import { KeyRound, Pencil, Plus, ShieldCheck, Trash2, X } from 'lucide-react'
import { adminAPI } from '../api/admin'
import { libraryAPI } from '../api/library'
import { licenseAPI, type LicenseStatus } from '../api/license'
import type { Library, User } from '../types'
import { APIConfigsPanel } from '../components/APIConfigsPanel'
import { ManagementShortcuts } from '../components/ManagementShortcuts'
@@ -163,15 +164,30 @@ function LibraryPanel() {
function UsersPanel() {
const [users, setUsers] = useState<User[]>([])
const [licenseStatus, setLicenseStatus] = useState<LicenseStatus | null>(null)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [editingID, setEditingID] = useState<string | null>(null)
const [editingUsername, setEditingUsername] = useState('')
const refresh = () => adminAPI.listUsers().then(setUsers)
const refresh = async () => {
const [nextUsers, nextLicense] = await Promise.all([
adminAPI.listUsers(),
licenseAPI.status().catch(() => null),
])
setUsers(nextUsers)
setLicenseStatus(nextLicense)
}
useEffect(() => {
refresh().catch(() => undefined)
}, [])
const unlimitedUsers =
licenseStatus?.active === true &&
(licenseStatus.unlimited_users === true || licenseStatus.max_users == null)
const maxUsers = unlimitedUsers ? null : (licenseStatus?.max_users ?? 20)
const userLimitReached = maxUsers != null && users.length >= maxUsers
const userLimitLabel = unlimitedUsers ? '不限制' : String(maxUsers)
const handleCreate = async (e: FormEvent) => {
e.preventDefault()
try {
@@ -182,7 +198,7 @@ function UsersPanel() {
await refresh()
} catch (err: unknown) {
const msg =
(err as { response?: { data?: { error?: string } } })?.response?.data?.error ??
userCreateErrorMessage(err) ??
'添加用户失败'
toast.error(msg)
}
@@ -236,7 +252,7 @@ function UsersPanel() {
<div>
<h2 className="font-display text-lg font-semibold text-ink-600">用户管理</h2>
<p className="text-xs text-sand-500">
已创建 {users.length}/20 个用户;新增用户默认只有媒体库浏览、播放、外部播放器与第三方客户端观看权限。
已创建 {users.length}/{userLimitLabel} 个用户;新增用户默认只有媒体库浏览、播放、外部播放器与第三方客户端观看权限。
</p>
</div>
<span className="rounded-full border border-primary-400/30 px-3 py-1 text-xs text-brand-500">
@@ -249,7 +265,7 @@ function UsersPanel() {
placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={users.length >= 20}
disabled={userLimitReached}
/>
<input
required
@@ -259,9 +275,9 @@ function UsersPanel() {
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={users.length >= 20}
disabled={userLimitReached}
/>
<button type="submit" className="neon-button inline-flex items-center justify-center gap-2" disabled={users.length >= 20}>
<button type="submit" className="neon-button inline-flex items-center justify-center gap-2" disabled={userLimitReached}>
<Plus size={16} />
添加用户
</button>
@@ -356,3 +372,12 @@ function UsersPanel() {
</div>
)
}
function userCreateErrorMessage(err: unknown): string | undefined {
const data = (err as { response?: { data?: { error?: string; max_users?: number } } })?.response?.data
if (!data?.error) return undefined
if (data.error === 'user limit reached' && data.max_users != null) {
return `用户数量已达到授权上限:${data.max_users} 人`
}
return data.error
}
+9 -1
View File
@@ -27,6 +27,11 @@ function fmtDateTime(iso: string | null | undefined): string {
return new Date(iso).toLocaleString('zh-CN')
}
function fmtUserLimit(maxUsers: number | null | undefined, unlimited?: boolean): string {
if (unlimited || maxUsers == null) return '不限制'
return `${maxUsers} 人`
}
// ── Page ──
export function LicensePage() {
@@ -198,7 +203,10 @@ export function LicensePage() {
/>
<StatusBadge label="最近心跳" value={fmtDateTime(activation.heartbeat_at)} />
<StatusBadge label="客户端 IP" value={activation.ip ?? '—'} />
<StatusBadge label="用户额度" value={`${status?.max_users ?? 20} 人`} />
<StatusBadge
label="用户额度"
value={fmtUserLimit(status?.max_users, status?.unlimited_users)}
/>
</div>
)}