feat(server): add users management api (#7092)

This commit is contained in:
forehalo
2024-05-29 08:02:26 +00:00
parent 0dfcc97c52
commit bffbb1ea9f
4 changed files with 127 additions and 9 deletions

View File

@@ -39,6 +39,12 @@ export interface AuthRuntimeConfigurations {
* Whether allow anonymous users to sign up
*/
allowSignup: boolean;
/**
* Whether require email verification before access restricted resources
*/
requireEmailVerification: boolean;
/**
* The minimum and maximum length of the password when registering new users
*/
@@ -70,6 +76,10 @@ defineRuntimeConfig('auth', {
desc: 'Whether allow new registrations',
default: true,
},
requireEmailVerification: {
desc: 'Whether require email verification before accessing restricted resources',
default: true,
},
'password.min': {
desc: 'The minimum length of user password',
default: 8,

View File

@@ -4,13 +4,13 @@ import {
Context,
Int,
Mutation,
Parent,
Query,
registerEnumType,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { CurrentUser } from '../auth/current-user';
import { sessionUser } from '../auth/service';
import { Admin } from '../common';
import { UserService } from '../user/service';
@@ -33,7 +33,7 @@ export class FeatureManagementResolver {
name: 'features',
description: 'Enabled features of a user',
})
async userFeatures(@CurrentUser() user: CurrentUser) {
async userFeatures(@Parent() user: UserType) {
return this.feature.getActivatedUserFeatures(user.id);
}

View File

@@ -1,6 +1,8 @@
import { BadRequestException } from '@nestjs/common';
import {
Args,
Field,
InputType,
Int,
Mutation,
Query,
@@ -11,11 +13,12 @@ import { PrismaClient } from '@prisma/client';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { isNil, omitBy } from 'lodash-es';
import type { FileUpload } from '../../fundamentals';
import { EventEmitter, Throttle } from '../../fundamentals';
import type { Config, CryptoHelper, FileUpload } from '../../fundamentals';
import { Throttle } from '../../fundamentals';
import { CurrentUser } from '../auth/current-user';
import { Public } from '../auth/guard';
import { sessionUser } from '../auth/service';
import { Admin } from '../common';
import { AvatarStorage } from '../storage';
import { validators } from '../utils/validators';
import { UserService } from './service';
@@ -32,8 +35,7 @@ export class UserResolver {
constructor(
private readonly prisma: PrismaClient,
private readonly storage: AvatarStorage,
private readonly users: UserService,
private readonly event: EventEmitter
private readonly users: UserService
) {}
@Throttle('strict')
@@ -132,8 +134,113 @@ export class UserResolver {
async deleteAccount(
@CurrentUser() user: CurrentUser
): Promise<DeleteAccount> {
const deletedUser = await this.users.deleteUser(user.id);
this.event.emit('user.deleted', deletedUser);
await this.users.deleteUser(user.id);
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 };
}
}

View File

@@ -170,7 +170,8 @@ export class UserService {
}
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')