Notes and Expiry fields add (#453)
* Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Notes and Expiry add * fix: cleanup files --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com>
This commit was merged in pull request #453.
This commit is contained in:
@@ -210,6 +210,8 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
stats_config TEXT,
|
||||
docker_config TEXT,
|
||||
terminal_config TEXT,
|
||||
notes TEXT,
|
||||
expiration_date TEXT,
|
||||
use_socks5 INTEGER,
|
||||
socks5_host TEXT,
|
||||
socks5_port INTEGER,
|
||||
@@ -575,6 +577,9 @@ const migrateSchema = () => {
|
||||
);
|
||||
addColumnIfNotExists("ssh_data", "docker_config", "TEXT");
|
||||
|
||||
addColumnIfNotExists("ssh_data", "notes", "TEXT");
|
||||
addColumnIfNotExists("ssh_data", "expiration_date", "TEXT");
|
||||
|
||||
// SOCKS5 Proxy columns
|
||||
addColumnIfNotExists("ssh_data", "use_socks5", "INTEGER");
|
||||
addColumnIfNotExists("ssh_data", "socks5_host", "TEXT");
|
||||
|
||||
@@ -93,6 +93,8 @@ export const sshData = sqliteTable("ssh_data", {
|
||||
statsConfig: text("stats_config"),
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
notes: text("notes"),
|
||||
expirationDate: text("expiration_date"),
|
||||
|
||||
useSocks5: integer("use_socks5", { mode: "boolean" }),
|
||||
socks5Host: text("socks5_host"),
|
||||
|
||||
@@ -14,7 +14,17 @@ import {
|
||||
hostAccess,
|
||||
userRoles,
|
||||
} from "../db/schema.js";
|
||||
import { eq, and, desc, isNotNull, or, isNull, gte, sql, inArray } from "drizzle-orm";
|
||||
import {
|
||||
eq,
|
||||
and,
|
||||
desc,
|
||||
isNotNull,
|
||||
or,
|
||||
isNull,
|
||||
gte,
|
||||
sql,
|
||||
inArray,
|
||||
} from "drizzle-orm";
|
||||
import type { Request, Response } from "express";
|
||||
import multer from "multer";
|
||||
import { sshLogger } from "../../utils/logger.js";
|
||||
@@ -245,12 +255,15 @@ router.post(
|
||||
statsConfig,
|
||||
terminalConfig,
|
||||
forceKeyboardInteractive,
|
||||
notes,
|
||||
expirationDate,
|
||||
useSocks5,
|
||||
socks5Host,
|
||||
socks5Port,
|
||||
socks5Username,
|
||||
socks5Password,
|
||||
socks5ProxyChain,
|
||||
overrideCredentialUsername,
|
||||
} = hostData;
|
||||
|
||||
console.log("POST /db/ssh - Received SOCKS5 data:", {
|
||||
@@ -285,6 +298,7 @@ router.post(
|
||||
username,
|
||||
authType: effectiveAuthType,
|
||||
credentialId: credentialId || null,
|
||||
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
|
||||
pin: pin ? 1 : 0,
|
||||
enableTerminal: enableTerminal ? 1 : 0,
|
||||
enableTunnel: enableTunnel ? 1 : 0,
|
||||
@@ -301,12 +315,16 @@ router.post(
|
||||
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
|
||||
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||
notes: notes || null,
|
||||
expirationDate: expirationDate || null,
|
||||
useSocks5: useSocks5 ? 1 : 0,
|
||||
socks5Host: socks5Host || null,
|
||||
socks5Port: socks5Port || null,
|
||||
socks5Username: socks5Username || null,
|
||||
socks5Password: socks5Password || null,
|
||||
socks5ProxyChain: socks5ProxyChain ? JSON.stringify(socks5ProxyChain) : null,
|
||||
socks5ProxyChain: socks5ProxyChain
|
||||
? JSON.stringify(socks5ProxyChain)
|
||||
: null,
|
||||
};
|
||||
|
||||
if (effectiveAuthType === "password") {
|
||||
@@ -489,13 +507,20 @@ router.put(
|
||||
statsConfig,
|
||||
terminalConfig,
|
||||
forceKeyboardInteractive,
|
||||
notes,
|
||||
expirationDate,
|
||||
useSocks5,
|
||||
socks5Host,
|
||||
socks5Port,
|
||||
socks5Username,
|
||||
socks5Password,
|
||||
socks5ProxyChain,
|
||||
overrideCredentialUsername,
|
||||
} = hostData;
|
||||
|
||||
// Temporary logging to debug notes and expirationDate
|
||||
console.log("DEBUG - Update host data:", { notes, expirationDate });
|
||||
|
||||
if (
|
||||
!isNonEmptyString(userId) ||
|
||||
!isNonEmptyString(ip) ||
|
||||
@@ -523,6 +548,7 @@ router.put(
|
||||
username,
|
||||
authType: effectiveAuthType,
|
||||
credentialId: credentialId || null,
|
||||
overrideCredentialUsername: overrideCredentialUsername ? 1 : 0,
|
||||
pin: pin ? 1 : 0,
|
||||
enableTerminal: enableTerminal ? 1 : 0,
|
||||
enableTunnel: enableTunnel ? 1 : 0,
|
||||
@@ -539,12 +565,16 @@ router.put(
|
||||
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
|
||||
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||
notes: notes || null,
|
||||
expirationDate: expirationDate || null,
|
||||
useSocks5: useSocks5 ? 1 : 0,
|
||||
socks5Host: socks5Host || null,
|
||||
socks5Port: socks5Port || null,
|
||||
socks5Username: socks5Username || null,
|
||||
socks5Password: socks5Password || null,
|
||||
socks5ProxyChain: socks5ProxyChain ? JSON.stringify(socks5ProxyChain) : null,
|
||||
socks5ProxyChain: socks5ProxyChain
|
||||
? JSON.stringify(socks5ProxyChain)
|
||||
: null,
|
||||
};
|
||||
|
||||
if (effectiveAuthType === "password") {
|
||||
@@ -742,6 +772,8 @@ router.get(
|
||||
credentialId: sshData.credentialId,
|
||||
overrideCredentialUsername: sshData.overrideCredentialUsername,
|
||||
quickActions: sshData.quickActions,
|
||||
notes: sshData.notes,
|
||||
expirationDate: sshData.expirationDate,
|
||||
|
||||
// Shared access info
|
||||
isShared: sql<boolean>`${hostAccess.id} IS NOT NULL`,
|
||||
@@ -755,12 +787,11 @@ router.get(
|
||||
eq(hostAccess.hostId, sshData.id),
|
||||
or(
|
||||
eq(hostAccess.userId, userId),
|
||||
roleIds.length > 0 ? inArray(hostAccess.roleId, roleIds) : sql`false`,
|
||||
),
|
||||
or(
|
||||
isNull(hostAccess.expiresAt),
|
||||
gte(hostAccess.expiresAt, now),
|
||||
roleIds.length > 0
|
||||
? inArray(hostAccess.roleId, roleIds)
|
||||
: sql`false`,
|
||||
),
|
||||
or(isNull(hostAccess.expiresAt), gte(hostAccess.expiresAt, now)),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
@@ -769,10 +800,7 @@ router.get(
|
||||
and(
|
||||
// Shared to user directly (not expired)
|
||||
eq(hostAccess.userId, userId),
|
||||
or(
|
||||
isNull(hostAccess.expiresAt),
|
||||
gte(hostAccess.expiresAt, now),
|
||||
),
|
||||
or(isNull(hostAccess.expiresAt), gte(hostAccess.expiresAt, now)),
|
||||
),
|
||||
roleIds.length > 0
|
||||
? and(
|
||||
|
||||
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-transparent dark:bg-input/30 px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] duration-200 outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 disabled:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -670,6 +670,8 @@
|
||||
"folder": "Folder",
|
||||
"tags": "Tags",
|
||||
"pin": "Pin",
|
||||
"notes": "Notes",
|
||||
"expirationDate": "Expiration Date",
|
||||
"passwordRequired": "Password is required when using password authentication",
|
||||
"sshKeyRequired": "SSH Private Key is required when using key authentication",
|
||||
"keyTypeRequired": "Key Type is required when using key authentication",
|
||||
@@ -1615,6 +1617,8 @@
|
||||
"folder": "folder",
|
||||
"password": "password",
|
||||
"keyPassword": "key password",
|
||||
"notes": "Add notes about this host...",
|
||||
"expirationDate": "Select expiration date",
|
||||
"pastePrivateKey": "Paste your private key here...",
|
||||
"pastePublicKey": "Paste your public key here...",
|
||||
"credentialName": "My SSH Server",
|
||||
|
||||
@@ -665,6 +665,8 @@
|
||||
"folder": "Папка",
|
||||
"tags": "Теги",
|
||||
"pin": "Закрепить",
|
||||
"notes": "Заметки",
|
||||
"expirationDate": "Дата истечения",
|
||||
"passwordRequired": "Пароль требуется при использовании аутентификации по паролю",
|
||||
"sshKeyRequired": "Приватный SSH-ключ требуется при использовании аутентификации по ключу",
|
||||
"keyTypeRequired": "Тип ключа требуется при использовании аутентификации по ключу",
|
||||
@@ -915,7 +917,7 @@
|
||||
"noProxyNodes": "Узлы прокси не настроены. Нажмите 'Добавить узел прокси' чтобы добавить.",
|
||||
"proxyNode": "Узел прокси",
|
||||
"proxyType": "Тип прокси",
|
||||
"advancedAuthSettings": "Расширенные настройки аутентификации"
|
||||
"advancedAuthSettings": "Расширенные настройки аутентификации",
|
||||
"advancedAuthSettings": "Расширенные настройки аутентификации",
|
||||
"addQuickAction": "Добавить Quick Action",
|
||||
"allHostsInFolderDeleted": "{{count}} хостов успешно удалены из папки \"{{folder}}\"",
|
||||
@@ -1609,6 +1611,8 @@
|
||||
"folder": "папка",
|
||||
"password": "пароль",
|
||||
"keyPassword": "пароль ключа",
|
||||
"notes": "Добавьте заметки об этом хосте...",
|
||||
"expirationDate": "Выберите дату истечения",
|
||||
"pastePrivateKey": "Вставьте ваш приватный ключ здесь...",
|
||||
"pastePublicKey": "Вставьте ваш публичный ключ здесь...",
|
||||
"credentialName": "Мой SSH-сервер",
|
||||
|
||||
@@ -47,6 +47,8 @@ export interface SSHHost {
|
||||
quickActions?: QuickAction[];
|
||||
statsConfig?: string | Record<string, unknown>;
|
||||
terminalConfig?: TerminalConfig;
|
||||
notes?: string;
|
||||
expirationDate?: string;
|
||||
|
||||
useSocks5?: boolean;
|
||||
socks5Host?: string;
|
||||
@@ -107,6 +109,8 @@ export interface SSHHostData {
|
||||
quickActions?: QuickActionData[];
|
||||
statsConfig?: string | Record<string, unknown>;
|
||||
terminalConfig?: TerminalConfig;
|
||||
notes?: string;
|
||||
expirationDate?: string;
|
||||
|
||||
// SOCKS5 Proxy configuration
|
||||
useSocks5?: boolean;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@/components/ui/form.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area.tsx";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import {
|
||||
@@ -534,6 +535,8 @@ export function HostManagerEditor({
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
notes: z.string().optional(),
|
||||
expirationDate: z.string().optional(),
|
||||
useSocks5: z.boolean().optional(),
|
||||
socks5Host: z.string().optional(),
|
||||
socks5Port: z.coerce.number().min(1).max(65535).optional(),
|
||||
@@ -639,6 +642,8 @@ export function HostManagerEditor({
|
||||
statsConfig: DEFAULT_STATS_CONFIG,
|
||||
terminalConfig: DEFAULT_TERMINAL_CONFIG,
|
||||
forceKeyboardInteractive: false,
|
||||
notes: "",
|
||||
expirationDate: "",
|
||||
useSocks5: false,
|
||||
socks5Host: "",
|
||||
socks5Port: 1080,
|
||||
@@ -741,6 +746,8 @@ export function HostManagerEditor({
|
||||
: [],
|
||||
},
|
||||
forceKeyboardInteractive: Boolean(cleanedHost.forceKeyboardInteractive),
|
||||
notes: cleanedHost.notes || "",
|
||||
expirationDate: cleanedHost.expirationDate || "",
|
||||
useSocks5: Boolean(cleanedHost.useSocks5),
|
||||
socks5Host: cleanedHost.socks5Host || "",
|
||||
socks5Port: cleanedHost.socks5Port || 1080,
|
||||
@@ -863,15 +870,6 @@ export function HostManagerEditor({
|
||||
...data,
|
||||
};
|
||||
|
||||
if (proxyMode === "single") {
|
||||
submitData.socks5ProxyChain = [];
|
||||
} else if (proxyMode === "chain") {
|
||||
submitData.socks5Host = "";
|
||||
submitData.socks5Port = 1080;
|
||||
submitData.socks5Username = "";
|
||||
submitData.socks5Password = "";
|
||||
}
|
||||
|
||||
if (data.authType !== "credential") {
|
||||
submitData.credentialId = undefined;
|
||||
}
|
||||
@@ -1390,6 +1388,47 @@ export function HostManagerEditor({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expirationDate"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-10">
|
||||
<FormLabel>{t("hosts.expirationDate")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="date"
|
||||
placeholder={t("placeholders.expirationDate")}
|
||||
value={field.value || ""}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-26">
|
||||
<FormLabel>{t("hosts.notes")}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t("placeholders.notes")}
|
||||
className="resize-none"
|
||||
rows={3}
|
||||
value={field.value || ""}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormLabel className="mb-3 mt-3 font-bold">
|
||||
{t("hosts.authentication")}
|
||||
|
||||
@@ -931,6 +931,8 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
||||
: null,
|
||||
terminalConfig: hostData.terminalConfig || null,
|
||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
||||
notes: hostData.notes || "",
|
||||
expirationDate: hostData.expirationDate || "",
|
||||
useSocks5: Boolean(hostData.useSocks5),
|
||||
socks5Host: hostData.socks5Host || null,
|
||||
socks5Port: hostData.socks5Port || null,
|
||||
@@ -1009,6 +1011,8 @@ export async function updateSSHHost(
|
||||
: null,
|
||||
terminalConfig: hostData.terminalConfig || null,
|
||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
||||
notes: hostData.notes || "",
|
||||
expirationDate: hostData.expirationDate || "",
|
||||
useSocks5: Boolean(hostData.useSocks5),
|
||||
socks5Host: hostData.socks5Host || null,
|
||||
socks5Port: hostData.socks5Port || null,
|
||||
@@ -3240,7 +3244,9 @@ export async function updateRole(
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRole(roleId: number): Promise<{ success: boolean }> {
|
||||
export async function deleteRole(
|
||||
roleId: number,
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
const response = await rbacApi.delete(`/rbac/roles/${roleId}`);
|
||||
return response.data;
|
||||
@@ -3250,7 +3256,9 @@ export async function deleteRole(roleId: number): Promise<{ success: boolean }>
|
||||
}
|
||||
|
||||
// User-Role Management
|
||||
export async function getUserRoles(userId: string): Promise<{ roles: UserRole[] }> {
|
||||
export async function getUserRoles(
|
||||
userId: string,
|
||||
): Promise<{ roles: UserRole[] }> {
|
||||
try {
|
||||
const response = await rbacApi.get(`/rbac/users/${userId}/roles`);
|
||||
return response.data;
|
||||
@@ -3264,7 +3272,9 @@ export async function assignRoleToUser(
|
||||
roleId: number,
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
const response = await rbacApi.post(`/rbac/users/${userId}/roles`, { roleId });
|
||||
const response = await rbacApi.post(`/rbac/users/${userId}/roles`, {
|
||||
roleId,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "assign role to user");
|
||||
@@ -3276,7 +3286,9 @@ export async function removeRoleFromUser(
|
||||
roleId: number,
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
const response = await rbacApi.delete(`/rbac/users/${userId}/roles/${roleId}`);
|
||||
const response = await rbacApi.delete(
|
||||
`/rbac/users/${userId}/roles/${roleId}`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "remove role from user");
|
||||
@@ -3295,14 +3307,19 @@ export async function shareHost(
|
||||
},
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
const response = await rbacApi.post(`/rbac/host/${hostId}/share`, shareData);
|
||||
const response = await rbacApi.post(
|
||||
`/rbac/host/${hostId}/share`,
|
||||
shareData,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "share host");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHostAccess(hostId: number): Promise<{ accessList: AccessRecord[] }> {
|
||||
export async function getHostAccess(
|
||||
hostId: number,
|
||||
): Promise<{ accessList: AccessRecord[] }> {
|
||||
try {
|
||||
const response = await rbacApi.get(`/rbac/host/${hostId}/access`);
|
||||
return response.data;
|
||||
@@ -3316,10 +3333,14 @@ export async function revokeHostAccess(
|
||||
accessId: number,
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
const response = await rbacApi.delete(`/rbac/host/${hostId}/access/${accessId}`);
|
||||
const response = await rbacApi.delete(
|
||||
`/rbac/host/${hostId}/access/${accessId}`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "revoke host access");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DOCKER MANAGEMENT API
|
||||
|
||||
Reference in New Issue
Block a user