Skip to content

Commit

Permalink
Release 0.28.0 (#151)
Browse files Browse the repository at this point in the history
* SK-217 Implemented group chat images  (#117)

* SK-189 added `avatar_object` field for user object

* SK-189 implemented the logic of creating links for downloading user avatars

* SK-189 renamed downloadFile to getFileDownloadUrl

* SK-217 added `image_object` for Conversation fields

* SK-217 added `image_url` field to the system event

* SK-217 added sending update request to other users

* SK-217 cleanup

* SK-217 minor fix

* SK-217 added image_object for conversationCreate

* SK-217 add log

* SK-217 remove log

* SK-217 add log

* SK-217 remove log

* SK-194 updated `typing` object (#121)

* Sk-194 updated typing object

* SK-194 updated API.md

* SK-234 removed file name from uuid in file `object_id` (#122)

* updated CHANGELOG.md (0.24.0 -> 0.25.0)

* Updated tests for status typing (removed tests for deleted fields)

* FM-27 added device_token for push subscription (#136)

* Release 0.25.0 (#123)

* SK-217 Implemented group chat images  (#117)

* SK-189 added `avatar_object` field for user object

* SK-189 implemented the logic of creating links for downloading user avatars

* SK-189 renamed downloadFile to getFileDownloadUrl

* SK-217 added `image_object` for Conversation fields

* SK-217 added `image_url` field to the system event

* SK-217 added sending update request to other users

* SK-217 cleanup

* SK-217 minor fix

* SK-217 added image_object for conversationCreate

* SK-217 add log

* SK-217 remove log

* SK-217 add log

* SK-217 remove log

* SK-194 updated `typing` object (#121)

* Sk-194 updated typing object

* SK-194 updated API.md

* SK-234 removed file name from uuid in file `object_id` (#122)

* updated CHANGELOG.md (0.24.0 -> 0.25.0)

* Release 0.25.0 (#124)

* SK-217 Implemented group chat images  (#117)

* SK-189 added `avatar_object` field for user object

* SK-189 implemented the logic of creating links for downloading user avatars

* SK-189 renamed downloadFile to getFileDownloadUrl

* SK-217 added `image_object` for Conversation fields

* SK-217 added `image_url` field to the system event

* SK-217 added sending update request to other users

* SK-217 cleanup

* SK-217 minor fix

* SK-217 added image_object for conversationCreate

* SK-217 add log

* SK-217 remove log

* SK-217 add log

* SK-217 remove log

* SK-194 updated `typing` object (#121)

* Sk-194 updated typing object

* SK-194 updated API.md

* SK-234 removed file name from uuid in file `object_id` (#122)

* updated CHANGELOG.md (0.24.0 -> 0.25.0)

* Updated tests for status typing (removed tests for deleted fields)

* Update API.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* updated package.json

* Update README.md

* Сhange development flow (#132)

* FM-27 added `device_token` for push subscription

* FM-27 minor cleanup

* FM-27 removed `rsmq-promise`, updated bull queue  methods

* FM-27 renamed minor comments

---------

Co-authored-by: IgorKhomenko <[email protected]>
Co-authored-by: Igor Khomenko <[email protected]>
Co-authored-by: ku9nov <[email protected]>

* updated CHANGELOG.md

* renamed env file

* updated tests for `push_subcriptions`, added indexes for the `push_subcriptions` collection

* SK-273 remove `platform` field from pushEvent object (#141)

* SK-272 extended `conversation_list` request to allow request convs by ids (#142)

* SK-272 added `ids` options for conversation_list, added test, updated API.md

* SK-272 added limit for ids field - 10

* SK-272 updated API.md

* Updated CHANGELOG.md

* Fixes different naming of methods

* Updated CHANGELOG.md

* Added `cid` field for push notification object

* SK-278  implemented support for `refresh_token`, added `connect_socket` request (#147)

* SK-278 added `connect_socket` request, implemented support for `refresh_token`

* SK-278 updated API.md

* SK-278 init HttpAuthController

* SK-278 minor fix

* SK-278 deprecated user_logout

* SK-278 added log

* SK-278 tmpToken -> refresh_token

* SK-278 added cookie signature

* SK-278 minor fix

* SK-278 removed log

* SK-278 remove token from cookie if it`ss incorrect

* SK-278  added function of reading header

* SK-278 removed error validation from  getAccessTokenFromHeader

* SK-278 added logs

* SK-278 removed logs

* SK-278 updated API.md

* SK-278 fixed expired_at field in auth response

* SK-278 fixed access and refresh jwt expiresIn

* SK-278 added refresh_token to all variants of response for auth

* SK-278 create new refresh token every time

* SK-278 added log

* SK-278 replaced cors settings

* SK-278 added onAborted handler for http requests

* Updated `user_search` query param, changed status for `unauthorized` error (#146)

* Updated user_search param name & updated error status for `Unauthorized`

* added logs

* removed logs

* Added a load testing scheme (#149)

* added load test

* updated test plan

* updated README.md

* added new envs

* updated pm2.config.cjs

* added testing results

* updated RESULTS.md

* updated RESULTS.md

* updated CHANGELOG.md

* revert pm2.config.cjs

* undo revert pm2.config.cjs

* fix tests

---------

Co-authored-by: IgorKhomenko <[email protected]>
Co-authored-by: Igor Khomenko <[email protected]>
Co-authored-by: ku9nov <[email protected]>
Co-authored-by: ku9nov <[email protected]>
  • Loading branch information
5 people authored Jan 29, 2025
1 parent 3562fb3 commit 8c0aaa8
Show file tree
Hide file tree
Showing 34 changed files with 1,014 additions and 84 deletions.
8 changes: 5 additions & 3 deletions .env.docker
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ CONVERSATION_MAX_PARTICIPANTS=50
CONVERSATION_PRELOAD_COUNT=1000
JWT_ACCESS_SECRET=4c09be6a35bec6d4089cde4be5ca57a6ee85da5739579cbecb0b8253f268eca2
JWT_ACCESS_TOKEN_EXPIRES_IN=10800
JWT_REFRESH_TOKEN_EXPIRES_IN=1209600
JWT_REFRESH_SECRET=4c09be6a35bec6d4089cde4be5ca57a6ee85da5739579cbecb0b8253f268eca2
OPERATIONS_LOG_EXPIRES_IN=1209600
NODE_CLUSTER_DATA_EXPIRES_IN=30000
REDIS_SUB_DATA_EXPIRES_IN=30000
FILE_UPLOAD_URL_EXPIRES_IN=3600
FILE_DOWNLOAD_URL_EXPIRES_IN=604800

ENCRYPTION_MESSAGE_EXPIRED_IN=2592000
ENCRYPTION_DEVICE_TOKEN_EXPIRES_IN=2592000
SSL_KEY_FILE_NAME=
SSL_CERT_FILE_NAME=

# set this env if you want to override the os.hostname() value
HOSTNAME=NodeName

HOSTNAME=NodeName
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ CONVERSATION_MAX_PARTICIPANTS=50
CONVERSATION_PRELOAD_COUNT=1000
JWT_ACCESS_SECRET=token
JWT_ACCESS_TOKEN_EXPIRES_IN=10800
JWT_REFRESH_SECRET=refresh
JWT_REFRESH_TOKEN_EXPIRES_IN=1209600
OPERATIONS_LOG_EXPIRES_IN=1209600
NODE_CLUSTER_DATA_EXPIRES_IN=30000
REDIS_SUB_DATA_EXPIRES_IN=30000
Expand All @@ -53,4 +55,5 @@ PUSH_QUEUE_DRIVER=sama_native

SAMA_NATIVE_PUSH_QUEUE_NAME=push_notifications


COOKIE_SECRET=secret
CORS_ORIGIN=http://localhost:3000
125 changes: 125 additions & 0 deletions APIs/JSON/controllers/http/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import BaseHttpController from "./base.js"

import signature from "cookie-signature"

import extractRefreshTokenFromCookie from "../../../JSON/utils/extract_refresh_token_from_cookie.js"
import { ERROR_STATUES } from "../../../../app/constants/errors.js"

import ServiceLocatorContainer from "@sama/common/ServiceLocatorContainer.js"

class HttpAuthController extends BaseHttpController {
#getRefreshTokenCookie(res, req) {
const cookieHeader = this.getCookie(req)

const signedToken = extractRefreshTokenFromCookie(cookieHeader)
if (!signedToken) return null

const unsignedToken = signature.unsign(signedToken.slice(2), process.env.COOKIE_SECRET)
if (unsignedToken !== false) {
return unsignedToken
} else {
// this.#setRefreshTokenCookie(res, signedToken, true)
throw new Error(ERROR_STATUES.INCORRECT_TOKEN.message, {
cause: {
status: ERROR_STATUES.INCORRECT_TOKEN.status,
message: ERROR_STATUES.INCORRECT_TOKEN.message,
},
})
}
}

#getAccessTokenFromHeader(req) {
const token = req.getHeader("authorization")?.split(" ")?.[1]
return token || null
}

async login(res, req) {
try {
const refresh_token = this.#getRefreshTokenCookie(res, req)
const access_token = this.#getAccessTokenFromHeader(req)

const { login, password, device_id } = await this.parseJsonBody(res)
if (!device_id) {
throw new Error(ERROR_STATUES.DEVICE_ID_MISSED.message, {
cause: { status: ERROR_STATUES.DEVICE_ID_MISSED.status, message: ERROR_STATUES.DEVICE_ID_MISSED.message },
})
}

const userAuthOperation = ServiceLocatorContainer.use("UserAuthOperation")
const userInfo = { device_id }

if (login && password) {
Object.assign(userInfo, { login, password })
} else if (access_token || refresh_token) {
userInfo.token = access_token || refresh_token
} else {
throw new Error(ERROR_STATUES.MISSING_AUTH_CREDENTIALS.message, {
cause: {
status: ERROR_STATUES.MISSING_AUTH_CREDENTIALS.status,
message: ERROR_STATUES.MISSING_AUTH_CREDENTIALS.message,
},
})
}

const { user, token: accessToken } = await userAuthOperation.perform(null, userInfo)

const newRefreshToken = await userAuthOperation.createRefreshToken(user, device_id)

const accessTokenExpiredAt =
new Date(accessToken.updated_at).getTime() + process.env.JWT_ACCESS_TOKEN_EXPIRES_IN * 1000

this.setStatus(res, 200)
.setRefreshToken(res, newRefreshToken.token)
.sendResponse(res, { user, access_token: accessToken.token, expired_at: accessTokenExpiredAt })
} catch (err) {
console.log(err)
this.setStatus(res, err.cause?.status || 500).sendResponse(res, {
message: err.cause?.message || ERROR_STATUES.INTERNAL_SERVER.message,
})
}
}

async logout(res, req) {
try {
const refresh_token = this.#getRefreshTokenCookie(res, req)
const access_token = this.#getAccessTokenFromHeader(req)

const sessionService = ServiceLocatorContainer.use("SessionService")
const userTokenRepo = ServiceLocatorContainer.use("UserTokenRepository")

const accessTokenRecord = await userTokenRepo.findToken(access_token, null, "access")
if (!accessTokenRecord) {
throw new Error(ERROR_STATUES.MISSING_AUTH_CREDENTIALS.message, {
cause: {
status: ERROR_STATUES.MISSING_AUTH_CREDENTIALS.status,
message: ERROR_STATUES.MISSING_AUTH_CREDENTIALS.message,
},
})
}

const refreshTokenRecord = await userTokenRepo.findToken(refresh_token, accessTokenRecord.device_id, "refresh")

if (!refreshTokenRecord) {
throw new Error(ERROR_STATUES.INCORRECT_TOKEN.message, {
cause: { status: ERROR_STATUES.INCORRECT_TOKEN.status, message: ERROR_STATUES.INCORRECT_TOKEN.message },
})
}

const userId = refreshTokenRecord?.user_id

const ws = sessionService.getUserDevices(userId).find((el) => el.deviceId === refreshTokenRecord.device_id)?.ws

const userLogoutOperation = ServiceLocatorContainer.use("UserLogoutOperation")
await userLogoutOperation.perform(ws)

this.setStatus(res, 200).setRefreshToken(res, refreshTokenRecord.token, true).sendResponse(res, { success: true })
} catch (err) {
console.log(err)
this.setStatus(res, err.cause?.status || 500).sendResponse(res, {
message: err.cause?.message || ERROR_STATUES.INTERNAL_SERVER.message,
})
}
}
}

export default new HttpAuthController()
53 changes: 53 additions & 0 deletions APIs/JSON/controllers/http/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import BaseController from "@sama/common/controller.js"

import signature from "cookie-signature"

export default class BaseHttpController extends BaseController {
async parseJsonBody(res) {
return new Promise((resolve, reject) => {
let buffer = Buffer.alloc(0)
res.onData((chunk, isLast) => {
buffer = Buffer.concat([buffer, Buffer.from(chunk)])
if (isLast) {
try {
resolve(JSON.parse(buffer.toString()))
} catch (error) {
reject(new Error("Invalid JSON input"))
}
}
})

res.onAborted(() => reject(new Error("Request aborted")))
})
}

#setCorsHeaders(res) {
res.writeHeader("Access-Control-Allow-Origin", process.env.CORS_ORIGIN || "*")
res.writeHeader("Access-Control-Allow-Credentials", "true")
res.writeHeader("Access-Control-Allow-Methods", "POST")
res.writeHeader("Access-Control-Allow-Headers", "Content-Type, Authorization")
}

getCookie(req) {
return req.getHeader("cookie")
}

setStatus(res, status) {
res.writeStatus(`${status}`)
return this
}

setRefreshToken(res, token, isRemove = false) {
const signedToken = `s:` + signature.sign(token, process.env.COOKIE_SECRET)
res.writeHeader(
"Set-Cookie",
`refresh_token=${signedToken}; Max-Age=${isRemove ? 0 : process.env.JWT_REFRESH_TOKEN_EXPIRES_IN}; HttpOnly; SameSite=Lax; Secure;`
)
return this
}

sendResponse(res, data) {
this.#setCorsHeaders(res)
res.end(JSON.stringify(data))
}
}
13 changes: 13 additions & 0 deletions APIs/JSON/controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ class UsersController extends BaseJSONController {
return new Response().addBackMessage({ response: { id: requestId, user: user.visibleParams() } })
}

async connect(ws, data) {
const { id: requestId, connect: userConnectionParams } = data

const userConnectSocketOperation = ServiceLocatorContainer.use("UserConnectSocketOperation")
const { user } = await userConnectSocketOperation.perform(ws, userConnectionParams)

return new Response()
.addBackMessage({ response: { id: requestId, success: true } })
.updateLastActivityStatus(
new LastActivityStatusResponse(user.native_id, MAIN_CONSTANTS.LAST_ACTIVITY_STATUS.ONLINE)
)
}

async login(ws, data) {
const { id: requestId, user_login: userInfo } = data

Expand Down
2 changes: 1 addition & 1 deletion APIs/JSON/middleware/auth_guard.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class AuthGuardMiddleware extends BaseMiddleware {
handle(ws, json) {
const sessionService = ServiceLocatorContainer.use("SessionService")

if (!sessionService.getSession(ws) && !json?.user_create && !json?.user_login) {
if (!sessionService.getSession(ws) && !json?.user_create && !json?.user_login && !json?.connect) {
throw new Error(ERROR_STATUES.UNAUTHORIZED.message, {
cause: ERROR_STATUES.UNAUTHORIZED,
})
Expand Down
12 changes: 12 additions & 0 deletions APIs/JSON/routes/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export const routes = {
MessagesController.middleware(authGuardMiddleware, ws, json)
.validate(json.system_message, messagesSchemaValidation.system)
.sendSystem(ws, json),
connect: (ws, json) =>
UsersController.middleware(authGuardMiddleware, ws, json)
.validate(json.connect, usersSchemaValidation.connect)
.connect(ws, json),
user_create: (ws, json) =>
UsersController.middleware(authGuardMiddleware, ws, json)
.validate(json.user_create, usersSchemaValidation.create)
Expand All @@ -61,10 +65,18 @@ export const routes = {
.validate(json.user_edit, usersSchemaValidation.edit)
.middleware(authGuardMiddleware, ws, json)
.edit(ws, json),
/**
* @deprecated **WARNING**: `user_login` request is deprecated
* Therefore, we recommend using the new http route `/login` for user authorization.
*/
user_login: (ws, json) =>
UsersController.middleware(authGuardMiddleware, ws, json)
.validate(json.user_login, usersSchemaValidation.login)
.login(ws, json),
/**
* @deprecated **WARNING**: `user_logout` request is deprecated
* Therefore, we recommend using the new http route `/logout` to log the user out of the system.
*/
user_logout: (ws, json) =>
UsersController.middleware(authGuardMiddleware, ws, json)
.validate(json.user_logout, usersSchemaValidation.logout)
Expand Down
8 changes: 8 additions & 0 deletions APIs/JSON/utils/extract_refresh_token_from_cookie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function extractRefreshTokenFromCookie(cookieHeader) {
return cookieHeader
? cookieHeader
.split("; ")
.find((cookie) => cookie.startsWith("refresh_token="))
?.split("=")[1]
: null
}
17 changes: 14 additions & 3 deletions APIs/JSON/validations/users_schema_validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const usersSchemaValidation = {
email: Joi.string(),
// .pattern(/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/),
phone: Joi.string().min(3).max(15),
deviceId: Joi.alternatives().try(Joi.number().max(255).required(), Joi.string().max(255).required()),
device_id: Joi.alternatives().try(Joi.number().max(255).required(), Joi.string().max(255).required()),
}),
edit: Joi.object({
current_password: Joi.string().error(
Expand All @@ -47,6 +47,17 @@ export const usersSchemaValidation = {
file_blur_hash: Joi.string().max(255),
}),
}).with("current_password", "current_password"),
connect: Joi.object({
token: Joi.string(),
device_id: Joi.alternatives()
.try(Joi.number(), Joi.string().max(255))
.required()
.error(
new Error(ERROR_STATUES.DEVICE_ID_MISSED.message, {
cause: ERROR_STATUES.DEVICE_ID_MISSED,
})
),
}),
login: Joi.object({
login: Joi.string().error(
new Error(ERROR_STATUES.USER_LOGIN_OR_PASS.message, {
Expand All @@ -55,7 +66,7 @@ export const usersSchemaValidation = {
),
token: Joi.string(),
password: Joi.string(),
deviceId: Joi.alternatives()
device_id: Joi.alternatives()
.try(Joi.number(), Joi.string().max(255))
.required()
.error(
Expand All @@ -70,7 +81,7 @@ export const usersSchemaValidation = {
logout: Joi.object({}).required(),
delete: Joi.object({}).required(),
search: Joi.object({
login: Joi.string().required(),
keyword: Joi.string().required(),
limit: Joi.number().min(1).max(100),
updated_at: Joi.object({
gt: Joi.date(),
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 0.28.0

### Feature

- Implemented a progressive authentication flow to enhance security
- Added a load testing scheme

### Updated

- Updated `user_search` query param (`login` -> `keyword`)
- Changed status for `Unauthorized` error

## 0.27.0

### Improvements
Expand Down
14 changes: 11 additions & 3 deletions app/constants/errors.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const ERROR_STATUES = {
// Default -->
UNAUTHORIZED: { status: 404, message: "Unauthorized." },
UNAUTHORIZED: { status: 401, message: "Unauthorized." },
FORBIDDEN: { status: 403, message: "Forbidden." },
BAD_REQUEST: { status: 400, message: "Bad Request." },
INVALID_DATA_FORMAT: {
Expand Down Expand Up @@ -50,7 +50,7 @@ export const ERROR_STATUES = {
status: 422,
message: "Incorrect username or password.",
},
DEVICE_ID_MISSED: { status: 422, message: `'deviceId' is required.` },
DEVICE_ID_MISSED: { status: 422, message: `'device_id' is required.` },
// Contacts -->
CONTACT_NOT_FOUND: { status: 422, message: "Contact not found." },
CONTACT_ID_MISSED: { status: 422, message: "Contact id is missed." },
Expand Down Expand Up @@ -121,7 +121,7 @@ export const ERROR_STATUES = {
STATUS_T_MISSED: { status: 422, message: `Status 't' missed.` },
// Push Notification -->
INCORRECT_PLATFROM_TYPE: { status: 422, message: "Incorrect platform type." },
INCORRECT_DEVICE_ID: { status: 422, message: "Incorrect deviceId." },
INCORRECT_DEVICE_ID: { status: 422, message: "Incorrect device id." },
INCORRECT_RECIPIENTS_IDS: {
status: 422,
message: "Incorrect recipients IDs.",
Expand All @@ -146,6 +146,14 @@ export const ERROR_STATUES = {
status: 422,
message: `'cids' field is required.`,
},
INTERNAL_SERVER: {
status: 500,
message: "Internal server error.",
},
MISSING_AUTH_CREDENTIALS: {
status: 401,
message: "Missing authentication credentials.",
},
}

export const requiredError = (field) => {
Expand Down
Loading

0 comments on commit 8c0aaa8

Please sign in to comment.