forked from trent_larson/crowd-funder-for-time-pwa
Merge branch 'master' into active_did_redux
This commit is contained in:
@@ -95,7 +95,18 @@ export default class UserNameDialog extends Vue {
|
||||
*/
|
||||
async onClickSaveChanges() {
|
||||
try {
|
||||
await this.$updateSettings({ firstName: this.givenName });
|
||||
// Get the current active DID to save to user-specific settings
|
||||
const settings = await this.$accountSettings();
|
||||
const activeDid = settings.activeDid;
|
||||
|
||||
if (activeDid) {
|
||||
// Save to user-specific settings for the current identity
|
||||
await this.$saveUserSettings(activeDid, { firstName: this.givenName });
|
||||
} else {
|
||||
// Fallback to master settings if no active DID
|
||||
await this.$saveSettings({ firstName: this.givenName });
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
this.callback(this.givenName);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1689,3 +1689,11 @@ export const NOTIFY_CONTACTS_ADDED_CONFIRM = {
|
||||
title: "They're Added To Your List",
|
||||
message: "Would you like to go to the main page now?",
|
||||
};
|
||||
|
||||
// ImportAccountView.vue specific constants
|
||||
// Used in: ImportAccountView.vue (onImportClick method - duplicate account warning)
|
||||
export const NOTIFY_DUPLICATE_ACCOUNT_IMPORT = {
|
||||
title: "Account Already Imported",
|
||||
message:
|
||||
"This account has already been imported. Please use a different seed phrase or check your existing accounts.",
|
||||
};
|
||||
|
||||
134
src/libs/util.ts
134
src/libs/util.ts
@@ -614,58 +614,65 @@ export const retrieveAllAccountsMetadata = async (): Promise<
|
||||
return result;
|
||||
};
|
||||
|
||||
export const DUPLICATE_ACCOUNT_ERROR = "Cannot import duplicate account.";
|
||||
|
||||
/**
|
||||
* Saves a new identity to both SQL and Dexie databases
|
||||
* Saves a new identity to SQL database
|
||||
*/
|
||||
export async function saveNewIdentity(
|
||||
identity: IIdentifier,
|
||||
mnemonic: string,
|
||||
derivationPath: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// add to the new sql db
|
||||
const platformService = await getPlatformService();
|
||||
// add to the new sql db
|
||||
const platformService = await getPlatformService();
|
||||
|
||||
const secrets = await platformService.dbQuery(
|
||||
`SELECT secretBase64 FROM secret`,
|
||||
// Check if account already exists before attempting to save
|
||||
const existingAccount = await platformService.dbQuery(
|
||||
"SELECT did FROM accounts WHERE did = ?",
|
||||
[identity.did],
|
||||
);
|
||||
|
||||
if (existingAccount?.values?.length) {
|
||||
throw new Error(
|
||||
`Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`,
|
||||
);
|
||||
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
||||
throw new Error(
|
||||
"No initial encryption supported. We recommend you clear your data and start over.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const secretBase64 = secrets.values[0][0] as string;
|
||||
const secrets = await platformService.dbQuery(
|
||||
`SELECT secretBase64 FROM secret`,
|
||||
);
|
||||
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
||||
throw new Error(
|
||||
"No initial encryption supported. We recommend you clear your data and start over.",
|
||||
);
|
||||
}
|
||||
|
||||
const secret = base64ToArrayBuffer(secretBase64);
|
||||
const identityStr = JSON.stringify(identity);
|
||||
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
|
||||
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
||||
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
||||
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
||||
const secretBase64 = secrets.values[0][0] as string;
|
||||
|
||||
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||
const params = [
|
||||
new Date().toISOString(),
|
||||
derivationPath,
|
||||
identity.did,
|
||||
encryptedIdentityBase64,
|
||||
encryptedMnemonicBase64,
|
||||
identity.keys[0].publicKeyHex,
|
||||
];
|
||||
await platformService.dbExec(sql, params);
|
||||
const secret = base64ToArrayBuffer(secretBase64);
|
||||
const identityStr = JSON.stringify(identity);
|
||||
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
|
||||
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
||||
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
||||
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
||||
|
||||
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||
const params = [
|
||||
new Date().toISOString(),
|
||||
derivationPath,
|
||||
identity.did,
|
||||
encryptedIdentityBase64,
|
||||
encryptedMnemonicBase64,
|
||||
identity.keys[0].publicKeyHex,
|
||||
];
|
||||
await platformService.dbExec(sql, params);
|
||||
|
||||
// Update active identity in the active_identity table instead of settings
|
||||
await platformService.updateActiveDid(identity.did);
|
||||
|
||||
await platformService.insertNewDidIntoSettings(identity.did);
|
||||
} catch (error) {
|
||||
logger.error("Failed to update default settings:", error);
|
||||
throw new Error(
|
||||
"Failed to set default settings. Please try again or restart the app.",
|
||||
);
|
||||
}
|
||||
await platformService.insertNewDidIntoSettings(identity.did);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1034,3 +1041,58 @@ export async function importFromMnemonic(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an account with the given DID already exists in the database
|
||||
*
|
||||
* @param did - The DID to check for duplicates
|
||||
* @returns Promise<boolean> - True if account already exists, false otherwise
|
||||
* @throws Error if database query fails
|
||||
*/
|
||||
export async function checkForDuplicateAccount(did: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Checks if an account with the given DID already exists in the database
|
||||
*
|
||||
* @param mnemonic - The mnemonic phrase to derive DID from
|
||||
* @param derivationPath - The derivation path to use
|
||||
* @returns Promise<boolean> - True if account already exists, false otherwise
|
||||
* @throws Error if database query fails
|
||||
*/
|
||||
export async function checkForDuplicateAccount(
|
||||
mnemonic: string,
|
||||
derivationPath: string,
|
||||
): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Implementation of checkForDuplicateAccount with overloaded signatures
|
||||
*/
|
||||
export async function checkForDuplicateAccount(
|
||||
didOrMnemonic: string,
|
||||
derivationPath?: string,
|
||||
): Promise<boolean> {
|
||||
let didToCheck: string;
|
||||
|
||||
if (derivationPath) {
|
||||
// Derive the DID from mnemonic and derivation path
|
||||
const [address, privateHex, publicHex] = deriveAddress(
|
||||
didOrMnemonic.trim().toLowerCase(),
|
||||
derivationPath,
|
||||
);
|
||||
|
||||
const newId = newIdentifier(address, privateHex, publicHex, derivationPath);
|
||||
didToCheck = newId.did;
|
||||
} else {
|
||||
// Use the provided DID directly
|
||||
didToCheck = didOrMnemonic;
|
||||
}
|
||||
|
||||
// Check if an account with this DID already exists
|
||||
const platformService = await getPlatformService();
|
||||
const existingAccount = await platformService.dbQuery(
|
||||
"SELECT did FROM accounts WHERE did = ?",
|
||||
[didToCheck],
|
||||
);
|
||||
|
||||
return (existingAccount?.values?.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
@@ -71,22 +71,22 @@
|
||||
contactFromDid?.seesMe && contactFromDid.did !== activeDid
|
||||
"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="They can see you"
|
||||
title="They can see your activity"
|
||||
@click="confirmSetVisibility(contactFromDid, false)"
|
||||
>
|
||||
<font-awesome icon="eye" class="fa-fw" />
|
||||
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||
<font-awesome icon="eye" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="
|
||||
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
|
||||
"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="They cannot see you"
|
||||
title="They cannot see your activity"
|
||||
@click="confirmSetVisibility(contactFromDid, true)"
|
||||
>
|
||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -95,11 +95,11 @@
|
||||
contactFromDid.did !== activeDid
|
||||
"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="I view their content"
|
||||
title="You watch their activity"
|
||||
@click="confirmViewContent(contactFromDid, false)"
|
||||
>
|
||||
<font-awesome icon="eye" class="fa-fw" />
|
||||
<font-awesome icon="arrow-down" class="fa-fw" />
|
||||
<font-awesome icon="eye" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="
|
||||
@@ -107,11 +107,11 @@
|
||||
contactFromDid?.did !== activeDid
|
||||
"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="I do not view their content"
|
||||
title="You do not watch their activity"
|
||||
@click="confirmViewContent(contactFromDid, true)"
|
||||
>
|
||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||
<font-awesome icon="arrow-down" class="fa-fw" />
|
||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
@@ -319,8 +319,9 @@
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>
|
||||
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
|
||||
click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
|
||||
Beware that this will erase your existing contact & settings.
|
||||
click Advanced, and follow the instructions to "Import Contacts".
|
||||
(There is currently no way to import other settings, so you'll have to recreate
|
||||
by hand your search area, filters, etc.)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -336,14 +337,18 @@
|
||||
|
||||
<h2 class="text-xl font-semibold">How do I erase my data from my device?</h2>
|
||||
<p>
|
||||
Before doing this, you may want to back up your data with the instructions above.
|
||||
Before doing this, you should back up your data with the instructions above.
|
||||
Note that this does not erase data sent to our servers (see contact info below)
|
||||
</p>
|
||||
<ul>
|
||||
<li class="list-disc list-outside ml-4">
|
||||
Mobile
|
||||
<ul>
|
||||
<li class="list-disc list-outside ml-4">
|
||||
Home Screen: hold down on the icon, and choose to delete it
|
||||
App Store app: hold down on the icon, then uninstall it
|
||||
</li>
|
||||
<li class="list-disc list-outside ml-4">
|
||||
Home Screen PWA: hold down on the icon, and delete it
|
||||
</li>
|
||||
<li class="list-disc list-outside ml-4">
|
||||
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
|
||||
@@ -415,15 +420,6 @@
|
||||
different page.
|
||||
</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">
|
||||
Where do I get help with notifications?
|
||||
</h2>
|
||||
<p>
|
||||
<router-link class="text-blue-500" to="/help-notifications"
|
||||
>Here.</router-link
|
||||
>
|
||||
</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">
|
||||
This app is misbehaving, like showing me a blank screen or failing to show my personal data.
|
||||
What can I do?
|
||||
@@ -434,10 +430,13 @@
|
||||
</p>
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>
|
||||
Drag down on the screen to refresh it; do that multiple times, because
|
||||
For mobile apps, make sure you're connected to the internet.
|
||||
</li>
|
||||
<li>
|
||||
For PWAs, drag down on the screen to refresh it; do that multiple times, because
|
||||
it sometimes takes multiple tries for the app to refresh to the latest version.
|
||||
You can see the version information at the bottom of this page; the best
|
||||
way to determine the latest version is to open this page in an incognito/private
|
||||
way to determine the latest version is to open TimeSafari.app in an incognito/private
|
||||
browser window and look at the version there.
|
||||
</li>
|
||||
<li>
|
||||
@@ -468,9 +467,6 @@
|
||||
</ul>
|
||||
Then reload Time Safari.
|
||||
</li>
|
||||
<li>
|
||||
Restart your device.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you still have problems, you can clear the cache (see "erase my data" above)
|
||||
@@ -508,16 +504,12 @@
|
||||
</p>
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>
|
||||
If using notifications, a server stores push token data. That can be revoked at any time
|
||||
by disabling notifications on the Profile <font-awesome icon="circle-user" class="fa-fw" /> page.
|
||||
</li>
|
||||
<li>
|
||||
If sending images, a server stores them, too. They can be removed by editing the claim
|
||||
and deleting them.
|
||||
If sending images, a server stores them. They can be removed by editing each claim
|
||||
and deleting the image.
|
||||
</li>
|
||||
<li>
|
||||
If sending other partner system data (eg. to Trustroots) a public key and message
|
||||
data are stored on a server. Those can be removed via direct personal request.
|
||||
data are stored on a server. Those can be removed via direct personal request (via contact below).
|
||||
</li>
|
||||
<li>
|
||||
For all other claim data,
|
||||
|
||||
@@ -88,9 +88,15 @@ import { Router } from "vue-router";
|
||||
|
||||
import { AppString, NotificationIface } from "../constants/app";
|
||||
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
|
||||
import { retrieveAccountCount, importFromMnemonic } from "../libs/util";
|
||||
import {
|
||||
retrieveAccountCount,
|
||||
importFromMnemonic,
|
||||
checkForDuplicateAccount,
|
||||
DUPLICATE_ACCOUNT_ERROR,
|
||||
} from "../libs/util";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from "@/constants/notifications";
|
||||
|
||||
/**
|
||||
* Import Account View Component
|
||||
@@ -198,6 +204,19 @@ export default class ImportAccountView extends Vue {
|
||||
}
|
||||
|
||||
try {
|
||||
// Check for duplicate account before importing
|
||||
const isDuplicate = await checkForDuplicateAccount(
|
||||
this.mnemonic,
|
||||
this.derivationPath,
|
||||
);
|
||||
if (isDuplicate) {
|
||||
this.notify.warning(
|
||||
NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await importFromMnemonic(
|
||||
this.mnemonic,
|
||||
this.derivationPath,
|
||||
@@ -223,9 +242,20 @@ export default class ImportAccountView extends Vue {
|
||||
this.$router.push({ name: "account" });
|
||||
} catch (error: unknown) {
|
||||
this.$logError("Import failed: " + error);
|
||||
|
||||
// Check if this is a duplicate account error from saveNewIdentity
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage.includes(DUPLICATE_ACCOUNT_ERROR)) {
|
||||
this.notify.warning(
|
||||
NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.notify.error(
|
||||
(error instanceof Error ? error.message : String(error)) ||
|
||||
"Failed to import account.",
|
||||
errorMessage || "Failed to import account.",
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ import {
|
||||
retrieveAllAccountsMetadata,
|
||||
retrieveFullyDecryptedAccount,
|
||||
saveNewIdentity,
|
||||
checkForDuplicateAccount,
|
||||
} from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||
@@ -171,6 +172,16 @@ export default class ImportAccountView extends Vue {
|
||||
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
||||
|
||||
try {
|
||||
// Check for duplicate account before creating
|
||||
const isDuplicate = await checkForDuplicateAccount(newId.did);
|
||||
if (isDuplicate) {
|
||||
this.notify.warning(
|
||||
"This derived account already exists. Please try a different derivation path.",
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await saveNewIdentity(newId, mne, newDerivPath);
|
||||
|
||||
// record that as the active DID
|
||||
|
||||
@@ -110,10 +110,22 @@ export default class NewEditAccountView extends Vue {
|
||||
* @async
|
||||
*/
|
||||
async onClickSaveChanges() {
|
||||
await this.$updateSettings({
|
||||
firstName: this.givenName,
|
||||
lastName: "", // deprecated, pre v 0.1.3
|
||||
});
|
||||
// Get the current active DID to save to user-specific settings
|
||||
const settings = await this.$accountSettings();
|
||||
const activeDid = settings.activeDid;
|
||||
|
||||
if (activeDid) {
|
||||
// Save to user-specific settings for the current identity
|
||||
await this.$saveUserSettings(activeDid, {
|
||||
firstName: this.givenName,
|
||||
});
|
||||
} else {
|
||||
// Fallback to master settings if no active DID
|
||||
await this.$saveSettings({
|
||||
firstName: this.givenName,
|
||||
});
|
||||
}
|
||||
|
||||
this.$router.back();
|
||||
}
|
||||
|
||||
|
||||
@@ -69,10 +69,17 @@
|
||||
<div v-if="claimCountWithHidden > 0" class="border-b border-slate-300 pb-2">
|
||||
<span>
|
||||
{{ claimCountWithHiddenText }}
|
||||
so if you expected but do not see details from someone then ask them to
|
||||
check that their activity is visible to you on their Contacts
|
||||
<font-awesome icon="users" class="text-slate-500" />
|
||||
page.
|
||||
If you don't see expected info above for someone, ask them to check that
|
||||
their activity is visible to you (
|
||||
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||
<font-awesome icon="eye" class="fa-fw" />
|
||||
) on
|
||||
<a
|
||||
class="text-blue-500 underline cursor-pointer"
|
||||
@click="copyContactsLinkToClipboard"
|
||||
>
|
||||
this page </a
|
||||
>.
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="claimCountByUser > 0" class="border-b border-slate-300 pb-2">
|
||||
@@ -120,10 +127,11 @@ import { DateTime } from "luxon";
|
||||
import * as R from "ramda";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { NotificationIface, APP_SERVER } from "../constants/app";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import {
|
||||
GenericCredWrapper,
|
||||
@@ -148,6 +156,7 @@ import {
|
||||
NOTIFY_ALL_CONFIRMATIONS_ERROR,
|
||||
NOTIFY_GIVE_SEND_ERROR,
|
||||
NOTIFY_CLAIMS_SEND_ERROR,
|
||||
NOTIFY_COPIED_TO_CLIPBOARD,
|
||||
createConfirmationSuccessMessage,
|
||||
createCombinedSuccessMessage,
|
||||
} from "@/constants/notifications";
|
||||
@@ -195,8 +204,8 @@ export default class QuickActionBvcEndView extends Vue {
|
||||
get claimCountWithHiddenText() {
|
||||
if (this.claimCountWithHidden === 0) return "";
|
||||
return this.claimCountWithHidden === 1
|
||||
? "There is 1 other claim with hidden details,"
|
||||
: `There are ${this.claimCountWithHidden} other claims with hidden details,`;
|
||||
? "There is 1 other claim with hidden details."
|
||||
: `There are ${this.claimCountWithHidden} other claims with hidden details.`;
|
||||
}
|
||||
|
||||
get claimCountByUserText() {
|
||||
@@ -296,6 +305,25 @@ export default class QuickActionBvcEndView extends Vue {
|
||||
(this.$router as Router).push(route);
|
||||
}
|
||||
|
||||
copyContactsLinkToClipboard() {
|
||||
const deepLinkUrl = `${APP_SERVER}/deep-link/did/${this.activeDid}`;
|
||||
useClipboard()
|
||||
.copy(deepLinkUrl)
|
||||
.then(() => {
|
||||
this.notify.success(
|
||||
NOTIFY_COPIED_TO_CLIPBOARD.message("Your info link"),
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Failed to copy to clipboard:", error);
|
||||
this.notify.error(
|
||||
"Failed to copy link to clipboard. Please try again.",
|
||||
TIMEOUTS.SHORT,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async record() {
|
||||
try {
|
||||
if (this.claimsToConfirmSelected.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user