fix: resolve TypeScript and ESLint errors across the codebase
- Fixed @typescript-eslint/no-unused-vars errors (31 instances) - Fixed @typescript-eslint/no-explicit-any errors in backend (~22 instances) - Fixed @typescript-eslint/no-explicit-any errors in frontend (~60 instances) - Fixed prefer-const errors (5 instances) - Fixed no-empty-object-type and rules-of-hooks errors - Added proper type assertions for database operations - Improved type safety in authentication and encryption modules - Enhanced type definitions for API routes and SSH operations All TypeScript compilation errors resolved. Application builds and runs successfully.
This commit is contained in:
@@ -702,24 +702,22 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
const getNestedValue = (
|
||||
obj: Record<string, unknown>,
|
||||
path: string,
|
||||
): any => {
|
||||
): unknown => {
|
||||
if (!path || !obj) return null;
|
||||
return path.split(".").reduce((current, key) => current?.[key], obj);
|
||||
};
|
||||
|
||||
const identifier =
|
||||
getNestedValue(userInfo, config.identifier_path) ||
|
||||
const identifier = (getNestedValue(userInfo, config.identifier_path) ||
|
||||
userInfo[config.identifier_path] ||
|
||||
userInfo.sub ||
|
||||
userInfo.email ||
|
||||
userInfo.preferred_username;
|
||||
userInfo.preferred_username) as string;
|
||||
|
||||
const name =
|
||||
getNestedValue(userInfo, config.name_path) ||
|
||||
const name = (getNestedValue(userInfo, config.name_path) ||
|
||||
userInfo[config.name_path] ||
|
||||
userInfo.name ||
|
||||
userInfo.given_name ||
|
||||
identifier;
|
||||
identifier) as string;
|
||||
|
||||
if (!identifier) {
|
||||
authLogger.error(
|
||||
@@ -753,14 +751,14 @@ router.get("/oidc/callback", async (req, res) => {
|
||||
is_admin: isFirstUser,
|
||||
is_oidc: true,
|
||||
oidc_identifier: identifier,
|
||||
client_id: config.client_id,
|
||||
client_secret: config.client_secret,
|
||||
issuer_url: config.issuer_url,
|
||||
authorization_url: config.authorization_url,
|
||||
token_url: config.token_url,
|
||||
identifier_path: config.identifier_path,
|
||||
name_path: config.name_path,
|
||||
scopes: config.scopes,
|
||||
client_id: String(config.client_id),
|
||||
client_secret: String(config.client_secret),
|
||||
issuer_url: String(config.issuer_url),
|
||||
authorization_url: String(config.authorization_url),
|
||||
token_url: String(config.token_url),
|
||||
identifier_path: String(config.identifier_path),
|
||||
name_path: String(config.name_path),
|
||||
scopes: String(config.scopes),
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { eq, and } from "drizzle-orm";
|
||||
import { fileLogger } from "../utils/logger.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import type { AuthenticatedRequest } from "../../types/index.js";
|
||||
|
||||
function isExecutableFile(permissions: string, fileName: string): boolean {
|
||||
const hasExecutePermission =
|
||||
@@ -166,7 +167,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
credentialId,
|
||||
} = req.body;
|
||||
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!userId) {
|
||||
fileLogger.error("SSH connection rejected: no authenticated user", {
|
||||
@@ -246,7 +247,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
const config: any = {
|
||||
const config: Record<string, unknown> = {
|
||||
host: ip,
|
||||
port: port || 22,
|
||||
username,
|
||||
@@ -417,7 +418,9 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
});
|
||||
} else {
|
||||
if (resolvedCredentials.password) {
|
||||
const responses = prompts.map(() => resolvedCredentials.password || "");
|
||||
const responses = prompts.map(
|
||||
() => resolvedCredentials.password || "",
|
||||
);
|
||||
finish(responses);
|
||||
} else {
|
||||
finish(prompts.map(() => ""));
|
||||
@@ -432,7 +435,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
const { sessionId, totpCode } = req.body;
|
||||
|
||||
const userId = (req as any).userId;
|
||||
const userId = (req as AuthenticatedRequest).userId;
|
||||
|
||||
if (!userId) {
|
||||
fileLogger.error("TOTP verification rejected: no authenticated user", {
|
||||
@@ -454,7 +457,9 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
sessionId,
|
||||
userId,
|
||||
});
|
||||
return res.status(404).json({ error: "TOTP session expired. Please reconnect." });
|
||||
return res
|
||||
.status(404)
|
||||
.json({ error: "TOTP session expired. Please reconnect." });
|
||||
}
|
||||
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
@@ -462,8 +467,12 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
if (Date.now() - session.createdAt > 120000) {
|
||||
try {
|
||||
session.client.end();
|
||||
} catch {}
|
||||
return res.status(408).json({ error: "TOTP session timeout. Please reconnect." });
|
||||
} catch {
|
||||
// Ignore errors when closing timed out session
|
||||
}
|
||||
return res
|
||||
.status(408)
|
||||
.json({ error: "TOTP session timeout. Please reconnect." });
|
||||
}
|
||||
|
||||
session.finish([totpCode]);
|
||||
@@ -487,7 +496,10 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
userId,
|
||||
});
|
||||
|
||||
res.json({ status: "success", message: "TOTP verified, SSH connection established" });
|
||||
res.json({
|
||||
status: "success",
|
||||
message: "TOTP verified, SSH connection established",
|
||||
});
|
||||
});
|
||||
|
||||
session.client.on("error", (err) => {
|
||||
|
||||
@@ -194,7 +194,7 @@ class SSHConnectionPool {
|
||||
}
|
||||
|
||||
class RequestQueue {
|
||||
private queues = new Map<number, Array<() => Promise<any>>>();
|
||||
private queues = new Map<number, Array<() => Promise<unknown>>>();
|
||||
private processing = new Set<number>();
|
||||
|
||||
async queueRequest<T>(hostId: number, request: () => Promise<T>): Promise<T> {
|
||||
|
||||
@@ -903,7 +903,7 @@ async function connectSSHTunnel(
|
||||
});
|
||||
});
|
||||
|
||||
const connOptions: any = {
|
||||
const connOptions: Record<string, unknown> = {
|
||||
host: tunnelConfig.sourceIP,
|
||||
port: tunnelConfig.sourceSSHPort,
|
||||
username: tunnelConfig.sourceUsername,
|
||||
@@ -1065,7 +1065,7 @@ async function killRemoteTunnelByMarker(
|
||||
}
|
||||
|
||||
const conn = new Client();
|
||||
const connOptions: any = {
|
||||
const connOptions: Record<string, unknown> = {
|
||||
host: tunnelConfig.sourceIP,
|
||||
port: tunnelConfig.sourceSSHPort,
|
||||
username: tunnelConfig.sourceUsername,
|
||||
@@ -1461,10 +1461,10 @@ async function initializeAutoStartTunnels(): Promise<void> {
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
tunnelLogger.error(
|
||||
"Failed to initialize auto-start tunnels:",
|
||||
error.message,
|
||||
error instanceof Error ? error.message : "Unknown error",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,11 @@ class DatabaseFileEncryption {
|
||||
|
||||
const iv = crypto.randomBytes(16);
|
||||
|
||||
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any;
|
||||
const cipher = crypto.createCipheriv(
|
||||
this.ALGORITHM,
|
||||
key,
|
||||
iv,
|
||||
) as crypto.CipherGCM;
|
||||
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
@@ -78,7 +82,11 @@ class DatabaseFileEncryption {
|
||||
|
||||
const iv = crypto.randomBytes(16);
|
||||
|
||||
const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any;
|
||||
const cipher = crypto.createCipheriv(
|
||||
this.ALGORITHM,
|
||||
key,
|
||||
iv,
|
||||
) as crypto.CipherGCM;
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(sourceData),
|
||||
cipher.final(),
|
||||
@@ -163,7 +171,7 @@ class DatabaseFileEncryption {
|
||||
metadata.algorithm,
|
||||
key,
|
||||
Buffer.from(metadata.iv, "hex"),
|
||||
) as any;
|
||||
) as crypto.DecipherGCM;
|
||||
decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
|
||||
|
||||
const decryptedBuffer = Buffer.concat([
|
||||
@@ -233,7 +241,7 @@ class DatabaseFileEncryption {
|
||||
metadata.algorithm,
|
||||
key,
|
||||
Buffer.from(metadata.iv, "hex"),
|
||||
) as any;
|
||||
) as crypto.DecipherGCM;
|
||||
decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
|
||||
@@ -234,7 +234,9 @@ export class DatabaseMigration {
|
||||
memoryDb.exec("PRAGMA foreign_keys = OFF");
|
||||
|
||||
for (const table of tables) {
|
||||
const rows = originalDb.prepare(`SELECT * FROM ${table.name}`).all();
|
||||
const rows = originalDb
|
||||
.prepare(`SELECT * FROM ${table.name}`)
|
||||
.all() as Record<string, unknown>[];
|
||||
|
||||
if (rows.length > 0) {
|
||||
const columns = Object.keys(rows[0]);
|
||||
@@ -244,7 +246,7 @@ export class DatabaseMigration {
|
||||
);
|
||||
|
||||
const insertTransaction = memoryDb.transaction(
|
||||
(dataRows: any[]) => {
|
||||
(dataRows: Record<string, unknown>[]) => {
|
||||
for (const row of dataRows) {
|
||||
const values = columns.map((col) => row[col]);
|
||||
insertStmt.run(values);
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { FieldCrypto } from "./field-crypto.js";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
|
||||
interface DatabaseInstance {
|
||||
prepare: (sql: string) => {
|
||||
all: (param?: unknown) => unknown[];
|
||||
get: (param?: unknown) => unknown;
|
||||
run: (...params: unknown[]) => unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export class LazyFieldEncryption {
|
||||
private static readonly LEGACY_FIELD_NAME_MAP: Record<string, string> = {
|
||||
key_password: "keyPassword",
|
||||
@@ -182,12 +190,12 @@ export class LazyFieldEncryption {
|
||||
}
|
||||
|
||||
static migrateRecordSensitiveFields(
|
||||
record: any,
|
||||
record: Record<string, unknown>,
|
||||
sensitiveFields: string[],
|
||||
userKEK: Buffer,
|
||||
recordId: string,
|
||||
): {
|
||||
updatedRecord: any;
|
||||
updatedRecord: Record<string, unknown>;
|
||||
migratedFields: string[];
|
||||
needsUpdate: boolean;
|
||||
} {
|
||||
@@ -202,7 +210,7 @@ export class LazyFieldEncryption {
|
||||
try {
|
||||
const { encrypted, wasPlaintext, wasLegacyEncryption } =
|
||||
this.migrateFieldToEncrypted(
|
||||
fieldValue,
|
||||
fieldValue as string,
|
||||
userKEK,
|
||||
recordId,
|
||||
fieldName,
|
||||
@@ -279,7 +287,7 @@ export class LazyFieldEncryption {
|
||||
static async checkUserNeedsMigration(
|
||||
userId: string,
|
||||
userKEK: Buffer,
|
||||
db: any,
|
||||
db: DatabaseInstance,
|
||||
): Promise<{
|
||||
needsMigration: boolean;
|
||||
plaintextFields: Array<{
|
||||
@@ -298,7 +306,9 @@ export class LazyFieldEncryption {
|
||||
try {
|
||||
const sshHosts = db
|
||||
.prepare("SELECT * FROM ssh_data WHERE user_id = ?")
|
||||
.all(userId);
|
||||
.all(userId) as Array<
|
||||
Record<string, unknown> & { id: string | number }
|
||||
>;
|
||||
for (const host of sshHosts) {
|
||||
const sensitiveFields = this.getSensitiveFieldsForTable("ssh_data");
|
||||
const hostPlaintextFields: string[] = [];
|
||||
@@ -307,7 +317,7 @@ export class LazyFieldEncryption {
|
||||
if (
|
||||
host[field] &&
|
||||
this.fieldNeedsMigration(
|
||||
host[field],
|
||||
host[field] as string,
|
||||
userKEK,
|
||||
host.id.toString(),
|
||||
field,
|
||||
@@ -329,7 +339,9 @@ export class LazyFieldEncryption {
|
||||
|
||||
const sshCredentials = db
|
||||
.prepare("SELECT * FROM ssh_credentials WHERE user_id = ?")
|
||||
.all(userId);
|
||||
.all(userId) as Array<
|
||||
Record<string, unknown> & { id: string | number }
|
||||
>;
|
||||
for (const credential of sshCredentials) {
|
||||
const sensitiveFields =
|
||||
this.getSensitiveFieldsForTable("ssh_credentials");
|
||||
@@ -339,7 +351,7 @@ export class LazyFieldEncryption {
|
||||
if (
|
||||
credential[field] &&
|
||||
this.fieldNeedsMigration(
|
||||
credential[field],
|
||||
credential[field] as string,
|
||||
userKEK,
|
||||
credential.id.toString(),
|
||||
field,
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface LogContext {
|
||||
sessionId?: string;
|
||||
requestId?: string;
|
||||
duration?: number;
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const SENSITIVE_FIELDS = [
|
||||
|
||||
@@ -89,7 +89,7 @@ class UserDataImport {
|
||||
) {
|
||||
const importStats = await this.importSshHosts(
|
||||
targetUserId,
|
||||
exportData.userData.sshHosts,
|
||||
exportData.userData.sshHosts as Record<string, unknown>[],
|
||||
{ replaceExisting, dryRun, userDataKey },
|
||||
);
|
||||
result.summary.sshHostsImported = importStats.imported;
|
||||
@@ -104,7 +104,7 @@ class UserDataImport {
|
||||
) {
|
||||
const importStats = await this.importSshCredentials(
|
||||
targetUserId,
|
||||
exportData.userData.sshCredentials,
|
||||
exportData.userData.sshCredentials as Record<string, unknown>[],
|
||||
{ replaceExisting, dryRun, userDataKey },
|
||||
);
|
||||
result.summary.sshCredentialsImported = importStats.imported;
|
||||
@@ -129,7 +129,7 @@ class UserDataImport {
|
||||
) {
|
||||
const importStats = await this.importDismissedAlerts(
|
||||
targetUserId,
|
||||
exportData.userData.dismissedAlerts,
|
||||
exportData.userData.dismissedAlerts as Record<string, unknown>[],
|
||||
{ replaceExisting, dryRun },
|
||||
);
|
||||
result.summary.dismissedAlertsImported = importStats.imported;
|
||||
@@ -159,7 +159,7 @@ class UserDataImport {
|
||||
|
||||
private static async importSshHosts(
|
||||
targetUserId: string,
|
||||
sshHosts: any[],
|
||||
sshHosts: Record<string, unknown>[],
|
||||
options: {
|
||||
replaceExisting: boolean;
|
||||
dryRun: boolean;
|
||||
@@ -198,7 +198,9 @@ class UserDataImport {
|
||||
|
||||
delete processedHostData.id;
|
||||
|
||||
await getDb().insert(sshData).values(processedHostData);
|
||||
await getDb()
|
||||
.insert(sshData)
|
||||
.values(processedHostData as unknown as typeof sshData.$inferInsert);
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
@@ -213,7 +215,7 @@ class UserDataImport {
|
||||
|
||||
private static async importSshCredentials(
|
||||
targetUserId: string,
|
||||
credentials: any[],
|
||||
credentials: Record<string, unknown>[],
|
||||
options: {
|
||||
replaceExisting: boolean;
|
||||
dryRun: boolean;
|
||||
@@ -254,7 +256,11 @@ class UserDataImport {
|
||||
|
||||
delete processedCredentialData.id;
|
||||
|
||||
await getDb().insert(sshCredentials).values(processedCredentialData);
|
||||
await getDb()
|
||||
.insert(sshCredentials)
|
||||
.values(
|
||||
processedCredentialData as unknown as typeof sshCredentials.$inferInsert,
|
||||
);
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
@@ -269,7 +275,7 @@ class UserDataImport {
|
||||
|
||||
private static async importFileManagerData(
|
||||
targetUserId: string,
|
||||
fileManagerData: any,
|
||||
fileManagerData: Record<string, unknown>,
|
||||
options: { replaceExisting: boolean; dryRun: boolean },
|
||||
) {
|
||||
let imported = 0;
|
||||
@@ -356,7 +362,7 @@ class UserDataImport {
|
||||
|
||||
private static async importDismissedAlerts(
|
||||
targetUserId: string,
|
||||
alerts: any[],
|
||||
alerts: Record<string, unknown>[],
|
||||
options: { replaceExisting: boolean; dryRun: boolean },
|
||||
) {
|
||||
let imported = 0;
|
||||
@@ -376,7 +382,7 @@ class UserDataImport {
|
||||
.where(
|
||||
and(
|
||||
eq(dismissedAlerts.userId, targetUserId),
|
||||
eq(dismissedAlerts.alertId, alert.alertId),
|
||||
eq(dismissedAlerts.alertId, alert.alertId as string),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -395,10 +401,12 @@ class UserDataImport {
|
||||
if (existing.length > 0 && options.replaceExisting) {
|
||||
await getDb()
|
||||
.update(dismissedAlerts)
|
||||
.set(newAlert)
|
||||
.set(newAlert as typeof dismissedAlerts.$inferInsert)
|
||||
.where(eq(dismissedAlerts.id, existing[0].id));
|
||||
} else {
|
||||
await getDb().insert(dismissedAlerts).values(newAlert);
|
||||
await getDb()
|
||||
.insert(dismissedAlerts)
|
||||
.values(newAlert as typeof dismissedAlerts.$inferInsert);
|
||||
}
|
||||
|
||||
imported++;
|
||||
|
||||
@@ -5,8 +5,7 @@ import { Eye, EyeOff } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PasswordInputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
type PasswordInputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export const PasswordInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
|
||||
@@ -8,7 +8,10 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
|
||||
const originalToast = toast;
|
||||
|
||||
const rateLimitedToast = (message: string, options?: any) => {
|
||||
const rateLimitedToast = (
|
||||
message: string,
|
||||
options?: Record<string, unknown>,
|
||||
) => {
|
||||
const now = Date.now();
|
||||
const lastToast = lastToastRef.current;
|
||||
|
||||
@@ -25,13 +28,13 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
};
|
||||
|
||||
Object.assign(toast, {
|
||||
success: (message: string, options?: any) =>
|
||||
success: (message: string, options?: Record<string, unknown>) =>
|
||||
rateLimitedToast(message, { ...options, type: "success" }),
|
||||
error: (message: string, options?: any) =>
|
||||
error: (message: string, options?: Record<string, unknown>) =>
|
||||
rateLimitedToast(message, { ...options, type: "error" }),
|
||||
warning: (message: string, options?: any) =>
|
||||
warning: (message: string, options?: Record<string, unknown>) =>
|
||||
rateLimitedToast(message, { ...options, type: "warning" }),
|
||||
info: (message: string, options?: any) =>
|
||||
info: (message: string, options?: Record<string, unknown>) =>
|
||||
rateLimitedToast(message, { ...options, type: "info" }),
|
||||
message: rateLimitedToast,
|
||||
});
|
||||
|
||||
@@ -2,8 +2,7 @@ import * as React from "react";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
|
||||
@@ -14,7 +14,10 @@ export function VersionCheckModal({
|
||||
isAuthenticated = false,
|
||||
}: VersionCheckModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [versionInfo, setVersionInfo] = useState<any>(null);
|
||||
const [versionInfo, setVersionInfo] = useState<Record<
|
||||
string,
|
||||
unknown
|
||||
> | null>(null);
|
||||
const [versionChecking, setVersionChecking] = useState(false);
|
||||
const [versionDismissed] = useState(false);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface LogContext {
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
class FrontendLogger {
|
||||
|
||||
@@ -155,7 +155,9 @@ export function CredentialEditor({
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema) as any,
|
||||
resolver: zodResolver(formSchema) as unknown as Parameters<
|
||||
typeof useForm<FormData>
|
||||
>[0]["resolver"],
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
@@ -198,7 +200,7 @@ export function CredentialEditor({
|
||||
formData.publicKey = fullCredentialDetails.publicKey || "";
|
||||
formData.keyPassword = fullCredentialDetails.keyPassword || "";
|
||||
formData.keyType =
|
||||
(fullCredentialDetails.keyType as any) || ("auto" as const);
|
||||
(fullCredentialDetails.keyType as string) || ("auto" as const);
|
||||
}
|
||||
|
||||
form.reset(formData);
|
||||
|
||||
@@ -71,7 +71,15 @@ export function CredentialsManager({
|
||||
const [showDeployDialog, setShowDeployDialog] = useState(false);
|
||||
const [deployingCredential, setDeployingCredential] =
|
||||
useState<Credential | null>(null);
|
||||
const [availableHosts, setAvailableHosts] = useState<any[]>([]);
|
||||
const [availableHosts, setAvailableHosts] = useState<
|
||||
Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
username: string;
|
||||
}>
|
||||
>([]);
|
||||
const [selectedHostId, setSelectedHostId] = useState<string>("");
|
||||
const [deployLoading, setDeployLoading] = useState(false);
|
||||
const [hostSearchQuery, setHostSearchQuery] = useState("");
|
||||
@@ -207,10 +215,13 @@ export function CredentialsManager({
|
||||
);
|
||||
await fetchCredentials();
|
||||
window.dispatchEvent(new CustomEvent("credentials:changed"));
|
||||
} catch (err: any) {
|
||||
if (err.response?.data?.details) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
response?: { data?: { error?: string; details?: string } };
|
||||
};
|
||||
if (error.response?.data?.details) {
|
||||
toast.error(
|
||||
`${err.response.data.error}\n${err.response.data.details}`,
|
||||
`${error.response.data.error}\n${error.response.data.details}`,
|
||||
);
|
||||
} else {
|
||||
toast.error(t("credentials.failedToDeleteCredential"));
|
||||
|
||||
@@ -96,15 +96,19 @@ export function DiffViewer({
|
||||
|
||||
setContent1(response1.content || "");
|
||||
setContent2(response2.content || "");
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to load files for diff:", error);
|
||||
|
||||
const errorData = error?.response?.data;
|
||||
const err = error as {
|
||||
message?: string;
|
||||
response?: { data?: { tooLarge?: boolean; error?: string } };
|
||||
};
|
||||
const errorData = err?.response?.data;
|
||||
if (errorData?.tooLarge) {
|
||||
setError(t("fileManager.fileTooLarge", { error: errorData.error }));
|
||||
} else if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
err.message?.includes("connection") ||
|
||||
err.message?.includes("established")
|
||||
) {
|
||||
setError(
|
||||
t("fileManager.sshConnectionFailed", {
|
||||
@@ -117,9 +121,7 @@ export function DiffViewer({
|
||||
setError(
|
||||
t("fileManager.loadFileFailed", {
|
||||
error:
|
||||
error.message ||
|
||||
errorData?.error ||
|
||||
t("fileManager.unknownError"),
|
||||
err.message || errorData?.error || t("fileManager.unknownError"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -157,12 +159,13 @@ export function DiffViewer({
|
||||
t("fileManager.downloadFileSuccess", { name: file.name }),
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to download file:", error);
|
||||
const err = error as { message?: string };
|
||||
toast.error(
|
||||
t("fileManager.downloadFileFailed") +
|
||||
": " +
|
||||
(error.message || t("fileManager.unknownError")),
|
||||
(err.message || t("fileManager.unknownError")),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -289,7 +289,7 @@ function getLanguageExtension(filename: string) {
|
||||
return language ? loadLanguage(language) : null;
|
||||
}
|
||||
|
||||
function formatFileSize(bytes?: number, t?: any): string {
|
||||
function formatFileSize(bytes?: number, t?: (key: string) => string): string {
|
||||
if (!bytes) return t ? t("fileManager.unknownSize") : "Unknown size";
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
@@ -323,7 +323,9 @@ export function FileViewer({
|
||||
const [pdfScale, setPdfScale] = useState(1.2);
|
||||
const [pdfError, setPdfError] = useState(false);
|
||||
const [markdownEditMode, setMarkdownEditMode] = useState(false);
|
||||
const editorRef = useRef<any>(null);
|
||||
const editorRef = useRef<{
|
||||
view?: { dispatch: (transaction: unknown) => void };
|
||||
} | null>(null);
|
||||
|
||||
const fileTypeInfo = getFileType(file.name);
|
||||
|
||||
|
||||
@@ -157,28 +157,40 @@ export function FileWindow({
|
||||
|
||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||
setIsEditable(!mediaExtensions.includes(extension || ""));
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to load file:", error);
|
||||
|
||||
const errorData = error?.response?.data;
|
||||
const err = error as {
|
||||
message?: string;
|
||||
isFileNotFound?: boolean;
|
||||
response?: {
|
||||
status?: number;
|
||||
data?: {
|
||||
tooLarge?: boolean;
|
||||
error?: string;
|
||||
fileNotFound?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
const errorData = err?.response?.data;
|
||||
if (errorData?.tooLarge) {
|
||||
toast.error(`File too large: ${errorData.error}`, {
|
||||
duration: 10000,
|
||||
});
|
||||
} else if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
err.message?.includes("connection") ||
|
||||
err.message?.includes("established")
|
||||
) {
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
||||
);
|
||||
} else {
|
||||
const errorMessage =
|
||||
errorData?.error || error.message || "Unknown error";
|
||||
errorData?.error || err.message || "Unknown error";
|
||||
const isFileNotFound =
|
||||
(error as any).isFileNotFound ||
|
||||
err.isFileNotFound ||
|
||||
errorData?.fileNotFound ||
|
||||
error.response?.status === 404 ||
|
||||
err.response?.status === 404 ||
|
||||
errorMessage.includes("File not found") ||
|
||||
errorMessage.includes("No such file or directory") ||
|
||||
errorMessage.includes("cannot access") ||
|
||||
@@ -229,10 +241,11 @@ export function FileWindow({
|
||||
const contentSize = new Blob([fileContent]).size;
|
||||
file.size = contentSize;
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to load file content:", error);
|
||||
const err = error as { message?: string };
|
||||
toast.error(
|
||||
`${t("fileManager.failedToLoadFile")}: ${error.message || t("fileManager.unknownError")}`,
|
||||
`${t("fileManager.failedToLoadFile")}: ${err.message || t("fileManager.unknownError")}`,
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -258,19 +271,20 @@ export function FileWindow({
|
||||
}
|
||||
|
||||
toast.success(t("fileManager.fileSavedSuccessfully"));
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to save file:", error);
|
||||
|
||||
const err = error as { message?: string };
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
err.message?.includes("connection") ||
|
||||
err.message?.includes("established")
|
||||
) {
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
||||
);
|
||||
} else {
|
||||
toast.error(
|
||||
`${t("fileManager.failedToSaveFile")}: ${error.message || t("fileManager.unknownError")}`,
|
||||
`${t("fileManager.failedToSaveFile")}: ${err.message || t("fileManager.unknownError")}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -335,19 +349,20 @@ export function FileWindow({
|
||||
|
||||
toast.success(t("fileManager.fileDownloadedSuccessfully"));
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to download file:", error);
|
||||
|
||||
const err = error as { message?: string };
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
err.message?.includes("connection") ||
|
||||
err.message?.includes("established")
|
||||
) {
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
||||
);
|
||||
} else {
|
||||
toast.error(
|
||||
`Failed to download file: ${error.message || "Unknown error"}`,
|
||||
`Failed to download file: ${err.message || "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,17 @@ export function TerminalWindow({
|
||||
const { t } = useTranslation();
|
||||
const { closeWindow, maximizeWindow, focusWindow, windows } =
|
||||
useWindowManager();
|
||||
const terminalRef = React.useRef<any>(null);
|
||||
const terminalRef = React.useRef<{ fit?: () => void } | null>(null);
|
||||
const resizeTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (resizeTimeoutRef.current) {
|
||||
clearTimeout(resizeTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const currentWindow = windows.find((w) => w.id === windowId);
|
||||
if (!currentWindow) {
|
||||
return null;
|
||||
@@ -70,14 +78,6 @@ export function TerminalWindow({
|
||||
}, 100);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (resizeTimeoutRef.current) {
|
||||
clearTimeout(resizeTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const terminalTitle = executeCommand
|
||||
? t("terminal.runTitle", { host: hostConfig.name, command: executeCommand })
|
||||
: initialPath
|
||||
|
||||
@@ -21,7 +21,11 @@ export function HostManager({
|
||||
const [activeTab, setActiveTab] = useState("host_viewer");
|
||||
const [editingHost, setEditingHost] = useState<SSHHost | null>(null);
|
||||
|
||||
const [editingCredential, setEditingCredential] = useState<any | null>(null);
|
||||
const [editingCredential, setEditingCredential] = useState<{
|
||||
id: number;
|
||||
name?: string;
|
||||
username: string;
|
||||
} | null>(null);
|
||||
const { state: sidebarState } = useSidebar();
|
||||
|
||||
const handleEditHost = (host: SSHHost) => {
|
||||
@@ -34,7 +38,11 @@ export function HostManager({
|
||||
setActiveTab("host_viewer");
|
||||
};
|
||||
|
||||
const handleEditCredential = (credential: any) => {
|
||||
const handleEditCredential = (credential: {
|
||||
id: number;
|
||||
name?: string;
|
||||
username: string;
|
||||
}) => {
|
||||
setEditingCredential(credential);
|
||||
setActiveTab("add_credential");
|
||||
};
|
||||
|
||||
@@ -60,7 +60,14 @@ interface SSHHost {
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: any[];
|
||||
tunnelConnections: Array<{
|
||||
sourcePort: number;
|
||||
endpointPort: number;
|
||||
endpointHost: string;
|
||||
maxRetries: number;
|
||||
retryInterval: number;
|
||||
autoStart: boolean;
|
||||
}>;
|
||||
statsConfig?: StatsConfig;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -79,7 +86,9 @@ export function HostManagerEditor({
|
||||
const { t } = useTranslation();
|
||||
const [folders, setFolders] = useState<string[]>([]);
|
||||
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
|
||||
const [credentials, setCredentials] = useState<any[]>([]);
|
||||
const [credentials, setCredentials] = useState<
|
||||
Array<{ id: number; username: string; authType: string }>
|
||||
>([]);
|
||||
|
||||
const [authTab, setAuthTab] = useState<"password" | "key" | "credential">(
|
||||
"password",
|
||||
@@ -292,7 +301,7 @@ export function HostManagerEditor({
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema) as any,
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
ip: "",
|
||||
@@ -377,7 +386,17 @@ export function HostManagerEditor({
|
||||
} else if (defaultAuthType === "key") {
|
||||
formData.key = editingHost.id ? "existing_key" : editingHost.key;
|
||||
formData.keyPassword = cleanedHost.keyPassword || "";
|
||||
formData.keyType = (cleanedHost.keyType as any) || "auto";
|
||||
formData.keyType =
|
||||
(cleanedHost.keyType as
|
||||
| "auto"
|
||||
| "ssh-rsa"
|
||||
| "ssh-ed25519"
|
||||
| "ecdsa-sha2-nistp256"
|
||||
| "ecdsa-sha2-nistp384"
|
||||
| "ecdsa-sha2-nistp521"
|
||||
| "ssh-dss"
|
||||
| "ssh-rsa-sha2-256"
|
||||
| "ssh-rsa-sha2-512") || "auto";
|
||||
} else if (defaultAuthType === "credential") {
|
||||
formData.credentialId =
|
||||
cleanedHost.credentialId || "existing_credential";
|
||||
@@ -430,7 +449,7 @@ export function HostManagerEditor({
|
||||
data.name = `${data.username}@${data.ip}`;
|
||||
}
|
||||
|
||||
const submitData: any = {
|
||||
const submitData: Record<string, unknown> = {
|
||||
name: data.name,
|
||||
ip: data.ip,
|
||||
port: data.port,
|
||||
|
||||
@@ -11,7 +11,16 @@ interface NetworkWidgetProps {
|
||||
export function NetworkWidget({ metrics }: NetworkWidgetProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const network = (metrics as any)?.network;
|
||||
const metricsWithNetwork = metrics as ServerMetrics & {
|
||||
network?: {
|
||||
interfaces?: Array<{
|
||||
name: string;
|
||||
state: string;
|
||||
ip: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
const network = metricsWithNetwork?.network;
|
||||
const interfaces = network?.interfaces || [];
|
||||
|
||||
return (
|
||||
@@ -30,7 +39,7 @@ export function NetworkWidget({ metrics }: NetworkWidgetProps) {
|
||||
<p className="text-sm">{t("serverStats.noInterfacesFound")}</p>
|
||||
</div>
|
||||
) : (
|
||||
interfaces.map((iface: any, index: number) => (
|
||||
interfaces.map((iface, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-3 rounded-lg bg-dark-bg/50 border border-dark-border/30 hover:bg-dark-bg/60 transition-colors"
|
||||
|
||||
@@ -11,7 +11,20 @@ interface ProcessesWidgetProps {
|
||||
export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const processes = (metrics as any)?.processes;
|
||||
const metricsWithProcesses = metrics as ServerMetrics & {
|
||||
processes?: {
|
||||
total?: number;
|
||||
running?: number;
|
||||
top?: Array<{
|
||||
pid: number;
|
||||
cpu: number;
|
||||
mem: number;
|
||||
command: string;
|
||||
user: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
const processes = metricsWithProcesses?.processes;
|
||||
const topProcesses = processes?.top || [];
|
||||
|
||||
return (
|
||||
@@ -46,7 +59,7 @@ export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{topProcesses.map((proc: any, index: number) => (
|
||||
{topProcesses.map((proc, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-2.5 rounded-lg bg-dark-bg/30 hover:bg-dark-bg/50 transition-colors border border-dark-border/20"
|
||||
|
||||
@@ -11,7 +11,14 @@ interface SystemWidgetProps {
|
||||
export function SystemWidget({ metrics }: SystemWidgetProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const system = (metrics as any)?.system;
|
||||
const metricsWithSystem = metrics as ServerMetrics & {
|
||||
system?: {
|
||||
hostname?: string;
|
||||
os?: string;
|
||||
kernel?: string;
|
||||
};
|
||||
};
|
||||
const system = metricsWithSystem?.system;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||
|
||||
@@ -11,7 +11,13 @@ interface UptimeWidgetProps {
|
||||
export function UptimeWidget({ metrics }: UptimeWidgetProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const uptime = (metrics as any)?.uptime;
|
||||
const metricsWithUptime = metrics as ServerMetrics & {
|
||||
uptime?: {
|
||||
formatted?: string;
|
||||
seconds?: number;
|
||||
};
|
||||
};
|
||||
const uptime = metricsWithUptime?.uptime;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||
|
||||
@@ -11,7 +11,7 @@ interface SSHTunnelViewerProps {
|
||||
action: "connect" | "disconnect" | "cancel",
|
||||
host: SSHHost,
|
||||
tunnelIndex: number,
|
||||
) => Promise<any>;
|
||||
) => Promise<unknown>;
|
||||
}
|
||||
|
||||
export function TunnelViewer({
|
||||
|
||||
@@ -164,7 +164,7 @@ export function HomepageAuth({
|
||||
}
|
||||
|
||||
try {
|
||||
let res, meRes;
|
||||
let res;
|
||||
if (tab === "login") {
|
||||
res = await loginUser(localUsername, password);
|
||||
} else {
|
||||
@@ -194,7 +194,7 @@ export function HomepageAuth({
|
||||
throw new Error(t("errors.loginFailed"));
|
||||
}
|
||||
|
||||
[meRes] = await Promise.all([getUserInfo()]);
|
||||
const [meRes] = await Promise.all([getUserInfo()]);
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
setLoggedIn(true);
|
||||
@@ -217,16 +217,22 @@ export function HomepageAuth({
|
||||
setTotpRequired(false);
|
||||
setTotpCode("");
|
||||
setTotpTempToken("");
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
message?: string;
|
||||
response?: { data?: { error?: string } };
|
||||
};
|
||||
const errorMessage =
|
||||
err?.response?.data?.error || err?.message || t("errors.unknownError");
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
t("errors.unknownError");
|
||||
toast.error(errorMessage);
|
||||
setInternalLoggedIn(false);
|
||||
setLoggedIn(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
if (err?.response?.data?.error?.includes("Database")) {
|
||||
if (error?.response?.data?.error?.includes("Database")) {
|
||||
setDbConnectionFailed(true);
|
||||
} else {
|
||||
setDbError(null);
|
||||
@@ -242,10 +248,14 @@ export function HomepageAuth({
|
||||
await initiatePasswordReset(localUsername);
|
||||
setResetStep("verify");
|
||||
toast.success(t("messages.resetCodeSent"));
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
message?: string;
|
||||
response?: { data?: { error?: string } };
|
||||
};
|
||||
toast.error(
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
t("errors.failedPasswordReset"),
|
||||
);
|
||||
} finally {
|
||||
@@ -260,8 +270,9 @@ export function HomepageAuth({
|
||||
setTempToken(response.tempToken);
|
||||
setResetStep("newPassword");
|
||||
toast.success(t("messages.codeVerified"));
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.error || t("errors.failedVerifyCode"));
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
toast.error(error?.response?.data?.error || t("errors.failedVerifyCode"));
|
||||
} finally {
|
||||
setResetLoading(false);
|
||||
}
|
||||
@@ -296,9 +307,10 @@ export function HomepageAuth({
|
||||
|
||||
setTab("login");
|
||||
resetPasswordState();
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
toast.error(
|
||||
err?.response?.data?.error || t("errors.failedCompleteReset"),
|
||||
error?.response?.data?.error || t("errors.failedCompleteReset"),
|
||||
);
|
||||
} finally {
|
||||
setResetLoading(false);
|
||||
@@ -359,11 +371,15 @@ export function HomepageAuth({
|
||||
setTotpCode("");
|
||||
setTotpTempToken("");
|
||||
toast.success(t("messages.loginSuccess"));
|
||||
} catch (err: any) {
|
||||
const errorCode = err?.response?.data?.code;
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
message?: string;
|
||||
response?: { data?: { code?: string; error?: string } };
|
||||
};
|
||||
const errorCode = error?.response?.data?.code;
|
||||
const errorMessage =
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
t("errors.invalidTotpCode");
|
||||
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
@@ -391,10 +407,14 @@ export function HomepageAuth({
|
||||
}
|
||||
|
||||
window.location.replace(authUrl);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
message?: string;
|
||||
response?: { data?: { error?: string } };
|
||||
};
|
||||
const errorMessage =
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
t("errors.failedOidcLogin");
|
||||
toast.error(errorMessage);
|
||||
setOidcLoading(false);
|
||||
|
||||
@@ -23,7 +23,14 @@ interface SSHHost {
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: any[];
|
||||
tunnelConnections: Array<{
|
||||
sourcePort: number;
|
||||
endpointPort: number;
|
||||
endpointHost: string;
|
||||
maxRetries: number;
|
||||
retryInterval: number;
|
||||
autoStart: boolean;
|
||||
}>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ export function Host({ host }: HostProps): React.ReactElement {
|
||||
: `${host.username}@${host.ip}:${host.port}`;
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId: number | undefined;
|
||||
let cancelled = false;
|
||||
|
||||
const fetchStatus = async () => {
|
||||
@@ -29,13 +28,14 @@ export function Host({ host }: HostProps): React.ReactElement {
|
||||
if (!cancelled) {
|
||||
setServerStatus(res?.status === "online" ? "online" : "offline");
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
if (!cancelled) {
|
||||
if (error?.response?.status === 503) {
|
||||
const err = error as { response?: { status?: number } };
|
||||
if (err?.response?.status === 503) {
|
||||
setServerStatus("offline");
|
||||
} else if (error?.response?.status === 504) {
|
||||
} else if (err?.response?.status === 504) {
|
||||
setServerStatus("degraded");
|
||||
} else if (error?.response?.status === 404) {
|
||||
} else if (err?.response?.status === 404) {
|
||||
setServerStatus("offline");
|
||||
} else {
|
||||
setServerStatus("offline");
|
||||
@@ -46,7 +46,7 @@ export function Host({ host }: HostProps): React.ReactElement {
|
||||
|
||||
fetchStatus();
|
||||
|
||||
intervalId = window.setInterval(fetchStatus, 30000);
|
||||
const intervalId = window.setInterval(fetchStatus, 30000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -19,7 +19,16 @@ interface TabContextType {
|
||||
setCurrentTab: (tabId: number) => void;
|
||||
setSplitScreenTab: (tabId: number) => void;
|
||||
getTab: (tabId: number) => Tab | undefined;
|
||||
updateHostConfig: (hostId: number, newHostConfig: any) => void;
|
||||
updateHostConfig: (
|
||||
hostId: number,
|
||||
newHostConfig: {
|
||||
id: number;
|
||||
name?: string;
|
||||
username: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
},
|
||||
) => void;
|
||||
}
|
||||
|
||||
const TabContext = createContext<TabContextType | undefined>(undefined);
|
||||
@@ -98,7 +107,9 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
id,
|
||||
title: effectiveTitle,
|
||||
terminalRef:
|
||||
tabData.type === "terminal" ? React.createRef<any>() : undefined,
|
||||
tabData.type === "terminal"
|
||||
? React.createRef<{ disconnect?: () => void }>()
|
||||
: undefined,
|
||||
};
|
||||
setTabs((prev) => [...prev, newTab]);
|
||||
setCurrentTab(id);
|
||||
@@ -140,7 +151,16 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
return tabs.find((tab) => tab.id === tabId);
|
||||
};
|
||||
|
||||
const updateHostConfig = (hostId: number, newHostConfig: any) => {
|
||||
const updateHostConfig = (
|
||||
hostId: number,
|
||||
newHostConfig: {
|
||||
id: number;
|
||||
name?: string;
|
||||
username: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
},
|
||||
) => {
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) => {
|
||||
if (tab.hostConfig && tab.hostConfig.id === hostId) {
|
||||
|
||||
@@ -49,10 +49,14 @@ export function PasswordReset({ userInfo }: PasswordResetProps) {
|
||||
await initiatePasswordReset(userInfo.username);
|
||||
setResetStep("verify");
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
message?: string;
|
||||
response?: { data?: { error?: string } };
|
||||
};
|
||||
setError(
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
t("common.failedToInitiatePasswordReset"),
|
||||
);
|
||||
} finally {
|
||||
@@ -80,9 +84,10 @@ export function PasswordReset({ userInfo }: PasswordResetProps) {
|
||||
setTempToken(response.tempToken);
|
||||
setResetStep("newPassword");
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
setError(
|
||||
err?.response?.data?.error || t("common.failedToVerifyResetCode"),
|
||||
error?.response?.data?.error || t("common.failedToVerifyResetCode"),
|
||||
);
|
||||
} finally {
|
||||
setResetLoading(false);
|
||||
@@ -110,9 +115,11 @@ export function PasswordReset({ userInfo }: PasswordResetProps) {
|
||||
|
||||
toast.success(t("common.passwordResetSuccess"));
|
||||
resetPasswordState();
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
setError(
|
||||
err?.response?.data?.error || t("common.failedToCompletePasswordReset"),
|
||||
error?.response?.data?.error ||
|
||||
t("common.failedToCompletePasswordReset"),
|
||||
);
|
||||
} finally {
|
||||
setResetLoading(false);
|
||||
|
||||
@@ -66,8 +66,9 @@ export function TOTPSetup({
|
||||
setSecret(response.secret);
|
||||
setSetupStep("qr");
|
||||
setIsSettingUp(true);
|
||||
} catch (err: any) {
|
||||
setError(err?.response?.data?.error || "Failed to start TOTP setup");
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
setError(error?.response?.data?.error || "Failed to start TOTP setup");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -86,8 +87,9 @@ export function TOTPSetup({
|
||||
setBackupCodes(response.backup_codes);
|
||||
setSetupStep("backup");
|
||||
toast.success(t("auth.twoFactorEnabledSuccess"));
|
||||
} catch (err: any) {
|
||||
setError(err?.response?.data?.error || "Invalid verification code");
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
setError(error?.response?.data?.error || "Invalid verification code");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -105,8 +107,9 @@ export function TOTPSetup({
|
||||
setDisableCode("");
|
||||
onStatusChange?.(false);
|
||||
toast.success(t("auth.twoFactorDisabled"));
|
||||
} catch (err: any) {
|
||||
setError(err?.response?.data?.error || "Failed to disable TOTP");
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
setError(error?.response?.data?.error || "Failed to disable TOTP");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -122,8 +125,11 @@ export function TOTPSetup({
|
||||
);
|
||||
setBackupCodes(response.backup_codes);
|
||||
toast.success(t("auth.newBackupCodesGenerated"));
|
||||
} catch (err: any) {
|
||||
setError(err?.response?.data?.error || "Failed to generate backup codes");
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
setError(
|
||||
error?.response?.data?.error || "Failed to generate backup codes",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -62,8 +62,9 @@ export function UserProfile({ isTopbarOpen = true }: UserProfileProps) {
|
||||
is_oidc: info.is_oidc,
|
||||
totp_enabled: info.totp_enabled || false,
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err?.response?.data?.error || t("errors.loadFailed"));
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
setError(error?.response?.data?.error || t("errors.loadFailed"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,14 @@ interface SSHHost {
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: any[];
|
||||
tunnelConnections: Array<{
|
||||
sourcePort: number;
|
||||
endpointPort: number;
|
||||
endpointHost: string;
|
||||
maxRetries: number;
|
||||
retryInterval: number;
|
||||
autoStart: boolean;
|
||||
}>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
: `${host.username}@${host.ip}:${host.port}`;
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId: number | undefined;
|
||||
let cancelled = false;
|
||||
|
||||
const fetchStatus = async () => {
|
||||
@@ -29,13 +28,14 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
if (!cancelled) {
|
||||
setServerStatus(res?.status === "online" ? "online" : "offline");
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
if (!cancelled) {
|
||||
if (error?.response?.status === 503) {
|
||||
const err = error as { response?: { status?: number } };
|
||||
if (err?.response?.status === 503) {
|
||||
setServerStatus("offline");
|
||||
} else if (error?.response?.status === 504) {
|
||||
} else if (err?.response?.status === 504) {
|
||||
setServerStatus("degraded");
|
||||
} else if (error?.response?.status === 404) {
|
||||
} else if (err?.response?.status === 404) {
|
||||
setServerStatus("offline");
|
||||
} else {
|
||||
setServerStatus("offline");
|
||||
@@ -46,7 +46,7 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
|
||||
fetchStatus();
|
||||
|
||||
intervalId = window.setInterval(fetchStatus, 30000);
|
||||
const intervalId = window.setInterval(fetchStatus, 30000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -42,7 +42,14 @@ interface SSHHost {
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: any[];
|
||||
tunnelConnections: Array<{
|
||||
sourcePort: number;
|
||||
endpointPort: number;
|
||||
endpointHost: string;
|
||||
maxRetries: number;
|
||||
retryInterval: number;
|
||||
autoStart: boolean;
|
||||
}>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
...tabData,
|
||||
id,
|
||||
title: computeUniqueTitle(tabData.title),
|
||||
terminalRef: React.createRef<any>(),
|
||||
terminalRef: React.createRef<{ disconnect?: () => void }>(),
|
||||
};
|
||||
setTabs((prev) => [...prev, newTab]);
|
||||
setCurrentTab(id);
|
||||
|
||||
@@ -162,7 +162,7 @@ export function HomepageAuth({
|
||||
}
|
||||
|
||||
try {
|
||||
let res, meRes;
|
||||
let res;
|
||||
if (tab === "login") {
|
||||
res = await loginUser(localUsername, password);
|
||||
} else {
|
||||
@@ -192,7 +192,7 @@ export function HomepageAuth({
|
||||
throw new Error(t("errors.loginFailed"));
|
||||
}
|
||||
|
||||
[meRes] = await Promise.all([getUserInfo()]);
|
||||
const [meRes] = await Promise.all([getUserInfo()]);
|
||||
|
||||
setInternalLoggedIn(true);
|
||||
setLoggedIn(true);
|
||||
@@ -215,16 +215,22 @@ export function HomepageAuth({
|
||||
setTotpRequired(false);
|
||||
setTotpCode("");
|
||||
setTotpTempToken("");
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
message?: string;
|
||||
response?: { data?: { error?: string } };
|
||||
};
|
||||
const errorMessage =
|
||||
err?.response?.data?.error || err?.message || t("errors.unknownError");
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
t("errors.unknownError");
|
||||
toast.error(errorMessage);
|
||||
setInternalLoggedIn(false);
|
||||
setLoggedIn(false);
|
||||
setIsAdmin(false);
|
||||
setUsername(null);
|
||||
setUserId(null);
|
||||
if (err?.response?.data?.error?.includes("Database")) {
|
||||
if (error?.response?.data?.error?.includes("Database")) {
|
||||
setDbError(t("errors.databaseConnection"));
|
||||
} else {
|
||||
setDbError(null);
|
||||
@@ -241,10 +247,14 @@ export function HomepageAuth({
|
||||
await initiatePasswordReset(localUsername);
|
||||
setResetStep("verify");
|
||||
toast.success(t("messages.resetCodeSent"));
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
message?: string;
|
||||
response?: { data?: { error?: string } };
|
||||
};
|
||||
toast.error(
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
t("errors.failedPasswordReset"),
|
||||
);
|
||||
} finally {
|
||||
@@ -260,8 +270,9 @@ export function HomepageAuth({
|
||||
setTempToken(response.tempToken);
|
||||
setResetStep("newPassword");
|
||||
toast.success(t("messages.codeVerified"));
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.error || t("errors.failedVerifyCode"));
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
toast.error(error?.response?.data?.error || t("errors.failedVerifyCode"));
|
||||
} finally {
|
||||
setResetLoading(false);
|
||||
}
|
||||
@@ -298,9 +309,10 @@ export function HomepageAuth({
|
||||
|
||||
setTab("login");
|
||||
resetPasswordState();
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { error?: string } } };
|
||||
toast.error(
|
||||
err?.response?.data?.error || t("errors.failedCompleteReset"),
|
||||
error?.response?.data?.error || t("errors.failedCompleteReset"),
|
||||
);
|
||||
} finally {
|
||||
setResetLoading(false);
|
||||
@@ -364,11 +376,15 @@ export function HomepageAuth({
|
||||
setTotpCode("");
|
||||
setTotpTempToken("");
|
||||
toast.success(t("messages.loginSuccess"));
|
||||
} catch (err: any) {
|
||||
const errorCode = err?.response?.data?.code;
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
message?: string;
|
||||
response?: { data?: { code?: string; error?: string } };
|
||||
};
|
||||
const errorCode = error?.response?.data?.code;
|
||||
const errorMessage =
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
t("errors.invalidTotpCode");
|
||||
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
@@ -397,10 +413,14 @@ export function HomepageAuth({
|
||||
}
|
||||
|
||||
window.location.replace(authUrl);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
const error = err as {
|
||||
message?: string;
|
||||
response?: { data?: { error?: string } };
|
||||
};
|
||||
const errorMessage =
|
||||
err?.response?.data?.error ||
|
||||
err?.message ||
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
t("errors.failedOidcLogin");
|
||||
toast.error(errorMessage);
|
||||
setOidcLoading(false);
|
||||
|
||||
@@ -23,7 +23,14 @@ interface SSHHost {
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: any[];
|
||||
tunnelConnections: Array<{
|
||||
sourcePort: number;
|
||||
endpointPort: number;
|
||||
endpointHost: string;
|
||||
maxRetries: number;
|
||||
retryInterval: number;
|
||||
autoStart: boolean;
|
||||
}>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
: `${host.username}@${host.ip}:${host.port}`;
|
||||
|
||||
useEffect(() => {
|
||||
let intervalId: number | undefined;
|
||||
let cancelled = false;
|
||||
|
||||
const fetchStatus = async () => {
|
||||
@@ -29,13 +28,14 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
if (!cancelled) {
|
||||
setServerStatus(res?.status === "online" ? "online" : "offline");
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
if (!cancelled) {
|
||||
if (error?.response?.status === 503) {
|
||||
const err = error as { response?: { status?: number } };
|
||||
if (err?.response?.status === 503) {
|
||||
setServerStatus("offline");
|
||||
} else if (error?.response?.status === 504) {
|
||||
} else if (err?.response?.status === 504) {
|
||||
setServerStatus("degraded");
|
||||
} else if (error?.response?.status === 404) {
|
||||
} else if (err?.response?.status === 404) {
|
||||
setServerStatus("offline");
|
||||
} else {
|
||||
setServerStatus("offline");
|
||||
@@ -46,7 +46,7 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement {
|
||||
|
||||
fetchStatus();
|
||||
|
||||
intervalId = window.setInterval(fetchStatus, 30000);
|
||||
const intervalId = window.setInterval(fetchStatus, 30000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
|
||||
@@ -43,7 +43,14 @@ interface SSHHost {
|
||||
enableTunnel: boolean;
|
||||
enableFileManager: boolean;
|
||||
defaultPath: string;
|
||||
tunnelConnections: any[];
|
||||
tunnelConnections: Array<{
|
||||
sourcePort: number;
|
||||
endpointPort: number;
|
||||
endpointHost: string;
|
||||
maxRetries: number;
|
||||
retryInterval: number;
|
||||
autoStart: boolean;
|
||||
}>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
...tabData,
|
||||
id,
|
||||
title: computeUniqueTitle(tabData.title),
|
||||
terminalRef: React.createRef<any>(),
|
||||
terminalRef: React.createRef<{ disconnect?: () => void }>(),
|
||||
};
|
||||
setTabs((prev) => [...prev, newTab]);
|
||||
setCurrentTab(id);
|
||||
|
||||
@@ -114,9 +114,10 @@ export function useDragToDesktop({ sshSessionId }: UseDragToDesktopProps) {
|
||||
}, 10000);
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to drag to desktop:", error);
|
||||
const errorMessage = error.message || "Drag failed";
|
||||
const err = error as { message?: string };
|
||||
const errorMessage = err.message || "Drag failed";
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
@@ -223,9 +224,10 @@ export function useDragToDesktop({ sshSessionId }: UseDragToDesktopProps) {
|
||||
}));
|
||||
}, 15000);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to batch drag to desktop:", error);
|
||||
const errorMessage = error.message || "Batch drag failed";
|
||||
const err = error as { message?: string };
|
||||
const errorMessage = err.message || "Batch drag failed";
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
|
||||
@@ -34,7 +34,9 @@ export function useDragToSystemDesktop({ sshSessionId }: UseDragToSystemProps) {
|
||||
options: DragToSystemOptions;
|
||||
} | null>(null);
|
||||
|
||||
const saveLastDirectory = async (fileHandle: any) => {
|
||||
const saveLastDirectory = async (fileHandle: {
|
||||
getParent?: () => Promise<unknown>;
|
||||
}) => {
|
||||
try {
|
||||
if ("indexedDB" in window && fileHandle.getParent) {
|
||||
const dirHandle = await fileHandle.getParent();
|
||||
@@ -133,10 +135,33 @@ export function useDragToSystemDesktop({ sshSessionId }: UseDragToSystemProps) {
|
||||
const fileName =
|
||||
fileList.length === 1 ? fileList[0].name : `files_${Date.now()}.zip`;
|
||||
|
||||
let fileHandle: any = null;
|
||||
let fileHandle: {
|
||||
createWritable?: () => Promise<{
|
||||
write: (data: Blob) => Promise<void>;
|
||||
close: () => Promise<void>;
|
||||
}>;
|
||||
getParent?: () => Promise<unknown>;
|
||||
} | null = null;
|
||||
if (isFileSystemAPISupported()) {
|
||||
try {
|
||||
fileHandle = await (window as any).showSaveFilePicker({
|
||||
fileHandle = await (
|
||||
window as Window & {
|
||||
showSaveFilePicker?: (options: {
|
||||
suggestedName: string;
|
||||
startIn: string;
|
||||
types: Array<{
|
||||
description: string;
|
||||
accept: Record<string, string[]>;
|
||||
}>;
|
||||
}) => Promise<{
|
||||
createWritable?: () => Promise<{
|
||||
write: (data: Blob) => Promise<void>;
|
||||
close: () => Promise<void>;
|
||||
}>;
|
||||
getParent?: () => Promise<unknown>;
|
||||
}>;
|
||||
}
|
||||
).showSaveFilePicker!({
|
||||
suggestedName: fileName,
|
||||
startIn: "desktop",
|
||||
types: [
|
||||
@@ -156,8 +181,9 @@ export function useDragToSystemDesktop({ sshSessionId }: UseDragToSystemProps) {
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError") {
|
||||
} catch (error: unknown) {
|
||||
const err = error as { name?: string };
|
||||
if (err.name === "AbortError") {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isDownloading: false,
|
||||
@@ -211,8 +237,9 @@ export function useDragToSystemDesktop({ sshSessionId }: UseDragToSystemProps) {
|
||||
}, 1000);
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.message || "Save failed";
|
||||
} catch (error: unknown) {
|
||||
const err = error as { message?: string };
|
||||
const errorMessage = err.message || "Save failed";
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
|
||||
Reference in New Issue
Block a user