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:
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})+)(/(.)*)?(\\?(.)*)?'
|
||||
)
|
||||
Reference in New Issue
Block a user