mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
More MSSQL strict null (#22402)
This commit is contained in:
@@ -19,7 +19,7 @@ export class AzureBlobService implements mssql.IAzureBlobService {
|
|||||||
undefined,
|
undefined,
|
||||||
e => {
|
e => {
|
||||||
this.client.logFailedRequest(contracts.CreateSasRequest.type, e);
|
this.client.logFailedRequest(contracts.CreateSasRequest.type, e);
|
||||||
return Promise.resolve(undefined);
|
return Promise.reject(e);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class AzdataExtensionBookContributionProvider extends Disposable implements Book
|
|||||||
vscode.extensions.onDidChange(async () => {
|
vscode.extensions.onDidChange(async () => {
|
||||||
const currentContributions = this.getCurrentContributions();
|
const currentContributions = this.getCurrentContributions();
|
||||||
const existingContributions = this._contributions || undefined;
|
const existingContributions = this._contributions || undefined;
|
||||||
if (!arrays.equals(existingContributions, currentContributions, BookContributions.equal)) {
|
if (!existingContributions || !arrays.equals(existingContributions, currentContributions, BookContributions.equal)) {
|
||||||
await this.unregisterCommands();
|
await this.unregisterCommands();
|
||||||
this._contributions = currentContributions;
|
this._contributions = currentContributions;
|
||||||
await this.registerCommands();
|
await this.registerCommands();
|
||||||
|
|||||||
@@ -30,38 +30,35 @@ export class SqlAssessmentService implements mssql.ISqlAssessmentService {
|
|||||||
private constructor(context: AppContext, protected readonly client: SqlOpsDataClient) {
|
private constructor(context: AppContext, protected readonly client: SqlOpsDataClient) {
|
||||||
context.registerService(constants.SqlAssessmentService, this);
|
context.registerService(constants.SqlAssessmentService, this);
|
||||||
}
|
}
|
||||||
async assessmentInvoke(ownerUri: string, targetType: azdata.sqlAssessment.SqlAssessmentTargetType): Promise<azdata.SqlAssessmentResult | undefined> {
|
async assessmentInvoke(ownerUri: string, targetType: azdata.sqlAssessment.SqlAssessmentTargetType): Promise<azdata.SqlAssessmentResult> {
|
||||||
let params: contracts.SqlAssessmentParams = { ownerUri: ownerUri, targetType: targetType };
|
let params: contracts.SqlAssessmentParams = { ownerUri: ownerUri, targetType: targetType };
|
||||||
try {
|
try {
|
||||||
return this.client.sendRequest(contracts.SqlAssessmentInvokeRequest.type, params);
|
return await this.client.sendRequest(contracts.SqlAssessmentInvokeRequest.type, params);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
this.client.logFailedRequest(contracts.SqlAssessmentInvokeRequest.type, e);
|
this.client.logFailedRequest(contracts.SqlAssessmentInvokeRequest.type, e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
async getAssessmentItems(ownerUri: string, targetType: azdata.sqlAssessment.SqlAssessmentTargetType): Promise<azdata.SqlAssessmentResult | undefined> {
|
async getAssessmentItems(ownerUri: string, targetType: azdata.sqlAssessment.SqlAssessmentTargetType): Promise<azdata.SqlAssessmentResult> {
|
||||||
let params: contracts.SqlAssessmentParams = { ownerUri: ownerUri, targetType: targetType };
|
let params: contracts.SqlAssessmentParams = { ownerUri: ownerUri, targetType: targetType };
|
||||||
try {
|
try {
|
||||||
return this.client.sendRequest(contracts.GetSqlAssessmentItemsRequest.type, params);
|
return await this.client.sendRequest(contracts.GetSqlAssessmentItemsRequest.type, params);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
this.client.logFailedRequest(contracts.GetSqlAssessmentItemsRequest.type, e);
|
this.client.logFailedRequest(contracts.GetSqlAssessmentItemsRequest.type, e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
async generateAssessmentScript(items: azdata.SqlAssessmentResultItem[], targetServerName: string, targetDatabaseName: string, taskExecutionMode: azdata.TaskExecutionMode): Promise<azdata.ResultStatus | undefined> {
|
async generateAssessmentScript(items: azdata.SqlAssessmentResultItem[], targetServerName: string, targetDatabaseName: string, taskExecutionMode: azdata.TaskExecutionMode): Promise<azdata.ResultStatus> {
|
||||||
let params: contracts.GenerateSqlAssessmentScriptParams = { items: items, targetServerName: targetServerName, targetDatabaseName: targetDatabaseName, taskExecutionMode: taskExecutionMode };
|
let params: contracts.GenerateSqlAssessmentScriptParams = { items: items, targetServerName: targetServerName, targetDatabaseName: targetDatabaseName, taskExecutionMode: taskExecutionMode };
|
||||||
try {
|
try {
|
||||||
return this.client.sendRequest(contracts.GenerateSqlAssessmentScriptRequest.type, params);
|
return await this.client.sendRequest(contracts.GenerateSqlAssessmentScriptRequest.type, params);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
this.client.logFailedRequest(contracts.GenerateSqlAssessmentScriptRequest.type, e);
|
this.client.logFailedRequest(contracts.GenerateSqlAssessmentScriptRequest.type, e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export class SqlNotebookController implements vscode.Disposable {
|
|||||||
private readonly _connProvider: azdata.ConnectionProvider;
|
private readonly _connProvider: azdata.ConnectionProvider;
|
||||||
private readonly _connectionLabelItem: vscode.StatusBarItem;
|
private readonly _connectionLabelItem: vscode.StatusBarItem;
|
||||||
|
|
||||||
private _queryCompleteHandler: QueryCompletionHandler;
|
private _queryCompleteHandler: QueryCompletionHandler | undefined;
|
||||||
private _queryMessageHandler: QueryMessageHandler;
|
private _queryMessageHandler: QueryMessageHandler | undefined;
|
||||||
private _activeCellUri: string;
|
private _activeCellUri: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -84,7 +84,7 @@ export class SqlNotebookController implements vscode.Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleActiveEditorChanged(editor: vscode.TextEditor): Promise<void> {
|
private async handleActiveEditorChanged(editor: vscode.TextEditor | undefined): Promise<void> {
|
||||||
let notebook = editor?.document.notebook;
|
let notebook = editor?.document.notebook;
|
||||||
if (!notebook) {
|
if (!notebook) {
|
||||||
// Hide status bar item if the current editor isn't a notebook
|
// Hide status bar item if the current editor isn't a notebook
|
||||||
@@ -133,19 +133,19 @@ export class SqlNotebookController implements vscode.Disposable {
|
|||||||
|
|
||||||
private updateCellConnection(notebookUri: vscode.Uri, connection: azdata.connection.Connection): void {
|
private updateCellConnection(notebookUri: vscode.Uri, connection: azdata.connection.Connection): void {
|
||||||
let docUri = vscode.window.activeTextEditor?.document.uri;
|
let docUri = vscode.window.activeTextEditor?.document.uri;
|
||||||
if (docUri?.scheme === this._cellUriScheme && docUri?.path === notebookUri.path) {
|
if (docUri && docUri.scheme === this._cellUriScheme && docUri.path === notebookUri.path) {
|
||||||
if (this._activeCellUri) {
|
if (this._activeCellUri) {
|
||||||
this._connProvider.disconnect(this._activeCellUri).then(() => undefined, error => console.log(error));
|
this._connProvider.disconnect(this._activeCellUri).then(() => undefined, error => console.log(error));
|
||||||
}
|
}
|
||||||
this._activeCellUri = docUri.toString();
|
this._activeCellUri = docUri.toString();
|
||||||
// Delay connecting in case user is clicking between cells a lot
|
// Delay connecting in case user is clicking between cells a lot
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this._activeCellUri === docUri.toString()) {
|
if (this._activeCellUri === docUri!.toString()) {
|
||||||
let profile = this.getConnectionProfile(connection);
|
let profile = this.getConnectionProfile(connection);
|
||||||
this._connProvider.connect(docUri.toString(), profile).then(
|
this._connProvider.connect(docUri!.toString(), profile).then(
|
||||||
connected => {
|
connected => {
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
console.log(`Failed to update cell connection for cell: ${docUri.toString()}`);
|
console.log(`Failed to update cell connection for cell: ${docUri!.toString()}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
@@ -157,7 +157,7 @@ export class SqlNotebookController implements vscode.Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async changeConnection(notebook?: vscode.NotebookDocument): Promise<azdata.connection.Connection | undefined> {
|
private async changeConnection(notebook?: vscode.NotebookDocument): Promise<azdata.connection.Connection | undefined> {
|
||||||
let connection: azdata.connection.Connection;
|
let connection: azdata.connection.Connection | undefined;
|
||||||
let notebookUri = notebook?.uri ?? vscode.window.activeTextEditor?.document.notebook?.uri;
|
let notebookUri = notebook?.uri ?? vscode.window.activeTextEditor?.document.notebook?.uri;
|
||||||
if (notebookUri) {
|
if (notebookUri) {
|
||||||
connection = await azdata.connection.openConnectionDialog(['MSSQL']);
|
connection = await azdata.connection.openConnectionDialog(['MSSQL']);
|
||||||
@@ -207,7 +207,7 @@ export class SqlNotebookController implements vscode.Disposable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelHandler: vscode.Disposable;
|
let cancelHandler: vscode.Disposable | undefined;
|
||||||
try {
|
try {
|
||||||
const ownerUri = await azdata.connection.getUriForConnection(connection.connectionId);
|
const ownerUri = await azdata.connection.getUriForConnection(connection.connectionId);
|
||||||
await this._queryProvider.runQueryString(ownerUri, cell.document.getText());
|
await this._queryProvider.runQueryString(ownerUri, cell.document.getText());
|
||||||
@@ -221,7 +221,7 @@ export class SqlNotebookController implements vscode.Disposable {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let resultSummary of batchSummary.resultSetSummaries) {
|
for (let resultSummary of batchSummary.resultSetSummaries || []) {
|
||||||
if (execution.token.isCancellationRequested) {
|
if (execution.token.isCancellationRequested) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -302,9 +302,7 @@ export class SqlNotebookController implements vscode.Disposable {
|
|||||||
]);
|
]);
|
||||||
execution.end(false, Date.now());
|
execution.end(false, Date.now());
|
||||||
} finally {
|
} finally {
|
||||||
if (cancelHandler) {
|
cancelHandler?.dispose();
|
||||||
cancelHandler.dispose();
|
|
||||||
}
|
|
||||||
this._queryCompleteHandler = undefined;
|
this._queryCompleteHandler = undefined;
|
||||||
this._queryMessageHandler = undefined;
|
this._queryMessageHandler = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class SqlToolsServer {
|
|||||||
private client: SqlOpsDataClient;
|
private client: SqlOpsDataClient;
|
||||||
private config: IConfig;
|
private config: IConfig;
|
||||||
private disposables = new Array<{ dispose: () => void }>();
|
private disposables = new Array<{ dispose: () => void }>();
|
||||||
public installDirectory: string | undefined = undefined;
|
public installDirectory: string;
|
||||||
|
|
||||||
public async start(context: AppContext): Promise<SqlOpsDataClient> {
|
public async start(context: AppContext): Promise<SqlOpsDataClient> {
|
||||||
try {
|
try {
|
||||||
@@ -134,7 +134,7 @@ export class SqlToolsServer {
|
|||||||
const rawConfig = await fs.readFile(path.join(configDir, 'config.json'));
|
const rawConfig = await fs.readFile(path.join(configDir, 'config.json'));
|
||||||
this.config = JSON.parse(rawConfig.toString());
|
this.config = JSON.parse(rawConfig.toString());
|
||||||
this.config.installDirectory = path.join(configDir, this.config.installDirectory);
|
this.config.installDirectory = path.join(configDir, this.config.installDirectory);
|
||||||
this.config.proxy = vscode.workspace.getConfiguration('http').get('proxy');
|
this.config.proxy = vscode.workspace.getConfiguration('http').get<string>('proxy', '');
|
||||||
this.config.strictSSL = vscode.workspace.getConfiguration('http').get('proxyStrictSSL', true);
|
this.config.strictSSL = vscode.workspace.getConfiguration('http').get('proxyStrictSSL', true);
|
||||||
return getOrDownloadServer(this.config, handleServerProviderEvent);
|
return getOrDownloadServer(this.config, handleServerProviderEvent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as crypto from 'crypto';
|
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as findRemoveSync from 'find-remove';
|
import * as findRemoveSync from 'find-remove';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
@@ -36,7 +35,7 @@ export const isLinux = os.platform() === 'linux';
|
|||||||
export function getAppDataPath() {
|
export function getAppDataPath() {
|
||||||
let platform = process.platform;
|
let platform = process.platform;
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE'], 'AppData', 'Roaming');
|
case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE'] || '', 'AppData', 'Roaming');
|
||||||
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support');
|
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support');
|
||||||
case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
|
case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
|
||||||
default: throw new Error('Platform not supported');
|
default: throw new Error('Platform not supported');
|
||||||
@@ -79,7 +78,7 @@ export function getAzureCoreExtConfiguration(config: string = azureExtensionConf
|
|||||||
return vscode.workspace.getConfiguration(config);
|
return vscode.workspace.getConfiguration(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfigLogFilesRemovalLimit(): number {
|
export function getConfigLogFilesRemovalLimit(): number | undefined {
|
||||||
let config = getConfiguration();
|
let config = getConfiguration();
|
||||||
if (config) {
|
if (config) {
|
||||||
return Number((config[configLogFilesRemovalLimit]).toFixed(0));
|
return Number((config[configLogFilesRemovalLimit]).toFixed(0));
|
||||||
@@ -88,7 +87,7 @@ export function getConfigLogFilesRemovalLimit(): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfigLogRetentionSeconds(): number {
|
export function getConfigLogRetentionSeconds(): number | undefined {
|
||||||
let config = getConfiguration();
|
let config = getConfiguration();
|
||||||
if (config) {
|
if (config) {
|
||||||
return Number((config[configLogRetentionMinutes] * 60).toFixed(0));
|
return Number((config[configLogRetentionMinutes] * 60).toFixed(0));
|
||||||
@@ -131,7 +130,7 @@ export function getConfigPiiLogging(): boolean {
|
|||||||
export function getConfigPreloadDatabaseModel(): boolean {
|
export function getConfigPreloadDatabaseModel(): boolean {
|
||||||
let config = getConfiguration();
|
let config = getConfiguration();
|
||||||
if (config) {
|
if (config) {
|
||||||
return config.get<boolean>(tableDesignerPreloadConfig);
|
return config.get<boolean>(tableDesignerPreloadConfig, false);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -150,15 +149,13 @@ export function getParallelMessageProcessingConfig(): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const setting = config.inspect(parallelMessageProcessingConfig);
|
const setting = config.inspect(parallelMessageProcessingConfig);
|
||||||
return (azdata.env.quality === azdata.env.AppQuality.dev && setting.globalValue === undefined && setting.workspaceValue === undefined) ? true : config[parallelMessageProcessingConfig];
|
return (azdata.env.quality === azdata.env.AppQuality.dev && setting?.globalValue === undefined && setting?.workspaceValue === undefined) ? true : config[parallelMessageProcessingConfig];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAzureAuthenticationLibraryConfig(): string {
|
export function getAzureAuthenticationLibraryConfig(): string {
|
||||||
const config = getAzureCoreExtConfiguration();
|
const config = getAzureCoreExtConfiguration();
|
||||||
if (config) {
|
if (config) {
|
||||||
return config.has(azureAuthenticationLibraryConfig)
|
return config.get<string>(azureAuthenticationLibraryConfig, 'MSAL'); // default Auth library
|
||||||
? config.get<string>(azureAuthenticationLibraryConfig)
|
|
||||||
: 'MSAL'; // default Auth library
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return 'MSAL';
|
return 'MSAL';
|
||||||
@@ -168,9 +165,7 @@ export function getAzureAuthenticationLibraryConfig(): string {
|
|||||||
export function getEnableSqlAuthenticationProviderConfig(): boolean {
|
export function getEnableSqlAuthenticationProviderConfig(): boolean {
|
||||||
const config = getConfiguration();
|
const config = getConfiguration();
|
||||||
if (config) {
|
if (config) {
|
||||||
return config.has(enableSqlAuthenticationProviderConfig)
|
return config.get<boolean>(enableSqlAuthenticationProviderConfig, false); // disabled by default
|
||||||
? config.get<boolean>(enableSqlAuthenticationProviderConfig)
|
|
||||||
: false; // disabled by default
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return false;
|
return false;
|
||||||
@@ -215,70 +210,6 @@ export function ensure(target: { [key: string]: any }, key: string): any {
|
|||||||
return target[key];
|
return target[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPackageInfo {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
aiKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPackageInfo(packageJson: any): IPackageInfo {
|
|
||||||
if (packageJson) {
|
|
||||||
return {
|
|
||||||
name: packageJson.name,
|
|
||||||
version: packageJson.version,
|
|
||||||
aiKey: packageJson.aiKey
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateUserId(): Promise<string> {
|
|
||||||
return new Promise<string>(resolve => {
|
|
||||||
try {
|
|
||||||
let interfaces = os.networkInterfaces();
|
|
||||||
let mac;
|
|
||||||
for (let key of Object.keys(interfaces)) {
|
|
||||||
let item = interfaces[key][0];
|
|
||||||
if (!item.internal) {
|
|
||||||
mac = item.mac;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mac) {
|
|
||||||
resolve(crypto.createHash('sha256').update(mac + os.homedir(), 'utf8').digest('hex'));
|
|
||||||
} else {
|
|
||||||
resolve(generateGuid());
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
resolve(generateGuid()); // fallback
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateGuid(): string {
|
|
||||||
let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
|
|
||||||
// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
|
|
||||||
let oct: string = '';
|
|
||||||
let tmp: number;
|
|
||||||
/* tslint:disable:no-bitwise */
|
|
||||||
for (let a: number = 0; a < 4; a++) {
|
|
||||||
tmp = (4294967296 * Math.random()) | 0;
|
|
||||||
oct += hexValues[tmp & 0xF] +
|
|
||||||
hexValues[tmp >> 4 & 0xF] +
|
|
||||||
hexValues[tmp >> 8 & 0xF] +
|
|
||||||
hexValues[tmp >> 12 & 0xF] +
|
|
||||||
hexValues[tmp >> 16 & 0xF] +
|
|
||||||
hexValues[tmp >> 20 & 0xF] +
|
|
||||||
hexValues[tmp >> 24 & 0xF] +
|
|
||||||
hexValues[tmp >> 28 & 0xF];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively'
|
|
||||||
let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
|
|
||||||
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
|
|
||||||
/* tslint:enable:no-bitwise */
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyPlatform(): Thenable<boolean> {
|
export function verifyPlatform(): Thenable<boolean> {
|
||||||
if (os.platform() === 'darwin' && parseFloat(os.release()) < 16) {
|
if (os.platform() === 'darwin' && parseFloat(os.release()) < 16) {
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
@@ -320,7 +251,7 @@ export function isObjectExplorerContext(object: any): object is azdata.ObjectExp
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getUserHome(): string {
|
export function getUserHome(): string {
|
||||||
return process.env.HOME || process.env.USERPROFILE;
|
return process.env.HOME || process.env.USERPROFILE || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidNumber(maybeNumber: any) {
|
export function isValidNumber(maybeNumber: any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user