1
Fork 0

Add typescript/multiplayer-backend

This commit is contained in:
prescientmoon 2024-03-04 17:11:19 +01:00
commit 7908f1221e
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
35 changed files with 4140 additions and 12 deletions

View file

@ -1,6 +1,6 @@
# Javascript
| Name | Description |
| -------------------------------------- | -------------------------------------------------- |
| [clever-dots](./clever-dots/) | Half broken genetic algorithm implementation in js |
| [linear-regression](linear-regression) | Basic linear regression implementation |
| Name | Description |
| ---------------------------------------- | -------------------------------------------------- |
| [clever-dots](./clever-dots/) | Half broken genetic algorithm implementation in js |
| [linear-regression](./linear-regression) | Basic linear regression implementation |

View file

@ -1,10 +1,11 @@
# Typescript
| Name | Description |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| [lunardash](./lunardash/) | Rhythm game I dropped super early into development |
| [option](./option/) | Typescript implementation of the `Maybe` monad |
| [wave38](./wave38/) | Remake of [wave37](https://github.com/Mateiadrielrafael/wave37) I dropped super early into development. |
| [pleix-frontend](./pleix-frontend/) | No idea what `pleix` was supposed to be, but this was essentially just a bunch of experiments with [lit-html](https://lit.dev/) |
| [monadic](./monadic) | Custom web framework inspired by [halogen](https://github.com/purescript-halogen/purescript-halogen) |
| [og-website](./og-website) | My first ever personal website |
| Name | Description |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| [lunardash](./lunardash/) | Rhythm game I dropped super early into development |
| [monadic](./monadic) | Custom web framework inspired by [halogen](https://github.com/purescript-halogen/purescript-halogen) |
| [multiplayer-backend](./multiplayer-backend) | Unfinished server for some multiplayer game. Later rebranded into `pleix-backend` |
| [og-website](./og-website) | My first ever personal website |
| [option](./option/) | Typescript implementation of the `Maybe` monad |
| [pleix-frontend](./pleix-frontend/) | No idea what `pleix` was supposed to be, but this was essentially just a bunch of experiments with [lit-html](https://lit.dev/) |
| [wave38](./wave38/) | Remake of [wave37](https://github.com/Mateiadrielrafael/wave37) I dropped super early into development. |

View file

@ -0,0 +1,61 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Matei Adriel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,3 @@
# mp-game-server
This was one of my older projects, with it i learned how to use express-session, and eventually (for some reason) i did sessions manually (well, at least i tried).

View file

@ -0,0 +1,26 @@
//interfaces
export interface routeList {
[key: string]: string
}
export interface whiteListUrl {
url: string
methods?: string
}
//routes
export const baseUrl = "./src/routes/"
export const routes: routeList = {
"/logs": "logging/logs.ts",
"/auth": "auth/auth.ts"
}
//whitelist of urls
export const whiteList: whiteListUrl[] = [{
url: "localhost:4200"
},{
url: "localhost:8000"
},{
url: "localhost:3000"
}]
export const staticRoutes:string[] = []

View file

@ -0,0 +1,12 @@
{
"type": "service_account",
"project_id": "planets-io",
"private_key_id": "64f5c2a6dab6521e5a7236332aca1b72bc49cd4b",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCsQqNWLgveNEud\nvPbYNStN3+KXGqnV2km6JUDQ431oQ4uObKZ8neG6T0cSbQv5j7BmBjLAZIkIGKCc\nLEfRDyUOPlP3l1eBf1txpBoj5YsnH7dBBbi1TqbKihJF0nuCzrYn0zUpVqVIVH8q\nKbs1twFecxd9OIxBY+MaGUI2FQsu8O1m7Xs2RCsur61CAFTvtKeJFdsDAOAuK33b\nws5Nu3WY0cw7yDNGQAY7kDmWNIL4pfbcN1aVsOAQZcF/W/QtEfnaZSfcE1M410tE\n8H0NCXuupsyyoWWuOt/crFmFK3Fd6bg2MHbHJt3y7Xl12xI7OZWdeOiQ0zdri8+j\n6CULzRNLAgMBAAECggEAPtjvK8WP+el9fxbOvpIjcv2Qd424UfgatwnZbHzbJ7uh\nAnz8fvHBRc6fwQa8/Dei5Y40XQIxBp2pl2EGcK1EWT5/NxFl0S7Mh5kWGmuoicUm\nAFici7We371hLk09V7ugNMZ3mlXs5odBf/oKve2/V5zJAJwte7v8QuzdPcsOsfiG\nWguvSMQ1IoQn92KSFpYbxkXtfN3eQ34pE/zKMbkneoNmNaTZG3tfBjIoODvzas/x\nw0Ey8np5LOAmPnPpf92x7dj6vOtMdGtHZMhBwzc0kPE4UXy/EfIXOjIyG82nRckF\njxVANNvBGCqTfW0O+mTbUVUUcvKTpJ3JrPbG5vRNTQKBgQDbnI+MyL1xmRRT9uJR\nxyJeLQZWPc/dfVmn3uitQXHrVO0QWbAJb8NpOF0YrMKa1ODzTvJgYGLpVk3bvQT4\nu1MQKSycK5bT2wl7nRlhBNeFD6kng1YZ1jmEa/0ec1+Fl/Z8Pl2Rocj/U4YrfHna\nOAVz2Pk4cv/WT8MrnLS5w6XaZQKBgQDIzYqBNdzfCf1filAAb/SeDi2+871qVl2n\nDGTj09q8Q2GBlHiReWCKYz3ENyKxQ/93OUbNFnAzqkVYgCfBhMetq6/B3wcJblO7\nMdYAOi9Bxj+yYN7M5iRCQrNT/pYMyaVngrpTYp6Yyi4OBK4X2bTnN29mdHEXgmQh\nJIbXRtsD7wKBgB5KDqZ9PVvAoEQgZgCfdYcFsL0OU3AaDNaRcQgMYbjygqvhotz6\nVDpb8sMneMPAHrKQLe/K+3lxVNW80UW5GHC/LQ7xGCFqMXmiJlDySQNqMItpmuN/\nX3l7J3MeuIsFdZKXS3J9nOpSS8wNpATL+zyKLX1ypwSZBbMrLuX8LRDdAoGAGJKu\npGlHAjRiwVJXv/XzZXQuvPNu9phjFZI6tnayid9lC0p0BrlnyweiA2UL5c2AccKc\nm3RnfqsTmWT1eIUbOnGMLJlybwEFVh8fJR/5sH7mRJn+Kezv+vvWnizFiAMVLrmc\nu5+R8Pis8iI4e8q0jKcIBu2w+UOHpVam+ak+HBkCgYBdPjpnl8Wn/0qtRSX2H4u9\nweF1R1KSRdLtWrG7JIB3gyEsHW6yhAmwKNj1ss4wG9/P7YF7QGKQLQJ6VJ56Snjy\n6gUVamFo8apu+rNFj1U+mok/UbltgzlMrP1G/G/yUJTp16547IvSrR4a8+TecWTg\n3QpvQ5GdifLzmzTL4MGDCg==\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-ssnid@planets-io.iam.gserviceaccount.com",
"client_id": "112323453968848896098",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-ssnid%40planets-io.iam.gserviceaccount.com"
}

View file

@ -0,0 +1,6 @@
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node ./src/main.ts"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
{
"name": "mp-game-back-end",
"version": "1.0.0",
"description": "a multiplayer game (idk for now)",
"main": "src/main.ts",
"scripts": {
"dev": "nodemon"
},
"repository": {
"type": "git",
"url": "\u0016https://github.com/Mateiadrielrafael/mp-game-server"
},
"keywords": [
"mp"
],
"author": "Matei Adriel",
"license": "ISC",
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/chalk": "^2.2.0",
"@types/cors": "^2.8.5",
"@types/dotenv": "^6.1.1",
"@types/express": "^4.16.1",
"@types/express-session": "^1.15.12",
"@types/express-validator": "^3.0.0",
"@types/lodash": "^4.14.123",
"@types/mongoose": "^5.5.0",
"@types/morgan": "^1.7.35",
"nodemon": "^1.18.10",
"ts-node": "^8.0.3"
},
"dependencies": {
"bcryptjs": "^2.4.3",
"chalk": "^2.4.2",
"cors": "^2.8.5",
"dotenv": "^7.0.0",
"express": "^4.16.4",
"express-validator": "^5.3.1",
"lodash": "^4.17.11",
"mongoose": "^5.5.7",
"morgan": "^1.9.1",
"ts-mongoose": "0.0.14",
"typescript": "^3.4.1"
}
}

View file

@ -0,0 +1 @@
export * from "./preventGlobalError"

View file

@ -0,0 +1,15 @@
import { Request, Response } from "express";
export const preventGlobalErrors = (func: (req: Request, res: Response) => any) => {
return async (req: Request, res: Response) => {
try {
await func(req,res)
}
catch (err) {
res.json({
succes: false,
errors: [err]
}).status(400)
}
}
}

View file

@ -0,0 +1 @@
export * from "./respond"

View file

@ -0,0 +1,10 @@
import { Response } from "express";
export const respond = (res:Response, succes = true, data: any = {},
errors:any[] = [], status = 200) => {
res.json({
succes,
data,
errors
}).status(status)
}

View file

@ -0,0 +1,45 @@
import * as express from "express"
import chalk from "chalk";
import * as cors from "cors"
import { staticRoutes } from "../config";
import { routes } from "./routes"
import { json } from "body-parser";
import { morganChalk } from "./middleware/morgan";
import { sessionMiddleware } from "./middleware/sessions"
// const firestore = store(sessions)
export interface serverSetupResults {
app: express.Application
}
export const setupServer = (): Promise<serverSetupResults> =>
new Promise(async (res, rej) => {
try {
//create express app
const app = express()
app.use(
cors(),
json(),
morganChalk,
sessionMiddleware
)
//load static routes
staticRoutes.forEach(route => {
app.use(express.static(`${route}`))
})
//Load normal routes
for (let i in routes) {
app.use(`/${i}`, routes[i])
}
console.log(chalk.bold.green("👏 Succesfully creatd server!"))
res({ app })
}
catch (err) {
rej(err)
}
})

View file

@ -0,0 +1,27 @@
import { config } from "dotenv"
import { setupServer } from "./createServer";
import { connected } from "./services/db/mongo"
import chalk from "chalk";
//⚓ connect to mongodb
connected.then(val => {
console.log(chalk.bold.green("⚓ Succesfully connected to mongoDb!!!"))
}).catch(err => {
console.log(err)
console.log(chalk.bold.red("😭 Something went wrong when connecting to mongoDb!!!"))
process.exit(1)
})
//🗻 extract env variables
config()
const main = async () => {
//create the server
const { app } = await setupServer()
//start listeing to requests
app.listen(process.env.PORT)
}
main()

View file

@ -0,0 +1,26 @@
import * as morgan from "morgan"
import chalk from "chalk"
const { floor } = Math
export const morganChalk = morgan(function (tokens, req, res) {
const status = Number(tokens.status(req,res))
const statusFirstDigit = floor(status / 100)
const emoji = (status === 200) ?
"👌" : (status === 203) ?
"🔎" : (status == 202) ?
"🚧" : (status === 302) ?
"🏹" : (statusFirstDigit === 2) ?
"🌝" : (statusFirstDigit === 4) ?
"😠" :
"❓"
return [
chalk.bold(`${emoji} `),
chalk.green.bold(tokens.method(req, res)),
chalk.red.bold(tokens.status(req, res)),
chalk.blue.italic(tokens.url(req, res)),
chalk.yellow(`${tokens['response-time'](req, res)} ms`),
].join(' ');
});

View file

@ -0,0 +1,12 @@
import { Request, Response } from "express"
/**
* boilerplate for reddirecting
* @param url the url to reddirect to
* @returns the middleware
*/
export const reddirect = (url: string) =>
(req: Request, res: Response, next: any) => {
res.redirect(url)
next()
}

View file

@ -0,0 +1 @@
export * from "./sessions"

View file

@ -0,0 +1,65 @@
import { Response, Request } from "express";
import { SessionDataDoc, SessionData } from "../../models/SessionData"
const getToken = (req: Request) => {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { // Authorization: Bearer g1jipjgi1ifjioj
// Handle token presented as a Bearer token in the Authorization header
return req.headers.authorization.split(' ')[1];
} else if (req.query && req.query.token) {
// Handle token presented as URI param
return req.query.token;
} else if (req.cookies && req.cookies.token) {
// Handle token presented as a cookie parameter
return req.cookies.token;
}
// If we return null, we couldn't find a token.
// In this case, the JWT middleware will return a 401 (unauthorized) to the client for this request
return null;
}
export const sessionMiddleware = async (req: Request, res: Response, next: Function) => {
const token = getToken(req)
//if we are trying to get an token, allow this
if (req.path === "/token")
return next()
//if we dont have any token
if (!token)
return res.json({ succes: false }).status(400)
//try searching for the object in the database
const result = await SessionData.findOne({ token })
if (!result)
return res.json({ succes: false }).status(400)
const data = JSON.parse(result.data)
if (!req.session)
//@ts-ignore
req.session = {}
for (let i in data)
req.session[i] = data[i]
req.session.save = async () => {
const toSave:any = {}
for (let i in req.session) {
if (i == "save") continue
toSave[i] = req.session[i]
}
const data:string = JSON.stringify(toSave)
return await result.updateOne({
token,
data
})
}
next()
}

View file

@ -0,0 +1,9 @@
import { createSchema, Type, typedModel, ExtractDoc } from "ts-mongoose"
const PasswordSchema = createSchema({
uid: Type.string(),
password: Type.string()
})
export const Passsord = typedModel("Password", PasswordSchema)
export type PasswordDoc = ExtractDoc<typeof PasswordSchema>;

View file

@ -0,0 +1,9 @@
import { createSchema, Type, typedModel, ExtractDoc } from "ts-mongoose"
const SessionDataSchema = createSchema({
token: Type.string(),
data: Type.string()
})
export const SessionData = typedModel("SessionData", SessionDataSchema)
export type SessionDataDoc = ExtractDoc<typeof SessionDataSchema>;

View file

@ -0,0 +1,13 @@
import { createSchema, Type, typedModel, ExtractDoc } from "ts-mongoose"
const UserSchema = createSchema({
name: Type.string(),
email: Type.string(),
photo: Type.string(),
friends: Type.array().of(Type.string()),
description: Type.optionalString(),
uid: Type.string()
})
export const User = typedModel("User", UserSchema)
export type UserDoc = ExtractDoc<typeof UserSchema>;

View file

@ -0,0 +1,151 @@
import { Router, Response, Request } from "express"
import * as validator from "express-validator"
import { generateUid, encodePassword } from "../../services/auth/signup";
import { savePassword } from "../../services/auth/signup";
import { config } from "dotenv"
import { compare } from "bcryptjs";
import { User, UserDoc } from "../../models/User";
import { Passsord } from "../../models/Password";
import { preventGlobalErrors } from "../../common/preventGlobalError";
import { respond } from "../../common/respond";
//extract env variables
config()
const router = Router()
const authHandler = preventGlobalErrors(async (req: Request, res: Response) => {
//if already logged in return the uid
if (req.session.uid)
return respond(res, true, { uid: req.session.uid })
//get data from body
const { password, email, name } = req.body
//check if the email isnt used
if (await User.findOne({ email }))
return respond(res, false, { email }, ["email is already used"], 400)
//validate
req.check("email", "email isnt valid").isEmail()
const errors = req.validationErrors() as any[]
if (errors)
return respond(res, false, {}, errors, 400)
//generate an uid
const uid = await generateUid()
const user = new User({
email, name, uid,
friends: [],
photo: process.env.DEFAULTPROFILEPIC
} as UserDoc) //used for my editor to help me
//save things in session
req.session.uid = uid
req.session.save(() => { })
await Promise.all([
encodePassword(password).then(result => savePassword(uid, result)),
user.save()
])
//send uid back
return respond(res,true,{ uid })
})
const account = async (req: Request, res: Response) => {
try {
if (!req.session.uid)
res.json({
succes: false,
data: {},
errors: ["uid doesnt exist"]
}).status(203)
res.json({
succes: true,
data: {
uid: req.session.uid
}
})
}
catch (err) {
//send erros to clinet
res.json({ succes: false, errors: [err] })
}
}
const login = async (req: Request, res: Response) => {
try {
//check if we have an email or an username, and if we are note already logged in
if (req.session.uid)
return res.redirect("account")
let type = "name"
if (req.body.email)
type = "email"
const { password } = req.body
const data = req.body[(type == "name") ? "name" : type]
const doc = await User.findOne({ [data]: req.body[data] })
const uid = doc.uid
//check if the password is good
const passwordHash = await Passsord.findOne({ uid })
const match = await compare(password, passwordHash.password)
//return result
if (!match)
return res.json({
succes: false,
errors: ["wrong password"],
data: {
uid,
hash: passwordHash.password,
password,
[data]: req.body[data]
}
}).status(400)
//update the session ond save it
req.session.uid = uid
req.session.save(() => { })
//send it to the clinet
return res.json({ uid })
}
catch (errors) {
return res.json({ errors })
}
}
const logout = async (req: Request, res: Response) => {
//clear the uid
req.session.uid = undefined
//save it to the db
req.session.save(() => { })
res.send({ succes: true })
}
router.use("*", validator())
router
.post("/", authHandler)
.post("/login", login)
.get("/account", account)
.get("/logout", logout)
.get("/uid", (req, res) => {
res.send(req.session.uid)
})
.get("/test/uid", async (req, res) => {
res.send(await generateUid())
})
export const auth = router

View file

@ -0,0 +1,2 @@
export * from "./auth"
export * from "./token"

View file

@ -0,0 +1,30 @@
import { Router, Response, Request } from "express"
import { randomBytes } from "crypto"
import { SessionData,SessionDataDoc } from "../../models/SessionData";
const router = Router()
const getToken = async (req: Request, res: Response) => {
//generate token
const token = randomBytes(16).toString("hex")
//save token into db
const data = new SessionData({
token,
data:"{}"
} as SessionDataDoc)
await data.save()
res.json({
succes:true,
data:{
token
}
})
}
router.get("/", getToken)
export const token = router

View file

@ -0,0 +1,9 @@
import { auth, token } from "./auth"
import { logs } from "./logging"
import { Router } from "express";
export const routes:{[key:string]:Router} = {
auth,
logs,
token
}

View file

@ -0,0 +1 @@
export * from "./logs"

View file

@ -0,0 +1,10 @@
export interface log{
title:string;
body:string;
time:Date;
elapsed?:number;
}
export interface shortConsole{
logs:log[];
log:(name:string,body:string) => any;
}

View file

@ -0,0 +1,14 @@
import { Router, Response, Request } from "express"
import { shortLogger } from "./shortLog";
import { reddirect } from "../../middleware/reddirect";
const router = Router()
const getShortLogs = (req: Request, res: Response) => {
res.json(shortLogger.logs)
}
router.get("/", getShortLogs)
router.use("/*", reddirect("/logs"))
export const logs = router

View file

@ -0,0 +1,32 @@
import { performance } from "perf_hooks"
import { config } from "dotenv";
import { shortConsole } from "./interfaces";
config()
//clear the console
if (process.env.MODE == "DEV")
console.clear()
//used to log things
const shortLogger: shortConsole = {
//holds all the logs
logs: [],
//log a new message
log: (title: string, body: string) => {
//push the logs to the log array
shortLogger.logs.push({
title,
body,
time: new Date(),
elapsed: Math.floor(performance.now())
})
//if we are in dev mode log it
if (process.env.MODE == "DEV")
console.log(body)
}
}
export { shortLogger }

View file

@ -0,0 +1,45 @@
import * as bcrypt from "bcryptjs"
import { promisify } from "util";
import { randomBytes } from "crypto";
import { Passsord, PasswordDoc } from "../../models/Password";
import { User } from "../../models/User";
//promisify functions
const hash = promisify(bcrypt.hash)
export async function generateUid(): Promise<string> {
//generate uid
const uid = randomBytes(16).toString("hex")
//check if document exists
const doc = await User.findOne({ uid })
//if theres already an user with the same id, regenerate
if (doc)
return generateUid()
//then return the uid
return uid
}
export function encodePassword(password: string): Promise<string> {
return hash(password, 10)
}
export function savePassword(uid: string, password: string) {
const passwordInstance = new Passsord({
password, uid
} as PasswordDoc)
return passwordInstance.save()
}
export async function getPassword(uid: string) {
//get doc
// const doc = await database.doc(`passwords/${uid}`).get()
//return result
// return doc.data()
return "train"
}

View file

@ -0,0 +1 @@
export * from "./mongo"

View file

@ -0,0 +1,16 @@
import { config } from "dotenv"
import { connect, model, connection } from "mongoose"
config()
connect(process.env.DATABASE, { useNewUrlParser: true })
export const connected = new Promise((res, rej) => {
connection.on("open", (...args: any[]) => {
res(...args)
})
connection.on("error", (...args: any[]) => {
rej(...args)
})
})
export { connection }

View file

@ -0,0 +1,33 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"types/*"
]
},
"module": "commonjs",
"removeComments": true,
"lib": [
"es2015",
"es2017",
"dom"
],
"noImplicitAny": true,
"alwaysStrict": true,
"moduleResolution": "Node",
"experimentalDecorators": true,
"downlevelIteration": true,
"target": "ESNext",
"resolveJsonModule": true,
"jsx": "preserve"
},
"include": [
"src"
],
"exclude": [
"node_modules",
"dist",
"old_client"
]
}