added: MeService

This commit is contained in:
Sergey Yarkov 2021-08-08 02:00:56 +03:00
parent a852e82451
commit e6880e3e0e
17 changed files with 308 additions and 151 deletions

View File

@ -1,12 +1,10 @@
import { Exception } from '@adonisjs/core/build/standalone';
import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import Hash from '@ioc:Adonis/Core/Hash';
import Logger from '@ioc:Adonis/Core/Logger';
/**
* Models
* Services
*/
import Contact from 'App/Models/Contact';
import MeService from 'App/Services/MeService';
/**
* Validators
@ -14,89 +12,60 @@ import Contact from 'App/Models/Contact';
import ChangePasswordValidator from 'App/Validators/Password/ChangePasswordValidator';
import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator';
export default class MeController {
private readonly guard: 'api';
import BaseController from '../../BaseController';
private readonly Contact: typeof Contact;
@inject()
export default class MeController extends BaseController {
private meService: MeService;
constructor() {
this.Contact = Contact;
constructor(meService: MeService) {
super();
this.meService = meService;
}
/**
* Shows user info of the access token resource owner.
* GET /me
*/
public async show({ response, auth }: HttpContextContract) {
const user = await auth.use(this.guard).authenticate();
public async show(ctx: HttpContextContract) {
const result = await this.meService.fetchUserContacts(ctx.auth);
await user.load('contacts');
await user.load('roles');
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
return response.ok({
message: 'Fetched data about me.',
data: user,
});
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
* Change password of authenticated user
* PATCH /me/password
*/
public async changePassword({ request, response, auth }: HttpContextContract) {
await request.validate(ChangePasswordValidator);
public async changePassword(ctx: HttpContextContract) {
const payload = await ctx.request.validate(ChangePasswordValidator);
const result = await this.meService.changeUserPassword(payload.oldPassword, payload.newPassword, ctx.auth);
const user = await auth.use(this.guard).authenticate();
const oldPassword: string = request.input('oldPassword');
const newPassword: string = request.input('newPassword');
if (oldPassword === newPassword) {
throw new Exception('The new password is the same as the old one.', 400, 'E_PASSWORD_VALUES');
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
/**
* Verify old password
*/
if (!(await Hash.verify(user.password, oldPassword))) {
throw new Exception('Invalid credentials.', 403, 'E_INVALID_CREDENTIALS');
}
/**
* Update user password
*/
user.password = newPassword;
await user.save();
Logger.info(`Password of user with id: "${user.id}" was updated.`);
return response.ok({
message: 'The password was successfully changed to the new value.',
});
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
* Update contacts of authenticated user
* PATCH /me/contacts
* PUT /me/contacts
*/
public async updateContacts({ request, response, auth }: HttpContextContract) {
const payload = await request.validate(UpdateContactsValidator);
const user = await auth.use(this.guard).authenticate();
const contacts = await this.Contact.findByOrFail('user_id', user.id);
public async updateContacts(ctx: HttpContextContract) {
const payload = await ctx.request.validate(UpdateContactsValidator);
const result = await this.meService.updateUserContacts(payload, ctx.auth);
/**
* Update contacts
*/
if (payload.email) contacts.email = payload.email;
contacts.phone_number = payload.phone_number ?? null;
contacts.vk_id = payload.vk_id ?? null;
contacts.twitter_id = payload.twitter_id ?? null;
contacts.telegram_id = payload.telegram_id ?? null;
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
await contacts.save();
return response.ok({
message: `Contacts has been successfully updated.`,
data: contacts,
});
return this.sendResponse(ctx, result.data, result.message, result.status);
}
}
new Ioc().make(MeController);

View File

@ -0,0 +1,12 @@
enum StatusCodeEnum {
OK = 200,
CREATED = 201,
NO_CONTENT = 204,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
INTERNAL_SERVER = 500,
}
export default StatusCodeEnum;

View File

@ -9,9 +9,6 @@ export default class Contact extends BaseModel {
@column({ serializeAs: null })
public user_id: string;
@column()
public email: string;
@column()
public phone_number: string | null;

View File

@ -30,6 +30,9 @@ export default class User extends BaseModel {
@column({ serializeAs: null })
public login: string;
@column()
public email: string;
@column({ serializeAs: null })
public password: string;

View File

@ -1,7 +1,14 @@
/**
* Models
*/
import Contact from 'App/Models/Contact';
import User from 'App/Models/User';
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
/**
* Validators
*/
import CreateContactsValidator from 'App/Validators/Contacts/CreateContactsValidator';
import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator';
export default class ContactRepository {
private Contact: typeof Contact;
@ -10,6 +17,17 @@ export default class ContactRepository {
this.Contact = Contact;
}
/**
* Get contacts by user id
*
* @param userId User id
* @returns User contacts or null
*/
public async getByUser(userId: string | number): Promise<Contact | null> {
const contacts = await this.Contact.query().where('user_id', userId).first();
return contacts;
}
/**
* Create user contacts in database
*
@ -17,10 +35,17 @@ export default class ContactRepository {
* @param data Data contacts
* @returns Created contacts
*/
public async create(user: User, data: Pick<CreateUserValidator['schema']['props'], 'email'>) {
public async create(user: User, data: CreateContactsValidator['schema']['props']): Promise<Contact> {
const contacts = await this.Contact.create(data);
/**
* Associate user
*/
await contacts.related('user').associate(user);
/**
* Load updated contacts to user
*/
await user.load('contacts');
await user.load('roles');
@ -32,13 +57,16 @@ export default class ContactRepository {
*
* @param user User
* @param data Data to update
* @returns
* @returns Updated contacts or null
*/
public async update(user: User, data: Pick<UpdateUserValidator['schema']['props'], 'email'>) {
public async update(user: User, data: UpdateContactsValidator['schema']['props']): Promise<Contact | null> {
const contacts = await this.Contact.query().where('user_id', user.id).first();
if (contacts) {
contacts.email = data.email;
contacts.phone_number = data.phone_number ?? null;
contacts.telegram_id = data.telegram_id ?? null;
contacts.twitter_id = data.twitter_id ?? null;
contacts.vk_id = data.vk_id ?? null;
await contacts.save();
await user.load('contacts');
@ -46,15 +74,6 @@ export default class ContactRepository {
return contacts;
}
/**
* Create new user contacts if contacts doesn't exist
*/
await user.related('contacts').create({
email: data.email,
});
await user.load('contacts');
return user.contacts;
return null;
}
}

View File

@ -1,3 +1,6 @@
/**
* Models
*/
import Role from 'App/Models/Role';
export default class RoleRepository {

View File

@ -1,5 +1,15 @@
/* eslint-disable @typescript-eslint/naming-convention */
import Hash from '@ioc:Adonis/Core/Hash';
/**
* Models
*/
import User from 'App/Models/User';
/**
* Validators
*/
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
@ -16,7 +26,7 @@ export default class UserRepository {
* @param id User id
* @returns User or null
*/
public async getById(id: string | number) {
public async getById(id: string | number): Promise<User | null> {
const data = await this.User.query().preload('contacts').preload('roles').where('id', id).first();
return data;
}
@ -28,7 +38,7 @@ export default class UserRepository {
* @param value Comparsion column
* @returns User or null
*/
public async getByColumn(column: string, value: string | number) {
public async getByColumn(column: string, value: string | number): Promise<User | null> {
const data = await this.User.query().preload('contacts').preload('roles').where(column, value).first();
return data;
}
@ -39,7 +49,7 @@ export default class UserRepository {
* @param role Role
* @returns Array of users with role
*/
public async getAllByRole(role: string) {
public async getAllByRole(role: string): Promise<User[]> {
const data = await this.User.query()
.preload('contacts')
.preload('roles')
@ -53,7 +63,7 @@ export default class UserRepository {
* @param params Params to find
* @returns Array of users
*/
public async getAll(params?: any) {
public async getAll(params?: any): Promise<User[]> {
const { email, login, first_name, last_name, role }: any = params || {};
const query = this.User.query();
@ -87,14 +97,8 @@ export default class UserRepository {
* @param data Data for create user
* @returns Created user
*/
public async create(data: CreateUserValidator['schema']['props']) {
const createdUser = await this.User.create({
first_name: data.first_name,
last_name: data.last_name,
login: data.login,
password: data.password,
});
public async create(data: CreateUserValidator['schema']['props']): Promise<User> {
const createdUser = await this.User.create(data);
return createdUser;
}
@ -105,18 +109,11 @@ export default class UserRepository {
* @param data Data to update
* @returns User or null if user is not found
*/
public async update(id: number | string, data: UpdateUserValidator['schema']['props']) {
public async update(id: number | string, data: UpdateUserValidator['schema']['props']): Promise<User | null> {
const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first();
if (user) {
await user
.merge({
first_name: data.first_name,
last_name: data.last_name,
login: data.login,
password: data.password,
})
.save();
await user.merge(data).save();
return user;
}
@ -129,7 +126,7 @@ export default class UserRepository {
* @param id User id
* @returns Deleted user or null if user is not found
*/
public async delete(id: number | string) {
public async delete(id: number | string): Promise<User | null> {
const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first();
if (user) {
@ -139,4 +136,9 @@ export default class UserRepository {
return null;
}
public async updatePassword(password: string, id: string | number): Promise<any> {
const hashedPass = await Hash.make(password);
return this.User.query().where('id', id).update({ password: hashedPass }).first();
}
}

View File

@ -8,6 +8,11 @@ import Logger from '@ioc:Adonis/Core/Logger';
import { AuthContract } from '@ioc:Adonis/Addons/Auth';
import IResponse from 'App/Datatypes/Interfaces/IResponse';
/**
* Enums
*/
import StatusCodeEnum from 'App/Datatypes/Enums/StatusCodeEnum';
/**
* Reposirories
*/
@ -17,7 +22,7 @@ import UserRepository from 'App/Repositories/UserRepository';
export default class AuthService {
private userRepository: UserRepository;
private guard: 'api';
private authGuard: 'api';
constructor(userRepository: UserRepository) {
this.userRepository = userRepository;
@ -37,11 +42,11 @@ export default class AuthService {
if (!user) {
return {
success: false,
status: 404,
status: StatusCodeEnum.NOT_FOUND,
message: 'User not found.',
data: {},
error: {
code: 'E_USER_NOT_FOUND',
code: 'E_NOT_FOUND',
},
};
}
@ -52,7 +57,7 @@ export default class AuthService {
if (!(await Hash.verify(user.password, password))) {
return {
success: false,
status: 403,
status: StatusCodeEnum.UNAUTHORIZED,
message: 'Invalid credentials.',
data: {},
error: {
@ -64,7 +69,7 @@ export default class AuthService {
/**
* Create new token
*/
const token = await auth.use(this.guard).generate(user, {
const token = await auth.use(this.authGuard).generate(user, {
expiresIn: '1d',
userRoles: user.roles.map(role => role.slug),
});
@ -73,7 +78,7 @@ export default class AuthService {
return {
success: true,
status: 200,
status: StatusCodeEnum.OK,
message: 'Login success.',
data: token.toJSON(),
};
@ -86,11 +91,11 @@ export default class AuthService {
* @returns Logout message
*/
public async logout(auth: AuthContract): Promise<IResponse> {
await auth.use(this.guard).revoke();
await auth.use(this.authGuard).revoke();
return {
success: true,
status: 200,
status: StatusCodeEnum.OK,
message: 'Logout success.',
data: {},
};

134
app/Services/MeService.ts Normal file
View File

@ -0,0 +1,134 @@
import { inject, Ioc } from '@adonisjs/core/build/standalone';
import { AuthContract } from '@ioc:Adonis/Addons/Auth';
import Hash from '@ioc:Adonis/Core/Hash';
import Logger from '@ioc:Adonis/Core/Logger';
import StatusCodeEnum from 'App/Datatypes/Enums/StatusCodeEnum';
import IResponse from 'App/Datatypes/Interfaces/IResponse';
import ContactRepository from 'App/Repositories/ContactRepository';
import UserRepository from 'App/Repositories/UserRepository';
import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator';
@inject()
export default class MeService {
private userRepository: UserRepository;
private contactRepository: ContactRepository;
private authGuard: 'api';
constructor(userRepository: UserRepository, contactRepository: ContactRepository) {
this.userRepository = userRepository;
this.contactRepository = contactRepository;
}
/**
* Get authorized user data
*
* @param auth AuthContract
* @returns Response
*/
public async fetchUserContacts(auth: AuthContract): Promise<IResponse> {
const user = await auth.use(this.authGuard).authenticate();
await user.load('roles');
await user.load('contacts');
return {
success: true,
status: StatusCodeEnum.OK,
message: 'Fetched authorized user data.',
data: user,
};
}
/**
* Change password of authorized user
*
* @param oldPassword Old password
* @param newPassword New password
* @param auth AuthContract
* @returns Response
*/
public async changeUserPassword(oldPassword: string, newPassword: string, auth: AuthContract): Promise<IResponse> {
const user = await auth.use(this.authGuard).authenticate();
/**
* Verify old password
*/
if (!(await Hash.verify(user.password, oldPassword))) {
return {
success: false,
status: StatusCodeEnum.UNAUTHORIZED,
message: 'Invalid credentials.',
data: {},
error: {
code: 'E_INVALID_CREDENTIALS',
},
};
}
/**
* Update password
*/
const passwordUpdated = await this.userRepository.updatePassword(newPassword, user.id);
if (passwordUpdated) {
Logger.info(`Password of user with id: "${user.id}" was updated.`);
return {
success: true,
status: StatusCodeEnum.OK,
message: 'Password updated.',
data: {},
};
}
return {
success: false,
status: StatusCodeEnum.BAD_REQUEST,
message: 'Unable to change password',
data: {},
error: {
code: 'E_BAD_REQUEST',
},
};
}
/**
* Update user conctact
*
* @param data Data to update
* @param auth AuthContract
* @returns Updated user contacts
*/
public async updateUserContacts(
data: UpdateContactsValidator['schema']['props'],
auth: AuthContract
): Promise<IResponse> {
const user = await auth.use(this.authGuard).authenticate();
const contacts = await this.contactRepository.update(user, data);
if (contacts) {
return {
success: true,
status: StatusCodeEnum.OK,
message: 'User contacts updated.',
data: contacts,
};
}
/**
* Create user contacts if they don't exist
*/
await this.contactRepository.create(user, data);
return {
success: true,
status: StatusCodeEnum.OK,
message: 'User contacts updated.',
data: user.contacts,
};
}
}
new Ioc().make(MeService);

View File

@ -5,6 +5,11 @@ import { inject, Ioc } from '@adonisjs/core/build/standalone';
*/
import IResponse from 'App/Datatypes/Interfaces/IResponse';
/**
* Enums
*/
import StatusCodeEnum from 'App/Datatypes/Enums/StatusCodeEnum';
/**
* Models
*/
@ -13,7 +18,6 @@ import Role from 'App/Models/Role';
/**
* Repositories
*/
import ContactRepository from 'App/Repositories/ContactRepository';
import RoleRepository from 'App/Repositories/RoleRepository';
import UserRepository from 'App/Repositories/UserRepository';
@ -27,13 +31,10 @@ import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
export default class UserService {
private userRepository: UserRepository;
private contactRepository: ContactRepository;
private roleRepository: RoleRepository;
constructor(userRepository: UserRepository, contactRepository: ContactRepository, roleRepository: RoleRepository) {
constructor(userRepository: UserRepository, roleRepository: RoleRepository) {
this.userRepository = userRepository;
this.contactRepository = contactRepository;
this.roleRepository = roleRepository;
}
@ -49,18 +50,18 @@ export default class UserService {
if (!user) {
return {
success: false,
status: 404,
status: StatusCodeEnum.NOT_FOUND,
message: 'User not found.',
data: {},
error: {
code: 'E_USER_NOT_FOUND',
code: 'E_NOT_FOUND',
},
};
}
return {
success: true,
status: 200,
status: StatusCodeEnum.OK,
message: 'Fetched user.',
data: user,
};
@ -77,7 +78,7 @@ export default class UserService {
return {
success: true,
status: 200,
status: StatusCodeEnum.OK,
message: 'Fetched all users.',
data: users,
};
@ -92,12 +93,9 @@ export default class UserService {
public async createUser(data: CreateUserValidator['schema']['props']): Promise<IResponse> {
const user = await this.userRepository.create(data);
/* Associate user contacts */
await this.contactRepository.create(user, { email: data.email });
return {
success: true,
status: 201,
status: StatusCodeEnum.CREATED,
message: 'User created.',
data: user,
};
@ -116,23 +114,18 @@ export default class UserService {
if (!user) {
return {
success: false,
status: 404,
status: StatusCodeEnum.NOT_FOUND,
message: 'User not found.',
data: {},
error: {
code: 'E_USER_NOT_FOUND',
code: 'E_NOT_FOUND',
},
};
}
/**
* Update user contacts
*/
await this.contactRepository.update(user, data);
return {
success: true,
status: 200,
status: StatusCodeEnum.OK,
message: 'User updated.',
data: user,
};
@ -150,18 +143,18 @@ export default class UserService {
if (!user) {
return {
success: false,
status: 404,
status: StatusCodeEnum.NOT_FOUND,
message: 'User not found.',
data: {},
error: {
code: 'E_USER_NOT_FOUND',
code: 'E_NOT_FOUND',
},
};
}
return {
success: true,
status: 200,
status: StatusCodeEnum.OK,
message: 'User deleted.',
data: user,
};
@ -180,11 +173,11 @@ export default class UserService {
if (!user) {
return {
success: false,
status: 404,
status: StatusCodeEnum.NOT_FOUND,
message: 'User not found.',
data: {},
error: {
code: 'E_USER_NOT_FOUND',
code: 'E_NOT_FOUND',
},
};
}
@ -208,11 +201,11 @@ export default class UserService {
if (attachedRole) {
return {
success: false,
status: 404,
status: StatusCodeEnum.BAD_REQUEST,
message: `Role "${attachedRole.name}" already attached to that user.`,
data: {},
error: {
code: 'E_ROLE_ATTACHED',
code: 'E_BAD_REQUEST',
},
};
}
@ -222,7 +215,7 @@ export default class UserService {
return {
success: true,
status: 200,
status: StatusCodeEnum.OK,
message: 'Roles attached.',
data: user.roles,
};
@ -241,11 +234,11 @@ export default class UserService {
if (!user) {
return {
success: false,
status: 404,
status: StatusCodeEnum.NOT_FOUND,
message: 'User not found.',
data: {},
error: {
code: 'E_USER_NOT_FOUND',
code: 'E_NOT_FOUND',
},
};
}
@ -266,11 +259,11 @@ export default class UserService {
if (notAttachedRole) {
return {
success: false,
status: 404,
status: StatusCodeEnum.BAD_REQUEST,
message: `Role "${notAttachedRole.name}" not attached to that user.`,
data: {},
error: {
code: 'E_ROLE_NOT_ATTACHED',
code: 'E_BAD_REQUEST',
},
};
}
@ -280,7 +273,7 @@ export default class UserService {
return {
success: true,
status: 200,
status: StatusCodeEnum.OK,
message: 'Roles detached.',
data: user.roles,
};

View File

@ -0,0 +1,20 @@
import { schema, rules } from '@ioc:Adonis/Core/Validator';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export default class CreateContactsValidator {
constructor(protected ctx: HttpContextContract) {}
public schema = schema.create({
phone_number: schema.string.optional({}, [
rules.mobile({
locales: ['ru-RU', 'en-US'],
strict: true,
}),
]),
vk_id: schema.string.optional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]),
twitter_id: schema.string.optional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]),
telegram_id: schema.string.optional({}, [rules.regex(/^[a-zA-Z0-9_]+$/)]),
});
public messages = {};
}

View File

@ -5,7 +5,6 @@ export default class UpdateContactsValidator {
constructor(protected ctx: HttpContextContract) {}
public schema = schema.create({
email: schema.string.optional({}, [rules.email()]),
phone_number: schema.string.optional({}, [
rules.mobile({
locales: ['ru-RU', 'en-US'],

View File

@ -27,7 +27,7 @@ export default class CreateUserValidator {
first_name: schema.string(),
last_name: schema.string(),
login: schema.string({}, [rules.maxLength(128), rules.unique({ table: 'users', column: 'login' })]),
email: schema.string({}, [rules.email(), rules.unique({ table: 'contacts', column: 'email' })]),
email: schema.string({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]),
password: schema.string({}, [rules.minLength(6), rules.maxLength(128)]),
});

View File

@ -8,7 +8,7 @@ export default class UpdateUserValidator {
first_name: schema.string(),
last_name: schema.string(),
login: schema.string({}, [rules.unique({ table: 'users', column: 'login' })]),
email: schema.string({}, [rules.email(), rules.unique({ table: 'contacts', column: 'email' })]),
email: schema.string({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]),
password: schema.string(),
});

View File

@ -7,7 +7,7 @@ import User from 'App/Models/User';
export const ContactFactory = Factory.define(Contact, ({ faker }) => {
return {
email: faker.internet.exampleEmail(),
phone_number: faker.phone.phoneNumber(),
};
}).build();
@ -16,6 +16,7 @@ export const UserFactory = Factory.define(User, ({ faker }) => {
first_name: faker.name.firstName(),
last_name: faker.name.lastName(),
login: `${faker.lorem.word(6)}${faker.datatype.number(10)}`,
email: faker.internet.exampleEmail(),
password: '12345',
};
})

View File

@ -9,6 +9,7 @@ export default class Users extends BaseSchema {
table.string('first_name', 24).notNullable();
table.string('last_name', 24).notNullable();
table.string('login', 128).notNullable().unique();
table.string('email').notNullable().unique();
table.string('password').notNullable();
table.timestamp('created_at', { useTz: true });
table.timestamp('updated_at', { useTz: true });

View File

@ -7,7 +7,6 @@ export default class Contacts extends BaseSchema {
this.schema.createTable(this.tableName, table => {
table.increments('id');
table.string('user_id', 21).unsigned().references('users.id').unique().onDelete('CASCADE');
table.string('email').unique().notNullable();
table.string('phone_number').unique();
table.string('vk_id').unique();
table.string('twitter_id').unique();