1
0
forked from noxious/server

my 13th reason

This commit is contained in:
Dennis Postma 2024-05-28 21:54:34 +02:00
parent 4157ff5b5c
commit 80ec470fd4
30 changed files with 283 additions and 70 deletions

View File

@ -2,3 +2,4 @@
ENV=development
PORT=4000
DATABASE_URL="mysql://root@localhost:3306/nq"
JWT_SECRET="secret"

119
package-lock.json generated
View File

@ -10,6 +10,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"prisma": "^5.13.0",
"socket.io": "^4.7.5",
"ts-node": "^10.9.2",
@ -18,6 +19,7 @@
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.12.11",
"@types/socket.io": "^3.0.2",
"nodemon": "^3.1.0"
@ -229,6 +231,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz",
"integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -436,6 +448,12 @@
"node": ">=8"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -617,6 +635,15 @@
"url": "https://dotenvx.com"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -1043,6 +1070,97 @@
"node": ">=0.12.0"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsonwebtoken/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@ -1374,7 +1492,6 @@
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"

View File

@ -10,6 +10,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"prisma": "^5.13.0",
"socket.io": "^4.7.5",
"ts-node": "^10.9.2",
@ -18,6 +19,7 @@
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.12.11",
"@types/socket.io": "^3.0.2",
"nodemon": "^3.1.0"

View File

