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, maxVigor, 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';
33 import { router as repairRouter } from './locations/repair';
35 import * as Alert from './views/alert';
36 import { renderPlayerBar } from './views/player-bar'
37 import { renderEquipmentDetails, renderStore } from './views/stores';
38 import { renderMap } from './views/map';
39 import { renderProfilePage } from './views/profile';
40 import { renderSkills } from './views/skills';
41 import { renderInventoryPage } from './views/inventory';
42 import { renderMonsterSelector, renderOnlyMonsterSelector } from './views/monster-selector';
43 import { renderFight, renderFightPreRound, renderRoundDetails } from './views/fight';
44 import { renderTravel, travelButton } from './views/travel';
45 import { renderChatMessage } from './views/chat';
48 import { createMonsters } from '../../seeds/monsters';
49 import { createAllCitiesAndLocations } from '../../seeds/cities';
50 import { createShopItems, createShopEquipment } from '../../seeds/shop_items';
51 import { Item, PlayerItem, ShopItem } from 'shared/items';
52 import { equip, unequip } from './equipment';
53 import { HealthPotionSmall } from '../shared/items/health_potion';
59 const app = express();
60 const server = http.createServer(app);
62 app.use(express.static(join(__dirname, '..', '..', 'public')));
63 app.use(bodyParser.urlencoded({ extended: true }));
64 app.use(express.json());
66 const io = new Server(server);
68 const cache = new Map<string, any>();
69 const chatHistory: Message[] = [];
71 app.use((req, res, next) => {
72 console.log(req.method, req.url);
76 const fightRateLimiter = rateLimit({
77 windowMs: parseInt(process.env.RATE_LIMIT_WINDOW || '30000'),
78 max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '20'),
79 standardHeaders: true,
81 handler: (req, res, next, options) => {
82 logger.log(`Blocked request: [${req.headers['x-authtoken']}: ${req.method} ${req.path}]`);
83 res.status(options.statusCode).send(options.message);
87 async function bootstrapSocket(socket: Socket, player: Player) {
88 // ref to get the socket id for a particular player
89 cache.set(`socket:${player.id}`, socket.id);
90 // ref to get the player object
91 cache.set(`token:${player.id}`, player);
93 socket.emit('authToken', player.id);
95 socket.emit('chat', renderChatMessage(broadcastMessage('server', `${player.username} just logged in`)));
98 io.on('connection', async socket => {
99 logger.log(`socket ${socket.id} connected, authToken: ${socket.handshake.headers['x-authtoken']}`);
101 let authToken = socket.handshake.headers['x-authtoken'].toString() === 'null' ? null : socket.handshake.headers['x-authtoken'].toString();
105 logger.log(`Attempting to load player with id ${authToken}`);
106 player = await loadPlayer(authToken);
109 logger.log(`Creating player`);
110 player = await createPlayer();
111 authToken = player.id;
112 socket.handshake.headers['x-authtoken'] = authToken;
115 logger.log(`Socket [${socket.id}] auth token: ${player.id}`);
117 bootstrapSocket(socket, player);
119 socket.on('disconnect', () => {
120 console.log(`Player ${player.username} left`);
121 io.emit('status', `${io.sockets.sockets.size} Online (v${version})`);
125 io.emit('status', `${io.sockets.sockets.size} Online (v${version})`);
126 // this is a special event to let the client know it can start
128 socket.emit('ready');
132 app.use(healerRouter);
133 app.use(professionRouter);
134 app.use(repairRouter);
137 app.get('/chat/history', authEndpoint, async (req: AuthRequest, res: Response) => {
138 let html = chatHistory.map(renderChatMessage);
140 res.send(html.join("\n"));
143 app.post('/chat', authEndpoint, async (req: AuthRequest, res: Response) => {
144 const msg = req.body.message.trim();
146 if(!msg || !msg.length) {
151 let message: Message;
152 if(msg.startsWith('/server lmnop')) {
153 if(msg === '/server lmnop refresh-monsters') {
154 await createMonsters();
155 message = broadcastMessage('server', 'Monster refresh!');
157 else if(msg === '/server lmnop refresh-cities') {
158 await createAllCitiesAndLocations();
159 message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!');
161 else if(msg === '/server lmnop refresh-shops') {
162 await createShopItems();
163 await createShopEquipment();
164 message = broadcastMessage('server', 'Refresh shop items');
167 const str = msg.split('/server lmnop ')[1];
169 message = broadcastMessage('server', str);
174 message = broadcastMessage(req.player.username, xss(msg, {
177 chatHistory.push(message);
178 chatHistory.slice(-10);
182 io.emit('chat', renderChatMessage(message));
187 app.get('/player', authEndpoint, async (req: AuthRequest, res: Response) => {
188 const equipment = await getEquippedItems(req.player.id);
189 res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
192 app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Response) => {
193 const equipment = await getEquippedItems(req.player.id);
194 const stat = req.params.stat;
195 if(!['strength', 'constitution', 'dexterity', 'intelligence'].includes(stat)) {
196 res.send(Alert.ErrorAlert(`Sorry, that's not a valid stat to increase`));
200 if(req.player.stat_points <= 0) {
201 res.send(Alert.ErrorAlert(`Sorry, you don't have enough stat points`));
205 req.player.stat_points -= 1;
208 req.player.hp = maxHp(req.player.constitution, req.player.level);
209 req.player.vigor = maxVigor(req.player.constitution, req.player.level);
210 updatePlayer(req.player);
212 res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
215 app.get('/player/skills', authEndpoint, async (req: AuthRequest, res: Response) => {
216 const skills = await getPlayerSkills(req.player.id);
218 res.send(renderSkills(skills));
221 app.get('/player/inventory', authEndpoint, async (req: AuthRequest, res: Response) => {
222 const [inventory, items] = await Promise.all([
223 getInventory(req.player.id),
224 getPlayersItems(req.player.id)
227 res.send(renderInventoryPage(inventory, items));
230 app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => {
231 const inventoryItem = await getInventoryItem(req.player.id, req.params.item_id);
232 const equippedItems = await getEquippedItems(req.player.id);
233 const requestedSlot = req.params.slot;
234 let desiredSlot: EquipmentSlot = inventoryItem.equipment_slot;
237 // handes the situation where you're trying to equip an item
238 // that can be equipped to any hand
239 if(inventoryItem.equipment_slot === 'ANY_HAND') {
240 if(requestedSlot === 'LEFT_HAND' || requestedSlot === 'RIGHT_HAND') {
241 // get the players current equipment in that slot!
242 if(equippedItems.some(v => {
243 return v.equipment_slot === requestedSlot || v.equipment_slot === 'TWO_HANDED';
248 desiredSlot = requestedSlot;
253 if(requestedSlot === 'TWO_HANDED') {
254 if(equippedItems.some(v => {
255 return v.equipment_slot === 'LEFT_HAND' || v.equipment_slot === 'RIGHT_HAND';
262 await equip(req.player.id, inventoryItem, desiredSlot);
263 const socketId = cache.get(`socket:${req.player.id}`).toString();
264 io.to(socketId).emit('updatePlayer', req.player);
265 io.to(socketId).emit('alert', {
267 text: `You equipped your ${inventoryItem.name}`
274 const [inventory, items] = await Promise.all([
275 getInventory(req.player.id),
276 getPlayersItems(req.player.id)
279 res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player));
282 app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => {
283 const [item, ] = await Promise.all([
284 getInventoryItem(req.player.id, req.params.item_id),
285 unequip(req.player.id, req.params.item_id)
288 const [inventory, items] = await Promise.all([
289 getInventory(req.player.id),
290 getPlayersItems(req.player.id)
293 res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player));
296 app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => {
297 const fight = await loadMonsterFromFight(req.player.id);
298 const travelPlan = await getTravelPlan(req.player.id);
299 let closestTown = req.player.city_id;
302 closestTown = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
306 const data: MonsterForFight = {
312 fight_trigger: fight.fight_trigger
314 const location = await getMonsterLocation(fight.ref_id);
317 res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, closestTown));
322 const chanceToSeeMonster = random(0, 100);
323 const things: any[] = [];
324 if(chanceToSeeMonster <= 30) {
325 const monster = await getRandomMonster([closestTown]);
326 things.push(monster);
330 const nextAction = cache[`step:${req.player.id}`] || 0;
332 res.send(renderPlayerBar(req.player) + renderTravel({
335 closestTown: closestTown,
341 // display the city info!
342 const [city, locations, paths] = await Promise.all([
343 getCityDetails(req.player.city_id),
344 getAllServices(req.player.city_id),
345 getAllPaths(req.player.city_id)
348 res.send(renderPlayerBar(req.player) + await renderMap({city, locations, paths}, closestTown));
354 // used to purchase equipment from a particular shop
355 app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
356 const item = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
359 logger.log(`Invalid item [${req.params.item_id}]`);
360 return res.sendStatus(400);
363 if(req.player.gold < item.cost) {
364 res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.cost.toLocaleString()}G to purchase this.`));
368 req.player.gold -= item.cost;
370 await updatePlayer(req.player);
371 await addInventoryItem(req.player.id, item);
373 res.send(renderPlayerBar(req.player) + 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 res.send(renderPlayerBar(req.player) + Alert.SuccessAlert(`You purchased a ${item.name}`));
398 // used to display equipment modals in a store, validates that
399 // the equipment is actually in this store before displaying
401 app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => {
402 const equipment = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
405 logger.log(`Invalid equipment [${req.params.item_id}]`);
406 return res.sendStatus(400);
411 <div class="item-modal-overview">
413 <img src="${equipment.icon ? `/assets/img/icons/equipment/${equipment.icon}` : 'https://via.placeholder.com/64x64'}" title="${equipment.name}" alt="${equipment.name}">
416 ${renderEquipmentDetails(equipment, req.player)}
419 <div class="actions">
420 <button hx-put="/location/${equipment.location_id}/equipment/${equipment.id}" formmethod="dialog" value="cancel" class="green">Buy</button>
421 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
429 // used to display item modals in a store, validates that
430 // the item is actually in this store before displaying
432 app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => {
433 const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
436 logger.log(`Invalid item [${req.params.item_id}]`);
437 return res.sendStatus(400);
442 <div class="item-modal-overview">
444 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}">
447 <h4>${item.name}</h4>
448 <p>${item.description}</p>
451 <div class="actions">
452 <button hx-put="/location/${item.location_id}/items/${item.id}" formmethod="dialog" value="cancel" class="red">Buy</button>
453 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
461 app.put('/item/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
462 const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
465 console.log(`Can't find item [${req.params.item_id}]`);
469 if(item.amount < 1) {
470 res.send(Alert.ErrorAlert(`You dont have enough ${item.name}`));
476 switch(item.effect_name) {
478 const hpGain = HealthPotionSmall.effect(req.player);
480 req.player.hp += hpGain;
482 if(req.player.hp > maxHp(req.player.constitution, req.player.level)) {
483 req.player.hp = maxHp(req.player.constitution, req.player.level);
488 await updateItemCount(req.player.id, item.item_id, -1);
489 await updatePlayer(req.player);
491 const inventory = await getInventory(req.player.id);
492 const items = await getPlayersItems(req.player.id);
496 renderPlayerBar(req.player),
497 renderInventoryPage(inventory, items, 'ITEMS'),
498 Alert.SuccessAlert(`You used the ${item.name}`)
504 app.get('/modal/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
505 const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
508 logger.log(`Invalid item [${req.params.item_id}]`);
509 return res.sendStatus(400);
514 <div class="item-modal-overview">
516 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}">
519 <h4>${item.name}</h4>
520 <p>${item.description}</p>
523 <div class="actions">
524 <button hx-put="/item/${item.item_id}" formmethod="dialog" value="cancel" hx-target="#inventory" class="red">Use</button>
525 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
533 app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => {
534 const location = await getService(parseInt(req.params.location_id));
536 if(!location || location.city_id !== req.player.city_id) {
537 logger.log(`Invalid location: [${req.params.location_id}]`);
540 const [shopEquipment, shopItems] = await Promise.all([
541 listShopItems({location_id: location.id}),
542 getShopItems(location.id),
545 const html = await renderStore(shopEquipment, shopItems, req.player, location);
550 app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => {
551 const location = await getService(parseInt(req.params.location_id));
552 if(!location || location.city_id !== req.player.city_id) {
554 logger.log(`Invalid location: [${req.params.location_id}]`);
558 const monsters: Monster[] = await getMonsterList(location.id);
559 res.send(renderOnlyMonsterSelector(monsters, 0, location));
562 app.post('/travel', authEndpoint, async (req: AuthRequest, res: Response) => {
563 const destination_id = parseInt(req.body.destination_id);
565 if(!destination_id || isNaN(destination_id)) {
566 logger.log(`Invalid destination_id [${req.body.destination_id}]`);
567 return res.sendStatus(400);
570 const travelPlan = travel(req.player, req.body.destination_id);
572 res.json(travelPlan);
575 app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) => {
576 const fightBlockKey = `fightturn:${req.player.id}`;
578 if(cache[fightBlockKey] && cache[fightBlockKey] > Date.now()) {
579 res.status(429).send(Alert.ErrorAlert('Hmm, you are fight too quickly'));
584 cache[fightBlockKey] = Date.now() + CONSTANT.FIGHT_ATTACK_DELAY;
585 const monster = await loadMonsterWithFaction(req.player.id);
588 res.send(Alert.ErrorAlert('Not in a fight'));
592 const fightData = await fightRound(req.player, monster, {
593 action: req.body.action,
594 target: req.body.fightTarget
598 let html = renderFight(
600 renderRoundDetails(fightData.roundData),
601 fightData.roundData.winner === 'in-progress',
605 if(fightData.roundData.winner !== 'in-progress') {
606 delete cache[fightBlockKey];
609 if(fightData.monsters.length && monster.fight_trigger === 'explore') {
610 html += renderMonsterSelector(fightData.monsters, monster.ref_id);
613 let travelSection = '';
614 if(monster.fight_trigger === 'travel' && fightData.roundData.winner === 'player') {
615 // you're travellinga dn you won.. display the keep walking!
616 const travelPlan = await getTravelPlan(req.player.id);
617 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
618 travelSection = travelButton(0);
621 const playerBar = renderPlayerBar(fightData.player);
623 res.send(html + travelSection + playerBar);
626 app.post('/fight', fightRateLimiter, authEndpoint, async (req: AuthRequest, res: Response) => {
627 if(req.player.hp <= 0) {
628 logger.log(`Player didn\'t have enough hp`);
629 return res.sendStatus(400);
632 const monsterId: number = req.body.monsterId;
633 const fightTrigger: FightTrigger = req.body.fightTrigger ?? 'travel';
636 logger.log(`Missing monster Id ${monsterId}`);
637 return res.sendStatus(400);
640 if(!fightTrigger || !['travel', 'explore'].includes(fightTrigger)) {
641 logger.log(`Invalid fight trigger [${fightTrigger}]`);
642 return res.sendStatus(400);
645 const monster = await loadMonster(monsterId);
648 logger.log(`Couldnt find monster for ${monsterId}`);
649 return res.sendStatus(400);
652 const fight = await createFight(req.player.id, monster, fightTrigger);
653 const location = await getService(monster.location_id);
656 const data: MonsterForFight = {
662 fight_trigger: fight.fight_trigger
665 res.send(renderFightPreRound(data, true, location, location.city_id));
668 app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) => {
669 const stepTimerKey = `step:${req.player.id}`;
671 const travelPlan = await getTravelPlan(req.player.id);
673 res.send(Alert.ErrorAlert('You don\'t have a travel plan'));
677 if(cache[stepTimerKey]) {
678 if(cache[stepTimerKey] > Date.now()) {
679 res.send(Alert.ErrorAlert('Hmm.. travelling too quickly'));
684 travelPlan.current_position++;
686 if(travelPlan.current_position >= travelPlan.total_distance) {
687 const travel = await completeTravel(req.player.id);
689 req.player.city_id = travel.destination_id;
690 await movePlayer(travel.destination_id, req.player.id);
692 const [city, locations, paths] = await Promise.all([
693 getCityDetails(travel.destination_id),
694 getAllServices(travel.destination_id),
695 getAllPaths(travel.destination_id)
698 delete cache[stepTimerKey];
699 res.send(await renderMap({city, locations, paths}, req.player.city_id));
702 const walkingText: string[] = [
703 'You take a step forward',
706 // update existing plan..
707 // decide if they will run into anything
708 const travelPlan = await stepForward(req.player.id);
710 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
712 const chanceToSeeMonster = random(0, 100);
713 const things: any[] = [];
714 if(chanceToSeeMonster <= 30) {
715 const monster = await getRandomMonster([closest]);
716 things.push(monster);
720 const nextAction = Date.now() + CONSTANT.STEP_DELAY;
722 cache[stepTimerKey] = nextAction;
724 res.send(renderTravel({
727 closestTown: closest,
728 walkingText: sample(walkingText),
735 app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res: Response) => {
736 // puts the player back in their starting town
737 // doesn't matter if they don't have one
739 await clearTravelPlan(req.player.id);
741 const fight = await loadMonsterFromFight(req.player.id);
743 // go to the fight screen
744 const data: MonsterForFight = {
750 fight_trigger: fight.fight_trigger
752 const location = await getMonsterLocation(fight.ref_id);
754 res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, req.player.city_id));
757 const [city, locations, paths] = await Promise.all([
758 getCityDetails(req.player.city_id),
759 getAllServices(req.player.city_id),
760 getAllPaths(req.player.city_id)
763 res.send(renderPlayerBar(req.player) + await renderMap({city, locations, paths}, req.player.city_id));
769 app.post('/travel/:destination_id', authEndpoint, async (req: AuthRequest, res: Response) => {
770 if(req.player.hp <= 0) {
771 logger.log(`Player didn\'t have enough hp`);
772 res.send(Alert.ErrorAlert('Sorry, you need some HP to start travelling.'));
776 const destination = await getCityDetails(parseInt(req.params.destination_id));
779 res.send(Alert.ErrorAlert(`Thats not a valid desination`));
783 const travelPlan = await travel(req.player, destination.id);
785 res.send(renderTravel({
789 closestTown: req.player.city_id,
794 app.get('/settings', authEndpoint, async (req: AuthRequest, res: Response) => {
797 if(req.player.account_type === 'session') {
798 warning += `<div class="alert error">If you log out without signing up first, this account is lost forever.</div>`;
801 html += '<a href="#" hx-post="/logout">Logout</a>';
802 res.send(warning + html);
805 app.post('/logout', authEndpoint, async (req: AuthRequest, res: Response) => {
806 // ref to get the socket id for a particular player
807 cache.delete(`socket:${req.player.id}`);
808 // ref to get the player object
809 cache.delete(`token:${req.player.id}`);
811 logger.log(`${req.player.username} logged out`);
817 app.post('/auth', async (req: Request, res: Response) => {
818 if(req.body.authType === 'login') {
819 loginHandler(req, res);
821 else if(req.body.authType === 'signup') {
822 signupHandler(req, res);
825 logger.log(`Invalid auth type [${req.body.authType}]`);
831 async function signupHandler(req: Request, res: Response) {
832 const {username, password} = req.body;
833 const authToken = req.headers['x-authtoken'];
835 if(!username || !password || !authToken) {
836 res.send(Alert.ErrorAlert('Invalid username/password'));
841 const player = await loadPlayer(authToken.toString());
842 logger.log(`Attempted claim for ${player.username}`);
844 await signup(authToken.toString(), username, password);
846 await db('players').where({id: player.id}).update({
847 account_type: 'auth',
851 logger.log(`${username} claimed ${player.username}`);
853 io.emit('chat', broadcastMessage('server', `${player.username} is now ${username}`));
855 res.setHeader('hx-refresh', 'true');
860 if(e?.constraint === 'players_username_unique') {
861 res.send(Alert.ErrorAlert('That username is already taken'));
864 res.send(Alert.ErrorAlert('Please try again'));
869 async function loginHandler (req: Request, res: Response) {
870 const {username, password} = req.body;
871 if(!username || !username.length) {
872 res.send(Alert.ErrorAlert('Invalid username'));
875 if(!password || !password.length) {
876 res.send(Alert.ErrorAlert('Invalid password'));
881 const player = await login(username, password);
882 io.sockets.sockets.forEach(socket => {
883 if(socket.handshake.headers['x-authtoken'] === req.headers['x-authtoken']) {
884 bootstrapSocket(socket, player);
891 res.send(Alert.ErrorAlert('That user doesn\'t exist'));
895 server.listen(process.env.API_PORT, () => {
896 logger.log(`Listening on port ${process.env.API_PORT}`);