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:
@@ -6,6 +6,7 @@ import { Config } from "./config.ts";
|
|||||||
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
|
import { AuthType, User, UserTokenFormat } from "./data/users.ts";
|
||||||
import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts";
|
import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
import Matchmaking from "./data/live/base.ts";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
@@ -311,6 +312,21 @@ export function AuthenticationType(type: AuthType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LoginLock(rq: express.Request<NoBody, NoBody, { LoginLock: string }>, rs: express.Response, nxt: express.NextFunction) {
|
||||||
|
log.d(`LoginLock for ${rs.locals.profile.getId()}: ${rq.body.LoginLock}`);
|
||||||
|
const matches = Matchmaking.lockMatches(rs.locals.profile, rq.body.LoginLock);
|
||||||
|
if (matches == null) {
|
||||||
|
rs.json(genericResponseFormat(true, "Login Lock failure"));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (matches) nxt();
|
||||||
|
else {
|
||||||
|
rs.json(genericResponseFormat(true, "Login Lock failure"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type NoBody = Record<string | number | symbol, never>;
|
export type NoBody = Record<string | number | symbol, never>;
|
||||||
|
|
||||||
export * as APIUtils from "./apiutils.ts";
|
export * as APIUtils from "./apiutils.ts";
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ class MatchmakingBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lockMatches(prof: Profile, lock: string) {
|
lockMatches(prof: Profile, lock: string) {
|
||||||
const maybeLock = loginLocks.get(prof.getId());
|
const checkLock = loginLocks.get(prof.getId());
|
||||||
if (maybeLock) return maybeLock == lock;
|
if (checkLock) return checkLock == lock;
|
||||||
else return false;
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteLoginLock(prof: Profile) {
|
deleteLoginLock(prof: Profile) {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class PlayerPresence {
|
|||||||
playerId: number;
|
playerId: number;
|
||||||
statusVisibility: PlayerStatusVisibility;
|
statusVisibility: PlayerStatusVisibility;
|
||||||
deviceClass: DeviceClass;
|
deviceClass: DeviceClass;
|
||||||
vrMovementMode: VRMovementMode | undefined;
|
vrMovementMode: VRMovementMode;
|
||||||
roomInstance: RoomInstance | null;
|
roomInstance: RoomInstance | null;
|
||||||
|
|
||||||
lastSeen: Date;
|
lastSeen: Date;
|
||||||
@@ -85,9 +85,9 @@ class PlayerPresence {
|
|||||||
roomInstance: this.roomInstance,
|
roomInstance: this.roomInstance,
|
||||||
statusVisibility: this.statusVisibility,
|
statusVisibility: this.statusVisibility,
|
||||||
deviceClass: this.deviceClass,
|
deviceClass: this.deviceClass,
|
||||||
vrMovementMode: this.vrMovementMode ? this.vrMovementMode : undefined
|
vrMovementMode: this.vrMovementMode
|
||||||
}
|
}
|
||||||
return Object.assign({}, exp); // hard copy/clone
|
return Object.assign({}, exp); // hard clone
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOffline() {
|
updateOffline() {
|
||||||
|
|||||||
5
src/data/profile/base/events.ts
Normal file
5
src/data/profile/base/events.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export class ProfileEventsManager {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,35 +1,79 @@
|
|||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { Config } from "../../config.ts";
|
import { Config } from "../../config.ts";
|
||||||
import { GameConfigs } from "../config.ts";
|
import { GameConfigs } from "../config.ts";
|
||||||
import { ProfileContentManager } from "./profilemanagerbase.ts";
|
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
||||||
|
import { Redis } from "../../db.ts";
|
||||||
|
|
||||||
|
const log = new Logging("ProfileProgression");
|
||||||
|
|
||||||
const serverConfig = Config.getConfig();
|
|
||||||
const config = GameConfigs.getConfig();
|
const config = GameConfigs.getConfig();
|
||||||
/**
|
|
||||||
* Level -> Required XP
|
|
||||||
*/
|
|
||||||
const requiredXpMap: Map<number, number> = new Map();
|
|
||||||
|
|
||||||
export class ProfileProgressionManager extends ProfileContentManager {
|
export class ProfileProgressionManager extends ProfileContentManager {
|
||||||
|
|
||||||
constructor() {
|
async #getNextLevelRequiredXp() {
|
||||||
super();
|
const xp = await this.getXp();
|
||||||
// fill `requiredXpMap` using `config.public` values
|
if (typeof config?.LevelProgressionMaps == 'undefined') return null;
|
||||||
}
|
for (const item of config?.LevelProgressionMaps) {
|
||||||
|
if (xp >= item.RequiredXp) {
|
||||||
|
|
||||||
#getRequiredXp(level: number) {
|
const next = config?.LevelProgressionMaps[config?.LevelProgressionMaps.indexOf(item) + 1];
|
||||||
if (level > serverConfig.public.maxLevels) return null;
|
if (typeof next == 'undefined') return null;
|
||||||
else {
|
else return next.RequiredXp;
|
||||||
const req = requiredXpMap.get(level);
|
|
||||||
return req ? req : null;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getLevel() {
|
/**
|
||||||
return 30; // temporary
|
* Set the profile's exact # of XP
|
||||||
|
* @returns The new # of XP
|
||||||
|
*/
|
||||||
|
async setXp(xp: number) {
|
||||||
|
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId!.toString(), Redis.KeyGroups.Profiles.Xp), xp.toString());
|
||||||
|
return xp;
|
||||||
}
|
}
|
||||||
|
|
||||||
getXp() {
|
/**
|
||||||
return 0; // temporary
|
* Adds an integer to the profile's # of XP
|
||||||
|
* @returns The new # of XP; returns 0 if data is in error
|
||||||
|
*/
|
||||||
|
async addXp(xp: number) {
|
||||||
|
const currentXp = await this.getXp();
|
||||||
|
if (currentXp == null) return 0;
|
||||||
|
await this.setXp(currentXp + xp);
|
||||||
|
return currentXp + xp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the player's current level
|
||||||
|
* @returns The player's level based on their current XP; returns 1 if data is in error
|
||||||
|
*/
|
||||||
|
async getLevel() {
|
||||||
|
const xp = await this.getXp();
|
||||||
|
if (xp == null) return 1; // fallback since progression data is required
|
||||||
|
|
||||||
|
if (typeof config?.LevelProgressionMaps == 'undefined') return null;
|
||||||
|
for (const item of config?.LevelProgressionMaps) {
|
||||||
|
if (xp >= item.RequiredXp) {
|
||||||
|
|
||||||
|
const current = config?.LevelProgressionMaps[config?.LevelProgressionMaps.indexOf(item)];
|
||||||
|
if (typeof current == 'undefined') return null;
|
||||||
|
else return current.Level;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getXp() {
|
||||||
|
if (!this.profileIsSet()) throw this.profileNotSetError;
|
||||||
|
let data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId!.toString(), Redis.KeyGroups.Profiles.Xp));
|
||||||
|
if (data == null) data = (await this.setXp(0)).toString();
|
||||||
|
|
||||||
|
const parsedData = parseInt(data);
|
||||||
|
if (isNaN(parsedData)) {
|
||||||
|
log.w(`Parsed xp data for ${this.profileId} is NaN!`);
|
||||||
|
return 0; // fallback since progression data is required
|
||||||
|
} else return parsedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
20
src/data/profile/reputation.ts
Normal file
20
src/data/profile/reputation.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
||||||
|
|
||||||
|
export class ProfileReputationManager extends ProfileContentManager {
|
||||||
|
|
||||||
|
async getReputation() { // async temporary
|
||||||
|
return {
|
||||||
|
AccountId: this.profileId,
|
||||||
|
Noterity: 0.0,
|
||||||
|
CheerGeneral: 0,
|
||||||
|
CheerHelpful: 0,
|
||||||
|
CheerSportsman: 0,
|
||||||
|
CheerCreative: 0,
|
||||||
|
CheerCredit: 0,
|
||||||
|
SubscriberCount: 0,
|
||||||
|
SubscribedCount: 0,
|
||||||
|
SelectedCheer: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Redis } from "../../db.ts";
|
import { Redis } from "../../db.ts";
|
||||||
import { SettingKey } from "../content/settings.ts";
|
import { SettingKey } from "../content/settings.ts";
|
||||||
import { ProfileContentManager } from "./profilemanagerbase.ts";
|
import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
||||||
|
|
||||||
export interface Setting {
|
export interface Setting {
|
||||||
Key: string;
|
Key: string;
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import { z } from "zod";
|
|||||||
import { SignalRSocketHandler } from "../socket/socket.ts";
|
import { SignalRSocketHandler } from "../socket/socket.ts";
|
||||||
import { ProfileSettingsManager } from "./profile/settings.ts";
|
import { ProfileSettingsManager } from "./profile/settings.ts";
|
||||||
import { ProfileProgressionManager } from "./profile/progression.ts";
|
import { ProfileProgressionManager } from "./profile/progression.ts";
|
||||||
|
import { ProfileReputationManager } from "./profile/reputation.ts";
|
||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
|
const log = new Logging("Profiles");
|
||||||
|
|
||||||
interface ProfileInitOptions {
|
interface ProfileInitOptions {
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
@@ -79,6 +83,11 @@ class Profile {
|
|||||||
static async init(options?: ProfileInitOptions) {
|
static async init(options?: ProfileInitOptions) {
|
||||||
const optionsSpecified = typeof options !== "undefined";
|
const optionsSpecified = typeof options !== "undefined";
|
||||||
|
|
||||||
|
if (options?.username) {
|
||||||
|
const existingUser = await Profile.byName(options.username);
|
||||||
|
if (existingUser == null) return null;
|
||||||
|
}
|
||||||
|
|
||||||
const newId = await this.getUniqueId();
|
const newId = await this.getUniqueId();
|
||||||
const newUsername = optionsSpecified
|
const newUsername = optionsSpecified
|
||||||
? options.username
|
? options.username
|
||||||
@@ -143,6 +152,7 @@ class Profile {
|
|||||||
|
|
||||||
Settings = new ProfileSettingsManager();
|
Settings = new ProfileSettingsManager();
|
||||||
Progression = new ProfileProgressionManager();
|
Progression = new ProfileProgressionManager();
|
||||||
|
Reputation = new ProfileReputationManager();
|
||||||
|
|
||||||
constructor(id: number) {
|
constructor(id: number) {
|
||||||
this.#id = id;
|
this.#id = id;
|
||||||
@@ -150,6 +160,7 @@ class Profile {
|
|||||||
// Set IDs for all content managers
|
// Set IDs for all content managers
|
||||||
this.Settings.setProfile(this.#id);
|
this.Settings.setProfile(this.#id);
|
||||||
this.Progression.setProfile(this.#id);
|
this.Progression.setProfile(this.#id);
|
||||||
|
this.Reputation.setProfile(this.#id);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInstance(instance: RoomInstance | null) {
|
setInstance(instance: RoomInstance | null) {
|
||||||
@@ -178,11 +189,20 @@ class Profile {
|
|||||||
|
|
||||||
async getKnownDeviceClass() {
|
async getKnownDeviceClass() {
|
||||||
const data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass));
|
const data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass));
|
||||||
|
if (data == null) {
|
||||||
|
log.w(`No known device class for ${this.#id}`);
|
||||||
|
return DeviceClass.Unknown;
|
||||||
|
}
|
||||||
|
const parsedData = parseInt(data);
|
||||||
|
if (isNaN(parsedData)) {
|
||||||
|
log.w(`Malformed device class for ${this.#id}`);
|
||||||
|
return DeviceClass.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
const DeviceClassEnum = z.nativeEnum(DeviceClass);
|
const DeviceClassEnum = z.nativeEnum(DeviceClass);
|
||||||
type DeviceClassEnum = z.infer<typeof DeviceClassEnum>
|
type DeviceClassEnum = z.infer<typeof DeviceClassEnum>
|
||||||
|
|
||||||
const result = DeviceClassEnum.safeParse(data);
|
const result = DeviceClassEnum.safeParse(parsedData);
|
||||||
if (result.success) return result.data;
|
if (result.success) return result.data;
|
||||||
else return DeviceClass.Unknown;
|
else return DeviceClass.Unknown;
|
||||||
}
|
}
|
||||||
@@ -193,11 +213,20 @@ class Profile {
|
|||||||
|
|
||||||
async getVRMovementMode() {
|
async getVRMovementMode() {
|
||||||
const data = await this.Settings.getSetting(SettingKey.VRMovementMode);
|
const data = await this.Settings.getSetting(SettingKey.VRMovementMode);
|
||||||
|
if (data == null) {
|
||||||
|
log.w(`No known device class for ${this.#id}`);
|
||||||
|
return VRMovementMode.Teleport;
|
||||||
|
}
|
||||||
|
const parsedData = parseInt(data);
|
||||||
|
if (isNaN(parsedData)) {
|
||||||
|
log.w(`Malformed device class for ${this.#id}`);
|
||||||
|
return VRMovementMode.Teleport;
|
||||||
|
}
|
||||||
|
|
||||||
const VRMovementModeEnum = z.nativeEnum(VRMovementMode);
|
const VRMovementModeEnum = z.nativeEnum(VRMovementMode);
|
||||||
type VRMovementModeEnum = z.infer<typeof VRMovementModeEnum>
|
type VRMovementModeEnum = z.infer<typeof VRMovementModeEnum>
|
||||||
|
|
||||||
const result = VRMovementModeEnum.safeParse(data);
|
const result = VRMovementModeEnum.safeParse(parsedData);
|
||||||
if (result.success) return result.data;
|
if (result.success) return result.data;
|
||||||
else return VRMovementMode.Teleport;
|
else return VRMovementMode.Teleport;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export const KeyGroups = {
|
|||||||
DisplayName: "displayName",
|
DisplayName: "displayName",
|
||||||
Settings: "settings",
|
Settings: "settings",
|
||||||
DeviceClass: "deviceClass",
|
DeviceClass: "deviceClass",
|
||||||
|
Xp: "xp",
|
||||||
},
|
},
|
||||||
Operators: "operators",
|
Operators: "operators",
|
||||||
Users: {
|
Users: {
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ try {
|
|||||||
if (path === '/negotiate' && req.method == 'POST')
|
if (path === '/negotiate' && req.method == 'POST')
|
||||||
return new Response(JSON.stringify({}));
|
return new Response(JSON.stringify({}));
|
||||||
|
|
||||||
|
|
||||||
if (!upgrade) return new Response(null, { status: 401 });
|
if (!upgrade) return new Response(null, { status: 401 });
|
||||||
|
|
||||||
const authResult = await authenticate(req);
|
const authResult = await authenticate(req);
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import { APIUtils } from "../apiutils.ts";
|
||||||
import { route as VersionCheckRoute } from "./api/versioncheck.ts";
|
import { route as VersionCheckRoute } from "./api/versioncheck.ts";
|
||||||
import { route as ConfigRoute } from "./api/config.ts";
|
import { route as ConfigRoute } from "./api/config.ts";
|
||||||
import { route as GameConfig } from "./api/gameconfigs.ts";
|
import { route as GameConfig } from "./api/gameconfigs.ts";
|
||||||
import { route as PlayerReportingRoute } from "./api/PlayerReporting.ts";
|
import { route as PlayerReportingRoute } from "./api/PlayerReporting.ts";
|
||||||
import { route as MessagesRoute } from "./api/messages.ts";
|
import { route as MessagesRoute } from "./api/messages.ts";
|
||||||
import { route as RelationshipsRoute } from "./api/relationships.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");
|
export const route = APIUtils.createRouter("/api");
|
||||||
|
|
||||||
@@ -14,3 +18,7 @@ route.router.use(GameConfig.path, GameConfig.router);
|
|||||||
route.router.use(PlayerReportingRoute.path, PlayerReportingRoute.router);
|
route.router.use(PlayerReportingRoute.path, PlayerReportingRoute.router);
|
||||||
route.router.use(MessagesRoute.path, MessagesRoute.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);
|
||||||
@@ -18,7 +18,7 @@ const HileMessageSchema = z.object({
|
|||||||
route.router.post('/v1/hile',
|
route.router.post('/v1/hile',
|
||||||
|
|
||||||
APIUtils.Authentication,
|
APIUtils.Authentication,
|
||||||
express.json(),
|
express.urlencoded({ extended: true }),
|
||||||
APIUtils.validateRequestBody(HileMessageSchema),
|
APIUtils.validateRequestBody(HileMessageSchema),
|
||||||
|
|
||||||
(rq: express.Request<NoBody, NoBody, HileMessage>, rs) => {
|
(rq: express.Request<NoBody, NoBody, HileMessage>, rs) => {
|
||||||
|
|||||||
24
src/routes/api/playerReputation.ts
Normal file
24
src/routes/api/playerReputation.ts
Normal 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
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");
|
export const route = APIUtils.createRouter("/players");
|
||||||
|
|
||||||
@@ -6,12 +11,22 @@ route.router.get('/v1/progression/:id',
|
|||||||
|
|
||||||
APIUtils.Authentication,
|
APIUtils.Authentication,
|
||||||
|
|
||||||
async (_rq, rs) => {
|
async (rq: express.Request<{ id: string }>, rs) => {
|
||||||
rs.json({
|
const unparsedPlayerId = rq.params.id;
|
||||||
PlayerId: rs.locals.profile.getId(),
|
const parsedPlayerId = parseInt(unparsedPlayerId);
|
||||||
Level: await rs.locals.profile.Progression.getLevel(), // await is temporary
|
if (isNaN(parsedPlayerId)) {
|
||||||
Xp: await rs.locals.profile.Progression.getXp()
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
);
|
);
|
||||||
11
src/routes/api/playersubscriptions.ts
Normal file
11
src/routes/api/playersubscriptions.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
22
src/routes/api/settings.ts
Normal file
22
src/routes/api/settings.ts
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
@@ -44,10 +44,11 @@ route.router.get(
|
|||||||
}
|
}
|
||||||
image = await Image.decode(imageSource);
|
image = await Image.decode(imageSource);
|
||||||
|
|
||||||
let cropSquare: boolean = false;
|
let cropSquare: number | null = null;
|
||||||
if (typeof rq.query.cropSquare == "string") {
|
if (typeof rq.query.cropSquare == "string") {
|
||||||
const d = JSON.parse(rq.query.cropSquare);
|
const num = parseInt(rq.query.cropSquare);
|
||||||
if (typeof d == "boolean" && d) cropSquare = true;
|
if (isNaN(num)) cropSquare = null;
|
||||||
|
else cropSquare = num;
|
||||||
}
|
}
|
||||||
let width: number | null = null;
|
let width: number | null = null;
|
||||||
if (typeof rq.query.width == "string") {
|
if (typeof rq.query.width == "string") {
|
||||||
@@ -84,7 +85,7 @@ route.router.get(
|
|||||||
}
|
}
|
||||||
} else if (width) image.resize(width, Image.RESIZE_AUTO);
|
} else if (width) image.resize(width, Image.RESIZE_AUTO);
|
||||||
else if (height) image.resize(Image.RESIZE_AUTO, height);
|
else if (height) image.resize(Image.RESIZE_AUTO, height);
|
||||||
if (cropSquare) {
|
if (cropSquare == 1) {
|
||||||
if (image.width > image.height) {
|
if (image.width > image.height) {
|
||||||
image.crop(
|
image.crop(
|
||||||
Math.round(image.width / 2) - Math.round(image.height / 2),
|
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()));
|
rs.type("png").send(Buffer.from(await image.encode()));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import express from "express";
|
|||||||
import Matchmaking from "../../data/live/base.ts";
|
import Matchmaking from "../../data/live/base.ts";
|
||||||
import Presence from "../../data/live/presence.ts";
|
import Presence from "../../data/live/presence.ts";
|
||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/users.ts";
|
||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
|
||||||
|
const log = new Logging("MatchPlayerRoute");
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/player');
|
export const route = APIUtils.createRouter('/player');
|
||||||
|
|
||||||
@@ -28,4 +31,33 @@ route.router.post('/login',
|
|||||||
rs.sendStatus(200);
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
@@ -40,7 +40,7 @@ const AuthRequestRootSchema = z.object({
|
|||||||
pubkey: z.string(),
|
pubkey: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const rateLimit = new APIUtils.RateLimiter(60, 1);
|
const rateLimit = new APIUtils.RateLimiter(60, 2);
|
||||||
|
|
||||||
route.router.post("/auth",
|
route.router.post("/auth",
|
||||||
|
|
||||||
@@ -57,9 +57,7 @@ route.router.post("/auth",
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rq.body.message.server_id !== config.public.serverId) {
|
if (rq.body.message.server_id !== config.public.serverId) {
|
||||||
log.w(
|
log.w(`Auth request failed (serverId mismatch), config error?\n given ID: '${rq.body.message.server_id}'\n our ID: '${config.public.serverId}'`);
|
||||||
`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.");
|
authFailed("Authentication request not intended for this server.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -114,9 +112,7 @@ route.router.post("/auth",
|
|||||||
} else user = obj;
|
} else user = obj;
|
||||||
}
|
}
|
||||||
if (!(await user.addNonce(rq.body.message.nonce))) {
|
if (!(await user.addNonce(rq.body.message.nonce))) {
|
||||||
log.w(
|
log.w(`Client '${rq.body.client_id}' has already used nonce. Replay attack?`);
|
||||||
`Client '${rq.body.client_id}' has already used nonce. Replay attack?`,
|
|
||||||
);
|
|
||||||
authFailed("Authentication request failed.");
|
authFailed("Authentication request failed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,17 +34,17 @@ export class SignalRSocketHandler {
|
|||||||
|
|
||||||
#onMessage(message: Message) {
|
#onMessage(message: Message) {
|
||||||
if (message.kind == MessageKind.Protocol) {
|
if (message.kind == MessageKind.Protocol) {
|
||||||
this.#send({});
|
this.sendRaw({});
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type - 1]})\n ${JSON.stringify(message.data)}`);
|
this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #init() {
|
async #init() {
|
||||||
this.#log.source += this.#profile.getId().toString();
|
this.#log.source += this.#profile.getId().toString();
|
||||||
|
|
||||||
this.#log.i(`Player '${(await this.#profile.export())?.username}' (${this.#profile.getId()}) created hub socket`);
|
this.#log.i(`Created hub socket`);
|
||||||
|
|
||||||
this.#socket.addEventListener('message', message => {
|
this.#socket.addEventListener('message', message => {
|
||||||
try {
|
try {
|
||||||
@@ -68,9 +68,19 @@ export class SignalRSocketHandler {
|
|||||||
this.#log.e(`Socket error: ${err}`);
|
this.#log.e(`Socket error: ${err}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.#socket.addEventListener('close', this.destroy(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
#send(data: object) {
|
destroy(sock: SignalRSocketHandler) {
|
||||||
|
return () => {
|
||||||
|
sock.sendRaw({ type: 7, error: "Socket closed by server" });
|
||||||
|
sock.#socket.close();
|
||||||
|
sock.#log.i(`Closed hub socket`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRaw(data: object) {
|
||||||
this.#socket.send(`${JSON.stringify(data)}\u001e`);
|
this.#socket.send(`${JSON.stringify(data)}\u001e`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ export const SignalRMessageSchema = z.discriminatedUnion("type", [
|
|||||||
|
|
||||||
export enum TargetResultType {
|
export enum TargetResultType {
|
||||||
Success,
|
Success,
|
||||||
Failure
|
Failure,
|
||||||
|
NotATarget
|
||||||
}
|
}
|
||||||
interface TargetResultBase {
|
interface TargetResultBase {
|
||||||
type: TargetResultType
|
type: TargetResultType
|
||||||
@@ -122,4 +123,7 @@ export interface TargetResultSuccess<T = unknown> extends TargetResultBase {
|
|||||||
export interface TargetResultFailure extends TargetResultBase {
|
export interface TargetResultFailure extends TargetResultBase {
|
||||||
type: TargetResultType.Failure
|
type: TargetResultType.Failure
|
||||||
}
|
}
|
||||||
export type TargetResult = TargetResultSuccess | TargetResultFailure;
|
export interface TargetResultNotATarget extends TargetResultBase {
|
||||||
|
type: TargetResultType.NotATarget
|
||||||
|
}
|
||||||
|
export type TargetResult = TargetResultSuccess | TargetResultFailure | TargetResultNotATarget;
|
||||||
Reference in New Issue
Block a user