Converted more procedural programming to OOP

This commit is contained in:
Dennis Postma 2024-12-26 23:34:25 +01:00
parent b7f448cb17
commit e571cf2230
46 changed files with 449 additions and 382 deletions

View File

@ -0,0 +1,18 @@
import { Request, Response } from 'express'
export abstract class BaseController {
protected sendSuccess(res: Response, data?: any, message?: string, status: number = 200) {
return res.status(status).json({
success: true,
message,
data
})
}
protected sendError(res: Response, message: string, status: number = 400) {
return res.status(status).json({
success: false,
message
})
}
}

View File

@ -1,5 +1,5 @@
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { User } from './user' import { User } from './user'
import { Zone } from './zone' import { Zone } from './zone'
import { CharacterType } from './characterType' import { CharacterType } from './characterType'

View File

@ -1,5 +1,5 @@
import { Entity, Enum, ManyToOne, PrimaryKey } from '@mikro-orm/core' import { Entity, Enum, ManyToOne, PrimaryKey } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Character } from './character' import { Character } from './character'
import { CharacterItem } from './characterItem' import { CharacterItem } from './characterItem'
import { CharacterEquipmentSlotType } from '#application/enums' import { CharacterEquipmentSlotType } from '#application/enums'

View File

@ -1,5 +1,5 @@
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Character } from './character' import { Character } from './character'
import { Sprite } from './sprite' import { Sprite } from './sprite'
import { CharacterGender } from '#application/enums' import { CharacterGender } from '#application/enums'

View File

@ -1,5 +1,5 @@
import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import { Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Character } from './character' import { Character } from './character'
import { Item } from './item' import { Item } from './item'
import { CharacterEquipment } from './characterEquipment' import { CharacterEquipment } from './characterEquipment'

View File

@ -1,5 +1,5 @@
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Character } from './character' import { Character } from './character'
import { Sprite } from './sprite' import { Sprite } from './sprite'
import { CharacterGender, CharacterRace } from '#application/enums' import { CharacterGender, CharacterRace } from '#application/enums'

View File

@ -1,5 +1,5 @@
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Character } from './character' import { Character } from './character'
import { Zone } from './zone' import { Zone } from './zone'

View File

@ -1,5 +1,5 @@
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Sprite } from './sprite' import { Sprite } from './sprite'
import { CharacterItem } from './characterItem' import { CharacterItem } from './characterItem'
import { ItemType, ItemRarity } from '#application/enums' import { ItemType, ItemRarity } from '#application/enums'

View File

@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { ZoneObject } from './zoneObject' import { ZoneObject } from './zoneObject'
import { UUID } from '#application/types' import { UUID } from '#application/types'

View File

@ -1,5 +1,5 @@
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { User } from './user' import { User } from './user'
@Entity() @Entity()

View File

@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { SpriteAction } from './spriteAction' import { SpriteAction } from './spriteAction'
import { UUID } from '#application/types' import { UUID } from '#application/types'

View File

@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Sprite } from './sprite' import { Sprite } from './sprite'
import { UUID } from '#application/types' import { UUID } from '#application/types'

View File

@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Entity, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { UUID } from '#application/types' import { UUID } from '#application/types'
@Entity() @Entity()

View File

@ -1,5 +1,5 @@
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Character } from './character' import { Character } from './character'
import { PasswordResetToken } from './passwordResetToken' import { PasswordResetToken } from './passwordResetToken'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'

View File

