Compare commits

...

18 Commits

Author SHA1 Message Date
27d8c7cff6 Finish password reset (hopefully) 2024-11-03 21:54:54 +01:00
b9a7f9aa8e Typo 2024-11-03 01:33:43 +01:00
5b6b968541 Merge remote-tracking branch 'origin/feature/#182-reset-password' 2024-11-03 01:27:46 +01:00
93abf4b631 Updated token hash, use repo instead of prisma for data fetching 2024-11-03 00:50:00 +01:00
0de574b9e1 npm update 2024-11-03 00:33:06 +01:00
c04c52aed0 Merge remote-tracking branch 'origin/main' into feature/#182-reset-password
# Conflicts:
#	src/utilities/http.ts
2024-11-02 21:43:25 +01:00
3f19730bd8 Added param to JSdoc 2024-11-02 21:26:47 +01:00
d0e3c95bb0 npm update 2024-11-02 02:18:17 +01:00
82f51b2b7e Added pw token expiry check, temporarily commented mailer code due to bugs 2024-11-02 01:46:50 +01:00
1b9db64854 npm update 2024-10-31 12:31:40 +01:00
41c71d5964 Added list_sprite_actions http endpoint 2024-10-30 15:26:39 +01:00
bd04dc2ab8 Continuation dynamic asset loading 2024-10-30 09:34:07 +01:00
a4e96f9ede (WIP) Added pw reset token row, added checks to reset function 2024-10-29 22:49:21 +01:00
f6bac403a2 Minor changes 2024-10-28 23:41:54 +01:00
8460d0b535 Worked on http endpoints for dynamic tile loading 2024-10-28 23:23:10 +01:00
5a36d10f0e Added reset password function + basic mail layout 2024-10-27 21:30:33 +01:00
8f8f019ab7 Add email field and add it to register logic 2024-10-27 17:25:45 +01:00
6a1823586a Commented out http endpoint 2024-10-26 02:41:41 +02:00
17 changed files with 467 additions and 92 deletions

View File

@ -11,4 +11,10 @@ ALLOW_DIAGONAL_MOVEMENT=false
# Default character create values
DEFAULT_CHARACTER_ZONE="0"
DEFAULT_CHARACTER_POS_X="0"
DEFAULT_CHARACTER_POS_Y="0"
DEFAULT_CHARACTER_POS_Y="0"
# SMTP configuration
SMTP_HOST="my.directonline.io"
SMTP_PORT="587"
SMTP_USER="no-reply@sylvan.quest"
SMTP_PASSWORD="Z%kI*1xe67WuGg"

51
package-lock.json generated
View File

@ -14,6 +14,7 @@
"express": "^4.19.2",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.15",
"pino": "^9.3.2",
"prisma": "^5.17.0",
"sharp": "^0.33.4",
@ -27,6 +28,7 @@
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.14.11",
"@types/nodemailer": "^6.4.16",
"nodemon": "^3.1.4",
"prettier": "^3.3.3"
}
@ -719,14 +721,24 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.17.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.1.tgz",
"integrity": "sha512-j2VlPv1NnwPJbaCNv69FO/1z4lId0QmGvpT41YxitRtWlg96g/j8qcv2RKsLKe2F6OJgyXhupN1Xo17b2m139Q==",
"version": "20.17.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.5.tgz",
"integrity": "sha512-n8FYY/pRxu496441gIcAQFZPKXbhsd6VZygcq+PTSZ75eMh/Ke0hCAROdUa21qiFqKNsPPYic46yXDO1JGiPBQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/@types/nodemailer": {
"version": "6.4.16",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz",
"integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": {
"version": "6.9.16",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
@ -778,9 +790,9 @@
}
},
"node_modules/acorn": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
"integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==",
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@ -926,9 +938,9 @@
"license": "BSD-3-Clause"
},
"node_modules/bullmq": {
"version": "5.21.2",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.21.2.tgz",
"integrity": "sha512-LPuNoGaDc5CON2X6h4cJ2iVfd+B+02xubFU+IB/fyJHd+/HqUZRqnlYryUCAuhVHBhUKtA6oyVdJxqSa62i+og==",
"version": "5.23.0",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.23.0.tgz",
"integrity": "sha512-VILKTIOwo9AopMyVqvDhQ1qyLrOtBSfu+G2bntgauQfxYzT7ETj+h2HeUe7a9i9AU/+OXJGYYm49NHJedEz7VQ==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.6.0",
@ -1895,9 +1907,9 @@
"license": "MIT"
},
"node_modules/msgpackr": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.0.tgz",
"integrity": "sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==",
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz",
"integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
@ -1955,6 +1967,15 @@
"node-gyp-build-optional-packages-test": "build-test.js"
}
},
"node_modules/nodemailer": {
"version": "6.9.16",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
"integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz",
@ -2719,9 +2740,9 @@
}
},
"node_modules/tslib": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-is": {

