Further the login process

* Matchmaking login locks (created and checked only in memory for now)
* Profile reputation temporary implementation
* Profiles now no longer initialize if a user with the same username is found
* vrMovementMode in presence is now required, falls back to 'Teleport'
* Progression implementation began
* API routes: Settings, player subscriptions, reputation, progression
* cropSquare in image query is not a boolean, rather a number representing a boolean
* Hile reporting uses forms, not json
* Presence heartbeat and logout
* Socket changes: Close event listener (destroy), send message function, targets further started
This commit is contained in:
2025-03-29 23:09:40 -04:00
parent 1af0206b6a
commit 026f9c8bd8
22 changed files with 294 additions and 56 deletions

View File

@@ -1,10 +1,14 @@
import { APIUtils } from "../apiutils.ts";
import { route as VersionCheckRoute } from "./api/versioncheck.ts";
import { route as ConfigRoute } from "./api/config.ts";
import { route as GameConfig } from "./api/gameconfigs.ts";
import { route as PlayerReportingRoute } from "./api/PlayerReporting.ts";
import { route as MessagesRoute } from "./api/messages.ts";
import { route as RelationshipsRoute } from "./api/relationships.ts";
import { APIUtils } from "../apiutils.ts";
import { route as PlayersRoute } from "./api/players.ts"
import { route as SettingsRoute } from "./api/settings.ts";
import { route as PlayerSubscriptionsRoute } from "./api/playersubscriptions.ts";
import { route as PlayerReputationRoute } from "./api/playerReputation.ts";
export const route = APIUtils.createRouter("/api");
@@ -13,4 +17,8 @@ route.router.use(ConfigRoute.path, ConfigRoute.router);
route.router.use(GameConfig.path, GameConfig.router);
route.router.use(PlayerReportingRoute.path, PlayerReportingRoute.router);
route.router.use(MessagesRoute.path, MessagesRoute.router);
route.router.use(RelationshipsRoute.path, RelationshipsRoute.router);
route.router.use(RelationshipsRoute.path, RelationshipsRoute.router);
route.router.use(PlayersRoute.path, PlayersRoute.router);
route.router.use(SettingsRoute.path, SettingsRoute.router);
route.router.use(PlayerSubscriptionsRoute.path, PlayerSubscriptionsRoute.router);
route.router.use(PlayerReputationRoute.path, PlayerReputationRoute.router);

View File