@ -1,5 +1,5 @@
import { Entity, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
@Entity() @Entity()
export class World extends BaseEntity { export class World extends BaseEntity {

View File

@ -1,5 +1,5 @@
import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core' import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { ZoneEffect } from './zoneEffect' import { ZoneEffect } from './zoneEffect'
import { ZoneEventTile } from './zoneEventTile' import { ZoneEventTile } from './zoneEventTile'
import { ZoneEventTileTeleport } from './zoneEventTileTeleport' import { ZoneEventTileTeleport } from './zoneEventTileTeleport'

View File

@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Zone } from './zone' import { Zone } from './zone'
import { UUID } from '#application/types' import { UUID } from '#application/types'

View File

@ -1,5 +1,5 @@
import { Entity, Enum, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, Enum, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Zone } from './zone' import { Zone } from './zone'
import { ZoneEventTileType } from '#application/enums' import { ZoneEventTileType } from '#application/enums'
import { ZoneEventTileTeleport } from './zoneEventTileTeleport' import { ZoneEventTileTeleport } from './zoneEventTileTeleport'

View File

@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Entity, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Zone } from './zone' import { Zone } from './zone'
import { ZoneEventTile } from './zoneEventTile' import { ZoneEventTile } from './zoneEventTile'
import { UUID } from '#application/types' import { UUID } from '#application/types'

View File

@ -1,5 +1,5 @@
import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core' import { Entity, ManyToOne, PrimaryKey, Property } from '@mikro-orm/core'
import { BaseEntity } from '#application/bases/baseEntity' import { BaseEntity } from '#application/base/baseEntity'
import { Zone } from './zone' import { Zone } from './zone'
import { MapObject } from '#entities/mapObject' import { MapObject } from '#entities/mapObject'
import { UUID } from '#application/types' import { UUID } from '#application/types'

View File

@ -1,114 +0,0 @@
import { Router, Request, Response } from 'express'
import fs from 'fs'
import { httpLogger } from '#application/logger'
import { getPublicPath } from '#application/storage'
import TileRepository from '#repositories/tileRepository'
import ZoneRepository from '#repositories/zoneRepository'
import SpriteRepository from '#repositories/spriteRepository'
import { AssetData } from '#application/types'
import { FilterValue } from '@mikro-orm/core'
const router = Router()
// Get all tiles
router.get('/assets/list_tiles', async (req: Request, res: Response) => {
let assets: AssetData[] = []
const tiles = await TileRepository.getAll()
for (const tile of tiles) {
assets.push({
key: tile.id,
data: '/assets/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as AssetData)
}
res.json(assets)
})
// Get tiles by zone
router.get('/assets/list_tiles/:zoneId', async (req: Request, res: Response) => {
const zoneId = req.params.zoneId
if (!zoneId || parseInt(zoneId) === 0) {
return res.status(400).json({ message: 'Invalid zone ID' })
}
const zone = await ZoneRepository.getById(parseInt(zoneId))
if (!zone) {
return res.status(404).json({ message: 'Zone not found' })
}
let assets: AssetData[] = []
const tiles = await TileRepository.getByZoneId(parseInt(zoneId))
for (const tile of tiles) {
assets.push({
key: tile.id,
data: '/assets/tiles/' + tile.id + '.png',
group: 'tiles',
updatedAt: tile.updatedAt
} as AssetData)
}
res.json(assets)
})
// Get sprite actions
router.get('/assets/list_sprite_actions/:spriteId', async (req: Request, res: Response) => {
const spriteId = req.params.spriteId as FilterValue<`${string}-${string}-${string}-${string}-${string}`>
if (!spriteId) {
return res.status(400).json({ message: 'Invalid sprite ID' })
}
const sprite = await SpriteRepository.getById(spriteId)
if (!sprite) {
return res.status(404).json({ message: 'Sprite not found' })
}
let assets: AssetData[] = []
sprite.spriteActions.getItems().forEach((spriteAction) => {
assets.push({
key: sprite.id + '-' + spriteAction.action,
data: '/assets/sprites/' + sprite.id + '/' + spriteAction.action + '.png',
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites',
updatedAt: sprite.updatedAt,
originX: Number(spriteAction.originX.toString()),
originY: Number(spriteAction.originY.toString()),
isAnimated: spriteAction.isAnimated,
frameCount: JSON.parse(JSON.stringify(spriteAction.sprites)).length,
frameWidth: spriteAction.frameWidth,
frameHeight: spriteAction.frameHeight,
frameRate: spriteAction.frameRate
})
})
res.json(assets)
})
// Download asset file
router.get('/assets/:type/:spriteId?/:file', (req: Request, res: Response) => {
const assetType = req.params.type
const spriteId = req.params.spriteId
const fileName = req.params.file
let assetPath
if (assetType === 'sprites' && spriteId) {
assetPath = getPublicPath(assetType, spriteId, fileName)
} else {
assetPath = getPublicPath(assetType, fileName)
}
if (!fs.existsSync(assetPath)) {
httpLogger.error(`File not found: ${assetPath}`)
return res.status(404).send('Asset not found')
}
res.sendFile(assetPath, (err) => {
if (err) {
httpLogger.error('Error sending file:', err)
res.status(500).send('Error downloading the asset')
}
})
})
export default router

View File

@ -1,90 +0,0 @@
import { Router, Request, Response } from 'express'
import UserService from '#services/userService'
import jwt from 'jsonwebtoken'
import config from '#application/config'
import { loginAccountSchema, registerAccountSchema, resetPasswordSchema, newPasswordSchema } from '#application/zodTypes'
const router = Router()
// Login endpoint
router.post('/login', async (req: Request, res: Response) => {
const { username, password } = req.body
try {
loginAccountSchema.parse({ username, password })
} catch (error: any) {
return res.status(400).json({ message: error.errors[0]?.message })
}
const userService = new UserService()
const user = await userService.login(username, password)
if (user && typeof user !== 'boolean') {
const token = jwt.sign({ id: user.getId() }, config.JWT_SECRET, { expiresIn: '4h' })
return res.status(200).json({ token })
}
return res.status(400).json({ message: 'Failed to login' })
})
// Register endpoint
router.post('/register', async (req: Request, res: Response) => {
const { username, email, password } = req.body
try {
registerAccountSchema.parse({ username, email, password })
} catch (error: any) {
return res.status(400).json({ message: error.errors[0]?.message })
}
const userService = new UserService()
const user = await userService.register(username, email, password)
if (user) {
return res.status(200).json({ message: 'User registered' })
}
return res.status(400).json({ message: 'Failed to register user' })
})
// Reset password endpoint
router.post('/reset-password', async (req: Request, res: Response) => {
const { email } = req.body
try {
resetPasswordSchema.parse({ email })
} catch (error: any) {
return res.status(400).json({ message: error.errors[0]?.message })
}
const userService = new UserService()
const sentEmail = await userService.requestPasswordReset(email)
if (sentEmail) {
return res.status(200).json({ message: 'Email has been sent' })
}
return res.status(400).json({ message: 'Failed to send password reset request. Perhaps one has already been sent recently, check your spam folder.' })
})
// New password endpoint
router.post('/new-password', async (req: Request, res: Response) => {
const { urlToken, password } = req.body
try {
newPasswordSchema.parse({ urlToken, password })
} catch (error: any) {
return res.status(400).json({ message: error.errors[0]?.message })
}
const userService = new UserService()
const resetPassword = await userService.resetPassword(urlToken, password)
if (resetPassword) {
return res.status(200).json({ message: 'Password has been reset' })
}
return res.status(400).json({ message: 'Failed to set new password' })
})
export default router

View File

@ -1,85 +0,0 @@
/**
* Avatar generator API routes
*/
import { Router, Request, Response } from 'express'
import sharp from 'sharp'
import fs from 'fs'
import CharacterRepository from '#repositories/characterRepository'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
import { getPublicPath } from '#application/storage'
const router = Router()
interface AvatarOptions {
characterTypeId: number
characterHairId?: number
}
async function generateAvatar(res: Response, options: AvatarOptions) {
try {
const characterType = await CharacterTypeRepository.getById(options.characterTypeId)
if (!characterType?.sprite?.id) {
return res.status(404).json({ message: 'Character type not found' })
}
const bodySpritePath = getPublicPath('sprites', characterType.sprite.id, 'idle_right_down.png')
if (!fs.existsSync(bodySpritePath)) {
console.error(`Body sprite file not found: ${bodySpritePath}`)
return res.status(404).json({ message: 'Body sprite file not found' })
}
let avatar = sharp(bodySpritePath).extend({
top: 2,
bottom: 2,
background: { r: 0, g: 0, b: 0, alpha: 0 }
})
if (options.characterHairId) {
const characterHair = await CharacterHairRepository.getById(options.characterHairId)
if (characterHair?.sprite?.id) {
const hairSpritePath = getPublicPath('sprites', characterHair.sprite.id, 'front.png')
if (fs.existsSync(hairSpritePath)) {
avatar = avatar.composite([
{
input: hairSpritePath,
gravity: 'north'
// Top is originY in min @TODO finish me #287
// top: Math.round(Number(characterHair.sprite!.spriteActions.find((action) => action.action === 'front')?.originY ?? 0)),
// left: 0
}
])
} else {
console.error(`Hair sprite file not found: ${hairSpritePath}`)
}
}
}
res.setHeader('Content-Type', 'image/png')
return avatar.pipe(res)
} catch (error) {
console.error('Error generating avatar:', error)
return res.status(500).json({ message: 'Error generating avatar' })
}
}
router.get('/avatar/:characterName', async (req: Request, res: Response) => {
const character = await CharacterRepository.getByName(req.params.characterName)
if (!character?.characterType) {
return res.status(404).json({ message: 'Character or character type not found' })
}
return generateAvatar(res, {
characterTypeId: character.characterType.id,
characterHairId: character.characterHair?.id
})
})
router.get('/avatar/s/:characterTypeId/:characterHairId?', async (req: Request, res: Response) => {
return generateAvatar(res, {
characterTypeId: parseInt(req.params.characterTypeId),
characterHairId: req.params.characterHairId ? parseInt(req.params.characterHairId) : undefined
})
})
export default router

View File

@ -0,0 +1,122 @@
import { Request, Response } from 'express'
import fs from 'fs'
import { BaseController } from '#application/base/baseController'
import { httpLogger } from '#application/logger'
import { getPublicPath } from '#application/storage'
import TileRepository from '#repositories/tileRepository'
import ZoneRepository from '#repositories/zoneRepository'
import SpriteRepository from '#repositories/spriteRepository'
import { AssetData } from '#application/types'
import { FilterValue } from '@mikro-orm/core'
export class AssetsController extends BaseController {
/**
* List tiles
* @param req
* @param res
*/
public async listTiles(req: Request, res: Response) {
const assets: AssetData[] = []
const tiles = await TileRepository.getAll()
for (const tile of tiles) {
assets.push({
key: tile.id,
data: '/assets/tiles/' + tile.getId() + '.png',
group: 'tiles',
updatedAt: tile.getUpdatedAt()
} as AssetData)
}
return this.sendSuccess(res, assets)
}
/**
* List tiles by zone
* @param req
* @param res
*/
public async listTilesByZone(req: Request, res: Response) {
const zoneId = parseInt(req.params.zoneId)
if (!zoneId || zoneId === 0) {
return this.sendError(res, 'Invalid zone ID', 400)
}
const zone = await ZoneRepository.getById(zoneId)
if (!zone) {
return this.sendError(res, 'Zone not found', 404)
}
const assets: AssetData[] = []
const tiles = await TileRepository.getByZoneId(zoneId)
for (const tile of tiles) {
assets.push({
key: tile.getId(),
data: '/assets/tiles/' + tile.getId() + '.png',
group: 'tiles',
updatedAt: tile.getUpdatedAt()
} as AssetData)
}
return this.sendSuccess(res, assets)
}
/**
* List sprite actions
* @param req
* @param res
*/
public async listSpriteActions(req: Request, res: Response) {
const spriteId = req.params.spriteId as FilterValue<`${string}-${string}-${string}-${string}-${string}`>
if (!spriteId) {
return this.sendError(res, 'Invalid sprite ID', 400)
}
const sprite = await SpriteRepository.getById(spriteId)
if (!sprite) {
return this.sendError(res, 'Sprite not found', 404)
}
const assets: AssetData[] = sprite.spriteActions.getItems().map((spriteAction) => ({
key: sprite.id + '-' + spriteAction.action,
data: '/assets/sprites/' + sprite.id + '/' + spriteAction.action + '.png',
group: spriteAction.isAnimated ? 'sprite_animations' : 'sprites',
updatedAt: sprite.updatedAt,
originX: Number(spriteAction.originX.toString()),
originY: Number(spriteAction.originY.toString()),
isAnimated: spriteAction.isAnimated,
frameCount: JSON.parse(JSON.stringify(spriteAction.sprites)).length,
frameWidth: spriteAction.frameWidth,
frameHeight: spriteAction.frameHeight,
frameRate: spriteAction.frameRate
}))
return this.sendSuccess(res, assets)
}
/**
* Download asset
* @param req
* @param res
*/
public async downloadAsset(req: Request, res: Response) {
const { type, spriteId, file } = req.params
const assetPath = type === 'sprites' && spriteId ? getPublicPath(type, spriteId, file) : getPublicPath(type, file)
if (!fs.existsSync(assetPath)) {
httpLogger.error(`File not found: ${assetPath}`)
return this.sendError(res, 'Asset not found', 404)
}
res.sendFile(assetPath, (err) => {
if (err) {
httpLogger.error('Error sending file:', err)
this.sendError(res, 'Error downloading the asset', 500)
}
})
}
}

View File

@ -0,0 +1,104 @@
import jwt from 'jsonwebtoken'
import { Request, Response } from 'express'
import { BaseController } from '#application/base/baseController'
import UserService from '#services/userService'
import config from '#application/config'
import { loginAccountSchema, registerAccountSchema, resetPasswordSchema, newPasswordSchema } from '#application/zodTypes'
export class AuthController extends BaseController {
private userService: UserService
constructor() {
super()
this.userService = new UserService()
}
/**
* Login user
* @param req
* @param res
*/
public async login(req: Request, res: Response) {
const { username, password } = req.body
try {
loginAccountSchema.parse({ username, password })
const user = await this.userService.login(username, password)
if (user && typeof user !== 'boolean') {
const token = jwt.sign({ id: user.getId() }, config.JWT_SECRET, { expiresIn: '4h' })
return this.sendSuccess(res, { token })
}
return this.sendError(res, 'Invalid credentials')
} catch (error: any) {
return this.sendError(res, error.errors?.[0]?.message || 'Validation error')
}
}
/**
* Register user
* @param req
* @param res
*/
public async register(req: Request, res: Response) {
const { username, email, password } = req.body
try {
registerAccountSchema.parse({ username, email, password })
const user = await this.userService.register(username, email, password)
if (user) {
return this.sendSuccess(res, null, 'User registered successfully')
}
return this.sendError(res, 'Failed to register user')
} catch (error: any) {
return this.sendError(res, error.errors?.[0]?.message || 'Validation error')
}
}
/**
* Request password reset
* @param req
* @param res
*/
public async requestPasswordReset(req: Request, res: Response) {
const { email } = req.body
try {
resetPasswordSchema.parse({ email })
const sentEmail = await this.userService.requestPasswordReset(email)
if (sentEmail) {
return this.sendSuccess(res, null, 'Password reset email sent')
}
return this.sendError(res, 'Failed to send password reset request')
} catch (error: any) {
return this.sendError(res, error.errors?.[0]?.message || 'Validation error')
}
}
/**
* Reset password
* @param req
* @param res
*/
public async resetPassword(req: Request, res: Response) {
const { urlToken, password } = req.body
try {
newPasswordSchema.parse({ urlToken, password })
const resetPassword = await this.userService.resetPassword(urlToken, password)
if (resetPassword) {
return this.sendSuccess(res, null, 'Password has been reset')
}
return this.sendError(res, 'Failed to reset password')
} catch (error: any) {
return this.sendError(res, error.errors?.[0]?.message || 'Validation error')
}
}
}

View File

@ -0,0 +1,85 @@
import { Request, Response } from 'express'
import sharp from 'sharp'
import fs from 'fs'
import { BaseController } from '#application/base/baseController'
import CharacterRepository from '#repositories/characterRepository'
import CharacterHairRepository from '#repositories/characterHairRepository'
import CharacterTypeRepository from '#repositories/characterTypeRepository'
import { getPublicPath } from '#application/storage'
interface AvatarOptions {
characterTypeId: number
characterHairId?: number
}
export class AvatarController extends BaseController {
/**
* Get avatar by character
* @param req
* @param res
*/
public async getByName(req: Request, res: Response) {
const character = await CharacterRepository.getByName(req.params.characterName)
if (!character?.characterType) {
return this.sendError(res, 'Character or character type not found', 404)
}
return this.generateAvatar(res, {
characterTypeId: character.characterType.id,
characterHairId: character.characterHair?.id
})
}
/**
* Get avatar by character type and hair
* @param req
* @param res
*/
public async getByParams(req: Request, res: Response) {
return this.generateAvatar(res, {
characterTypeId: parseInt(req.params.characterTypeId),
characterHairId: req.params.characterHairId ? parseInt(req.params.characterHairId) : undefined
})
}
/**
* Generate avatar
* @param res
* @param options
* @private
*/
private async generateAvatar(res: Response, options: AvatarOptions) {
try {
const characterType = await CharacterTypeRepository.getById(options.characterTypeId)
if (!characterType?.sprite?.id) {
return this.sendError(res, 'Character type not found', 404)
}
const bodySpritePath = getPublicPath('sprites', characterType.sprite.id, 'idle_right_down.png')
if (!fs.existsSync(bodySpritePath)) {
return this.sendError(res, 'Body sprite file not found', 404)
}
let avatar = sharp(bodySpritePath).extend({
top: 2,
bottom: 2,
background: { r: 0, g: 0, b: 0, alpha: 0 }
})
if (options.characterHairId) {
const characterHair = await CharacterHairRepository.getById(options.characterHairId)
if (characterHair?.sprite?.id) {
const hairSpritePath = getPublicPath('sprites', characterHair.sprite.id, 'front.png')
if (fs.existsSync(hairSpritePath)) {
avatar = avatar.composite([{ input: hairSpritePath, gravity: 'north' }])
}
}
}
res.setHeader('Content-Type', 'image/png')
return avatar.pipe(res)
} catch (error) {
return this.sendError(res, 'Error generating avatar', 500)
}
}
}

View File

@ -1,22 +0,0 @@
import { Application } from 'express'
import { httpLogger } from '#application/logger'
import fs from 'fs'
import path from 'path'
import { getAppPath } from '#application/storage'
async function addHttpRoutes(app: Application) {
const routeFiles = fs.readdirSync(__dirname).filter((file) => {
return file !== 'index.ts' && file !== 'index.js' && (file.endsWith('.ts') || file.endsWith('.js'))
})
for (const file of routeFiles) {
const route = await import(getAppPath('http', file))
// Use the router directly without additional path prefix
app.use('/', route.default)
httpLogger.info(`Loaded routes from ${file}`)
}
httpLogger.info('Web routes added')
}
export { addHttpRoutes }

36
src/http/router.ts Normal file
View File

@ -0,0 +1,36 @@
import { Application } from 'express'
import { AuthController } from './controllers/auth'
import { AvatarController } from './controllers/avatar'
import { AssetsController } from './controllers/assets'
export class HttpRouter {
private readonly app: Application
private readonly authController: AuthController
private readonly avatarController: AvatarController
private readonly assetsController: AssetsController
constructor(app: Application) {
this.app = app
this.authController = new AuthController()
this.avatarController = new AvatarController()
this.assetsController = new AssetsController()
}
public async boot() {
// Auth routes
this.app.post('/login', (req, res) => this.authController.login(req, res))
this.app.post('/register', (req, res) => this.authController.register(req, res))
this.app.post('/reset-password-request', (req, res) => this.authController.requestPasswordReset(req, res))
this.app.post('/reset-password', (req, res) => this.authController.resetPassword(req, res))
// Avatar routes
this.app.get('/avatar/:characterName', (req, res) => this.avatarController.getByName(req, res))
this.app.get('/avatar/s/:characterTypeId/:characterHairId?', (req, res) => this.avatarController.getByParams(req, res))
// Assets routes
this.app.get('/assets/list_tiles', (req, res) => this.assetsController.listTiles(req, res))
this.app.get('/assets/list_tiles/:zoneId', (req, res) => this.assetsController.listTilesByZone(req, res))
this.app.get('/assets/list_sprite_actions/:spriteId', (req, res) => this.assetsController.listSpriteActions(req, res))
this.app.get('/assets/:type/:spriteId?/:file', (req, res) => this.assetsController.downloadAsset(req, res))
}
}

View File

@ -1,47 +1,55 @@
import { verify } from 'jsonwebtoken' import { verify } from 'jsonwebtoken'
import { TSocket } from '#application/types' import { TSocket } from '#application/types'
import config from '#application/config' import config from '#application/config'
import UserRepository from '#repositories/userRepository'
import { User } from '@prisma/client'
import { gameLogger } from '#application/logger' import { gameLogger } from '#application/logger'
/** export class SocketAuthenticator {
* Socket io jwt auth middleware private socket: TSocket
* @param socket private readonly next: any
* @param next
*/ constructor(socket: TSocket, next: any) {
export async function Authentication(socket: TSocket, next: any) { this.socket = socket
if (!socket.request.headers.cookie) { this.next = next
gameLogger.warn('No cookie provided')
return next(new Error('Authentication error'))
} }
/** public async authenticate(): Promise<void> {
* Parse cookies if (!this.socket.request.headers.cookie) {
*/ gameLogger.warn('No cookie provided')
const cookies = socket.request.headers.cookie.split('; ').reduce((prev: any, current: any) => { return this.next(new Error('Authentication error'))
const [name, value] = current.split('=') }
prev[name] = value
return prev
}, {})
const token = cookies['token'] const token = this.parseCookies()['token']
/** if (!token) {
* Verify token, if valid, set user on socket and continue gameLogger.warn('No token provided')
*/ return this.next(new Error('Authentication error'))
if (token) { }
verify(token, config.JWT_SECRET, async (err: any, decoded: any) => {
this.verifyToken(token)
}
private parseCookies(): Record<string, string> {
return this.socket.request.headers.cookie.split('; ').reduce((prev: any, current: any) => {
const [name, value] = current.split('=')
prev[name] = value
return prev
}, {})
}
private verifyToken(token: string): void {
verify(token, config.JWT_SECRET, (err: any, decoded: any) => {
if (err) { if (err) {
gameLogger.error('Invalid token') gameLogger.error('Invalid token')
return next(new Error('Authentication error')) return this.next(new Error('Authentication error'))
} }
socket.userId = decoded.id this.socket.userId = decoded.id
next() this.next()
}) })
} else {
gameLogger.warn('No token provided')
return next(new Error('Authentication error'))
} }
} }
export async function Authentication(socket: TSocket, next: any) {
const authenticator = new SocketAuthenticator(socket, next)
await authenticator.authenticate()
}

