diff --git a/package-lock.json b/package-lock.json index 06f039c..6a1fd97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@notionhq/client": "^2.2.3", "@sentry/node": "^7.50.0", "@temporalio/client": "^1.11.3", - "@togethercrew.dev/db": "^3.0.72", + "@togethercrew.dev/db": "^3.0.78", "@togethercrew.dev/tc-messagebroker": "^0.0.50", "@types/express-session": "^1.17.7", "@types/morgan": "^1.9.5", @@ -3658,9 +3658,9 @@ "dev": true }, "node_modules/@togethercrew.dev/db": { - "version": "3.0.77", - "resolved": "https://registry.npmjs.org/@togethercrew.dev/db/-/db-3.0.77.tgz", - "integrity": "sha512-fy1l9GnPAhVlpm79tcn7/4+mIgBBLkCi8XSDi61JLHv/dJJ3XaNKLccQ8naRIkBzVQaN89KyFdxnmFCZ9Xn9Hg==", + "version": "3.0.78", + "resolved": "https://registry.npmjs.org/@togethercrew.dev/db/-/db-3.0.78.tgz", + "integrity": "sha512-/JiwW6ja4cATyn2tvho4xBe+hNEBqQ8w14M7Lm0Xqu6ckEagIk+4ptOLr7uDNUw9poDZ9ybYnzdqyGX7DDMSrw==", "license": "ISC", "dependencies": { "discord.js": "^14.7.1", @@ -6392,9 +6392,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -6416,7 +6416,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -6431,6 +6431,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { @@ -11150,9 +11154,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, "node_modules/path-type": { diff --git a/package.json b/package.json index d8bea98..c734165 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@notionhq/client": "^2.2.3", "@sentry/node": "^7.50.0", "@temporalio/client": "^1.11.3", - "@togethercrew.dev/db": "^3.0.72", + "@togethercrew.dev/db": "^3.0.78", "@togethercrew.dev/tc-messagebroker": "^0.0.50", "@types/express-session": "^1.17.7", "@types/morgan": "^1.9.5", diff --git a/src/config/telegram.ts b/src/config/telegram.ts new file mode 100644 index 0000000..68812ac --- /dev/null +++ b/src/config/telegram.ts @@ -0,0 +1,4 @@ +export const verificationToken = { + verificationCodeLength: 5, + allowedCharacters: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', +}; diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 3c25915..bebda8c 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -73,9 +73,15 @@ const refreshTokens = catchAsync(async function (req: Request, res: Response) { res.send({ ...tokens }); }); +const generateToken = catchAsync(async function (req: Request, res: Response) { + const token = await tokenService.generateTelegramVerificationToken(req.user.id, req.body.communityId); + res.send(token); +}); + export default { discordAuthorize, discordAuthorizeCallback, refreshTokens, logout, + generateToken, }; diff --git a/src/docs/auth.doc.yml b/src/docs/auth.doc.yml index beb727e..43b9dd4 100644 --- a/src/docs/auth.doc.yml +++ b/src/docs/auth.doc.yml @@ -6,9 +6,9 @@ paths: summary: Discord OAuth2 Authorization description: Redirects the user to the Discord OAuth2 authorization page. responses: - "302": + '302': description: Found (Redirect to Discord OAuth2 page) - $ref: "#/components/responses/Found" + $ref: '#/components/responses/Found' /api/v1/auth/refresh-tokens: post: @@ -28,7 +28,7 @@ paths: required: - refreshToken responses: - "200": + '200': description: OK content: application/json: @@ -39,16 +39,16 @@ paths: type: object properties: access: - $ref: "#/components/schemas/Token" + $ref: '#/components/schemas/Token' refresh: - $ref: "#/components/schemas/Token" - "400": + $ref: '#/components/schemas/Token' + '400': description: Bad Request - $ref: "#/components/responses/BadRequest" - "401": + $ref: '#/components/responses/BadRequest' + '401': description: Unauthorized - $ref: "#/components/responses/Unauthorized" - + $ref: '#/components/responses/Unauthorized' + /api/v1/auth/logout: post: tags: @@ -67,12 +67,57 @@ paths: required: - refreshToken responses: - "204": + '204': description: No Content (Successfully logged out) - $ref: "#/components/responses/NoContent" - "400": + $ref: '#/components/responses/NoContent' + '400': description: Bad Request - $ref: "#/components/responses/BadRequest" - "404": + $ref: '#/components/responses/BadRequest' + '404': description: Not found (Invalid refresh token) - $ref: "#/components/responses/NotFound" \ No newline at end of file + $ref: '#/components/responses/NotFound' + + /api/v1/auth/generate-token: + post: + tags: + - Auth + summary: Generate token for verfication purpose. + description: Generate token for verfication purpose (telegram_verification) with specific requirements based on the token type. + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + type: + type: string + enum: ['telegram_verification'] + required: true + communityId: + type: string + format: objectId + description: The communityId, required for telegram_verification. + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + value: + type: string + expiresAt: + type: date + example: + value: 'JK1NE' + expiresAt: '2024-12-20T07:28:57.718Z' + '400': + description: Bad Request + $ref: '#/components/responses/BadRequest' + '401': + description: Unauthorized + $ref: '#/components/responses/Unauthorized' diff --git a/src/docs/platform.doc.yml b/src/docs/platform.doc.yml index e919b01..3921810 100644 --- a/src/docs/platform.doc.yml +++ b/src/docs/platform.doc.yml @@ -68,6 +68,7 @@ paths: - notion - mediaWiki - discourse + - telegram description: Name of the platform to create. Must be one of the supported platforms. community: type: string @@ -188,6 +189,12 @@ paths: isInProgress: type: boolean description: Metadata for Discourse. + - type: object + required: [chat] + properties: + chat: + type: object + description: Metadata for Telegram. responses: '201': description: Platform created successfully. diff --git a/src/routes/v1/auth.route.ts b/src/routes/v1/auth.route.ts index 29ef246..d88b4ae 100644 --- a/src/routes/v1/auth.route.ts +++ b/src/routes/v1/auth.route.ts @@ -1,12 +1,13 @@ import express from 'express'; import { authController } from '../../controllers'; import { authValidation } from '../../validations'; -import { validate } from '../../middlewares'; +import { validate, auth } from '../../middlewares'; const router = express.Router(); // Routes router.get('/discord/authorize', authController.discordAuthorize); router.get('/discord/authorize/callback', authController.discordAuthorizeCallback); +router.post('/generate-token', auth(), validate(authValidation.generateToken), authController.generateToken); router.post('/logout', validate(authValidation.logout), authController.logout); router.post('/refresh-tokens', validate(authValidation.refreshTokens), authController.refreshTokens); diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 32bbabd..583ac7d 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -9,6 +9,8 @@ import { IToken, Token, IUser } from '@togethercrew.dev/db'; import { IDiscordOAuth2EchangeCode, IAuthTokens } from 'src/interfaces'; import { Auth } from 'googleapis'; import discordServices from './discord'; +import crypto from 'crypto'; +import { verificationToken } from '../config/telegram'; /** * Generate token * @param {IUser} user @@ -42,6 +44,7 @@ async function saveToken( expires: moment.Moment, type: string, blacklisted = false, + community?: Types.ObjectId, ): Promise { const tokenDoc: IToken = await Token.create({ token, @@ -49,6 +52,7 @@ async function saveToken( expires: expires.toDate(), type, blacklisted, + community, }); return tokenDoc; } @@ -187,6 +191,40 @@ async function saveNotionAccessToken(userId: Types.ObjectId, accessToken: string await saveToken(accessToken, userId, accessTokenExpires, TokenTypeNames.NOTION_ACCESS); } +/** + * Generate a random verification code using crypto + * @param {number} length - Length of the code + * @param {string} allowedCharacters - Characters to choose from + * @returns {string} + */ +function generateRandomCode(length: number, allowedCharacters: string): string { + const charsLength = allowedCharacters.length; + const randomBytes = crypto.randomBytes(length); + let code = ''; + for (let i = 0; i < length; i++) { + const randomIndex = randomBytes[i] % charsLength; + code += allowedCharacters.charAt(randomIndex); + } + return code; +} + +/** + * Generate a Telegram verification token + * @param {Types.ObjectId} userId + * @param {Types.ObjectId} communityId + * @returns {Promise<{ value: string; expiresAt: Date }>} + */ +async function generateTelegramVerificationToken(userId: Types.ObjectId, communityId: Types.ObjectId) { + const { verificationCodeLength, allowedCharacters } = verificationToken; + + const value = generateRandomCode(verificationCodeLength, allowedCharacters); + const expiresAt = moment().add(10, 'minutes'); + + await saveToken(value, userId, expiresAt, TokenTypeNames.TELEGRAM_VERIFICATION, false, communityId); + + return { value, expiresAt: expiresAt.toDate() }; +} + export default { generateToken, verifyToken, @@ -196,4 +234,5 @@ export default { saveDiscordOAuth2Tokens, saveGoogleOAuth2Tokens, saveNotionAccessToken, + generateTelegramVerificationToken, }; diff --git a/src/validations/auth.validation.ts b/src/validations/auth.validation.ts index 0ccc2de..3e0d42d 100644 --- a/src/validations/auth.validation.ts +++ b/src/validations/auth.validation.ts @@ -1,4 +1,6 @@ import Joi from 'joi'; +import { objectId } from './custom.validation'; +import { TokenTypeNames } from '@togethercrew.dev/db'; const logout = { body: Joi.object().required().keys({ @@ -11,8 +13,21 @@ const refreshTokens = { refreshToken: Joi.string().required(), }), }; +const generateToken = { + body: Joi.object().keys({ + type: Joi.string().required().valid(TokenTypeNames.TELEGRAM_VERIFICATION, TokenTypeNames.ACCESS), + communityId: Joi.string() + .custom(objectId) + .when('type', { + is: Joi.string().valid(TokenTypeNames.TELEGRAM_VERIFICATION), + then: Joi.required(), + otherwise: Joi.forbidden(), + }), + }), +}; export default { logout, refreshTokens, + generateToken, }; diff --git a/src/validations/platform.validation.ts b/src/validations/platform.validation.ts index 256190d..d75a10e 100644 --- a/src/validations/platform.validation.ts +++ b/src/validations/platform.validation.ts @@ -13,6 +13,14 @@ const discordCreateMetadata = () => { }); }; +const telegramMetadata = () => { + return Joi.object() + .required() + .keys({ + chat: Joi.object().unknown(true).required(), + }); +}; + const discordUpdateMetadata = () => { return Joi.object().keys({ selectedChannels: Joi.array().items(Joi.string()), @@ -120,6 +128,10 @@ const createPlatform = { is: PlatformNames.Discourse, then: discourseMetadata(), }, + { + is: PlatformNames.Telegram, + then: telegramMetadata, + }, ], }).required(), }),