typescript(multiplayer-backend): initial commit
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
1544ca69e2
commit
7f3b756378
21 changed files with 807 additions and 80 deletions
typescript/multiplayer-backend/src
|
@ -1,26 +1,56 @@
|
|||
import * as express from "express"
|
||||
import { baseUrl, routes } from "../config";
|
||||
import { performance } from "perf_hooks"
|
||||
import { shortLogger } from "./routes/logging/shortLog";
|
||||
import * as sessions from "express-session"
|
||||
import chalk from "chalk";
|
||||
|
||||
export interface serverSetupResults{
|
||||
time:number;
|
||||
app:express.Application
|
||||
import { staticRoutes } from "../config";
|
||||
import { routes } from "./routes"
|
||||
import { urlencoded } from "body-parser";
|
||||
import { database } from "./services/db/firestore"
|
||||
import { morganChalk } from "./middleware/morgan";
|
||||
import { sessionMiddleware } from "./middleware/sessions"
|
||||
|
||||
|
||||
// @ts-ignore no declaration file
|
||||
// import * as store from "firestore-store"
|
||||
import * as store from "connect-mongo"
|
||||
import { connection, connected } from "./services/db/mongo";
|
||||
|
||||
// const firestore = store(sessions)
|
||||
export interface serverSetupResults {
|
||||
app: express.Application
|
||||
}
|
||||
|
||||
export const setupServer = ():serverSetupResults => {
|
||||
export const setupServer = (): Promise<serverSetupResults> =>
|
||||
new Promise(async (res, rej) => {
|
||||
try {
|
||||
let MongoStore = store(sessions)
|
||||
|
||||
const start = performance.now()
|
||||
const app = express()
|
||||
await connected
|
||||
|
||||
for (let i in routes) {
|
||||
const route = require(`${process.cwd()}/${baseUrl}${routes[i]}`)
|
||||
app.use(i, route.router)
|
||||
}
|
||||
//create express app
|
||||
const app = express()
|
||||
|
||||
const time = performance.now() - start
|
||||
const message = `Server created in: ${Math.floor(time)}ms`
|
||||
shortLogger.log("Server created",message)
|
||||
app.use(urlencoded({ extended: true }), sessions({
|
||||
secret: process.env.SESSION_SECRET,
|
||||
saveUninitialized: false,
|
||||
resave: false,
|
||||
store: new MongoStore({ mongooseConnection: connection })
|
||||
}), morganChalk, sessionMiddleware)
|
||||
|
||||
return {app,time}
|
||||
}
|
||||
//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)
|
||||
}
|
||||
})
|
|
@ -1,11 +1,27 @@
|
|||
import { config } from "dotenv"
|
||||
import { setupServer } from "./createServer";
|
||||
import { connected } from "./services/db/mongo"
|
||||
import chalk from "chalk";
|
||||
|
||||
//extract env variables
|
||||
//⚓ 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()
|
||||
|
||||
//create the server
|
||||
const {app} = setupServer()
|
||||
const main = async () => {
|
||||
|
||||
//start listeing to requests
|
||||
app.listen(process.env.PORT)
|
||||
//create the server
|
||||
const { app } = await setupServer()
|
||||
|
||||
//start listeing to requests
|
||||
app.listen(process.env.PORT)
|
||||
}
|
||||
|
||||
main()
|
|
@ -1,5 +1,8 @@
|
|||
import { Request, Response } from "express"
|
||||
import { whiteList, whiteListUrl } from "../../config"
|
||||
import { whiteList } from "../../config"
|
||||
import { includes } from "lodash"
|
||||
|
||||
const urls = whiteList.map(urlObject => urlObject.url)
|
||||
|
||||
//TODO: remove from here
|
||||
const defaultMethods = "GET, POST, OPTIONS, PUT, PATCH, DELETE"
|
||||
|
@ -9,21 +12,17 @@ const defaultMethods = "GET, POST, OPTIONS, PUT, PATCH, DELETE"
|
|||
* @param url the url to allow
|
||||
* @param methods the methods to allow the url to do
|
||||
*/
|
||||
export const corsErrorFixer = ({url,methods}:whiteListUrl) => {
|
||||
return (req: Request, res: Response, next: any) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', url) // Website you wish to allow to connect
|
||||
res.setHeader('Access-Control-Allow-Methods', methods || defaultMethods) //// Request methods you wish to allow
|
||||
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type') // Request headers you wish to allow
|
||||
export const corsErrorFixer = (req: Request, res: Response, next: any) => {
|
||||
const host = req.get("host")
|
||||
const methods: any = whiteList.find(value => value.url == host) || { methods: undefined }
|
||||
|
||||
// Pass to next layer of middleware
|
||||
next();
|
||||
}
|
||||
}
|
||||
if (!includes(urls, host))
|
||||
return next()
|
||||
|
||||
/**
|
||||
* used to allow all urls on the whitelist
|
||||
*/
|
||||
export const allowWhitelist = () => {
|
||||
return whiteList.map(value => corsErrorFixer(value))
|
||||
}
|
||||
res.setHeader('Access-Control-Allow-Origin', host) // Website you wish to allow to connect
|
||||
res.setHeader('Access-Control-Allow-Methods', methods.methods || defaultMethods) //// Request methods you wish to allow
|
||||
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type') // Request headers you wish to allow
|
||||
|
||||
// Pass to next layer of middleware
|
||||
next();
|
||||
}
|
|
@ -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(' ');
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export * from "./sessions"
|
|
@ -0,0 +1,24 @@
|
|||
import { Response, Request } from "express";
|
||||
|
||||
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 = (req: Request, res: Response, next: Function) => {
|
||||
const token = getToken(req)
|
||||
console.log(token)
|
||||
|
||||
next()
|
||||
}
|
9
typescript/multiplayer-backend/src/models/Password.ts
Normal file
9
typescript/multiplayer-backend/src/models/Password.ts
Normal 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>;
|
13
typescript/multiplayer-backend/src/models/User.ts
Normal file
13
typescript/multiplayer-backend/src/models/User.ts
Normal 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>;
|
|
@ -1,11 +1,49 @@
|
|||
import { Router, Response, Request, urlencoded } from "express"
|
||||
import { Router, Response, Request } from "express"
|
||||
import { reddirect } from "../../middleware/reddirect";
|
||||
import { allowWhitelist } from "../../middleware/corsErrorMiddleware";
|
||||
import * as sessions from "express-session"
|
||||
import * as validator from "express-validator"
|
||||
import { generateUid, encodePassword } from "../../services/auth/signup";
|
||||
import { savePassword } from "../../services/auth/signup";
|
||||
import { corsErrorFixer } from "../../middleware/corsErrorMiddleware";
|
||||
import { config } from "dotenv"
|
||||
import { compare } from "bcryptjs";
|
||||
import { User, UserDoc } from "../../models/User";
|
||||
import { Passsord } from "../../models/Password";
|
||||
|
||||
//extract env variables
|
||||
config()
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get("/test", (req, res) => {
|
||||
res.send("just a test")
|
||||
})
|
||||
|
||||
const loginHtml = (req: Request, res: Response) => {
|
||||
res.send(`
|
||||
<form action="/auth/login" method=post>
|
||||
<div>
|
||||
<label for=email>email</label>
|
||||
<input type=text id=email name=email>
|
||||
</div>
|
||||
<div>
|
||||
<label for=password>password</label>
|
||||
<input type=password id=password name=password>
|
||||
</div>
|
||||
<div>
|
||||
<label for=name>name</label>
|
||||
<input type=name id=name name=name>
|
||||
</div>
|
||||
<button type=submit onclick="
|
||||
alert('click')
|
||||
fetch('/',{
|
||||
headers: {
|
||||
authorization: 'do u see this?'
|
||||
}
|
||||
})">Submit</button>
|
||||
</form>
|
||||
`)
|
||||
}
|
||||
|
||||
const sayHello = (req: Request, res: Response) => {
|
||||
res.send(`
|
||||
<form action="/auth" method=post>
|
||||
|
@ -13,34 +51,172 @@ const sayHello = (req: Request, res: Response) => {
|
|||
<label for=email>email</label>
|
||||
<input type=text id=email name=email>
|
||||
</div>
|
||||
<div>
|
||||
<label for=password>password</label>
|
||||
<input type=password id=password name=password>
|
||||
</div>
|
||||
<div>
|
||||
<label for=name>name</label>
|
||||
<input type=name id=name name=name>
|
||||
</div>
|
||||
<button type=submit>Submit</button>
|
||||
</form>
|
||||
`)
|
||||
}
|
||||
|
||||
const auth = (req: Request, res: Response, next: any) => {
|
||||
//validate
|
||||
req.check("email", "email isnt valid").isEmail()
|
||||
const errors = req.validationErrors()
|
||||
const authHandler = async (req: Request, res: Response) => {
|
||||
try {
|
||||
//if already logged in return the uid
|
||||
if (req.session.uid)
|
||||
return res.redirect("auth/account")
|
||||
|
||||
//if we have erros mark it
|
||||
if (errors)
|
||||
req.session.errors = errors
|
||||
//get data from body
|
||||
const { password, email, name } = req.body
|
||||
|
||||
//reddirect to page
|
||||
res.send(errors || "Succes!!!")
|
||||
//check if the email isnt used
|
||||
if (await User.findOne({ name }))
|
||||
res.redirect("login")
|
||||
|
||||
//validate
|
||||
req.check("email", "email isnt valid").isEmail()
|
||||
const errors = req.validationErrors()
|
||||
|
||||
if (errors)
|
||||
res.json({ error: `${req.body.email} is not a valid email` })
|
||||
|
||||
//generate an uid
|
||||
const uid = await generateUid()
|
||||
|
||||
const user = new User({
|
||||
email,
|
||||
friends: [],
|
||||
name,
|
||||
photo: process.env.DEFAULTPROFILEPIC,
|
||||
uid
|
||||
} as UserDoc) //used for my editor to help me
|
||||
|
||||
encodePassword(password)
|
||||
|
||||
//save the password and the user in the db
|
||||
await Promise.all([
|
||||
encodePassword(password).then(result => savePassword(uid, result)),
|
||||
user.save()
|
||||
])
|
||||
|
||||
//save things in session
|
||||
req.session.uid = uid
|
||||
|
||||
//save in the session
|
||||
req.session.save(() => { })
|
||||
|
||||
//send uid back
|
||||
res.json({
|
||||
succes: true,
|
||||
data: {
|
||||
uid
|
||||
}
|
||||
}).status(200)
|
||||
}
|
||||
catch (errors) {
|
||||
//send erros to clinet
|
||||
res.json({ errors })
|
||||
}
|
||||
}
|
||||
|
||||
router.use(...allowWhitelist())
|
||||
router.use("*", urlencoded({ extended: true }), validator(), sessions({
|
||||
secret: process.env.SESSION_SECRET,
|
||||
saveUninitialized: false,
|
||||
resave: false
|
||||
}))
|
||||
const account = async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!req.session.uid)
|
||||
res.json({
|
||||
succes: false,
|
||||
errors: ["uid doesnt exist"]
|
||||
}).status(203)
|
||||
|
||||
router.get("/", sayHello)
|
||||
router.post("/", auth)
|
||||
res.json({
|
||||
succes: true,
|
||||
data: {
|
||||
uid: req.session.uid
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (errors) {
|
||||
//send erros to clinet
|
||||
res.json({ errors })
|
||||
}
|
||||
}
|
||||
|
||||
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(), corsErrorFixer)
|
||||
|
||||
router
|
||||
.get("/", sayHello)
|
||||
.post("/", authHandler)
|
||||
.get("/login", loginHtml)
|
||||
.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())
|
||||
})
|
||||
|
||||
router.use("/*", reddirect("/auth"))
|
||||
|
||||
export { router }
|
||||
export const auth = router
|
1
typescript/multiplayer-backend/src/routes/auth/index.ts
Normal file
1
typescript/multiplayer-backend/src/routes/auth/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./auth"
|
8
typescript/multiplayer-backend/src/routes/index.ts
Normal file
8
typescript/multiplayer-backend/src/routes/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { auth } from "./auth"
|
||||
import { logs } from "./logging"
|
||||
import { Router } from "express";
|
||||
|
||||
export const routes:{[key:string]:Router} = {
|
||||
auth,
|
||||
logs
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./logs"
|
|
@ -1,7 +1,6 @@
|
|||
import { Router, Response, Request } from "express"
|
||||
import { shortLogger } from "./shortLog";
|
||||
import { reddirect } from "../../middleware/reddirect";
|
||||
import { allowWhitelist } from "../../middleware/corsErrorMiddleware";
|
||||
|
||||
const router = Router()
|
||||
|
||||
|
@ -9,8 +8,7 @@ const getShortLogs = (req: Request, res: Response) => {
|
|||
res.json(shortLogger.logs)
|
||||
}
|
||||
|
||||
router.use(...allowWhitelist())
|
||||
router.get("/", getShortLogs)
|
||||
router.use("/*", reddirect("/logs"))
|
||||
|
||||
export { router }
|
||||
export const logs = router
|
45
typescript/multiplayer-backend/src/services/auth/signup.ts
Normal file
45
typescript/multiplayer-backend/src/services/auth/signup.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import * as bcrypt from "bcryptjs"
|
||||
import { promisify } from "util";
|
||||
import { randomBytes } from "crypto";
|
||||
import { database } from "../db/firestore";
|
||||
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()
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import * as admin from "firebase-admin"
|
||||
|
||||
export const firebase = admin.initializeApp({
|
||||
credential: admin.credential.cert('firebase-admin.json'),
|
||||
databaseURL: 'https://planets-io.firebaseio.com'
|
||||
});
|
||||
|
||||
export const database = firebase.firestore()
|
|
@ -0,0 +1 @@
|
|||
export * from "./mongo"
|
|
@ -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 }
|
Loading…
Add table
Add a link
Reference in a new issue