mirror of
https://github.com/FranP-code/shortly.git
synced 2025-10-13 00:43:28 +00:00
first working prototype of shortly
This commit is contained in:
@@ -1,15 +1,10 @@
|
|||||||
{
|
{
|
||||||
"env": {
|
"parser": "@typescript-eslint/parser",
|
||||||
"browser": true,
|
"plugins": ["@typescript-eslint"],
|
||||||
"commonjs": true,
|
|
||||||
"es2021": true
|
|
||||||
},
|
|
||||||
"extends": "standard",
|
|
||||||
"overrides": [
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest"
|
|
||||||
},
|
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"semi": ["error", "never"],
|
||||||
|
"quotes": [2, "single"],
|
||||||
|
"no-unused-vars": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": "error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
.env
|
||||||
|
dist
|
||||||
|
yarn-error.log
|
||||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
@@ -7,4 +7,11 @@ services:
|
|||||||
- .:/usr/src
|
- .:/usr/src
|
||||||
- /usr/src/node_modules
|
- /usr/src/node_modules
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- 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}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{"forbidden-ids": [
|
|
||||||
"settings",
|
|
||||||
"register",
|
|
||||||
"login",
|
|
||||||
"admin",
|
|
||||||
"franp",
|
|
||||||
"github",
|
|
||||||
"linkedin",
|
|
||||||
"add"
|
|
||||||
]}
|
|
||||||
5
nodemon.json
Normal file
5
nodemon.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"watch": ["src"],
|
||||||
|
"ext": "ts, json",
|
||||||
|
"exec": "ts-node ./src/app.ts"
|
||||||
|
}
|
||||||
73
package.json
73
package.json
@@ -1,33 +1,44 @@
|
|||||||
{
|
{
|
||||||
"name": "url-shorter",
|
"name": "shortly",
|
||||||
"description": "",
|
"description": "",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"start": "node src/index.js",
|
"start": "node dist/app.js",
|
||||||
"dev": "nodemon src/index.js"
|
"dev": "nodemon -L src/app.ts"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/monatheoctocat/my_package.git"
|
"url": "https://github.com/monatheoctocat/my_package.git"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "FranP-Code",
|
"author": "FranP-Code",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/monatheoctocat/my_package/issues"
|
"url": "https://github.com/monatheoctocat/my_package/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/monatheoctocat/my_package",
|
"homepage": "https://github.com/monatheoctocat/my_package",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/jwt": "^6.5.0",
|
"@types/express": "^4.17.15",
|
||||||
"fastify": "^4.10.2"
|
"dotenv": "^16.0.3",
|
||||||
},
|
"express": "^4.18.2",
|
||||||
"devDependencies": {
|
"jsonwebtoken": "^9.0.0",
|
||||||
"eslint": "^8.0.1",
|
"metadata-scraper": "^0.2.61",
|
||||||
"eslint-config-standard": "^17.0.0",
|
"mongoose": "5.13.14",
|
||||||
"eslint-plugin-import": "^2.25.2",
|
"ulid": "^2.3.0"
|
||||||
"eslint-plugin-n": "^15.0.0",
|
},
|
||||||
"eslint-plugin-promise": "^6.0.0",
|
"devDependencies": {
|
||||||
"nodemon": "^2.0.20"
|
"@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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/app.ts
Normal file
20
src/app.ts
Normal file
@@ -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}`)
|
||||||
|
})
|
||||||
19
src/index.js
19
src/index.js
@@ -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()
|
|
||||||
19
src/models/dbConnection.ts
Normal file
19
src/models/dbConnection.ts
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
58
src/models/queries/Url.queries.ts
Normal file
58
src/models/queries/Url.queries.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
111
src/models/queries/User.queries.ts
Normal file
111
src/models/queries/User.queries.ts
Normal file
@@ -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()
|
||||||
|
}
|
||||||
34
src/models/schemas/Url.schema.ts
Normal file
34
src/models/schemas/Url.schema.ts
Normal file
@@ -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<IUrl>(
|
||||||
|
{
|
||||||
|
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<IUrl> = model('Url', urlSchema)
|
||||||
52
src/models/schemas/User.schema.ts
Normal file
52
src/models/schemas/User.schema.ts
Normal file
@@ -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<IUser>(
|
||||||
|
{
|
||||||
|
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<IUser> = model('User', userSchema)
|
||||||
18
src/pages/getUrl/generateGetUrlPage.ts
Normal file
18
src/pages/getUrl/generateGetUrlPage.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export default function (metaData: any) {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>${metaData.title}</title>
|
||||||
|
<meta name="description" content="${metaData.description}">
|
||||||
|
<meta property="og:image" content="${metaData.image}">
|
||||||
|
<meta property="og:url" content="${metaData.url}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window.location.href = "${metaData.url}";
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
33
src/routes/addUrl/addUrl.ts
Normal file
33
src/routes/addUrl/addUrl.ts
Normal file
@@ -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(<string>username, <string>email, res)
|
||||||
|
const createdUrl = await createUrl({
|
||||||
|
email: <string>email,
|
||||||
|
password: <string>password,
|
||||||
|
url: <string>url,
|
||||||
|
username: <string>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
|
||||||
12
src/routes/get-url/forbidden-ids.json
Normal file
12
src/routes/get-url/forbidden-ids.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"ids": [
|
||||||
|
"settings",
|
||||||
|
"register",
|
||||||
|
"login",
|
||||||
|
"admin",
|
||||||
|
"franp",
|
||||||
|
"github",
|
||||||
|
"linkedin",
|
||||||
|
"add"
|
||||||
|
]
|
||||||
|
}
|
||||||
30
src/routes/get-url/getUrl.ts
Normal file
30
src/routes/get-url/getUrl.ts
Normal file
@@ -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
|
||||||
30
src/routes/getUser/getUser.ts
Normal file
30
src/routes/getUser/getUser.ts
Normal file
@@ -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(<string>username, <string>email, res)
|
||||||
|
const user = await getUserDataWithId({
|
||||||
|
email: <string>email,
|
||||||
|
username: <string>username,
|
||||||
|
password: <string>password,
|
||||||
|
})
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'user found',
|
||||||
|
user,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
res.status(400).json({
|
||||||
|
error: mongoErrorService(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export default router
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = async function (fastify, options) {
|
|
||||||
fastify.get('/', async (request, reply) => {
|
|
||||||
return { test: 'testValue' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
39
src/routes/signUp/signUp.ts
Normal file
39
src/routes/signUp/signUp.ts
Normal file
@@ -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: <string>email,
|
||||||
|
password: <string>password,
|
||||||
|
username: <string>username,
|
||||||
|
role: <UserRoles>role,
|
||||||
|
sponsorUid: <string>sponsor_uid,
|
||||||
|
firstUserPassword: <string>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
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = async function (fastify, options) {
|
|
||||||
fastify.get('/:id', async (request, reply) => {
|
|
||||||
const { id } = request.params
|
|
||||||
return { test: id }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
19
src/scripts/checkMissingData.ts
Normal file
19
src/scripts/checkMissingData.ts
Normal file
@@ -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,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/scripts/checkUsernameOrEmail.ts
Normal file
10
src/scripts/checkUsernameOrEmail.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/scripts/crypto.ts
Normal file
27
src/scripts/crypto.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import crypto from 'crypto'
|
||||||
|
import { CryptoWithContent } from '../utils/constants'
|
||||||
|
|
||||||
|
const algorithm = 'aes-256-ctr'
|
||||||
|
const secretKey = <string>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()
|
||||||
|
}
|
||||||
12
src/scripts/generateId.ts
Normal file
12
src/scripts/generateId.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
5
src/scripts/isUrl.ts
Normal file
5
src/scripts/isUrl.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { isUrlRegex } from '../utils/constants'
|
||||||
|
|
||||||
|
export default function (url: string) {
|
||||||
|
return !!isUrlRegex.test(url)
|
||||||
|
}
|
||||||
5
src/scripts/removeEmptyProperties.ts
Normal file
5
src/scripts/removeEmptyProperties.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
23
src/services/mongoErrorService.ts
Normal file
23
src/services/mongoErrorService.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
0
src/services/userService.ts
Normal file
0
src/services/userService.ts
Normal file
13
src/utils/constants.ts
Normal file
13
src/utils/constants.ts
Normal file
@@ -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})+)(/(.)*)?(\\?(.)*)?'
|
||||||
|
)
|
||||||
103
tsconfig.json
Normal file
103
tsconfig.json
Normal file
@@ -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 '<reference>'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. */
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user