diff --git a/src/application/base/baseController.ts b/src/application/base/baseController.ts new file mode 100644 index 0000000..4236f7b --- /dev/null +++ b/src/application/base/baseController.ts @@ -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 + }) + } +} diff --git a/src/application/bases/baseEntity.ts b/src/application/base/baseEntity.ts similarity index 100% rename from src/application/bases/baseEntity.ts rename to src/application/base/baseEntity.ts diff --git a/src/application/bases/baseRepository.ts b/src/application/base/baseRepository.ts similarity index 100% rename from src/application/bases/baseRepository.ts rename to src/application/base/baseRepository.ts diff --git a/src/entities/character.ts b/src/entities/character.ts index e0e878e..0954baa 100644 --- a/src/entities/character.ts +++ b/src/entities/character.ts @@ -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' diff --git a/src/entities/characterEquipment.ts b/src/entities/characterEquipment.ts index f79d2a8..dfc78b6 100644 --- a/src/entities/characterEquipment.ts +++ b/src/entities/characterEquipment.ts @@ -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' diff --git a/src/entities/characterHair.ts b/src/entities/characterHair.ts index 871e12a..a5efe4a 100644 --- a/src/entities/characterHair.ts +++ b/src/entities/characterHair.ts @@ -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' diff --git a/src/entities/characterItem.ts b/src/entities/characterItem.ts index 9b162c0..803c89f 100644 --- a/src/entities/characterItem.ts +++ b/src/entities/characterItem.ts @@ -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' diff --git a/src/entities/characterType.ts b/src/entities/characterType.ts index 7731233..79e5308 100644 --- a/src/entities/characterType.ts +++ b/src/entities/characterType.ts @@ -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' diff --git a/src/entities/chat.ts b/src/entities/chat.ts index 2d232eb..38ac9ab 100644 --- a/src/entities/chat.ts +++ b/src/entities/chat.ts @@ -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' diff --git a/src/entities/item.ts b/src/entities/item.ts index 21f3fdb..7ec7e9b 100644 --- a/src/entities/item.ts +++ b/src/entities/item.ts @@ -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' diff --git a/src/entities/mapObject.ts b/src/entities/mapObject.ts index 6b2879d..aaf47cf 100644 --- a/src/entities/mapObject.ts +++ b/src/entities/mapObject.ts @@ -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' diff --git a/src/entities/passwordResetToken.ts b/src/entities/passwordResetToken.ts index 070ec2e..e13ffec 100644 --- a/src/entities/passwordResetToken.ts +++ b/src/entities/passwordResetToken.ts @@ -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() diff --git a/src/entities/sprite.ts b/src/entities/sprite.ts index c9aa758..7e7f658 100644 --- a/src/entities/sprite.ts +++ b/src/entities/sprite.ts @@ -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' diff --git a/src/entities/spriteAction.ts b/src/entities/spriteAction.ts index 4c126a3..eda36cd 100644 --- a/src/entities/spriteAction.ts +++ b/src/entities/spriteAction.ts @@ -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' diff --git a/src/entities/tile.ts b/src/entities/tile.ts index 05921d8..987fba4 100644 --- a/src/entities/tile.ts +++ b/src/entities/tile.ts @@ -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() diff --git a/src/entities/user.ts b/src/entities/user.ts index bea56ad..d365c05 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -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' diff --git a/src/entities/world.ts b/src/entities/world.ts index 116a158..b9ec512 100644 --- a/src/entities/world.ts +++ b/src/entities/world.ts @@ -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 { diff --git a/src/entities/zone.ts b/src/entities/zone.ts index 54a445d..ea0e69e 100644 --- a/src/entities/zone.ts +++ b/src/entities/zone.ts @@ -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' diff --git a/src/entities/zoneEffect.ts b/src/entities/zoneEffect.ts index aefbc26..c548aa0 100644 --- a/src/entities/zoneEffect.ts +++ b/src/entities/zoneEffect.ts @@ -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' diff --git a/src/entities/zoneEventTile.ts b/src/entities/zoneEventTile.ts index 88b768f..34d594a 100644 --- a/src/entities/zoneEventTile.ts +++ b/src/entities/zoneEventTile.ts @@ -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' diff --git a/src/entities/zoneEventTileTeleport.ts b/src/entities/zoneEventTileTeleport.ts index cba9fd8..5a8dbb2 100644 --- a/src/entities/zoneEventTileTeleport.ts +++ b/src/entities/zoneEventTileTeleport.ts @@ -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' diff --git a/src/entities/zoneObject.ts b/src/entities/zoneObject.ts index 8b3ade0..8249447 100644 --- a/src/entities/zoneObject.ts +++ b/src/entities/zoneObject.ts @@ -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' diff --git a/src/http/assets.ts b/src/http/assets.ts deleted file mode 100644 index 4f3c3ae..0000000 --- a/src/http/assets.ts +++ /dev/null @@ -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 diff --git a/src/http/auth.ts b/src/http/auth.ts deleted file mode 100644 index c1f3420..0000000 --- a/src/http/auth.ts +++ /dev/null @@ -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 diff --git a/src/http/avatar.ts b/src/http/avatar.ts deleted file mode 100644 index 460176f..0000000 --- a/src/http/avatar.ts +++ /dev/null @@ -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 diff --git a/src/http/controllers/assets.ts b/src/http/controllers/assets.ts new file mode 100644 index 0000000..eecc3ad --- /dev/null +++ b/src/http/controllers/assets.ts @@ -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) + } + }) + } +} diff --git a/src/http/controllers/auth.ts b/src/http/controllers/auth.ts new file mode 100644 index 0000000..ab82084 --- /dev/null +++ b/src/http/controllers/auth.ts @@ -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') + } + } +} diff --git a/src/http/controllers/avatar.ts b/src/http/controllers/avatar.ts new file mode 100644 index 0000000..7b21e66 --- /dev/null +++ b/src/http/controllers/avatar.ts @@ -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) + } + } +} diff --git a/src/http/index.ts b/src/http/index.ts deleted file mode 100644 index 0bd8724..0000000 --- a/src/http/index.ts +++ /dev/null @@ -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 } diff --git a/src/http/router.ts b/src/http/router.ts new file mode 100644 index 0000000..76d5571 --- /dev/null +++ b/src/http/router.ts @@ -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)) + } +} diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index 252c9fb..f7ed4e5 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -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 { + 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 { + 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() +} diff --git a/src/repositories/characterHairRepository.ts b/src/repositories/characterHairRepository.ts index 0e73253..dd50b35 100644 --- a/src/repositories/characterHairRepository.ts +++ b/src/repositories/characterHairRepository.ts @@ -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 { diff --git a/src/repositories/characterRepository.ts b/src/repositories/characterRepository.ts index 6827ff2..8b1fb6e 100644 --- a/src/repositories/characterRepository.ts +++ b/src/repositories/characterRepository.ts @@ -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 { diff --git a/src/repositories/characterTypeRepository.ts b/src/repositories/characterTypeRepository.ts index afb2717..6de2e66 100644 --- a/src/repositories/characterTypeRepository.ts +++ b/src/repositories/characterTypeRepository.ts @@ -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 { diff --git a/src/repositories/chatRepository.ts b/src/repositories/chatRepository.ts index c94f4be..1c3b8fe 100644 --- a/src/repositories/chatRepository.ts +++ b/src/repositories/chatRepository.ts @@ -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 { diff --git a/src/repositories/itemRepository.ts b/src/repositories/itemRepository.ts index 49fc99d..874c988 100644 --- a/src/repositories/itemRepository.ts +++ b/src/repositories/itemRepository.ts @@ -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 { diff --git a/src/repositories/objectRepository.ts b/src/repositories/objectRepository.ts index 5b0eb26..e91e941 100644 --- a/src/repositories/objectRepository.ts +++ b/src/repositories/objectRepository.ts @@ -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 { diff --git a/src/repositories/passwordResetTokenRepository.ts b/src/repositories/passwordResetTokenRepository.ts index e6a4846..0081240 100644 --- a/src/repositories/passwordResetTokenRepository.ts +++ b/src/repositories/passwordResetTokenRepository.ts @@ -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 { diff --git a/src/repositories/spriteRepository.ts b/src/repositories/spriteRepository.ts index 4b3309c..f2d1820 100644 --- a/src/repositories/spriteRepository.ts +++ b/src/repositories/spriteRepository.ts @@ -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 { diff --git a/src/repositories/tileRepository.ts b/src/repositories/tileRepository.ts index a7815d4..234c1d7 100644 --- a/src/repositories/tileRepository.ts +++ b/src/repositories/tileRepository.ts @@ -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' diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index ae3d27b..e7b9f8e 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -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 { diff --git a/src/repositories/worldRepository.ts b/src/repositories/worldRepository.ts index 007e64f..ff3b110 100644 --- a/src/repositories/worldRepository.ts +++ b/src/repositories/worldRepository.ts @@ -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 { diff --git a/src/repositories/zoneEventTileRepository.ts b/src/repositories/zoneEventTileRepository.ts index af1aba6..ca23c56 100644 --- a/src/repositories/zoneEventTileRepository.ts +++ b/src/repositories/zoneEventTileRepository.ts @@ -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 { diff --git a/src/repositories/zoneRepository.ts b/src/repositories/zoneRepository.ts index 5a9da3c..016c80f 100644 --- a/src/repositories/zoneRepository.ts +++ b/src/repositories/zoneRepository.ts @@ -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' diff --git a/src/server.ts b/src/server.ts index 4d54149..ccd74f9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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() diff --git a/src/socketEvents/zone/characterMove.ts b/src/socketEvents/zone/characterMove.ts index 56e9cc1..fca9995 100644 --- a/src/socketEvents/zone/characterMove.ts +++ b/src/socketEvents/zone/characterMove.ts @@ -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() }