mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 16:44:56 +00:00
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "disabled" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
provider = "postgresql"
|
||||
@@ -21,6 +21,7 @@ model User {
|
||||
/// Indicate whether the user finished the signup progress.
|
||||
/// for example, the value will be false if user never registered and invited into a workspace by others.
|
||||
registered Boolean @default(true)
|
||||
disabled Boolean @default(false)
|
||||
|
||||
features UserFeature[]
|
||||
userStripeCustomer UserStripeCustomer?
|
||||
@@ -130,8 +131,8 @@ model WorkspaceDoc {
|
||||
mode Int @default(0) @db.SmallInt
|
||||
// Whether the doc is blocked
|
||||
blocked Boolean @default(false)
|
||||
title String? @db.VarChar
|
||||
summary String? @db.VarChar
|
||||
title String? @db.VarChar
|
||||
summary String? @db.VarChar
|
||||
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -647,16 +648,16 @@ enum NotificationLevel {
|
||||
}
|
||||
|
||||
model Notification {
|
||||
id String @id @default(uuid()) @db.VarChar
|
||||
userId String @map("user_id") @db.VarChar
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
level NotificationLevel
|
||||
read Boolean @default(false)
|
||||
type NotificationType
|
||||
body Json @db.JsonB
|
||||
id String @id @default(uuid()) @db.VarChar
|
||||
userId String @map("user_id") @db.VarChar
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
level NotificationLevel
|
||||
read Boolean @default(false)
|
||||
type NotificationType
|
||||
body Json @db.JsonB
|
||||
|
||||
user User @relation(name: "user_notifications", fields: [userId], references: [id], onDelete: Cascade)
|
||||
user User @relation(name: "user_notifications", fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
// for user notifications list, including read and unread, ordered by createdAt
|
||||
@@index([userId, createdAt, read])
|
||||
|
||||
@@ -214,9 +214,9 @@ test('should delete userSession success by userId', async t => {
|
||||
data: {},
|
||||
});
|
||||
await t.context.session.createOrRefreshUserSession(user.id, session.id);
|
||||
let count = await t.context.session.deleteUserSession(user.id);
|
||||
let count = await t.context.session.deleteUserSessions(user.id);
|
||||
t.is(count, 1);
|
||||
count = await t.context.session.deleteUserSession(user.id);
|
||||
count = await t.context.session.deleteUserSessions(user.id);
|
||||
t.is(count, 0);
|
||||
});
|
||||
|
||||
@@ -228,7 +228,7 @@ test('should delete userSession success by userId and sessionId', async t => {
|
||||
data: {},
|
||||
});
|
||||
await t.context.session.createOrRefreshUserSession(user.id, session.id);
|
||||
const count = await t.context.session.deleteUserSession(user.id, session.id);
|
||||
const count = await t.context.session.deleteUserSessions(user.id, session.id);
|
||||
t.is(count, 1);
|
||||
});
|
||||
|
||||
@@ -240,7 +240,7 @@ test('should delete userSession fail when sessionId not match', async t => {
|
||||
data: {},
|
||||
});
|
||||
await t.context.session.createOrRefreshUserSession(user.id, session.id);
|
||||
const count = await t.context.session.deleteUserSession(
|
||||
const count = await t.context.session.deleteUserSessions(
|
||||
user.id,
|
||||
'not-exists-session-id'
|
||||
);
|
||||
|
||||
@@ -181,21 +181,6 @@ test('should get workspace user by id', async t => {
|
||||
t.is(workspaceUser!.email, user.email);
|
||||
});
|
||||
|
||||
test('should get workspace user by email', async t => {
|
||||
const user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
});
|
||||
|
||||
const workspaceUser = await t.context.user.getWorkspaceUserByEmail(
|
||||
user.email
|
||||
);
|
||||
|
||||
t.not(workspaceUser, null);
|
||||
t.is(workspaceUser!.id, user.id);
|
||||
t.true(!('password' in workspaceUser!));
|
||||
t.is(workspaceUser!.email, user.email);
|
||||
});
|
||||
|
||||
test('should get user by email', async t => {
|
||||
const user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
@@ -305,6 +290,7 @@ test('should paginate users', async t => {
|
||||
name: `test-paginate-${i}`,
|
||||
email: `test-paginate-${i}@affine.pro`,
|
||||
createdAt: new Date(now + i),
|
||||
disabled: i % 2 === 0,
|
||||
})
|
||||
)
|
||||
);
|
||||
@@ -317,14 +303,94 @@ test('should paginate users', async t => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should check if user exists', async t => {
|
||||
// #region disabled user
|
||||
test('should not get disabled user by default', async t => {
|
||||
const user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
const user2 = await t.context.user.get(user.id);
|
||||
const user3 = await t.context.user.getPublicUser(user.id);
|
||||
const user4 = await t.context.user.getPublicUserByEmail(user.email);
|
||||
const userList1 = await t.context.user.getPublicUsers([user.id]);
|
||||
const user5 = await t.context.user.getWorkspaceUser(user.id);
|
||||
const userList2 = await t.context.user.getWorkspaceUsers([user.id]);
|
||||
|
||||
t.is(user2, null);
|
||||
t.is(user3, null);
|
||||
t.is(user4, null);
|
||||
t.is(user5, null);
|
||||
t.is(userList1.length, 0);
|
||||
t.is(userList2.length, 0);
|
||||
});
|
||||
|
||||
test('should get disabled user `withDisabled`', async t => {
|
||||
const user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
const user2 = await t.context.user.get(user.id, { withDisabled: true });
|
||||
const user3 = await t.context.user.getUserByEmail(user.email, {
|
||||
withDisabled: true,
|
||||
});
|
||||
|
||||
t.is(user2!.id, user.id);
|
||||
t.is(user3!.id, user.id);
|
||||
});
|
||||
|
||||
test('should not be able to update email to disabled user', async t => {
|
||||
const user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
disabled: false,
|
||||
});
|
||||
const user2 = await t.context.user.create({
|
||||
email: 'test2@affine.pro',
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
await t.throwsAsync(
|
||||
t.context.user.update(user.id, {
|
||||
email: user2.email,
|
||||
}),
|
||||
{
|
||||
instanceOf: EmailAlreadyUsed,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('should ban user', async t => {
|
||||
const user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
});
|
||||
t.true(await t.context.user.exists(user.id));
|
||||
t.false(await t.context.user.exists('non-existing-user'));
|
||||
const event = t.context.module.get(EventBus);
|
||||
const spy = Sinon.spy();
|
||||
event.on('user.deleted', spy);
|
||||
|
||||
await t.context.user.ban(user.id);
|
||||
|
||||
t.true(spy.calledOnce);
|
||||
const user2 = await t.context.user.get(user.id);
|
||||
t.is(user2, null);
|
||||
});
|
||||
|
||||
test('should enable user', async t => {
|
||||
const user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
const user2 = await t.context.user.enable(user.id);
|
||||
|
||||
t.is(user2.disabled, false);
|
||||
|
||||
const user3 = await t.context.user.get(user.id);
|
||||
t.is(user3!.id, user.id);
|
||||
});
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region ConnectedAccount
|
||||
|
||||
test('should create, get, update, delete connected account', async t => {
|
||||
|
||||
@@ -10,11 +10,11 @@ import type { CurrentUser } from './session';
|
||||
export function sessionUser(
|
||||
user: Pick<
|
||||
User,
|
||||
'id' | 'email' | 'avatarUrl' | 'name' | 'emailVerifiedAt'
|
||||
'id' | 'email' | 'avatarUrl' | 'name' | 'emailVerifiedAt' | 'disabled'
|
||||
> & { password?: string | null }
|
||||
): CurrentUser {
|
||||
// use pick to avoid unexpected fields
|
||||
return assign(pick(user, 'id', 'email', 'avatarUrl', 'name'), {
|
||||
return assign(pick(user, 'id', 'email', 'avatarUrl', 'name', 'disabled'), {
|
||||
hasPassword: user.password !== null,
|
||||
emailVerified: user.emailVerifiedAt !== null,
|
||||
});
|
||||
@@ -105,7 +105,7 @@ export class AuthService implements OnApplicationBootstrap {
|
||||
if (!userId) {
|
||||
await this.models.session.deleteSession(sessionId);
|
||||
} else {
|
||||
await this.models.session.deleteUserSession(userId, sessionId);
|
||||
await this.models.session.deleteUserSessions(userId, sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ export class AuthService implements OnApplicationBootstrap {
|
||||
}
|
||||
|
||||
async revokeUserSessions(userId: string) {
|
||||
return await this.models.session.deleteUserSession(userId);
|
||||
return await this.models.session.deleteUserSessions(userId);
|
||||
}
|
||||
|
||||
getSessionOptionsFromRequest(req: Request) {
|
||||
|
||||
@@ -45,7 +45,7 @@ export const CurrentUser = createParamDecorator(
|
||||
);
|
||||
|
||||
export interface CurrentUser
|
||||
extends Pick<User, 'id' | 'email' | 'avatarUrl' | 'name'> {
|
||||
extends Pick<User, 'id' | 'email' | 'avatarUrl' | 'name' | 'disabled'> {
|
||||
hasPassword: boolean | null;
|
||||
emailVerified: boolean;
|
||||
}
|
||||
|
||||
@@ -202,7 +202,9 @@ export class UserManagementResolver {
|
||||
description: 'Get user by id',
|
||||
})
|
||||
async getUser(@Args('id') id: string) {
|
||||
const user = await this.models.user.get(id);
|
||||
const user = await this.models.user.get(id, {
|
||||
withDisabled: true,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
@@ -217,7 +219,9 @@ export class UserManagementResolver {
|
||||
nullable: true,
|
||||
})
|
||||
async getUserByEmail(@Args('email') email: string) {
|
||||
const user = await this.models.user.getUserByEmail(email);
|
||||
const user = await this.models.user.getUserByEmail(email, {
|
||||
withDisabled: true,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
@@ -256,7 +260,7 @@ export class UserManagementResolver {
|
||||
}
|
||||
|
||||
@Mutation(() => UserType, {
|
||||
description: 'Update a user',
|
||||
description: 'Update an user',
|
||||
})
|
||||
async updateUser(
|
||||
@Args('id') id: string,
|
||||
@@ -282,4 +286,18 @@ export class UserManagementResolver {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => UserType, {
|
||||
description: 'Ban an user',
|
||||
})
|
||||
async banUser(@Args('id') id: string): Promise<UserType> {
|
||||
return sessionUser(await this.models.user.ban(id));
|
||||
}
|
||||
|
||||
@Mutation(() => UserType, {
|
||||
description: 'Reenable an banned user',
|
||||
})
|
||||
async enableUser(@Args('id') id: string): Promise<UserType> {
|
||||
return sessionUser(await this.models.user.enable(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ export class UserType implements CurrentUser {
|
||||
nullable: true,
|
||||
})
|
||||
createdAt?: Date | null;
|
||||
|
||||
@Field(() => Boolean, {
|
||||
description: 'User is disabled',
|
||||
})
|
||||
disabled!: boolean;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
||||
@@ -144,6 +144,14 @@ export class DocUserModel extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteByUserId(userId: string) {
|
||||
await this.db.workspaceDocUserRole.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getOwner(workspaceId: string, docId: string) {
|
||||
return await this.db.workspaceDocUserRole.findFirst({
|
||||
where: {
|
||||
|
||||
@@ -123,7 +123,7 @@ export class SessionModel extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUserSession(userId: string, sessionId?: string) {
|
||||
async deleteUserSessions(userId: string, sessionId?: string) {
|
||||
const { count } = await this.db.userSession.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
@@ -131,7 +131,7 @@ export class SessionModel extends BaseModel {
|
||||
},
|
||||
});
|
||||
this.logger.log(
|
||||
`Deleted user session success by userId: ${userId} and sessionId: ${sessionId}`
|
||||
`Deleted user sessions success by userId: ${userId} and sessionId: ${sessionId}`
|
||||
);
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Transactional } from '@nestjs-cls/transactional';
|
||||
import { type ConnectedAccount, Prisma, type User } from '@prisma/client';
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import {
|
||||
CryptoHelper,
|
||||
@@ -49,6 +51,10 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
interface UserFilter {
|
||||
withDisabled?: boolean;
|
||||
}
|
||||
|
||||
export type PublicUser = Pick<User, keyof typeof publicUserSelect>;
|
||||
export type WorkspaceUser = Pick<User, keyof typeof workspaceUserSelect>;
|
||||
export type { ConnectedAccount, User };
|
||||
@@ -62,52 +68,49 @@ export class UserModel extends BaseModel {
|
||||
super();
|
||||
}
|
||||
|
||||
async get(id: string) {
|
||||
async get(id: string, filter: UserFilter = {}) {
|
||||
return this.db.user.findUnique({
|
||||
where: { id },
|
||||
where: { id, disabled: filter.withDisabled ? undefined : false },
|
||||
});
|
||||
}
|
||||
|
||||
async exists(id: string) {
|
||||
const count = await this.db.user.count({
|
||||
where: { id },
|
||||
});
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
async getPublicUser(id: string): Promise<PublicUser | null> {
|
||||
return this.db.user.findUnique({
|
||||
select: publicUserSelect,
|
||||
where: { id },
|
||||
where: { id, disabled: false },
|
||||
});
|
||||
}
|
||||
|
||||
async getPublicUsers(ids: string[]): Promise<PublicUser[]> {
|
||||
return this.db.user.findMany({
|
||||
select: publicUserSelect,
|
||||
where: { id: { in: ids } },
|
||||
where: { id: { in: ids }, disabled: false },
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceUser(id: string): Promise<WorkspaceUser | null> {
|
||||
return this.db.user.findUnique({
|
||||
select: workspaceUserSelect,
|
||||
where: { id },
|
||||
where: { id, disabled: false },
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkspaceUsers(ids: string[]): Promise<WorkspaceUser[]> {
|
||||
return this.db.user.findMany({
|
||||
select: workspaceUserSelect,
|
||||
where: { id: { in: ids } },
|
||||
where: { id: { in: ids }, disabled: false },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | null> {
|
||||
async getUserByEmail(
|
||||
email: string,
|
||||
filter: UserFilter = {}
|
||||
): Promise<User | null> {
|
||||
const rows = await this.db.$queryRaw<User[]>`
|
||||
SELECT id, name, email, password, registered, email_verified as emailVerifiedAt, avatar_url as avatarUrl, registered, created_at as createdAt
|
||||
FROM "users"
|
||||
WHERE lower("email") = lower(${email})
|
||||
${Prisma.raw(filter.withDisabled ? '' : 'AND disabled = false')}
|
||||
`;
|
||||
|
||||
return rows[0] ?? null;
|
||||
@@ -141,23 +144,14 @@ export class UserModel extends BaseModel {
|
||||
SELECT id, name, avatar_url as avatarUrl
|
||||
FROM "users"
|
||||
WHERE lower("email") = lower(${email})
|
||||
`;
|
||||
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
async getWorkspaceUserByEmail(email: string): Promise<WorkspaceUser | null> {
|
||||
const rows = await this.db.$queryRaw<WorkspaceUser[]>`
|
||||
SELECT id, name, email, avatar_url as avatarUrl
|
||||
FROM "users"
|
||||
WHERE lower("email") = lower(${email})
|
||||
AND disabled = false
|
||||
`;
|
||||
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
async create(data: CreateUserInput) {
|
||||
let user = await this.getUserByEmail(data.email);
|
||||
let user = await this.getUserByEmail(data.email, { withDisabled: true });
|
||||
|
||||
if (user) {
|
||||
throw new EmailAlreadyUsed();
|
||||
@@ -183,13 +177,16 @@ export class UserModel extends BaseModel {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async update(id: string, data: UpdateUserInput) {
|
||||
if (data.password) {
|
||||
data.password = await this.crypto.encryptPassword(data.password);
|
||||
}
|
||||
|
||||
if (data.email) {
|
||||
const user = await this.getUserByEmail(data.email);
|
||||
const user = await this.getUserByEmail(data.email, {
|
||||
withDisabled: true,
|
||||
});
|
||||
if (user && user.id !== id) {
|
||||
throw new EmailAlreadyUsed();
|
||||
}
|
||||
@@ -211,7 +208,7 @@ export class UserModel extends BaseModel {
|
||||
* When user created by others invitation, we will leave it as unregistered.
|
||||
*/
|
||||
async fulfill(email: string, data: Omit<UpdateUserInput, 'email'> = {}) {
|
||||
const user = await this.getUserByEmail(email);
|
||||
const user = await this.getUserByEmail(email, { withDisabled: true });
|
||||
|
||||
if (!user) {
|
||||
return this.create({
|
||||
@@ -258,6 +255,30 @@ export class UserModel extends BaseModel {
|
||||
return user;
|
||||
}
|
||||
|
||||
async ban(id: string) {
|
||||
// ban an user barely share the same logic with delete an user,
|
||||
// but keep the record with `disabled` flag
|
||||
// we delete the account and create it again to trigger all cleanups
|
||||
let user = await this.delete(id);
|
||||
user = await this.db.user.create({
|
||||
data: {
|
||||
...omit(user, 'id'),
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
await this.event.emitAsync('user.postCreated', user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async enable(id: string) {
|
||||
return await this.db.user.update({
|
||||
where: { id },
|
||||
data: { disabled: false },
|
||||
});
|
||||
}
|
||||
|
||||
async pagination(skip: number = 0, take: number = 20, after?: Date) {
|
||||
return this.db.user.findMany({
|
||||
where: {
|
||||
|
||||
@@ -196,6 +196,14 @@ export class WorkspaceUserModel extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteByUserId(userId: string) {
|
||||
await this.db.workspaceUserRole.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async get(workspaceId: string, userId: string) {
|
||||
return await this.db.workspaceUserRole.findUnique({
|
||||
where: {
|
||||
|
||||
@@ -574,7 +574,7 @@ export class SubscriptionService implements OnApplicationBootstrap {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await this.models.user.getWorkspaceUserByEmail(customer.email);
|
||||
const user = await this.models.user.getUserByEmail(customer.email);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
@@ -620,9 +620,7 @@ export class SubscriptionService implements OnApplicationBootstrap {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await this.models.user.getWorkspaceUserByEmail(
|
||||
invoice.customer_email
|
||||
);
|
||||
const user = await this.models.user.getUserByEmail(invoice.customer_email);
|
||||
|
||||
return {
|
||||
userId: user?.id,
|
||||
|
||||
@@ -618,6 +618,9 @@ type InviteUserType {
|
||||
"""User email verified"""
|
||||
createdAt: DateTime @deprecated(reason: "useless")
|
||||
|
||||
"""User is disabled"""
|
||||
disabled: Boolean
|
||||
|
||||
"""User email"""
|
||||
email: String
|
||||
|
||||
@@ -767,6 +770,9 @@ type Mutation {
|
||||
addContextDoc(options: AddContextDocInput!): [CopilotContextListItem!]!
|
||||
addWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean!
|
||||
approveMember(userId: String!, workspaceId: String!): Boolean!
|
||||
|
||||
"""Ban an user"""
|
||||
banUser(id: String!): UserType!
|
||||
cancelSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType!
|
||||
changeEmail(email: String!, token: String!): UserType!
|
||||
changePassword(newPassword: String!, token: String!, userId: String): Boolean!
|
||||
@@ -810,6 +816,9 @@ type Mutation {
|
||||
deleteUser(id: String!): DeleteAccount!
|
||||
deleteWorkspace(id: String!): Boolean!
|
||||
|
||||
"""Reenable an banned user"""
|
||||
enableUser(id: String!): UserType!
|
||||
|
||||
"""Create a chat session"""
|
||||
forkCopilotSession(options: ForkChatSessionInput!): String!
|
||||
generateLicenseKey(sessionId: String!): String!
|
||||
@@ -864,7 +873,7 @@ type Mutation {
|
||||
updateRuntimeConfigs(updates: JSONObject!): [ServerRuntimeConfigType!]!
|
||||
updateSubscriptionRecurring(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, recurring: SubscriptionRecurring!, workspaceId: String): SubscriptionType!
|
||||
|
||||
"""Update a user"""
|
||||
"""Update an user"""
|
||||
updateUser(id: String!, input: ManageUserInput!): UserType!
|
||||
|
||||
"""update user enabled feature"""
|
||||
@@ -1359,6 +1368,9 @@ type UserType {
|
||||
"""User email verified"""
|
||||
createdAt: DateTime @deprecated(reason: "useless")
|
||||
|
||||
"""User is disabled"""
|
||||
disabled: Boolean!
|
||||
|
||||
"""User email"""
|
||||
email: String!
|
||||
|
||||
|
||||
Reference in New Issue
Block a user