mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
feat: enforce RBAC and harden collaboration features (#1327)
* feat: enforce RBAC and harden collaboration features - Mount requirePermission on hosts/snippets/credentials/automations/AI routes - Seed and backfill system role permissions on every dialect at startup - Support personal credential overrides for RDP/VNC/Telnet shared hosts - Broadcast participant presence in shared terminal sessions - Make audit log forwarding configurable from the admin panel - Add role members endpoint and snippet folder sharing * fix: enforce RBAC across split routes
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useXTerm } from "react-xtermjs";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { AlertCircle, Eye } from "lucide-react";
|
||||
import { AlertCircle, Eye, Users } from "lucide-react";
|
||||
import {
|
||||
resolveShareLink,
|
||||
type ResolvedShareLink,
|
||||
@@ -44,6 +44,41 @@ async function resolveTerminalWsBaseUrl(): Promise<string> {
|
||||
return `${wsProtocol}://${window.location.host}${getBasePath()}/ssh/websocket/`;
|
||||
}
|
||||
|
||||
export interface SessionParticipantInfo {
|
||||
isOwner: boolean;
|
||||
permissionLevel: "read-write" | "read-only";
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
function ParticipantsBadge({
|
||||
participants,
|
||||
ownerLabel,
|
||||
}: {
|
||||
participants: SessionParticipantInfo[];
|
||||
ownerLabel: string;
|
||||
}) {
|
||||
if (participants.length < 2) return null;
|
||||
const names = participants
|
||||
.map((participant) =>
|
||||
participant.isOwner ? ownerLabel : (participant.label ?? "?"),
|
||||
)
|
||||
.join(", ");
|
||||
return (
|
||||
<div
|
||||
className="absolute top-3 left-3 z-20 flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-elevated, rgba(0,0,0,0.6))",
|
||||
color: "var(--foreground)",
|
||||
border: "1px solid var(--border-base)",
|
||||
}}
|
||||
title={names}
|
||||
>
|
||||
<Users className="size-3.5" />
|
||||
{participants.length}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReadOnlyBadge({ label }: { label: string }) {
|
||||
return (
|
||||
<div
|
||||
@@ -93,6 +128,9 @@ function GuestTerminalView({
|
||||
const { t } = useTranslation();
|
||||
const { instance: terminal, ref: xtermRef } = useXTerm();
|
||||
const [ended, setEnded] = useState<string | null>(null);
|
||||
const [participants, setParticipants] = useState<SessionParticipantInfo[]>(
|
||||
[],
|
||||
);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const pingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
@@ -140,6 +178,11 @@ function GuestTerminalView({
|
||||
case "data":
|
||||
if (typeof msg.data === "string") terminal.write(msg.data);
|
||||
break;
|
||||
case "participants":
|
||||
if (Array.isArray(msg.participants)) {
|
||||
setParticipants(msg.participants as SessionParticipantInfo[]);
|
||||
}
|
||||
break;
|
||||
case "sessionExpired":
|
||||
case "sessionTerminatedByOwner":
|
||||
case "session_ended":
|
||||
@@ -176,6 +219,10 @@ function GuestTerminalView({
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<ParticipantsBadge
|
||||
participants={participants}
|
||||
ownerLabel={t("sessionSharing.guestView.ownerLabel")}
|
||||
/>
|
||||
{share.permissionLevel === "read-only" && (
|
||||
<ReadOnlyBadge label={t("sessionSharing.guestView.readOnlyBadge")} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user