@ -1,11 +1,11 @@
import {User} from "@prisma/client";
interface ILoggedInUsers {
type TLoggedInUsers = {
users: User[];
}
class UserManager {
private loggedInUsers: ILoggedInUsers[] = [];
private loggedInUsers: TLoggedInUsers[] = [];
// Method to initialize user manager
public async boot() {

View File

@ -1,14 +1,14 @@
import {Character, Zone} from "@prisma/client";
import ZoneRepository from "./repositories/zone.repository";
import ZoneService from "./services/zone.service";
import ZoneRepository from "./repositories/ZoneRepository";
import ZoneService from "./services/ZoneService";
interface ILoadedZone {
type TLoadedZone = {
zone: Zone;
characters: Character[];
}
class ZoneManager {
private loadedZones: ILoadedZone[] = [];
private loadedZones: TLoadedZone[] = [];
// Method to initialize zone manager
public async boot() {

View File

@ -0,0 +1,8 @@
import { Socket, Server } from "socket.io";
import {TSocket} from "../types/TSocket";
export default function CharacterConnect(socket: TSocket, io: Server) {
socket.on('character:connect', (data: any) => {
console.log(`---User ${socket.user?.id} has joined.`);
});
}

View File

@ -1,5 +1,5 @@
import { Socket, Server } from "socket.io";
import ZoneRepository from "../repositories/zone.repository";
import ZoneRepository from "../repositories/ZoneRepository";
import ZoneManager from "../ZoneManager";
import {Zone} from "@prisma/client";
@ -14,7 +14,7 @@ interface IZoneLoad {
* @param socket
* @param io
*/
export default function characterZoneLoad(socket: Socket, io: Server) {
export default function CharacterZoneLoad(socket: Socket, io: Server) {
socket.on('character:zone:load', async (data: IZoneLoad) => {
console.log(`---User ${socket.id} has requested zone.`);

View File

@ -0,0 +1,8 @@
import { Socket, Server } from "socket.io";
import {TSocket} from "../types/TSocket";
export default function CharactersGet(socket: TSocket, io: Server) {
socket.on('characters:get', async (data: any) => {
console.log(socket.user);
});
}

View File

@ -1,7 +1,7 @@
import { Socket, Server } from "socket.io";
export default function user_connect(socket: Socket, io: Server) {
socket.on('disconnect', (data) => {
export default function Disconnect(socket: Socket, io: Server) {
socket.on('disconnect', (data: any) => {
console.log(`---User ${socket.id} has disconnected.`);
});
}

7
src/app/events/Login.ts Normal file
View File

@ -0,0 +1,7 @@
import { Socket, Server } from "socket.io";
export default function Login(socket: Socket, io: Server) {
socket.on('login', (data: any) => {
console.log(`---User ${socket.id} has logged in.`);
});
}

View File

@ -1,9 +0,0 @@
import { Socket, Server } from "socket.io";
import {ISocket} from "../interfaces/socket";
export default function characterConnect(socket: ISocket, io: Server) {
socket.on('character:connect', (data) => {
socket.user.username = 'hello'
console.log(`---User ${socket.id} has joined.`);
});
}

View File

@ -1,9 +0,0 @@
import { Socket, Server } from "socket.io";
import {ISocket} from "../interfaces/socket";
export default function characterConnect(socket: ISocket, io: Server) {
socket.on('characters:get', async (data) => {
console.log(socket.user.username)
console.log(`---characters requested.`);
});
}

17
src/app/index.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import "socket.io";
declare module "socket.io" {
interface Socket {
data: {
user: {
id: string;
username: string;
};
};
handshake: {
headers: {
cookie: string;
};
};
}
}

View File

@ -1,5 +0,0 @@
import {Socket} from "socket.io";
export interface ISocket extends Socket {
user?: any;
}

View File

@ -1,6 +0,0 @@
import {Character} from "@prisma/client";
interface zoneCharacters extends Character
{
}

View File

@ -0,0 +1,37 @@
// socket io jwt auth middleware
import { verify } from 'jsonwebtoken';
import { TSocket } from '../types/TSocket';
import config from "../utilities/Config";
import UserRepository from "../repositories/UserRepository";
import {User} from "@prisma/client";
export async function Authentication (socket: TSocket, next: any) {
if (!socket.request.headers.cookie) {
console.log('No cookie provided');
return next(new Error('Authentication error'));
}
const cookies = socket.request.headers.cookie.split('; ').reduce((prev: any, current: any) => {
const [name, value] = current.split('=');
prev[name] = value;
return prev;
}, {});
const token = cookies['token'];
if (token) {
verify(token, config.JWT_SECRET, async (err: any, decoded: any) => {
if (err) {
console.log('err');
return next(new Error('Authentication error'));
}
socket.user = await UserRepository.getById(decoded.id) as User;
next();
});
} else {
console.log('No token provided');
next(new Error('Authentication error'));
}
}

View File

@ -1,6 +1,6 @@
import prisma from '../utilities/prisma'; // Import the global Prisma instance
import prisma from '../utilities/Prisma'; // Import the global Prisma instance
import {Character} from '@prisma/client';
import CharacterService from "../services/character.service";
import CharacterService from "../services/CharacterService";
class CharacterRepository {
async getByUserId(userId: number): Promise<Character[] | null> {

View File

@ -1,7 +1,19 @@
import prisma from '../utilities/prisma'; // Import the global Prisma instance
import prisma from '../utilities/Prisma'; // Import the global Prisma instance
import { User } from '@prisma/client';
class UserRepository {
async getById(id: number): Promise<User | null> {
try {
return await prisma.user.findUnique({
where: {
id,
},
});
} catch (error: any) {
// Handle error
throw new Error(`Failed to get user by ID: ${error.message}`);
}
}
async getByUsername(username: string): Promise<User | null> {
try {
return await prisma.user.findUnique({
@ -14,7 +26,6 @@ class UserRepository {
throw new Error(`Failed to get user by username: ${error.message}`);
}
}
async create(username: string, password: string): Promise<User> {
try {
return await prisma.user.create({

View File

@ -1,5 +1,5 @@
import { Zone } from '@prisma/client';
import prisma from '../utilities/prisma'; // Import the global Prisma instance
import prisma from '../utilities/Prisma'; // Import the global Prisma instance
class ZoneRepository {
async getFirst(): Promise<Zone | null> {

View File

@ -1,6 +1,6 @@
import bcrypt from "bcryptjs";
import UserRepository from "../repositories/user.repository";
import CharacterRepository from "../repositories/character.repository";
import UserRepository from "../repositories/UserRepository";
import CharacterRepository from "../repositories/CharacterRepository";
class UserService {
async login(username: string, password: string): Promise<boolean | any> {

View File

@ -1,5 +1,5 @@
import {Zone} from "@prisma/client";
import ZoneRepository from "../repositories/zone.repository";
import ZoneRepository from "../repositories/ZoneRepository";
class ZoneService
{

View File

@ -1,8 +1,7 @@
import { Socket } from 'socket.io';
import {Character as ICharacter, User} from "@prisma/client";
interface Character extends Socket
{
export type TCharacter = Socket & {
user?: User,
character?: ICharacter
}

16
src/app/types/TSocket.ts Normal file
View File

@ -0,0 +1,16 @@
import {Socket} from "socket.io";
import { User } from '@prisma/client';
export type TSocket = Socket & {
user?: User
handshake?: {
query?: {
token?: any
}
}
request?: {
headers?: {
cookie?: any
}
}
}

View File

@ -0,0 +1,5 @@
import {Character} from "@prisma/client";
export type TZoneCharacter = Character & {
}

View File

@ -5,6 +5,7 @@ dotenv.config();
class config {
static ENV: string = process.env.ENV || "prod";
static PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 5000;
static JWT_SECRET: string = process.env.JWT_SECRET || "secret";
}
export default config;

View File

@ -1,7 +1,15 @@
import { Request, Response } from 'express';
import UserService from '../services/user.service';
/**
* Resources:
* https://stackoverflow.com/questions/76131891/what-is-the-best-method-for-socket-io-authentication
*
*/
async function addAuthRoutes(app: any) {
import {Application, Request, Response} from 'express';
import UserService from '../services/UserService';
import jwt from "jsonwebtoken";
import config from "./Config";
async function addAuthRoutes(app: Application) {
app.post('/login', async (req: Request, res: Response) => {
const { username, password } = req.body;
@ -9,7 +17,8 @@ async function addAuthRoutes(app: any) {
const user = await userService.login(username, password);
if (user) {
return res.status(200).json(user);
const token = jwt.sign({ id: user.id }, config.JWT_SECRET, { expiresIn: '1h' });
return res.status(200).json({ token });
}
return res.status(401).json({ message: 'Invalid credentials' });
});
@ -21,7 +30,8 @@ async function addAuthRoutes(app: any) {
const user = await userService.register(username, password);
if (user) {
return res.status(201).json(user);
const token = jwt.sign({ id: user.id }, config.JWT_SECRET, { expiresIn: '1h' });
return res.status(201).json({ token });
}
return res.status(400).json({ message: 'Failed to register user' });
});
@ -29,4 +39,4 @@ async function addAuthRoutes(app: any) {
console.log('[✅] Auth routes added');
}
export default { addAuthRoutes };
export { addAuthRoutes };

View File

@ -1,19 +1,21 @@
import fs from "fs";
import path from "path";
import express, {Application} from 'express';
import http from 'http';
import {createServer as httpServer} from 'http';
import {addAuthRoutes} from './app/utilities/Http';
import cors from 'cors';
import {Server as SocketServer, Socket} from 'socket.io';
import config from './app/utilities/config';
import prisma from './app/utilities/prisma';
import api from "./app/utilities/api";
import {Server as SocketServer} from 'socket.io';
import {TSocket} from "./app/types/TSocket";
import config from './app/utilities/Config';
import prisma from './app/utilities/Prisma';
import ZoneManager from "./app/ZoneManager";
import UserManager from "./app/UserManager";
import {Authentication} from "./app/middleware/Authentication";
export class Server
{
private readonly app: Application;
private readonly server: any;
private readonly http: any;
private readonly io: SocketServer;
/**
@ -24,8 +26,9 @@ export class Server
this.app.use(cors());
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
this.server = http.createServer(this.app)
this.io = new SocketServer(this.server);
this.http = httpServer(this.app)
this.io = new SocketServer(this.http);
this.io.use(Authentication)
}
/**
@ -46,14 +49,14 @@ export class Server
// Start the server
try {
await this.server.listen(config.PORT);
await this.http.listen(config.PORT);
console.log('[✅] Socket.IO running on port', config.PORT);
} catch (error: any) {
throw new Error(`[❌] Socket.IO failed to start: ${error.message}`);
}
// Add API routes
await api.addAuthRoutes(this.app);
// Add http API routes
await addAuthRoutes(this.app);
// Load user manager
await UserManager.boot();
@ -70,7 +73,7 @@ export class Server
* @param socket
* @private
*/
private async handleConnection(socket: Socket) {
private async handleConnection(socket: TSocket) {
const eventsPath = path.join(__dirname, 'app', 'events');
try {
const files: string[] = await fs.promises.readdir(eventsPath);

View File

@ -31,7 +31,7 @@
// "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'. */
"typeRoots": ["./node_modules/@types", "./src/app"], /* 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. */