mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-02 08:35:24 +00:00
v26.10.6 (#1739)
* feat: let the login page send the token back to the mobile app * feat: add host-scripts entry to the plugin sdk * feat: let plugins say how they show in the mobile app * feat: frame host for running one plugin view outside the shell * fix: build the SDK host modules in production mode * chore: note the SDK host build fix in the changelog * feat: list plugin engine ranges for the mobile app * feat: host color and icon columns that sync * chore: update translations and plugin pins * fix: test * fix: show source address when rejecting proxy auth * fix: deleting your own account removes all of its data * fix(deps): update body-parser to 2.4.0 (#1736) * fix: body-parser 보안 패치 반영 * chore: 자동 검사를 막던 기존 JSON 서식 정리 --------- Co-authored-by: renovys <180682734+renovys@users.noreply.github.com> * fix(sdk): decode command output as GBK when it is not valid UTF-8 (#1735) * fix: honor shared credentials on shared hosts (#1730) * fix: honor shared credentials on shared hosts and in personal SSH auth * fix: snapshot shared SSH auth from a credential shared with the host owner * fix: size DataView list columns by the table's own width (#1713) * fix: size DataView list columns by the table's own width The list grid was built from every column, so columns hidden below md/lg left empty tracks and the visible cells landed under the wrong headers. Each width now gets a template of only its visible columns (CSS variables + container queries on the table, so a narrow panel counts, not the window), the header uses the same gap as the rows, and rows use a minimum height so wrapped cells no longer overlap. * test: wait for the title bar offset effect before asserting The bar shows once getTitleBarVisible resolves, but --termix-titlebar-height is set in a later passive effect, so reading it right after findByText raced (3-5 of 40 runs under load, on main too). * chore: rerun CI against dev-26.10.6 * fix: unlock passphrase keys once per connect burst (#1737) * feat: give ssh transports the host origin and sync id (#1717) * fix: wake other clients after a sync push (Plugin-Sync#1) * feat: look up local row ids by sync id for the sync plugin (Plugin-Sync#4) * feat: rail items can show a status dot (Plugin-Sync#5) * fix: add the desktop shared copy banner text (#1709) * feat: sidebar preference for alternating rows (#1712) * fix: striped rows text * feat: shell can open a saved host's editor for plugins (Plugin-SSH-Terminal#36) * fix: show the logo in the macos menu bar (#1728) * fix: number shortcuts skip the home tab (#1724) * fix: show plugins that could not be downloaded as failed (#1714) * fix: pass the host id as text when opening its editor * fix: retry a plugin bundle that was not ready yet (#1708) * fix: let commonjs plugins fall back to core packages on the desktop * fix: let non docker installs switch nginx to https (#1711) * feat: remove a folder share from its hosts too (#1719) * chore: changelog for merged pull requests * fix: accept the release .sig when installing a plugin from a file * feat: folder logins for shared hosts, read only plugin sync, full desktop builds * fix: test * fix: stop dev runner restart loops and narrow plugin watchers * chore: update translations and plugin pins * chore: lint, format, and bump version to 26.10.6 --------- Co-authored-by: renovys <renovy@gmail.com> Co-authored-by: renovys <180682734+renovys@users.noreply.github.com> Co-authored-by: Sebastian Cao <cycsmail@gmail.com> Co-authored-by: Jonathan Rehuel Lewerissa <jonathan.rehuel16@gmail.com> Co-authored-by: Rhiz3K <33246262+Rhiz3K@users.noreply.github.com>
This commit is contained in:
168 files changed
+35063
-339
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
import { isAuthOverrideProtocol } from "../../hosts/protocol-auth/registry.js";
|
||||
import {
|
||||
assertHostsUnlocked,
|
||||
hostLock,
|
||||
hasHostLocks,
|
||||
requireUnlockedHost,
|
||||
sendHostLocked,
|
||||
@@ -36,8 +37,10 @@ import {
|
||||
createCurrentHostResolutionRepository,
|
||||
createCurrentRbacAccessRepository,
|
||||
createCurrentRoleRepository,
|
||||
createCurrentSharedFolderAuthOverrideRepository,
|
||||
createCurrentUserRepository,
|
||||
} from "../repositories/factory.js";
|
||||
import { findUsableCredential } from "../../hosts/usable-credential.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -722,14 +725,40 @@ router.get(
|
||||
},
|
||||
);
|
||||
|
||||
/** The host grants that went to the same user or role as a folder rule. */
|
||||
export function grantsForFolderRule<
|
||||
T extends { userId?: string | null; roleId?: number | null },
|
||||
>(entries: T[], rule: { userId?: string | null; roleId?: number | null }): T[] {
|
||||
return entries.filter((entry) =>
|
||||
rule.userId
|
||||
? entry.userId === rule.userId
|
||||
: rule.roleId != null && entry.roleId === rule.roleId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /rbac/folder/access/{id}:
|
||||
* delete:
|
||||
* summary: Stop a folder share from applying to hosts added later
|
||||
* description: Grants already fanned out to hosts stay; revoke those per host.
|
||||
* description: Grants already fanned out to hosts stay, unless fromHosts=true, which also removes the same user's or role's access from every host in the folder and its subfolders that the caller may manage sharing on.
|
||||
* tags:
|
||||
* - RBAC
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* - in: query
|
||||
* name: fromHosts
|
||||
* schema:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Removed. removedFromHosts counts the host grants removed.
|
||||
* 404:
|
||||
* description: No such rule.
|
||||
*/
|
||||
router.delete(
|
||||
"/folder/access/:id",
|
||||
@@ -741,11 +770,35 @@ router.delete(
|
||||
}
|
||||
try {
|
||||
const repository = createCurrentFolderAccessRepository();
|
||||
if (!(await repository.findById(ruleId, req.userId!))) {
|
||||
const rule = await repository.findById(ruleId, req.userId!);
|
||||
if (!rule) {
|
||||
return res.status(404).json({ error: "Rule not found" });
|
||||
}
|
||||
await repository.deleteById(ruleId, req.userId!);
|
||||
res.json({ success: true });
|
||||
|
||||
let removedFromHosts = 0;
|
||||
if (req.query.fromHosts === "true") {
|
||||
const hosts =
|
||||
await createCurrentHostFolderRepository().listHostsInFolder(
|
||||
req.userId!,
|
||||
rule.folder,
|
||||
);
|
||||
const access = createCurrentRbacAccessRepository();
|
||||
for (const host of hosts) {
|
||||
if (hostLock(host.id)) continue;
|
||||
if (!(await canManageHostSharing(req.userId!, host.id)).allowed)
|
||||
continue;
|
||||
let changed = false;
|
||||
const entries = await access.listHostAccess(host.id);
|
||||
for (const entry of grantsForFolderRule(entries, rule)) {
|
||||
await access.revokeHostAccess(entry.id, host.id);
|
||||
removedFromHosts++;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) await pruneHostGrants(host.id);
|
||||
}
|
||||
}
|
||||
res.json({ success: true, removedFromHosts });
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to delete folder access rule", error, {
|
||||
operation: "delete_folder_access",
|
||||
@@ -2174,4 +2227,157 @@ router.get(
|
||||
},
|
||||
);
|
||||
|
||||
/** A folder path as hosts store it ("Parent / Child"), or null. */
|
||||
function folderParam(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const folder = value
|
||||
.split(" / ")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
.join(" / ");
|
||||
return folder && folder.length <= 255 ? folder : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /rbac/folder-auth/{protocol}:
|
||||
* get:
|
||||
* summary: Get the current user's personal login for shared hosts in a folder
|
||||
* tags: [RBAC]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: protocol
|
||||
* required: true
|
||||
* schema: { type: string }
|
||||
* - in: query
|
||||
* name: folder
|
||||
* required: true
|
||||
* schema: { type: string }
|
||||
* responses:
|
||||
* 200:
|
||||
* description: The folder, protocol and credential id, or null when none is set.
|
||||
* 400:
|
||||
* description: Invalid folder or protocol.
|
||||
*/
|
||||
router.get(
|
||||
"/folder-auth/:protocol",
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
const userId = (req as AuthenticatedRequest).userId!;
|
||||
const protocol = req.params.protocol;
|
||||
const folder = folderParam(req.query.folder);
|
||||
if (!folder) return res.status(400).json({ error: "Invalid folder" });
|
||||
if (!isAuthOverrideProtocol(protocol)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Invalid authentication protocol" });
|
||||
}
|
||||
const credentialId =
|
||||
await createCurrentSharedFolderAuthOverrideRepository().findCredentialId(
|
||||
userId,
|
||||
folder,
|
||||
protocol,
|
||||
);
|
||||
res.json({ folder, protocol, credentialId });
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to get folder login", error);
|
||||
res.status(500).json({ error: "Failed to fetch credential" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /rbac/folder-auth/{protocol}:
|
||||
* put:
|
||||
* summary: Set the current user's personal login for shared hosts in a folder
|
||||
* description: >
|
||||
* Used for every host shared with the user in this folder and its
|
||||
* subfolders that has no personal login of its own. credentialId null
|
||||
* clears it.
|
||||
* tags: [RBAC]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: protocol
|
||||
* required: true
|
||||
* schema: { type: string }
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [folder, credentialId]
|
||||
* properties:
|
||||
* folder: { type: string }
|
||||
* credentialId: { type: integer, nullable: true }
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Saved.
|
||||
* 400:
|
||||
* description: Invalid folder, protocol or credential id.
|
||||
* 404:
|
||||
* description: The credential is not one the user can use.
|
||||
*/
|
||||
router.put(
|
||||
"/folder-auth/:protocol",
|
||||
authenticateJWT,
|
||||
requireDataAccess,
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
try {
|
||||
const userId = (req as AuthenticatedRequest).userId!;
|
||||
const protocol = req.params.protocol;
|
||||
const folder = folderParam(req.body?.folder);
|
||||
const credentialId = req.body?.credentialId ?? null;
|
||||
if (!folder) return res.status(400).json({ error: "Invalid folder" });
|
||||
if (!isAuthOverrideProtocol(protocol)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Invalid authentication protocol" });
|
||||
}
|
||||
if (
|
||||
credentialId !== null &&
|
||||
(!Number.isInteger(credentialId) || credentialId <= 0)
|
||||
) {
|
||||
return res.status(400).json({
|
||||
error: "credentialId must be a positive integer or null",
|
||||
});
|
||||
}
|
||||
|
||||
const repository = createCurrentSharedFolderAuthOverrideRepository();
|
||||
if (credentialId === null) {
|
||||
await repository.clearCredential(userId, folder, protocol);
|
||||
} else {
|
||||
if (!(await findUsableCredential(credentialId, userId))) {
|
||||
return res.status(404).json({ error: "Credential not found" });
|
||||
}
|
||||
await repository.setCredential(userId, folder, protocol, credentialId);
|
||||
}
|
||||
|
||||
const { ipAddress, userAgent } = getRequestMeta(req);
|
||||
await logAudit({
|
||||
userId,
|
||||
username: await getAuditUsername(userId),
|
||||
action:
|
||||
credentialId === null
|
||||
? "clear_shared_folder_auth_override"
|
||||
: "set_shared_folder_auth_override",
|
||||
resourceType: "folder",
|
||||
resourceName: folder,
|
||||
details: JSON.stringify({ protocol, credentialId }),
|
||||
ipAddress,
|
||||
userAgent,
|
||||
success: true,
|
||||
}).catch(() => undefined);
|
||||
|
||||
res.json({ success: true, folder, protocol, credentialId });
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to set folder login", error);
|
||||
res.status(500).json({ error: "Failed to update credential" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
Reference in new issue
Block a user