From 856e01d33916d2dc2be8850c6259ab27c7c27dd8 Mon Sep 17 00:00:00 2001 From: Francisco Pessano Date: Thu, 12 Jan 2023 21:18:49 -0300 Subject: [PATCH] first working prototype of shortly --- .eslintrc.json | 17 ++-- .gitignore | 5 +- .nvmrc | 1 + .prettierrc | 6 ++ docker-compose.yml | 9 +- forbidden-ids.json | 10 --- nodemon.json | 5 ++ package.json | 73 +++++++++------- src/app.ts | 20 +++++ src/index.js | 19 ----- src/models/dbConnection.ts | 19 +++++ src/models/queries/Url.queries.ts | 58 +++++++++++++ src/models/queries/User.queries.ts | 111 +++++++++++++++++++++++++ src/models/schemas/Url.schema.ts | 34 ++++++++ src/models/schemas/User.schema.ts | 52 ++++++++++++ src/pages/getUrl/generateGetUrlPage.ts | 18 ++++ src/routes/add-url.js | 16 ---- src/routes/addUrl/addUrl.ts | 33 ++++++++ src/routes/get-url/forbidden-ids.json | 12 +++ src/routes/get-url/getUrl.ts | 30 +++++++ src/routes/getUser/getUser.ts | 30 +++++++ src/routes/index.js | 5 -- src/routes/signUp/signUp.ts | 39 +++++++++ src/routes/url-shortener.js | 6 -- src/scripts/checkMissingData.ts | 19 +++++ src/scripts/checkUsernameOrEmail.ts | 10 +++ src/scripts/crypto.ts | 27 ++++++ src/scripts/generateId.ts | 12 +++ src/scripts/isUrl.ts | 5 ++ src/scripts/removeEmptyProperties.ts | 5 ++ src/services/mongoErrorService.ts | 23 +++++ src/services/userService.ts | 0 src/utils/constants.ts | 13 +++ tsconfig.json | 103 +++++++++++++++++++++++ 34 files changed, 745 insertions(+), 100 deletions(-) create mode 100644 .nvmrc create mode 100644 .prettierrc delete mode 100644 forbidden-ids.json create mode 100644 nodemon.json create mode 100644 src/app.ts delete mode 100644 src/index.js create mode 100644 src/models/dbConnection.ts create mode 100644 src/models/queries/Url.queries.ts create mode 100644 src/models/queries/User.queries.ts create mode 100644 src/models/schemas/Url.schema.ts create mode 100644 src/models/schemas/User.schema.ts create mode 100644 src/pages/getUrl/generateGetUrlPage.ts delete mode 100644 src/routes/add-url.js create mode 100644 src/routes/addUrl/addUrl.ts create mode 100644 src/routes/get-url/forbidden-ids.json create mode 100644 src/routes/get-url/getUrl.ts create mode 100644 src/routes/getUser/getUser.ts delete mode 100644 src/routes/index.js create mode 100644 src/routes/signUp/signUp.ts delete mode 100644 src/routes/url-shortener.js create mode 100644 src/scripts/checkMissingData.ts create mode 100644 src/scripts/checkUsernameOrEmail.ts create mode 100644 src/scripts/crypto.ts create mode 100644 src/scripts/generateId.ts create mode 100644 src/scripts/isUrl.ts create mode 100644 src/scripts/removeEmptyProperties.ts create mode 100644 src/services/mongoErrorService.ts create mode 100644 src/services/userService.ts create mode 100644 src/utils/constants.ts create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json index faf2b00..c8f8a2f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,15 +1,10 @@ { - "env": { - "browser": true, - "commonjs": true, - "es2021": true - }, - "extends": "standard", - "overrides": [ - ], - "parserOptions": { - "ecmaVersion": "latest" - }, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], "rules": { + "semi": ["error", "never"], + "quotes": [2, "single"], + "no-unused-vars": "error", + "@typescript-eslint/no-unused-vars": "error" } } diff --git a/.gitignore b/.gitignore index 97008e5..97b1b6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules -yarn.lock \ No newline at end of file +yarn.lock +.env +dist +yarn-error.log \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..95c758c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.12.1 \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e74ed9f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 4, + "semi": false, + "singleQuote": true +} diff --git a/docker-compose.yml b/docker-compose.yml index 8ad0fb6..17795c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,4 +7,11 @@ services: - .:/usr/src - /usr/src/node_modules ports: - - "3000:3000" \ No newline at end of file + - 3000:3000 + mongo: + image: mongo + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGODB_LINK: ${MONGODB_LINK} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD} \ No newline at end of file diff --git a/forbidden-ids.json b/forbidden-ids.json deleted file mode 100644 index 6b7b4ed..0000000 --- a/forbidden-ids.json +++ /dev/null @@ -1,10 +0,0 @@ -{"forbidden-ids": [ - "settings", - "register", - "login", - "admin", - "franp", - "github", - "linkedin", - "add" -]} \ No newline at end of file diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..503f2c1 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,5 @@ +{ + "watch": ["src"], + "ext": "ts, json", + "exec": "ts-node ./src/app.ts" +} diff --git a/package.json b/package.json index 14587a2..5e05855 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,44 @@ { - "name": "url-shorter", - "description": "", - "version": "1.0.0", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node src/index.js", - "dev": "nodemon src/index.js" - }, - "repository": { - "type": "git", - "url": "https://github.com/monatheoctocat/my_package.git" - }, - "keywords": [], - "author": "FranP-Code", - "license": "ISC", - "bugs": { - "url": "https://github.com/monatheoctocat/my_package/issues" - }, - "homepage": "https://github.com/monatheoctocat/my_package", - "dependencies": { - "@fastify/jwt": "^6.5.0", - "fastify": "^4.10.2" - }, - "devDependencies": { - "eslint": "^8.0.1", - "eslint-config-standard": "^17.0.0", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0", - "eslint-plugin-promise": "^6.0.0", - "nodemon": "^2.0.20" - } + "name": "shortly", + "description": "", + "version": "1.0.0", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node dist/app.js", + "dev": "nodemon -L src/app.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/monatheoctocat/my_package.git" + }, + "keywords": [], + "author": "FranP-Code", + "license": "ISC", + "bugs": { + "url": "https://github.com/monatheoctocat/my_package/issues" + }, + "homepage": "https://github.com/monatheoctocat/my_package", + "dependencies": { + "@types/express": "^4.17.15", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.0", + "metadata-scraper": "^0.2.61", + "mongoose": "5.13.14", + "ulid": "^2.3.0" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.0", + "@types/mongodb": "^4.0.7", + "@typescript-eslint/eslint-plugin": "^5.48.0", + "@typescript-eslint/parser": "^5.48.0", + "eslint": "^8.31.0", + "eslint-config-standard": "^17.0.0", + "eslint-plugin-n": "^15.0.0", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-unused-imports": "^2.0.0", + "nodemon": "^2.0.20", + "ts-node": "^10.9.1", + "typescript": "^4.9.4" + } } diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..bd5bd31 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,20 @@ +import express from 'express' +require('dotenv').config() +import databaseConnection from './models/dbConnection' +import signUp from './routes/signUp/signUp' +import getUser from './routes/getUser/getUser' +import addUrl from './routes/addUrl/addUrl' +import getUrl from './routes/get-url/getUrl' + +const app = express() +const port = process.env.PORT || 3000 + +databaseConnection() +app.use('/get-user', getUser) +app.use('/sign-up', signUp) +app.use('/add-url', addUrl) +app.use('/', getUrl) + +app.listen(port, () => { + console.log(`shortly on port ${port}`) +}) diff --git a/src/index.js b/src/index.js deleted file mode 100644 index e35d34d..0000000 --- a/src/index.js +++ /dev/null @@ -1,19 +0,0 @@ -const fastify = require('fastify')({ logger: true }) - -const addUrl = require('./routes/add-url') -const index = require('./routes/index') -const urlShortener = require('./routes/url-shortener') - -fastify.register(index) -fastify.register(urlShortener) -fastify.register(addUrl) - -const start = async () => { - try { - await fastify.listen({ port: process.env.PORT || 3000, host: '0.0.0.0' }) - } catch (err) { - fastify.log.error(err) - process.exit(1) - } -} -start() diff --git a/src/models/dbConnection.ts b/src/models/dbConnection.ts new file mode 100644 index 0000000..d28328a --- /dev/null +++ b/src/models/dbConnection.ts @@ -0,0 +1,19 @@ +const mongoose = require('mongoose') + +export default function databaseConnection() { + mongoose + .connect( + `mongodb://root:${process.env.MONGO_INITDB_ROOT_PASSWORD}@${process.env.MONGODB_LINK}`, + { + serverSelectionTimeoutMS: 5000, + } + ) + .catch((err: any) => console.log(err)) + + mongoose.connection.once('open', async () => { + console.log('DB connected') + }) + mongoose.connection.once('error', (err: any) => { + console.log(err) + }) +} diff --git a/src/models/queries/Url.queries.ts b/src/models/queries/Url.queries.ts new file mode 100644 index 0000000..e24aea4 --- /dev/null +++ b/src/models/queries/Url.queries.ts @@ -0,0 +1,58 @@ +/* eslint-disable quotes */ +import { decrypt } from '../../scripts/crypto' +import generateId from '../../scripts/generateId' +import isUrl from '../../scripts/isUrl' +import removeEmptyProperties from '../../scripts/removeEmptyProperties' +import { IUrl, UrlModel } from '../schemas/Url.schema' +import { UserModel } from '../schemas/User.schema' + +export async function createUrl( + data: { + id?: string + url: string + username?: string + email?: string + password: string + } & ({ username: string } | { email: string }) +) { + const { id, url, username, email, password } = data + if (!isUrl(url)) { + throw new Error('url invalid') + } + if (process.env.ALLOW_DUPLICATED_LINKS !== 'true') { + const existingUrl = await UrlModel.findOne({ + url, + }) + if (existingUrl) { + return existingUrl + } + const user = await UserModel.findOne( + removeEmptyProperties({ id, username, email }) + ) + if (!user) { + throw new Error(JSON.stringify({ message: "user don't found" })) + } + if ( + decrypt({ content: user.password, iv: user.crypto.iv }) === password + ) { + const newUrl: IUrl = { + id: generateId(5), + url, + dateCreated: new Date(), + uploadedByUser: user.id, + } + return UrlModel.create(newUrl) + } + } else { + throw new Error(JSON.stringify({ message: "user don't found" })) + } +} + +export async function getUrlById(data: { id: string }) { + const { id } = data + const url = await UrlModel.findOne({ id }) + if (!url) { + throw new Error('no url') + } + return url +} diff --git a/src/models/queries/User.queries.ts b/src/models/queries/User.queries.ts new file mode 100644 index 0000000..dc99646 --- /dev/null +++ b/src/models/queries/User.queries.ts @@ -0,0 +1,111 @@ +/* eslint-disable quotes */ +import { IUser, UserModel } from '../schemas/User.schema' +import { ulid } from 'ulid' +import { decrypt, encrypt } from '../../scripts/crypto' +import { UserRoles } from '../../utils/constants' +import removeEmptyProperties from '../../scripts/removeEmptyProperties' + +export async function getUserData( + data: { + userId?: number + username?: string + email?: string + } & ({ username: string } | { email: string }) +) { + return UserModel.find(removeEmptyProperties(data)) +} + +export async function getUserDataWithId( + data: { + username?: string + email?: string + password?: string + } & ({ username: string } | { email: string }) +) { + const { username, email, password } = data + const user = await UserModel.findOne( + removeEmptyProperties({ username, email }) + ) + if (!user) { + throw new Error(JSON.stringify({ message: "user don't found" })) + } + if ( + decrypt({ + content: user.password, + iv: user.crypto.iv, + }) === password + ) { + return { + id: user.id, + username: user.username, + email: user.email, + role: user.role, + } + } else { + throw new Error(JSON.stringify({ message: "password don't match" })) + } +} + +export async function createUser(data: { + email: string + password: string + username: string + role: UserRoles + sponsorUid: string + firstUserPassword?: string +}) { + const { email, password, username, role, sponsorUid } = data + const { iv, content: encryptedPassword } = encrypt(password) + const ulidSeed = process.env.ULID_SEED && parseInt(process.env.ULID_SEED) + const newUser: IUser = { + id: typeof ulidSeed === 'number' ? ulid(ulidSeed) : ulid(), + dateCreated: new Date(), + email, + password: encryptedPassword, + role, + username, + crypto: { iv }, + } + const existingUsername = await UserModel.countDocuments({ username }) + const existingEmail = await UserModel.countDocuments({ email }) + if (existingUsername || existingEmail) { + throw new Error( + JSON.stringify({ + message: 'fields duplicated', + fieldsDuplicated: [ + existingUsername && 'username', + existingEmail && 'email', + ], + }) + ) + } + const firstUserCreated = !(await UserModel.count()) + if (firstUserCreated) { + if (data.firstUserPassword !== process.env.FIRST_USER_PASSWORD) { + throw new Error( + JSON.stringify({ + message: 'wrong first user password', + }) + ) + } + } else { + console.log(sponsorUid) + if (!sponsorUid) { + throw new Error( + JSON.stringify({ + message: 'no sponsor user param', + }) + ) + } + const sponsorUser = await UserModel.findOne({ id: sponsorUid }) + if (!sponsorUser) { + throw new Error( + JSON.stringify({ + message: 'no sponsor user', + }) + ) + } + } + const userModelCreation = new UserModel(newUser) + await userModelCreation.save() +} diff --git a/src/models/schemas/Url.schema.ts b/src/models/schemas/Url.schema.ts new file mode 100644 index 0000000..013df6b --- /dev/null +++ b/src/models/schemas/Url.schema.ts @@ -0,0 +1,34 @@ +import { model, Model, Schema } from 'mongoose' + +export interface IUrl { + id: string + url: string + dateCreated: Date + uploadedByUser: string +} + +export const urlSchema = new Schema( + { + id: { + type: String, + unique: true, + required: true, + }, + url: { + type: String, + required: true, + }, + dateCreated: { + type: Date, + default: new Date(), + required: true, + }, + uploadedByUser: { + type: String, + required: true, + }, + }, + { collection: 'url', timestamps: true } +) + +export const UrlModel: Model = model('Url', urlSchema) diff --git a/src/models/schemas/User.schema.ts b/src/models/schemas/User.schema.ts new file mode 100644 index 0000000..ec8f001 --- /dev/null +++ b/src/models/schemas/User.schema.ts @@ -0,0 +1,52 @@ +import { Schema, model, Model } from 'mongoose' +import { Crypto, UserRoles } from '../../utils/constants' +export interface IUser { + id: string + username: string + email: string + password: string + dateCreated: Date + role: UserRoles + crypto: Crypto +} + +export const userSchema = new Schema( + { + id: { + type: String, + required: true, + unique: true, + }, + username: { + type: String, + required: true, + unique: true, + }, + email: { + type: String, + required: true, + lowercase: true, + unique: true, + }, + password: { + type: String, + required: true, + }, + dateCreated: { + type: Date, + default: new Date(), + required: true, + }, + role: { + type: String, + required: true, + }, + crypto: { + type: Object, + required: true, + }, + }, + { collection: 'user', timestamps: true } +) + +export const UserModel: Model = model('User', userSchema) diff --git a/src/pages/getUrl/generateGetUrlPage.ts b/src/pages/getUrl/generateGetUrlPage.ts new file mode 100644 index 0000000..db1e82f --- /dev/null +++ b/src/pages/getUrl/generateGetUrlPage.ts @@ -0,0 +1,18 @@ +export default function (metaData: any) { + return ` + + + + ${metaData.title} + + + + + + + + + ` +} diff --git a/src/routes/add-url.js b/src/routes/add-url.js deleted file mode 100644 index ec856c3..0000000 --- a/src/routes/add-url.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = async function (fastify, options) { - fastify.register(require('@fastify/jwt'), { - secret: 'supersecret123' - }) - - fastify.post('/add', (req, reply) => { - // some code - const token = fastify.jwt.sign({ abc: 123 }) - console.log(token) - reply.send({ token }) - }) - - fastify.listen({ port: 3000 }, (err) => { - if (err) throw err - }) -} diff --git a/src/routes/addUrl/addUrl.ts b/src/routes/addUrl/addUrl.ts new file mode 100644 index 0000000..5c01b00 --- /dev/null +++ b/src/routes/addUrl/addUrl.ts @@ -0,0 +1,33 @@ +import { Router, Request, Response } from 'express' +import { createUrl } from '../../models/queries/Url.queries' +import checkMissingData from '../../scripts/checkMissingData' +import checkUsernameOrEmail from '../../scripts/checkUsernameOrEmail' +import mongoErrorService from '../../services/mongoErrorService' +const router = Router() + +router.post('/', async (req: Request, res: Response) => { + const values = ['url', 'password'] + const { url, email, username, password } = req.headers + try { + checkMissingData(values, req) + checkUsernameOrEmail(username, email, res) + const createdUrl = await createUrl({ + email: email, + password: password, + url: url, + username: username, + }) + res.status(200).json({ + message: 'url created', + url: createdUrl, + }) + } catch (error) { + console.log(error) + res.status(400).json({ + error: mongoErrorService(error), + }) + } + return +}) + +export default router diff --git a/src/routes/get-url/forbidden-ids.json b/src/routes/get-url/forbidden-ids.json new file mode 100644 index 0000000..1f855c2 --- /dev/null +++ b/src/routes/get-url/forbidden-ids.json @@ -0,0 +1,12 @@ +{ + "ids": [ + "settings", + "register", + "login", + "admin", + "franp", + "github", + "linkedin", + "add" + ] +} diff --git a/src/routes/get-url/getUrl.ts b/src/routes/get-url/getUrl.ts new file mode 100644 index 0000000..ac583ad --- /dev/null +++ b/src/routes/get-url/getUrl.ts @@ -0,0 +1,30 @@ +import { Router, Request, Response } from 'express' +import getMetaData from 'metadata-scraper' +import { getUrlById } from '../../models/queries/Url.queries' +import generateGetUrlPage from '../../pages/getUrl/generateGetUrlPage' +import mongoErrorService from '../../services/mongoErrorService' +import { ids as forbiddenIds } from './forbidden-ids.json' +const router = Router() + +router.get('/:urlId', async (req: Request, res: Response) => { + const { urlId } = req.params + if (!urlId || forbiddenIds.includes(urlId)) { + res.status(400).json({ + message: 'forbidden id', + }) + } + try { + const { url } = await getUrlById({ + id: urlId, + }) + const metaData = await getMetaData(url) + res.send(generateGetUrlPage(metaData)) + } catch (error) { + console.log(error) + res.status(400).json({ + error: mongoErrorService(error), + }) + } +}) + +export default router diff --git a/src/routes/getUser/getUser.ts b/src/routes/getUser/getUser.ts new file mode 100644 index 0000000..4aceaa3 --- /dev/null +++ b/src/routes/getUser/getUser.ts @@ -0,0 +1,30 @@ +import { Router, Request, Response } from 'express' +import { getUserDataWithId } from '../../models/queries/User.queries' +import checkMissingData from '../../scripts/checkMissingData' +import checkUsernameOrEmail from '../../scripts/checkUsernameOrEmail' +import mongoErrorService from '../../services/mongoErrorService' +const router = Router() + +router.get('/', async (req: Request, res: Response) => { + const values = ['password'] + const { email, username, password } = req.headers + try { + checkMissingData(values, req) + checkUsernameOrEmail(username, email, res) + const user = await getUserDataWithId({ + email: email, + username: username, + password: password, + }) + res.status(201).json({ + message: 'user found', + user, + }) + } catch (error) { + console.log(error) + res.status(400).json({ + error: mongoErrorService(error), + }) + } +}) +export default router diff --git a/src/routes/index.js b/src/routes/index.js deleted file mode 100644 index c9f4f33..0000000 --- a/src/routes/index.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = async function (fastify, options) { - fastify.get('/', async (request, reply) => { - return { test: 'testValue' } - }) -} diff --git a/src/routes/signUp/signUp.ts b/src/routes/signUp/signUp.ts new file mode 100644 index 0000000..f45bf0e --- /dev/null +++ b/src/routes/signUp/signUp.ts @@ -0,0 +1,39 @@ +import { Router, Request, Response } from 'express' +import { createUser } from '../../models/queries/User.queries' +import checkMissingData from '../../scripts/checkMissingData' +import mongoErrorService from '../../services/mongoErrorService' +import { UserRoles } from '../../utils/constants' +const router = Router() + +router.post('/', async (req: Request, res: Response) => { + const values = ['email', 'password', 'username', 'role'] + const { + email, + first_user_password, + password, + role, + sponsor_uid, + username, + } = req.headers + try { + checkMissingData(values, req) + await createUser({ + email: email, + password: password, + username: username, + role: role, + sponsorUid: sponsor_uid, + firstUserPassword: first_user_password, + }) + res.status(201).json({ + message: 'user added', + }) + } catch (error) { + console.log(error) + res.status(400).json({ + error: mongoErrorService(error), + }) + } +}) + +export default router diff --git a/src/routes/url-shortener.js b/src/routes/url-shortener.js deleted file mode 100644 index c378b1f..0000000 --- a/src/routes/url-shortener.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = async function (fastify, options) { - fastify.get('/:id', async (request, reply) => { - const { id } = request.params - return { test: id } - }) -} diff --git a/src/scripts/checkMissingData.ts b/src/scripts/checkMissingData.ts new file mode 100644 index 0000000..7ce3e57 --- /dev/null +++ b/src/scripts/checkMissingData.ts @@ -0,0 +1,19 @@ +import { Request } from 'express' + +export default function (values: string[], req: Request) { + const missingData = values.reduce((arr: string[], valueName) => { + if (!req.headers[valueName]) { + return [...arr, valueName] + } else { + return arr + } + }, []) + if (missingData.length) { + throw new Error( + JSON.stringify({ + mesasage: 'no data from user', + properties: missingData, + }) + ) + } +} diff --git a/src/scripts/checkUsernameOrEmail.ts b/src/scripts/checkUsernameOrEmail.ts new file mode 100644 index 0000000..cb20b0c --- /dev/null +++ b/src/scripts/checkUsernameOrEmail.ts @@ -0,0 +1,10 @@ +import { Response } from 'express' + +export default function (username: string, email: string, res: Response) { + if (![username, email].filter((value) => value).length) { + res.status(400).json({ + message: 'not username or email', + }) + return + } +} diff --git a/src/scripts/crypto.ts b/src/scripts/crypto.ts new file mode 100644 index 0000000..3741a57 --- /dev/null +++ b/src/scripts/crypto.ts @@ -0,0 +1,27 @@ +import crypto from 'crypto' +import { CryptoWithContent } from '../utils/constants' + +const algorithm = 'aes-256-ctr' +const secretKey = process.env.ENCRYPT_KEY +const iv = crypto.randomBytes(16) +export const encrypt = (text: string) => { + const cipher = crypto.createCipheriv(algorithm, secretKey, iv) + const encrypted = Buffer.concat([cipher.update(text), cipher.final()]) + return { + iv: iv.toString('hex'), + content: encrypted.toString('hex'), + } +} + +export const decrypt = (hash: CryptoWithContent) => { + const decipher = crypto.createDecipheriv( + algorithm, + secretKey, + Buffer.from(hash.iv, 'hex') + ) + const decrpyted = Buffer.concat([ + decipher.update(Buffer.from(hash.content, 'hex')), + decipher.final(), + ]) + return decrpyted.toString() +} diff --git a/src/scripts/generateId.ts b/src/scripts/generateId.ts new file mode 100644 index 0000000..d4e569d --- /dev/null +++ b/src/scripts/generateId.ts @@ -0,0 +1,12 @@ +import { charactersForIdGeneration } from '../utils/constants' + +export default function (length: number) { + let result = '' + const charactersLength = charactersForIdGeneration.length + for (let i = 0; i < length; i++) { + result += charactersForIdGeneration.charAt( + Math.floor(Math.random() * charactersLength) + ) + } + return result +} diff --git a/src/scripts/isUrl.ts b/src/scripts/isUrl.ts new file mode 100644 index 0000000..582a174 --- /dev/null +++ b/src/scripts/isUrl.ts @@ -0,0 +1,5 @@ +import { isUrlRegex } from '../utils/constants' + +export default function (url: string) { + return !!isUrlRegex.test(url) +} diff --git a/src/scripts/removeEmptyProperties.ts b/src/scripts/removeEmptyProperties.ts new file mode 100644 index 0000000..4786548 --- /dev/null +++ b/src/scripts/removeEmptyProperties.ts @@ -0,0 +1,5 @@ +export default function (obj: Object) { + // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars + const nonEmptyEntries = Object.entries(obj).filter(([key, value]) => value) + return Object.fromEntries(nonEmptyEntries) +} diff --git a/src/services/mongoErrorService.ts b/src/services/mongoErrorService.ts new file mode 100644 index 0000000..b5cc456 --- /dev/null +++ b/src/services/mongoErrorService.ts @@ -0,0 +1,23 @@ +import { MongoServerError } from 'mongodb' + +export default function mongoErrorService(error: any) { + let responseMessage: any + if (error instanceof MongoServerError) { + switch (error.code) { + case 1: + responseMessage = '1' + break + + default: + responseMessage = error.message + break + } + } else { + try { + responseMessage = JSON.parse(error.message) + } catch { + responseMessage = error + } + } + return responseMessage +} diff --git a/src/services/userService.ts b/src/services/userService.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..27da121 --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,13 @@ +export type Crypto = { + iv: string +} +export type CryptoWithContent = { + iv: string + content: string +} +export type UserRoles = 'admin' | 'user' +export const charactersForIdGeneration = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-()!_><' +export const isUrlRegex = new RegExp( + '^(http[s]?:\\/\\/(www\\.)?|ftp:\\/\\/(www\\.)?|www\\.){1}([0-9A-Za-z-\\.@:%_+~#=]+)+((\\.[a-zA-Z]{2,3})+)(/(.)*)?(\\?(.)*)?' +) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..37a68f6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,103 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, + // "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}