mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-09-19 09:40:13 +00:00
Stabilize CI runtime for updated deps and unblock Dependabot auto-format flow (#185)
* chore: apply ci and dependabot remediation changes * chore: auto-fix linting and formatting issues --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -20,7 +20,7 @@ jobs:
|
|||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 22.19.0
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: package-lock.json
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
src/main.tsx
|
src/main.tsx
|
||||||
|
.github/workflows/*
|
||||||
|
|||||||
Generated
+3
-1
@@ -3460,7 +3460,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
|
|||||||
+94
-23
@@ -47,20 +47,56 @@ export const deleteApiToken = async (id: number): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SCOPE_OPTIONS = [
|
export const SCOPE_OPTIONS = [
|
||||||
{ value: "nezha:inventory:read", label: "Inventory: read", desc: "List servers & groups (server.list)" },
|
{
|
||||||
{ value: "nezha:inventory:delete", label: "Inventory: delete", desc: "Delete servers & server groups" },
|
value: "nezha:inventory:read",
|
||||||
|
label: "Inventory: read",
|
||||||
|
desc: "List servers & groups (server.list)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "nezha:inventory:delete",
|
||||||
|
label: "Inventory: delete",
|
||||||
|
desc: "Delete servers & server groups",
|
||||||
|
},
|
||||||
{ value: "nezha:inventory:*", label: "Inventory: all", desc: "List + delete servers & groups" },
|
{ value: "nezha:inventory:*", label: "Inventory: all", desc: "List + delete servers & groups" },
|
||||||
{ value: "nezha:server:read", label: "Server: read", desc: "Inspect a server, read files & metrics (needs server id)" },
|
{
|
||||||
|
value: "nezha:server:read",
|
||||||
|
label: "Server: read",
|
||||||
|
desc: "Inspect a server, read files & metrics (needs server id)",
|
||||||
|
},
|
||||||
{ value: "nezha:server:write", label: "Server: write", desc: "Edit a server, push files" },
|
{ value: "nezha:server:write", label: "Server: write", desc: "Edit a server, push files" },
|
||||||
{ value: "nezha:server:delete", label: "Server: delete", desc: "Delete files on a server" },
|
{ value: "nezha:server:delete", label: "Server: delete", desc: "Delete files on a server" },
|
||||||
{ value: "nezha:server:exec", label: "Server: exec", desc: "Run shell commands on servers" },
|
{ value: "nezha:server:exec", label: "Server: exec", desc: "Run shell commands on servers" },
|
||||||
{ value: "nezha:server:*", label: "Server: all", desc: "Every server operation (read+write+delete+exec)" },
|
{
|
||||||
{ value: "nezha:service:read", label: "Service monitor: read", desc: "List service monitors & history" },
|
value: "nezha:server:*",
|
||||||
{ value: "nezha:service:write", label: "Service monitor: write", desc: "Create / edit service monitors" },
|
label: "Server: all",
|
||||||
{ value: "nezha:service:delete", label: "Service monitor: delete", desc: "Delete service monitors" },
|
desc: "Every server operation (read+write+delete+exec)",
|
||||||
{ value: "nezha:service:*", label: "Service monitor: all", desc: "Every service monitor permission" },
|
},
|
||||||
|
{
|
||||||
|
value: "nezha:service:read",
|
||||||
|
label: "Service monitor: read",
|
||||||
|
desc: "List service monitors & history",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "nezha:service:write",
|
||||||
|
label: "Service monitor: write",
|
||||||
|
desc: "Create / edit service monitors",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "nezha:service:delete",
|
||||||
|
label: "Service monitor: delete",
|
||||||
|
desc: "Delete service monitors",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "nezha:service:*",
|
||||||
|
label: "Service monitor: all",
|
||||||
|
desc: "Every service monitor permission",
|
||||||
|
},
|
||||||
{ value: "nezha:alertrule:read", label: "Alert rule: read", desc: "List alert rules" },
|
{ value: "nezha:alertrule:read", label: "Alert rule: read", desc: "List alert rules" },
|
||||||
{ value: "nezha:alertrule:write", label: "Alert rule: write", desc: "Create / edit alert rules" },
|
{
|
||||||
|
value: "nezha:alertrule:write",
|
||||||
|
label: "Alert rule: write",
|
||||||
|
desc: "Create / edit alert rules",
|
||||||
|
},
|
||||||
{ value: "nezha:alertrule:delete", label: "Alert rule: delete", desc: "Delete alert rules" },
|
{ value: "nezha:alertrule:delete", label: "Alert rule: delete", desc: "Delete alert rules" },
|
||||||
{ value: "nezha:alertrule:*", label: "Alert rule: all", desc: "Every alert-rule permission" },
|
{ value: "nezha:alertrule:*", label: "Alert rule: all", desc: "Every alert-rule permission" },
|
||||||
{ value: "nezha:cron:read", label: "Cron: read", desc: "List scheduled tasks" },
|
{ value: "nezha:cron:read", label: "Cron: read", desc: "List scheduled tasks" },
|
||||||
@@ -69,13 +105,41 @@ export const SCOPE_OPTIONS = [
|
|||||||
{ value: "nezha:cron:exec", label: "Cron: trigger", desc: "Manually trigger scheduled tasks" },
|
{ value: "nezha:cron:exec", label: "Cron: trigger", desc: "Manually trigger scheduled tasks" },
|
||||||
{ value: "nezha:cron:*", label: "Cron: all", desc: "Every cron permission" },
|
{ value: "nezha:cron:*", label: "Cron: all", desc: "Every cron permission" },
|
||||||
{ value: "nezha:notification:read", label: "Notification: read", desc: "List notifications" },
|
{ value: "nezha:notification:read", label: "Notification: read", desc: "List notifications" },
|
||||||
{ value: "nezha:notification:write", label: "Notification: write", desc: "Create / edit notifications" },
|
{
|
||||||
{ value: "nezha:notification:delete", label: "Notification: delete", desc: "Delete notifications" },
|
value: "nezha:notification:write",
|
||||||
{ value: "nezha:notification:*", label: "Notification: all", desc: "Every notification permission" },
|
label: "Notification: write",
|
||||||
{ value: "nezha:notification-group:read", label: "Notification group: read", desc: "List notification groups" },
|
desc: "Create / edit notifications",
|
||||||
{ value: "nezha:notification-group:write", label: "Notification group: write", desc: "Create / edit groups" },
|
},
|
||||||
{ value: "nezha:notification-group:delete", label: "Notification group: delete", desc: "Delete groups" },
|
{
|
||||||
{ value: "nezha:notification-group:*", label: "Notification group: all", desc: "Every notification-group permission" },
|
value: "nezha:notification:delete",
|
||||||
|
label: "Notification: delete",
|
||||||
|
desc: "Delete notifications",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "nezha:notification:*",
|
||||||
|
label: "Notification: all",
|
||||||
|
desc: "Every notification permission",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "nezha:notification-group:read",
|
||||||
|
label: "Notification group: read",
|
||||||
|
desc: "List notification groups",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "nezha:notification-group:write",
|
||||||
|
label: "Notification group: write",
|
||||||
|
desc: "Create / edit groups",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "nezha:notification-group:delete",
|
||||||
|
label: "Notification group: delete",
|
||||||
|
desc: "Delete groups",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "nezha:notification-group:*",
|
||||||
|
label: "Notification group: all",
|
||||||
|
desc: "Every notification-group permission",
|
||||||
|
},
|
||||||
{ value: "nezha:ddns:read", label: "DDNS: read", desc: "List DDNS profiles" },
|
{ value: "nezha:ddns:read", label: "DDNS: read", desc: "List DDNS profiles" },
|
||||||
{ value: "nezha:ddns:write", label: "DDNS: write", desc: "Create / edit DDNS profiles" },
|
{ value: "nezha:ddns:write", label: "DDNS: write", desc: "Create / edit DDNS profiles" },
|
||||||
{ value: "nezha:ddns:delete", label: "DDNS: delete", desc: "Delete DDNS profiles" },
|
{ value: "nezha:ddns:delete", label: "DDNS: delete", desc: "Delete DDNS profiles" },
|
||||||
@@ -86,21 +150,27 @@ export const SCOPE_OPTIONS = [
|
|||||||
{ value: "nezha:nat:*", label: "NAT: all", desc: "Every NAT permission" },
|
{ value: "nezha:nat:*", label: "NAT: all", desc: "Every NAT permission" },
|
||||||
{ value: "nezha:transfer:read", label: "Transfer: read", desc: "Read server transfer state" },
|
{ value: "nezha:transfer:read", label: "Transfer: read", desc: "Read server transfer state" },
|
||||||
{ value: "nezha:transfer:write", label: "Transfer: write", desc: "Cancel / retry transfers" },
|
{ value: "nezha:transfer:write", label: "Transfer: write", desc: "Cancel / retry transfers" },
|
||||||
{ value: "nezha:transfer:delete", label: "Transfer: delete", desc: "Delete server transfer records" },
|
{
|
||||||
|
value: "nezha:transfer:delete",
|
||||||
|
label: "Transfer: delete",
|
||||||
|
desc: "Delete server transfer records",
|
||||||
|
},
|
||||||
{ value: "nezha:transfer:*", label: "Transfer: all", desc: "Every transfer permission" },
|
{ value: "nezha:transfer:*", label: "Transfer: all", desc: "Every transfer permission" },
|
||||||
{ value: "nezha:admin:*", label: "Admin: all (admin only)", desc: "User / WAF / Setting / Online-user management" },
|
{
|
||||||
|
value: "nezha:admin:*",
|
||||||
|
label: "Admin: all (admin only)",
|
||||||
|
desc: "User / WAF / Setting / Online-user management",
|
||||||
|
},
|
||||||
{ value: "nezha:*", label: "Everything (admin only)", desc: "Full access to all resources" },
|
{ value: "nezha:*", label: "Everything (admin only)", desc: "Full access to all resources" },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type Scope = (typeof SCOPE_OPTIONS)[number]["value"]
|
export type Scope = (typeof SCOPE_OPTIONS)[number]["value"]
|
||||||
|
|
||||||
export type ParseServerIDsResult =
|
export type ParseServerIDsResult =
|
||||||
| { ok: true; value: number[] | undefined }
|
{ ok: true; value: number[] | undefined } | { ok: false; error: string }
|
||||||
| { ok: false; error: string }
|
|
||||||
|
|
||||||
export type ParseExpiresInDaysResult =
|
export type ParseExpiresInDaysResult =
|
||||||
| { ok: true; value: number | undefined }
|
{ ok: true; value: number | undefined } | { ok: false; error: string }
|
||||||
| { ok: false; error: string }
|
|
||||||
|
|
||||||
// Validates the "expires in days" field before it is sent to the backend.
|
// Validates the "expires in days" field before it is sent to the backend.
|
||||||
// The backend model field is `ExpiresInDays int`, so a fractional value would
|
// The backend model field is `ExpiresInDays int`, so a fractional value would
|
||||||
@@ -130,7 +200,8 @@ export function parseServerIDsInput(raw: string): ParseServerIDsResult {
|
|||||||
// Server IDs are uint64 on the backend; reject values that lose
|
// Server IDs are uint64 on the backend; reject values that lose
|
||||||
// precision as a JS number, otherwise the PAT could bind to a
|
// precision as a JS number, otherwise the PAT could bind to a
|
||||||
// different server than the operator typed.
|
// different server than the operator typed.
|
||||||
if (!Number.isSafeInteger(n) || n <= 0) return { ok: false, error: `invalid server id: ${p}` }
|
if (!Number.isSafeInteger(n) || n <= 0)
|
||||||
|
return { ok: false, error: `invalid server id: ${p}` }
|
||||||
if (seen.has(n)) continue
|
if (seen.has(n)) continue
|
||||||
seen.add(n)
|
seen.add(n)
|
||||||
out.push(n)
|
out.push(n)
|
||||||
|
|||||||
+5
-1
@@ -26,7 +26,11 @@ export const deleteServer = async (id: number[]): Promise<void> => {
|
|||||||
export const batchMoveServer = async (
|
export const batchMoveServer = async (
|
||||||
data: ModelBatchMoveServerForm,
|
data: ModelBatchMoveServerForm,
|
||||||
): Promise<ModelBatchMoveServerResult[]> => {
|
): Promise<ModelBatchMoveServerResult[]> => {
|
||||||
return fetcher<ModelBatchMoveServerResult[]>(FetcherMethod.POST, "/api/v1/batch-move/server", data)
|
return fetcher<ModelBatchMoveServerResult[]>(
|
||||||
|
FetcherMethod.POST,
|
||||||
|
"/api/v1/batch-move/server",
|
||||||
|
data,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const forceUpdateServer = async (id: number[]): Promise<ModelServerTaskResponse> => {
|
export const forceUpdateServer = async (id: number[]): Promise<ModelServerTaskResponse> => {
|
||||||
|
|||||||
@@ -195,10 +195,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
|||||||
<DialogDescription />
|
<DialogDescription />
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="space-y-2 my-2"
|
|
||||||
>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
|
|||||||
@@ -71,7 +71,10 @@ const arraysEqual = (a: Uint8Array, b: Uint8Array) => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl, ...props }) => {
|
export const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({
|
||||||
|
wsUrl,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const fmRef = useRef<HTMLDivElement>(null)
|
const fmRef = useRef<HTMLDivElement>(null)
|
||||||
const wsRef = useRef<WebSocket | null>(null)
|
const wsRef = useRef<WebSocket | null>(null)
|
||||||
@@ -342,7 +345,10 @@ export const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({
|
|||||||
try {
|
try {
|
||||||
await copyToClipboard(formatPath(currentPath))
|
await copyToClipboard(formatPath(currentPath))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const description = error instanceof Error ? error.message : t("Results.UnExpectedError")
|
const description =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: t("Results.UnExpectedError")
|
||||||
toast("FM" + " " + t("Error"), {
|
toast("FM" + " " + t("Error"), {
|
||||||
description,
|
description,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
type NotificationFormInput = z.input<typeof notificationFormSchema>
|
type NotificationFormInput = z.input<typeof notificationFormSchema>
|
||||||
type NotificationFormData = z.output<typeof notificationFormSchema>
|
type NotificationFormData = z.output<typeof notificationFormSchema>
|
||||||
type NotificationDefaults = ModelNotification & Partial<Pick<NotificationFormData, "skip_check">>
|
type NotificationDefaults = ModelNotification &
|
||||||
|
Partial<Pick<NotificationFormData, "skip_check">>
|
||||||
const notificationDefaults: NotificationDefaults | undefined = data
|
const notificationDefaults: NotificationDefaults | undefined = data
|
||||||
|
|
||||||
const form = useForm<NotificationFormInput, unknown, NotificationFormData>({
|
const form = useForm<NotificationFormInput, unknown, NotificationFormData>({
|
||||||
@@ -133,10 +134,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
<DialogDescription />
|
<DialogDescription />
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="space-y-2 my-2"
|
|
||||||
>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
|
|||||||
@@ -467,7 +467,9 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
{publicNoteObj.billingDataMod
|
{publicNoteObj.billingDataMod
|
||||||
?.startDate
|
?.startDate
|
||||||
? new Date(
|
? new Date(
|
||||||
publicNoteObj.billingDataMod!.startDate!,
|
publicNoteObj
|
||||||
|
.billingDataMod!
|
||||||
|
.startDate!,
|
||||||
).toLocaleDateString()
|
).toLocaleDateString()
|
||||||
: "YYYY-MM-DD"}
|
: "YYYY-MM-DD"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -492,7 +494,9 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
.billingDataMod
|
.billingDataMod
|
||||||
?.startDate
|
?.startDate
|
||||||
? new Date(
|
? new Date(
|
||||||
publicNoteObj.billingDataMod!.startDate!,
|
publicNoteObj
|
||||||
|
.billingDataMod!
|
||||||
|
.startDate!,
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ const badgeVariants = cva(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
extends HTMLAttributes<HTMLDivElement>,
|
extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||||
VariantProps<typeof badgeVariants> {}
|
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ const buttonVariants = cva(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||||
VariantProps<typeof buttonVariants> {
|
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const FormField = <
|
|||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||||
>({
|
>({
|
||||||
...props
|
...props
|
||||||
}: ControllerProps<TFieldValues, TName>) => {
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
return (
|
return (
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
<Controller {...props} />
|
<Controller {...props} />
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
type SVGProps = React.ComponentPropsWithoutRef<"svg">
|
type SVGProps = React.ComponentPropsWithoutRef<"svg">
|
||||||
type IconMarkupState =
|
type IconMarkupState = { status: "ready"; icon: ParsedIconMarkup } | { status: "missing" }
|
||||||
| { status: "ready"; icon: ParsedIconMarkup }
|
|
||||||
| { status: "missing" }
|
|
||||||
|
|
||||||
type ParsedIconMarkup = {
|
type ParsedIconMarkup = {
|
||||||
title?: string
|
title?: string
|
||||||
@@ -26,7 +24,10 @@ const iconLoadersBySlug = Object.fromEntries(
|
|||||||
const iconMarkupCache = new Map<string, IconMarkupState>()
|
const iconMarkupCache = new Map<string, IconMarkupState>()
|
||||||
|
|
||||||
function toProviderSlug(provider: string) {
|
function toProviderSlug(provider: string) {
|
||||||
return provider.trim().replace(/[^a-z0-9]+/gi, "").toLowerCase()
|
return provider
|
||||||
|
.trim()
|
||||||
|
.replace(/[^a-z0-9]+/gi, "")
|
||||||
|
.toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadProviderIconMarkup(provider: string) {
|
function loadProviderIconMarkup(provider: string) {
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ const multiSelectVariants = cva(
|
|||||||
* Props for MultiSelect component
|
* Props for MultiSelect component
|
||||||
*/
|
*/
|
||||||
interface MultiSelectProps
|
interface MultiSelectProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends
|
||||||
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof multiSelectVariants> {
|
VariantProps<typeof multiSelectVariants> {
|
||||||
/**
|
/**
|
||||||
* An array of option objects to be displayed in the multi-select component.
|
* An array of option objects to be displayed in the multi-select component.
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ const sheetVariants = cva(
|
|||||||
)
|
)
|
||||||
|
|
||||||
interface SheetContentProps
|
interface SheetContentProps
|
||||||
extends ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
extends
|
||||||
|
ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||||
VariantProps<typeof sheetVariants> {
|
VariantProps<typeof sheetVariants> {
|
||||||
setOpen: Dispatch<SetStateAction<boolean>>
|
setOpen: Dispatch<SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const login = useCallback(async (username: string, password: string) => {
|
const login = useCallback(
|
||||||
|
async (username: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
await loginRequest(username, password)
|
await loginRequest(username, password)
|
||||||
const user = await getProfile()
|
const user = await getProfile()
|
||||||
@@ -73,7 +74,9 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
// mount probe; clear loading so ProtectedRoute stops blanking.
|
// mount probe; clear loading so ProtectedRoute stops blanking.
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [navigate, setProfile, t])
|
},
|
||||||
|
[navigate, setProfile, t],
|
||||||
|
)
|
||||||
|
|
||||||
const loginOauth2 = useCallback(async () => {
|
const loginOauth2 = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+1
-1
@@ -117,5 +117,5 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@apply rounded-full border-[1px] border-solid border-transparent bg-border bg-clip-padding;
|
@apply bg-border rounded-full border-[1px] border-solid border-transparent bg-clip-padding;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,10 @@ export default function ApiTokensPage() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
) : !data || data.length === 0 ? (
|
) : !data || data.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="h-24 text-center text-muted-foreground">
|
<TableCell
|
||||||
|
colSpan={6}
|
||||||
|
className="h-24 text-center text-muted-foreground"
|
||||||
|
>
|
||||||
{t("NoResults")}
|
{t("NoResults")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -119,10 +122,14 @@ export default function ApiTokensPage() {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs">
|
<TableCell className="text-xs">
|
||||||
{tok.server_ids?.length ? tok.server_ids.join(", ") : t("ApiTokenAllServers")}
|
{tok.server_ids?.length
|
||||||
|
? tok.server_ids.join(", ")
|
||||||
|
: t("ApiTokenAllServers")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs">
|
<TableCell className="text-xs">
|
||||||
{tok.expires_at ? new Date(tok.expires_at).toLocaleString() : t("ApiTokenNever")}
|
{tok.expires_at
|
||||||
|
? new Date(tok.expires_at).toLocaleString()
|
||||||
|
: t("ApiTokenNever")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs">
|
<TableCell className="text-xs">
|
||||||
{tok.last_used_at
|
{tok.last_used_at
|
||||||
@@ -239,7 +246,8 @@ function CreateApiTokenDialog({
|
|||||||
<Label>{t("Scopes")}</Label>
|
<Label>{t("Scopes")}</Label>
|
||||||
<div className="max-h-72 space-y-2 overflow-y-auto pr-2">
|
<div className="max-h-72 space-y-2 overflow-y-auto pr-2">
|
||||||
{SCOPE_OPTIONS.map((s) => {
|
{SCOPE_OPTIONS.map((s) => {
|
||||||
const adminOnly = s.value === "nezha:*" || s.value === "nezha:admin:*"
|
const adminOnly =
|
||||||
|
s.value === "nezha:*" || s.value === "nezha:admin:*"
|
||||||
const disabled = adminOnly && !isAdmin
|
const disabled = adminOnly && !isAdmin
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
@@ -248,12 +256,16 @@ function CreateApiTokenDialog({
|
|||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={scopes.includes(s.value)}
|
checked={scopes.includes(s.value)}
|
||||||
onCheckedChange={() => !disabled && toggleScope(s.value)}
|
onCheckedChange={() =>
|
||||||
|
!disabled && toggleScope(s.value)
|
||||||
|
}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-mono text-xs">{s.value}</span>
|
<span className="font-mono text-xs">{s.value}</span>
|
||||||
<span className="text-xs text-muted-foreground">{s.desc}</span>
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{s.desc}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -224,9 +224,13 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<div className="px-8 py-1">
|
<div className="px-8 py-1">
|
||||||
{safeExternalHref(template.repository) ? (
|
{safeExternalHref(
|
||||||
|
template.repository,
|
||||||
|
) ? (
|
||||||
<a
|
<a
|
||||||
href={safeExternalHref(template.repository)}
|
href={safeExternalHref(
|
||||||
|
template.repository,
|
||||||
|
)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
|
className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
|
||||||
@@ -552,9 +556,7 @@ export default function SettingsPage() {
|
|||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
<Label className="text-sm">
|
<Label className="text-sm">{t("EnableMCP")}</Label>
|
||||||
{t("EnableMCP")}
|
|
||||||
</Label>
|
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { swrFetcher } from "@/api/api"
|
import { swrFetcher } from "@/api/api"
|
||||||
import { cancelServerTransfer, retryServerTransfer } from "@/api/transfer"
|
import { cancelServerTransfer, retryServerTransfer } from "@/api/transfer"
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -45,10 +45,7 @@ export default function TransferPage() {
|
|||||||
const { profile } = useAuth()
|
const { profile } = useAuth()
|
||||||
const isAdmin = profile?.role === 0
|
const isAdmin = profile?.role === 0
|
||||||
const callerID = profile?.id
|
const callerID = profile?.id
|
||||||
const { data, mutate, error } = useSWR<ModelServerTransfer[]>(
|
const { data, mutate, error } = useSWR<ModelServerTransfer[]>("/api/v1/transfer", swrFetcher)
|
||||||
"/api/v1/transfer",
|
|
||||||
swrFetcher,
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -182,9 +179,7 @@ export default function TransferPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<h1 className="mt-6 text-2xl font-semibold">{t("Transfer.Title")}</h1>
|
<h1 className="mt-6 text-2xl font-semibold">{t("Transfer.Title")}</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-1 max-w-2xl">
|
<p className="text-sm text-muted-foreground mt-1 max-w-2xl">{t("Transfer.PageHint")}</p>
|
||||||
{t("Transfer.PageHint")}
|
|
||||||
</p>
|
|
||||||
<Table className="mt-6">
|
<Table className="mt-6">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ const realFetch = globalThis.fetch
|
|||||||
|
|
||||||
function mockFetch(payload: unknown, ok = true, success = true) {
|
function mockFetch(payload: unknown, ok = true, success = true) {
|
||||||
globalThis.fetch = vi.fn(async () => {
|
globalThis.fetch = vi.fn(async () => {
|
||||||
return new Response(JSON.stringify({ success, error: success ? "" : "boom", data: payload }), {
|
return new Response(
|
||||||
|
JSON.stringify({ success, error: success ? "" : "boom", data: payload }),
|
||||||
|
{
|
||||||
status: ok ? 200 : 500,
|
status: ok ? 200 : 500,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}) as unknown as typeof fetch
|
}) as unknown as typeof fetch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +97,9 @@ test("createApiToken POSTs /api/v1/api-tokens and serializes scopes / server_ids
|
|||||||
|
|
||||||
test("createApiToken surfaces server error via thrown Error", async () => {
|
test("createApiToken surfaces server error via thrown Error", async () => {
|
||||||
mockFetch(null, true, false)
|
mockFetch(null, true, false)
|
||||||
await expect(
|
await expect(createApiToken({ name: "x", scopes: ["nezha:server:read"] })).rejects.toThrow(
|
||||||
createApiToken({ name: "x", scopes: ["nezha:server:read"] }),
|
"boom",
|
||||||
).rejects.toThrow("boom")
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("listApiTokens normalizes null scopes to an empty array so the table cannot crash", async () => {
|
test("listApiTokens normalizes null scopes to an empty array so the table cannot crash", async () => {
|
||||||
@@ -107,9 +110,7 @@ test("listApiTokens normalizes null scopes to an empty array so the table cannot
|
|||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
data: [
|
data: [{ id: 1, name: "legacy", scopes: null, created_at: "2025-01-01T00:00:00Z" }],
|
||||||
{ id: 1, name: "legacy", scopes: null, created_at: "2025-01-01T00:00:00Z" },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ const setProfileSpy = vi.fn((p: any) => {
|
|||||||
|
|
||||||
vi.mock("./useMainStore", () => ({}))
|
vi.mock("./useMainStore", () => ({}))
|
||||||
vi.mock("@/hooks/useMainStore", () => ({
|
vi.mock("@/hooks/useMainStore", () => ({
|
||||||
useMainStore: (selector: any) =>
|
useMainStore: (selector: any) => selector({ profile: profileStore, setProfile: setProfileSpy }),
|
||||||
selector({ profile: profileStore, setProfile: setProfileSpy }),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock("react-i18next", () => ({
|
vi.mock("react-i18next", () => ({
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ const setProfileSpy = vi.fn((p: any) => {
|
|||||||
|
|
||||||
vi.mock("./useMainStore", () => ({}))
|
vi.mock("./useMainStore", () => ({}))
|
||||||
vi.mock("@/hooks/useMainStore", () => ({
|
vi.mock("@/hooks/useMainStore", () => ({
|
||||||
useMainStore: (selector: any) =>
|
useMainStore: (selector: any) => selector({ profile: profileStore, setProfile: setProfileSpy }),
|
||||||
selector({ profile: profileStore, setProfile: setProfileSpy }),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock("react-i18next", () => ({
|
vi.mock("react-i18next", () => ({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BatchMoveServerIcon } from "@/components/batch-move-server-icon"
|
||||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||||
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
||||||
|
|
||||||
@@ -26,8 +27,6 @@ vi.mock("@/api/server", () => ({
|
|||||||
batchMoveServer: (...args: unknown[]) => batchMoveServer(...args),
|
batchMoveServer: (...args: unknown[]) => batchMoveServer(...args),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import { BatchMoveServerIcon } from "@/components/batch-move-server-icon"
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
toastCalls.length = 0
|
toastCalls.length = 0
|
||||||
batchMoveServer.mockReset()
|
batchMoveServer.mockReset()
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ test("auto refresh-token uses POST (backend route is POST)", async () => {
|
|||||||
// Revoke (DELETE) commonly returns 204 / empty body. The fetcher must not
|
// Revoke (DELETE) commonly returns 204 / empty body. The fetcher must not
|
||||||
// blow up on response.json() of an empty body and must resolve successfully.
|
// blow up on response.json() of an empty body and must resolve successfully.
|
||||||
test("DELETE tolerates an empty 204 response body", async () => {
|
test("DELETE tolerates an empty 204 response body", async () => {
|
||||||
globalThis.fetch = vi.fn(async () => new Response(null, { status: 204 })) as unknown as typeof fetch
|
globalThis.fetch = vi.fn(
|
||||||
|
async () => new Response(null, { status: 204 }),
|
||||||
|
) as unknown as typeof fetch
|
||||||
|
|
||||||
await expect(fetcher(FetcherMethod.DELETE, "/api/v1/api-tokens/9")).resolves.toBeUndefined()
|
await expect(fetcher(FetcherMethod.DELETE, "/api/v1/api-tokens/9")).resolves.toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { act, render, waitFor } from "@testing-library/react"
|
import { act, render, waitFor } from "@testing-library/react"
|
||||||
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
||||||
|
|
||||||
|
import { FMComponent } from "../components/fm"
|
||||||
|
|
||||||
type DivProps = React.ComponentPropsWithoutRef<"div"> & { asChild?: boolean }
|
type DivProps = React.ComponentPropsWithoutRef<"div"> & { asChild?: boolean }
|
||||||
type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
|
type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
@@ -106,8 +108,6 @@ vi.mock("@/lib/utils", () => ({
|
|||||||
formatPath: (path: string) => path,
|
formatPath: (path: string) => path,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import { FMComponent } from "../components/fm"
|
|
||||||
|
|
||||||
const webSockets: MockWebSocket[] = []
|
const webSockets: MockWebSocket[] = []
|
||||||
|
|
||||||
class MockWebSocket extends EventTarget implements WebSocket {
|
class MockWebSocket extends EventTarget implements WebSocket {
|
||||||
@@ -235,7 +235,10 @@ test("FM websocket lifecycle reuses the socket on path changes", async () => {
|
|||||||
|
|
||||||
socket.sent.length = 0
|
socket.sent.length = 0
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await socket.onmessage?.call(socket, new MessageEvent("message", { data: encodeCompleteMessage() }))
|
await socket.onmessage?.call(
|
||||||
|
socket,
|
||||||
|
new MessageEvent("message", { data: encodeCompleteMessage() }),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(webSockets).toHaveLength(1)
|
expect(webSockets).toHaveLength(1)
|
||||||
|
|||||||
@@ -32,15 +32,11 @@ function renderAtPath(path: string, child: React.ReactNode) {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard/login"
|
path="/dashboard/login"
|
||||||
element={
|
element={<ProtectedRoute>{child}</ProtectedRoute>}
|
||||||
<ProtectedRoute>{child}</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard/*"
|
path="/dashboard/*"
|
||||||
element={
|
element={<ProtectedRoute>{child}</ProtectedRoute>}
|
||||||
<ProtectedRoute>{child}</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@@ -50,18 +46,12 @@ function renderAtPath(path: string, child: React.ReactNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("ProtectedRoute does not mount children for protected paths while auth is loading", async () => {
|
test("ProtectedRoute does not mount children for protected paths while auth is loading", async () => {
|
||||||
await renderAtPath(
|
await renderAtPath("/dashboard", <div data-testid="protected-child">protected</div>)
|
||||||
"/dashboard",
|
|
||||||
<div data-testid="protected-child">protected</div>,
|
|
||||||
)
|
|
||||||
expect(document.querySelector("[data-testid='protected-child']")).toBeNull()
|
expect(document.querySelector("[data-testid='protected-child']")).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("ProtectedRoute renders children on the login page even while auth is loading", async () => {
|
test("ProtectedRoute renders children on the login page even while auth is loading", async () => {
|
||||||
await renderAtPath(
|
await renderAtPath("/dashboard/login", <div data-testid="login-child">login</div>)
|
||||||
"/dashboard/login",
|
|
||||||
<div data-testid="login-child">login</div>,
|
|
||||||
)
|
|
||||||
expect(document.querySelector("[data-testid='login-child']")).not.toBeNull()
|
expect(document.querySelector("[data-testid='login-child']")).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -100,9 +90,6 @@ test("ProtectedRoute redirects unauthenticated users without mounting protected
|
|||||||
test("ProtectedRoute renders children once an authenticated profile resolves", async () => {
|
test("ProtectedRoute renders children once an authenticated profile resolves", async () => {
|
||||||
mockLoading = false
|
mockLoading = false
|
||||||
mockProfile = { id: 1, role: 0 }
|
mockProfile = { id: 1, role: 0 }
|
||||||
await renderAtPath(
|
await renderAtPath("/dashboard", <div data-testid="protected-child">protected</div>)
|
||||||
"/dashboard",
|
|
||||||
<div data-testid="protected-child">protected</div>,
|
|
||||||
)
|
|
||||||
expect(document.querySelector("[data-testid='protected-child']")).not.toBeNull()
|
expect(document.querySelector("[data-testid='protected-child']")).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ vi.mock("@/hooks/useNotfication", () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const updateSettings = vi.fn()
|
const updateSettings = vi.fn()
|
||||||
vi.mock("@/api/settings", () => ({ updateSettings: (...args: unknown[]) => updateSettings(...args) }))
|
vi.mock("@/api/settings", () => ({
|
||||||
|
updateSettings: (...args: unknown[]) => updateSettings(...args),
|
||||||
|
}))
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
toastCalls.length = 0
|
toastCalls.length = 0
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ModelServerTransfer } from "@/types"
|
||||||
import { act, render, screen, waitFor } from "@testing-library/react"
|
import { act, render, screen, waitFor } from "@testing-library/react"
|
||||||
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
||||||
|
|
||||||
@@ -47,8 +48,6 @@ vi.mock("@/hooks/useMainStore", () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import type { ModelServerTransfer } from "@/types"
|
|
||||||
|
|
||||||
let mockedRows: ModelServerTransfer[] = []
|
let mockedRows: ModelServerTransfer[] = []
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -127,10 +126,7 @@ test("non-admin member who is only the ToUserID or InitiatorID sees neither Canc
|
|||||||
|
|
||||||
test("admin sees both Cancel for pending and Retry for terminal", async () => {
|
test("admin sees both Cancel for pending and Retry for terminal", async () => {
|
||||||
mockProfile = { id: 1, role: 0 }
|
mockProfile = { id: 1, role: 0 }
|
||||||
mockedRows = [
|
mockedRows = [makeRow({ id: 1, status: 0 }), makeRow({ id: 2, status: 2 })]
|
||||||
makeRow({ id: 1, status: 0 }),
|
|
||||||
makeRow({ id: 2, status: 2 }),
|
|
||||||
]
|
|
||||||
|
|
||||||
await renderPage()
|
await renderPage()
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,10 @@ test("an API token can authenticate /mcp", async ({ page }) => {
|
|||||||
headers: await csrfHeaders(page),
|
headers: await csrfHeaders(page),
|
||||||
data: { ...baseSettings, enable_mcp: true },
|
data: { ...baseSettings, enable_mcp: true },
|
||||||
})
|
})
|
||||||
expect(enableResp.ok(), "PATCH /api/v1/setting must succeed to enable MCP for the test").toBeTruthy()
|
expect(
|
||||||
|
enableResp.ok(),
|
||||||
|
"PATCH /api/v1/setting must succeed to enable MCP for the test",
|
||||||
|
).toBeTruthy()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiResp = await page.request.post("/api/v1/api-tokens", {
|
const apiResp = await page.request.post("/api/v1/api-tokens", {
|
||||||
|
|||||||
+12
-2
@@ -1,6 +1,13 @@
|
|||||||
import { expect } from "@playwright/test"
|
import { expect } from "@playwright/test"
|
||||||
|
|
||||||
import { csrfRequest, defaultAdmin, expectAuthenticated, expectUnauthenticated, loginAs, test } from "./fixtures"
|
import {
|
||||||
|
csrfRequest,
|
||||||
|
defaultAdmin,
|
||||||
|
expectAuthenticated,
|
||||||
|
expectUnauthenticated,
|
||||||
|
loginAs,
|
||||||
|
test,
|
||||||
|
} from "./fixtures"
|
||||||
|
|
||||||
test("login persists session via cookie and getProfile succeeds", async ({ page }) => {
|
test("login persists session via cookie and getProfile succeeds", async ({ page }) => {
|
||||||
await loginAs(page, defaultAdmin)
|
await loginAs(page, defaultAdmin)
|
||||||
@@ -51,7 +58,10 @@ test("password change rotates TokenVersion and revokes existing session", async
|
|||||||
reject_password: false,
|
reject_password: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(restoreResp.ok(), "password restore must succeed so other suites can still log in").toBeTruthy()
|
expect(
|
||||||
|
restoreResp.ok(),
|
||||||
|
"password restore must succeed so other suites can still log in",
|
||||||
|
).toBeTruthy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Page, Request, expect, test as base } from "@playwright/test"
|
import { Page, Request, test as base, expect } from "@playwright/test"
|
||||||
|
|
||||||
export type LoginContext = {
|
export type LoginContext = {
|
||||||
username: string
|
username: string
|
||||||
@@ -39,7 +39,8 @@ export async function csrfHeaders(page: Page): Promise<Record<string, string>> {
|
|||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
async () => {
|
async () => {
|
||||||
value = (await page.context().cookies()).find((c) => c.name === "nz-csrf")?.value ?? ""
|
value =
|
||||||
|
(await page.context().cookies()).find((c) => c.name === "nz-csrf")?.value ?? ""
|
||||||
return value
|
return value
|
||||||
},
|
},
|
||||||
{ timeout: 10_000 },
|
{ timeout: 10_000 },
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ test("file manager creation only accepts POST", async ({ adminPage: page }) => {
|
|||||||
})
|
})
|
||||||
expect(postResp.status()).toBe(200)
|
expect(postResp.status()).toBe(200)
|
||||||
const body = await postResp.json()
|
const body = await postResp.json()
|
||||||
expect(body.success, "without a connected agent server the POST surfaces a Service error, but the route is reachable").not.toBe(true)
|
expect(
|
||||||
|
body.success,
|
||||||
|
"without a connected agent server the POST surfaces a Service error, but the route is reachable",
|
||||||
|
).not.toBe(true)
|
||||||
expect(body.error).toBeTruthy()
|
expect(body.error).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { expect } from "@playwright/test"
|
|||||||
|
|
||||||
import { csrfHeaders, test } from "./fixtures"
|
import { csrfHeaders, test } from "./fixtures"
|
||||||
|
|
||||||
test("server-group hides guest-empty groups from anonymous callers", async ({ adminPage: page, browser }) => {
|
test("server-group hides guest-empty groups from anonymous callers", async ({
|
||||||
|
adminPage: page,
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
const tag = Date.now().toString(36)
|
const tag = Date.now().toString(36)
|
||||||
const visibleName = `e2e-visible-${tag}`
|
const visibleName = `e2e-visible-${tag}`
|
||||||
const hiddenName = `e2e-hidden-${tag}`
|
const hiddenName = `e2e-hidden-${tag}`
|
||||||
@@ -12,7 +15,9 @@ test("server-group hides guest-empty groups from anonymous callers", async ({ ad
|
|||||||
// scenario boils down to "no group is guest-visible", which the assertion below still covers.
|
// scenario boils down to "no group is guest-visible", which the assertion below still covers.
|
||||||
const serversResp = await page.request.get("/api/v1/server")
|
const serversResp = await page.request.get("/api/v1/server")
|
||||||
expect(serversResp.ok()).toBeTruthy()
|
expect(serversResp.ok()).toBeTruthy()
|
||||||
const serversBody = (await serversResp.json()) as { data: Array<{ id: number; hide_for_guest?: boolean }> }
|
const serversBody = (await serversResp.json()) as {
|
||||||
|
data: Array<{ id: number; hide_for_guest?: boolean }>
|
||||||
|
}
|
||||||
const publicServer = serversBody.data?.find((s) => !s.hide_for_guest)
|
const publicServer = serversBody.data?.find((s) => !s.hide_for_guest)
|
||||||
|
|
||||||
const createdGroupIDs: number[] = []
|
const createdGroupIDs: number[] = []
|
||||||
@@ -40,9 +45,14 @@ test("server-group hides guest-empty groups from anonymous callers", async ({ ad
|
|||||||
data: Array<{ group: { name: string }; servers: number[] }>
|
data: Array<{ group: { name: string }; servers: number[] }>
|
||||||
}
|
}
|
||||||
const names = (guestBody.data || []).map((it) => it.group.name)
|
const names = (guestBody.data || []).map((it) => it.group.name)
|
||||||
expect(names, "guest must NOT see groups with zero visible servers").not.toContain(hiddenName)
|
expect(names, "guest must NOT see groups with zero visible servers").not.toContain(
|
||||||
|
hiddenName,
|
||||||
|
)
|
||||||
if (publicServer) {
|
if (publicServer) {
|
||||||
expect(names, "guest still sees groups that contain a guest-visible server").toContain(visibleName)
|
expect(
|
||||||
|
names,
|
||||||
|
"guest still sees groups that contain a guest-visible server",
|
||||||
|
).toContain(visibleName)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await guestCtx.close()
|
await guestCtx.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user