1 import * as otel from './tracing';
2 import { version } from "../../package.json";
3 import { config as dotenv } from 'dotenv';
4 import { join } from 'path';
5 import express, {Request, Response} from 'express';
6 import bodyParser from 'body-parser';
8 import { rateLimit } from 'express-rate-limit';
10 import http from 'http';
11 import { Server, Socket } from 'socket.io';
12 import * as CONSTANT from '../shared/constants';
13 import { logger } from './lib/logger';
14 import { loadPlayer, createPlayer, updatePlayer, movePlayer } from './player';
15 import { random, sample } from 'lodash';
16 import {broadcastMessage, Message} from '../shared/message';
17 import {maxHp, Player} from '../shared/player';
18 import {createFight, getMonsterList, getMonsterLocation, getRandomMonster, loadMonster, loadMonsterFromFight, loadMonsterWithFaction} from './monster';
19 import {addInventoryItem, getEquippedItems, getInventory, getInventoryItem} from './inventory';
20 import { getItemFromPlayer, getItemFromShop, getPlayersItems, getShopItems, givePlayerItem, updateItemCount } from './items';
21 import {FightTrigger, Monster, MonsterForFight} from '../shared/monsters';
22 import {getShopEquipment, listShopItems } from './shopEquipment';
23 import {EquipmentSlot} from '../shared/inventory';
24 import { clearTravelPlan, completeTravel, getAllPaths, getAllServices, getCityDetails, getService, getTravelPlan, stepForward, travel } from './map';
25 import { signup, login, authEndpoint, AuthRequest } from './auth';
26 import {db} from './lib/db';
27 import { getPlayerSkills} from './skills';
29 import { fightRound, blockPlayerInFight } from './fight';
31 import { router as healerRouter } from './locations/healer';
32 import { router as professionRouter } from './locations/recruiter';
34 import * as Alert from './views/alert';
35 import { renderPlayerBar } from './views/player-bar'
36 import { renderEquipmentDetails, renderStore } from './views/stores';
37 import { renderMap } from './views/map';
38 import { renderProfilePage } from './views/profile';
39 import { renderSkills } from './views/skills';
40 import { renderInventoryPage } from './views/inventory';
41 import { renderMonsterSelector, renderOnlyMonsterSelector } from './views/monster-selector';
42 import { renderFight, renderFightPreRound, renderRoundDetails } from './views/fight';
43 import { renderTravel, travelButton } from './views/travel';
44 import { renderChatMessage } from './views/chat';
47 import { createMonsters } from '../../seeds/monsters';
48 import { createAllCitiesAndLocations } from '../../seeds/cities';
49 import { createShopItems, createShopEquipment } from '../../seeds/shop_items';
50 import { Item, PlayerItem, ShopItem } from 'shared/items';
51 import { equip, unequip } from './equipment';
52 import { HealthPotionSmall } from '../shared/items/health_potion';
58 const app = express();
59 const server = http.createServer(app);
61 app.use(express.static(join(__dirname, '..', '..', 'public')));
62 app.use(bodyParser.urlencoded({ extended: true }));
63 app.use(express.json());
65 const io = new Server(server);
67 const cache = new Map<string, any>();
68 const chatHistory: Message[] = [];
70 app.use((req, res, next) => {
71 console.log(req.method, req.url);
75 const fightRateLimiter = rateLimit({
76 windowMs: parseInt(process.env.RATE_LIMIT_WINDOW || '30000'),
77 max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '20'),
78 standardHeaders: true,
80 handler: (req, res, next, options) => {
81 logger.log(`Blocked request: [${req.headers['x-authtoken']}: ${req.method} ${req.path}]`);
82 res.status(options.statusCode).send(options.message);
86 async function bootstrapSocket(socket: Socket, player: Player) {
87 // ref to get the socket id for a particular player
88 cache.set(`socket:${player.id}`, socket.id);
89 // ref to get the player object
90 cache.set(`token:${player.id}`, player);
92 socket.emit('authToken', player.id);
94 socket.emit('chat', renderChatMessage(broadcastMessage('server', `${player.username} just logged in`)));
97 io.on('connection', async socket => {
98 logger.log(`socket ${socket.id} connected, authToken: ${socket.handshake.headers['x-authtoken']}`);
100 let authToken = socket.handshake.headers['x-authtoken'].toString() === 'null' ? null : socket.handshake.headers['x-authtoken'].toString();
104 logger.log(`Attempting to load player with id ${authToken}`);
105 player = await loadPlayer(authToken);
108 logger.log(`Creating player`);
109 player = await createPlayer();
110 authToken = player.id;
111 socket.handshake.headers['x-authtoken'] = authToken;
114 logger.log(`Socket [${socket.id}] auth token: ${player.id}`);
116 bootstrapSocket(socket, player);
118 socket.on('disconnect', () => {
119 console.log(`Player ${player.username} left`);
120 io.emit('status', `${io.sockets.sockets.size} Online (v${version})`);
124 io.emit('status', `${io.sockets.sockets.size} Online (v${version})`);
125 // this is a special event to let the client know it can start
127 socket.emit('ready');
131 app.use(healerRouter);
132 app.use(professionRouter);
135 app.get('/chat/history', authEndpoint, async (req: AuthRequest, res: Response) => {
136 let html = chatHistory.map(renderChatMessage);
138 res.send(html.join("\n"));
141 app.post('/chat', authEndpoint, async (req: AuthRequest, res: Response) => {
142 const msg = req.body.message.trim();
144 if(!msg || !msg.length) {
149 let message: Message;
150 if(msg.startsWith('/server lmnop')) {
151 if(msg === '/server lmnop refresh-monsters') {
152 await createMonsters();
153 message = broadcastMessage('server', 'Monster refresh!');
155 else if(msg === '/server lmnop refresh-cities') {
156 await createAllCitiesAndLocations();
157 message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!');
159 else if(msg === '/server lmnop refresh-shops') {
160 await createShopItems();
161 await createShopEquipment();
162 message = broadcastMessage('server', 'Refresh shop items');
165 const str = msg.split('/server lmnop ')[1];
167 message = broadcastMessage('server', str);
172 message = broadcastMessage(req.player.username, xss(msg, {
175 chatHistory.push(message);
176 chatHistory.slice(-10);
180 io.emit('chat', renderChatMessage(message));
185 app.get('/player', authEndpoint, async (req: AuthRequest, res: Response) => {
186 const inventory = await getEquippedItems(req.player.id);
188 res.send(renderPlayerBar(req.player, inventory) + renderProfilePage(req.player));
191 app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Response) => {
192 const stat = req.params.stat;
193 if(!['strength', 'constitution', 'dexterity', 'intelligence'].includes(stat)) {
194 res.send(Alert.ErrorAlert(`Sorry, that's not a valid stat to increase`));
198 if(req.player.stat_points <= 0) {
199 res.send(Alert.ErrorAlert(`Sorry, you don't have enough stat points`));
203 req.player.stat_points -= 1;
206 updatePlayer(req.player);
208 const equippedItems = await getEquippedItems(req.player.id);
209 res.send(renderPlayerBar(req.player, equippedItems) + renderProfilePage(req.player));
212 app.get('/player/skills', authEndpoint, async (req: AuthRequest, res: Response) => {
213 const skills = await getPlayerSkills(req.player.id);
215 res.send(renderSkills(skills));
218 app.get('/player/inventory', authEndpoint, async (req: AuthRequest, res: Response) => {
219 const [inventory, items] = await Promise.all([
220 getInventory(req.player.id),
221 getPlayersItems(req.player.id)
224 res.send(renderInventoryPage(inventory, items));
227 app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => {
228 const inventoryItem = await getInventoryItem(req.player.id, req.params.item_id);
229 const equippedItems = await getEquippedItems(req.player.id);
230 const requestedSlot = req.params.slot;
231 let desiredSlot: EquipmentSlot = inventoryItem.equipment_slot;
234 // handes the situation where you're trying to equip an item
235 // that can be equipped to any hand
236 if(inventoryItem.equipment_slot === 'ANY_HAND') {
237 if(requestedSlot === 'LEFT_HAND' || requestedSlot === 'RIGHT_HAND') {
238 // get the players current equipment in that slot!
239 if(equippedItems.some(v => {
240 return v.equipment_slot === requestedSlot || v.equipment_slot === 'TWO_HANDED';
245 desiredSlot = requestedSlot;
250 if(requestedSlot === 'TWO_HANDED') {
251 if(equippedItems.some(v => {
252 return v.equipment_slot === 'LEFT_HAND' || v.equipment_slot === 'RIGHT_HAND';
259 await equip(req.player.id, inventoryItem, desiredSlot);
260 const socketId = cache.get(`socket:${req.player.id}`).toString();
261 io.to(socketId).emit('updatePlayer', req.player);
262 io.to(socketId).emit('alert', {
264 text: `You equipped your ${inventoryItem.name}`
271 const [inventory, items] = await Promise.all([
272 getInventory(req.player.id),
273 getPlayersItems(req.player.id)
276 res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player, inventory));
279 app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => {
280 const [item, ] = await Promise.all([
281 getInventoryItem(req.player.id, req.params.item_id),
282 unequip(req.player.id, req.params.item_id)
285 const [inventory, items] = await Promise.all([
286 getInventory(req.player.id),
287 getPlayersItems(req.player.id)
290 res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player, inventory));
293 app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => {
294 const fight = await loadMonsterFromFight(req.player.id);
295 const travelPlan = await getTravelPlan(req.player.id);
296 let closestTown = req.player.city_id;
299 closestTown = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
302 const equippedItems = await getEquippedItems(req.player.id);
304 const data: MonsterForFight = {
310 fight_trigger: fight.fight_trigger
312 const location = await getMonsterLocation(fight.ref_id);
315 res.send(renderPlayerBar(req.player, equippedItems) + renderFightPreRound(data, true, location, closestTown));
320 const chanceToSeeMonster = random(0, 100);
321 const things: any[] = [];
322 if(chanceToSeeMonster <= 30) {
323 const monster = await getRandomMonster([closestTown]);
324 things.push(monster);
328 const nextAction = cache[`step:${req.player.id}`] || 0;
330 res.send(renderPlayerBar(req.player, equippedItems) + renderTravel({
333 closestTown: closestTown,
339 // display the city info!
340 const [city, locations, paths] = await Promise.all([
341 getCityDetails(req.player.city_id),
342 getAllServices(req.player.city_id),
343 getAllPaths(req.player.city_id)
346 res.send(renderPlayerBar(req.player, equippedItems) + await renderMap({city, locations, paths}, closestTown));
352 // used to purchase equipment from a particular shop
353 app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
354 const item = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
357 logger.log(`Invalid item [${req.params.item_id}]`);
358 return res.sendStatus(400);
361 if(req.player.gold < item.cost) {
362 res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.cost.toLocaleString()}G to purchase this.`));
366 req.player.gold -= item.cost;
368 await updatePlayer(req.player);
369 await addInventoryItem(req.player.id, item);
371 const equippedItems = await getEquippedItems(req.player.id);
373 res.send(renderPlayerBar(req.player, equippedItems) + Alert.SuccessAlert(`You purchased ${item.name}`));
376 // used to purchase items from a particular shop
377 app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
378 const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
381 logger.log(`Invalid item [${req.params.item_id}]`);
382 return res.sendStatus(400);
385 if(req.player.gold < item.price_per_unit) {
386 res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.price_per_unit.toLocaleString()}G to purchase this.`));
390 req.player.gold -= item.price_per_unit;
392 await updatePlayer(req.player);
393 await givePlayerItem(req.player.id, item.id, 1);
395 const equippedItems = await getEquippedItems(req.player.id);
397 res.send(renderPlayerBar(req.player, equippedItems) + Alert.SuccessAlert(`You purchased a ${item.name}`));
400 // used to display equipment modals in a store, validates that
401 // the equipment is actually in this store before displaying
403 app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => {
404 const equipment = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
407 logger.log(`Invalid equipment [${req.params.item_id}]`);
408 return res.sendStatus(400);
413 <div class="item-modal-overview">
415 <img src="${equipment.icon ? `/assets/img/icons/equipment/${equipment.icon}` : 'https://via.placeholder.com/64x64'}" title="${equipment.name}" alt="${equipment.name}">
418 ${renderEquipmentDetails(equipment, req.player)}
421 <div class="actions">
422 <button hx-put="/location/${equipment.location_id}/equipment/${equipment.id}" formmethod="dialog" value="cancel" class="green">Buy</button>
423 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
431 // used to display item modals in a store, validates that
432 // the item is actually in this store before displaying
434 app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => {
435 const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
438 logger.log(`Invalid item [${req.params.item_id}]`);
439 return res.sendStatus(400);
444 <div class="item-modal-overview">
446 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}">
449 <h4>${item.name}</h4>
450 <p>${item.description}</p>
453 <div class="actions">
454 <button hx-put="/location/${item.location_id}/items/${item.id}" formmethod="dialog" value="cancel" class="red">Buy</button>
455 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
463 app.put('/item/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
464 const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
467 console.log(`Can't find item [${req.params.item_id}]`);
471 if(item.amount < 1) {
472 res.send(Alert.ErrorAlert(`You dont have enough ${item.name}`));
478 switch(item.effect_name) {
480 const hpGain = HealthPotionSmall.effect(req.player);
482 req.player.hp += hpGain;
484 if(req.player.hp > maxHp(req.player.constitution, req.player.level)) {
485 req.player.hp = maxHp(req.player.constitution, req.player.level);
490 await updateItemCount(req.player.id, item.item_id, -1);
491 await updatePlayer(req.player);
493 const inventory = await getInventory(req.player.id);
494 const equippedItems = inventory.filter(i => i.is_equipped);
495 const items = await getPlayersItems(req.player.id);
499 renderPlayerBar(req.player, equippedItems),
500 renderInventoryPage(inventory, items, 'ITEMS'),
501 Alert.SuccessAlert(`You used the ${item.name}`)
507 app.get('/modal/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
508 const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
511 logger.log(`Invalid item [${req.params.item_id}]`);
512 return res.sendStatus(400);
517 <div class="item-modal-overview">
519 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}">
522 <h4>${item.name}</h4>
523 <p>${item.description}</p>
526 <div class="actions">
527 <button hx-put="/item/${item.item_id}" formmethod="dialog" value="cancel" hx-target="#inventory" class="red">Use</button>
528 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
536 app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => {
537 const location = await getService(parseInt(req.params.location_id));
539 if(!location || location.city_id !== req.player.city_id) {
540 logger.log(`Invalid location: [${req.params.location_id}]`);
543 const [shopEquipment, shopItems] = await Promise.all([
544 listShopItems({location_id: location.id}),
545 getShopItems(location.id),
548 const html = await renderStore(shopEquipment, shopItems, req.player, location);
553 app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => {
554 const location = await getService(parseInt(req.params.location_id));
555 if(!location || location.city_id !== req.player.city_id) {
557 logger.log(`Invalid location: [${req.params.location_id}]`);
561 const monsters: Monster[] = await getMonsterList(location.id);
562 res.send(renderOnlyMonsterSelector(monsters, 0, location));
565 app.post('/travel', authEndpoint, async (req: AuthRequest, res: Response) => {
566 const destination_id = parseInt(req.body.destination_id);
568 if(!destination_id || isNaN(destination_id)) {
569 logger.log(`Invalid destination_id [${req.body.destination_id}]`);
570 return res.sendStatus(400);
573 const travelPlan = travel(req.player, req.body.destination_id);
575 res.json(travelPlan);
578 app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) => {
579 const fightBlockKey = `fightturn:${req.player.id}`;
581 if(cache[fightBlockKey] && cache[fightBlockKey] > Date.now()) {
582 res.status(429).send(Alert.ErrorAlert('Hmm, you are fight too quickly'));
587 cache[fightBlockKey] = Date.now() + CONSTANT.FIGHT_ATTACK_DELAY;
588 const monster = await loadMonsterWithFaction(req.player.id);
591 res.send(Alert.ErrorAlert('Not in a fight'));
595 const fightData = await fightRound(req.player, monster, {
596 action: req.body.action,
597 target: req.body.fightTarget
601 let html = renderFight(
603 renderRoundDetails(fightData.roundData),
604 fightData.roundData.winner === 'in-progress',
608 if(fightData.roundData.winner !== 'in-progress') {
609 delete cache[fightBlockKey];
612 if(fightData.monsters.length && monster.fight_trigger === 'explore') {
613 html += renderMonsterSelector(fightData.monsters, monster.ref_id);
616 let travelSection = '';
617 if(monster.fight_trigger === 'travel' && fightData.roundData.winner === 'player') {
618 // you're travellinga dn you won.. display the keep walking!
619 const travelPlan = await getTravelPlan(req.player.id);
620 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
621 travelSection = travelButton(0);
624 const equippedItems = await getEquippedItems(req.player.id);
625 const playerBar = renderPlayerBar(fightData.player, equippedItems);
627 res.send(html + travelSection + playerBar);
630 app.post('/fight', fightRateLimiter, authEndpoint, async (req: AuthRequest, res: Response) => {
631 if(req.player.hp <= 0) {
632 logger.log(`Player didn\'t have enough hp`);
633 return res.sendStatus(400);
636 const monsterId: number = req.body.monsterId;
637 const fightTrigger: FightTrigger = req.body.fightTrigger ?? 'travel';
640 logger.log(`Missing monster Id ${monsterId}`);
641 return res.sendStatus(400);
644 if(!fightTrigger || !['travel', 'explore'].includes(fightTrigger)) {
645 logger.log(`Invalid fight trigger [${fightTrigger}]`);
646 return res.sendStatus(400);
649 const monster = await loadMonster(monsterId);
652 logger.log(`Couldnt find monster for ${monsterId}`);
653 return res.sendStatus(400);
656 const fight = await createFight(req.player.id, monster, fightTrigger);
657 const location = await getService(monster.location_id);
660 const data: MonsterForFight = {
666 fight_trigger: fight.fight_trigger
669 res.send(renderFightPreRound(data, true, location, location.city_id));
672 app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) => {
673 const stepTimerKey = `step:${req.player.id}`;
675 const travelPlan = await getTravelPlan(req.player.id);
677 res.send(Alert.ErrorAlert('You don\'t have a travel plan'));
681 if(cache[stepTimerKey]) {
682 if(cache[stepTimerKey] > Date.now()) {
683 res.send(Alert.ErrorAlert('Hmm.. travelling too quickly'));
688 travelPlan.current_position++;
690 if(travelPlan.current_position >= travelPlan.total_distance) {
691 const travel = await completeTravel(req.player.id);
693 req.player.city_id = travel.destination_id;
694 await movePlayer(travel.destination_id, req.player.id);
696 const [city, locations, paths] = await Promise.all([
697 getCityDetails(travel.destination_id),
698 getAllServices(travel.destination_id),
699 getAllPaths(travel.destination_id)
702 delete cache[stepTimerKey];
703 res.send(await renderMap({city, locations, paths}, req.player.city_id));
706 const walkingText: string[] = [
707 'You take a step forward',
710 // update existing plan..
711 // decide if they will run into anything
712 const travelPlan = await stepForward(req.player.id);
714 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
716 const chanceToSeeMonster = random(0, 100);
717 const things: any[] = [];
718 if(chanceToSeeMonster <= 30) {
719 const monster = await getRandomMonster([closest]);
720 things.push(monster);
724 const nextAction = Date.now() + CONSTANT.STEP_DELAY;
726 cache[stepTimerKey] = nextAction;
728 res.send(renderTravel({
731 closestTown: closest,
732 walkingText: sample(walkingText),
739 app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res: Response) => {
740 // puts the player back in their starting town
741 // doesn't matter if they don't have one
743 await clearTravelPlan(req.player.id);
744 const equippedItems = await getEquippedItems(req.player.id);
746 const fight = await loadMonsterFromFight(req.player.id);
748 // go to the fight screen
749 const data: MonsterForFight = {
755 fight_trigger: fight.fight_trigger
757 const location = await getMonsterLocation(fight.ref_id);
759 res.send(renderPlayerBar(req.player, equippedItems) + renderFightPreRound(data, true, location, req.player.city_id));
762 const [city, locations, paths] = await Promise.all([
763 getCityDetails(req.player.city_id),
764 getAllServices(req.player.city_id),
765 getAllPaths(req.player.city_id)
768 res.send(renderPlayerBar(req.player, equippedItems) + await renderMap({city, locations, paths}, req.player.city_id));
774 app.post('/travel/:destination_id', authEndpoint, async (req: AuthRequest, res: Response) => {
775 if(req.player.hp <= 0) {
776 logger.log(`Player didn\'t have enough hp`);
777 res.send(Alert.ErrorAlert('Sorry, you need some HP to start travelling.'));
781 const destination = await getCityDetails(parseInt(req.params.destination_id));
784 res.send(Alert.ErrorAlert(`Thats not a valid desination`));
788 const travelPlan = await travel(req.player, destination.id);
790 res.send(renderTravel({
794 closestTown: req.player.city_id,
799 app.get('/settings', authEndpoint, async (req: AuthRequest, res: Response) => {
802 if(req.player.account_type === 'session') {
803 warning += `<div class="alert error">If you log out without signing up first, this account is lost forever.</div>`;
806 html += '<a href="#" hx-post="/logout">Logout</a>';
807 res.send(warning + html);
810 app.post('/logout', authEndpoint, async (req: AuthRequest, res: Response) => {
811 // ref to get the socket id for a particular player
812 cache.delete(`socket:${req.player.id}`);
813 // ref to get the player object
814 cache.delete(`token:${req.player.id}`);
816 logger.log(`${req.player.username} logged out`);
822 app.post('/auth', async (req: Request, res: Response) => {
823 if(req.body.authType === 'login') {
824 loginHandler(req, res);
826 else if(req.body.authType === 'signup') {
827 signupHandler(req, res);
830 logger.log(`Invalid auth type [${req.body.authType}]`);
836 async function signupHandler(req: Request, res: Response) {
837 const {username, password} = req.body;
838 const authToken = req.headers['x-authtoken'];
840 if(!username || !password || !authToken) {
841 res.send(Alert.ErrorAlert('Invalid username/password'));
846 const player = await loadPlayer(authToken.toString());
847 logger.log(`Attempted claim for ${player.username}`);
849 await signup(authToken.toString(), username, password);
851 await db('players').where({id: player.id}).update({
852 account_type: 'auth',
856 logger.log(`${username} claimed ${player.username}`);
858 io.emit('chat', broadcastMessage('server', `${player.username} is now ${username}`));
860 res.setHeader('hx-refresh', 'true');
865 if(e?.constraint === 'players_username_unique') {
866 res.send(Alert.ErrorAlert('That username is already taken'));
869 res.send(Alert.ErrorAlert('Please try again'));
874 async function loginHandler (req: Request, res: Response) {
875 const {username, password} = req.body;
876 if(!username || !username.length) {
877 res.send(Alert.ErrorAlert('Invalid username'));
880 if(!password || !password.length) {
881 res.send(Alert.ErrorAlert('Invalid password'));
886 const player = await login(username, password);
887 io.sockets.sockets.forEach(socket => {
888 if(socket.handshake.headers['x-authtoken'] === req.headers['x-authtoken']) {
889 bootstrapSocket(socket, player);
896 res.send(Alert.ErrorAlert('That user doesn\'t exist'));
900 server.listen(process.env.API_PORT, () => {
901 logger.log(`Listening on port ${process.env.API_PORT}`);