View File

@ -1,5 +1,5 @@
import { appLogger } from '#application/logger' import { appLogger } from '#application/logger'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { CharacterHair } from '#entities/characterHair' import { CharacterHair } from '#entities/characterHair'
class CharacterHairRepository extends BaseRepository { class CharacterHairRepository extends BaseRepository {

View File

@ -1,5 +1,5 @@
import { appLogger } from '#application/logger' import { appLogger } from '#application/logger'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { Character } from '#entities/character' import { Character } from '#entities/character'
class CharacterRepository extends BaseRepository { class CharacterRepository extends BaseRepository {

View File

@ -1,5 +1,5 @@
import { appLogger } from '#application/logger' import { appLogger } from '#application/logger'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { CharacterType } from '#entities/characterType' import { CharacterType } from '#entities/characterType'
class CharacterTypeRepository extends BaseRepository { class CharacterTypeRepository extends BaseRepository {

View File

@ -1,5 +1,5 @@
import { appLogger } from '#application/logger' import { appLogger } from '#application/logger'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { Chat } from '#entities/chat' import { Chat } from '#entities/chat'
class ChatRepository extends BaseRepository { class ChatRepository extends BaseRepository {

View File

@ -1,5 +1,5 @@
import { appLogger } from '#application/logger' import { appLogger } from '#application/logger'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { Item } from '#entities/item' import { Item } from '#entities/item'
class ItemRepository extends BaseRepository { class ItemRepository extends BaseRepository {

View File

@ -1,4 +1,4 @@
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
class ObjectRepository extends BaseRepository { class ObjectRepository extends BaseRepository {
async getById(id: string): Promise<any> { async getById(id: string): Promise<any> {

View File

@ -1,5 +1,5 @@
import { appLogger } from '#application/logger' import { appLogger } from '#application/logger'
import { BaseRepository } from '#application/bases/baseRepository' // Import the global Prisma instance import { BaseRepository } from '#application/base/baseRepository' // Import the global Prisma instance
import { PasswordResetToken } from '#entities/passwordResetToken' import { PasswordResetToken } from '#entities/passwordResetToken'
class PasswordResetTokenRepository extends BaseRepository { class PasswordResetTokenRepository extends BaseRepository {

View File

@ -1,5 +1,5 @@
import { FilterValue } from '@mikro-orm/core' import { FilterValue } from '@mikro-orm/core'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { Sprite } from '#entities/sprite' import { Sprite } from '#entities/sprite'
class SpriteRepository extends BaseRepository { class SpriteRepository extends BaseRepository {

View File

@ -1,5 +1,5 @@
import { FilterValue } from '@mikro-orm/core' import { FilterValue } from '@mikro-orm/core'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { Tile } from '#entities/tile' import { Tile } from '#entities/tile'
import { Zone } from '#entities/zone' import { Zone } from '#entities/zone'
import { unduplicateArray } from '#application/utilities' import { unduplicateArray } from '#application/utilities'

View File

@ -1,5 +1,5 @@
import { appLogger } from '#application/logger' import { appLogger } from '#application/logger'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { User } from '#entities/user' import { User } from '#entities/user'
class UserRepository extends BaseRepository { class UserRepository extends BaseRepository {

View File

@ -1,5 +1,5 @@
import { gameLogger } from '#application/logger' import { gameLogger } from '#application/logger'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { World } from '#entities/world' import { World } from '#entities/world'
class WorldRepository extends BaseRepository { class WorldRepository extends BaseRepository {

View File

@ -1,5 +1,5 @@
import { appLogger } from '#application/logger' import { appLogger } from '#application/logger'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { ZoneEventTile } from '#entities/zoneEventTile' import { ZoneEventTile } from '#entities/zoneEventTile'
class ZoneEventTileRepository extends BaseRepository { class ZoneEventTileRepository extends BaseRepository {

View File

@ -1,5 +1,5 @@
import { appLogger } from '#application/logger' import { appLogger } from '#application/logger'
import { BaseRepository } from '#application/bases/baseRepository' import { BaseRepository } from '#application/base/baseRepository'
import { ZoneEventTile } from '#entities/zoneEventTile' import { ZoneEventTile } from '#entities/zoneEventTile'
import { ZoneObject } from '#entities/zoneObject' import { ZoneObject } from '#entities/zoneObject'
import { Zone } from '#entities/zone' import { Zone } from '#entities/zone'

View File

@ -3,7 +3,6 @@ import express, { Application } from 'express'
import config from '#application/config' import config from '#application/config'
import { getAppPath } from '#application/storage' import { getAppPath } from '#application/storage'
import { createServer as httpServer, Server as HTTPServer } from 'http' import { createServer as httpServer, Server as HTTPServer } from 'http'
import { addHttpRoutes } from './http'
import cors from 'cors' import cors from 'cors'
import { Server as SocketServer } from 'socket.io' import { Server as SocketServer } from 'socket.io'
import { Authentication } from '#middleware/authentication' import { Authentication } from '#middleware/authentication'
@ -16,6 +15,7 @@ import CommandManager from '#managers/commandManager'
import QueueManager from '#managers/queueManager' import QueueManager from '#managers/queueManager'
import DateManager from '#managers/dateManager' import DateManager from '#managers/dateManager'
import WeatherManager from '#managers/weatherManager' import WeatherManager from '#managers/weatherManager'
import { HttpRouter } from '#http/router'
export class Server { export class Server {
private readonly app: Application private readonly app: Application
@ -64,8 +64,9 @@ export class Server {
appLogger.error(`Socket.IO failed to start: ${error.message}`) appLogger.error(`Socket.IO failed to start: ${error.message}`)
} }
// Add http API routes // Load HTTP routes
await addHttpRoutes(this.app) const httpRouter = new HttpRouter(this.app)
await httpRouter.boot()
// Load queue manager // Load queue manager
await QueueManager.boot(this.io) await QueueManager.boot(this.io)
@ -77,7 +78,7 @@ export class Server {
// await DateManager.boot(this.io) // await DateManager.boot(this.io)
// Load weather manager // Load weather manager
await WeatherManager.boot(this.io) // await WeatherManager.boot(this.io)
// Load zoneEditor manager // Load zoneEditor manager
await ZoneManager.boot() await ZoneManager.boot()

View File

@ -65,15 +65,19 @@ export default class CharacterMove {
break break
} }
this.characterService.updatePosition(character, end) // Update position first
// Send minimal data character.positionX = end.x
character.positionY = end.y
// Then emit with the same properties
this.io.in(character.zone!.id.toString()).emit('character:move', { this.io.in(character.zone!.id.toString()).emit('character:move', {
id: character.id, id: character.id,
positionX: end.x, positionX: character.positionX,
positionY: end.y, positionY: character.positionY,
rotation: character.rotation, rotation: character.rotation,
isMoving: true isMoving: true
}) })
await this.characterService.applyMovementDelay() await this.characterService.applyMovementDelay()
} }