View File

@ -15,6 +15,7 @@
"express": "^4.19.2",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.15",
"pino": "^9.3.2",
"prisma": "^5.17.0",
"sharp": "^0.33.4",
@ -28,6 +29,7 @@
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.14.11",
"@types/nodemailer": "^6.4.16",
"nodemon": "^3.1.4",
"prettier": "^3.3.3"
}

View File

@ -40,10 +40,23 @@ CREATE TABLE `SpriteAction` (
CREATE TABLE `User` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`username` VARCHAR(191) NOT NULL,
`email` VARCHAR(191) NOT NULL,
`password` VARCHAR(191) NOT NULL,
`online` BOOLEAN NOT NULL DEFAULT false,
UNIQUE INDEX `User_username_key`(`username`),
UNIQUE INDEX `User_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `PasswordResetToken` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`userId` INTEGER NOT NULL,
`token` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE INDEX `PasswordResetToken_token_key`(`token`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@ -202,6 +215,9 @@ ALTER TABLE `Chat` ADD CONSTRAINT `Chat_zoneId_fkey` FOREIGN KEY (`zoneId`) REFE
-- AddForeignKey
ALTER TABLE `SpriteAction` ADD CONSTRAINT `SpriteAction_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `PasswordResetToken` ADD CONSTRAINT `PasswordResetToken_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `CharacterType` ADD CONSTRAINT `CharacterType_spriteId_fkey` FOREIGN KEY (`spriteId`) REFERENCES `Sprite`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,9 +1,19 @@
model User {
id Int @id @default(autoincrement())
username String @unique
password String
online Boolean @default(false)
characters Character[]
id Int @id @default(autoincrement())
username String @unique
email String @unique
password String
online Boolean @default(false)
characters Character[]
passwordResetTokens PasswordResetToken[]
}
model PasswordResetToken {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
token String @unique
createdAt DateTime @default(now())
}
enum CharacterGender {
@ -61,4 +71,4 @@ model CharacterItem {
itemId String
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
quantity Int
}
}

View File

@ -0,0 +1,44 @@
import prisma from '../utilities/prisma' // Import the global Prisma instance
class PasswordResetTokenRepository {
async getById(id: number): Promise<any> {
try {
return await prisma.passwordResetToken.findUnique({
where: {
id
}
})
} catch (error: any) {
// Handle error
throw new Error(`Failed to get password reset token by ID: ${error.message}`)
}
}
async getByUserId(userId: number): Promise<any> {
try {
return await prisma.passwordResetToken.findFirst({
where: {
userId
}
})
} catch (error: any) {
// Handle error
throw new Error(`Failed to get password reset token by user ID: ${error.message}`)
}
}
async getByToken(token: string): Promise<any> {
try {
return await prisma.passwordResetToken.findFirst({
where: {
token
}
})
} catch (error: any) {
// Handle error
throw new Error(`Failed to get password reset token by token: ${error.message}`)
}
}
}
export default new PasswordResetTokenRepository()

View File

@ -1,5 +1,8 @@
import prisma from '../utilities/prisma' // Import the global Prisma instance
import { Tile } from '@prisma/client'
import zoneRepository from './zoneRepository'
import { unduplicateArray } from '../utilities/utilities'
import { FlattenZoneArray } from '../utilities/zone'
class TileRepository {
async getById(id: string): Promise<Tile | null> {
@ -8,9 +11,28 @@ class TileRepository {
})
}
async getByIds(ids: string[]): Promise<Tile[]> {
return prisma.tile.findMany({
where: {
id: {
in: ids
}
}
})
}
async getAll(): Promise<Tile[]> {
return prisma.tile.findMany()
}
async getByZoneId(zoneId: number): Promise<Tile[]> {
const zone = await zoneRepository.getById(zoneId)
if (!zone) return []
const zoneTileArray = unduplicateArray(FlattenZoneArray(JSON.parse(JSON.stringify(zone.tiles))))
return this.getByIds(zoneTileArray)
}
}
export default new TileRepository()

View File

@ -27,6 +27,19 @@ class UserRepository {
throw new Error(`Failed to get user by username: ${error.message}`)
}
}
async getByEmail(email: string): Promise<User | null> {
try {
return await prisma.user.findUnique({
where: {
email
}
})
} catch (error: any) {
// Handle error
throw new Error(`Failed to get user by email: ${error.message}`)
}
}
}
export default new UserRepository()

View File

@ -2,7 +2,7 @@ import { Zone, ZoneEventTile, ZoneEventTileType, ZoneObject } from '@prisma/clie
import prisma from '../utilities/prisma'
import { ZoneEventTileWithTeleport } from '../socketEvents/zone/characterMove'
import { appLogger } from '../utilities/logger'
import { TAsset } from '../utilities/types'
import { AssetData } from '../utilities/types'
import tileRepository from './tileRepository'
class ZoneRepository {
@ -91,52 +91,6 @@ class ZoneRepository {
return []
}
}
async getZoneAssets(zone_id: number): Promise<TAsset[]> {
const zone = await this.getById(zone_id)
if (!zone) return []
const assets: TAsset[] = []
// zone.tiles is prisma jsonvalue
let tiles = JSON.parse(JSON.stringify(zone.tiles))
tiles = [...new Set(tiles.flat())]
// Add tile assets
for (const tile of tiles) {
const tileInfo = await tileRepository.getById(tile)
if (!tileInfo) continue
assets.push({
key: tileInfo.id,
url: '/assets/tiles/' + tileInfo.id + '.png',
group: 'tiles',
updatedAt: tileInfo?.updatedAt || new Date()
})
}
// Add object assets
for (const zoneObject of zone.zoneObjects) {
if (!zoneObject.object) continue
assets.push({
key: zoneObject.object.id,
url: '/assets/objects/' + zoneObject.object.id + '.png',
group: 'objects',
updatedAt: zoneObject.object.updatedAt || new Date()
})
}
// Filter out duplicate assets
return assets.reduce((acc: TAsset[], current) => {
const x = acc.find((item) => item.key === current.key && item.group === current.group)
if (!x) {
return acc.concat([current])
} else {
return acc
}
}, [])
}
}
export default new ZoneRepository()

View File

@ -1,7 +1,10 @@
import bcrypt from 'bcryptjs'
import UserRepository from '../repositories/userRepository'
import PasswordResetTokenRepository from '../repositories/passwordResetTokenRepository'
import prisma from '../utilities/prisma'
import { User } from '@prisma/client'
import { User, PasswordResetToken } from '@prisma/client'
import config from '../utilities/config'
import NodeMailer from 'nodemailer'
/**
* User service
@ -31,18 +34,104 @@ class UserService {
/**
* Register user
* @param username
* @param email
* @param password
*/
async register(username: string, password: string): Promise<boolean | User> {
async register(username: string, email: string, password: string): Promise<boolean | User> {
const user = await UserRepository.getByUsername(username)
if (user) {
return false
}
const userByEmail = await UserRepository.getByEmail(email)
if (userByEmail) {
return false
}
const hashedPassword = await bcrypt.hash(password, 10)
return prisma.user.create({
data: {
username,
email,
password: hashedPassword
}
})
}
/**
* Reset password
* @param email
*/
async resetPassword(email: string): Promise<boolean> {
const user = await UserRepository.getByEmail(email)
if ( !user ) return false
const token = await bcrypt.hash(new Date().getTime().toString(), 10)
const latestToken = await PasswordResetTokenRepository.getByUserId(user.id)
//Check if password reset has been requested recently
if (latestToken) {
const tokenExpiryDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
const isTokenExpired = latestToken.createdAt < tokenExpiryDate
if (!isTokenExpired) return false
await prisma.passwordResetToken.delete({
where: {
id: latestToken.id
}
})
}
await prisma.passwordResetToken.create({
data: {
userId: user.id,
token: token,
}
});
const transporter = NodeMailer.createTransport({
host: config.SMTP_HOST,
port: config.SMTP_PORT,
secure: false,
auth: {
user: config.SMTP_USER,
pass: config.SMTP_PASSWORD,
},
});
try {
await transporter.sendMail({
from: config.SMTP_USER,
to: email,
subject: "Reset your password",
text: "A password reset has been requested, reset your password here: " + config.CLIENT_URL + "#" + token, // Plain text body
html: "<p>A password reset has been requested, reset your password here: <a href='" + config.CLIENT_URL + "#" + token + "'>" + config.CLIENT_URL + "#" + token + "</a></p>", // Html body
});
return true
} catch (error: any) {
return false
}
}
/**
* Set new password
* @param urlToken
* @param password
*/
async newPassword(urlToken: string, password: string): Promise<boolean | User> {
const tokenData = await PasswordResetTokenRepository.getByToken(urlToken)
if (!tokenData) {
return false
}
const hashedPassword = await bcrypt.hash(password, 10)
return prisma.user.update({
where: { id: tokenData.userId },
data: {
password: hashedPassword
}
})

View File

@ -1,4 +1,14 @@
import prisma from '../utilities/prisma'
import { AssetData } from '../utilities/types'
import tileRepository from '../repositories/tileRepository'
import zoneRepository from '../repositories/zoneRepository'
import { Object, Zone, ZoneObject } from '@prisma/client'
type getZoneAsetsZoneType = Zone & {
zoneObjects: (ZoneObject & {
object: Object
})[]
}
class ZoneService {
async createDemoZone(): Promise<boolean> {
@ -27,6 +37,49 @@ class ZoneService {
console.log('Demo zone created.')
return true
}
async getZoneAssets(zone: getZoneAsetsZoneType): Promise<AssetData[]> {
const assets: AssetData[] = []
// zone.tiles is prisma jsonvalue
let tiles = JSON.parse(JSON.stringify(zone.tiles))
tiles = [...new Set(tiles.flat())]
// Add tile assets
for (const tile of tiles) {
const tileInfo = await tileRepository.getById(tile)
if (!tileInfo) continue
assets.push({
key: tileInfo.id,
data: '/assets/tiles/' + tileInfo.id + '.png',
group: 'tiles',
updatedAt: tileInfo?.updatedAt || new Date()
} as AssetData)
}
// Add object assets
for (const zoneObject of zone.zoneObjects) {
if (!zoneObject.object) continue
assets.push({
key: zoneObject.object.id,
data: '/assets/objects/' + zoneObject.object.id + '.png',
group: 'objects',
updatedAt: zoneObject.object.updatedAt || new Date()
} as AssetData)
}
// Filter out duplicate assets
return assets.reduce((acc: AssetData[], current) => {
const x = acc.find((item) => item.key === current.key && item.group === current.group)
if (!x) {
return acc.concat([current])
} else {
return acc
}
}, [])
}
}
export default ZoneService

View File

@ -7,6 +7,7 @@ class config {
static REDIS_URL: string = process.env.REDIS_URL || 'redis://@127.0.0.1:6379/4'
static HOST: string = process.env.HOST || '0.0.0.0'
static PORT: number = process.env.PORT ? parseInt(process.env.PORT) : 6969
static CLIENT_URL: string = process.env.CLIENT_URL ? process.env.CLIENT_URL : 'https://sylvan.quest/'
static JWT_SECRET: string = process.env.JWT_SECRET || 'secret'
static ALLOW_DIAGONAL_MOVEMENT: boolean = process.env.ALLOW_DIAGONAL_MOVEMENT === 'true'
@ -14,6 +15,11 @@ class config {
static DEFAULT_CHARACTER_ZONE: number = parseInt(process.env.DEFAULT_CHARACTER_ZONE || '1')
static DEFAULT_CHARACTER_X: number = parseInt(process.env.DEFAULT_CHARACTER_POS_X || '0')
static DEFAULT_CHARACTER_Y: number = parseInt(process.env.DEFAULT_CHARACTER_POS_Y || '0')
static SMTP_HOST: string = process.env.SMTP_HOST || 'my.directonline.io'
static SMTP_PORT: number = process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT) : 587
static SMTP_USER: string = process.env.SMTP_USER || 'no-reply@sylvan.quest'
static SMTP_PASSWORD: string = process.env.SMTP_PASSWORD || 'password'
}
export default config

View File

@ -2,11 +2,14 @@ import { Application, Request, Response } from 'express'
import UserService from '../services/userService'
import jwt from 'jsonwebtoken'
import config from './config'
import { loginAccountSchema, registerAccountSchema } from './zodTypes'
import { loginAccountSchema, registerAccountSchema, resetPasswordSchema, newPasswordSchema } from './zodTypes'
import fs from 'fs'
import { httpLogger } from './logger'
import { getPublicPath } from './storage'
import zoneRepository from '../repositories/zoneRepository'
import TileRepository from '../repositories/tileRepository'
import { AssetData } from './types'
import ZoneRepository from '../repositories/zoneRepository'
import SpriteRepository from '../repositories/spriteRepository'
async function addHttpRoutes(app: Application) {
/**
@ -40,16 +43,16 @@ async function addHttpRoutes(app: Application) {
* @param res
*/
app.post('/register', async (req: Request, res: Response) => {
const { username, password } = req.body
const { username, email, password } = req.body
try {
registerAccountSchema.parse({ username, password })
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, password)
const user = await userService.register(username, email, password)
if (user) {
return res.status(200).json({ message: 'User registered' })
@ -58,12 +61,82 @@ async function addHttpRoutes(app: Application) {
return res.status(400).json({ message: 'Failed to register user' })
})
/**
* Reset password
* @param req
* @param res
*/
app.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.resetPassword( 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' })
})
/**
* New password
* @param req
* @param res
*/
app.post('/new-password', async (req: Request, res: Response) => {
const { urlToken, password } = req.body
try {
newPasswordSchema.parse({ password })
} catch (error: any) {
return res.status(400).json({ message: error.errors[0]?.message })
}
const userService = new UserService()
const resetPassword = await userService.newPassword( 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' })
})
/**
* Get all tiles from a zone as an array of ids
* @param req
* @param res
*/
app.get('/assets/tiles/:zoneId', async (req: Request, res: Response) => {
app.get('/assets/list_tiles', async (req: Request, res: Response) => {
// Get all tiles
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)
}
// Return the array
res.json(assets)
})
/**
* Get all tiles from a zone and serve as AssetData array
* @param req
* @param res
*/
app.get('/assets/list_tiles/:zoneId', async (req: Request, res: Response) => {
const zoneId = req.params.zoneId
// Check if zoneId is valid number
@ -72,28 +145,59 @@ async function addHttpRoutes(app: Application) {
}
// Get zone by id
const zone = await zoneRepository.getById(parseInt(zoneId))
const zone = await ZoneRepository.getById(parseInt(zoneId))
if (!zone) {
return res.status(404).json({ message: 'Zone not found' })
}
let tiles = zone.tiles;
// Convert to array
tiles = JSON.parse(JSON.stringify(tiles)) as string[]
// Flatten the array
tiles = [...new Set(tiles.flat())]
// Remove duplicates
tiles = tiles.filter((value, index, self) => self.indexOf(value) === index);
// Get all tiles
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)
}
// Return the array
res.json(tiles)
res.json(assets)
})
app.get('/assets/list_sprite_actions/:spriteId', async (req: Request, res: Response) => {
const spriteId = req.params.spriteId
// Check if spriteId is valid number
if (!spriteId || parseInt(spriteId) === 0) {
return res.status(400).json({ message: 'Invalid sprite ID' })
}
// Get sprite by id
const sprite = await SpriteRepository.getById(spriteId)
if (!sprite) {
return res.status(404).json({ message: 'Sprite not found' })
}
let assets: AssetData[] = []
sprite.spriteActions.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,
isAnimated: spriteAction.isAnimated,
frameCount: JSON.parse(JSON.stringify(spriteAction.sprites)).length,
frameWidth: spriteAction.frameWidth,
frameHeight: spriteAction.frameHeight
})
})
// Return the array
res.json(assets)
})
/**
* Get a specific asset
* Download asset file
* @param req
* @param res
*/

View File

@ -21,11 +21,12 @@ export type ExtendedCharacter = Character & {
resetMovement: boolean
}
export type TAsset = {
export type AssetData = {
key: string
url: string
data: string
group: 'tiles' | 'objects' | 'sprites' | 'sprite_animations' | 'sound' | 'music' | 'ui' | 'font' | 'other'
updatedAt: Date
isAnimated?: boolean
frameCount?: number
frameWidth?: number
frameHeight?: number

View File

@ -0,0 +1,3 @@
export function unduplicateArray(array: any[]) {
return [...new Set(array.flat())]
}

View File

@ -20,6 +20,28 @@ export const registerAccountSchema = z.object({
.min(3, { message: 'Name must be at least 3 characters long' })
.max(255, { message: 'Name must be at most 255 characters long' })
.regex(/^[A-Za-z][A-Za-z0-9_-]*$/, { message: 'Name must start with a letter and can only contain letters, numbers, underscores, or dashes' }),
email: z
.string()
.min(3, { message: 'Email must be at least 3 characters long' })
.max(255, { message: 'Email must be at most 255 characters long' })
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' }),
password: z
.string()
.min(8, {
message: 'Password must be at least 8 characters long'
})
.max(255)
})
export const resetPasswordSchema = z.object({
email: z
.string()
.min(3, { message: 'Email must be at least 3 characters long' })
.max(255, { message: 'Email must be at most 255 characters long' })
.regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/, { message: 'Email must be valid' })
})
export const newPasswordSchema = z.object({
password: z
.string()
.min(8, {

9
src/utilities/zone.ts Normal file
View File

@ -0,0 +1,9 @@
export function FlattenZoneArray(tiles: string[][]) {
const normalArray = []
for (const row of tiles) {
normalArray.push(...row)
}
return normalArray
}