From 65332d494721b35880edf22e327c31a572283cd6 Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Thu, 21 Apr 2022 01:03:34 +0200 Subject: [PATCH] Implement login, find user, update user (partly) --- .../src/graphql-resolvers/user-resolver.ts | 144 ++++-------------- .../input/user-register.input.ts | 27 +--- .../graphql-types/input/user-update.input.ts | 19 +++ backend/src/graphql-types/list.ts | 17 +++ backend/src/graphql-types/user.ts | 53 ++++++- backend/src/models/user.model.ts | 33 +--- 6 files changed, 126 insertions(+), 167 deletions(-) create mode 100644 backend/src/graphql-types/input/user-update.input.ts create mode 100644 backend/src/graphql-types/list.ts diff --git a/backend/src/graphql-resolvers/user-resolver.ts b/backend/src/graphql-resolvers/user-resolver.ts index 872c5ec..5adabb9 100644 --- a/backend/src/graphql-resolvers/user-resolver.ts +++ b/backend/src/graphql-resolvers/user-resolver.ts @@ -1,4 +1,4 @@ -import { arg, GraphQLBindings, mutation, query, resolver, ResolverData } from '@loopback/graphql'; +import { arg, authorized, GraphQLBindings, Int, mutation, query, resolver, ResolverData } from '@loopback/graphql'; import { User } from '../models'; import { repository } from '@loopback/repository'; import { UserRepository } from '../repositories'; @@ -10,6 +10,8 @@ import { SecurityBindings, UserProfile } from '@loopback/security'; import { genSalt, hash } from 'bcryptjs'; import { UserRegisterInput } from '../graphql-types/input/user-register.input'; import { validated } from '../helpers'; +import { LoginResult } from '../graphql-types/user'; +import { UserUpdateInput } from '../graphql-types/input/user-update.input'; @resolver(of => User) export class UserResolver { @@ -39,20 +41,10 @@ export class UserResolver { 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, - ): Promise<{ token: string, user: Omit }> { + @mutation(returns => LoginResult) + async login(@arg('email') email: string, @arg('password') password: string): Promise { // ensure the user exists, and the password is correct - const user = await this.userService.verifyCredentials(credentials); + const user = await this.userService.verifyCredentials({email, password}); // convert a User object into a UserProfile object (reduced set of properties) const userProfile = this.userService.convertToUserProfile(user); @@ -61,16 +53,11 @@ export class UserResolver { return {token, user}; } - @post('/users/logout', { - responses: { - '204': { - description: 'Logged out', - }, - }, - }) - @authenticate('jwt') - async logout(@inject(RestBindings.Http.REQUEST) request: Request): Promise { - const split = request.headers.authorization?.split(' '); + @authorized() + @mutation(returns => Boolean) + async logout(@inject(GraphQLBindings.RESOLVER_DATA) request: any): Promise { + console.log('request:', request); //TODO + const split = request.headers.get('Authorization')?.split(' '); if (split && split.length > 1) { if (this.jwtService.revokeToken) { await this.jwtService.revokeToken(split[1]); @@ -78,107 +65,38 @@ export class UserResolver { console.error('Cannot revoke token'); } } + return true; } - @get('/users/count') - @response(200, { - description: 'User model count', - content: {'application/json': {schema: CountSchema}}, - }) - @authenticate('jwt') - async count( - @param.where(User) where?: Where, - ): Promise { - return this.userRepository.count(where); + @authorized() + @query(returns => [User]) + async find(user: Partial): Promise { + return this.userRepository.find({}); //TODO } - @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, - ): Promise { - return this.userRepository.find(filter); + @authorized() + @query(returns => User) + async findById(@arg('id', returns => Int) id: number): Promise { + return this.userRepository.findById(id); } - @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, - ): Promise { - 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 - ): Promise { - 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 { - if (id === +this.user.id) { + @authorized() + @mutation(returns => Boolean) + async updateById(@arg('id', returns => Int) id: number, @arg('user') user: UserUpdateInput): Promise { + if (id === +this.user?.id) { //TODO: this.user 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'); + throw new Error('Cannot change admin status of self'); } } await this.userRepository.updateById(id, user); + return true; } - @del('/users/{id}') - @response(204, { - description: 'User DELETE success', - }) - @authenticate('jwt') - async deleteById(@param.path.number('id') id: number): Promise { + @authorized() + @mutation(returns => Boolean) + async deleteById(id: number): Promise { await this.userRepository.deleteById(id); - }*/ + return true; + } } diff --git a/backend/src/graphql-types/input/user-register.input.ts b/backend/src/graphql-types/input/user-register.input.ts index 63fafc0..f8ba22c 100644 --- a/backend/src/graphql-types/input/user-register.input.ts +++ b/backend/src/graphql-types/input/user-register.input.ts @@ -1,37 +1,18 @@ import { field, inputType } from '@loopback/graphql'; import { User } from '../../models'; import { model, property } from '@loopback/repository'; +import { UserProperties } from '../user'; @model() @inputType() export class UserRegisterInput implements Pick { - @property({ - type: 'string', - required: true, - jsonSchema: { - pattern: /[A-Za-z0-9.+_-]+@[A-Za-z.-_]*(u-szeged.hu)|(szte.hu)/.source - } - }) + @property(UserProperties.email) @field() email: string; - @property({ - type: 'string', - required: true, - jsonSchema: { - pattern: /([A-Za-z-.]+ )+[A-Za-z-.]+/.source - } - }) + @property(UserProperties.name) @field() name: string; - @property({ - type: 'string', - required: true, - jsonSchema: { - minLength: 8, - maxLength: 255 - }, - hidden: true - }) + @property(UserProperties.password) @field() password: string; } diff --git a/backend/src/graphql-types/input/user-update.input.ts b/backend/src/graphql-types/input/user-update.input.ts new file mode 100644 index 0000000..ae2386d --- /dev/null +++ b/backend/src/graphql-types/input/user-update.input.ts @@ -0,0 +1,19 @@ +import { User } from '../../models'; +import { field, inputType } from '@loopback/graphql'; +import { UserProperties } from '../user'; +import { property } from '@loopback/repository'; + +@inputType() +export class UserUpdateInput implements Partial> { + @field({nullable: true}) + @property(UserProperties.email) + email?: string; + @field({nullable: true}) + @property(UserProperties.name) + name?: string; + @field({nullable: true}) + @property(UserProperties.password) + password?: string; + @field({nullable: true}) + isAdmin?: boolean; +} diff --git a/backend/src/graphql-types/list.ts b/backend/src/graphql-types/list.ts new file mode 100644 index 0000000..a1c6117 --- /dev/null +++ b/backend/src/graphql-types/list.ts @@ -0,0 +1,17 @@ +import { field, inputType, objectType } from '@loopback/graphql'; + +@inputType() +export class ListInput { + @field() + offset: number; + @field() + limit: number; +} + +@objectType() +export class ListResponse { + @field() + count: number; + @field() + list: T[]; +} diff --git a/backend/src/graphql-types/user.ts b/backend/src/graphql-types/user.ts index 5001174..33e27fe 100644 --- a/backend/src/graphql-types/user.ts +++ b/backend/src/graphql-types/user.ts @@ -1,6 +1,53 @@ import { User } from '../models'; +import { field, objectType } from '@loopback/graphql'; -export class LoginResult { - token: string; - user: Omit; +@objectType() +export class UserResult implements Pick { + @field() + email: string; + @field() + id?: number; + @field() + isAdmin: boolean; + @field() + name: string; } + +@objectType() +export class LoginResult { + @field() + token: string; + @field() + user: UserResult; +} + +export const UserProperties = { + id: { + type: 'number', + id: true, + generated: true, + }, + email: { + type: 'string', + required: true, + jsonSchema: { + pattern: /[A-Za-z\d.+_-]+@[A-Za-z.-_]*(u-szeged.hu)|(szte.hu)/.source + } + }, + name: { + type: 'string', + required: true, + jsonSchema: { + pattern: /([A-Za-z-.]+ )+[A-Za-z-.]+/.source + } + }, + password: { + type: 'string', + required: true, + jsonSchema: { + minLength: 8, + maxLength: 255 + }, + hidden: true + } +}; diff --git a/backend/src/models/user.model.ts b/backend/src/models/user.model.ts index f765931..6c06d7f 100644 --- a/backend/src/models/user.model.ts +++ b/backend/src/models/user.model.ts @@ -2,47 +2,24 @@ 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'; +import { UserProperties } from '../graphql-types/user'; @model() @objectType() export class User extends Entity { - @property({ - type: 'number', - id: true, - generated: true, - }) + @property(UserProperties.id) @field() id?: number; - @property({ - type: 'string', - required: true, - jsonSchema: { - pattern: /[A-Za-z0-9.+_-]+@[A-Za-z.-_]*(u-szeged.hu)|(szte.hu)/.source - } - }) + @property(UserProperties.email) @field() email: string; - @property({ - type: 'string', - required: true, - jsonSchema: { - pattern: /([A-Za-z-.]+ )+[A-Za-z-.]+/.source - } - }) + @property(UserProperties.name) @field() name: string; - @property({ - type: 'string', - required: true, - jsonSchema: { - minLength: 8, - maxLength: 255 - }, - hidden: true - }) + @property(UserProperties.password) @field() password: string;