From 7e81d7b4ff4ec88b6e89167cdf263f8f318f4088 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Tue, 1 Feb 2022 22:47:36 +0300 Subject: [PATCH 01/18] fix: cleaning up sessions --- CHANGELOG.md | 6 ++++++ app/Services/WsService.ts | 16 ++++++++++++-- app/Store/SessionStore.ts | 45 +++++++++++++++++++++++++++++++-------- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b259d72..43c28f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ - API deployment with Docker and Nginx web server. +## [1.1.1] - + +### Fix + +- Cleaning up session store before starting websocket server + ## [1.1.0] - 2022-01-31 ### Added diff --git a/app/Services/WsService.ts b/app/Services/WsService.ts index 2cc313f..7d29e51 100644 --- a/app/Services/WsService.ts +++ b/app/Services/WsService.ts @@ -59,8 +59,20 @@ class WsService { }); this.sessionStore = new RedisSessionStore(Redis.connection('session')); - this.setupMiddlewares(); - this.listen(); + /** + * Clean up sessions before start + */ + this.sessionStore.destroyAllSessions().then(() => { + /** + * Middlewares + */ + this.setupMiddlewares(); + + /** + * On connection socket + */ + this.listen(); + }); } private listen() { diff --git a/app/Store/SessionStore.ts b/app/Store/SessionStore.ts index 12e43c6..afa715c 100644 --- a/app/Store/SessionStore.ts +++ b/app/Store/SessionStore.ts @@ -25,6 +25,20 @@ class RedisSessionStore { }; } + private async scanSessionKeys(): Promise> { + const keys = new Set(); + let nextIndex = 0; + + do { + // eslint-disable-next-line no-await-in-loop + const [nextIndexAsStr, result] = await this.redisClient.scan(nextIndex, 'MATCH', '*', 'COUNT', 100); + nextIndex = parseInt(nextIndexAsStr, 10); + result.forEach(s => keys.add(s)); + } while (nextIndex !== 0); + + return keys; + } + /** * Return session data by session id * @@ -40,15 +54,7 @@ class RedisSessionStore { * Returns the all sessions from store */ public async getSessions(): Promise> { - const keys = new Set(); - let nextIndex = 0; - - do { - // eslint-disable-next-line no-await-in-loop - const [nextIndexAsStr, result] = await this.redisClient.scan(nextIndex, 'MATCH', '*', 'COUNT', 100); - nextIndex = parseInt(nextIndexAsStr, 10); - result.forEach(s => keys.add(s)); - } while (nextIndex !== 0); + const keys = await this.scanSessionKeys(); const commands = Array.from(keys).map(key => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [prefix, ...id] = key.split('-'); @@ -103,6 +109,27 @@ class RedisSessionStore { return 0; } + + /** + * Remove all sessions from redis store + * + * @returns Count of deleted sessions + */ + public async destroyAllSessions() { + const keys = await this.scanSessionKeys().then(values => + Array.from(values).map(v => { + const [, ...key] = v.split('-'); + return key.join('-'); + }) + ); + + if (keys.length > 0) { + const deleted = await this.redisClient.del(keys); + return deleted; + } + + return 0; + } } export default RedisSessionStore; From a0cba27c48440349df1dd8653b97b6acaac9c97d Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Wed, 2 Feb 2022 17:14:52 +0300 Subject: [PATCH 02/18] feat: added last login column & about column to user model --- app/Controllers/Http/Api/v1/MeController.ts | 16 ++++++++++ app/Models/User.ts | 6 ++++ app/Repositories/UserRepository.ts | 27 ++++++++++++++++ app/Services/AuthService.ts | 8 +++++ app/Services/MeService.ts | 32 +++++++++++++++++++ .../User/UpdateUserInfoValidator.ts | 12 +++++++ app/Validators/User/UpdateUserValidator.ts | 1 + database/migrations/1626545059989_users.ts | 2 ++ routes/apis/v1/me.ts | 1 + 9 files changed, 105 insertions(+) create mode 100644 app/Validators/User/UpdateUserInfoValidator.ts diff --git a/app/Controllers/Http/Api/v1/MeController.ts b/app/Controllers/Http/Api/v1/MeController.ts index c9db38e..21ce2c9 100644 --- a/app/Controllers/Http/Api/v1/MeController.ts +++ b/app/Controllers/Http/Api/v1/MeController.ts @@ -12,6 +12,7 @@ import MeService from 'App/Services/MeService'; */ import ChangePasswordValidator from 'App/Validators/Password/ChangePasswordValidator'; import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator'; +import UpdateUserInfoValidator from 'App/Validators/User/UpdateUserInfoValidator'; import BaseController from '../../BaseController'; @@ -108,6 +109,21 @@ export default class MeController extends BaseController { return this.sendResponse(ctx, result.data, result.message, result.status); } + + /** + * Update personal info of user + * PATCH /me/info + */ + public async updateInfo(ctx: HttpContextContract) { + const payload = await ctx.request.validate(UpdateUserInfoValidator); + const result = await this.meService.updateUserInfo(payload, ctx.auth); + + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } + + return this.sendResponse(ctx, result.data, result.message, result.status); + } } new Ioc().make(MeController); diff --git a/app/Models/User.ts b/app/Models/User.ts index 695fa6b..5d9cc55 100644 --- a/app/Models/User.ts +++ b/app/Models/User.ts @@ -70,6 +70,12 @@ export default class User extends BaseModel { }) public lessons_progress: HasMany; + @column() + public about: string | null; + + @column.dateTime() + public last_login: DateTime | null; + @column.dateTime({ autoCreate: true }) public createdAt: DateTime; diff --git a/app/Repositories/UserRepository.ts b/app/Repositories/UserRepository.ts index ed9b30e..a0651eb 100644 --- a/app/Repositories/UserRepository.ts +++ b/app/Repositories/UserRepository.ts @@ -1,4 +1,5 @@ import Hash from '@ioc:Adonis/Core/Hash'; +import { DateTime } from 'luxon'; /** * Models @@ -11,6 +12,7 @@ import Role from 'App/Models/Role'; */ import CreateUserValidator from 'App/Validators/User/CreateUserValidator'; import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator'; +import UpdateUserInfoValidator from 'App/Validators/User/UpdateUserInfoValidator'; export default class UserRepository { private User: typeof User; @@ -159,6 +161,31 @@ export default class UserRepository { return null; } + /** + * Updates the time of last login column in database for user + * + * @param id User id + */ + public async updateLastLogin(id: number | string) { + await this.User.query().where('id', id).update({ last_login: DateTime.now() }).first(); + } + + /** + * Update personal info of user + * + * @param data New fields + */ + public async updateInfo(id: string | number, data: UpdateUserInfoValidator['schema']['props']) { + const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first(); + + if (user) { + await user.merge(data).save(); + return { about: user.about }; + } + + return null; + } + /** * Rewrite all user roles * diff --git a/app/Services/AuthService.ts b/app/Services/AuthService.ts index 6b6e47d..bf0e212 100644 --- a/app/Services/AuthService.ts +++ b/app/Services/AuthService.ts @@ -70,8 +70,16 @@ export default class AuthService { userName: user.fullname, }); + /** + * Set cookie toke to response + */ ctx.response.cookie('token', token.token); + /** + * Save last login date + */ + await this.userRepository.updateLastLogin(user.id); + return { success: true, status: HttpStatusEnum.OK, diff --git a/app/Services/MeService.ts b/app/Services/MeService.ts index 5fb3854..15183e8 100644 --- a/app/Services/MeService.ts +++ b/app/Services/MeService.ts @@ -13,6 +13,7 @@ import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValid import CodeErrorEnum from 'App/Datatypes/Enums/CodeErrorEnum'; import RoleHelper from 'App/Helpers/RoleHelper'; import RoleEnum from 'App/Datatypes/Enums/RoleEnum'; +import UpdateUserInfoValidator from 'App/Validators/User/UpdateUserInfoValidator'; @inject() export default class MeService { @@ -279,6 +280,37 @@ export default class MeService { data: user.contacts, }; } + + public async updateUserInfo( + data: UpdateUserInfoValidator['schema']['props'], + auth: AuthContract + ): Promise { + const user = await auth.use(this.authGuard).authenticate(); + const info = await this.userRepository.updateInfo(user.id, data); + + if (info) { + const { about } = info; + + return { + success: true, + status: HttpStatusEnum.OK, + message: 'User info updated.', + data: { + about, + }, + }; + } + + return { + success: false, + status: HttpStatusEnum.SERVICE_UNAVAILABLE, + message: 'Cannot update personal info.', + data: {}, + error: { + code: 'E_SERVICE_UNAVAILABLE', + }, + }; + } } new Ioc().make(MeService); diff --git a/app/Validators/User/UpdateUserInfoValidator.ts b/app/Validators/User/UpdateUserInfoValidator.ts new file mode 100644 index 0000000..f9f1034 --- /dev/null +++ b/app/Validators/User/UpdateUserInfoValidator.ts @@ -0,0 +1,12 @@ +import { schema } from '@ioc:Adonis/Core/Validator'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; + +export default class UpdateUserInfoValidator { + constructor(protected ctx: HttpContextContract) {} + + public schema = schema.create({ + about: schema.string.optional(), + }); + + public messages = {}; +} diff --git a/app/Validators/User/UpdateUserValidator.ts b/app/Validators/User/UpdateUserValidator.ts index ca9d91f..5d60734 100644 --- a/app/Validators/User/UpdateUserValidator.ts +++ b/app/Validators/User/UpdateUserValidator.ts @@ -8,6 +8,7 @@ export default class UpdateUserValidator { public schema = schema.create({ first_name: schema.string.optional(), last_name: schema.string.optional(), + about: schema.string.optional(), login: schema.string.optional({}, [rules.unique({ table: 'users', column: 'login' })]), email: schema.string.optional({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]), password: schema.string.optional(), diff --git a/database/migrations/1626545059989_users.ts b/database/migrations/1626545059989_users.ts index bda7219..8ec846e 100644 --- a/database/migrations/1626545059989_users.ts +++ b/database/migrations/1626545059989_users.ts @@ -8,9 +8,11 @@ export default class Users extends BaseSchema { table.string('id', 21).primary(); table.string('first_name', 24).notNullable(); table.string('last_name', 24).notNullable(); + table.text('about'); table.string('login', 128).notNullable().unique(); table.string('email').notNullable().unique(); table.string('password').notNullable(); + table.timestamp('last_login', { useTz: true }); table.timestamp('created_at', { useTz: true }); table.timestamp('updated_at', { useTz: true }); }); diff --git a/routes/apis/v1/me.ts b/routes/apis/v1/me.ts index 840c6d8..38e79e3 100644 --- a/routes/apis/v1/me.ts +++ b/routes/apis/v1/me.ts @@ -2,6 +2,7 @@ import Route from '@ioc:Adonis/Core/Route'; Route.group(() => { Route.get('/', 'Api/v1/MeController.show').middleware('role:admin,teacher,student').as('me.show'); + Route.patch('/info', 'Api/v1/MeController.updateInfo').middleware('role:admin,teacher,student').as('me.update-info'); Route.patch('/email', 'Api/v1/MeController.changeEmail') .middleware('role:admin,teacher,student') .as('me.change-email'); From 9dc74376f2b8e96deac5f99b58b65c8ce8a4b70a Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Wed, 2 Feb 2022 17:18:48 +0300 Subject: [PATCH 03/18] Updating Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43c28f0..29ade0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ## [1.1.1] - +## Added + +- About description for user and `last_login` column date + ### Fix - Cleaning up session store before starting websocket server From 452d7d9557b9ee0a002bbde097a20ad9ea0d1460 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 3 Feb 2022 22:32:47 +0300 Subject: [PATCH 04/18] feat: updating info of authorized user --- app/Repositories/UserRepository.ts | 3 +- app/Services/MeService.ts | 61 +++++++++++++------ .../User/UpdateUserInfoValidator.ts | 11 +++- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/app/Repositories/UserRepository.ts b/app/Repositories/UserRepository.ts index a0651eb..1f7ee8a 100644 --- a/app/Repositories/UserRepository.ts +++ b/app/Repositories/UserRepository.ts @@ -177,9 +177,10 @@ export default class UserRepository { */ public async updateInfo(id: string | number, data: UpdateUserInfoValidator['schema']['props']) { const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first(); + const { about } = data; if (user) { - await user.merge(data).save(); + await user.merge({ about }).save(); return { about: user.about }; } diff --git a/app/Services/MeService.ts b/app/Services/MeService.ts index 15183e8..ebd7875 100644 --- a/app/Services/MeService.ts +++ b/app/Services/MeService.ts @@ -248,9 +248,9 @@ export default class MeService { /** * Update user conctact * - * @param data Data to update + * @param data Fields to update * @param auth AuthContract - * @returns Updated user contacts + * @returns IResponse */ public async updateUserContacts( data: UpdateContactsValidator['schema']['props'], @@ -281,34 +281,59 @@ export default class MeService { }; } + /** + * Update info about user + * + * @param data Fields to update + * @param auth AuthContract + * @returns IResponse + */ public async updateUserInfo( data: UpdateUserInfoValidator['schema']['props'], auth: AuthContract ): Promise { const user = await auth.use(this.authGuard).authenticate(); - const info = await this.userRepository.updateInfo(user.id, data); - - if (info) { - const { about } = info; + if (Object.keys(data).length === 0) { return { - success: true, - status: HttpStatusEnum.OK, - message: 'User info updated.', - data: { - about, + success: false, + status: HttpStatusEnum.BAD_REQUEST, + message: 'You must specify at least one field.', + data: {}, + error: { + code: 'E_BAD_REQUEST', }, }; } + /** + * Personal info updating + */ + if (data.about !== undefined) { + await this.userRepository.updateInfo(user.id, data); + } + + /** + * Contacts updating + */ + if (data.phone_number !== undefined || data.twitter_id !== undefined || data.telegram_id !== undefined) { + const contacts = await this.contactRepository.update(user, { + phone_number: data.phone_number, + twitter_id: data.twitter_id, + telegram_id: data.telegram_id, + vk_id: data.vk_id, + }); + + if (!contacts) { + await this.contactRepository.create(user, data); + } + } + return { - success: false, - status: HttpStatusEnum.SERVICE_UNAVAILABLE, - message: 'Cannot update personal info.', - data: {}, - error: { - code: 'E_SERVICE_UNAVAILABLE', - }, + success: true, + status: HttpStatusEnum.OK, + message: 'User contacts updated.', + data: { ...data }, }; } } diff --git a/app/Validators/User/UpdateUserInfoValidator.ts b/app/Validators/User/UpdateUserInfoValidator.ts index f9f1034..b50c183 100644 --- a/app/Validators/User/UpdateUserInfoValidator.ts +++ b/app/Validators/User/UpdateUserInfoValidator.ts @@ -1,11 +1,18 @@ -import { schema } from '@ioc:Adonis/Core/Validator'; +import { schema, rules } from '@ioc:Adonis/Core/Validator'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; export default class UpdateUserInfoValidator { constructor(protected ctx: HttpContextContract) {} public schema = schema.create({ - about: schema.string.optional(), + about: schema.string.nullableAndOptional(), + phone_number: schema.string.nullableAndOptional({}, [ + rules.mobile({ locales: ['ru-RU', 'en-US'], strict: true }), + rules.unique({ table: 'contacts', column: 'phone_number' }), + ]), + vk_id: schema.string.nullableAndOptional({}, [rules.regex(/^([a-zA-Z0-9_]){1,64}$/)]), + twitter_id: schema.string.nullableAndOptional({}, [rules.regex(/(^|[^@\w])@(\w{1,15})\b/)]), + telegram_id: schema.string.nullableAndOptional({}, [rules.regex(/(^|[^@\w])@(\w{1,64})\b/)]), }); public messages = {}; From 953283247b4197d6b49ecb41a14fc8f1f6a32033 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 6 Feb 2022 22:33:38 +0300 Subject: [PATCH 05/18] refactor: user online --- CHANGELOG.md | 1 + app/Controllers/Ws/OnlineController.ts | 11 +++++------ app/Services/StatService.ts | 4 ++-- app/Services/WsService.ts | 2 +- app/Store/SessionStore.ts | 8 ++++---- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29ade0f..76a1971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ## Added - About description for user and `last_login` column date +- Updating info api route of authorized user ### Fix diff --git a/app/Controllers/Ws/OnlineController.ts b/app/Controllers/Ws/OnlineController.ts index 9f59b86..1d2c1fc 100644 --- a/app/Controllers/Ws/OnlineController.ts +++ b/app/Controllers/Ws/OnlineController.ts @@ -23,9 +23,8 @@ export default class OnlineController { this.socket.on('disconnect', reason => this.onDisconnected(reason)); } - public sendOnlineCount(online: number) { - this.socket.emit('user:online', online); - this.socket.broadcast.emit('user:online', online); + public sendOnline(online: string[]) { + this.io.sockets.emit('user:online', online); } public setUserOnline() { @@ -37,8 +36,8 @@ export default class OnlineController { */ this.sessionStore .saveSession(sessionId, { userId, userName, connected: true }) - .then(() => this.sessionStore.getOnlineSessionsCount()) - .then(online => this.sendOnlineCount(online)); + .then(() => this.sessionStore.getOnlineSessions()) + .then(online => this.sendOnline(online)); } } @@ -54,7 +53,7 @@ export default class OnlineController { */ this.sessionStore .saveSession(sessionId, { userId, userName, connected: false }) - .then(() => this.sessionStore.getOnlineSessionsCount()) + .then(() => this.sessionStore.getOnlineSessions()) .then(online => this.socket.broadcast.emit('user:online', online)); } } diff --git a/app/Services/StatService.ts b/app/Services/StatService.ts index d50ff32..8815509 100644 --- a/app/Services/StatService.ts +++ b/app/Services/StatService.ts @@ -20,7 +20,7 @@ export default class StatService { ` ); - const online = await Ws.sessionStore.getOnlineSessionsCount(); + const online = await Ws.sessionStore.getOnlineSessions(); return { success: true, @@ -28,7 +28,7 @@ export default class StatService { message: 'Fetched stat.', data: { ...count.rows[0], - online, + online: online.length, }, }; } diff --git a/app/Services/WsService.ts b/app/Services/WsService.ts index 7d29e51..84c05dc 100644 --- a/app/Services/WsService.ts +++ b/app/Services/WsService.ts @@ -19,7 +19,7 @@ export interface ServerToClientEvents { */ 'user:session': (data: { sessionId: string | undefined; userId: string | undefined }) => void; 'user:connected': (data: { userId: string; userName: string }) => void; - 'user:online': (data: number) => void; + 'user:online': (data: string[]) => void; } export interface ClientToServerEvents { diff --git a/app/Store/SessionStore.ts b/app/Store/SessionStore.ts index afa715c..9f6a935 100644 --- a/app/Store/SessionStore.ts +++ b/app/Store/SessionStore.ts @@ -57,7 +57,7 @@ class RedisSessionStore { const keys = await this.scanSessionKeys(); const commands = Array.from(keys).map(key => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [prefix, ...id] = key.split('-'); + const [, ...id] = key.split('-'); return ['hmget', id.join('-'), 'userId', 'userName', 'connected']; }); @@ -70,11 +70,11 @@ class RedisSessionStore { /** * Returns the unique sessions with connected flag `true` */ - public async getOnlineSessionsCount() { + public async getOnlineSessions() { const sessions = await this.getSessions(); - const online = new Set(sessions.filter(s => s?.connected === true).map(s => s?.userId)); + const online = new Set(sessions.filter((s): s is SessionDataType => s?.connected === true).map(s => s?.userId)); - return online.size; + return Array.from(online); } /** From 7b75877bd7a8ebabb4cdd514cab47e21c14f4743 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Mon, 7 Feb 2022 00:28:00 +0300 Subject: [PATCH 06/18] fix: validator message --- app/Validators/User/UpdateUserInfoValidator.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Validators/User/UpdateUserInfoValidator.ts b/app/Validators/User/UpdateUserInfoValidator.ts index b50c183..86682aa 100644 --- a/app/Validators/User/UpdateUserInfoValidator.ts +++ b/app/Validators/User/UpdateUserInfoValidator.ts @@ -15,5 +15,7 @@ export default class UpdateUserInfoValidator { telegram_id: schema.string.nullableAndOptional({}, [rules.regex(/(^|[^@\w])@(\w{1,64})\b/)]), }); - public messages = {}; + public messages = { + 'phone_number.unique': 'This phone number is not available.', + }; } From 971a4b8c218ee89bec8270acb71530ed2abeea71 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 10 Feb 2022 22:22:40 +0300 Subject: [PATCH 07/18] feat: added first impl of messenger --- app/Controllers/Ws/OnlineController.ts | 2 +- app/Services/WsService.ts | 24 ++++++++++++++++++++++-- app/Store/SessionStore.ts | 8 +++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/Controllers/Ws/OnlineController.ts b/app/Controllers/Ws/OnlineController.ts index 1d2c1fc..59b132b 100644 --- a/app/Controllers/Ws/OnlineController.ts +++ b/app/Controllers/Ws/OnlineController.ts @@ -23,7 +23,7 @@ export default class OnlineController { this.socket.on('disconnect', reason => this.onDisconnected(reason)); } - public sendOnline(online: string[]) { + public sendOnline(online: [string, { userId: string; userName: string }][]) { this.io.sockets.emit('user:online', online); } diff --git a/app/Services/WsService.ts b/app/Services/WsService.ts index 84c05dc..1e118e0 100644 --- a/app/Services/WsService.ts +++ b/app/Services/WsService.ts @@ -19,7 +19,12 @@ export interface ServerToClientEvents { */ 'user:session': (data: { sessionId: string | undefined; userId: string | undefined }) => void; 'user:connected': (data: { userId: string; userName: string }) => void; - 'user:online': (data: string[]) => void; + 'user:online': (data: [string, { userId: string; userName: string }][]) => void; + + /** + * Message + */ + 'message:private': (data: { content: string; from: string; to: string }) => void; } export interface ClientToServerEvents { @@ -28,6 +33,11 @@ export interface ClientToServerEvents { */ 'user:logout': () => void; 'user:status': () => void; + + /** + * Message + */ + 'message:private': (data: { content: string; to: string }) => void; } export interface InterServerEvents {} @@ -76,18 +86,28 @@ class WsService { } private listen() { - this.io.on('connection', socket => { + this.io.on('connection', async socket => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const onlineController = new OnlineController(socket, this.io, this.sessionStore); const { sessionId, userId, userName } = socket.data; const isExistSocketData = !!(sessionId && userId && userName); + if (isExistSocketData) { + socket.join(userId); + } + /** * Send session to client */ socket.emit('user:session', { sessionId: socket.data.sessionId, userId: socket.data.userId }); + socket.on('message:private', ({ content, to }) => { + if (isExistSocketData) { + this.io.to(to).emit('message:private', { content, from: userId, to }); + } + }); + /** * Destroy session on user logout request */ diff --git a/app/Store/SessionStore.ts b/app/Store/SessionStore.ts index 9f6a935..6510075 100644 --- a/app/Store/SessionStore.ts +++ b/app/Store/SessionStore.ts @@ -72,7 +72,13 @@ class RedisSessionStore { */ public async getOnlineSessions() { const sessions = await this.getSessions(); - const online = new Set(sessions.filter((s): s is SessionDataType => s?.connected === true).map(s => s?.userId)); + const online = new Map(); + + sessions.forEach(s => { + if (s?.connected === true) { + online.set(s.userId, s); + } + }); return Array.from(online); } From 3e16fe6a190c9c700ea4b48b436246f88ea0b8ab Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Fri, 11 Feb 2022 20:18:22 +0300 Subject: [PATCH 08/18] feat: sending message time --- app/Services/WsService.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/Services/WsService.ts b/app/Services/WsService.ts index 1e118e0..781def5 100644 --- a/app/Services/WsService.ts +++ b/app/Services/WsService.ts @@ -12,6 +12,7 @@ import OnlineController from 'App/Controllers/Ws/OnlineController'; */ import AuthMiddleware from 'App/Middleware/Ws/Auth'; import RedisSessionStore from 'App/Store/SessionStore'; +import { DateTime } from 'luxon'; export interface ServerToClientEvents { /** @@ -22,9 +23,9 @@ export interface ServerToClientEvents { 'user:online': (data: [string, { userId: string; userName: string }][]) => void; /** - * Message + * Chat */ - 'message:private': (data: { content: string; from: string; to: string }) => void; + 'chat:message': (data: { content: string; time: string; from: string; to: string }) => void; } export interface ClientToServerEvents { @@ -35,9 +36,9 @@ export interface ClientToServerEvents { 'user:status': () => void; /** - * Message + * Chat */ - 'message:private': (data: { content: string; to: string }) => void; + 'chat:message': (data: { content: string; to: string }) => void; } export interface InterServerEvents {} @@ -102,9 +103,9 @@ class WsService { */ socket.emit('user:session', { sessionId: socket.data.sessionId, userId: socket.data.userId }); - socket.on('message:private', ({ content, to }) => { + socket.on('chat:message', ({ content, to }) => { if (isExistSocketData) { - this.io.to(to).emit('message:private', { content, from: userId, to }); + this.io.to(to).emit('chat:message', { content, from: userId, to, time: DateTime.now().toISO() }); } }); From 28fe68aa267a696e97b40d9deed97ee0d27b6026 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sat, 12 Feb 2022 18:23:20 +0300 Subject: [PATCH 09/18] feat: chat history finished --- app/Controllers/Http/Api/v1/MeController.ts | 14 +++++ app/Services/MeService.ts | 64 +++++++++++++++++-- app/Services/WsService.ts | 26 +++++++- app/Store/MessageStore.ts | 69 +++++++++++++++++++++ config/redis.ts | 8 +++ contracts/redis.ts | 1 + package-lock.json | 32 +++++----- package.json | 2 +- routes/apis/v1/me.ts | 3 + 9 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 app/Store/MessageStore.ts diff --git a/app/Controllers/Http/Api/v1/MeController.ts b/app/Controllers/Http/Api/v1/MeController.ts index 21ce2c9..2216cf4 100644 --- a/app/Controllers/Http/Api/v1/MeController.ts +++ b/app/Controllers/Http/Api/v1/MeController.ts @@ -124,6 +124,20 @@ export default class MeController extends BaseController { return this.sendResponse(ctx, result.data, result.message, result.status); } + + /** + * Get messages of conversation by chat id + * GET /me/messages + */ + public async chatHistory(ctx: HttpContextContract) { + const result = await this.meService.fetchChatHistory(ctx.params.chat_id, ctx.auth); + + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } + + return this.sendResponse(ctx, result.data, result.message, result.status); + } } new Ioc().make(MeController); diff --git a/app/Services/MeService.ts b/app/Services/MeService.ts index ebd7875..15c74f4 100644 --- a/app/Services/MeService.ts +++ b/app/Services/MeService.ts @@ -5,15 +5,36 @@ import Redis from '@ioc:Adonis/Addons/Redis'; import Mail from '@ioc:Adonis/Addons/Mail'; import Hash from '@ioc:Adonis/Core/Hash'; import Logger from '@ioc:Adonis/Core/Logger'; + +/** + * Datatypes + */ +import RoleEnum from 'App/Datatypes/Enums/RoleEnum'; import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum'; import IResponse from 'App/Datatypes/Interfaces/IResponse'; +import CodeErrorEnum from 'App/Datatypes/Enums/CodeErrorEnum'; + +/** + * Repositories + */ import ContactRepository from 'App/Repositories/ContactRepository'; import UserRepository from 'App/Repositories/UserRepository'; -import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator'; -import CodeErrorEnum from 'App/Datatypes/Enums/CodeErrorEnum'; -import RoleHelper from 'App/Helpers/RoleHelper'; -import RoleEnum from 'App/Datatypes/Enums/RoleEnum'; + +/** + * Validator + */ import UpdateUserInfoValidator from 'App/Validators/User/UpdateUserInfoValidator'; +import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator'; + +/** + * Helpers + */ +import RoleHelper from 'App/Helpers/RoleHelper'; + +/** + * Services + */ +import Ws from './WsService'; @inject() export default class MeService { @@ -336,6 +357,41 @@ export default class MeService { data: { ...data }, }; } + + /** + * Fetch history of chat + * + * @param chatId Id of user + * @param auth AuthContract + * @returns IResponse + */ + public async fetchChatHistory(chatId: string, auth: AuthContract): Promise { + const firstUser = await auth.use(this.authGuard).authenticate(); + const secondUser = await this.userRepository.getById(chatId); + + if (!secondUser) { + return { + success: false, + status: HttpStatusEnum.NOT_FOUND, + message: 'Chat not found.', + data: {}, + error: { + code: 'E_NOT_FOUND', + }, + }; + } + + const history = await Ws.messageStore.getHistory(firstUser.id, secondUser.id); + + return { + success: true, + status: HttpStatusEnum.OK, + message: 'Chat history fetched.', + data: { + history, + }, + }; + } } new Ioc().make(MeService); diff --git a/app/Services/WsService.ts b/app/Services/WsService.ts index 781def5..731016f 100644 --- a/app/Services/WsService.ts +++ b/app/Services/WsService.ts @@ -1,6 +1,7 @@ import { Server } from 'socket.io'; import Redis from '@ioc:Adonis/Addons/Redis'; import AdonisServer from '@ioc:Adonis/Core/Server'; +import { DateTime } from 'luxon'; /** * Controllers @@ -11,8 +12,12 @@ import OnlineController from 'App/Controllers/Ws/OnlineController'; * Middlewares */ import AuthMiddleware from 'App/Middleware/Ws/Auth'; + +/** + * Stores + */ import RedisSessionStore from 'App/Store/SessionStore'; -import { DateTime } from 'luxon'; +import RedisMessageStore from 'App/Store/MessageStore'; export interface ServerToClientEvents { /** @@ -54,6 +59,8 @@ class WsService { public sessionStore: RedisSessionStore; + public messageStore: RedisMessageStore; + public booted = false; public boot() { @@ -68,7 +75,12 @@ class WsService { credentials: true, }, }); + + /** + * Stores + */ this.sessionStore = new RedisSessionStore(Redis.connection('session')); + this.messageStore = new RedisMessageStore(Redis.connection('message')); /** * Clean up sessions before start @@ -103,9 +115,17 @@ class WsService { */ socket.emit('user:session', { sessionId: socket.data.sessionId, userId: socket.data.userId }); - socket.on('chat:message', ({ content, to }) => { + socket.on('chat:message', async ({ content, to }) => { if (isExistSocketData) { - this.io.to(to).emit('chat:message', { content, from: userId, to, time: DateTime.now().toISO() }); + const message = { + content, + from: userId, + to, + time: DateTime.now().toISO(), + }; + + await this.messageStore.add(userId, to, message); + this.io.to(to).emit('chat:message', message); } }); diff --git a/app/Store/MessageStore.ts b/app/Store/MessageStore.ts new file mode 100644 index 0000000..f289bdb --- /dev/null +++ b/app/Store/MessageStore.ts @@ -0,0 +1,69 @@ +import { RedisConnectionContract } from '@ioc:Adonis/Addons/Redis'; +import Logger from '@ioc:Adonis/Core/Logger'; +import { DateTime } from 'luxon'; +import { nanoid } from 'nanoid'; + +type MessageDataType = { + from: string; + to: string; + content: string; + time: string; +}; + +const MESSAGE_TTL = 185 * 24 * 60 * 60; // 185 days + +class RedisMessageStore { + public redisClient: RedisConnectionContract; + + constructor(redisClient: RedisConnectionContract) { + this.redisClient = redisClient; + } + + public async add(firstUserId: string, secondUserId: string, data: MessageDataType): Promise<'OK' | 'ERROR'> { + try { + const msgId = nanoid(); + const msgKey = `${firstUserId}:${msgId}`; + const key = `message:${msgKey}`; + + await this.redisClient + .multi() + .hmset(key, { + from: data.from, + to: data.to, + content: data.content, + time: DateTime.now().toISO(), + }) + .expire(key, MESSAGE_TTL) + .exec(); + + const convKey = this.createConversationKey(firstUserId, secondUserId); + await this.redisClient.lpush(convKey, msgKey); + + return 'OK'; + } catch (error) { + Logger.error(error); + return 'ERROR'; + } + } + + public async getHistory(firstUserId: string, secondUserId: string): Promise { + const convKey = this.createConversationKey(firstUserId, secondUserId); + const msgKeys = await this.redisClient.lrange(convKey, 0, -1); + const messages = await Promise.all(msgKeys.map(key => `message:${key}`).map(key => this.getMessage(key))); + + return messages.sort((m1, m2) => new Date(m1.time).valueOf() - new Date(m2.time).valueOf()); + } + + private async getMessage(key: string): Promise { + const message = (await this.redisClient.hgetall(key)) as MessageDataType; + return message; + } + + // eslint-disable-next-line class-methods-use-this + private createConversationKey(userId1: string, userId2: string): string { + const [id1, id2] = [userId1, userId2].sort(); + return `conversation:${id1}-${id2}`; + } +} + +export default RedisMessageStore; diff --git a/config/redis.ts b/config/redis.ts index a19f00e..075fa67 100644 --- a/config/redis.ts +++ b/config/redis.ts @@ -51,6 +51,14 @@ const redisConfig: RedisConfig = { keyPrefix: 'session-', healthCheck: true, }, + message: { + host: Env.get('REDIS_HOST'), + port: Env.get('REDIS_PORT'), + password: Env.get('REDIS_PASSWORD', ''), + keyPrefix: '', + db: 2, + healthCheck: true, + }, }, }; diff --git a/contracts/redis.ts b/contracts/redis.ts index 55f2fec..698585a 100644 --- a/contracts/redis.ts +++ b/contracts/redis.ts @@ -9,5 +9,6 @@ declare module '@ioc:Adonis/Addons/Redis' { interface RedisConnectionsList { local: RedisConnectionConfig; session: RedisConnectionConfig; + message: RedisConnectionConfig; } } diff --git a/package-lock.json b/package-lock.json index 86a0dc0..df2a993 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "1.0.1", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -937,13 +937,13 @@ } }, "@adonisjs/redis": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@adonisjs/redis/-/redis-7.0.9.tgz", - "integrity": "sha512-d+NDJuC3BG2p5YhH1vMqLXzgWDaaosk2YTklmCyvZIT0LAueXNVCyJrxduAdRA2EH/81Y5MyRhubEBnp/wQl4w==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@adonisjs/redis/-/redis-7.1.0.tgz", + "integrity": "sha512-jbe6gYMWmONVKUxQRM4Cu3851YofK91Tr1nzT1P924i/8Nb4btW4tYoU9qmjRaFfjfybQPordmEcFXXSiABDhw==", "requires": { - "@poppinss/utils": "^3.2.0", - "@types/ioredis": "^4.27.1", - "ioredis": "^4.27.8" + "@poppinss/utils": "^3.3.1", + "@types/ioredis": "^4.28.3", + "ioredis": "^4.28.2" }, "dependencies": { "@poppinss/utils": { @@ -971,9 +971,9 @@ } }, "slugify": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.1.tgz", - "integrity": "sha512-5ofqMTbetNhxlzjYYLBaZFQd6oiTuSkQlyfPEFIMwgUABlZQ0hbk5xIV9Ydd5jghWeRoO7GkiJliUvTpLOjNRA==" + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.5.tgz", + "integrity": "sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ==" } } }, @@ -2503,9 +2503,9 @@ "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" }, "@types/ioredis": { - "version": "4.27.8", - "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.27.8.tgz", - "integrity": "sha512-THsyghYuFI6h/UzwEaeSeagRxiDG1P/NIiL5uTjN7bcbQHwDP6nMWJfEmY0iuu3pOzl1j0FgRpAYz7WWX2eW0Q==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.8.tgz", + "integrity": "sha512-mULOyO2smtvkE1zmzRRA4P0+1UjEqusi014kXOL1q3CY0RgqkR5/wKvv+vAJbPw2Q66wPyylKeevUy+m/FaRMg==", "requires": { "@types/node": "*" } @@ -5525,9 +5525,9 @@ "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" }, "ioredis": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.0.tgz", - "integrity": "sha512-I+zkeeWp3XFgPT2CtJKxvaF5FjGBGt4yGYljRjQecdQKteThuAsKqffeF1lgHVlYnuNeozRbPOCDNZ7tDWPeig==", + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz", + "integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==", "requires": { "cluster-key-slot": "^1.1.0", "debug": "^4.3.1", diff --git a/package.json b/package.json index 878c3be..c1c19b0 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@adonisjs/drive-s3": "^1.0.5", "@adonisjs/lucid": "^16.3.2", "@adonisjs/mail": "^7.2.4", - "@adonisjs/redis": "^7.0.9", + "@adonisjs/redis": "^7.1.0", "@adonisjs/repl": "^3.1.7", "@adonisjs/view": "^6.1.1", "@types/socket.io": "^3.0.2", diff --git a/routes/apis/v1/me.ts b/routes/apis/v1/me.ts index 38e79e3..c386537 100644 --- a/routes/apis/v1/me.ts +++ b/routes/apis/v1/me.ts @@ -2,6 +2,9 @@ import Route from '@ioc:Adonis/Core/Route'; Route.group(() => { Route.get('/', 'Api/v1/MeController.show').middleware('role:admin,teacher,student').as('me.show'); + Route.get('/messages/:chat_id', 'Api/v1/MeController.chatHistory') + .middleware('role:admin,teacher,student') + .as('me.chat-history'); Route.patch('/info', 'Api/v1/MeController.updateInfo').middleware('role:admin,teacher,student').as('me.update-info'); Route.patch('/email', 'Api/v1/MeController.changeEmail') .middleware('role:admin,teacher,student') From 046e3a28f30c531d526419b3b3c521a412d8e5a1 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sat, 12 Feb 2022 22:26:07 +0300 Subject: [PATCH 10/18] fix: error while updating user info --- CHANGELOG.md | 2 ++ app/Services/MeService.ts | 12 ++++-------- config/cloudinary.ts | 11 ----------- 3 files changed, 6 insertions(+), 19 deletions(-) delete mode 100644 config/cloudinary.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 76a1971..77c40e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,13 @@ ## Added +- Chat between users. (chat history). - About description for user and `last_login` column date - Updating info api route of authorized user ### Fix +- Fixed updating user info - Cleaning up session store before starting websocket server ## [1.1.0] - 2022-01-31 diff --git a/app/Services/MeService.ts b/app/Services/MeService.ts index 15c74f4..0343a3b 100644 --- a/app/Services/MeService.ts +++ b/app/Services/MeService.ts @@ -314,6 +314,7 @@ export default class MeService { auth: AuthContract ): Promise { const user = await auth.use(this.authGuard).authenticate(); + const { about, ...newContacts } = data; if (Object.keys(data).length === 0) { return { @@ -330,7 +331,7 @@ export default class MeService { /** * Personal info updating */ - if (data.about !== undefined) { + if (about !== undefined) { await this.userRepository.updateInfo(user.id, data); } @@ -338,15 +339,10 @@ export default class MeService { * Contacts updating */ if (data.phone_number !== undefined || data.twitter_id !== undefined || data.telegram_id !== undefined) { - const contacts = await this.contactRepository.update(user, { - phone_number: data.phone_number, - twitter_id: data.twitter_id, - telegram_id: data.telegram_id, - vk_id: data.vk_id, - }); + const contacts = await this.contactRepository.update(user, newContacts); if (!contacts) { - await this.contactRepository.create(user, data); + await this.contactRepository.create(user, newContacts); } } diff --git a/config/cloudinary.ts b/config/cloudinary.ts deleted file mode 100644 index 17782fd..0000000 --- a/config/cloudinary.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Env from '@ioc:Adonis/Core/Env'; -import { CloudinaryConfig } from '@ioc:Adonis/Addons/Cloudinary'; - -const cloudinaryConfig: CloudinaryConfig = { - cloudName: Env.get('CLOUDINARY_CLOUD_NAME'), - apiKey: Env.get('CLOUDINARY_API_KEY'), - apiSecret: Env.get('CLOUDINARY_API_SECRET'), - secure: Env.get('CLOUDINARY_SECURE', true), -}; - -export default cloudinaryConfig; From 4056dde970b59071d28fc23cecc7dc7a706b2a62 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Mon, 14 Feb 2022 21:58:07 +0300 Subject: [PATCH 11/18] fix: fixed role check middleware --- app/Middleware/Role.ts | 19 +++++++++++++++--- app/Services/AuthService.ts | 1 + .../Contacts/UpdateContactsValidator.ts | 20 +++++++++++++++---- .../User/UpdateUserInfoValidator.ts | 18 ++++++++++++++--- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/app/Middleware/Role.ts b/app/Middleware/Role.ts index ac9b78b..a6ca473 100644 --- a/app/Middleware/Role.ts +++ b/app/Middleware/Role.ts @@ -1,5 +1,6 @@ import { Exception } from '@adonisjs/core/build/standalone'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +import User from 'App/Models/User'; export default class Role { error: { @@ -23,7 +24,7 @@ export default class Role { * @param {string[]} roles - Array of roles for route * @param {string[]} userRole - Array of roles of current user */ - protected checkRoles(roles: string[], userRoles: string[]): void { + protected check(roles: string[], userRoles: string[]): void { if (!roles.some(role => userRoles.includes(role))) { /** * Failed to verify user rights @@ -33,8 +34,20 @@ export default class Role { } public async handle({ auth }: HttpContextContract, next: () => Promise, roles: string[]) { - const { userRoles } = auth.use('api').token?.meta; - this.checkRoles(roles, userRoles); + const { userId } = auth.use('api').token?.meta; + const user = await User.query().where('id', userId).preload('roles').first(); + + if (!user) { + /** + * User not found + */ + throw new Exception('Cannot identify current user.', this.error.status, this.error.code); + } + + this.check( + roles, + user.roles.map(role => role.slug) + ); await next(); } } diff --git a/app/Services/AuthService.ts b/app/Services/AuthService.ts index bf0e212..b3bdcc2 100644 --- a/app/Services/AuthService.ts +++ b/app/Services/AuthService.ts @@ -66,6 +66,7 @@ export default class AuthService { */ const token = await ctx.auth.use(this.authGuard).generate(user, { expiresIn: '1d', + userId: user.id, userRoles: user.roles.map(role => role.slug), userName: user.fullname, }); diff --git a/app/Validators/Contacts/UpdateContactsValidator.ts b/app/Validators/Contacts/UpdateContactsValidator.ts index 5f98644..d8bef23 100644 --- a/app/Validators/Contacts/UpdateContactsValidator.ts +++ b/app/Validators/Contacts/UpdateContactsValidator.ts @@ -12,12 +12,24 @@ export default class UpdateContactsValidator { }), rules.unique({ table: 'contacts', column: 'phone_number' }), ]), - vk_id: schema.string.nullableAndOptional({}, [rules.regex(/^([a-zA-Z0-9_]){1,64}$/)]), - twitter_id: schema.string.nullableAndOptional({}, [rules.regex(/(^|[^@\w])@(\w{1,15})\b/)]), - telegram_id: schema.string.nullableAndOptional({}, [rules.regex(/(^|[^@\w])@(\w{1,64})\b/)]), + vk_id: schema.string.nullableAndOptional({}, [ + rules.regex(/^([a-zA-Z0-9_]){1,64}$/), + rules.unique({ table: 'contacts', column: 'vk_id' }), + ]), + twitter_id: schema.string.nullableAndOptional({}, [ + rules.regex(/(^|[^@\w])@(\w{1,15})\b/), + rules.unique({ table: 'contacts', column: 'twitter_id' }), + ]), + telegram_id: schema.string.nullableAndOptional({}, [ + rules.regex(/(^|[^@\w])@(\w{1,64})\b/), + rules.unique({ table: 'contacts', column: 'telegram_id' }), + ]), }); public messages = { - 'phone_number.unique': 'This phone number is not available', + 'phone_number.unique': 'This phone number is not available.', + 'vk_id.unique': 'This ID is not available.', + 'twitter_id.unique': 'Twitter username is not available.', + 'telegram_id.unique': 'Telegram username is not available.', }; } diff --git a/app/Validators/User/UpdateUserInfoValidator.ts b/app/Validators/User/UpdateUserInfoValidator.ts index 86682aa..762ad17 100644 --- a/app/Validators/User/UpdateUserInfoValidator.ts +++ b/app/Validators/User/UpdateUserInfoValidator.ts @@ -10,12 +10,24 @@ export default class UpdateUserInfoValidator { rules.mobile({ locales: ['ru-RU', 'en-US'], strict: true }), rules.unique({ table: 'contacts', column: 'phone_number' }), ]), - vk_id: schema.string.nullableAndOptional({}, [rules.regex(/^([a-zA-Z0-9_]){1,64}$/)]), - twitter_id: schema.string.nullableAndOptional({}, [rules.regex(/(^|[^@\w])@(\w{1,15})\b/)]), - telegram_id: schema.string.nullableAndOptional({}, [rules.regex(/(^|[^@\w])@(\w{1,64})\b/)]), + vk_id: schema.string.nullableAndOptional({}, [ + rules.regex(/^([a-zA-Z0-9_]){1,64}$/), + rules.unique({ table: 'contacts', column: 'vk_id' }), + ]), + twitter_id: schema.string.nullableAndOptional({}, [ + rules.regex(/(^|[^@\w])@(\w{1,15})\b/), + rules.unique({ table: 'contacts', column: 'twitter_id' }), + ]), + telegram_id: schema.string.nullableAndOptional({}, [ + rules.regex(/(^|[^@\w])@(\w{1,64})\b/), + rules.unique({ table: 'contacts', column: 'telegram_id' }), + ]), }); public messages = { 'phone_number.unique': 'This phone number is not available.', + 'vk_id.unique': 'This ID is not available.', + 'twitter_id.unique': 'Twitter username is not available.', + 'telegram_id.unique': 'Telegram username is not available.', }; } From 877e15bcde6542e5909f4eb30fcb67b8d004cbb2 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 20 Feb 2022 23:02:11 +0300 Subject: [PATCH 12/18] feat: saving notifications --- app/Services/MeService.ts | 7 ++++- app/Services/WsService.ts | 5 ++++ app/Store/NotificationStore.ts | 53 ++++++++++++++++++++++++++++++++++ config/redis.ts | 8 +++++ contracts/redis.ts | 1 + 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 app/Store/NotificationStore.ts diff --git a/app/Services/MeService.ts b/app/Services/MeService.ts index 0343a3b..a0a8595 100644 --- a/app/Services/MeService.ts +++ b/app/Services/MeService.ts @@ -77,11 +77,16 @@ export default class MeService { }); } + const notifications = await Ws.notificationStore.getNotifications(user.id); + return { success: true, status: HttpStatusEnum.OK, message: 'Fetched authorized user data.', - data: user, + data: { + ...user.toJSON(), + notifications, + }, }; } diff --git a/app/Services/WsService.ts b/app/Services/WsService.ts index 731016f..fb08d15 100644 --- a/app/Services/WsService.ts +++ b/app/Services/WsService.ts @@ -18,6 +18,7 @@ import AuthMiddleware from 'App/Middleware/Ws/Auth'; */ import RedisSessionStore from 'App/Store/SessionStore'; import RedisMessageStore from 'App/Store/MessageStore'; +import RedisNotificationStore from 'App/Store/NotificationStore'; export interface ServerToClientEvents { /** @@ -61,6 +62,8 @@ class WsService { public messageStore: RedisMessageStore; + public notificationStore: RedisNotificationStore; + public booted = false; public boot() { @@ -81,6 +84,7 @@ class WsService { */ this.sessionStore = new RedisSessionStore(Redis.connection('session')); this.messageStore = new RedisMessageStore(Redis.connection('message')); + this.notificationStore = new RedisNotificationStore(Redis.connection('notification')); /** * Clean up sessions before start @@ -125,6 +129,7 @@ class WsService { }; await this.messageStore.add(userId, to, message); + await this.notificationStore.add(to, `You received a new message.`); this.io.to(to).emit('chat:message', message); } }); diff --git a/app/Store/NotificationStore.ts b/app/Store/NotificationStore.ts new file mode 100644 index 0000000..9009ac5 --- /dev/null +++ b/app/Store/NotificationStore.ts @@ -0,0 +1,53 @@ +import { RedisConnectionContract } from '@ioc:Adonis/Addons/Redis'; +import Logger from '@ioc:Adonis/Core/Logger'; +import { nanoid } from 'nanoid'; + +const NOTIFICATION_TTL = 185 * 24 * 60 * 60; // 185 days + +type NotificationType = { + content: string; + time: string; +}; + +class RedisNotificationStore { + public redisClient: RedisConnectionContract; + + constructor(redisClient: RedisConnectionContract) { + this.redisClient = redisClient; + } + + public async add(userId: string, message: string): Promise<'OK' | 'ERROR'> { + try { + const key = `notification:${nanoid()}`; + const timestamp = Date.now(); + + await this.redisClient.zadd(`user:${userId}`, timestamp, key); + await this.redisClient + .multi() + .hmset(key, { + content: message, + time: new Date(timestamp).toISOString(), + }) + .expire(key, NOTIFICATION_TTL) + .exec(); + + return 'OK'; + } catch (error) { + Logger.error(error); + return 'ERROR'; + } + } + + public async getNotifications(userId: string) { + const keys = await this.redisClient.zrevrangebyscore(`user:${userId}`, '+inf', '-inf'); + const notifications = await Promise.all(keys.map(key => this.get(key))); + return notifications; + } + + private async get(key: string): Promise { + const notification = (await this.redisClient.hgetall(key)) as NotificationType; + return notification; + } +} + +export default RedisNotificationStore; diff --git a/config/redis.ts b/config/redis.ts index 075fa67..805b5bc 100644 --- a/config/redis.ts +++ b/config/redis.ts @@ -59,6 +59,14 @@ const redisConfig: RedisConfig = { db: 2, healthCheck: true, }, + notification: { + host: Env.get('REDIS_HOST'), + port: Env.get('REDIS_PORT'), + password: Env.get('REDIS_PASSWORD', ''), + keyPrefix: '', + db: 3, + healthCheck: true, + }, }, }; diff --git a/contracts/redis.ts b/contracts/redis.ts index 698585a..c124ecc 100644 --- a/contracts/redis.ts +++ b/contracts/redis.ts @@ -10,5 +10,6 @@ declare module '@ioc:Adonis/Addons/Redis' { local: RedisConnectionConfig; session: RedisConnectionConfig; message: RedisConnectionConfig; + notification: RedisConnectionConfig; } } From cd811195f1ea11290e2c30798f19d0847447877f Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Wed, 23 Feb 2022 20:02:37 +0300 Subject: [PATCH 13/18] feat: added history limit to messages store --- CHANGELOG.md | 1 + app/Services/WsService.ts | 7 ++++--- app/Store/MessageStore.ts | 8 ++++++++ app/Store/NotificationStore.ts | 26 ++++++++++++++------------ 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c40e9..92aaab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ## Added +- Saving latest notifications about new messages - Chat between users. (chat history). - About description for user and `last_login` column date - Updating info api route of authorized user diff --git a/app/Services/WsService.ts b/app/Services/WsService.ts index fb08d15..65fb100 100644 --- a/app/Services/WsService.ts +++ b/app/Services/WsService.ts @@ -31,7 +31,7 @@ export interface ServerToClientEvents { /** * Chat */ - 'chat:message': (data: { content: string; time: string; from: string; to: string }) => void; + 'chat:message': (data: { content: string; time: string; from: string; to: string; notificationId: string }) => void; } export interface ClientToServerEvents { @@ -129,8 +129,9 @@ class WsService { }; await this.messageStore.add(userId, to, message); - await this.notificationStore.add(to, `You received a new message.`); - this.io.to(to).emit('chat:message', message); + + const notification = await this.notificationStore.add(to, `You received a new private message`, 'MESSAGE'); + this.io.to(to).emit('chat:message', { ...message, notificationId: notification.id }); } }); diff --git a/app/Store/MessageStore.ts b/app/Store/MessageStore.ts index f289bdb..1403b0b 100644 --- a/app/Store/MessageStore.ts +++ b/app/Store/MessageStore.ts @@ -11,6 +11,7 @@ type MessageDataType = { }; const MESSAGE_TTL = 185 * 24 * 60 * 60; // 185 days +const MAX_MESSAGES_HISTORY = 100; class RedisMessageStore { public redisClient: RedisConnectionContract; @@ -37,8 +38,15 @@ class RedisMessageStore { .exec(); const convKey = this.createConversationKey(firstUserId, secondUserId); + const msgKeys = await this.redisClient.lrange(convKey, 0, -1); + await this.redisClient.lpush(convKey, msgKey); + if (msgKeys.length >= MAX_MESSAGES_HISTORY) { + const deletedMsgKey = await this.redisClient.rpop(convKey); + await this.redisClient.del(`message:${deletedMsgKey}`); + } + return 'OK'; } catch (error) { Logger.error(error); diff --git a/app/Store/NotificationStore.ts b/app/Store/NotificationStore.ts index 9009ac5..bd42094 100644 --- a/app/Store/NotificationStore.ts +++ b/app/Store/NotificationStore.ts @@ -5,6 +5,8 @@ import { nanoid } from 'nanoid'; const NOTIFICATION_TTL = 185 * 24 * 60 * 60; // 185 days type NotificationType = { + id: string; + type: 'SYSTEM' | 'MESSAGE'; content: string; time: string; }; @@ -16,25 +18,25 @@ class RedisNotificationStore { this.redisClient = redisClient; } - public async add(userId: string, message: string): Promise<'OK' | 'ERROR'> { + public async add(userId: string, content: string, type: NotificationType['type']): Promise { try { - const key = `notification:${nanoid()}`; + const id = nanoid(); + const key = `notification:${id}`; const timestamp = Date.now(); + const notification = { + id, + content, + time: new Date(timestamp).toISOString(), + type, + }; await this.redisClient.zadd(`user:${userId}`, timestamp, key); - await this.redisClient - .multi() - .hmset(key, { - content: message, - time: new Date(timestamp).toISOString(), - }) - .expire(key, NOTIFICATION_TTL) - .exec(); + await this.redisClient.multi().hmset(key, notification).expire(key, NOTIFICATION_TTL).exec(); - return 'OK'; + return notification; } catch (error) { Logger.error(error); - return 'ERROR'; + return Promise.reject(error); } } From c6472013a368e1845458190f01cf21b8ab7d8bd5 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Mon, 28 Feb 2022 21:39:34 +0300 Subject: [PATCH 14/18] feat: delete notifications --- app/Services/WsService.ts | 23 ++++++++++++++++++++--- app/Store/NotificationStore.ts | 8 ++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/app/Services/WsService.ts b/app/Services/WsService.ts index 65fb100..5d6370d 100644 --- a/app/Services/WsService.ts +++ b/app/Services/WsService.ts @@ -31,7 +31,13 @@ export interface ServerToClientEvents { /** * Chat */ - 'chat:message': (data: { content: string; time: string; from: string; to: string; notificationId: string }) => void; + 'chat:message': (data: { + content: string; + time: string; + from: string; + to: string; + notificationId?: string | undefined; + }) => void; } export interface ClientToServerEvents { @@ -45,6 +51,11 @@ export interface ClientToServerEvents { * Chat */ 'chat:message': (data: { content: string; to: string }) => void; + + /** + * Notification + */ + 'notification:read': (data: { userId: string; ids: string[] }) => void; } export interface InterServerEvents {} @@ -130,11 +141,17 @@ class WsService { await this.messageStore.add(userId, to, message); - const notification = await this.notificationStore.add(to, `You received a new private message`, 'MESSAGE'); - this.io.to(to).emit('chat:message', { ...message, notificationId: notification.id }); + if (userId !== to) { + const notification = await this.notificationStore.add(to, `You received a new private message`, 'MESSAGE'); + this.io.to(to).emit('chat:message', { ...message, notificationId: notification.id }); + } else { + this.io.to(to).emit('chat:message', { ...message }); + } } }); + socket.on('notification:read', data => this.notificationStore.del(data.userId, data.ids)); + /** * Destroy session on user logout request */ diff --git a/app/Store/NotificationStore.ts b/app/Store/NotificationStore.ts index bd42094..50e90e0 100644 --- a/app/Store/NotificationStore.ts +++ b/app/Store/NotificationStore.ts @@ -50,6 +50,14 @@ class RedisNotificationStore { const notification = (await this.redisClient.hgetall(key)) as NotificationType; return notification; } + + public async del(userId: string, keys: string[]): Promise { + const k = keys.map(v => `notification:${v}`); + const count = await this.redisClient.zrem(`user:${userId}`, k); + await this.redisClient.del(k); + + return count; + } } export default RedisNotificationStore; From 97ed27a50a207d30760576c831ea4200ad00add3 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Tue, 1 Mar 2022 18:10:04 +0300 Subject: [PATCH 15/18] feat: added chat controller --- app/Controllers/Http/Api/v1/ChatController.ts | 38 ++++++++++++++++ app/Store/MessageStore.ts | 43 ++++++++++++++++++- routes/apis/v1/chat.ts | 7 +++ routes/apis/v1/index.ts | 5 +++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 app/Controllers/Http/Api/v1/ChatController.ts create mode 100644 routes/apis/v1/chat.ts diff --git a/app/Controllers/Http/Api/v1/ChatController.ts b/app/Controllers/Http/Api/v1/ChatController.ts new file mode 100644 index 0000000..7f9e3ad --- /dev/null +++ b/app/Controllers/Http/Api/v1/ChatController.ts @@ -0,0 +1,38 @@ +import { inject, Ioc } from '@adonisjs/core/build/standalone'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +import { ConversationType } from 'App/Store/MessageStore'; +import IResponse from 'App/Datatypes/Interfaces/IResponse'; + +/** + * Enums + */ +import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum'; + +/** + * Services + */ +import WsService from 'App/Services/WsService'; + +import BaseController from '../../BaseController'; + +@inject() +export default class ChatController extends BaseController { + /** + * Get list of conversations (users) + * GET /chat/conversations + */ + public async getConversations(ctx: HttpContextContract) { + const user = await ctx.auth.authenticate(); + const data = await WsService.messageStore.getConversations(user.id); + const result: IResponse = { + success: true, + status: HttpStatusEnum.OK, + message: 'Fetched conversations.', + data, + }; + + return this.sendResponse(ctx, result.data, result.message, result.status); + } +} + +new Ioc().make(ChatController); diff --git a/app/Store/MessageStore.ts b/app/Store/MessageStore.ts index 1403b0b..a15e71e 100644 --- a/app/Store/MessageStore.ts +++ b/app/Store/MessageStore.ts @@ -1,5 +1,6 @@ import { RedisConnectionContract } from '@ioc:Adonis/Addons/Redis'; import Logger from '@ioc:Adonis/Core/Logger'; +import User from 'App/Models/User'; import { DateTime } from 'luxon'; import { nanoid } from 'nanoid'; @@ -10,6 +11,12 @@ type MessageDataType = { time: string; }; +export type ConversationType = { + userId: string; + fullname: string; + lastMessage: MessageDataType; +}; + const MESSAGE_TTL = 185 * 24 * 60 * 60; // 185 days const MAX_MESSAGES_HISTORY = 100; @@ -62,6 +69,40 @@ class RedisMessageStore { return messages.sort((m1, m2) => new Date(m1.time).valueOf() - new Date(m2.time).valueOf()); } + public async getConversations(userId: string): Promise { + /** + * Getting conversations that had some message history + */ + const conversations = await this.redisClient.keys(`conversation:*${userId}*`); + const userIds = conversations + .map(id => id.split(':')[1]) + .map(id => (userId !== id.split('&')[0] ? id.split('&')[0] : id.split('&')[1])); + + /** + * Fetch this users + */ + const users = await User.query().select('id', 'first_name', 'last_name').where('id', 'in', userIds); + + /** + * Fetch last message of each conversation + */ + const result = await Promise.all( + users.map(async u => { + const convKey = this.createConversationKey(userId, u.id); + const msgKey = await this.redisClient.lindex(convKey, 0); + const lastMessage = await this.getMessage(`message:${msgKey}`); + + return { + userId: u.id, + fullname: `${u.first_name} ${u.last_name}`, + lastMessage, + }; + }) + ); + + return result; + } + private async getMessage(key: string): Promise { const message = (await this.redisClient.hgetall(key)) as MessageDataType; return message; @@ -70,7 +111,7 @@ class RedisMessageStore { // eslint-disable-next-line class-methods-use-this private createConversationKey(userId1: string, userId2: string): string { const [id1, id2] = [userId1, userId2].sort(); - return `conversation:${id1}-${id2}`; + return `conversation:${id1}&${id2}`; } } diff --git a/routes/apis/v1/chat.ts b/routes/apis/v1/chat.ts new file mode 100644 index 0000000..0be2749 --- /dev/null +++ b/routes/apis/v1/chat.ts @@ -0,0 +1,7 @@ +import Route from '@ioc:Adonis/Core/Route'; + +Route.group(() => { + Route.get('/conversations', 'Api/v1/ChatController.getConversations') + .middleware('role:admin,teacher,student') + .as('chat.get-conversations'); +}).prefix('chat'); diff --git a/routes/apis/v1/index.ts b/routes/apis/v1/index.ts index a967e66..9b10d4f 100644 --- a/routes/apis/v1/index.ts +++ b/routes/apis/v1/index.ts @@ -68,6 +68,11 @@ Route.group(() => { * Stat module */ require('./stat'); + + /** + * Chat module + */ + require('./chat'); }).middleware('auth'); }) .prefix('v1') From a8c2e65bfb650a6b3f1cfb9e02670d349ad438f8 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Tue, 1 Mar 2022 22:46:48 +0300 Subject: [PATCH 16/18] fix: fixed error on deleting notification --- app/Store/NotificationStore.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Store/NotificationStore.ts b/app/Store/NotificationStore.ts index 50e90e0..dd98e57 100644 --- a/app/Store/NotificationStore.ts +++ b/app/Store/NotificationStore.ts @@ -52,6 +52,8 @@ class RedisNotificationStore { } public async del(userId: string, keys: string[]): Promise { + if (keys.length === 0) return 0; + const k = keys.map(v => `notification:${v}`); const count = await this.redisClient.zrem(`user:${userId}`, k); await this.redisClient.del(k); From 7332d7a0c9c93cbf92ba9a27d5ab68a584ad7672 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 6 Mar 2022 22:54:42 +0300 Subject: [PATCH 17/18] refactor: refactored routes import --- .adonisrc.json | 11 +-- routes/api.ts | 11 --- routes/apis/v1/index.ts | 79 ------------------- routes/index.ts | 34 -------- start/routes.ts | 17 ++++ {routes => start/routes}/apis/v1/auth.ts | 2 +- .../routes}/apis/v1/categories.ts | 4 +- {routes => start/routes}/apis/v1/chat.ts | 4 +- {routes => start/routes}/apis/v1/courses.ts | 4 +- {routes => start/routes}/apis/v1/health.ts | 4 +- {routes => start/routes}/apis/v1/images.ts | 4 +- {routes => start/routes}/apis/v1/lessons.ts | 4 +- {routes => start/routes}/apis/v1/me.ts | 4 +- {routes => start/routes}/apis/v1/stat.ts | 4 +- {routes => start/routes}/apis/v1/users.ts | 4 +- {routes => start/routes}/videos.ts | 0 16 files changed, 47 insertions(+), 143 deletions(-) delete mode 100644 routes/api.ts delete mode 100644 routes/apis/v1/index.ts delete mode 100644 routes/index.ts create mode 100644 start/routes.ts rename {routes => start/routes}/apis/v1/auth.ts (89%) rename {routes => start/routes}/apis/v1/categories.ts (91%) rename {routes => start/routes}/apis/v1/chat.ts (81%) rename {routes => start/routes}/apis/v1/courses.ts (98%) rename {routes => start/routes}/apis/v1/health.ts (88%) rename {routes => start/routes}/apis/v1/images.ts (87%) rename {routes => start/routes}/apis/v1/lessons.ts (95%) rename {routes => start/routes}/apis/v1/me.ts (95%) rename {routes => start/routes}/apis/v1/stat.ts (77%) rename {routes => start/routes}/apis/v1/users.ts (94%) rename {routes => start/routes}/videos.ts (100%) diff --git a/.adonisrc.json b/.adonisrc.json index e444ba7..5ada5b9 100644 --- a/.adonisrc.json +++ b/.adonisrc.json @@ -24,12 +24,7 @@ "Database": "database", "Contracts": "contracts" }, - "preloads": [ - "./routes/index", - "./start/kernel", - "./start/event", - "./start/bouncer" - ], + "preloads": ["./start/routes", "./start/kernel", "./start/event", "./start/bouncer"], "providers": [ "./providers/AppProvider", "@adonisjs/core", @@ -42,9 +37,7 @@ "@adonisjs/bouncer", "@adonisjs/drive-s3" ], - "aceProviders": [ - "@adonisjs/repl" - ], + "aceProviders": ["@adonisjs/repl"], "metaFiles": [ { "pattern": "resources/views/**/*.edge", diff --git a/routes/api.ts b/routes/api.ts deleted file mode 100644 index e78f64a..0000000 --- a/routes/api.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable global-require */ -import Route from '@ioc:Adonis/Core/Route'; - -Route.group(() => { - /** - * v1 - */ - require('./apis/v1/index'); -}) - .prefix('api') - .as('api'); diff --git a/routes/apis/v1/index.ts b/routes/apis/v1/index.ts deleted file mode 100644 index 9b10d4f..0000000 --- a/routes/apis/v1/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable global-require */ -import Route from '@ioc:Adonis/Core/Route'; - -Route.group(() => { - Route.get('/', () => { - return 'Backend API.'; - }).as('index'); - - /** - * - * Auth module - * - */ - require('./auth'); - - Route.group(() => { - /** - * - * Health check - * - */ - require('./health'); - - /** - * - * Users module - * - */ - require('./users'); - - /** - * - * Me module - * - */ - require('./me'); - - /** - * - * Courses module - * - */ - require('./courses'); - - /** - * - * Lessons module - * - */ - require('./lessons'); - - /** - * - * Images module - * - * This module is not necessary yet - */ - // require('./images'); - - /** - * - * Categories module - * - */ - require('./categories'); - - /** - * Stat module - */ - require('./stat'); - - /** - * Chat module - */ - require('./chat'); - }).middleware('auth'); -}) - .prefix('v1') - .as('v1'); diff --git a/routes/index.ts b/routes/index.ts deleted file mode 100644 index cfc24ed..0000000 --- a/routes/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint-disable global-require */ -import Route from '@ioc:Adonis/Core/Route'; - -/* -|-------------------------------------------------------------------------- -| Routes -|-------------------------------------------------------------------------- -| -| This file is dedicated for defining HTTP routes. A single file is enough -| for majority of projects, however you can define routes in different -| files and just make sure to import them inside this file. For example -| -| Define routes in following two files -| ├── start/routes/cart.ts -| ├── start/routes/customer.ts -| -| and then import them inside `start/routes.ts` as follows -| -| import './routes/cart' -| import './routes/customer' -| -*/ - -Route.group(() => { - /** - * Api routes - */ - require('./api'); - - /** - * Other routes - */ - require('./videos'); -}); diff --git a/start/routes.ts b/start/routes.ts new file mode 100644 index 0000000..57c0d27 --- /dev/null +++ b/start/routes.ts @@ -0,0 +1,17 @@ +/** + * API v1 + */ +import './routes/apis/v1/auth'; +import './routes/apis/v1/categories'; +import './routes/apis/v1/chat'; +import './routes/apis/v1/courses'; +import './routes/apis/v1/health'; +import './routes/apis/v1/lessons'; +import './routes/apis/v1/me'; +import './routes/apis/v1/stat'; +import './routes/apis/v1/users'; + +/** + * Other routes + */ +import './routes/videos'; diff --git a/routes/apis/v1/auth.ts b/start/routes/apis/v1/auth.ts similarity index 89% rename from routes/apis/v1/auth.ts rename to start/routes/apis/v1/auth.ts index 90d8ee8..3fb4fba 100644 --- a/routes/apis/v1/auth.ts +++ b/start/routes/apis/v1/auth.ts @@ -3,4 +3,4 @@ import Route from '@ioc:Adonis/Core/Route'; Route.group(() => { Route.post('login', 'Api/v1/AuthController.login').as('auth.login'); Route.post('logout', 'Api/v1/AuthController.logout').middleware('auth').as('auth.logout'); -}).prefix('auth'); +}).prefix('api/v1/auth'); diff --git a/routes/apis/v1/categories.ts b/start/routes/apis/v1/categories.ts similarity index 91% rename from routes/apis/v1/categories.ts rename to start/routes/apis/v1/categories.ts index 7e021fc..759dbff 100644 --- a/routes/apis/v1/categories.ts +++ b/start/routes/apis/v1/categories.ts @@ -6,4 +6,6 @@ Route.group(() => { Route.post('/', 'Api/v1/CategoriesController.create').middleware('role:admin,teacher').as('categories.create'); Route.delete('/:id', 'Api/v1/CategoriesController.delete').middleware('role:admin,teacher').as('categories.delete'); Route.patch('/:id', 'Api/v1/CategoriesController.update').middleware('role:admin,teacher').as('categories.update'); -}).prefix('categories'); +}) + .prefix('api/v1/categories') + .middleware('auth'); diff --git a/routes/apis/v1/chat.ts b/start/routes/apis/v1/chat.ts similarity index 81% rename from routes/apis/v1/chat.ts rename to start/routes/apis/v1/chat.ts index 0be2749..e5de68d 100644 --- a/routes/apis/v1/chat.ts +++ b/start/routes/apis/v1/chat.ts @@ -4,4 +4,6 @@ Route.group(() => { Route.get('/conversations', 'Api/v1/ChatController.getConversations') .middleware('role:admin,teacher,student') .as('chat.get-conversations'); -}).prefix('chat'); +}) + .prefix('api/v1/chat') + .middleware('auth'); diff --git a/routes/apis/v1/courses.ts b/start/routes/apis/v1/courses.ts similarity index 98% rename from routes/apis/v1/courses.ts rename to start/routes/apis/v1/courses.ts index 087a483..25d5708 100644 --- a/routes/apis/v1/courses.ts +++ b/start/routes/apis/v1/courses.ts @@ -61,4 +61,6 @@ Route.group(() => { Route.get('/:id/lessons-progress', 'Api/v1/CoursesController.getLessonsProgress') .middleware('role:admin,teacher,student') .as('courses.get-lessons-progress'); -}).prefix('courses'); +}) + .prefix('api/v1/courses') + .middleware('auth'); diff --git a/routes/apis/v1/health.ts b/start/routes/apis/v1/health.ts similarity index 88% rename from routes/apis/v1/health.ts rename to start/routes/apis/v1/health.ts index b3ba089..0090e9f 100644 --- a/routes/apis/v1/health.ts +++ b/start/routes/apis/v1/health.ts @@ -8,4 +8,6 @@ Route.group(() => { return report.healthy ? ctx.response.ok(report) : ctx.response.badRequest(report); }).as('health'); -}).prefix('health'); +}) + .prefix('api/v1/health') + .middleware('auth'); diff --git a/routes/apis/v1/images.ts b/start/routes/apis/v1/images.ts similarity index 87% rename from routes/apis/v1/images.ts rename to start/routes/apis/v1/images.ts index bae3929..fbbf7de 100644 --- a/routes/apis/v1/images.ts +++ b/start/routes/apis/v1/images.ts @@ -5,4 +5,6 @@ Route.group(() => { Route.delete('/:id', 'Api/v1/ImagesController.delete').as('images.delete'); Route.get('/', 'Api/v1/ImagesController.list').as('images.list'); Route.get('/:id', 'Api/v1/ImagesController.show').as('images.show'); -}).prefix('images'); +}) + .prefix('api/v1/images') + .middleware('auth'); diff --git a/routes/apis/v1/lessons.ts b/start/routes/apis/v1/lessons.ts similarity index 95% rename from routes/apis/v1/lessons.ts rename to start/routes/apis/v1/lessons.ts index 7893120..ee261f4 100644 --- a/routes/apis/v1/lessons.ts +++ b/start/routes/apis/v1/lessons.ts @@ -18,4 +18,6 @@ Route.group(() => { Route.get('/:id/progress', 'Api/v1/LessonsController.getVideoProgress') .middleware('role:admin,teacher,student') .as('lessons.get-video-progress'); -}).prefix('lessons'); +}) + .prefix('api/v1/lessons') + .middleware('auth'); diff --git a/routes/apis/v1/me.ts b/start/routes/apis/v1/me.ts similarity index 95% rename from routes/apis/v1/me.ts rename to start/routes/apis/v1/me.ts index c386537..d7b847c 100644 --- a/routes/apis/v1/me.ts +++ b/start/routes/apis/v1/me.ts @@ -18,4 +18,6 @@ Route.group(() => { Route.patch('/contacts', 'Api/v1/MeController.updateContacts') .middleware('role:admin,teacher,student') .as('me.update-contacts'); -}).prefix('me'); +}) + .prefix('api/v1/me') + .middleware('auth'); diff --git a/routes/apis/v1/stat.ts b/start/routes/apis/v1/stat.ts similarity index 77% rename from routes/apis/v1/stat.ts rename to start/routes/apis/v1/stat.ts index 109a29e..9c17d3c 100644 --- a/routes/apis/v1/stat.ts +++ b/start/routes/apis/v1/stat.ts @@ -2,4 +2,6 @@ import Route from '@ioc:Adonis/Core/Route'; Route.group(() => { Route.get('/', 'Api/v1/StatController.index').middleware('role:admin,teacher,student').as('stat.index'); -}).prefix('stat'); +}) + .prefix('api/v1/stat') + .middleware('auth'); diff --git a/routes/apis/v1/users.ts b/start/routes/apis/v1/users.ts similarity index 94% rename from routes/apis/v1/users.ts rename to start/routes/apis/v1/users.ts index 41b392e..e19a453 100644 --- a/routes/apis/v1/users.ts +++ b/start/routes/apis/v1/users.ts @@ -12,4 +12,6 @@ Route.group(() => { Route.delete('/:id/detach-roles', 'Api/v1/UsersController.detachRoles') .middleware('role:admin') .as('users.detach-role'); -}).prefix('users'); +}) + .prefix('api/v1/users') + .middleware('auth'); diff --git a/routes/videos.ts b/start/routes/videos.ts similarity index 100% rename from routes/videos.ts rename to start/routes/videos.ts From 1934e85c841eb5ccde6686f0e83ad6bcd79a6a36 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 6 Mar 2022 23:04:32 +0300 Subject: [PATCH 18/18] changelog --- CHANGELOG.md | 8 +++++--- package.json | 2 +- start/routes.ts | 2 ++ start/routes/index.ts | 3 +++ 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 start/routes/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 92aaab0..c0729d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,17 +4,19 @@ - API deployment with Docker and Nginx web server. -## [1.1.1] - +## [1.2.1] - 2022-03-06 ## Added - Saving latest notifications about new messages -- Chat between users. (chat history). -- About description for user and `last_login` column date +- Saving recent conversation messages based on constant (now 100) +- Simple chat implementation between users +- Added `about` and `last_login` columns for user - Updating info api route of authorized user ### Fix +- Refactored routes import - Fixed updating user info - Cleaning up session store before starting websocket server diff --git a/package.json b/package.json index c1c19b0..28ce582 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "1.1.0", + "version": "1.2.1", "private": true, "scripts": { "build": "node ace build --production", diff --git a/start/routes.ts b/start/routes.ts index 57c0d27..2149622 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -1,3 +1,5 @@ +import './routes/index'; + /** * API v1 */ diff --git a/start/routes/index.ts b/start/routes/index.ts new file mode 100644 index 0000000..b4523b0 --- /dev/null +++ b/start/routes/index.ts @@ -0,0 +1,3 @@ +import Route from '@ioc:Adonis/Core/Route'; + +Route.get('/', () => 'Educt Backend API');