Implement login, find user, update user (partly)
This commit is contained in:
parent
099b40e77a
commit
65332d4947
6 changed files with 126 additions and 167 deletions
|
@ -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 { User } from '../models';
|
||||||
import { repository } from '@loopback/repository';
|
import { repository } from '@loopback/repository';
|
||||||
import { UserRepository } from '../repositories';
|
import { UserRepository } from '../repositories';
|
||||||
|
@ -10,6 +10,8 @@ import { SecurityBindings, UserProfile } from '@loopback/security';
|
||||||
import { genSalt, hash } from 'bcryptjs';
|
import { genSalt, hash } from 'bcryptjs';
|
||||||
import { UserRegisterInput } from '../graphql-types/input/user-register.input';
|
import { UserRegisterInput } from '../graphql-types/input/user-register.input';
|
||||||
import { validated } from '../helpers';
|
import { validated } from '../helpers';
|
||||||
|
import { LoginResult } from '../graphql-types/user';
|
||||||
|
import { UserUpdateInput } from '../graphql-types/input/user-update.input';
|
||||||
|
|
||||||
@resolver(of => User)
|
@resolver(of => User)
|
||||||
export class UserResolver {
|
export class UserResolver {
|
||||||
|
@ -39,20 +41,10 @@ export class UserResolver {
|
||||||
return (await this.userRepository.find())[0];
|
return (await this.userRepository.find())[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/*@mutation(returns => LoginResult)
|
@mutation(returns => LoginResult)
|
||||||
async login(
|
async login(@arg('email') email: string, @arg('password') password: string): Promise<LoginResult> {
|
||||||
@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
|
// 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)
|
// convert a User object into a UserProfile object (reduced set of properties)
|
||||||
const userProfile = this.userService.convertToUserProfile(user);
|
const userProfile = this.userService.convertToUserProfile(user);
|
||||||
|
|
||||||
|
@ -61,16 +53,11 @@ export class UserResolver {
|
||||||
return {token, user};
|
return {token, user};
|
||||||
}
|
}
|
||||||
|
|
||||||
@post('/users/logout', {
|
@authorized()
|
||||||
responses: {
|
@mutation(returns => Boolean)
|
||||||
'204': {
|
async logout(@inject(GraphQLBindings.RESOLVER_DATA) request: any): Promise<boolean> {
|
||||||
description: 'Logged out',
|
console.log('request:', request); //TODO
|
||||||
},
|
const split = request.headers.get('Authorization')?.split(' ');
|
||||||
},
|
|
||||||
})
|
|
||||||
@authenticate('jwt')
|
|
||||||
async logout(@inject(RestBindings.Http.REQUEST) request: Request): Promise<void> {
|
|
||||||
const split = request.headers.authorization?.split(' ');
|
|
||||||
if (split && split.length > 1) {
|
if (split && split.length > 1) {
|
||||||
if (this.jwtService.revokeToken) {
|
if (this.jwtService.revokeToken) {
|
||||||
await this.jwtService.revokeToken(split[1]);
|
await this.jwtService.revokeToken(split[1]);
|
||||||
|
@ -78,107 +65,38 @@ export class UserResolver {
|
||||||
console.error('Cannot revoke token');
|
console.error('Cannot revoke token');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@get('/users/count')
|
@authorized()
|
||||||
@response(200, {
|
@query(returns => [User])
|
||||||
description: 'User model count',
|
async find(user: Partial<User>): Promise<User[]> {
|
||||||
content: {'application/json': {schema: CountSchema}},
|
return this.userRepository.find({}); //TODO
|
||||||
})
|
|
||||||
@authenticate('jwt')
|
|
||||||
async count(
|
|
||||||
@param.where(User) where?: Where<User>,
|
|
||||||
): Promise<Count> {
|
|
||||||
return this.userRepository.count(where);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@get('/users')
|
@authorized()
|
||||||
@response(200, {
|
@query(returns => User)
|
||||||
description: 'Array of User model instances',
|
async findById(@arg('id', returns => Int) id: number): Promise<User> {
|
||||||
content: {
|
return this.userRepository.findById(id);
|
||||||
'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')
|
@authorized()
|
||||||
@response(200, {
|
@mutation(returns => Boolean)
|
||||||
description: 'User PATCH success count',
|
async updateById(@arg('id', returns => Int) id: number, @arg('user') user: UserUpdateInput): Promise<boolean> {
|
||||||
content: {'application/json': {schema: CountSchema}},
|
if (id === +this.user?.id) { //TODO: this.user
|
||||||
})
|
|
||||||
@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);
|
const loggedInUser = await this.userService.findUserById(this.user.id);
|
||||||
if (user.isAdmin !== undefined && loggedInUser.isAdmin !== user.isAdmin) {
|
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);
|
await this.userRepository.updateById(id, user);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@del('/users/{id}')
|
@authorized()
|
||||||
@response(204, {
|
@mutation(returns => Boolean)
|
||||||
description: 'User DELETE success',
|
async deleteById(id: number): Promise<Boolean> {
|
||||||
})
|
|
||||||
@authenticate('jwt')
|
|
||||||
async deleteById(@param.path.number('id') id: number): Promise<void> {
|
|
||||||
await this.userRepository.deleteById(id);
|
await this.userRepository.deleteById(id);
|
||||||
}*/
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,18 @@
|
||||||
import { field, inputType } from '@loopback/graphql';
|
import { field, inputType } from '@loopback/graphql';
|
||||||
import { User } from '../../models';
|
import { User } from '../../models';
|
||||||
import { model, property } from '@loopback/repository';
|
import { model, property } from '@loopback/repository';
|
||||||
|
import { UserProperties } from '../user';
|
||||||
|
|
||||||
@model()
|
@model()
|
||||||
@inputType()
|
@inputType()
|
||||||
export class UserRegisterInput implements Pick<User, 'email' | 'name' | 'password'> {
|
export class UserRegisterInput implements Pick<User, 'email' | 'name' | 'password'> {
|
||||||
@property({
|
@property(UserProperties.email)
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
jsonSchema: {
|
|
||||||
pattern: /[A-Za-z0-9.+_-]+@[A-Za-z.-_]*(u-szeged.hu)|(szte.hu)/.source
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@field()
|
@field()
|
||||||
email: string;
|
email: string;
|
||||||
@property({
|
@property(UserProperties.name)
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
jsonSchema: {
|
|
||||||
pattern: /([A-Za-z-.]+ )+[A-Za-z-.]+/.source
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@field()
|
@field()
|
||||||
name: string;
|
name: string;
|
||||||
@property({
|
@property(UserProperties.password)
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
jsonSchema: {
|
|
||||||
minLength: 8,
|
|
||||||
maxLength: 255
|
|
||||||
},
|
|
||||||
hidden: true
|
|
||||||
})
|
|
||||||
@field()
|
@field()
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
19
backend/src/graphql-types/input/user-update.input.ts
Normal file
19
backend/src/graphql-types/input/user-update.input.ts
Normal file
|
@ -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<Pick<User, 'name' | 'email' | 'password' | 'isAdmin'>> {
|
||||||
|
@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;
|
||||||
|
}
|
17
backend/src/graphql-types/list.ts
Normal file
17
backend/src/graphql-types/list.ts
Normal file
|
@ -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<T> {
|
||||||
|
@field()
|
||||||
|
count: number;
|
||||||
|
@field()
|
||||||
|
list: T[];
|
||||||
|
}
|
|
@ -1,6 +1,53 @@
|
||||||
import { User } from '../models';
|
import { User } from '../models';
|
||||||
|
import { field, objectType } from '@loopback/graphql';
|
||||||
|
|
||||||
export class LoginResult {
|
@objectType()
|
||||||
token: string;
|
export class UserResult implements Pick<User, 'id' | 'email' | 'name' | 'isAdmin'> {
|
||||||
user: Omit<User, 'id' | 'password'>;
|
@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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -2,47 +2,24 @@ 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';
|
import { field, objectType } from '@loopback/graphql';
|
||||||
|
import { UserProperties } from '../graphql-types/user';
|
||||||
|
|
||||||
@model()
|
@model()
|
||||||
@objectType()
|
@objectType()
|
||||||
export class User extends Entity {
|
export class User extends Entity {
|
||||||
@property({
|
@property(UserProperties.id)
|
||||||
type: 'number',
|
|
||||||
id: true,
|
|
||||||
generated: true,
|
|
||||||
})
|
|
||||||
@field()
|
@field()
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
||||||
@property({
|
@property(UserProperties.email)
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
jsonSchema: {
|
|
||||||
pattern: /[A-Za-z0-9.+_-]+@[A-Za-z.-_]*(u-szeged.hu)|(szte.hu)/.source
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@field()
|
@field()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@property({
|
@property(UserProperties.name)
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
jsonSchema: {
|
|
||||||
pattern: /([A-Za-z-.]+ )+[A-Za-z-.]+/.source
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@field()
|
@field()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@property({
|
@property(UserProperties.password)
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
jsonSchema: {
|
|
||||||
minLength: 8,
|
|
||||||
maxLength: 255
|
|
||||||
},
|
|
||||||
hidden: true
|
|
||||||
})
|
|
||||||
@field()
|
@field()
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue