You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-09 00:22:08 +03:00
fix #1659 - add support for db-number in client options url
This commit is contained in:
@@ -1,24 +1,25 @@
|
|||||||
# `createClient` configuration
|
# `createClient` configuration
|
||||||
|
|
||||||
| Property | Default | Description |
|
| Property | Default | Description |
|
||||||
|--------------------------|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
|
|--------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| socket | | Object defining socket connection properties |
|
| url | | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) |
|
||||||
| socket.url | | `[redis[s]:]//[[username][:password]@][host][:port]` |
|
| socket | | Object defining socket connection properties |
|
||||||
| socket.host | `'localhost'` | Hostname to connect to |
|
| socket.host | `'localhost'` | Hostname to connect to |
|
||||||
| socket.port | `6379` | Port to connect to |
|
| socket.port | `6379` | Port to connect to |
|
||||||
| socket.username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
|
| socket.connectTimeout | `5000` | The timeout for connecting to the Redis Server (in milliseconds) |
|
||||||
| socket.password | | ACL password or the old "--requirepass" password |
|
| socket.noDelay | `true` | Enable/disable the use of [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
|
||||||
| socket.connectTimeout | `5000` | The timeout for connecting to the Redis Server (in milliseconds) |
|
| socket.keepAlive | `5000` | Enable/disable the [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality |
|
||||||
| socket.noDelay | `true` | Enable/disable the use of [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
|
| socket.tls | | Set to `true` to enable [TLS Configuration](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) |
|
||||||
| socket.keepAlive | `5000` | Enable/disable the [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality |
|
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
|
||||||
| socket.tls | | Set to `true` to enable [TLS Configuration](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) |
|
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
|
||||||
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
|
| password | | ACL password or the old "--requirepass" password |
|
||||||
| modules | | Object defining which [Redis Modules](https://redis.io/modules) to include (TODO - document) |
|
| database | | Database number to connect to (see [`SELECT`](https://redis.io/commands/select) command) |
|
||||||
| scripts | | Object defining Lua scripts to use with this client. See [Lua Scripts](../README.md#lua-scripts) |
|
| modules | | Object defining which [Redis Modules](https://redis.io/modules) to include (TODO - document) |
|
||||||
| commandsQueueMaxLength | | Maximum length of the client's internal command queue |
|
| scripts | | Object defining Lua Scripts to use with this client (see [Lua Scripts](../README.md#lua-scripts)) |
|
||||||
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
|
| commandsQueueMaxLength | | Maximum length of the client's internal command queue |
|
||||||
| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](v3-to-v4.md)) |
|
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
|
||||||
| isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) |
|
| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](v3-to-v4.md)) |
|
||||||
|
| isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) |
|
||||||
|
|
||||||
## Reconnect Strategy
|
## Reconnect Strategy
|
||||||
|
|
||||||
|
@@ -18,6 +18,53 @@ export const SQUARE_SCRIPT = defineScript({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Client', () => {
|
describe('Client', () => {
|
||||||
|
describe('parseURL', () => {
|
||||||
|
it('redis://user:secret@localhost:6379/0', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
RedisClient.parseURL('redis://user:secret@localhost:6379/0'),
|
||||||
|
{
|
||||||
|
socket: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 6379
|
||||||
|
},
|
||||||
|
username: 'user',
|
||||||
|
password: 'secret',
|
||||||
|
database: 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rediss://user:secret@localhost:6379/0', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
RedisClient.parseURL('rediss://user:secret@localhost:6379/0'),
|
||||||
|
{
|
||||||
|
socket: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 6379,
|
||||||
|
tls: true
|
||||||
|
},
|
||||||
|
username: 'user',
|
||||||
|
password: 'secret',
|
||||||
|
database: 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Invalid protocol', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => RedisClient.parseURL('redi://user:secret@localhost:6379/0'),
|
||||||
|
TypeError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Invalid pathname', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => RedisClient.parseURL('redis://user:secret@localhost:6379/NaN'),
|
||||||
|
TypeError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('authentication', () => {
|
describe('authentication', () => {
|
||||||
itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => {
|
itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -28,10 +75,8 @@ describe('Client', () => {
|
|||||||
|
|
||||||
it('should not retry connecting if failed due to wrong auth', async () => {
|
it('should not retry connecting if failed due to wrong auth', async () => {
|
||||||
const client = RedisClient.create({
|
const client = RedisClient.create({
|
||||||
socket: {
|
...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD],
|
||||||
...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD],
|
password: 'wrongpassword'
|
||||||
password: 'wrongpassword'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
@@ -49,7 +94,7 @@ describe('Client', () => {
|
|||||||
|
|
||||||
describe('legacyMode', () => {
|
describe('legacyMode', () => {
|
||||||
const client = RedisClient.create({
|
const client = RedisClient.create({
|
||||||
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN],
|
...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
|
||||||
scripts: {
|
scripts: {
|
||||||
square: SQUARE_SCRIPT
|
square: SQUARE_SCRIPT
|
||||||
},
|
},
|
||||||
@@ -173,9 +218,7 @@ describe('Client', () => {
|
|||||||
|
|
||||||
describe('events', () => {
|
describe('events', () => {
|
||||||
it('connect, ready, end', async () => {
|
it('connect, ready, end', async () => {
|
||||||
const client = RedisClient.create({
|
const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
|
||||||
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
client.connect(),
|
client.connect(),
|
||||||
@@ -550,9 +593,7 @@ describe('Client', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('client.quit', async () => {
|
it('client.quit', async () => {
|
||||||
const client = RedisClient.create({
|
const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
|
||||||
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.connect();
|
await client.connect();
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import RedisSocket, { RedisSocketOptions } from './socket';
|
import RedisSocket, { RedisSocketOptions, RedisNetSocketOptions, RedisTlsSocketOptions } from './socket';
|
||||||
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
|
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
|
||||||
import COMMANDS, { TransformArgumentsReply } from './commands';
|
import COMMANDS, { TransformArgumentsReply } from './commands';
|
||||||
import { RedisCommand, RedisModules, RedisReply } from './commands';
|
import { RedisCommand, RedisModules, RedisReply } from './commands';
|
||||||
@@ -12,9 +12,14 @@ import { HScanTuple } from './commands/HSCAN';
|
|||||||
import { encodeCommand, extendWithDefaultCommands, extendWithModulesAndScripts, transformCommandArguments } from './commander';
|
import { encodeCommand, extendWithDefaultCommands, extendWithModulesAndScripts, transformCommandArguments } from './commander';
|
||||||
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
||||||
import { ClientClosedError } from './errors';
|
import { ClientClosedError } from './errors';
|
||||||
|
import { URL } from 'url';
|
||||||
|
|
||||||
export interface RedisClientOptions<M, S> {
|
export interface RedisClientOptions<M, S> {
|
||||||
|
url?: string;
|
||||||
socket?: RedisSocketOptions;
|
socket?: RedisSocketOptions;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
database?: number;
|
||||||
modules?: M;
|
modules?: M;
|
||||||
scripts?: S;
|
scripts?: S;
|
||||||
commandsQueueMaxLength?: number;
|
commandsQueueMaxLength?: number;
|
||||||
@@ -71,6 +76,45 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
|
|||||||
return new Client(options);
|
return new Client(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static parseURL(url: string): RedisClientOptions<{}, {}> {
|
||||||
|
// https://www.iana.org/assignments/uri-schemes/prov/redis
|
||||||
|
const { hostname, port, protocol, username, password, pathname } = new URL(url),
|
||||||
|
parsed: RedisClientOptions<{}, {}> = {
|
||||||
|
socket: {
|
||||||
|
host: hostname
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (protocol === 'rediss:') {
|
||||||
|
(parsed.socket as RedisTlsSocketOptions).tls = true;
|
||||||
|
} else if (protocol !== 'redis:') {
|
||||||
|
throw new TypeError('Invalid protocol');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port) {
|
||||||
|
(parsed.socket as RedisNetSocketOptions).port = Number(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username) {
|
||||||
|
parsed.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
parsed.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.length > 1) {
|
||||||
|
const database = Number(pathname.substring(1));
|
||||||
|
if (isNaN(database)) {
|
||||||
|
throw new TypeError('Invalid pathname');
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
readonly #options?: RedisClientOptions<M, S>;
|
readonly #options?: RedisClientOptions<M, S>;
|
||||||
readonly #socket: RedisSocket;
|
readonly #socket: RedisSocket;
|
||||||
readonly #queue: RedisCommandsQueue;
|
readonly #queue: RedisCommandsQueue;
|
||||||
@@ -96,7 +140,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
|
|||||||
|
|
||||||
constructor(options?: RedisClientOptions<M, S>) {
|
constructor(options?: RedisClientOptions<M, S>) {
|
||||||
super();
|
super();
|
||||||
this.#options = options;
|
this.#options = this.#initiateOptions(options);
|
||||||
this.#socket = this.#initiateSocket();
|
this.#socket = this.#initiateSocket();
|
||||||
this.#queue = this.#initiateQueue();
|
this.#queue = this.#initiateQueue();
|
||||||
this.#isolationPool = createPool({
|
this.#isolationPool = createPool({
|
||||||
@@ -110,6 +154,23 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
|
|||||||
this.#legacyMode();
|
this.#legacyMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#initiateOptions(options?: RedisClientOptions<M, S>): RedisClientOptions<M, S> | undefined {
|
||||||
|
if (options?.url) {
|
||||||
|
const parsed = RedisClient.parseURL(options.url);
|
||||||
|
if (options.socket) {
|
||||||
|
parsed.socket = Object.assign(options.socket, parsed.socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(options, parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.database) {
|
||||||
|
this.#selectedDB = options.database;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
#initiateSocket(): RedisSocket {
|
#initiateSocket(): RedisSocket {
|
||||||
const socketInitiator = async (): Promise<void> => {
|
const socketInitiator = async (): Promise<void> => {
|
||||||
const v4Commands = this.#options?.legacyMode ? this.#v4 : this,
|
const v4Commands = this.#options?.legacyMode ? this.#v4 : this,
|
||||||
@@ -123,8 +184,8 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
|
|||||||
promises.push(v4Commands.readonly(RedisClient.commandOptions({ asap: true })));
|
promises.push(v4Commands.readonly(RedisClient.commandOptions({ asap: true })));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#options?.socket?.username || this.#options?.socket?.password) {
|
if (this.#options?.username || this.#options?.password) {
|
||||||
promises.push(v4Commands.auth(RedisClient.commandOptions({ asap: true }), this.#options.socket));
|
promises.push(v4Commands.auth(RedisClient.commandOptions({ asap: true }), this.#options));
|
||||||
}
|
}
|
||||||
|
|
||||||
const resubscribePromise = this.#queue.resubscribe();
|
const resubscribePromise = this.#queue.resubscribe();
|
||||||
|
@@ -6,8 +6,6 @@ import { ConnectionTimeoutError, ClientClosedError } from './errors';
|
|||||||
import { promiseTimeout } from './utils';
|
import { promiseTimeout } from './utils';
|
||||||
|
|
||||||
export interface RedisSocketCommonOptions {
|
export interface RedisSocketCommonOptions {
|
||||||
username?: string;
|
|
||||||
password?: string;
|
|
||||||
connectTimeout?: number;
|
connectTimeout?: number;
|
||||||
noDelay?: boolean;
|
noDelay?: boolean;
|
||||||
keepAlive?: number | false;
|
keepAlive?: number | false;
|
||||||
@@ -19,10 +17,6 @@ export interface RedisNetSocketOptions extends RedisSocketCommonOptions {
|
|||||||
host?: string;
|
host?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RedisUrlSocketOptions extends RedisSocketCommonOptions {
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RedisUnixSocketOptions extends RedisSocketCommonOptions {
|
export interface RedisUnixSocketOptions extends RedisSocketCommonOptions {
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
@@ -31,7 +25,7 @@ export interface RedisTlsSocketOptions extends RedisNetSocketOptions, tls.Secure
|
|||||||
tls: true;
|
tls: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RedisSocketOptions = RedisNetSocketOptions | RedisUrlSocketOptions | RedisUnixSocketOptions | RedisTlsSocketOptions;
|
export type RedisSocketOptions = RedisNetSocketOptions | RedisUnixSocketOptions | RedisTlsSocketOptions;
|
||||||
|
|
||||||
interface CreateSocketReturn<T> {
|
interface CreateSocketReturn<T> {
|
||||||
connectEvent: string;
|
connectEvent: string;
|
||||||
@@ -44,14 +38,6 @@ export default class RedisSocket extends EventEmitter {
|
|||||||
static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions {
|
static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions {
|
||||||
options ??= {};
|
options ??= {};
|
||||||
if (!RedisSocket.#isUnixSocket(options)) {
|
if (!RedisSocket.#isUnixSocket(options)) {
|
||||||
if (RedisSocket.#isUrlSocket(options)) {
|
|
||||||
const url = new URL(options.url);
|
|
||||||
(options as RedisNetSocketOptions).port = Number(url.port);
|
|
||||||
(options as RedisNetSocketOptions).host = url.hostname;
|
|
||||||
options.username = url.username;
|
|
||||||
options.password = url.password;
|
|
||||||
}
|
|
||||||
|
|
||||||
(options as RedisNetSocketOptions).port ??= 6379;
|
(options as RedisNetSocketOptions).port ??= 6379;
|
||||||
(options as RedisNetSocketOptions).host ??= '127.0.0.1';
|
(options as RedisNetSocketOptions).host ??= '127.0.0.1';
|
||||||
}
|
}
|
||||||
@@ -67,10 +53,6 @@ export default class RedisSocket extends EventEmitter {
|
|||||||
return Math.min(retries * 50, 500);
|
return Math.min(retries * 50, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
static #isUrlSocket(options: RedisSocketOptions): options is RedisUrlSocketOptions {
|
|
||||||
return Object.prototype.hasOwnProperty.call(options, 'url');
|
|
||||||
}
|
|
||||||
|
|
||||||
static #isUnixSocket(options: RedisSocketOptions): options is RedisUnixSocketOptions {
|
static #isUnixSocket(options: RedisSocketOptions): options is RedisUnixSocketOptions {
|
||||||
return Object.prototype.hasOwnProperty.call(options, 'path');
|
return Object.prototype.hasOwnProperty.call(options, 'path');
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'assert';
|
||||||
import RedisClient, { RedisClientType } from './client';
|
import RedisClient, { RedisClientOptions, RedisClientType } from './client';
|
||||||
import { execSync, spawn } from 'child_process';
|
import { execSync, spawn } from 'child_process';
|
||||||
import { once } from 'events';
|
import { once } from 'events';
|
||||||
import { RedisSocketOptions } from './socket';
|
import { RedisSocketOptions } from './socket';
|
||||||
@@ -9,6 +9,8 @@ import RedisCluster, { RedisClusterType } from './cluster';
|
|||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import { Context as MochaContext } from 'mocha';
|
import { Context as MochaContext } from 'mocha';
|
||||||
import { promiseTimeout } from './utils';
|
import { promiseTimeout } from './utils';
|
||||||
|
import { RedisModules } from './commands';
|
||||||
|
import { RedisLuaScripts } from './lua-script';
|
||||||
|
|
||||||
type RedisVersion = [major: number, minor: number, patch: number];
|
type RedisVersion = [major: number, minor: number, patch: number];
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ export enum TestRedisServers {
|
|||||||
PASSWORD
|
PASSWORD
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TEST_REDIS_SERVERS: Record<TestRedisServers, RedisSocketOptions> = <any>{};
|
export const TEST_REDIS_SERVERS: Record<TestRedisServers, RedisClientOptions<RedisModules, RedisLuaScripts>> = <any>{};
|
||||||
|
|
||||||
export enum TestRedisClusters {
|
export enum TestRedisClusters {
|
||||||
OPEN
|
OPEN
|
||||||
@@ -226,13 +228,17 @@ export async function spawnGlobalRedisCluster(type: TestRedisClusters | null, nu
|
|||||||
|
|
||||||
async function spawnOpenServer(): Promise<void> {
|
async function spawnOpenServer(): Promise<void> {
|
||||||
TEST_REDIS_SERVERS[TestRedisServers.OPEN] = {
|
TEST_REDIS_SERVERS[TestRedisServers.OPEN] = {
|
||||||
port: await spawnGlobalRedisServer()
|
socket: {
|
||||||
|
port: await spawnGlobalRedisServer()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function spawnPasswordServer(): Promise<void> {
|
async function spawnPasswordServer(): Promise<void> {
|
||||||
TEST_REDIS_SERVERS[TestRedisServers.PASSWORD] = {
|
TEST_REDIS_SERVERS[TestRedisServers.PASSWORD] = {
|
||||||
port: await spawnGlobalRedisServer(['--requirepass', 'password']),
|
socket: {
|
||||||
|
port: await spawnGlobalRedisServer(['--requirepass', 'password']),
|
||||||
|
},
|
||||||
password: 'password'
|
password: 'password'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -285,9 +291,7 @@ export function itWithClient(
|
|||||||
it(title, async function () {
|
it(title, async function () {
|
||||||
if (handleMinimumRedisVersion(this, options?.minimumRedisVersion)) return;
|
if (handleMinimumRedisVersion(this, options?.minimumRedisVersion)) return;
|
||||||
|
|
||||||
const client = RedisClient.create({
|
const client = RedisClient.create(TEST_REDIS_SERVERS[type]);
|
||||||
socket: TEST_REDIS_SERVERS[type]
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.connect();
|
await client.connect();
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user