Implement register mutation with validation
This commit is contained in:
parent
bf4a08ac90
commit
099b40e77a
6 changed files with 255 additions and 3 deletions
|
@ -6,6 +6,7 @@ import { AuthenticationComponent } from '@loopback/authentication';
|
|||
import { JWTAuthenticationComponent, UserServiceBindings } from '@loopback/authentication-jwt';
|
||||
import { SzakdolgozatUserService } from './services';
|
||||
import { GraphQLServer } from '@loopback/graphql';
|
||||
import { UserResolver } from './graphql-resolvers/user-resolver';
|
||||
|
||||
export { ApplicationConfig };
|
||||
|
||||
|
@ -18,8 +19,11 @@ export class SzakdolgozatBackendApplication extends BootMixin(
|
|||
super(options);
|
||||
|
||||
const server = this.server(GraphQLServer);
|
||||
this.configure(server.key).to({host: process.env.HOST, port: process.env.PORT});
|
||||
this.getServer(GraphQLServer).then(s => this.gqlServer = s);
|
||||
this.configure(server.key).to({host: process.env.HOST ?? '0.0.0.0', port: process.env.PORT ?? 3000});
|
||||
this.getServer(GraphQLServer).then(s => {
|
||||
this.gqlServer = s;
|
||||
s.resolver(UserResolver);
|
||||
});
|
||||
|
||||
// Authentication
|
||||
this.component(AuthenticationComponent);
|
||||
|
@ -27,5 +31,12 @@ export class SzakdolgozatBackendApplication extends BootMixin(
|
|||
this.service(SzakdolgozatUserService, UserServiceBindings.USER_SERVICE);
|
||||
|
||||
this.projectRoot = __dirname;
|
||||
this.bootOptions = {
|
||||
graphqlResolvers: {
|
||||
dirs: ['graphql-resolvers'],
|
||||
extensions: ['js'],
|
||||
nested: true
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
184
backend/src/graphql-resolvers/user-resolver.ts
Normal file
184
backend/src/graphql-resolvers/user-resolver.ts
Normal file
|
@ -0,0 +1,184 @@
|
|||
import { arg, GraphQLBindings, mutation, query, resolver, ResolverData } from '@loopback/graphql';
|
||||
import { User } from '../models';
|
||||
import { repository } from '@loopback/repository';
|
||||
import { UserRepository } from '../repositories';
|
||||
import { inject } from '@loopback/core';
|
||||
import { SzakdolgozatUserService } from '../services';
|
||||
import { TokenServiceBindings, UserServiceBindings } from '@loopback/authentication-jwt';
|
||||
import { TokenService } from '@loopback/authentication';
|
||||
import { SecurityBindings, UserProfile } from '@loopback/security';
|
||||
import { genSalt, hash } from 'bcryptjs';
|
||||
import { UserRegisterInput } from '../graphql-types/input/user-register.input';
|
||||
import { validated } from '../helpers';
|
||||
|
||||
@resolver(of => User)
|
||||
export class UserResolver {
|
||||
constructor(
|
||||
@repository('UserRepository') private readonly userRepository: UserRepository,
|
||||
@inject(UserServiceBindings.USER_SERVICE) private readonly userService: SzakdolgozatUserService,
|
||||
@inject(GraphQLBindings.RESOLVER_DATA) private readonly resolverData: ResolverData,
|
||||
@inject(TokenServiceBindings.TOKEN_SERVICE) public jwtService: TokenService,
|
||||
@inject(SecurityBindings.USER, {optional: true}) public user: UserProfile
|
||||
) {
|
||||
}
|
||||
|
||||
@mutation(returns => User)
|
||||
async register(@arg('user', validated(UserRegisterInput)) request: UserRegisterInput): Promise<Omit<User, 'password'>> {
|
||||
const password = await hash(request.password, await genSalt());
|
||||
const user = {
|
||||
email: request.email,
|
||||
name: request.name,
|
||||
password,
|
||||
isAdmin: false
|
||||
} as User;
|
||||
return this.userRepository.create(user);
|
||||
}
|
||||
|
||||
@query(returns => User)
|
||||
async test(request: User): Promise<User> {
|
||||
return (await this.userRepository.find())[0];
|
||||
}
|
||||
|
||||
/*@mutation(returns => LoginResult)
|
||||
async login(
|
||||
@requestBody({
|
||||
description: 'The input of login function',
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: getModelSchemaRef(User, {exclude: ['id', 'isAdmin', 'name']})
|
||||
},
|
||||
}
|
||||
}) credentials: Pick<User, 'email' | 'password'>,
|
||||
): Promise<{ token: string, user: Omit<User, 'id' | 'password'> }> {
|
||||
// ensure the user exists, and the password is correct
|
||||
const user = await this.userService.verifyCredentials(credentials);
|
||||
// convert a User object into a UserProfile object (reduced set of properties)
|
||||
const userProfile = this.userService.convertToUserProfile(user);
|
||||
|
||||
// create a JSON Web Token based on the user profile
|
||||
const token = await this.jwtService.generateToken(userProfile);
|
||||
return {token, user};
|
||||
}
|
||||
|
||||
@post('/users/logout', {
|
||||
responses: {
|
||||
'204': {
|
||||
description: 'Logged out',
|
||||
},
|
||||
},
|
||||
})
|
||||
@authenticate('jwt')
|
||||
async logout(@inject(RestBindings.Http.REQUEST) request: Request): Promise<void> {
|
||||
const split = request.headers.authorization?.split(' ');
|
||||
if (split && split.length > 1) {
|
||||
if (this.jwtService.revokeToken) {
|
||||
await this.jwtService.revokeToken(split[1]);
|
||||
} else {
|
||||
console.error('Cannot revoke token');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@get('/users/count')
|
||||
@response(200, {
|
||||
description: 'User model count',
|
||||
content: {'application/json': {schema: CountSchema}},
|
||||
})
|
||||
@authenticate('jwt')
|
||||
async count(
|
||||
@param.where(User) where?: Where<User>,
|
||||
): Promise<Count> {
|
||||
return this.userRepository.count(where);
|
||||
}
|
||||
|
||||
@get('/users')
|
||||
@response(200, {
|
||||
description: 'Array of User model instances',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: getModelSchemaRef(User, {includeRelations: true}),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@authenticate('jwt')
|
||||
async find(
|
||||
@param.filter(User) filter?: Filter<User>,
|
||||
): Promise<User[]> {
|
||||
return this.userRepository.find(filter);
|
||||
}
|
||||
|
||||
@patch('/users')
|
||||
@response(200, {
|
||||
description: 'User PATCH success count',
|
||||
content: {'application/json': {schema: CountSchema}},
|
||||
})
|
||||
@authenticate('jwt')
|
||||
async updateAll(
|
||||
@requestBody({
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: getModelSchemaRef(User, {partial: true}),
|
||||
},
|
||||
},
|
||||
})
|
||||
user: User,
|
||||
@param.where(User) where?: Where<User>,
|
||||
): Promise<Count> {
|
||||
return this.userRepository.updateAll(user, where);
|
||||
}
|
||||
|
||||
@get('/users/{id}')
|
||||
@response(200, {
|
||||
description: 'User model instance',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: getModelSchemaRef(User, {includeRelations: true}),
|
||||
},
|
||||
},
|
||||
})
|
||||
@authenticate('jwt')
|
||||
async findById(
|
||||
@param.path.number('id') id: number,
|
||||
@param.filter(User, {exclude: 'where'}) filter?: FilterExcludingWhere<User>
|
||||
): Promise<User> {
|
||||
return this.userRepository.findById(id, filter);
|
||||
}
|
||||
|
||||
@patch('/users/{id}')
|
||||
@response(204, {
|
||||
description: 'User PATCH success',
|
||||
})
|
||||
@authenticate('jwt')
|
||||
async updateById(
|
||||
@param.path.number('id') id: number,
|
||||
@requestBody({
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: getModelSchemaRef(User, {partial: true}),
|
||||
},
|
||||
},
|
||||
})
|
||||
user: User,
|
||||
): Promise<void> {
|
||||
if (id === +this.user.id) {
|
||||
const loggedInUser = await this.userService.findUserById(this.user.id);
|
||||
if (user.isAdmin !== undefined && loggedInUser.isAdmin !== user.isAdmin) {
|
||||
throw new HttpErrors.BadRequest('Cannot change admin status of self');
|
||||
}
|
||||
}
|
||||
await this.userRepository.updateById(id, user);
|
||||
}
|
||||
|
||||
@del('/users/{id}')
|
||||
@response(204, {
|
||||
description: 'User DELETE success',
|
||||
})
|
||||
@authenticate('jwt')
|
||||
async deleteById(@param.path.number('id') id: number): Promise<void> {
|
||||
await this.userRepository.deleteById(id);
|
||||
}*/
|
||||
}
|
37
backend/src/graphql-types/input/user-register.input.ts
Normal file
37
backend/src/graphql-types/input/user-register.input.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { field, inputType } from '@loopback/graphql';
|
||||
import { User } from '../../models';
|
||||
import { model, property } from '@loopback/repository';
|
||||
|
||||
@model()
|
||||
@inputType()
|
||||
export class UserRegisterInput implements Pick<User, 'email' | 'name' | 'password'> {
|
||||
@property({
|
||||
type: 'string',
|
||||
required: true,
|
||||
jsonSchema: {
|
||||
pattern: /[A-Za-z0-9.+_-]+@[A-Za-z.-_]*(u-szeged.hu)|(szte.hu)/.source
|
||||
}
|
||||
})
|
||||
@field()
|
||||
email: string;
|
||||
@property({
|
||||
type: 'string',
|
||||
required: true,
|
||||
jsonSchema: {
|
||||
pattern: /([A-Za-z-.]+ )+[A-Za-z-.]+/.source
|
||||
}
|
||||
})
|
||||
@field()
|
||||
name: string;
|
||||
@property({
|
||||
type: 'string',
|
||||
required: true,
|
||||
jsonSchema: {
|
||||
minLength: 8,
|
||||
maxLength: 255
|
||||
},
|
||||
hidden: true
|
||||
})
|
||||
@field()
|
||||
password: string;
|
||||
}
|
6
backend/src/graphql-types/user.ts
Normal file
6
backend/src/graphql-types/user.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { User } from '../models';
|
||||
|
||||
export class LoginResult {
|
||||
token: string;
|
||||
user: Omit<User, 'id' | 'password'>;
|
||||
}
|
6
backend/src/helpers.ts
Normal file
6
backend/src/helpers.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { jsonToSchemaObject, modelToJsonSchema, validateValueAgainstSchema } from '@loopback/rest';
|
||||
import { ClassType } from '@loopback/graphql';
|
||||
|
||||
export function validated<T>(type: ClassType) {
|
||||
return {validate: (input: T) => validateValueAgainstSchema(input, jsonToSchemaObject(modelToJsonSchema(type)))};
|
||||
}
|
|
@ -1,14 +1,17 @@
|
|||
import { Entity, model, property, hasMany } from '@loopback/repository';
|
||||
import { Entity, hasMany, model, property } from '@loopback/repository';
|
||||
import { Course } from './course.model';
|
||||
import { CourseUser } from './course-user.model';
|
||||
import { field, objectType } from '@loopback/graphql';
|
||||
|
||||
@model()
|
||||
@objectType()
|
||||
export class User extends Entity {
|
||||
@property({
|
||||
type: 'number',
|
||||
id: true,
|
||||
generated: true,
|
||||
})
|
||||
@field()
|
||||
id?: number;
|
||||
|
||||
@property({
|
||||
|
@ -18,6 +21,7 @@ export class User extends Entity {
|
|||
pattern: /[A-Za-z0-9.+_-]+@[A-Za-z.-_]*(u-szeged.hu)|(szte.hu)/.source
|
||||
}
|
||||
})
|
||||
@field()
|
||||
email: string;
|
||||
|
||||
@property({
|
||||
|
@ -27,6 +31,7 @@ export class User extends Entity {
|
|||
pattern: /([A-Za-z-.]+ )+[A-Za-z-.]+/.source
|
||||
}
|
||||
})
|
||||
@field()
|
||||
name: string;
|
||||
|
||||
@property({
|
||||
|
@ -38,15 +43,18 @@ export class User extends Entity {
|
|||
},
|
||||
hidden: true
|
||||
})
|
||||
@field()
|
||||
password: string;
|
||||
|
||||
@property({
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
})
|
||||
@field()
|
||||
isAdmin: boolean;
|
||||
|
||||
@hasMany(() => Course, {through: {model: () => CourseUser}})
|
||||
//@field()
|
||||
courses: Course[];
|
||||
|
||||
constructor(data?: Partial<User>) {
|
||||
|
|
Loading…
Reference in a new issue