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 { JWTAuthenticationComponent, UserServiceBindings } from '@loopback/authentication-jwt';
|
||||||
import { SzakdolgozatUserService } from './services';
|
import { SzakdolgozatUserService } from './services';
|
||||||
import { GraphQLServer } from '@loopback/graphql';
|
import { GraphQLServer } from '@loopback/graphql';
|
||||||
|
import { UserResolver } from './graphql-resolvers/user-resolver';
|
||||||
|
|
||||||
export { ApplicationConfig };
|
export { ApplicationConfig };
|
||||||
|
|
||||||
|
@ -18,8 +19,11 @@ export class SzakdolgozatBackendApplication extends BootMixin(
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
const server = this.server(GraphQLServer);
|
const server = this.server(GraphQLServer);
|
||||||
this.configure(server.key).to({host: process.env.HOST, port: process.env.PORT});
|
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);
|
this.getServer(GraphQLServer).then(s => {
|
||||||
|
this.gqlServer = s;
|
||||||
|
s.resolver(UserResolver);
|
||||||
|
});
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
this.component(AuthenticationComponent);
|
this.component(AuthenticationComponent);
|
||||||
|
@ -27,5 +31,12 @@ export class SzakdolgozatBackendApplication extends BootMixin(
|
||||||
this.service(SzakdolgozatUserService, UserServiceBindings.USER_SERVICE);
|
this.service(SzakdolgozatUserService, UserServiceBindings.USER_SERVICE);
|
||||||
|
|
||||||
this.projectRoot = __dirname;
|
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 { Course } from './course.model';
|
||||||
import { CourseUser } from './course-user.model';
|
import { CourseUser } from './course-user.model';
|
||||||
|
import { field, objectType } from '@loopback/graphql';
|
||||||
|
|
||||||
@model()
|
@model()
|
||||||
|
@objectType()
|
||||||
export class User extends Entity {
|
export class User extends Entity {
|
||||||
@property({
|
@property({
|
||||||
type: 'number',
|
type: 'number',
|
||||||
id: true,
|
id: true,
|
||||||
generated: true,
|
generated: true,
|
||||||
})
|
})
|
||||||
|
@field()
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
|
@ -18,6 +21,7 @@ export class User extends Entity {
|
||||||
pattern: /[A-Za-z0-9.+_-]+@[A-Za-z.-_]*(u-szeged.hu)|(szte.hu)/.source
|
pattern: /[A-Za-z0-9.+_-]+@[A-Za-z.-_]*(u-szeged.hu)|(szte.hu)/.source
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@field()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
|
@ -27,6 +31,7 @@ export class User extends Entity {
|
||||||
pattern: /([A-Za-z-.]+ )+[A-Za-z-.]+/.source
|
pattern: /([A-Za-z-.]+ )+[A-Za-z-.]+/.source
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@field()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
|
@ -38,15 +43,18 @@ export class User extends Entity {
|
||||||
},
|
},
|
||||||
hidden: true
|
hidden: true
|
||||||
})
|
})
|
||||||
|
@field()
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
@property({
|
@property({
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
required: true,
|
required: true,
|
||||||
})
|
})
|
||||||
|
@field()
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
|
||||||
@hasMany(() => Course, {through: {model: () => CourseUser}})
|
@hasMany(() => Course, {through: {model: () => CourseUser}})
|
||||||
|
//@field()
|
||||||
courses: Course[];
|
courses: Course[];
|
||||||
|
|
||||||
constructor(data?: Partial<User>) {
|
constructor(data?: Partial<User>) {
|
||||||
|
|
Loading…
Reference in a new issue