forked from noxious/server
Converted more procedural programming to OOP
This commit is contained in:
parent
b7f448cb17
commit
e571cf2230
18
src/application/base/baseController.ts
Normal file
18
src/application/base/baseController.ts
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
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 { Zone } from './zone'
|
||||
import { CharacterType } from './characterType'
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { CharacterItem } from './characterItem'
|
||||
import { CharacterEquipmentSlotType } from '#application/enums'
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { Sprite } from './sprite'
|
||||
import { CharacterGender } from '#application/enums'
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { Item } from './item'
|
||||
import { CharacterEquipment } from './characterEquipment'
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { Sprite } from './sprite'
|
||||
import { CharacterGender, CharacterRace } from '#application/enums'
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { Zone } from './zone'
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { CharacterItem } from './characterItem'
|
||||
import { ItemType, ItemRarity } from '#application/enums'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
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 { UUID } from '#application/types'
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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'
|
||||
|
||||
@Entity()
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
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 { UUID } from '#application/types'
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
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 { UUID } from '#application/types'
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
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'
|
||||
|
||||
@Entity()
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { PasswordResetToken } from './passwordResetToken'
|
||||
import bcrypt from 'bcryptjs'
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
|
||||
import { BaseEntity } from '#application/bases/baseEntity'
|
||||
import { BaseEntity } from '#application/base/baseEntity'
|
||||
|
||||
@Entity()
|
||||
export class World extends BaseEntity {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { ZoneEventTile } from './zoneEventTile'
|
||||
import { ZoneEventTileTeleport } from './zoneEventTileTeleport'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
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 { UUID } from '#application/types'
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { ZoneEventTileType } from '#application/enums'
|
||||
import { ZoneEventTileTeleport } from './zoneEventTileTeleport'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
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 { ZoneEventTile } from './zoneEventTile'
|
||||
import { UUID } from '#application/types'
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { MapObject } from '#entities/mapObject'
|
||||
import { UUID } from '#application/types'
|
||||
|
@ -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
|
@ -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
|
@ -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
|
122
src/http/controllers/assets.ts
Normal file
122
src/http/controllers/assets.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
104
src/http/controllers/auth.ts
Normal file
104
src/http/controllers/auth.ts
Normal 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')
|
||||
}
|
||||
}
|
||||
}
|
85
src/http/controllers/avatar.ts
Normal file
85
src/http/controllers/avatar.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
36
src/http/router.ts
Normal 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))
|
||||
}
|
||||
}
|
@ -1,47 +1,55 @@
|
||||
import { verify } from 'jsonwebtoken'
|
||||
import { TSocket } from '#application/types'
|
||||
import config from '#application/config'
|
||||
import UserRepository from '#repositories/userRepository'
|
||||
import { User } from '@prisma/client'
|
||||
import { gameLogger } from '#application/logger'
|
||||
|
||||
/**
|
||||
* Socket io jwt auth middleware
|
||||
* @param socket
|
||||
* @param next
|
||||
*/
|
||||
export async function Authentication(socket: TSocket, next: any) {
|
||||
if (!socket.request.headers.cookie) {
|
||||
gameLogger.warn('No cookie provided')
|
||||
return next(new Error('Authentication error'))
|
||||
export class SocketAuthenticator {
|
||||
private socket: TSocket
|
||||
private readonly next: any
|
||||
|
||||
constructor(socket: TSocket, next: any) {
|
||||
this.socket = socket
|
||||
this.next = next
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse cookies
|
||||
*/
|
||||
const cookies = socket.request.headers.cookie.split('; ').reduce((prev: any, current: any) => {
|
||||
const [name, value] = current.split('=')
|
||||
prev[name] = value
|
||||
return prev
|
||||
}, {})
|
||||
public async authenticate(): Promise<void> {
|
||||
if (!this.socket.request.headers.cookie) {
|
||||
gameLogger.warn('No cookie provided')
|
||||
return this.next(new Error('Authentication error'))
|
||||
}
|
||||
|
||||
const token = cookies['token']
|
||||
const token = this.parseCookies()['token']
|
||||
|
||||
/**
|
||||
* Verify token, if valid, set user on socket and continue
|
||||
*/
|
||||
if (token) {
|
||||
verify(token, config.JWT_SECRET, async (err: any, decoded: any) => {
|
||||
if (!token) {
|
||||
gameLogger.warn('No token provided')
|
||||
return this.next(new Error('Authentication error'))
|
||||
}
|
||||
|
||||
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) {
|
||||
gameLogger.error('Invalid token')
|
||||
return next(new Error('Authentication error'))
|
||||
return this.next(new Error('Authentication error'))
|
||||
}
|
||||
|
||||
socket.userId = decoded.id
|
||||
next()
|
||||
this.socket.userId = decoded.id
|
||||
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()
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { appLogger } from '#application/logger'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { CharacterHair } from '#entities/characterHair'
|
||||
|
||||
class CharacterHairRepository extends BaseRepository {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { appLogger } from '#application/logger'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { Character } from '#entities/character'
|
||||
|
||||
class CharacterRepository extends BaseRepository {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { appLogger } from '#application/logger'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { CharacterType } from '#entities/characterType'
|
||||
|
||||
class CharacterTypeRepository extends BaseRepository {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { appLogger } from '#application/logger'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { Chat } from '#entities/chat'
|
||||
|
||||
class ChatRepository extends BaseRepository {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { appLogger } from '#application/logger'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { Item } from '#entities/item'
|
||||
|
||||
class ItemRepository extends BaseRepository {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
|
||||
class ObjectRepository extends BaseRepository {
|
||||
async getById(id: string): Promise<any> {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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'
|
||||
|
||||
class PasswordResetTokenRepository extends BaseRepository {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FilterValue } from '@mikro-orm/core'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { Sprite } from '#entities/sprite'
|
||||
|
||||
class SpriteRepository extends BaseRepository {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FilterValue } from '@mikro-orm/core'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { Tile } from '#entities/tile'
|
||||
import { Zone } from '#entities/zone'
|
||||
import { unduplicateArray } from '#application/utilities'
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { appLogger } from '#application/logger'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { User } from '#entities/user'
|
||||
|
||||
class UserRepository extends BaseRepository {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { gameLogger } from '#application/logger'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { World } from '#entities/world'
|
||||
|
||||
class WorldRepository extends BaseRepository {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { appLogger } from '#application/logger'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { ZoneEventTile } from '#entities/zoneEventTile'
|
||||
|
||||
class ZoneEventTileRepository extends BaseRepository {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { appLogger } from '#application/logger'
|
||||
import { BaseRepository } from '#application/bases/baseRepository'
|
||||
import { BaseRepository } from '#application/base/baseRepository'
|
||||
import { ZoneEventTile } from '#entities/zoneEventTile'
|
||||
import { ZoneObject } from '#entities/zoneObject'
|
||||
import { Zone } from '#entities/zone'
|
||||
|
@ -3,7 +3,6 @@ import express, { Application } from 'express'
|
||||
import config from '#application/config'
|
||||
import { getAppPath } from '#application/storage'
|
||||
import { createServer as httpServer, Server as HTTPServer } from 'http'
|
||||
import { addHttpRoutes } from './http'
|
||||
import cors from 'cors'
|
||||
import { Server as SocketServer } from 'socket.io'
|
||||
import { Authentication } from '#middleware/authentication'
|
||||
@ -16,6 +15,7 @@ import CommandManager from '#managers/commandManager'
|
||||
import QueueManager from '#managers/queueManager'
|
||||
import DateManager from '#managers/dateManager'
|
||||
import WeatherManager from '#managers/weatherManager'
|
||||
import { HttpRouter } from '#http/router'
|
||||
|
||||
export class Server {
|
||||
private readonly app: Application
|
||||
@ -64,8 +64,9 @@ export class Server {
|
||||
appLogger.error(`Socket.IO failed to start: ${error.message}`)
|
||||
}
|
||||
|
||||
// Add http API routes
|
||||
await addHttpRoutes(this.app)
|
||||
// Load HTTP routes
|
||||
const httpRouter = new HttpRouter(this.app)
|
||||
await httpRouter.boot()
|
||||
|
||||
// Load queue manager
|
||||
await QueueManager.boot(this.io)
|
||||
@ -77,7 +78,7 @@ export class Server {
|
||||
// await DateManager.boot(this.io)
|
||||
|
||||
// Load weather manager
|
||||
await WeatherManager.boot(this.io)
|
||||
// await WeatherManager.boot(this.io)
|
||||
|
||||
// Load zoneEditor manager
|
||||
await ZoneManager.boot()
|
||||
|
@ -65,15 +65,19 @@ export default class CharacterMove {
|
||||
break
|
||||
}
|
||||
|
||||
this.characterService.updatePosition(character, end)
|
||||
// Send minimal data
|
||||
// Update position first
|
||||
character.positionX = end.x
|
||||
character.positionY = end.y
|
||||
|
||||
// Then emit with the same properties
|
||||
this.io.in(character.zone!.id.toString()).emit('character:move', {
|
||||
id: character.id,
|
||||
positionX: end.x,
|
||||
positionY: end.y,
|
||||
positionX: character.positionX,
|
||||
positionY: character.positionY,
|
||||
rotation: character.rotation,
|
||||
isMoving: true
|
||||
})
|
||||
|
||||
await this.characterService.applyMovementDelay()
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user