fix: replace explicit any types with proper TypeScript types
- Create explicit interfaces for Request extensions (AuthenticatedRequest, RequestWithHeaders) - Add type definitions for WebSocket messages and SSH connection data - Use generic types in DataCrypto methods instead of any return types - Define proper interfaces for file manager data structures - Replace catch block any types with unknown and proper type assertions - Add HostConfig and TabData interfaces for Server component Fixes 32 @typescript-eslint/no-explicit-any violations across 5 files
This commit is contained in:
@@ -1,14 +1,55 @@
|
||||
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
||||
import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2";
|
||||
import {
|
||||
Client,
|
||||
type ClientChannel,
|
||||
type PseudoTtyOptions,
|
||||
type ConnectConfig,
|
||||
} from "ssh2";
|
||||
import { parse as parseUrl } from "url";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { sshCredentials } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { sshLogger } from "../utils/logger.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { AuthManager, type JWTPayload } from "../utils/auth-manager.js";
|
||||
import { UserCrypto } from "../utils/user-crypto.js";
|
||||
|
||||
interface ConnectToHostData {
|
||||
cols: number;
|
||||
rows: number;
|
||||
hostConfig: {
|
||||
id: number;
|
||||
ip: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password?: string;
|
||||
key?: string;
|
||||
keyPassword?: string;
|
||||
keyType?: string;
|
||||
authType?: string;
|
||||
credentialId?: number;
|
||||
userId?: string;
|
||||
};
|
||||
initialPath?: string;
|
||||
executeCommand?: string;
|
||||
}
|
||||
|
||||
interface ResizeData {
|
||||
cols: number;
|
||||
rows: number;
|
||||
}
|
||||
|
||||
interface TOTPResponseData {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
interface WebSocketMessage {
|
||||
type: string;
|
||||
data?: ConnectToHostData | ResizeData | TOTPResponseData | string | unknown;
|
||||
code?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
const userCrypto = UserCrypto.getInstance();
|
||||
|
||||
@@ -79,7 +120,7 @@ const wss = new WebSocketServer({
|
||||
|
||||
wss.on("connection", async (ws: WebSocket, req) => {
|
||||
let userId: string | undefined;
|
||||
let userPayload: any;
|
||||
let userPayload: JWTPayload | undefined;
|
||||
|
||||
try {
|
||||
const url = parseUrl(req.url!, true);
|
||||
@@ -187,9 +228,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: any;
|
||||
let parsed: WebSocketMessage;
|
||||
try {
|
||||
parsed = JSON.parse(msg.toString());
|
||||
parsed = JSON.parse(msg.toString()) as WebSocketMessage;
|
||||
} catch (e) {
|
||||
sshLogger.error("Invalid JSON received", e, {
|
||||
operation: "websocket_message_invalid_json",
|
||||
@@ -203,16 +244,17 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
const { type, data } = parsed;
|
||||
|
||||
switch (type) {
|
||||
case "connectToHost":
|
||||
if (data.hostConfig) {
|
||||
data.hostConfig.userId = userId;
|
||||
case "connectToHost": {
|
||||
const connectData = data as ConnectToHostData;
|
||||
if (connectData.hostConfig) {
|
||||
connectData.hostConfig.userId = userId;
|
||||
}
|
||||
handleConnectToHost(data).catch((error) => {
|
||||
handleConnectToHost(connectData).catch((error) => {
|
||||
sshLogger.error("Failed to connect to host", error, {
|
||||
operation: "ssh_connect",
|
||||
userId,
|
||||
hostId: data.hostConfig?.id,
|
||||
ip: data.hostConfig?.ip,
|
||||
hostId: connectData.hostConfig?.id,
|
||||
ip: connectData.hostConfig?.ip,
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
@@ -224,43 +266,52 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "resize":
|
||||
handleResize(data);
|
||||
case "resize": {
|
||||
const resizeData = data as ResizeData;
|
||||
handleResize(resizeData);
|
||||
break;
|
||||
}
|
||||
|
||||
case "disconnect":
|
||||
cleanupSSH();
|
||||
break;
|
||||
|
||||
case "input":
|
||||
case "input": {
|
||||
const inputData = data as string;
|
||||
if (sshStream) {
|
||||
if (data === "\t") {
|
||||
sshStream.write(data);
|
||||
} else if (data.startsWith("\x1b")) {
|
||||
sshStream.write(data);
|
||||
if (inputData === "\t") {
|
||||
sshStream.write(inputData);
|
||||
} else if (
|
||||
typeof inputData === "string" &&
|
||||
inputData.startsWith("\x1b")
|
||||
) {
|
||||
sshStream.write(inputData);
|
||||
} else {
|
||||
try {
|
||||
sshStream.write(Buffer.from(data, "utf8"));
|
||||
sshStream.write(Buffer.from(inputData, "utf8"));
|
||||
} catch (error) {
|
||||
sshLogger.error("Error writing input to SSH stream", error, {
|
||||
operation: "ssh_input_encoding",
|
||||
userId,
|
||||
dataLength: data.length,
|
||||
dataLength: inputData.length,
|
||||
});
|
||||
sshStream.write(Buffer.from(data, "latin1"));
|
||||
sshStream.write(Buffer.from(inputData, "latin1"));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "ping":
|
||||
ws.send(JSON.stringify({ type: "pong" }));
|
||||
break;
|
||||
|
||||
case "totp_response":
|
||||
if (keyboardInteractiveFinish && data?.code) {
|
||||
const totpCode = data.code;
|
||||
case "totp_response": {
|
||||
const totpData = data as TOTPResponseData;
|
||||
if (keyboardInteractiveFinish && totpData?.code) {
|
||||
const totpCode = totpData.code;
|
||||
sshLogger.info("TOTP code received from user", {
|
||||
operation: "totp_response",
|
||||
userId,
|
||||
@@ -274,7 +325,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
operation: "totp_response_error",
|
||||
userId,
|
||||
hasCallback: !!keyboardInteractiveFinish,
|
||||
hasCode: !!data?.code,
|
||||
hasCode: !!totpData?.code,
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
@@ -284,6 +335,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
sshLogger.warn("Unknown message type received", {
|
||||
@@ -294,25 +346,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
}
|
||||
});
|
||||
|
||||
async function handleConnectToHost(data: {
|
||||
cols: number;
|
||||
rows: number;
|
||||
hostConfig: {
|
||||
id: number;
|
||||
ip: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password?: string;
|
||||
key?: string;
|
||||
keyPassword?: string;
|
||||
keyType?: string;
|
||||
authType?: string;
|
||||
credentialId?: number;
|
||||
userId?: string;
|
||||
};
|
||||
initialPath?: string;
|
||||
executeCommand?: string;
|
||||
}) {
|
||||
async function handleConnectToHost(data: ConnectToHostData) {
|
||||
const { cols, rows, hostConfig, initialPath, executeCommand } = data;
|
||||
const {
|
||||
id,
|
||||
@@ -642,7 +676,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
},
|
||||
);
|
||||
|
||||
const connectConfig: any = {
|
||||
const connectConfig: ConnectConfig = {
|
||||
host: ip,
|
||||
port,
|
||||
username,
|
||||
@@ -650,21 +684,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
keepaliveInterval: 30000,
|
||||
keepaliveCountMax: 3,
|
||||
readyTimeout: 60000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
|
||||
env: {
|
||||
TERM: "xterm-256color",
|
||||
LANG: "en_US.UTF-8",
|
||||
LC_ALL: "en_US.UTF-8",
|
||||
LC_CTYPE: "en_US.UTF-8",
|
||||
LC_MESSAGES: "en_US.UTF-8",
|
||||
LC_MONETARY: "en_US.UTF-8",
|
||||
LC_NUMERIC: "en_US.UTF-8",
|
||||
LC_TIME: "en_US.UTF-8",
|
||||
LC_COLLATE: "en_US.UTF-8",
|
||||
COLORTERM: "truecolor",
|
||||
},
|
||||
|
||||
algorithms: {
|
||||
kex: [
|
||||
@@ -688,6 +707,15 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
"aes256-cbc",
|
||||
"3des-cbc",
|
||||
],
|
||||
serverHostKey: [
|
||||
"ssh-rsa",
|
||||
"rsa-sha2-256",
|
||||
"rsa-sha2-512",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ssh-ed25519",
|
||||
],
|
||||
hmac: [
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
@@ -726,13 +754,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
if (resolvedCredentials.keyPassword) {
|
||||
connectConfig.passphrase = resolvedCredentials.keyPassword;
|
||||
}
|
||||
|
||||
if (
|
||||
resolvedCredentials.keyType &&
|
||||
resolvedCredentials.keyType !== "auto"
|
||||
) {
|
||||
connectConfig.privateKeyType = resolvedCredentials.keyType;
|
||||
}
|
||||
} catch (keyError) {
|
||||
sshLogger.error("SSH key format error: " + keyError.message);
|
||||
ws.send(
|
||||
@@ -766,7 +787,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
sshConn.connect(connectConfig);
|
||||
}
|
||||
|
||||
function handleResize(data: { cols: number; rows: number }) {
|
||||
function handleResize(data: ResizeData) {
|
||||
if (sshStream && sshStream.setWindow) {
|
||||
sshStream.setWindow(data.rows, data.cols, data.rows, data.cols);
|
||||
ws.send(
|
||||
@@ -788,8 +809,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
if (sshStream) {
|
||||
try {
|
||||
sshStream.end();
|
||||
} catch (e: any) {
|
||||
sshLogger.error("Error closing stream: " + e.message);
|
||||
} catch (e: unknown) {
|
||||
sshLogger.error(
|
||||
"Error closing stream: " +
|
||||
(e instanceof Error ? e.message : "Unknown error"),
|
||||
);
|
||||
}
|
||||
sshStream = null;
|
||||
}
|
||||
@@ -797,8 +821,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
if (sshConn) {
|
||||
try {
|
||||
sshConn.end();
|
||||
} catch (e: any) {
|
||||
sshLogger.error("Error closing connection: " + e.message);
|
||||
} catch (e: unknown) {
|
||||
sshLogger.error(
|
||||
"Error closing connection: " +
|
||||
(e instanceof Error ? e.message : "Unknown error"),
|
||||
);
|
||||
}
|
||||
sshConn = null;
|
||||
}
|
||||
@@ -809,8 +836,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
if (sshConn && sshStream) {
|
||||
try {
|
||||
sshStream.write("\x00");
|
||||
} catch (e: any) {
|
||||
sshLogger.error("SSH keepalive failed: " + e.message);
|
||||
} catch (e: unknown) {
|
||||
sshLogger.error(
|
||||
"SSH keepalive failed: " +
|
||||
(e instanceof Error ? e.message : "Unknown error"),
|
||||
);
|
||||
cleanupSSH();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,18 @@ interface JWTPayload {
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
userId?: string;
|
||||
pendingTOTP?: boolean;
|
||||
dataKey?: Buffer;
|
||||
}
|
||||
|
||||
interface RequestWithHeaders extends Request {
|
||||
headers: Request["headers"] & {
|
||||
"x-forwarded-proto"?: string;
|
||||
};
|
||||
}
|
||||
|
||||
class AuthManager {
|
||||
private static instance: AuthManager;
|
||||
private systemCrypto: SystemCrypto;
|
||||
@@ -163,7 +175,10 @@ class AuthManager {
|
||||
});
|
||||
}
|
||||
|
||||
getSecureCookieOptions(req: any, maxAge: number = 24 * 60 * 60 * 1000) {
|
||||
getSecureCookieOptions(
|
||||
req: RequestWithHeaders,
|
||||
maxAge: number = 24 * 60 * 60 * 1000,
|
||||
) {
|
||||
return {
|
||||
httpOnly: false,
|
||||
secure: req.secure || req.headers["x-forwarded-proto"] === "https",
|
||||
@@ -175,10 +190,11 @@ class AuthManager {
|
||||
|
||||
createAuthMiddleware() {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
let token = req.cookies?.jwt;
|
||||
const authReq = req as AuthenticatedRequest;
|
||||
let token = authReq.cookies?.jwt;
|
||||
|
||||
if (!token) {
|
||||
const authHeader = req.headers["authorization"];
|
||||
const authHeader = authReq.headers["authorization"];
|
||||
if (authHeader?.startsWith("Bearer ")) {
|
||||
token = authHeader.split(" ")[1];
|
||||
}
|
||||
@@ -194,15 +210,16 @@ class AuthManager {
|
||||
return res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
|
||||
(req as any).userId = payload.userId;
|
||||
(req as any).pendingTOTP = payload.pendingTOTP;
|
||||
authReq.userId = payload.userId;
|
||||
authReq.pendingTOTP = payload.pendingTOTP;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
createDataAccessMiddleware() {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const userId = (req as any).userId;
|
||||
const authReq = req as AuthenticatedRequest;
|
||||
const userId = authReq.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Authentication required" });
|
||||
}
|
||||
@@ -215,7 +232,7 @@ class AuthManager {
|
||||
});
|
||||
}
|
||||
|
||||
(req as any).dataKey = dataKey;
|
||||
authReq.dataKey = dataKey;
|
||||
next();
|
||||
};
|
||||
}
|
||||
@@ -256,8 +273,9 @@ class AuthManager {
|
||||
return res.status(403).json({ error: "Admin access required" });
|
||||
}
|
||||
|
||||
(req as any).userId = payload.userId;
|
||||
(req as any).pendingTOTP = payload.pendingTOTP;
|
||||
const authReq = req as AuthenticatedRequest;
|
||||
authReq.userId = payload.userId;
|
||||
authReq.pendingTOTP = payload.pendingTOTP;
|
||||
next();
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to verify admin privileges", error, {
|
||||
|
||||
@@ -3,6 +3,19 @@ import { LazyFieldEncryption } from "./lazy-field-encryption.js";
|
||||
import { UserCrypto } from "./user-crypto.js";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
|
||||
interface DatabaseInstance {
|
||||
prepare: (sql: string) => {
|
||||
all: (param?: unknown) => DatabaseRecord[];
|
||||
get: (param?: unknown) => DatabaseRecord;
|
||||
run: (...params: unknown[]) => unknown;
|
||||
};
|
||||
}
|
||||
|
||||
interface DatabaseRecord {
|
||||
id: number | string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
class DataCrypto {
|
||||
private static userCrypto: UserCrypto;
|
||||
|
||||
@@ -10,13 +23,13 @@ class DataCrypto {
|
||||
this.userCrypto = UserCrypto.getInstance();
|
||||
}
|
||||
|
||||
static encryptRecord(
|
||||
static encryptRecord<T extends Record<string, unknown>>(
|
||||
tableName: string,
|
||||
record: Record<string, unknown>,
|
||||
record: T,
|
||||
userId: string,
|
||||
userDataKey: Buffer,
|
||||
): any {
|
||||
const encryptedRecord = { ...record };
|
||||
): T {
|
||||
const encryptedRecord: Record<string, unknown> = { ...record };
|
||||
const recordId = record.id || "temp-" + Date.now();
|
||||
|
||||
for (const [fieldName, value] of Object.entries(record)) {
|
||||
@@ -30,18 +43,18 @@ class DataCrypto {
|
||||
}
|
||||
}
|
||||
|
||||
return encryptedRecord;
|
||||
return encryptedRecord as T;
|
||||
}
|
||||
|
||||
static decryptRecord(
|
||||
static decryptRecord<T extends Record<string, unknown>>(
|
||||
tableName: string,
|
||||
record: Record<string, unknown>,
|
||||
record: T,
|
||||
userId: string,
|
||||
userDataKey: Buffer,
|
||||
): any {
|
||||
): T {
|
||||
if (!record) return record;
|
||||
|
||||
const decryptedRecord = { ...record };
|
||||
const decryptedRecord: Record<string, unknown> = { ...record };
|
||||
const recordId = record.id;
|
||||
|
||||
for (const [fieldName, value] of Object.entries(record)) {
|
||||
@@ -55,30 +68,25 @@ class DataCrypto {
|
||||
}
|
||||
}
|
||||
|
||||
return decryptedRecord;
|
||||
return decryptedRecord as T;
|
||||
}
|
||||
|
||||
static decryptRecords(
|
||||
static decryptRecords<T extends Record<string, unknown>>(
|
||||
tableName: string,
|
||||
records: unknown[],
|
||||
records: T[],
|
||||
userId: string,
|
||||
userDataKey: Buffer,
|
||||
): unknown[] {
|
||||
): T[] {
|
||||
if (!Array.isArray(records)) return records;
|
||||
return records.map((record) =>
|
||||
this.decryptRecord(
|
||||
tableName,
|
||||
record as Record<string, unknown>,
|
||||
userId,
|
||||
userDataKey,
|
||||
),
|
||||
this.decryptRecord(tableName, record, userId, userDataKey),
|
||||
);
|
||||
}
|
||||
|
||||
static async migrateUserSensitiveFields(
|
||||
userId: string,
|
||||
userDataKey: Buffer,
|
||||
db: any,
|
||||
db: DatabaseInstance,
|
||||
): Promise<{
|
||||
migrated: boolean;
|
||||
migratedTables: string[];
|
||||
@@ -102,7 +110,7 @@ class DataCrypto {
|
||||
|
||||
const sshDataRecords = db
|
||||
.prepare("SELECT * FROM ssh_data WHERE user_id = ?")
|
||||
.all(userId);
|
||||
.all(userId) as DatabaseRecord[];
|
||||
for (const record of sshDataRecords) {
|
||||
const sensitiveFields =
|
||||
LazyFieldEncryption.getSensitiveFieldsForTable("ssh_data");
|
||||
@@ -137,7 +145,7 @@ class DataCrypto {
|
||||
|
||||
const sshCredentialsRecords = db
|
||||
.prepare("SELECT * FROM ssh_credentials WHERE user_id = ?")
|
||||
.all(userId);
|
||||
.all(userId) as DatabaseRecord[];
|
||||
for (const record of sshCredentialsRecords) {
|
||||
const sensitiveFields =
|
||||
LazyFieldEncryption.getSensitiveFieldsForTable("ssh_credentials");
|
||||
@@ -174,7 +182,7 @@ class DataCrypto {
|
||||
|
||||
const userRecord = db
|
||||
.prepare("SELECT * FROM users WHERE id = ?")
|
||||
.get(userId);
|
||||
.get(userId) as DatabaseRecord | undefined;
|
||||
if (userRecord) {
|
||||
const sensitiveFields =
|
||||
LazyFieldEncryption.getSensitiveFieldsForTable("users");
|
||||
@@ -225,7 +233,7 @@ class DataCrypto {
|
||||
static async reencryptUserDataAfterPasswordReset(
|
||||
userId: string,
|
||||
newUserDataKey: Buffer,
|
||||
db: any,
|
||||
db: DatabaseInstance,
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
reencryptedTables: string[];
|
||||
@@ -267,17 +275,21 @@ class DataCrypto {
|
||||
try {
|
||||
const records = db
|
||||
.prepare(`SELECT * FROM ${table} WHERE user_id = ?`)
|
||||
.all(userId);
|
||||
.all(userId) as DatabaseRecord[];
|
||||
|
||||
for (const record of records) {
|
||||
const recordId = record.id.toString();
|
||||
const updatedRecord: DatabaseRecord = { ...record };
|
||||
let needsUpdate = false;
|
||||
const updatedRecord = { ...record };
|
||||
|
||||
for (const fieldName of fields) {
|
||||
const fieldValue = record[fieldName];
|
||||
|
||||
if (fieldValue && fieldValue.trim() !== "") {
|
||||
if (
|
||||
fieldValue &&
|
||||
typeof fieldValue === "string" &&
|
||||
fieldValue.trim() !== ""
|
||||
) {
|
||||
try {
|
||||
const reencryptedValue = FieldCrypto.encryptField(
|
||||
fieldValue,
|
||||
@@ -389,29 +401,29 @@ class DataCrypto {
|
||||
return userDataKey;
|
||||
}
|
||||
|
||||
static encryptRecordForUser(
|
||||
static encryptRecordForUser<T extends Record<string, unknown>>(
|
||||
tableName: string,
|
||||
record: Record<string, unknown>,
|
||||
record: T,
|
||||
userId: string,
|
||||
): any {
|
||||
): T {
|
||||
const userDataKey = this.validateUserAccess(userId);
|
||||
return this.encryptRecord(tableName, record, userId, userDataKey);
|
||||
}
|
||||
|
||||
static decryptRecordForUser(
|
||||
static decryptRecordForUser<T extends Record<string, unknown>>(
|
||||
tableName: string,
|
||||
record: Record<string, unknown>,
|
||||
record: T,
|
||||
userId: string,
|
||||
): any {
|
||||
): T {
|
||||
const userDataKey = this.validateUserAccess(userId);
|
||||
return this.decryptRecord(tableName, record, userId, userDataKey);
|
||||
}
|
||||
|
||||
static decryptRecordsForUser(
|
||||
static decryptRecordsForUser<T extends Record<string, unknown>>(
|
||||
tableName: string,
|
||||
records: unknown[],
|
||||
records: T[],
|
||||
userId: string,
|
||||
): unknown[] {
|
||||
): T[] {
|
||||
const userDataKey = this.validateUserAccess(userId);
|
||||
return this.decryptRecords(tableName, records, userId, userDataKey);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user