fix(network): защитить ключи WireGuard и режим просмотра операций
This commit is contained in:
1 parent
c8f9c83f45
commit
a404da768d
23 files changed
+1648
-36
No files matched your search
@@ -1497,7 +1497,7 @@ function FiltersPage() {
|
||||
const allTunnels = !isLive ? greTunnels : (greByServer[selectedServerId] ?? [])
|
||||
const selectedServer = allServers.find(s => s.id === selectedServerId) ?? allServers[0]
|
||||
const totalRules = rulesets.reduce((s, r) => s + r.rules.length, 0)
|
||||
const mutationsLocked = movePending || (isLive && (applyBusy || historyRestoring || rulesReading || readyServerId !== selectedServerId || liveStale || liveLoadState !== "idle"))
|
||||
const mutationsLocked = (isLive && !can("mm:filters:write")) || movePending || (isLive && (applyBusy || historyRestoring || rulesReading || readyServerId !== selectedServerId || liveStale || liveLoadState !== "idle"))
|
||||
|
||||
const filterRailItems = useMemo<ServerTileItem[]>(() => (
|
||||
allServers.map((s) => ({
|
||||
@@ -1548,6 +1548,7 @@ function FiltersPage() {
|
||||
nextRules: FilterRule[] | (() => Promise<FilterRule[]>),
|
||||
source: "apply" | "copy" = "apply",
|
||||
) => {
|
||||
if (isLive && !can("mm:filters:write")) return { ok: false, error: "Нет прав на изменение фильтров" }
|
||||
if (!mounted.current) return { ok: false, error: "Источник данных изменён" }
|
||||
if (applyLock.current || applyBusy || historyRestoring) return { ok: false, error: "Дождитесь завершения текущей операции" }
|
||||
if (isLive && (liveStale || liveLoadState !== "idle" || (serverId === selectedServerId && readyServerId !== serverId))) {
|
||||
@@ -1864,6 +1865,7 @@ function FiltersPage() {
|
||||
}
|
||||
banner={
|
||||
<>
|
||||
{isLive && !can("mm:filters:write") && <p role="status" className="border-b px-6 py-2 text-sm text-muted-foreground">Режим просмотра. Изменение фильтров недоступно.</p>}
|
||||
{isLive && liveLoadState === "error" && (
|
||||
<div className="shrink-0 border-b border-destructive/30 bg-destructive/10 px-6 py-2.5 text-xs text-destructive flex items-center gap-2">
|
||||
<AlertCircleIcon className="size-3.5 shrink-0" />
|
||||
|
||||
@@ -2169,7 +2169,7 @@ function FirewallPageInner() {
|
||||
|
||||
const scopedFailures = isLive ? failures.filter(f => (selectedServerId === ALL_SERVERS_ID || f.serverId === selectedServerId) && (!f.family || f.family === ipFamily)) : []
|
||||
const firewallIncomplete = scopedFailures.length > 0
|
||||
const firewallWriteBlocked = firewallIncomplete || (isLive && !readTrusted)
|
||||
const firewallWriteBlocked = (isLive && !can("mm:network:write")) || firewallIncomplete || (isLive && !readTrusted)
|
||||
const scopedSliceTimes = sliceTimes.filter(slice => (selectedServerId === ALL_SERVERS_ID || slice.serverId === selectedServerId) && slice.family === ipFamily)
|
||||
const freshness = firewallTimeRange(scopedSliceTimes)
|
||||
const totalEnabled = familyRules.filter((r) => r.enabled).length
|
||||
@@ -2530,6 +2530,7 @@ function FirewallPageInner() {
|
||||
showAll
|
||||
allCount={displayServers.length}
|
||||
loading={isLive && dataLoading && displayServers.length === 0}
|
||||
banner={isLive && !can("mm:network:write") ? <p role="status" className="border-b px-4 py-2 text-sm text-muted-foreground">Режим просмотра. Изменение конфигурации недоступно.</p> : undefined}
|
||||
header={
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Управление" }, { label: "Firewall" }]}
|
||||
|
||||
@@ -257,7 +257,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
|
||||
|
||||
const historyServerId = selectedServerId === ALL_SERVERS_ID ? null : selectedServerId
|
||||
const mutationsLocked = !isLive || mutateBusy || dataLoading || !hasLoaded || liveStale || historyRestoring
|
||||
const mutationsLocked = !isLive || !can("mm:network:write") || mutateBusy || dataLoading || !hasLoaded || liveStale || historyRestoring
|
||||
function canManage(serverId: string) {
|
||||
return !mutationsLocked && !failedServerIds.includes(serverId) && liveServers.some(s => s.id === serverId && s.enabled)
|
||||
}
|
||||
@@ -548,6 +548,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
showAll
|
||||
allCount={displayServers.length}
|
||||
loading={isLive && dataLoading && displayServers.length === 0}
|
||||
banner={isLive && !can("mm:network:write") ? <p role="status" className="border-b px-4 py-2 text-sm text-muted-foreground">Режим просмотра. Изменение конфигурации недоступно.</p> : undefined}
|
||||
header={
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Управление" }, { label: "GRE-туннели" }]}
|
||||
|
||||
@@ -320,7 +320,7 @@ function IpsecPage({ selectedServerId, setSelectedServerId, workspaceTab, setWor
|
||||
const failedServerIds = new Set(failures.map(f => f.serverId))
|
||||
const scopeUnknown = missingSelectedServer || isLive && (!hasLoaded || !!dataError || (effectiveServerId === ALL_SERVERS_ID ? failures.length > 0 : failedServerIds.has(effectiveServerId)))
|
||||
|
||||
const canManage = (serverId: string) => isLive && hasLoaded && !dataError && !inventoryError &&
|
||||
const canManage = (serverId: string) => isLive && can("mm:network:write") && hasLoaded && !dataError && !inventoryError &&
|
||||
!loading && !busy && !historyRestoring && !failedServerIds.has(serverId) && liveServers.some(s => s.id === serverId)
|
||||
const assertDeviceReady = (serverId: string) => {
|
||||
if (operationLock.current || !canManage(serverId)) throw new Error("Сначала подтвердите актуальное состояние устройства: повторите опрос IPsec")
|
||||
@@ -637,6 +637,7 @@ function IpsecPage({ selectedServerId, setSelectedServerId, workspaceTab, setWor
|
||||
showAll
|
||||
allCount={displayClients.length}
|
||||
loading={isLive && loading && displayServers.length === 0}
|
||||
banner={isLive && !can("mm:network:write") ? <p role="status" className="border-b px-4 py-2 text-sm text-muted-foreground">Режим просмотра. Изменение конфигурации недоступно.</p> : undefined}
|
||||
header={
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Управление" }, { label: "IPsec / IKEv2" }]}
|
||||
|
||||
@@ -184,6 +184,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
}) {
|
||||
const { mode, backendUrl } = useDataSource()
|
||||
const isLive = mode === "live"
|
||||
const canWrite = !isLive || can("mm:network:write")
|
||||
|
||||
const [liveIfaces, setLiveIfaces] = useState<WgIfaceWithServer[]>([])
|
||||
const [liveServers, setLiveServers] = useState<Server[]>([])
|
||||
@@ -446,6 +447,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
setCreateError(null)
|
||||
setBusy(true)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
const ep = parseEndpoint(form.peerEndpoint)
|
||||
await createWireGuardInterface(backendUrl, {
|
||||
serverId: form.serverId,
|
||||
@@ -495,6 +497,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
setImportError(null)
|
||||
setBusy(true)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
const res = await importWireGuard(backendUrl, {
|
||||
serverId: args.serverId,
|
||||
content: args.content,
|
||||
@@ -538,6 +541,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
setTogglePending(context)
|
||||
setToggleError(null)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
const result = await patchWireGuardInterface(backendUrl, iface.serverId, iface.rosId, {
|
||||
disabled: iface.enabled,
|
||||
})
|
||||
@@ -564,6 +568,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
setDeleteBusy(true)
|
||||
setDeleteError(null)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
if (pendingDelete.kind === "iface") {
|
||||
if (!pendingDelete.iface.rosId) {
|
||||
if (active.current) toast.info("Доступно только в live-режиме")
|
||||
@@ -599,6 +604,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
setPeerError(null)
|
||||
setBusy(true)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
const ep = parseEndpoint(form.endpoint)
|
||||
await createWireGuardPeer(backendUrl, {
|
||||
serverId: peerIface.serverId,
|
||||
@@ -630,6 +636,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
exportPending.current = epoch
|
||||
setExportBusy(true)
|
||||
try {
|
||||
if (!can("mm:network:write")) throw new Error("Нет прав на изменение сети или экспорт ключей")
|
||||
const peer = format === "peer-conf" ? selectWgExportPeer(exportIface.peers, exportPeerId) : undefined
|
||||
if (format === "peer-conf" && !peer) throw new Error("Выбранный пир не найден. Обновите список и выберите пира заново.")
|
||||
const res = await exportWireGuard(backendUrl, {
|
||||
@@ -671,7 +678,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
}
|
||||
|
||||
const emptyCreateAction = (
|
||||
<Button size="sm" disabled={!!urlError || !catalogKnown || loadError} onClick={() => { setCreateError(null); setCreateOpen(true) }}>
|
||||
<Button size="sm" disabled={!canWrite || !!urlError || !catalogKnown || loadError} onClick={() => { setCreateError(null); setCreateOpen(true) }}>
|
||||
<PlusIcon className="size-4" />
|
||||
Новый интерфейс
|
||||
</Button>
|
||||
@@ -687,6 +694,7 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
showAll
|
||||
allCount={completeScope ? displayIfaces.length : undefined}
|
||||
loading={isLive && loading && displayServers.length === 0}
|
||||
banner={!canWrite ? <p role="status" className="border-b px-4 py-2 text-sm text-muted-foreground">Режим просмотра. Изменение конфигурации и экспорт ключей недоступны.</p> : undefined}
|
||||
header={
|
||||
<PageHeader
|
||||
crumbs={[{ label: "Управление" }, { label: "WireGuard" }]}
|
||||
@@ -719,11 +727,11 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button size="sm" variant="outline" disabled={!!urlError || !catalogKnown || loadError} onClick={() => { setImportError(null); setImportOpen(true) }}>
|
||||
<Button size="sm" variant="outline" disabled={!canWrite || !!urlError || !catalogKnown || loadError} onClick={() => { setImportError(null); setImportOpen(true) }}>
|
||||
<UploadIcon className="size-4" />
|
||||
Импорт
|
||||
</Button>
|
||||
<Button size="sm" disabled={!!urlError || !catalogKnown || loadError} onClick={() => { setCreateError(null); setCreateOpen(true) }}>
|
||||
<Button size="sm" disabled={!canWrite || !!urlError || !catalogKnown || loadError} onClick={() => { setCreateError(null); setCreateOpen(true) }}>
|
||||
<PlusIcon className="size-4" />
|
||||
Новый интерфейс
|
||||
</Button>
|
||||
@@ -843,15 +851,16 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
/>
|
||||
{catalogKnown && !urlError && (completeScope || filteredIfaces.length > 0) ? <WireguardDataGrid
|
||||
interfaces={filteredIfaces}
|
||||
canWrite={canWrite}
|
||||
compactServer={compactServer}
|
||||
emptyAction={emptyCreateAction}
|
||||
onExport={(iface) => openExport(iface, "rsc")}
|
||||
onAddPeer={selectedSourceError ? undefined : iface => { setPeerError(null); setPeerIface(iface) }}
|
||||
onToggle={selectedSourceError ? undefined : handleToggle}
|
||||
onAddPeer={!canWrite || selectedSourceError ? undefined : iface => { setPeerError(null); setPeerIface(iface) }}
|
||||
onToggle={!canWrite || selectedSourceError ? undefined : handleToggle}
|
||||
toggleBusy={togglePending !== null}
|
||||
onDelete={selectedSourceError ? undefined : (iface) => setPendingDelete({ kind: "iface", iface })}
|
||||
onDeletePeer={selectedSourceError ? undefined : (iface, peerId) => setPendingDelete({ kind: "peer", iface, peerId })}
|
||||
onExportPeer={(iface, peerId) => openExport(iface, "peer", peerId)}
|
||||
onDelete={!canWrite || selectedSourceError ? undefined : (iface) => setPendingDelete({ kind: "iface", iface })}
|
||||
onDeletePeer={!canWrite || selectedSourceError ? undefined : (iface, peerId) => setPendingDelete({ kind: "peer", iface, peerId })}
|
||||
onExportPeer={canWrite ? (iface, peerId) => openExport(iface, "peer", peerId) : undefined}
|
||||
/> : <p role="status" className="px-5 py-8 text-sm text-muted-foreground">Каталог интерфейсов пока неизвестен</p>}
|
||||
</DataPageCard>
|
||||
</TabsContent>
|
||||
@@ -869,22 +878,22 @@ function WireGuardPageContent({ selectedServerId, selectedInterfaceId, allInterf
|
||||
compactServer={compactServer}
|
||||
emptyAction={
|
||||
scopedIfaces.length === 1 ? (
|
||||
<Button size="sm" disabled={selectedSourceError} onClick={() => { setPeerError(null); setPeerIface(scopedIfaces[0]) }}>
|
||||
<Button size="sm" disabled={!canWrite || selectedSourceError} onClick={() => { setPeerError(null); setPeerIface(scopedIfaces[0]) }}>
|
||||
<PlusIcon className="size-4" />
|
||||
Добавить пира
|
||||
</Button>
|
||||
) : emptyCreateAction
|
||||
}
|
||||
onDeletePeer={selectedSourceError ? undefined : (row) => {
|
||||
onDeletePeer={!canWrite || selectedSourceError ? undefined : (row) => {
|
||||
const iface = scopedIfaces.find((i) => i.id === row.ifaceId && i.serverId === row.serverId)
|
||||
if (!iface) return
|
||||
setPendingDelete({ kind: "peer", iface, peerId: row.id })
|
||||
}}
|
||||
onExportPeer={(row) => {
|
||||
onExportPeer={canWrite ? (row) => {
|
||||
const iface = scopedIfaces.find((i) => i.id === row.ifaceId && i.serverId === row.serverId)
|
||||
if (!iface) return
|
||||
openExport(iface, "peer", row.id)
|
||||
}}
|
||||
} : undefined}
|
||||
/> : <p role="status" className="px-5 py-8 text-sm text-muted-foreground">Каталог пиров пока неизвестен</p>}
|
||||
</DataPageCard>
|
||||
</TabsContent>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import {createServer} from 'node:http'
|
||||
import {randomBytes} from 'node:crypto'
|
||||
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
|
||||
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
|
||||
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own208.invalid';process.env.AUTH_PORTAL_URL='https://own208.invalid'
|
||||
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js')
|
||||
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
|
||||
const key=randomBytes(32).toString('base64'),publicKey=randomBytes(32).toString('base64'),baseline=process.env.MMAPP_WG_SECRET_EXPECT==='leak',cases=[];let calls=0,app
|
||||
const native=createServer((req,res)=>{calls++;assert.equal(req.method,'GET');res.setHeader('content-type','application/json');res.end(JSON.stringify(req.url.endsWith('/interface/wireguard')?[{'.id':'*1',name:'own208-wg','listen-port':'55108',mtu:'1420','public-key':publicKey,'private-key':key,disabled:'false',running:'true'}]:[]))});await new Promise(r=>native.listen(0,'127.0.0.1',r))
|
||||
const report={date:'2026-10-13',block:'12.5.208',database,baseline,cases,scope:'Production compiled WireGuard GET/auth/mapper, own restricted PostgreSQL and local RouterOS HTTP fixture with nonempty synthetic private key; isolated signed JWT',passed:false}
|
||||
try{await initDatabase();const port=native.address().port;await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('own208-native','127.0.0.1',$1,false,true,'fixture','fixture')",[port]);app=await buildApp({logger:false,startScheduler:false});await app.ready()
|
||||
for(const role of ['reader','writer','admin']){const permission='mm:network:'+({reader:'read',writer:'write',admin:'admin'}[role]);const token=app.jwt.sign({sub:'own208-'+role,apps:['mm'],permissions:[permission],iss:process.env.AUTH_ISSUER},{expiresIn:'5m'});for(const flag of ['', '?includePrivateKey=1','?includePrivateKey=true']){const before=calls,r=await app.inject({method:'GET',url:'/api/wireguard'+flag,headers:{authorization:'Bearer '+token}});const denied=!baseline&&role==='reader'&&flag!=='';assert.equal(r.statusCode,denied?403:200);if(denied){assert.equal(calls,before);assert(!r.body.includes(key))}else{const dto=r.json();assert.equal(dto.interfaces.length,1);assert.equal(dto.interfaces[0].publicKey,publicKey);assert.equal(dto.interfaces[0].privateKey,flag?key:undefined)}cases.push({role,flag,status:r.statusCode,privateKeyExposed:!denied&&!!flag,nativeRequestCount:calls-before})}}
|
||||
const fakeAdmin=app.jwt.sign({sub:'own208-is-admin',apps:['mm'],permissions:['mm:network:read'],is_admin:true,iss:process.env.AUTH_ISSUER},{expiresIn:'5m'});const before=calls,r=await app.inject({method:'GET',url:'/api/wireguard?includePrivateKey=1',headers:{authorization:'Bearer '+fakeAdmin}});assert.equal(r.statusCode,baseline?200:403);if(!baseline)assert.equal(calls,before);cases.push({name:'is_admin alone',status:r.statusCode,permissionRequired:true});assert.equal((await app.inject({method:'GET',url:'/api/wireguard?includePrivateKey=1'})).statusCode,401);cases.push({role:'unauthenticated',status:401});report.passed=true
|
||||
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/wireguard208-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();native.closeAllConnections();await new Promise(r=>native.close(r));await closePool();fs.writeFileSync('/app/backend/acceptance/wireguard208-'+(baseline?'baseline':'fixed')+'-result.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}
|
||||
if(!report.passed)process.exitCode=1
|
||||
@@ -43,6 +43,7 @@ import { parseWireguardSnapshot, planWireguardRestore, planStaticInterfaceAddres
|
||||
import { executeRosOps } from "../services/ros-ops.js"
|
||||
import { parseDbServerId } from "../utils/server-id.js"
|
||||
import { z } from "zod"
|
||||
import { hasPermission } from "../lib/permissions.js"
|
||||
|
||||
function serverIdParam(v: string): string {
|
||||
return decodeURIComponent(v)
|
||||
@@ -126,6 +127,9 @@ const wireguardRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.get("/wireguard", async (req, reply) => {
|
||||
const q = req.query as { serverId?: string; includePrivateKey?: string }
|
||||
const includePrivateKey = q.includePrivateKey === "1" || q.includePrivateKey === "true"
|
||||
if (includePrivateKey && req.authUser && !hasPermission(req.authUser.permissions, "mm:network:write")) {
|
||||
return reply.status(403).send({ error: "Для чтения приватного ключа WireGuard нужны права на изменение сети" })
|
||||
}
|
||||
const result = await listWireGuardInterfaces({
|
||||
serverId: q.serverId,
|
||||
includePrivateKey,
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface WgIfaceWithServer extends WireGuardInterface {
|
||||
interface WireguardDataGridProps {
|
||||
interfaces: WgIfaceWithServer[]
|
||||
compactServer?: boolean
|
||||
canWrite?: boolean
|
||||
emptyAction?: ReactNode
|
||||
onExport: (iface: WgIfaceWithServer) => void
|
||||
onAddPeer?: (iface: WgIfaceWithServer) => void
|
||||
@@ -58,14 +59,14 @@ interface WireguardDataGridProps {
|
||||
onExportPeer?: (iface: WgIfaceWithServer, peerId: string) => void
|
||||
}
|
||||
|
||||
type WireguardActions = Pick<WireguardDataGridProps, "onExport" | "onAddPeer" | "onToggle" | "toggleBusy" | "onDelete">
|
||||
type WireguardActions = Pick<WireguardDataGridProps, "onExport" | "onAddPeer" | "onToggle" | "toggleBusy" | "onDelete" | "canWrite">
|
||||
const WireguardActionsContext = createContext<WireguardActions | null>(null)
|
||||
|
||||
// Preserve menu state while callbacks and the current row data update.
|
||||
function WireguardActionsCell({ row }: { row: { original: WgIfaceWithServer } }) {
|
||||
const actions = useContext(WireguardActionsContext)
|
||||
if (!actions) throw new Error("WireGuard actions context is missing")
|
||||
const { onExport, onAddPeer, onToggle, toggleBusy, onDelete } = actions
|
||||
const { onExport, onAddPeer, onToggle, toggleBusy, onDelete, canWrite = true } = actions
|
||||
const iface = row.original
|
||||
return (
|
||||
<div
|
||||
@@ -94,12 +95,12 @@ function WireguardActionsCell({ row }: { row: { original: WgIfaceWithServer } })
|
||||
}
|
||||
/>
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DropdownMenuItem onClick={() => onExport(iface)}>
|
||||
<DropdownMenuItem disabled={!canWrite} onClick={() => onExport(iface)}>
|
||||
<CodeXmlIcon className="size-4" />
|
||||
Экспорт
|
||||
</DropdownMenuItem>
|
||||
{onAddPeer && (
|
||||
<DropdownMenuItem onClick={() => onAddPeer(iface)}>
|
||||
<DropdownMenuItem disabled={!canWrite} onClick={() => onAddPeer(iface)}>
|
||||
<PlusIcon className="size-4" />
|
||||
Добавить пира
|
||||
</DropdownMenuItem>
|
||||
@@ -107,7 +108,7 @@ function WireguardActionsCell({ row }: { row: { original: WgIfaceWithServer } })
|
||||
{onToggle && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem disabled={toggleBusy} onClick={() => onToggle(iface)}>
|
||||
<DropdownMenuItem disabled={!canWrite || toggleBusy} onClick={() => onToggle(iface)}>
|
||||
<PowerIcon className="size-4" />
|
||||
{iface.enabled ? "Отключить" : "Включить"}
|
||||
</DropdownMenuItem>
|
||||
@@ -116,7 +117,7 @@ function WireguardActionsCell({ row }: { row: { original: WgIfaceWithServer } })
|
||||
{onDelete && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive" onClick={() => onDelete(iface)}>
|
||||
<DropdownMenuItem disabled={!canWrite} variant="destructive" onClick={() => onDelete(iface)}>
|
||||
<Trash2Icon className="size-4" />
|
||||
Удалить
|
||||
</DropdownMenuItem>
|
||||
@@ -131,6 +132,7 @@ function WireguardActionsCell({ row }: { row: { original: WgIfaceWithServer } })
|
||||
function WireguardDataGrid({
|
||||
interfaces,
|
||||
compactServer = false,
|
||||
canWrite = true,
|
||||
emptyAction,
|
||||
onExport,
|
||||
onAddPeer,
|
||||
@@ -311,7 +313,7 @@ function WireguardDataGrid({
|
||||
}
|
||||
|
||||
return (
|
||||
<WireguardActionsContext.Provider value={{ onExport, onAddPeer, onToggle, toggleBusy, onDelete }}>
|
||||
<WireguardActionsContext.Provider value={{ onExport, onAddPeer, onToggle, toggleBusy, onDelete, canWrite }}>
|
||||
<DataGridShell
|
||||
table={table}
|
||||
recordCount={interfaces.length}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# UX/UI: протокол внешней приёмки 12.5
|
||||
|
||||
Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13:208 закрывает WireGuard private-key GET дляreader иread-only операции пятисетевыхразделов; baseline/fixed SQL11, WireGuard profile56,53 unique E2E,20 live UI и2 own native интерфейса passed/cleaned. Backend/frontend ux-native208, source470/527 иcompiled1044 matched. Следующий209 — секреты GRE вGET /gre/tunnels и/filters/gre-tunnels.12.5 открыта.
|
||||
|
||||
IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md).
|
||||
|
||||
@@ -696,7 +696,7 @@ flowchart LR
|
||||
|
||||
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||
|
||||
Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13:208 закрывает WireGuard private-key GET дляreader иread-only операции пятисетевыхразделов; baseline/fixed SQL11, WireGuard profile56,53 unique E2E,20 live UI и2 own native интерфейса passed/cleaned. Backend/frontend ux-native208, source470/527 иcompiled1044 matched. Следующий209 — секреты GRE вGET /gre/tunnels и/filters/gre-tunnels.12.5 открыта.
|
||||
|
||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||
|
||||
@@ -2510,3 +2510,17 @@ Actual Portal reader настаром frontend ux-accessibility205 видел50
|
||||
Production frontend ux-history207/source527 matched, backend ux-lock200/Portal/AUTH_REQUIRED сохранены.24 actual live UI6sections×reader/admin×1440/320 наpopulated historical metadata: readerbuttons disabled, adminenabled послеstable device read, informativecopy, nooverflow, Escape→trigger, mutationrequests0. Первыйadminrecursive check сделан доокончанияread ивиделcorrect temporarybusy; тестисправлен наожиданиеready, productguard неослаблялся. Own Portal users удалены204. Credentials/native configs не менялись.
|
||||
|
||||
Протоколы: [live baseline](ux-ui/acceptance/history207-baseline-live-2026-10-13.json), [E2E40](ux-ui/acceptance/history207-e2e-2026-10-13.json), [live24](ux-ui/acceptance/history207-live-2026-10-13.json), [deployment](ux-ui/acceptance/history207-deployment-2026-10-13.json). Следующий208: source GET WireGuard includePrivateKey флагне проверяетwrite permission; scoped nativekey privacy +read-only network workspaces.12.5 открыта.
|
||||
|
||||
## 12.5.208 — приватные ключи WireGuard и режим просмотра сетевых разделов (2026-10-13)
|
||||
|
||||
Старый production backend выдавал синтетический непустой приватный ключ reader при GET WireGuard с includePrivateKey=1/true. Baseline подтверждён actual compiled auth/mapper на собственной ограниченной PostgreSQL БД и локальном RouterOS HTTP fixture. Теперь явный запрос приватного ключа требует mm:network:write/admin, иначе403 до любых native requests. Обычный GET200 сохраняет public key и не содержит private key. is_admin без необходимого permission не заменяет право. Writer/admin сохраняют прежнее действие, legacy AUTH_REQUIRED=false не менялся.
|
||||
|
||||
WireGuard, IPsec, GRE, Firewall и Filters показывают режим просмотра и запрещают writer controls при read-only JWT. Использованы существующие readiness/mutation helpers; WireGuard toolbar/import/key-export/peer actions и callbacks дополнительно защищены. Backend RBAC остаётся авторитетной проверкой. Установленные ReUI primitives, vendor files и theme не менялись: добавлены operation-state props/copy.
|
||||
|
||||
11 fixed SQL cases: reader/writer/admin ×обычный GET/два private flags, is_admin alone, безJWT401; reader denied native request count0. Baseline/fixed/profile own DB/role/container удалены. WireGuard profile прошёл14.6s, включая56 node:test проверок; другие скрипты профиля не прибавляются к этому числу. Скопированный JSON label ошибочно называл traffic-rate, фактически runner запускал test:wireguard; label исправлен с сохранением originalReportedCommand. TypeScript/backend build/ESLint0errors,12 старых warnings Filters остаются.53 unique E2E:20 readonly PC/mobile +33 history/readiness/refresh. Два первоначальных GRE writer failures были некорректной fixture без обязательного ipsec:null; исправлены только данные теста,20 повторных cases прошли. Повторы не складываются.
|
||||
|
||||
Backend/frontend ux-native208 развёрнуты;470/527 source files и1044 compiled artifacts совпали, backend healthy. На каждом CHR через реальный admin API создан собственный WG-интерфейс с уникальным именем/свободным портом. Real reader обычный GET200 безключа, private flags403, admin получает ключ/public key сохранён. Private material вevidence не сохранялся.20 live UI5sections×reader/admin×1440/320 проверили создание/import/key-export controls, mutation requests0. Затем оба собственных интерфейса иPortal reader удалены; отсутствие интерфейсов проверено. Пароли и исходные native объекты не менялись. Это API privacy/UI proof, не WireGuard handshake/data-plane тест.
|
||||
|
||||
Протоколы: [baseline](ux-ui/acceptance/wireguard208-baseline-result.json), [fixed SQL11](ux-ui/acceptance/wireguard208-fixed-result.json), [profile](ux-ui/acceptance/native208-profile-result.json), [E2E53](ux-ui/acceptance/native208-e2e-2026-10-13.json), [live UI20](ux-ui/acceptance/native208-live-ui-2026-10-13.json), [native privacy/cleanup2](ux-ui/acceptance/wireguard208-native-live-2026-10-13.json), [compiled](ux-ui/acceptance/compiled208-result.json), [deployment](ux-ui/acceptance/native208-deployment-2026-10-13.json).
|
||||
|
||||
Следующий209: GRE mapper содержит ipsecSecret иipsec.secret, проверить оба публичных GET иограничить выдачу секретов reader, сохранив crypto-state/public metadata иwriter editing.12.5 открыта.
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
# План реализации UX/UI по пятичасовым окнам
|
||||
|
||||
Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13:208 закрывает WireGuard private-key GET дляreader иread-only операции пятисетевыхразделов; baseline/fixed SQL11, WireGuard profile56,53 unique E2E,20 live UI и2 own native интерфейса passed/cleaned. Backend/frontend ux-native208, source470/527 иcompiled1044 matched. Следующий209 — секреты GRE вGET /gre/tunnels и/filters/gre-tunnels.12.5 открыта.
|
||||
|
||||
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-13. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
|
||||
|
||||
## Актуальная точка продолжения — 2026-10-13
|
||||
|
||||
Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13:208 закрывает WireGuard private-key GET дляreader иread-only операции пятисетевыхразделов; baseline/fixed SQL11, WireGuard profile56,53 unique E2E,20 live UI и2 own native интерфейса passed/cleaned. Backend/frontend ux-native208, source470/527 иcompiled1044 matched. Следующий209 — секреты GRE вGET /gre/tunnels и/filters/gre-tunnels.12.5 открыта.
|
||||
|
||||
Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты.
|
||||
|
||||
@@ -371,6 +371,6 @@ flowchart LR
|
||||
|
||||
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||
|
||||
Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13:208 закрывает WireGuard private-key GET дляreader иread-only операции пятисетевыхразделов; baseline/fixed SQL11, WireGuard profile56,53 unique E2E,20 live UI и2 own native интерфейса passed/cleaned. Backend/frontend ux-native208, source470/527 иcompiled1044 matched. Следующий209 — секреты GRE вGET /gre/tunnels и/filters/gre-tunnels.12.5 открыта.
|
||||
|
||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||
+15
-1
@@ -1,6 +1,6 @@
|
||||
# UX/UI: журнал реализации
|
||||
|
||||
Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13:208 закрывает WireGuard private-key GET дляreader иread-only операции пятисетевыхразделов; baseline/fixed SQL11, WireGuard profile56,53 unique E2E,20 live UI и2 own native интерфейса passed/cleaned. Backend/frontend ux-native208, source470/527 иcompiled1044 matched. Следующий209 — секреты GRE вGET /gre/tunnels и/filters/gre-tunnels.12.5 открыта.
|
||||
|
||||
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
|
||||
|
||||
@@ -38,6 +38,20 @@
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–196 и ограничения ниже. Runtime backend/frontend ux-peak196. |
|
||||
|
||||
## 12.5.208 — приватные ключи WireGuard и режим просмотра сетевых разделов (2026-10-13)
|
||||
|
||||
Старый production backend выдавал синтетический непустой приватный ключ reader при GET WireGuard с includePrivateKey=1/true. Baseline подтверждён actual compiled auth/mapper на собственной ограниченной PostgreSQL БД и локальном RouterOS HTTP fixture. Теперь явный запрос приватного ключа требует mm:network:write/admin, иначе403 до любых native requests. Обычный GET200 сохраняет public key и не содержит private key. is_admin без необходимого permission не заменяет право. Writer/admin сохраняют прежнее действие, legacy AUTH_REQUIRED=false не менялся.
|
||||
|
||||
WireGuard, IPsec, GRE, Firewall и Filters показывают режим просмотра и запрещают writer controls при read-only JWT. Использованы существующие readiness/mutation helpers; WireGuard toolbar/import/key-export/peer actions и callbacks дополнительно защищены. Backend RBAC остаётся авторитетной проверкой. Установленные ReUI primitives, vendor files и theme не менялись: добавлены operation-state props/copy.
|
||||
|
||||
11 fixed SQL cases: reader/writer/admin ×обычный GET/два private flags, is_admin alone, безJWT401; reader denied native request count0. Baseline/fixed/profile own DB/role/container удалены. WireGuard profile прошёл14.6s, включая56 node:test проверок; другие скрипты профиля не прибавляются к этому числу. Скопированный JSON label ошибочно называл traffic-rate, фактически runner запускал test:wireguard; label исправлен с сохранением originalReportedCommand. TypeScript/backend build/ESLint0errors,12 старых warnings Filters остаются.53 unique E2E:20 readonly PC/mobile +33 history/readiness/refresh. Два первоначальных GRE writer failures были некорректной fixture без обязательного ipsec:null; исправлены только данные теста,20 повторных cases прошли. Повторы не складываются.
|
||||
|
||||
Backend/frontend ux-native208 развёрнуты;470/527 source files и1044 compiled artifacts совпали, backend healthy. На каждом CHR через реальный admin API создан собственный WG-интерфейс с уникальным именем/свободным портом. Real reader обычный GET200 безключа, private flags403, admin получает ключ/public key сохранён. Private material вevidence не сохранялся.20 live UI5sections×reader/admin×1440/320 проверили создание/import/key-export controls, mutation requests0. Затем оба собственных интерфейса иPortal reader удалены; отсутствие интерфейсов проверено. Пароли и исходные native объекты не менялись. Это API privacy/UI proof, не WireGuard handshake/data-plane тест.
|
||||
|
||||
Протоколы: [baseline](ux-ui/acceptance/wireguard208-baseline-result.json), [fixed SQL11](ux-ui/acceptance/wireguard208-fixed-result.json), [profile](ux-ui/acceptance/native208-profile-result.json), [E2E53](ux-ui/acceptance/native208-e2e-2026-10-13.json), [live UI20](ux-ui/acceptance/native208-live-ui-2026-10-13.json), [native privacy/cleanup2](ux-ui/acceptance/wireguard208-native-live-2026-10-13.json), [compiled](ux-ui/acceptance/compiled208-result.json), [deployment](ux-ui/acceptance/native208-deployment-2026-10-13.json).
|
||||
|
||||
Следующий209: GRE mapper содержит ipsecSecret иipsec.secret, проверить оба публичных GET иограничить выдачу секретов reader, сохранив crypto-state/public metadata иwriter editing.12.5 открыта.
|
||||
|
||||
## 12.5.207 — права отката общей истории конфигураций (2026-10-13)
|
||||
|
||||
Actual Portal reader настаром frontend ux-accessibility205 видел50 enabled restore buttons GRE; безнажатия подтверждёнbaseline.5 из6 ConfigHistorySheet caller не передавалиrestoreDisabled,defaultfalse. Теперь propобязателен, defaulttrue дляuntyped omission; GRE/WireGuard/IPsec/Filters/Firewall передаютpermission/device/busy guards. Recursive routes ужеимелиguard. Reader требуетnetwork:write (Filters filters:write), disabled/missing device запрещаетaction; GRE/IPsec/Firewall используютexisting readiness guards. Callbacks пятиcaller повторно проверяютpermission непосредственно передrequest. ШаблоныReUI/Timeline/Sheet/vendor unchanged; текст объясняет недоступностьотката.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Оставшаяся внешняя приёмка — 2026-10-13
|
||||
|
||||
Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13:208 закрывает WireGuard private-key GET дляreader иread-only операции пятисетевыхразделов; baseline/fixed SQL11, WireGuard profile56,53 unique E2E,20 live UI и2 own native интерфейса passed/cleaned. Backend/frontend ux-native208, source470/527 иcompiled1044 matched. Следующий209 — секреты GRE вGET /gre/tunnels и/filters/gre-tunnels.12.5 открыта.
|
||||
|
||||
Это актуальная матрица после 12.5.207. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
Это актуальная матрица после 12.5.208. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
|
||||
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-lock200`, frontend `ux-history207`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend/frontend `ux-native208`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
|
||||
|
||||
| Раздел | Подтверждено | Осталось / условие завершения |
|
||||
| --- | --- | --- |
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
## Следующая последовательность — 2026-10-13
|
||||
|
||||
1. **208 — native read-only:** WireGuard private key flag GET permission иnetwork workspace write controls; SQL/native own fixtures, source guard/API/UI, безизменениячужихобъектов.
|
||||
1. **209 — GRE secret DTOs:** /gre/tunnels и/filters/gre-tunnels, синтетические ключи/own DB, роли исохранение crypto-state; не менятьnative config.
|
||||
2. **Профильный контроль:** для новых изменений выполнять затронутые SQL/API/UI проверки. Общая регрессия192 выполнена; не повторять её без изменений общей основы или нового основания.
|
||||
3. **Внешние зависимости:** Portal issuer/login/revoke, Telegram bot/chat/разрешённое сообщение, клиентские ОС, независимые WAN/public IPv6 и scale/multi-instance — отдельные группы по таблице выше. Ограничения текущего стенда не превращать в обещание приёмки всех конфигураций.
|
||||
|
||||
@@ -1426,3 +1426,17 @@ Actual Portal reader настаром frontend ux-accessibility205 видел50
|
||||
Production frontend ux-history207/source527 matched, backend ux-lock200/Portal/AUTH_REQUIRED сохранены.24 actual live UI6sections×reader/admin×1440/320 наpopulated historical metadata: readerbuttons disabled, adminenabled послеstable device read, informativecopy, nooverflow, Escape→trigger, mutationrequests0. Первыйadminrecursive check сделан доокончанияread ивиделcorrect temporarybusy; тестисправлен наожиданиеready, productguard неослаблялся. Own Portal users удалены204. Credentials/native configs не менялись.
|
||||
|
||||
Протоколы: [live baseline](ux-ui/acceptance/history207-baseline-live-2026-10-13.json), [E2E40](ux-ui/acceptance/history207-e2e-2026-10-13.json), [live24](ux-ui/acceptance/history207-live-2026-10-13.json), [deployment](ux-ui/acceptance/history207-deployment-2026-10-13.json). Следующий208: source GET WireGuard includePrivateKey флагне проверяетwrite permission; scoped nativekey privacy +read-only network workspaces.12.5 открыта.
|
||||
|
||||
## 12.5.208 — приватные ключи WireGuard и режим просмотра сетевых разделов (2026-10-13)
|
||||
|
||||
Старый production backend выдавал синтетический непустой приватный ключ reader при GET WireGuard с includePrivateKey=1/true. Baseline подтверждён actual compiled auth/mapper на собственной ограниченной PostgreSQL БД и локальном RouterOS HTTP fixture. Теперь явный запрос приватного ключа требует mm:network:write/admin, иначе403 до любых native requests. Обычный GET200 сохраняет public key и не содержит private key. is_admin без необходимого permission не заменяет право. Writer/admin сохраняют прежнее действие, legacy AUTH_REQUIRED=false не менялся.
|
||||
|
||||
WireGuard, IPsec, GRE, Firewall и Filters показывают режим просмотра и запрещают writer controls при read-only JWT. Использованы существующие readiness/mutation helpers; WireGuard toolbar/import/key-export/peer actions и callbacks дополнительно защищены. Backend RBAC остаётся авторитетной проверкой. Установленные ReUI primitives, vendor files и theme не менялись: добавлены operation-state props/copy.
|
||||
|
||||
11 fixed SQL cases: reader/writer/admin ×обычный GET/два private flags, is_admin alone, безJWT401; reader denied native request count0. Baseline/fixed/profile own DB/role/container удалены. WireGuard profile прошёл14.6s, включая56 node:test проверок; другие скрипты профиля не прибавляются к этому числу. Скопированный JSON label ошибочно называл traffic-rate, фактически runner запускал test:wireguard; label исправлен с сохранением originalReportedCommand. TypeScript/backend build/ESLint0errors,12 старых warnings Filters остаются.53 unique E2E:20 readonly PC/mobile +33 history/readiness/refresh. Два первоначальных GRE writer failures были некорректной fixture без обязательного ipsec:null; исправлены только данные теста,20 повторных cases прошли. Повторы не складываются.
|
||||
|
||||
Backend/frontend ux-native208 развёрнуты;470/527 source files и1044 compiled artifacts совпали, backend healthy. На каждом CHR через реальный admin API создан собственный WG-интерфейс с уникальным именем/свободным портом. Real reader обычный GET200 безключа, private flags403, admin получает ключ/public key сохранён. Private material вevidence не сохранялся.20 live UI5sections×reader/admin×1440/320 проверили создание/import/key-export controls, mutation requests0. Затем оба собственных интерфейса иPortal reader удалены; отсутствие интерфейсов проверено. Пароли и исходные native объекты не менялись. Это API privacy/UI proof, не WireGuard handshake/data-plane тест.
|
||||
|
||||
Протоколы: [baseline](ux-ui/acceptance/wireguard208-baseline-result.json), [fixed SQL11](ux-ui/acceptance/wireguard208-fixed-result.json), [profile](ux-ui/acceptance/native208-profile-result.json), [E2E53](ux-ui/acceptance/native208-e2e-2026-10-13.json), [live UI20](ux-ui/acceptance/native208-live-ui-2026-10-13.json), [native privacy/cleanup2](ux-ui/acceptance/wireguard208-native-live-2026-10-13.json), [compiled](ux-ui/acceptance/compiled208-result.json), [deployment](ux-ui/acceptance/native208-deployment-2026-10-13.json).
|
||||
|
||||
Следующий209: GRE mapper содержит ipsecSecret иipsec.secret, проверить оба публичных GET иограничить выдачу секретов reader, сохранив crypto-state/public metadata иwriter editing.12.5 открыта.
|
||||
+15
-1
@@ -1,6 +1,6 @@
|
||||
# Этап 12: итоговая приёмка UX/UI
|
||||
|
||||
Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13:208 закрывает WireGuard private-key GET дляreader иread-only операции пятисетевыхразделов; baseline/fixed SQL11, WireGuard profile56,53 unique E2E,20 live UI и2 own native интерфейса passed/cleaned. Backend/frontend ux-native208, source470/527 иcompiled1044 matched. Следующий209 — секреты GRE вGET /gre/tunnels и/filters/gre-tunnels.12.5 открыта.
|
||||
|
||||
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
|
||||
|
||||
@@ -3815,3 +3815,17 @@ Actual Portal reader настаром frontend ux-accessibility205 видел50
|
||||
Production frontend ux-history207/source527 matched, backend ux-lock200/Portal/AUTH_REQUIRED сохранены.24 actual live UI6sections×reader/admin×1440/320 наpopulated historical metadata: readerbuttons disabled, adminenabled послеstable device read, informativecopy, nooverflow, Escape→trigger, mutationrequests0. Первыйadminrecursive check сделан доокончанияread ивиделcorrect temporarybusy; тестисправлен наожиданиеready, productguard неослаблялся. Own Portal users удалены204. Credentials/native configs не менялись.
|
||||
|
||||
Протоколы: [live baseline](ux-ui/acceptance/history207-baseline-live-2026-10-13.json), [E2E40](ux-ui/acceptance/history207-e2e-2026-10-13.json), [live24](ux-ui/acceptance/history207-live-2026-10-13.json), [deployment](ux-ui/acceptance/history207-deployment-2026-10-13.json). Следующий208: source GET WireGuard includePrivateKey флагне проверяетwrite permission; scoped nativekey privacy +read-only network workspaces.12.5 открыта.
|
||||
|
||||
## 12.5.208 — приватные ключи WireGuard и режим просмотра сетевых разделов (2026-10-13)
|
||||
|
||||
Старый production backend выдавал синтетический непустой приватный ключ reader при GET WireGuard с includePrivateKey=1/true. Baseline подтверждён actual compiled auth/mapper на собственной ограниченной PostgreSQL БД и локальном RouterOS HTTP fixture. Теперь явный запрос приватного ключа требует mm:network:write/admin, иначе403 до любых native requests. Обычный GET200 сохраняет public key и не содержит private key. is_admin без необходимого permission не заменяет право. Writer/admin сохраняют прежнее действие, legacy AUTH_REQUIRED=false не менялся.
|
||||
|
||||
WireGuard, IPsec, GRE, Firewall и Filters показывают режим просмотра и запрещают writer controls при read-only JWT. Использованы существующие readiness/mutation helpers; WireGuard toolbar/import/key-export/peer actions и callbacks дополнительно защищены. Backend RBAC остаётся авторитетной проверкой. Установленные ReUI primitives, vendor files и theme не менялись: добавлены operation-state props/copy.
|
||||
|
||||
11 fixed SQL cases: reader/writer/admin ×обычный GET/два private flags, is_admin alone, безJWT401; reader denied native request count0. Baseline/fixed/profile own DB/role/container удалены. WireGuard profile прошёл14.6s, включая56 node:test проверок; другие скрипты профиля не прибавляются к этому числу. Скопированный JSON label ошибочно называл traffic-rate, фактически runner запускал test:wireguard; label исправлен с сохранением originalReportedCommand. TypeScript/backend build/ESLint0errors,12 старых warnings Filters остаются.53 unique E2E:20 readonly PC/mobile +33 history/readiness/refresh. Два первоначальных GRE writer failures были некорректной fixture без обязательного ipsec:null; исправлены только данные теста,20 повторных cases прошли. Повторы не складываются.
|
||||
|
||||
Backend/frontend ux-native208 развёрнуты;470/527 source files и1044 compiled artifacts совпали, backend healthy. На каждом CHR через реальный admin API создан собственный WG-интерфейс с уникальным именем/свободным портом. Real reader обычный GET200 безключа, private flags403, admin получает ключ/public key сохранён. Private material вevidence не сохранялся.20 live UI5sections×reader/admin×1440/320 проверили создание/import/key-export controls, mutation requests0. Затем оба собственных интерфейса иPortal reader удалены; отсутствие интерфейсов проверено. Пароли и исходные native объекты не менялись. Это API privacy/UI proof, не WireGuard handshake/data-plane тест.
|
||||
|
||||
Протоколы: [baseline](ux-ui/acceptance/wireguard208-baseline-result.json), [fixed SQL11](ux-ui/acceptance/wireguard208-fixed-result.json), [profile](ux-ui/acceptance/native208-profile-result.json), [E2E53](ux-ui/acceptance/native208-e2e-2026-10-13.json), [live UI20](ux-ui/acceptance/native208-live-ui-2026-10-13.json), [native privacy/cleanup2](ux-ui/acceptance/wireguard208-native-live-2026-10-13.json), [compiled](ux-ui/acceptance/compiled208-result.json), [deployment](ux-ui/acceptance/native208-deployment-2026-10-13.json).
|
||||
|
||||
Следующий209: GRE mapper содержит ipsecSecret иipsec.secret, проверить оба публичных GET иограничить выдачу секретов reader, сохранив crypto-state/public metadata иwriter editing.12.5 открыта.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Тестовый сервер UX/UI — 2026-10-10
|
||||
|
||||
Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13:208 закрывает WireGuard private-key GET дляreader иread-only операции пятисетевыхразделов; baseline/fixed SQL11, WireGuard profile56,53 unique E2E,20 live UI и2 own native интерфейса passed/cleaned. Backend/frontend ux-native208, source470/527 иcompiled1044 matched. Следующий209 — секреты GRE вGET /gre/tunnels и/filters/gre-tunnels.12.5 открыта.
|
||||
|
||||
| Параметр | Проверенное состояние |
|
||||
| --- | --- |
|
||||
@@ -708,7 +708,7 @@ flowchart LR
|
||||
|
||||
## 12.5.77 — основной блок IPsec (2026-10-09)
|
||||
|
||||
Актуальное продолжение 2026-10-13:207 history restore rights всех6 caller: defaultdeny, reader/disabled/write guards,40 E2E и24 live UI passed; GRE reader50 enabled baseline закрыт. Frontend ux-history207/backend ux-lock200. Следующий208 — WireGuard includePrivateKey GET иread-only controls сетевыхworkspaces.12.5 открыта.
|
||||
Актуальное продолжение 2026-10-13:208 закрывает WireGuard private-key GET дляreader иread-only операции пятисетевыхразделов; baseline/fixed SQL11, WireGuard profile56,53 unique E2E,20 live UI и2 own native интерфейса passed/cleaned. Backend/frontend ux-native208, source470/527 иcompiled1044 matched. Следующий209 — секреты GRE вGET /gre/tunnels и/filters/gre-tunnels.12.5 открыта.
|
||||
|
||||
Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md).
|
||||
|
||||
@@ -2241,3 +2241,17 @@ Actual Portal reader настаром frontend ux-accessibility205 видел50
|
||||
Production frontend ux-history207/source527 matched, backend ux-lock200/Portal/AUTH_REQUIRED сохранены.24 actual live UI6sections×reader/admin×1440/320 наpopulated historical metadata: readerbuttons disabled, adminenabled послеstable device read, informativecopy, nooverflow, Escape→trigger, mutationrequests0. Первыйadminrecursive check сделан доокончанияread ивиделcorrect temporarybusy; тестисправлен наожиданиеready, productguard неослаблялся. Own Portal users удалены204. Credentials/native configs не менялись.
|
||||
|
||||
Протоколы: [live baseline](ux-ui/acceptance/history207-baseline-live-2026-10-13.json), [E2E40](ux-ui/acceptance/history207-e2e-2026-10-13.json), [live24](ux-ui/acceptance/history207-live-2026-10-13.json), [deployment](ux-ui/acceptance/history207-deployment-2026-10-13.json). Следующий208: source GET WireGuard includePrivateKey флагне проверяетwrite permission; scoped nativekey privacy +read-only network workspaces.12.5 открыта.
|
||||
|
||||
## 12.5.208 — приватные ключи WireGuard и режим просмотра сетевых разделов (2026-10-13)
|
||||
|
||||
Старый production backend выдавал синтетический непустой приватный ключ reader при GET WireGuard с includePrivateKey=1/true. Baseline подтверждён actual compiled auth/mapper на собственной ограниченной PostgreSQL БД и локальном RouterOS HTTP fixture. Теперь явный запрос приватного ключа требует mm:network:write/admin, иначе403 до любых native requests. Обычный GET200 сохраняет public key и не содержит private key. is_admin без необходимого permission не заменяет право. Writer/admin сохраняют прежнее действие, legacy AUTH_REQUIRED=false не менялся.
|
||||
|
||||
WireGuard, IPsec, GRE, Firewall и Filters показывают режим просмотра и запрещают writer controls при read-only JWT. Использованы существующие readiness/mutation helpers; WireGuard toolbar/import/key-export/peer actions и callbacks дополнительно защищены. Backend RBAC остаётся авторитетной проверкой. Установленные ReUI primitives, vendor files и theme не менялись: добавлены operation-state props/copy.
|
||||
|
||||
11 fixed SQL cases: reader/writer/admin ×обычный GET/два private flags, is_admin alone, безJWT401; reader denied native request count0. Baseline/fixed/profile own DB/role/container удалены. WireGuard profile прошёл14.6s, включая56 node:test проверок; другие скрипты профиля не прибавляются к этому числу. Скопированный JSON label ошибочно называл traffic-rate, фактически runner запускал test:wireguard; label исправлен с сохранением originalReportedCommand. TypeScript/backend build/ESLint0errors,12 старых warnings Filters остаются.53 unique E2E:20 readonly PC/mobile +33 history/readiness/refresh. Два первоначальных GRE writer failures были некорректной fixture без обязательного ipsec:null; исправлены только данные теста,20 повторных cases прошли. Повторы не складываются.
|
||||
|
||||
Backend/frontend ux-native208 развёрнуты;470/527 source files и1044 compiled artifacts совпали, backend healthy. На каждом CHR через реальный admin API создан собственный WG-интерфейс с уникальным именем/свободным портом. Real reader обычный GET200 безключа, private flags403, admin получает ключ/public key сохранён. Private material вevidence не сохранялся.20 live UI5sections×reader/admin×1440/320 проверили создание/import/key-export controls, mutation requests0. Затем оба собственных интерфейса иPortal reader удалены; отсутствие интерфейсов проверено. Пароли и исходные native объекты не менялись. Это API privacy/UI proof, не WireGuard handshake/data-plane тест.
|
||||
|
||||
Протоколы: [baseline](ux-ui/acceptance/wireguard208-baseline-result.json), [fixed SQL11](ux-ui/acceptance/wireguard208-fixed-result.json), [profile](ux-ui/acceptance/native208-profile-result.json), [E2E53](ux-ui/acceptance/native208-e2e-2026-10-13.json), [live UI20](ux-ui/acceptance/native208-live-ui-2026-10-13.json), [native privacy/cleanup2](ux-ui/acceptance/wireguard208-native-live-2026-10-13.json), [compiled](ux-ui/acceptance/compiled208-result.json), [deployment](ux-ui/acceptance/native208-deployment-2026-10-13.json).
|
||||
|
||||
Следующий209: GRE mapper содержит ipsecSecret иipsec.secret, проверить оба публичных GET иограничить выдачу секретов reader, сохранив crypto-state/public metadata иwriter editing.12.5 открыта.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.208",
|
||||
"backendSourceFilesMatched": 470,
|
||||
"frontendSourceFilesMatched": 527,
|
||||
"compiledMatched": 1044,
|
||||
"backendImage": "mmapp-test/backend:ux-native208",
|
||||
"backendImageId": "sha256:46923e412d3e7c89f79a4c7757a64d8226029988ce4e865398ad224c6fb2b2e7",
|
||||
"frontendImage": "mmapp-test/frontend:ux-native208",
|
||||
"frontendImageId": "sha256:5672a5501fb316b5d8885854328607efa6d58ae60804d6b22adab0516b8e0ee56",
|
||||
"backendHealthy": true,
|
||||
"ownDatabasesRolesContainersRemoved": 3,
|
||||
"ownedNativeInterfacesCreatedAndRemoved": 2,
|
||||
"chrPasswordsChanged": false
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.208",
|
||||
"uniqueCases": 53,
|
||||
"finalReadonly": {
|
||||
"startTime": "2026-10-13T12:58:59.011Z",
|
||||
"duration": 27098.076,
|
||||
"expected": 20,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"unchangedRegressionPassed": 33,
|
||||
"initialFailedReadonlyFixture": 2,
|
||||
"initialFailureReason": "GRE fixture omitted required ipsec:null; corrected fixture, writer role UI enabled",
|
||||
"allUniquePassed": true,
|
||||
"scope": "20 reader/writer PC/mobile workspaces +33 history/readiness/refresh regressions; no real native mutation"
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.208",
|
||||
"runtime": "ux-native208",
|
||||
"cases": [
|
||||
{
|
||||
"section": "wireguard",
|
||||
"role": "reader",
|
||||
"width": 1440,
|
||||
"writeControlAllowed": false,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "ipsec",
|
||||
"role": "reader",
|
||||
"width": 1440,
|
||||
"writeControlAllowed": false,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "gre",
|
||||
"role": "reader",
|
||||
"width": 1440,
|
||||
"writeControlAllowed": false,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "firewall",
|
||||
"role": "reader",
|
||||
"width": 1440,
|
||||
"writeControlAllowed": false,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "filters",
|
||||
"role": "reader",
|
||||
"width": 1440,
|
||||
"writeControlAllowed": false,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "wireguard",
|
||||
"role": "admin",
|
||||
"width": 1440,
|
||||
"writeControlAllowed": true,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "ipsec",
|
||||
"role": "admin",
|
||||
"width": 1440,
|
||||
"writeControlAllowed": true,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "gre",
|
||||
"role": "admin",
|
||||
"width": 1440,
|
||||
"writeControlAllowed": true,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "firewall",
|
||||
"role": "admin",
|
||||
"width": 1440,
|
||||
"writeControlAllowed": true,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "filters",
|
||||
"role": "admin",
|
||||
"width": 1440,
|
||||
"writeControlAllowed": true,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "wireguard",
|
||||
"role": "reader",
|
||||
"width": 320,
|
||||
"writeControlAllowed": false,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "ipsec",
|
||||
"role": "reader",
|
||||
"width": 320,
|
||||
"writeControlAllowed": false,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "gre",
|
||||
"role": "reader",
|
||||
"width": 320,
|
||||
"writeControlAllowed": false,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "firewall",
|
||||
"role": "reader",
|
||||
"width": 320,
|
||||
"writeControlAllowed": false,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "filters",
|
||||
"role": "reader",
|
||||
"width": 320,
|
||||
"writeControlAllowed": false,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "wireguard",
|
||||
"role": "admin",
|
||||
"width": 320,
|
||||
"writeControlAllowed": true,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "ipsec",
|
||||
"role": "admin",
|
||||
"width": 320,
|
||||
"writeControlAllowed": true,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "gre",
|
||||
"role": "admin",
|
||||
"width": 320,
|
||||
"writeControlAllowed": true,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "firewall",
|
||||
"role": "admin",
|
||||
"width": 320,
|
||||
"writeControlAllowed": true,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
},
|
||||
{
|
||||
"section": "filters",
|
||||
"role": "admin",
|
||||
"width": 320,
|
||||
"writeControlAllowed": true,
|
||||
"actualNativeList": true,
|
||||
"applied": false
|
||||
}
|
||||
],
|
||||
"realPortalJWT": true,
|
||||
"apiMocks": false,
|
||||
"mutationRequests": 0,
|
||||
"pageErrors": 0,
|
||||
"ownWireguardInterfacesPresent": true,
|
||||
"scope": "Default create/import/key-export controls on actual data; no operations applied from UI",
|
||||
"ownWireguardInterfacesRemoved": true,
|
||||
"ownPortalUserRemoved": true
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.208",
|
||||
"database": "mm_accept_166_1791896427599",
|
||||
"scope": "ephemeral role and precreated owned database; no copied production credentials",
|
||||
"tests": [
|
||||
{
|
||||
"command": "npm --prefix backend run test:wireguard",
|
||||
"passed": true,
|
||||
"durationMs": 14595,
|
||||
"externalNativeDevices": false,
|
||||
"originalReportedCommand": "npm --prefix backend run test:traffic-rate",
|
||||
"labelCorrection": "Runner executed test:wireguard; copied reporting label incorrectly named traffic-rate. Actual profile stdout assertion verified."
|
||||
}
|
||||
],
|
||||
"scopedRoleVerified": true,
|
||||
"passed": true
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.208",
|
||||
"database": "mm_accept_166_1791895670647",
|
||||
"baseline": true,
|
||||
"cases": [
|
||||
{
|
||||
"role": "reader",
|
||||
"flag": "",
|
||||
"status": 200,
|
||||
"privateKeyExposed": false,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "reader",
|
||||
"flag": "?includePrivateKey=1",
|
||||
"status": 200,
|
||||
"privateKeyExposed": true,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "reader",
|
||||
"flag": "?includePrivateKey=true",
|
||||
"status": 200,
|
||||
"privateKeyExposed": true,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "writer",
|
||||
"flag": "",
|
||||
"status": 200,
|
||||
"privateKeyExposed": false,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "writer",
|
||||
"flag": "?includePrivateKey=1",
|
||||
"status": 200,
|
||||
"privateKeyExposed": true,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "writer",
|
||||
"flag": "?includePrivateKey=true",
|
||||
"status": 200,
|
||||
"privateKeyExposed": true,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "admin",
|
||||
"flag": "",
|
||||
"status": 200,
|
||||
"privateKeyExposed": false,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "admin",
|
||||
"flag": "?includePrivateKey=1",
|
||||
"status": 200,
|
||||
"privateKeyExposed": true,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "admin",
|
||||
"flag": "?includePrivateKey=true",
|
||||
"status": 200,
|
||||
"privateKeyExposed": true,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"name": "is_admin alone",
|
||||
"status": 200,
|
||||
"permissionRequired": true
|
||||
},
|
||||
{
|
||||
"role": "unauthenticated",
|
||||
"status": 401
|
||||
}
|
||||
],
|
||||
"scope": "Production compiled WireGuard GET/auth/mapper, own restricted PostgreSQL and local RouterOS HTTP fixture with nonempty synthetic private key; isolated signed JWT",
|
||||
"passed": true
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.208",
|
||||
"database": "mm_accept_166_1791896339709",
|
||||
"baseline": false,
|
||||
"cases": [
|
||||
{
|
||||
"role": "reader",
|
||||
"flag": "",
|
||||
"status": 200,
|
||||
"privateKeyExposed": false,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "reader",
|
||||
"flag": "?includePrivateKey=1",
|
||||
"status": 403,
|
||||
"privateKeyExposed": false,
|
||||
"nativeRequestCount": 0
|
||||
},
|
||||
{
|
||||
"role": "reader",
|
||||
"flag": "?includePrivateKey=true",
|
||||
"status": 403,
|
||||
"privateKeyExposed": false,
|
||||
"nativeRequestCount": 0
|
||||
},
|
||||
{
|
||||
"role": "writer",
|
||||
"flag": "",
|
||||
"status": 200,
|
||||
"privateKeyExposed": false,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "writer",
|
||||
"flag": "?includePrivateKey=1",
|
||||
"status": 200,
|
||||
"privateKeyExposed": true,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "writer",
|
||||
"flag": "?includePrivateKey=true",
|
||||
"status": 200,
|
||||
"privateKeyExposed": true,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "admin",
|
||||
"flag": "",
|
||||
"status": 200,
|
||||
"privateKeyExposed": false,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "admin",
|
||||
"flag": "?includePrivateKey=1",
|
||||
"status": 200,
|
||||
"privateKeyExposed": true,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"role": "admin",
|
||||
"flag": "?includePrivateKey=true",
|
||||
"status": 200,
|
||||
"privateKeyExposed": true,
|
||||
"nativeRequestCount": 4
|
||||
},
|
||||
{
|
||||
"name": "is_admin alone",
|
||||
"status": 403,
|
||||
"permissionRequired": true
|
||||
},
|
||||
{
|
||||
"role": "unauthenticated",
|
||||
"status": 401
|
||||
}
|
||||
],
|
||||
"scope": "Production compiled WireGuard GET/auth/mapper, own restricted PostgreSQL and local RouterOS HTTP fixture with nonempty synthetic private key; isolated signed JWT",
|
||||
"passed": true
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"date": "2026-10-13",
|
||||
"block": "12.5.208",
|
||||
"runtimeBackend": "ux-native208",
|
||||
"actualPortalJWT": true,
|
||||
"cases": [
|
||||
{
|
||||
"host": "194.226.121.150",
|
||||
"serverId": 1,
|
||||
"ownedInterfaceCreated": true,
|
||||
"createStatus": 201,
|
||||
"readerPublicList": 200,
|
||||
"readerPrivateFlags": [
|
||||
403,
|
||||
403
|
||||
],
|
||||
"readerPrivateKeyAbsent": true,
|
||||
"adminPrivateKeyAvailable": true,
|
||||
"publicKeyPreserved": true,
|
||||
"dataPlaneTest": false
|
||||
},
|
||||
{
|
||||
"host": "104.165.244.7",
|
||||
"serverId": 2,
|
||||
"ownedInterfaceCreated": true,
|
||||
"createStatus": 201,
|
||||
"readerPublicList": 200,
|
||||
"readerPrivateFlags": [
|
||||
403,
|
||||
403
|
||||
],
|
||||
"readerPrivateKeyAbsent": true,
|
||||
"adminPrivateKeyAvailable": true,
|
||||
"publicKeyPreserved": true,
|
||||
"dataPlaneTest": false
|
||||
}
|
||||
],
|
||||
"privateMaterialPublished": false,
|
||||
"cleanupCompleted": true,
|
||||
"scope": "Own WireGuard interface on each CHR; private/public API policy, not handshake or encrypted data-plane test",
|
||||
"ownPortalUserRemoved": true
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {test,expect,type Page} from '@playwright/test'
|
||||
const sections=['wireguard','ipsec','gre','firewall','filters'] as const
|
||||
const names={wireguard:'Новый интерфейс',ipsec:'Новый клиент',gre:'Добавить туннель',firewall:'Новое правило',filters:'Новое правило'}
|
||||
async function setup(page:Page,writer:boolean){let writes=0
|
||||
const jwt=[Buffer.from('{}').toString('base64url'),Buffer.from(JSON.stringify({sub:'own208',apps:['mm'],permissions:['mm:dashboard:read','mm:servers:read','mm:network:read','mm:filters:read',...(writer?['mm:network:write','mm:filters:write']:[])],exp:Math.floor(Date.now()/1000)+600})).toString('base64url'),'fixture'].join('.')
|
||||
await page.addInitScript(t=>localStorage.setItem('mmapp_token',t),jwt)
|
||||
await page.route('**/health',r=>r.fulfill({json:{ok:true}}))
|
||||
await page.route('**/api/**',r=>{const path=new URL(r.request().url()).pathname;if(r.request().method()!=='GET'){writes++;return r.fulfill({status:403,json:{error:'Unexpected mutation in readonly workspace fixture'}})}
|
||||
const iface={id:'1:*W',rosId:'*W',name:'own208-wg',serverId:'1',serverName:'Own208 router',listenPort:55208,mtu:1420,comment:'',enabled:true,status:'up',peers:[{id:'own208-peer',rosId:'*P',publicKey:'own208-public',allowedIps:['10.208.0.2/32'],disabled:false}]}
|
||||
const tunnel={id:'own208-gre',serverId:'1',name:'own208-gre',localAddress:'192.0.2.1',remoteAddress:'192.0.2.2',localInnerIp:'10.208.0.1/30',remoteInnerIp:'10.208.0.2/30',poolId:'live',ipsec:null,mtu:1476,keepaliveInterval:10,keepaliveRetries:3,dscp:'inherit',clampTcpMss:true,allowFastPath:true,comment:'',enabled:true,status:'up'}
|
||||
const rule={id:'own208-rule',rosId:'*R',serverId:'1',serverName:'Own208 router',family:'ip',table:'filter',chain:'forward',action:'accept',proto:'tcp',src:'',dst:'',port:'443',iface:'',comment:'Own208 rule',enabled:true,hits:1}
|
||||
const json=path==='/api/auth/config'?{required:true,portal_url:'http://own.portal.invalid'}:path==='/api/servers'?[{id:1,name:'Own208 router',host:'192.0.2.208',type:'exit-node',enabled:true,country:'RU',site:'OWN',asn:'',status:'online',latency:1}]:path==='/api/wireguard'?{interfaces:[iface]}:path==='/api/ipsec'?{servers:[],clients:[]}:path==='/api/gre/tunnels'||path==='/api/filters/gre-tunnels'?{tunnels:[tunnel]}:path==='/api/firewall/all'?{rules:[rule],addressLists:[]}:path==='/api/filters/rules'?{rulesets:[{serverId:'1',rules:[{id:'own208-filter',community:'65001:208',gateway:'',gatewayTunnelId:'',action:'blackhole',description:'Own208 filter'}]}],greTunnels:[],live:true}:path==='/api/recursive-routes'?{routes:[]}:path==='/api/evobgp/settings'?{enabled:false,baseUrl:'',secretConfigured:false}:{}
|
||||
return r.fulfill({json})});return ()=>writes
|
||||
}
|
||||
for(const width of [1440,320])for(const section of sections)for(const writer of [false,true])test(`Native208 ${section} ${writer?'writer':'reader'} ${width}`,async({page})=>{
|
||||
await page.setViewportSize({width,height:1000});const writes=await setup(page,writer);await page.goto('/'+section+'?server=1');const create=page.getByRole('button',{name:names[section],exact:true}).first();await expect(create).toBeVisible();if(writer){await expect(create).toBeEnabled()}else{await expect(create).toBeDisabled();await expect(page.getByRole('status').filter({hasText:'Режим просмотра'})).toBeVisible();await create.evaluate(b=>(b as HTMLButtonElement).click())}
|
||||
if(section==='wireguard'&&!writer){await expect(page.getByRole('button',{name:'Импорт',exact:true})).toBeDisabled();await page.getByRole('button',{name:'Действия: own208-wg',exact:true}).click();await expect(page.getByRole('menuitem',{name:'Экспорт',exact:true})).toBeDisabled();await expect(page.getByRole('menuitem',{name:'Добавить пира',exact:true})).toHaveCount(0);await page.keyboard.press('Escape')}
|
||||
expect(writes()).toBe(0);expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)).toBe(true)
|
||||
})
|
||||
Reference in new issue
Block a user