mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 19:53:59 +00:00
Migrate set initial password component to new API
This commit is contained in:
@@ -12,8 +12,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import {
|
||||
EncryptedString,
|
||||
EncString,
|
||||
UnsignedSharedKey,
|
||||
} from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -60,7 +60,7 @@ export class OrganizationUserResetPasswordService
|
||||
orgId: string,
|
||||
userKey: UserKey,
|
||||
trustedPublicKeys: Uint8Array[],
|
||||
): Promise<EncryptedString> {
|
||||
): Promise<UnsignedSharedKey> {
|
||||
if (userKey == null) {
|
||||
throw new Error("User key is required for recovery.");
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export class OrganizationUserResetPasswordService
|
||||
// RSA Encrypt user key with organization's public key
|
||||
const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey);
|
||||
|
||||
return encryptedKey.encryptedString;
|
||||
return encryptedKey.toUnsignedSharedKey();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,9 +208,8 @@ export class OrganizationUserResetPasswordService
|
||||
const encryptedKey = await this.buildRecoveryKey(org.id, newUserKey, trustedPublicKeys);
|
||||
|
||||
// Create/Execute request
|
||||
const request = new OrganizationUserResetPasswordWithIdRequest();
|
||||
const request = new OrganizationUserResetPasswordWithIdRequest(encryptedKey);
|
||||
request.organizationId = org.id;
|
||||
request.resetPasswordKey = encryptedKey;
|
||||
request.masterPasswordHash = "ignored";
|
||||
|
||||
requests.push(request);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { UnsignedSharedKey } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -214,9 +215,9 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
} else {
|
||||
// Remove reset password
|
||||
const request = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
// Note: This seems wrong, and should be replaced by null or undefined, since "" is not a valid UnsignedSharedKey.
|
||||
const request = new OrganizationUserResetPasswordEnrollmentRequest("" as UnsignedSharedKey);
|
||||
request.masterPasswordHash = "ignored";
|
||||
request.resetPasswordKey = "";
|
||||
this.actionPromise =
|
||||
this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.organization.id,
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { UnsignedSharedKey } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
|
||||
export class OrganizationUserResetPasswordEnrollmentRequest extends SecretVerificationRequest {
|
||||
resetPasswordKey: string;
|
||||
resetPasswordKey: UnsignedSharedKey;
|
||||
|
||||
constructor(unsignedSharedKey: UnsignedSharedKey) {
|
||||
super();
|
||||
this.resetPasswordKey = unsignedSharedKey;
|
||||
}
|
||||
}
|
||||
|
||||
export class OrganizationUserResetPasswordWithIdRequest extends OrganizationUserResetPasswordEnrollmentRequest {
|
||||
|
||||
@@ -218,9 +218,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
publicKey,
|
||||
);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(
|
||||
encryptedUserKey.toUnsignedSharedKey(),
|
||||
);
|
||||
resetRequest.masterPasswordHash = masterPasswordHash;
|
||||
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
|
||||
|
||||
return this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
this.orgId,
|
||||
|
||||
@@ -15,14 +15,20 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import {
|
||||
EncString,
|
||||
UnsignedSharedKey,
|
||||
} from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordAuthenticationData } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { firstValueFromOrThrow } from "@bitwarden/common/key-management/utils";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
@@ -52,11 +58,10 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
const {
|
||||
newMasterKey,
|
||||
newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash,
|
||||
newPassword,
|
||||
newPasswordHint,
|
||||
kdfConfig,
|
||||
salt,
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
resetPasswordAutoEnroll,
|
||||
@@ -67,20 +72,21 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
throw new Error(`${key} not found. Could not set password.`);
|
||||
}
|
||||
}
|
||||
if (userId == null) {
|
||||
throw new Error("userId not found. Could not set password.");
|
||||
}
|
||||
if (userType == null) {
|
||||
throw new Error("userType not found. Could not set password.");
|
||||
}
|
||||
|
||||
const masterKeyEncryptedUserKey = await this.makeMasterKeyEncryptedUserKey(
|
||||
newMasterKey,
|
||||
userId,
|
||||
assertNonNullish(userId, "userId", "setInitialPassword");
|
||||
assertNonNullish(userType, "userType", "setInitialPassword");
|
||||
const userKey = await this.keyService.makeUserKeyV1();
|
||||
const authenticationData =
|
||||
await this.masterPasswordService.makeMasterPasswordAuthenticationData(
|
||||
newPassword,
|
||||
kdfConfig,
|
||||
salt,
|
||||
);
|
||||
const unlockData = await this.masterPasswordService.makeMasterPasswordUnlockData(
|
||||
newPassword,
|
||||
kdfConfig,
|
||||
salt,
|
||||
userKey,
|
||||
);
|
||||
if (masterKeyEncryptedUserKey == null || !masterKeyEncryptedUserKey[1].encryptedString) {
|
||||
throw new Error("masterKeyEncryptedUserKey not found. Could not set password.");
|
||||
}
|
||||
|
||||
let keyPair: [string, EncString] | null = null;
|
||||
let keysRequest: KeysRequest | null = null;
|
||||
@@ -111,14 +117,11 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
// Existing key pair
|
||||
keyPair = [
|
||||
existingUserPublicKeyB64,
|
||||
await this.encryptService.wrapDecapsulationKey(
|
||||
existingUserPrivateKey,
|
||||
masterKeyEncryptedUserKey[0],
|
||||
),
|
||||
await this.encryptService.wrapDecapsulationKey(existingUserPrivateKey, userKey),
|
||||
];
|
||||
} else {
|
||||
// New key pair
|
||||
keyPair = await this.keyService.makeKeyPair(masterKeyEncryptedUserKey[0]);
|
||||
keyPair = await this.keyService.makeKeyPair(userKey);
|
||||
}
|
||||
|
||||
if (keyPair == null) {
|
||||
@@ -131,14 +134,12 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
|
||||
}
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
newServerMasterKeyHash,
|
||||
masterKeyEncryptedUserKey[1].encryptedString,
|
||||
const request = SetPasswordRequest.newConstructor(
|
||||
authenticationData,
|
||||
unlockData,
|
||||
newPasswordHint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
kdfConfig.kdfType,
|
||||
kdfConfig.iterations,
|
||||
);
|
||||
|
||||
await this.masterPasswordApiService.setPassword(request);
|
||||
@@ -147,12 +148,7 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
await this.updateAccountDecryptionProperties(
|
||||
newMasterKey,
|
||||
kdfConfig,
|
||||
masterKeyEncryptedUserKey,
|
||||
userId,
|
||||
);
|
||||
await this.updateAccountDecryptionProperties(kdfConfig, userKey, userId);
|
||||
|
||||
/**
|
||||
* Set the private key only for new JIT provisioned users in MP encryption orgs.
|
||||
@@ -165,34 +161,14 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
|
||||
}
|
||||
|
||||
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
|
||||
|
||||
if (resetPasswordAutoEnroll) {
|
||||
await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId);
|
||||
await this.handleResetPasswordAutoEnroll(authenticationData, orgId as OrganizationId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
private async makeMasterKeyEncryptedUserKey(
|
||||
masterKey: MasterKey,
|
||||
userId: UserId,
|
||||
): Promise<[UserKey, EncString]> {
|
||||
let masterKeyEncryptedUserKey: [UserKey, EncString] | null = null;
|
||||
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
|
||||
if (userKey == null) {
|
||||
masterKeyEncryptedUserKey = await this.keyService.makeUserKey(masterKey);
|
||||
} else {
|
||||
masterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey);
|
||||
}
|
||||
|
||||
return masterKeyEncryptedUserKey;
|
||||
}
|
||||
|
||||
private async updateAccountDecryptionProperties(
|
||||
masterKey: MasterKey,
|
||||
kdfConfig: KdfConfig,
|
||||
masterKeyEncryptedUserKey: [UserKey, EncString],
|
||||
userKey: UserKey,
|
||||
userId: UserId,
|
||||
) {
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
@@ -201,13 +177,12 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
userDecryptionOpts.hasMasterPassword = true;
|
||||
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
|
||||
await this.kdfConfigService.setKdfConfig(userId, kdfConfig);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId);
|
||||
await this.keyService.setUserKey(userKey, userId);
|
||||
}
|
||||
|
||||
private async handleResetPasswordAutoEnroll(
|
||||
masterKeyHash: string,
|
||||
orgId: string,
|
||||
authenticationData: MasterPasswordAuthenticationData,
|
||||
orgId: OrganizationId,
|
||||
userId: UserId,
|
||||
) {
|
||||
const organizationKeys = await this.organizationApiService.getKeys(orgId);
|
||||
@@ -219,27 +194,23 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi
|
||||
}
|
||||
|
||||
const orgPublicKey = Utils.fromB64ToArray(organizationKeys.publicKey);
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
|
||||
if (userKey == null) {
|
||||
throw new Error("userKey not found. Could not handle reset password auto enroll.");
|
||||
}
|
||||
const userKey = await firstValueFromOrThrow(this.keyService.userKey$(userId), "userKey");
|
||||
|
||||
// RSA encrypt user key with organization public key
|
||||
const orgPublicKeyEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
|
||||
userKey,
|
||||
orgPublicKey,
|
||||
);
|
||||
const orgPublicKeyEncryptedUserKey: UnsignedSharedKey = (
|
||||
await this.encryptService.encapsulateKeyUnsigned(userKey, orgPublicKey)
|
||||
).toUnsignedSharedKey();
|
||||
|
||||
if (orgPublicKeyEncryptedUserKey == null || !orgPublicKeyEncryptedUserKey.encryptedString) {
|
||||
if (orgPublicKeyEncryptedUserKey == null || !orgPublicKeyEncryptedUserKey) {
|
||||
throw new Error(
|
||||
"orgPublicKeyEncryptedUserKey not found. Could not handle reset password auto enroll.",
|
||||
);
|
||||
}
|
||||
|
||||
const enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
enrollmentRequest.masterPasswordHash = masterKeyHash;
|
||||
enrollmentRequest.resetPasswordKey = orgPublicKeyEncryptedUserKey.encryptedString;
|
||||
const enrollmentRequest = new OrganizationUserResetPasswordEnrollmentRequest(
|
||||
orgPublicKeyEncryptedUserKey,
|
||||
);
|
||||
enrollmentRequest.authenticateWith(authenticationData);
|
||||
|
||||
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
orgId,
|
||||
|
||||
@@ -222,11 +222,10 @@ export class SetInitialPasswordComponent implements OnInit {
|
||||
|
||||
try {
|
||||
const credentials: SetInitialPasswordCredentials = {
|
||||
newMasterKey: passwordInputResult.newMasterKey,
|
||||
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
|
||||
newPassword: passwordInputResult.newPassword,
|
||||
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||
kdfConfig: passwordInputResult.kdfConfig,
|
||||
salt: passwordInputResult.salt,
|
||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||
orgId: this.orgId,
|
||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MasterPasswordSalt } from "@bitwarden/common/key-management/master-password/types/master-password.types";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
@@ -42,12 +43,12 @@ export const SetInitialPasswordUserType: Readonly<{
|
||||
}> = Object.freeze(_SetInitialPasswordUserType);
|
||||
|
||||
export interface SetInitialPasswordCredentials {
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
newPassword: string;
|
||||
newPasswordHint: string;
|
||||
kdfConfig: KdfConfig;
|
||||
salt: MasterPasswordSalt;
|
||||
orgSsoIdentifier: string;
|
||||
// Note: This should be refactored to be a OrganizationId
|
||||
orgId: string;
|
||||
resetPasswordAutoEnroll: boolean;
|
||||
}
|
||||
|
||||
@@ -163,9 +163,10 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
|
||||
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(
|
||||
encryptedUserKey.toUnsignedSharedKey(),
|
||||
);
|
||||
resetRequest.masterPasswordHash = masterKeyHash;
|
||||
resetRequest.resetPasswordKey = encryptedUserKey.encryptedString;
|
||||
|
||||
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
orgId,
|
||||
|
||||
@@ -11,10 +11,7 @@ export class SecretVerificationRequest {
|
||||
/**
|
||||
* Mutates this request to include the master password authentication data, to authenticate the request.
|
||||
*/
|
||||
authenticateWith(
|
||||
masterPasswordAuthenticationData: MasterPasswordAuthenticationData,
|
||||
): SecretVerificationRequest {
|
||||
authenticateWith(masterPasswordAuthenticationData: MasterPasswordAuthenticationData) {
|
||||
this.masterPasswordHash = masterPasswordAuthenticationData.masterPasswordAuthenticationHash;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +57,10 @@ export class PasswordResetEnrollmentServiceImplementation
|
||||
// RSA Encrypt user's userKey.key with organization public key
|
||||
const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(userKey, orgPublicKey);
|
||||
|
||||
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
|
||||
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
|
||||
|
||||
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
organizationId,
|
||||
userId,
|
||||
resetRequest,
|
||||
new OrganizationUserResetPasswordEnrollmentRequest(encryptedKey.toUnsignedSharedKey()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify, Opaque } from "type-fest";
|
||||
|
||||
import { UnsignedSharedKey as SdkUnsignedSharedkey } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { EncryptionType, EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE } from "../../../platform/enums";
|
||||
import { Encrypted } from "../../../platform/interfaces/encrypted";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
@@ -47,6 +49,11 @@ export class EncString implements Encrypted {
|
||||
return this.encryptedString;
|
||||
}
|
||||
|
||||
// Note: Unsigned shared key should be split out of EncString completely. This currently lacks type-safety.
|
||||
toUnsignedSharedKey(): UnsignedSharedKey {
|
||||
return this.encryptedString as unknown as UnsignedSharedKey;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.encryptedString as string;
|
||||
}
|
||||
@@ -215,4 +222,14 @@ export class EncString implements Encrypted {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EncryptedString is symmetrically encrypted
|
||||
* Note: This will eventually be replaced by the SDK version of EncryptedString.
|
||||
*/
|
||||
export type EncryptedString = Opaque<string, "EncString">;
|
||||
|
||||
/**
|
||||
* UnsignedSharedKey is a key, encrypted with a public key, but not signed. The sender cannot be authenticated.
|
||||
* Note: This will eventually be replaced by the SDK version of UnsignedSharedKey.
|
||||
*/
|
||||
export type UnsignedSharedKey = Opaque<SdkUnsignedSharedkey, "UnsignedSharedKey">;
|
||||
|
||||
@@ -155,6 +155,10 @@ export abstract class KeyService {
|
||||
* @returns A new user key and the master key protected version of it
|
||||
*/
|
||||
abstract makeUserKey(masterKey: MasterKey | null): Promise<[UserKey, EncString]>;
|
||||
/**
|
||||
* Generates a new v1 user key
|
||||
*/
|
||||
abstract makeUserKeyV1(): Promise<UserKey>;
|
||||
/**
|
||||
* Clears the user's stored version of the user key
|
||||
* @param keySuffix The desired version of the key to clear
|
||||
@@ -208,6 +212,7 @@ export abstract class KeyService {
|
||||
): Promise<string>;
|
||||
/**
|
||||
* Compares the provided master password to the stored password hash.
|
||||
* @deprecated
|
||||
* @param masterPassword The user's master password
|
||||
* @param key The user's master key
|
||||
* @param userId The id of the user to do the operation for.
|
||||
|
||||
@@ -235,6 +235,11 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return this.buildProtectedSymmetricKey(masterKey, newUserKey);
|
||||
}
|
||||
|
||||
async makeUserKeyV1(): Promise<UserKey> {
|
||||
const key = await this.keyGenerationService.createKey(512);
|
||||
return key as UserKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key
|
||||
* @param userId The desired user
|
||||
|
||||
Reference in New Issue
Block a user