first working prototype of shortly

This commit is contained in:
2023-01-12 21:18:49 -03:00
parent a897ec5f95
commit 856e01d339
34 changed files with 745 additions and 100 deletions

20
src/app.ts Normal file
View 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}`)
})

View File

@@ -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()

View 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)
})
}

View 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
}

View 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()
}

View 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)

View 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)

View 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>
`
}

View File

@@ -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
})
}

View 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

View File

@@ -0,0 +1,12 @@
{
"ids": [
"settings",
"register",
"login",
"admin",
"franp",
"github",
"linkedin",
"add"
]
}

View 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

View 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

View File

@@ -1,5 +0,0 @@
module.exports = async function (fastify, options) {
fastify.get('/', async (request, reply) => {
return { test: 'testValue' }
})
}

View 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

View File

@@ -1,6 +0,0 @@
module.exports = async function (fastify, options) {
fastify.get('/:id', async (request, reply) => {
const { id } = request.params
return { test: id }
})
}

View 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,
})
)
}
}

View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
import { isUrlRegex } from '../utils/constants'
export default function (url: string) {
return !!isUrlRegex.test(url)
}

View 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)
}

View 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
}

View File

13
src/utils/constants.ts Normal file
View 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})+)(/(.)*)?(\\?(.)*)?'
)