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