@@ -18,7 +18,7 @@ const HileMessageSchema = z.object({
route.router.post('/v1/hile',
APIUtils.Authentication,
express.json(),
express.urlencoded({ extended: true }),
APIUtils.validateRequestBody(HileMessageSchema),
(rq: express.Request<NoBody, NoBody, HileMessage>, rs) => {

View File

@@ -0,0 +1,24 @@
import { APIUtils } from "../../apiutils.ts";
import UnifiedProfile from "../../data/profiles.ts";
import { AuthType } from "../../data/users.ts";
import express from "express";
export const route = APIUtils.createRouter("/playerReputation");
route.router.get('/v1/:id',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
(rq: express.Request<{ id: string }>, rs) => {
const unparsedPlayerId = rq.params.id;
const parsedPlayerId = parseInt(unparsedPlayerId);
if (isNaN(parsedPlayerId)) {
rs.json(APIUtils.genericResponseFormat(true, 'The player ID was invalid.'));
return;
}
rs.json(UnifiedProfile.get(parsedPlayerId).Reputation.getReputation());
}
);

View File

@@ -1,4 +1,9 @@
import Logging from "@proxnet/undead-logging";
import { APIUtils } from "../../apiutils.ts";
import express from "express";
import UnifiedProfile from "../../data/profiles.ts";
const log = new Logging("ProgressionRoute");
export const route = APIUtils.createRouter("/players");
@@ -6,12 +11,22 @@ route.router.get('/v1/progression/:id',
APIUtils.Authentication,
async (_rq, rs) => {
rs.json({
PlayerId: rs.locals.profile.getId(),
Level: await rs.locals.profile.Progression.getLevel(), // await is temporary
Xp: await rs.locals.profile.Progression.getXp()
});
async (rq: express.Request<{ id: string }>, rs) => {
const unparsedPlayerId = rq.params.id;
const parsedPlayerId = parseInt(unparsedPlayerId);
if (isNaN(parsedPlayerId)) {
rs.json(APIUtils.genericResponseFormat(true, 'The player ID was invalid.'));
return;
}
const profile = UnifiedProfile.get(parsedPlayerId);
const res = {
PlayerId: profile.getId(),
Level: await profile.Progression.getLevel(),
XP: await profile.Progression.getXp()
};
log.d(`prog res: ${JSON.stringify(res)}`);
rs.json(res);
}
);

View File

@@ -0,0 +1,11 @@
import { APIUtils } from "../../apiutils.ts";
export const route = APIUtils.createRouter("/playersubscriptions");
route.router.get('/v1/my',
(_rq, rs) => {
rs.json([]); // temporary: todo
}
);

View File

@@ -0,0 +1,22 @@
import Logging from "@proxnet/undead-logging";
import { APIUtils } from "../../apiutils.ts";
import { AuthType } from "../../data/users.ts";
const log = new Logging("SettingsRoute");
export const route = APIUtils.createRouter("/settings");
route.router.get('/v2',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
async (_rq, rs) => {
const settings = await rs.locals.profile.Settings.getSettings();
log.d(`settings res: ${JSON.stringify(settings)}`);
rs.json(settings);
}
);

View File

@@ -44,10 +44,11 @@ route.router.get(
}
image = await Image.decode(imageSource);
let cropSquare: boolean = false;
let cropSquare: number | null = null;
if (typeof rq.query.cropSquare == "string") {
const d = JSON.parse(rq.query.cropSquare);
if (typeof d == "boolean" && d) cropSquare = true;
const num = parseInt(rq.query.cropSquare);
if (isNaN(num)) cropSquare = null;
else cropSquare = num;
}
let width: number | null = null;
if (typeof rq.query.width == "string") {
@@ -84,7 +85,7 @@ route.router.get(
}
} else if (width) image.resize(width, Image.RESIZE_AUTO);
else if (height) image.resize(Image.RESIZE_AUTO, height);
if (cropSquare) {
if (cropSquare == 1) {
if (image.width > image.height) {
image.crop(
Math.round(image.width / 2) - Math.round(image.height / 2),
@@ -100,6 +101,7 @@ route.router.get(
);}
}
rs.setHeader('content-signature', 'key-id=KEY:RSA:p1.rec.net; data=aGk='); // enable image signature patch on client
rs.type("png").send(Buffer.from(await image.encode()));
},
);

View File

@@ -4,6 +4,9 @@ import express from "express";
import Matchmaking from "../../data/live/base.ts";
import Presence from "../../data/live/presence.ts";
import { AuthType } from "../../data/users.ts";
import Logging from "@proxnet/undead-logging";
const log = new Logging("MatchPlayerRoute");
export const route = APIUtils.createRouter('/player');
@@ -28,4 +31,33 @@ route.router.post('/login',
rs.sendStatus(200);
},
)
);
route.router.post('/logout',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
express.urlencoded({extended: true}),
APIUtils.validateRequestBody(LoginSchema),
(rq, rs) => {
Matchmaking.deleteLoginLock(rs.locals.profile);
}
)
route.router.post('/heartbeat',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
express.urlencoded({extended: true}),
APIUtils.validateRequestBody(LoginSchema),
APIUtils.LoginLock,
async (_rq, rs) => {
const pres = await Presence.get(rs.locals.profile);
log.d(`pres heartbeat for ${rs.locals.profile.getId()}: ${JSON.stringify(await pres.export())}`);
rs.json(await pres.export());
}
);

View File

@@ -40,7 +40,7 @@ const AuthRequestRootSchema = z.object({
pubkey: z.string(),
});
const rateLimit = new APIUtils.RateLimiter(60, 1);
const rateLimit = new APIUtils.RateLimiter(60, 2);
route.router.post("/auth",
@@ -57,9 +57,7 @@ route.router.post("/auth",
}
if (rq.body.message.server_id !== config.public.serverId) {
log.w(
`Auth request failed (serverId mismatch), config error?\n given ID: '${rq.body.message.server_id}'\n our ID: '${config.public.serverId}'`,
);
log.w(`Auth request failed (serverId mismatch), config error?\n given ID: '${rq.body.message.server_id}'\n our ID: '${config.public.serverId}'`);
authFailed("Authentication request not intended for this server.");
return;
}
@@ -114,9 +112,7 @@ route.router.post("/auth",
} else user = obj;
}
if (!(await user.addNonce(rq.body.message.nonce))) {
log.w(
`Client '${rq.body.client_id}' has already used nonce. Replay attack?`,
);
log.w(`Client '${rq.body.client_id}' has already used nonce. Replay attack?`);
authFailed("Authentication request failed.");
return;
}