feat(server): add users management api (#7092)
This commit is contained in:
@@ -39,6 +39,12 @@ export interface AuthRuntimeConfigurations {
|
|||||||
* Whether allow anonymous users to sign up
|
* Whether allow anonymous users to sign up
|
||||||
*/
|
*/
|
||||||
allowSignup: boolean;
|
allowSignup: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether require email verification before access restricted resources
|
||||||
|
*/
|
||||||
|
requireEmailVerification: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The minimum and maximum length of the password when registering new users
|
* The minimum and maximum length of the password when registering new users
|
||||||
*/
|
*/
|
||||||
@@ -70,6 +76,10 @@ defineRuntimeConfig('auth', {
|
|||||||
desc: 'Whether allow new registrations',
|
desc: 'Whether allow new registrations',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
requireEmailVerification: {
|
||||||
|
desc: 'Whether require email verification before accessing restricted resources',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
'password.min': {
|
'password.min': {
|
||||||
desc: 'The minimum length of user password',
|
desc: 'The minimum length of user password',
|
||||||
default: 8,
|
default: 8,
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import {
|
|||||||
Context,
|
Context,
|
||||||
Int,
|
Int,
|
||||||
Mutation,
|
Mutation,
|
||||||
|
Parent,
|
||||||
Query,
|
Query,
|
||||||
registerEnumType,
|
registerEnumType,
|
||||||
ResolveField,
|
ResolveField,
|
||||||
Resolver,
|
Resolver,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
|
|
||||||
import { CurrentUser } from '../auth/current-user';
|
|
||||||
import { sessionUser } from '../auth/service';
|
import { sessionUser } from '../auth/service';
|
||||||
import { Admin } from '../common';
|
import { Admin } from '../common';
|
||||||
import { UserService } from '../user/service';
|
import { UserService } from '../user/service';
|
||||||
@@ -33,7 +33,7 @@ export class FeatureManagementResolver {
|
|||||||
name: 'features',
|
name: 'features',
|
||||||
description: 'Enabled features of a user',
|
description: 'Enabled features of a user',
|
||||||
})
|
})
|
||||||
async userFeatures(@CurrentUser() user: CurrentUser) {
|
async userFeatures(@Parent() user: UserType) {
|
||||||
return this.feature.getActivatedUserFeatures(user.id);
|
return this.feature.getActivatedUserFeatures(user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
Args,
|
Args,
|
||||||
|
Field,
|
||||||
|
InputType,
|
||||||
Int,
|
Int,
|
||||||
Mutation,
|
Mutation,
|
||||||
Query,
|
Query,
|
||||||
@@ -11,11 +13,12 @@ import { PrismaClient } from '@prisma/client';
|
|||||||
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
|
||||||
import { isNil, omitBy } from 'lodash-es';
|
import { isNil, omitBy } from 'lodash-es';
|
||||||
|
|
||||||
import type { FileUpload } from '../../fundamentals';
|
import type { Config, CryptoHelper, FileUpload } from '../../fundamentals';
|
||||||
import { EventEmitter, Throttle } from '../../fundamentals';
|
import { Throttle } from '../../fundamentals';
|
||||||
import { CurrentUser } from '../auth/current-user';
|
import { CurrentUser } from '../auth/current-user';
|
||||||
import { Public } from '../auth/guard';
|
import { Public } from '../auth/guard';
|
||||||
import { sessionUser } from '../auth/service';
|
import { sessionUser } from '../auth/service';
|
||||||
|
import { Admin } from '../common';
|
||||||
import { AvatarStorage } from '../storage';
|
import { AvatarStorage } from '../storage';
|
||||||
import { validators } from '../utils/validators';
|
import { validators } from '../utils/validators';
|
||||||
import { UserService } from './service';
|
import { UserService } from './service';
|
||||||
@@ -32,8 +35,7 @@ export class UserResolver {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaClient,
|
private readonly prisma: PrismaClient,
|
||||||
private readonly storage: AvatarStorage,
|
private readonly storage: AvatarStorage,
|
||||||
private readonly users: UserService,
|
private readonly users: UserService
|
||||||
private readonly event: EventEmitter
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Throttle('strict')
|
@Throttle('strict')
|
||||||
@@ -132,8 +134,113 @@ export class UserResolver {
|
|||||||
async deleteAccount(
|
async deleteAccount(
|
||||||
@CurrentUser() user: CurrentUser
|
@CurrentUser() user: CurrentUser
|
||||||
): Promise<DeleteAccount> {
|
): Promise<DeleteAccount> {
|
||||||
const deletedUser = await this.users.deleteUser(user.id);
|
await this.users.deleteUser(user.id);
|
||||||
this.event.emit('user.deleted', deletedUser);
|
return { success: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
class ListUserInput {
|
||||||
|
@Field(() => Int, { nullable: true, defaultValue: 0 })
|
||||||
|
skip!: number;
|
||||||
|
|
||||||
|
@Field(() => Int, { nullable: true, defaultValue: 20 })
|
||||||
|
first!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
class CreateUserInput {
|
||||||
|
@Field(() => String)
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
name!: string | null;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
password!: string | null;
|
||||||
|
|
||||||
|
@Field(() => Boolean, { nullable: true, defaultValue: true })
|
||||||
|
requireEmailVerification!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Admin()
|
||||||
|
@Resolver(() => UserType)
|
||||||
|
export class UserManagementResolver {
|
||||||
|
constructor(
|
||||||
|
private readonly db: PrismaClient,
|
||||||
|
private readonly user: UserService,
|
||||||
|
private readonly crypto: CryptoHelper,
|
||||||
|
private readonly config: Config
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Query(() => [UserType], {
|
||||||
|
description: 'List registered users',
|
||||||
|
})
|
||||||
|
async users(
|
||||||
|
@Args({ name: 'filter', type: () => ListUserInput }) input: ListUserInput
|
||||||
|
): Promise<UserType[]> {
|
||||||
|
const users = await this.db.user.findMany({
|
||||||
|
select: { ...this.user.defaultUserSelect, password: true },
|
||||||
|
skip: input.skip,
|
||||||
|
take: input.first,
|
||||||
|
});
|
||||||
|
|
||||||
|
return users.map(sessionUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(() => UserType, {
|
||||||
|
name: 'userById',
|
||||||
|
description: 'Get user by id',
|
||||||
|
})
|
||||||
|
async getUser(@Args('id') id: string) {
|
||||||
|
const user = await this.db.user.findUnique({
|
||||||
|
select: { ...this.user.defaultUserSelect, password: true },
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => UserType, {
|
||||||
|
description: 'Create a new user',
|
||||||
|
})
|
||||||
|
async createUser(
|
||||||
|
@Args({ name: 'input', type: () => CreateUserInput }) input: CreateUserInput
|
||||||
|
) {
|
||||||
|
validators.assertValidEmail(input.email);
|
||||||
|
if (input.password) {
|
||||||
|
const config = await this.config.runtime.fetchAll({
|
||||||
|
'auth/password.max': true,
|
||||||
|
'auth/password.min': true,
|
||||||
|
});
|
||||||
|
validators.assertValidPassword(input.password, {
|
||||||
|
max: config['auth/password.max'],
|
||||||
|
min: config['auth/password.min'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await this.user.createAnonymousUser(input.email, {
|
||||||
|
password: input.password
|
||||||
|
? await this.crypto.encryptPassword(input.password)
|
||||||
|
: undefined,
|
||||||
|
registered: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// data returned by `createUser` does not satisfies `UserType`
|
||||||
|
return this.getUser(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => DeleteAccount, {
|
||||||
|
description: 'Delete a user account',
|
||||||
|
})
|
||||||
|
async deleteUser(@Args('id') id: string): Promise<DeleteAccount> {
|
||||||
|
await this.user.deleteUser(id);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,8 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(id: string) {
|
async deleteUser(id: string) {
|
||||||
return this.prisma.user.delete({ where: { id } });
|
const user = await this.prisma.user.delete({ where: { id } });
|
||||||
|
this.emitter.emit('user.deleted', user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent('user.updated')
|
@OnEvent('user.updated')
|
||||||
|
|||||||
Reference in New Issue
Block a user