mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-23 21:38:44 +08:00
feat(server): handle account deleting properly (#12399)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Users are now prevented from deleting their account if they own one or more team workspaces. A clear error message instructs users to transfer ownership or delete those workspaces first. - Disabled (banned) users are explicitly prevented from signing in or re-registering. - Added new error messages and translations to improve clarity around account deletion restrictions. - **Bug Fixes** - Disabled users are now explicitly handled to prevent sign-in attempts. - **Tests** - Introduced comprehensive end-to-end tests covering account deletion, banning, and re-registration scenarios. - **Chores** - Improved event handling for user deletion and subscription cancellation. - Updated localization resources with new error messages. - Renamed payment event handler class for clarity. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -154,12 +154,10 @@ export class TestingApp extends NestApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async login(user: MockedUser) {
|
async login(user: MockedUser) {
|
||||||
await this.POST('/api/auth/sign-in')
|
return await this.POST('/api/auth/sign-in').send({
|
||||||
.send({
|
email: user.email,
|
||||||
email: user.email,
|
password: user.password,
|
||||||
password: user.password,
|
});
|
||||||
})
|
|
||||||
.expect(200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchUser(userOrId: string | { id: string }) {
|
async switchUser(userOrId: string | { id: string }) {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import {
|
||||||
|
deleteAccountMutation,
|
||||||
|
disableUserMutation,
|
||||||
|
getCurrentUserQuery,
|
||||||
|
getWorkspaceQuery,
|
||||||
|
} from '@affine/graphql';
|
||||||
|
|
||||||
|
import { app, e2e, Mockers } from '../test';
|
||||||
|
|
||||||
|
const admin = await app.create(Mockers.User, {
|
||||||
|
feature: 'administrator',
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should be able to delete account', async t => {
|
||||||
|
const user = await app.signup();
|
||||||
|
const user2 = await app.create(Mockers.User);
|
||||||
|
const ws = await app.create(Mockers.Workspace, {
|
||||||
|
owner: user,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.create(Mockers.WorkspaceUser, {
|
||||||
|
workspaceId: ws.id,
|
||||||
|
userId: user2.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.gql({
|
||||||
|
query: deleteAccountMutation,
|
||||||
|
});
|
||||||
|
|
||||||
|
// assert session removed
|
||||||
|
const { currentUser } = await app.gql({
|
||||||
|
query: getCurrentUserQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(currentUser, null);
|
||||||
|
|
||||||
|
// assert login failed
|
||||||
|
const res = await app.login(user);
|
||||||
|
t.is(res.status, 400);
|
||||||
|
t.like(res.body, {
|
||||||
|
message: `Wrong user email or password: ${user.email}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// assert workspace access deleted
|
||||||
|
await app.login(user2);
|
||||||
|
await t.throwsAsync(
|
||||||
|
app.gql({
|
||||||
|
query: getWorkspaceQuery,
|
||||||
|
variables: {
|
||||||
|
id: ws.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message: `You do not have permission to access Space ${ws.id}.`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should not delete account if is owner of team workspace', async t => {
|
||||||
|
const user = await app.signup();
|
||||||
|
const ws = await app.create(Mockers.Workspace, {
|
||||||
|
owner: user,
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.create(Mockers.TeamWorkspace, {
|
||||||
|
id: ws.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.throwsAsync(
|
||||||
|
app.gql({
|
||||||
|
query: deleteAccountMutation,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Cannot delete account. You are the owner of one or more team workspaces. Please transfer ownership or delete them first.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should register deleted account again', async t => {
|
||||||
|
const user = await app.signup();
|
||||||
|
await app.gql({
|
||||||
|
query: deleteAccountMutation,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await app.POST('/api/auth/sign-in').send({
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
t.is(res.status, 200);
|
||||||
|
t.like(await app.mails.waitFor('SignUp'), {
|
||||||
|
to: user.email,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should ban account', async t => {
|
||||||
|
const user = await app.create(Mockers.User);
|
||||||
|
|
||||||
|
await app.login(admin);
|
||||||
|
|
||||||
|
const { banUser } = await app.gql({
|
||||||
|
query: disableUserMutation,
|
||||||
|
variables: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(banUser.disabled, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should not login banned account', async t => {
|
||||||
|
const user = await app.create(Mockers.User);
|
||||||
|
|
||||||
|
await app.login(admin);
|
||||||
|
|
||||||
|
await app.gql({
|
||||||
|
query: disableUserMutation,
|
||||||
|
variables: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await app.logout();
|
||||||
|
|
||||||
|
const res = await app.login(user);
|
||||||
|
t.is(res.status, 400);
|
||||||
|
t.like(res.body, {
|
||||||
|
message: `Wrong user email or password: ${user.email}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
e2e('should not signup banned account', async t => {
|
||||||
|
const user = await app.create(Mockers.User);
|
||||||
|
|
||||||
|
await app.login(admin);
|
||||||
|
|
||||||
|
await app.gql({
|
||||||
|
query: disableUserMutation,
|
||||||
|
variables: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await app.POST('/api/auth/sign-in').send({
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(res.status, 400);
|
||||||
|
t.like(res.body, {
|
||||||
|
message: `Wrong user email or password: ${user.email}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker';
|
|||||||
import { hashSync } from '@node-rs/argon2';
|
import { hashSync } from '@node-rs/argon2';
|
||||||
import type { Prisma, User } from '@prisma/client';
|
import type { Prisma, User } from '@prisma/client';
|
||||||
|
|
||||||
import type { UserFeatureName } from '../../models';
|
import { FeatureConfigs, type UserFeatureName } from '../../models';
|
||||||
import { Mocker } from './factory';
|
import { Mocker } from './factory';
|
||||||
|
|
||||||
export type MockUserInput = Prisma.UserCreateInput & {
|
export type MockUserInput = Prisma.UserCreateInput & {
|
||||||
@@ -15,33 +15,37 @@ export type MockedUser = Omit<User, 'password'> & {
|
|||||||
|
|
||||||
export class MockUser extends Mocker<MockUserInput, MockedUser> {
|
export class MockUser extends Mocker<MockUserInput, MockedUser> {
|
||||||
override async create(input?: Partial<MockUserInput>) {
|
override async create(input?: Partial<MockUserInput>) {
|
||||||
|
const { feature, ...userInput } = input ?? {};
|
||||||
const password = input?.password ?? faker.internet.password();
|
const password = input?.password ?? faker.internet.password();
|
||||||
const user = await this.db.user.create({
|
const user = await this.db.user.create({
|
||||||
data: {
|
data: {
|
||||||
email: faker.internet.email(),
|
email: faker.internet.email(),
|
||||||
name: faker.person.fullName(),
|
name: faker.person.fullName(),
|
||||||
password: password ? hashSync(password) : undefined,
|
password: password ? hashSync(password) : undefined,
|
||||||
...input,
|
...userInput,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input?.feature) {
|
if (feature) {
|
||||||
const feature = await this.db.feature.findFirst({
|
const featureRecord = await this.db.feature.findFirst({
|
||||||
where: {
|
where: {
|
||||||
name: input.feature,
|
name: feature,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!feature) {
|
if (!featureRecord) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Feature ${input.feature} does not exist in DB. You might forgot to run data-migration first.`
|
`Feature ${feature} does not exist in DB. You might forgot to run data-migration first.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = FeatureConfigs[feature];
|
||||||
await this.db.userFeature.create({
|
await this.db.userFeature.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
featureId: feature.id,
|
featureId: featureRecord.id,
|
||||||
|
name: feature,
|
||||||
|
type: config.type,
|
||||||
reason: 'test',
|
reason: 'test',
|
||||||
activated: true,
|
activated: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -792,10 +792,17 @@ export const USER_FRIENDLY_ERRORS = {
|
|||||||
type: 'action_forbidden',
|
type: 'action_forbidden',
|
||||||
message: 'Cannot delete all admin accounts.',
|
message: 'Cannot delete all admin accounts.',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Account errors
|
||||||
cannot_delete_own_account: {
|
cannot_delete_own_account: {
|
||||||
type: 'action_forbidden',
|
type: 'action_forbidden',
|
||||||
message: 'Cannot delete own account.',
|
message: 'Cannot delete own account.',
|
||||||
},
|
},
|
||||||
|
cannot_delete_account_with_owned_team_workspace: {
|
||||||
|
type: 'action_forbidden',
|
||||||
|
message:
|
||||||
|
'Cannot delete account. You are the owner of one or more team workspaces. Please transfer ownership or delete them first.',
|
||||||
|
},
|
||||||
|
|
||||||
// captcha errors
|
// captcha errors
|
||||||
captcha_verification_failed: {
|
captcha_verification_failed: {
|
||||||
|
|||||||
@@ -908,6 +908,12 @@ export class CannotDeleteOwnAccount extends UserFriendlyError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CannotDeleteAccountWithOwnedTeamWorkspace extends UserFriendlyError {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super('action_forbidden', 'cannot_delete_account_with_owned_team_workspace', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class CaptchaVerificationFailed extends UserFriendlyError {
|
export class CaptchaVerificationFailed extends UserFriendlyError {
|
||||||
constructor(message?: string) {
|
constructor(message?: string) {
|
||||||
super('bad_request', 'captcha_verification_failed', message);
|
super('bad_request', 'captcha_verification_failed', message);
|
||||||
@@ -1145,6 +1151,7 @@ export enum ErrorNames {
|
|||||||
MAILER_SERVICE_IS_NOT_CONFIGURED,
|
MAILER_SERVICE_IS_NOT_CONFIGURED,
|
||||||
CANNOT_DELETE_ALL_ADMIN_ACCOUNT,
|
CANNOT_DELETE_ALL_ADMIN_ACCOUNT,
|
||||||
CANNOT_DELETE_OWN_ACCOUNT,
|
CANNOT_DELETE_OWN_ACCOUNT,
|
||||||
|
CANNOT_DELETE_ACCOUNT_WITH_OWNED_TEAM_WORKSPACE,
|
||||||
CAPTCHA_VERIFICATION_FAILED,
|
CAPTCHA_VERIFICATION_FAILED,
|
||||||
INVALID_LICENSE_SESSION_ID,
|
INVALID_LICENSE_SESSION_ID,
|
||||||
LICENSE_REVEALED,
|
LICENSE_REVEALED,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
Throttle,
|
Throttle,
|
||||||
URLHelper,
|
URLHelper,
|
||||||
UseNamedGuard,
|
UseNamedGuard,
|
||||||
|
WrongSignInCredentials,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
import { Models, TokenType } from '../../models';
|
import { Models, TokenType } from '../../models';
|
||||||
import { validators } from '../utils/validators';
|
import { validators } from '../utils/validators';
|
||||||
@@ -162,7 +163,10 @@ export class AuthController {
|
|||||||
clientNonce?: string
|
clientNonce?: string
|
||||||
) {
|
) {
|
||||||
// send email magic link
|
// send email magic link
|
||||||
const user = await this.models.user.getUserByEmail(email);
|
const user = await this.models.user.getUserByEmail(email, {
|
||||||
|
withDisabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
if (!this.config.auth.allowSignup) {
|
if (!this.config.auth.allowSignup) {
|
||||||
throw new SignUpForbidden();
|
throw new SignUpForbidden();
|
||||||
@@ -191,6 +195,8 @@ export class AuthController {
|
|||||||
throw new InvalidEmail({ email });
|
throw new InvalidEmail({ email });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (user.disabled) {
|
||||||
|
throw new WrongSignInCredentials({ email });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ttlInSec = 30 * 60;
|
const ttlInSec = 30 * 60;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export class DocEventsListener {
|
|||||||
@OnEvent('user.deleted')
|
@OnEvent('user.deleted')
|
||||||
async clearUserWorkspaces(payload: Events['user.deleted']) {
|
async clearUserWorkspaces(payload: Events['user.deleted']) {
|
||||||
for (const workspace of payload.ownedWorkspaces) {
|
for (const workspace of payload.ownedWorkspaces) {
|
||||||
|
await this.models.workspace.delete(workspace);
|
||||||
await this.workspace.deleteSpace(workspace);
|
await this.workspace.deleteSpace(workspace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { type ConnectedAccount, Prisma, type User } from '@prisma/client';
|
|||||||
import { omit } from 'lodash-es';
|
import { omit } from 'lodash-es';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CannotDeleteAccountWithOwnedTeamWorkspace,
|
||||||
CryptoHelper,
|
CryptoHelper,
|
||||||
EmailAlreadyUsed,
|
EmailAlreadyUsed,
|
||||||
EventBus,
|
EventBus,
|
||||||
|
UserNotFound,
|
||||||
WrongSignInCredentials,
|
WrongSignInCredentials,
|
||||||
WrongSignInMethod,
|
WrongSignInMethod,
|
||||||
} from '../base';
|
} from '../base';
|
||||||
@@ -217,6 +219,10 @@ export class UserModel extends BaseModel {
|
|||||||
...data,
|
...data,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (user.disabled) {
|
||||||
|
throw new UserNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
if (user.registered) {
|
if (user.registered) {
|
||||||
delete data.registered;
|
delete data.registered;
|
||||||
} else {
|
} else {
|
||||||
@@ -237,13 +243,25 @@ export class UserModel extends BaseModel {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ownedWorkspaces(id: string) {
|
||||||
|
return await this.models.workspaceUser.getUserActiveRoles(id, {
|
||||||
|
role: WorkspaceRole.Owner,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async delete(id: string) {
|
async delete(id: string) {
|
||||||
const ownedWorkspaces = await this.models.workspaceUser.getUserActiveRoles(
|
const ownedWorkspaces = await this.ownedWorkspaces(id);
|
||||||
id,
|
|
||||||
{
|
for (const ws of ownedWorkspaces) {
|
||||||
role: WorkspaceRole.Owner,
|
const isTeamWorkspace = await this.models.workspace.isTeamWorkspace(
|
||||||
|
ws.workspaceId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTeamWorkspace) {
|
||||||
|
throw new CannotDeleteAccountWithOwnedTeamWorkspace();
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
const user = await this.db.user.delete({ where: { id } });
|
const user = await this.db.user.delete({ where: { id } });
|
||||||
|
|
||||||
this.event.emit('user.deleted', {
|
this.event.emit('user.deleted', {
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import { Models } from '../../models';
|
|||||||
import { SubscriptionPlan } from './types';
|
import { SubscriptionPlan } from './types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QuotaOverride {
|
export class PaymentEventHandlers {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspace: WorkspaceService,
|
private readonly workspace: WorkspaceService,
|
||||||
private readonly models: Models,
|
private readonly models: Models,
|
||||||
@@ -11,13 +11,13 @@ import { UserModule } from '../../core/user';
|
|||||||
import { WorkspaceModule } from '../../core/workspaces';
|
import { WorkspaceModule } from '../../core/workspaces';
|
||||||
import { StripeWebhookController } from './controller';
|
import { StripeWebhookController } from './controller';
|
||||||
import { SubscriptionCronJobs } from './cron';
|
import { SubscriptionCronJobs } from './cron';
|
||||||
|
import { PaymentEventHandlers } from './event';
|
||||||
import { LicenseController } from './license/controller';
|
import { LicenseController } from './license/controller';
|
||||||
import {
|
import {
|
||||||
SelfhostTeamSubscriptionManager,
|
SelfhostTeamSubscriptionManager,
|
||||||
UserSubscriptionManager,
|
UserSubscriptionManager,
|
||||||
WorkspaceSubscriptionManager,
|
WorkspaceSubscriptionManager,
|
||||||
} from './manager';
|
} from './manager';
|
||||||
import { QuotaOverride } from './quota';
|
|
||||||
import {
|
import {
|
||||||
SubscriptionResolver,
|
SubscriptionResolver,
|
||||||
UserSubscriptionResolver,
|
UserSubscriptionResolver,
|
||||||
@@ -49,7 +49,7 @@ import { StripeWebhook } from './webhook';
|
|||||||
SelfhostTeamSubscriptionManager,
|
SelfhostTeamSubscriptionManager,
|
||||||
SubscriptionCronJobs,
|
SubscriptionCronJobs,
|
||||||
WorkspaceSubscriptionResolver,
|
WorkspaceSubscriptionResolver,
|
||||||
QuotaOverride,
|
PaymentEventHandlers,
|
||||||
],
|
],
|
||||||
controllers: [StripeWebhookController, LicenseController],
|
controllers: [StripeWebhookController, LicenseController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
InternalServerError,
|
InternalServerError,
|
||||||
InvalidCheckoutParameters,
|
InvalidCheckoutParameters,
|
||||||
Mutex,
|
Mutex,
|
||||||
|
OnEvent,
|
||||||
SubscriptionAlreadyExists,
|
SubscriptionAlreadyExists,
|
||||||
SubscriptionPlanNotFound,
|
SubscriptionPlanNotFound,
|
||||||
TooManyRequest,
|
TooManyRequest,
|
||||||
@@ -683,4 +684,17 @@ export class UserSubscriptionManager extends SubscriptionManager {
|
|||||||
throw new Error('user should exists for stripe subscription or invoice.');
|
throw new Error('user should exists for stripe subscription or invoice.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent('user.deleted')
|
||||||
|
async onUserDeleted({ id }: Events['user.deleted']) {
|
||||||
|
const subscription = await this.db.subscription.findFirst({
|
||||||
|
where: {
|
||||||
|
targetId: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subscription?.stripeSubscriptionId) {
|
||||||
|
await this.stripe.subscriptions.cancel(subscription.stripeSubscriptionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -534,6 +534,7 @@ enum ErrorNames {
|
|||||||
BAD_REQUEST
|
BAD_REQUEST
|
||||||
BLOB_NOT_FOUND
|
BLOB_NOT_FOUND
|
||||||
BLOB_QUOTA_EXCEEDED
|
BLOB_QUOTA_EXCEEDED
|
||||||
|
CANNOT_DELETE_ACCOUNT_WITH_OWNED_TEAM_WORKSPACE
|
||||||
CANNOT_DELETE_ALL_ADMIN_ACCOUNT
|
CANNOT_DELETE_ALL_ADMIN_ACCOUNT
|
||||||
CANNOT_DELETE_OWN_ACCOUNT
|
CANNOT_DELETE_OWN_ACCOUNT
|
||||||
CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION
|
CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION
|
||||||
|
|||||||
@@ -706,6 +706,7 @@ export enum ErrorNames {
|
|||||||
BAD_REQUEST = 'BAD_REQUEST',
|
BAD_REQUEST = 'BAD_REQUEST',
|
||||||
BLOB_NOT_FOUND = 'BLOB_NOT_FOUND',
|
BLOB_NOT_FOUND = 'BLOB_NOT_FOUND',
|
||||||
BLOB_QUOTA_EXCEEDED = 'BLOB_QUOTA_EXCEEDED',
|
BLOB_QUOTA_EXCEEDED = 'BLOB_QUOTA_EXCEEDED',
|
||||||
|
CANNOT_DELETE_ACCOUNT_WITH_OWNED_TEAM_WORKSPACE = 'CANNOT_DELETE_ACCOUNT_WITH_OWNED_TEAM_WORKSPACE',
|
||||||
CANNOT_DELETE_ALL_ADMIN_ACCOUNT = 'CANNOT_DELETE_ALL_ADMIN_ACCOUNT',
|
CANNOT_DELETE_ALL_ADMIN_ACCOUNT = 'CANNOT_DELETE_ALL_ADMIN_ACCOUNT',
|
||||||
CANNOT_DELETE_OWN_ACCOUNT = 'CANNOT_DELETE_OWN_ACCOUNT',
|
CANNOT_DELETE_OWN_ACCOUNT = 'CANNOT_DELETE_OWN_ACCOUNT',
|
||||||
CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION = 'CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION',
|
CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION = 'CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION',
|
||||||
|
|||||||
@@ -8614,6 +8614,10 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Cannot delete own account.`
|
* `Cannot delete own account.`
|
||||||
*/
|
*/
|
||||||
["error.CANNOT_DELETE_OWN_ACCOUNT"](): string;
|
["error.CANNOT_DELETE_OWN_ACCOUNT"](): string;
|
||||||
|
/**
|
||||||
|
* `Cannot delete account. You are the owner of one or more team workspaces. Please transfer ownership or delete them first.`
|
||||||
|
*/
|
||||||
|
["error.CANNOT_DELETE_ACCOUNT_WITH_OWNED_TEAM_WORKSPACE"](): string;
|
||||||
/**
|
/**
|
||||||
* `Captcha verification failed.`
|
* `Captcha verification failed.`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2128,6 +2128,7 @@
|
|||||||
"error.MAILER_SERVICE_IS_NOT_CONFIGURED": "Mailer service is not configured.",
|
"error.MAILER_SERVICE_IS_NOT_CONFIGURED": "Mailer service is not configured.",
|
||||||
"error.CANNOT_DELETE_ALL_ADMIN_ACCOUNT": "Cannot delete all admin accounts.",
|
"error.CANNOT_DELETE_ALL_ADMIN_ACCOUNT": "Cannot delete all admin accounts.",
|
||||||
"error.CANNOT_DELETE_OWN_ACCOUNT": "Cannot delete own account.",
|
"error.CANNOT_DELETE_OWN_ACCOUNT": "Cannot delete own account.",
|
||||||
|
"error.CANNOT_DELETE_ACCOUNT_WITH_OWNED_TEAM_WORKSPACE": "Cannot delete account. You are the owner of one or more team workspaces. Please transfer ownership or delete them first.",
|
||||||
"error.CAPTCHA_VERIFICATION_FAILED": "Captcha verification failed.",
|
"error.CAPTCHA_VERIFICATION_FAILED": "Captcha verification failed.",
|
||||||
"error.INVALID_LICENSE_SESSION_ID": "Invalid session id to generate license key.",
|
"error.INVALID_LICENSE_SESSION_ID": "Invalid session id to generate license key.",
|
||||||
"error.LICENSE_REVEALED": "License key has been revealed. Please check your mail box of the one provided during checkout.",
|
"error.LICENSE_REVEALED": "License key has been revealed. Please check your mail box of the one provided during checkout.",
|
||||||
|
|||||||
Reference in New Issue
Block a user