woah man
This commit is contained in:
10
src/main.ts
10
src/main.ts
@@ -11,7 +11,7 @@ import { SignalRSocketHandler } from "./server/socket/signalr/socket.ts";
|
||||
import { PushNotificationId } from "./server/socket/signalr/types.ts";
|
||||
import { genericResponse } from "./util/api.ts";
|
||||
import { getNetConfig } from "./net.ts";
|
||||
import { TokenFormat, TokenType } from "./server/platforms/types.ts";
|
||||
import { PlatformType, TokenFormat, TokenType } from "./server/platforms/types.ts";
|
||||
import { HonoEnv } from "./util/types.ts";
|
||||
import { Context } from "@hono/hono";
|
||||
import { compress } from "@hono/hono/compress";
|
||||
@@ -185,4 +185,10 @@ Server.Commands.addRootCommand(new Command({
|
||||
z.coerce.number()
|
||||
]),
|
||||
help: 'Get ping (in ms) to the server'
|
||||
}));
|
||||
}));
|
||||
|
||||
Server.on('server.start', () => {
|
||||
Server.Profiles.get(1).then(prof => {
|
||||
if (!prof) Server.Profiles.create(PlatformType.HeadlessBot, "", "Coach", 1);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,12 @@
|
||||
import { createHonoRoute } from "../../../util/import.ts";
|
||||
import { authenticate, galvanicError, GalvanicErrors, RateLimiter, recNetError, statusResponse } from "../../../util/api.ts";
|
||||
import { RateLimiter, recNetResultResponse, statusResponse } from "../../../util/api.ts";
|
||||
import Server from "../../../server/server.ts";
|
||||
import z from "zod";
|
||||
import { transformCheckEnum, typedZValidator } from "../../../util/validators.ts";
|
||||
import { PlatformType } from "../../../server/platforms/types.ts";
|
||||
import Steam from "../../../util/steam/steam.ts";
|
||||
import { HTTPStatus } from "@oneday/http-status";
|
||||
import { accountRoute } from "./me/root.ts";
|
||||
|
||||
export const route = createHonoRoute('/account');
|
||||
|
||||
@@ -35,20 +36,22 @@ const createAccountBodySchema = z.object({
|
||||
});
|
||||
route.app.post('/create', postCreateRateLimiter.middle(), typedZValidator('form', createAccountBodySchema), async c => {
|
||||
|
||||
if (c.req.header("User-Agent") !== "BestHTTP") return recNetResultResponse(c, HTTPStatus.OK, false, "Platform error");
|
||||
|
||||
const form = c.req.valid('form');
|
||||
|
||||
if (typeof form.platform == 'undefined')
|
||||
return c.json(galvanicError(GalvanicErrors.jex));
|
||||
if (form.platform == null)
|
||||
return recNetResultResponse(c, HTTPStatus.OK, false, "Platform error");
|
||||
else if (form.platform == PlatformType.Steam) {
|
||||
const steam = await Steam.GetPlayerSummaries([form.platformId]);
|
||||
if (steam.length == 0)
|
||||
return c.json(galvanicError(GalvanicErrors.sploot));
|
||||
return recNetResultResponse(c, HTTPStatus.OK, false, "Steam profile could not be fetched");
|
||||
|
||||
const cachedlogins = await Server.Platforms.getCachedLogins(form.platform, form.platformId, true);
|
||||
if (cachedlogins.length == 0) {
|
||||
|
||||
const profile = await Server.Profiles.create(form.platform, form.platformId, steam[0].realname ?? steam[0].personaname);
|
||||
if (!profile) return c.json(galvanicError(GalvanicErrors.sploot));
|
||||
if (!profile) return recNetResultResponse(c, HTTPStatus.OK, false, "Account could not be created");
|
||||
|
||||
Server.Content.steamAvatarDownloadForProfile(profile, steam[0].avatarfull);
|
||||
|
||||
@@ -60,7 +63,7 @@ route.app.post('/create', postCreateRateLimiter.middle(), typedZValidator('form'
|
||||
} else {
|
||||
|
||||
const profile = await Server.Profiles.create(form.platform, form.platformId);
|
||||
if (!profile) return c.json(galvanicError(GalvanicErrors.sploot));
|
||||
if (!profile) return recNetResultResponse(c, HTTPStatus.OK, false, "Account could not be created");
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
@@ -68,14 +71,11 @@ route.app.post('/create', postCreateRateLimiter.middle(), typedZValidator('form'
|
||||
});
|
||||
|
||||
}
|
||||
} else return c.json(recNetError("Not a Steam user"));
|
||||
} else return recNetResultResponse(c, HTTPStatus.OK, false, "Not a Steam user");
|
||||
|
||||
});
|
||||
|
||||
route.app.get('/me', authenticate, c => {
|
||||
const profile = c.get('profile');
|
||||
return c.json(profile.selfExport());
|
||||
});
|
||||
route.app.route('/', accountRoute.app);
|
||||
|
||||
const getAccountByIdParamSchema = z.object({
|
||||
id: z.coerce.number().max(Math.pow(2, 31))
|
||||
|
||||
38
src/routes/accounts/routes/me/root.ts
Normal file
38
src/routes/accounts/routes/me/root.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { HTTPStatus } from "@oneday/http-status";
|
||||
import { authenticate, recNetResultResponse } from "../../../../util/api.ts";
|
||||
import { createHonoRoute } from "../../../../util/import.ts";
|
||||
import z from "zod";
|
||||
import { typedZValidator } from "../../../../util/validators.ts";
|
||||
import Server from "../../../../server/server.ts";
|
||||
|
||||
export const accountRoute = createHonoRoute("/");
|
||||
|
||||
accountRoute.app.get('/me', authenticate, c => {
|
||||
const profile = c.get('profile');
|
||||
return c.json(profile.selfExport());
|
||||
});
|
||||
|
||||
const changeUsernameFormSchema = z.object({
|
||||
username: z.string().min(4).max(48).regex(/^[A-Za-z0-9._-]+$/)
|
||||
});
|
||||
accountRoute.app.put('/me/username',
|
||||
|
||||
authenticate,
|
||||
typedZValidator('form', changeUsernameFormSchema, true, "Username must only contain letters, numbers, and any of these symbols: ._-"),
|
||||
|
||||
async c => {
|
||||
const newUsername = c.req.valid('form').username;
|
||||
const takenProf = await Server.Profiles.getByUsername(newUsername);
|
||||
if (takenProf) return recNetResultResponse(c, HTTPStatus.OK, false, "Username taken");
|
||||
else {
|
||||
await c.get('profile').setUsername(newUsername);
|
||||
return recNetResultResponse(c, HTTPStatus.OK, true, "Username updated successfully");
|
||||
}
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
// birthday: 1970-09-10T00:00:00.0000000Z (based on age entered in OOBE)
|
||||
accountRoute.app.put('/me/birthday', authenticate, c => {
|
||||
return recNetResultResponse(c, HTTPStatus.OK, true, "Stub. Birthdays are not saved in Galvanic Corrosion.");
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HTTPStatus } from "@oneday/http-status";
|
||||
import { profileAvatarSchema } from "../../../server/profiles/content/Avatar.ts";
|
||||
import Server from "../../../server/server.ts";
|
||||
import { authenticate } from "../../../util/api.ts";
|
||||
import { authenticate, statusResponse } from "../../../util/api.ts";
|
||||
import { createHonoRoute } from "../../../util/import.ts";
|
||||
import { typedZValidator } from "../../../util/validators.ts";
|
||||
|
||||
@@ -23,7 +24,7 @@ route.app.get('/v2', async c => {
|
||||
route.app.post('/v2/set', typedZValidator('json', profileAvatarSchema), async c => {
|
||||
const outfit = c.req.valid('json');
|
||||
await c.get('profile').Avatar.setAvatar(outfit);
|
||||
return c.status(200);
|
||||
return statusResponse(c, HTTPStatus.OK);
|
||||
});
|
||||
|
||||
route.app.get('/v3/saved', c => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import z from "zod";
|
||||
import Server from "../../../server/server.ts";
|
||||
import { createHonoRoute } from "../../../util/import.ts";
|
||||
import { typedZValidator } from "../../../util/validators.ts";
|
||||
|
||||
export const route = createHonoRoute("/config");
|
||||
|
||||
@@ -7,6 +9,23 @@ route.app.get('/v1/amplitude', c => {
|
||||
return c.json({AmplitudeKey: ""});
|
||||
});
|
||||
|
||||
function createNuxConfig(n: number) {
|
||||
return {
|
||||
Version: 0,
|
||||
ButtonNumber: n,
|
||||
Override: 0,
|
||||
DefaultRoomName: "DormRoom",
|
||||
DefaultTitle: "Dorm Room"
|
||||
}
|
||||
}
|
||||
// i don't know what id is right now. it is an arbitrary number given by the game.
|
||||
const cohortNuxIdParamSchema = z.object({
|
||||
id: z.coerce.number().nonnegative().max(32)
|
||||
});
|
||||
route.app.get('/v1/cohortnux/:id', typedZValidator('param', cohortNuxIdParamSchema), c => {
|
||||
return c.json([0, 1, 2, 3].map(n => createNuxConfig(n)));
|
||||
});
|
||||
|
||||
route.app.get('/v2', c => {
|
||||
return c.json(Server.getPublicConfig());
|
||||
});
|
||||
@@ -33,4 +33,10 @@ route.app.get('/v2/myrooms', authenticate, async c => {
|
||||
const rooms = exs.map(ex => ex.Room);
|
||||
|
||||
return c.json(rooms);
|
||||
});
|
||||
|
||||
route.app.get('/v1/hot', authenticate, async c => {
|
||||
const agRoomIds = Server.Rooms.getAgRoomIds();
|
||||
const factories = await Server.Rooms.getMany(...agRoomIds.values().toArray());
|
||||
return c.json((await Promise.all(factories.map(factory => factory.export()))).map(roomDetails => roomDetails.Room));
|
||||
});
|
||||
32
src/routes/auth/routes/account.ts
Normal file
32
src/routes/auth/routes/account.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import z from "zod";
|
||||
import { createHonoRoute } from "../../../util/import.ts";
|
||||
import { authenticate, recNetResultResponse } from "../../../util/api.ts";
|
||||
import { typedZValidator } from "../../../util/validators.ts";
|
||||
import { HTTPStatus } from "@oneday/http-status";
|
||||
|
||||
export const route = createHonoRoute('/account');
|
||||
|
||||
const changePasswordFormSchema = z.object({
|
||||
/**
|
||||
* Password requirements:
|
||||
* - Lowercase character
|
||||
* - Uppercase character
|
||||
* - A digit
|
||||
* - A special character
|
||||
* - 8 characters minimum
|
||||
*/
|
||||
newPassword: z.string().max(64).regex(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/)
|
||||
});
|
||||
route.app.post('/me/changepassword',
|
||||
|
||||
authenticate,
|
||||
typedZValidator('form', changePasswordFormSchema, true,
|
||||
"Password must contain a lowercase and uppercase character, a digit, a special character, and be at least 8 characters in length"
|
||||
),
|
||||
|
||||
async c => {
|
||||
await c.get('profile').setPassword(c.req.valid('form').newPassword);
|
||||
return recNetResultResponse(c, HTTPStatus.OK, true, "Password updated successfully");
|
||||
}
|
||||
|
||||
);
|
||||
@@ -22,7 +22,7 @@ const authBodyBaseSchema = z.object({
|
||||
}),
|
||||
platform_id: z.string().min(4),
|
||||
device_id: z.string().min(4),
|
||||
device_class: z.string().transform(transformCheckEnum<DeviceClass>(DeviceClass)),
|
||||
device_class: z.coerce.number().transform(transformCheckEnum<DeviceClass>(DeviceClass)),
|
||||
time: z.coerce.date(),
|
||||
ver: z.literal(gameVerString),
|
||||
asid: z.coerce.number(),
|
||||
@@ -97,12 +97,9 @@ route.app.post('/token', typedZValidator('form', tokenGrantSchema), async c => {
|
||||
|
||||
const profile = await Server.Profiles.get(token.sub);
|
||||
if (!profile) return error(TokenRequestError.AccessDenied);
|
||||
const accessToken = await Server.Platforms.getToken(profile, TokenType.Access);
|
||||
const refreshToken = await Server.Platforms.getToken(profile, TokenType.Refresh);
|
||||
|
||||
return c.json({
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
token: await Server.Platforms.getToken(profile, TokenType.Access),
|
||||
});
|
||||
} catch (err) {
|
||||
log.w(`Authentication error (token req): ${(err as Error).stack}`);
|
||||
|
||||
@@ -8,6 +8,8 @@ import Command from "../commands/command.ts";
|
||||
import z from "zod";
|
||||
import { PushNotificationId } from "../socket/signalr/types.ts";
|
||||
|
||||
const log = new Logging("Presence");
|
||||
|
||||
export enum VRMovementMode {
|
||||
TELEPORT,
|
||||
WALK
|
||||
@@ -87,6 +89,8 @@ export class Presence {
|
||||
}
|
||||
|
||||
export() {
|
||||
log.d(`profId ${this.#profile.getId()} presence exported. has instance: ${this.#profile.getInstance() !== null}`);
|
||||
|
||||
this.#lastExported = new Date();
|
||||
const e: PresenceExport = {
|
||||
playerId: this.#profile.getId(),
|
||||
@@ -127,7 +131,6 @@ export class ServerPresenceBase extends ServerContentBase {
|
||||
this.#intervalId = setInterval(() => {
|
||||
if (this.#presenceMap.size === 0) return;
|
||||
|
||||
this.#log.i('Clearing dead presences');
|
||||
this.#deleteDeadPresences();
|
||||
}, 300_000);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import Profile from "./profile.ts";
|
||||
import { SelfAccount, type RecNetAccount } from "./types/profile.ts";
|
||||
import Command from "./../commands/command.ts";
|
||||
import z from "zod";
|
||||
import { PlatformMask, PlatformType, ProfileRole, TokenType } from "../platforms/types.ts";
|
||||
import { PlatformMask, ProfileRole, TokenType } from "../platforms/types.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
|
||||
const profiles: Map<number, Profile> = new Map();
|
||||
@@ -36,29 +36,40 @@ class ProfileManagerBase extends ServerContentBase {
|
||||
if (await this.getByUsername(username)) username = await this.#getUnusedUsername();
|
||||
return username;
|
||||
}
|
||||
async #getUsernameDefault(username: string) {
|
||||
async #getUsernameDefault(username?: string) {
|
||||
if (!username) return await this.#getUnusedUsername();
|
||||
|
||||
const prof = await this.getByUsername(username);
|
||||
if (!prof) return username;
|
||||
|
||||
else return await this.#getUnusedUsername();
|
||||
}
|
||||
|
||||
async create(platform: PlatformType, platformId: string, username?: string) {
|
||||
const id = await this.#getUnusedId();
|
||||
const newUsername = username ? await this.#getUsernameDefault(username) : await this.#getUnusedUsername();
|
||||
async create(platform: number, platformId: string, username?: string, id?: number) {
|
||||
if (typeof id == 'number') {
|
||||
const prof = await this.get(id);
|
||||
if (prof) throw new Error("ID is in use");
|
||||
}
|
||||
|
||||
const newId = typeof id == 'number' ? id : await this.#getUnusedId();
|
||||
const newUsername = await this.#getUsernameDefault(username);
|
||||
|
||||
this.#log.d(`Creating account "${newUsername}" (${newId})`);
|
||||
|
||||
const newProfile: RecNetAccount = {
|
||||
accountId: id,
|
||||
accountId: newId,
|
||||
username: newUsername,
|
||||
displayName: newUsername,
|
||||
platforms: PlatformMask.None,
|
||||
profileImage: "DefaultProfileImage.png",
|
||||
createdAt: new Date()
|
||||
}
|
||||
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, id ], newProfile);
|
||||
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, newUsername ], id);
|
||||
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, newId ], newProfile);
|
||||
await this.kv.getKv().set([ ProfileManagerBase.profilesKey, newUsername ], newId);
|
||||
|
||||
await this.server.Platforms.addCachedLogin(platform, platformId, id);
|
||||
await this.server.Platforms.addCachedLogin(platform, platformId, newId);
|
||||
|
||||
return this.get(id);
|
||||
return this.get(newId);
|
||||
}
|
||||
|
||||
async get(id: number) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ProfileSettingsManager } from "./content/Settings.ts";
|
||||
import { ProfileSubscriptionsManager } from "./content/Subscriptions.ts";
|
||||
import ProfileManagerBase from "./manager.ts";
|
||||
import { recNetAccountSchema, SelfAccount, type RecNetAccount } from "./types/profile.ts";
|
||||
import { hash, verify } from "@felix/bcrypt";
|
||||
|
||||
class Profile {
|
||||
|
||||
@@ -95,7 +96,7 @@ class Profile {
|
||||
|
||||
async getRole(): Promise<ProfileRole> {
|
||||
const val = (await this.#kv.getKv().get<ProfileRole>(this.constructProfilePropertyKey('role'))).value;
|
||||
if (!val) return ProfileRole.Game;
|
||||
if (!val) return "gameClient";
|
||||
else return val;
|
||||
}
|
||||
|
||||
@@ -146,8 +147,8 @@ class Profile {
|
||||
if (this.#instance) this.#instance.removePlayer(this);
|
||||
inst.addPlayer(this);
|
||||
|
||||
this.#server.emit('presence.update', { profile: this, presence: this.#server.Presence.getPresence(this) });
|
||||
this.#instance = inst;
|
||||
this.#server.emit('presence.update', { profile: this, presence: this.#server.Presence.getPresence(this) });
|
||||
}
|
||||
|
||||
getId() {
|
||||
@@ -176,6 +177,15 @@ class Profile {
|
||||
return profile.getId() == this.getId();
|
||||
}
|
||||
|
||||
async setPassword(pass: string) {
|
||||
await this.#kv.getKv().set(this.constructProfilePropertyKey("password"), await hash(pass));
|
||||
}
|
||||
async verifyPassword(pass: string) {
|
||||
const hash = await this.#kv.getKv().get<string>(this.constructProfilePropertyKey("password"));
|
||||
if (hash.value == null) return false;
|
||||
else return await verify(pass, hash.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Profile;
|
||||
@@ -54,15 +54,11 @@ export async function authenticate(c: Context<HonoEnv>, nxt: Next) {
|
||||
} else return c.json(genericResponse(false, "Authorization required"), 401);
|
||||
}
|
||||
|
||||
export enum GalvanicErrors {
|
||||
jex = "jex", // Error in account creation, check platform
|
||||
sploot = "sploot", // Error in account creation, steamid was not valid or profile could not be created
|
||||
export function recNetResult(success: boolean, error?: string) {
|
||||
return {success, error};
|
||||
}
|
||||
export function galvanicError(code: GalvanicErrors) {
|
||||
return {success: false, error:`Galvanic Error (code: ${code})`};
|
||||
}
|
||||
export function recNetError(err: string) {
|
||||
return {success:false, error: err};
|
||||
export function recNetResultResponse(c: Context<HonoEnv>, status: HTTPStatus, success: boolean, error?: string) {
|
||||
return c.json(recNetResult(success, error), status as ContentfulStatusCode);
|
||||
}
|
||||
|
||||
export function generateRandomString(length: number) {
|
||||
@@ -126,7 +122,7 @@ export class RateLimiter {
|
||||
this.#addressIncrement(address);
|
||||
|
||||
const hits = this.#getAddressHits(address);
|
||||
if (hits && hits > this.#hitLimit) return c.json(recNetError("Rate Limited. Please try again later."), 429);
|
||||
if (hits && hits > this.#hitLimit) return c.json(recNetResult(false, "Rate Limited. Please try again later."), 429);
|
||||
else return next();
|
||||
};
|
||||
}
|
||||
|
||||
46
src/util/flags.ts
Normal file
46
src/util/flags.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
ChatGPT GPT-5 prompt:
|
||||
|
||||
Create a TypeScript class that can handle operations on a number
|
||||
similar to how the [Flags] attribute changes enums in C#.
|
||||
Include methods for checking the presence of a certain bit, adding a bit if it is not included, and removing bits.
|
||||
|
||||
Make the class generic.
|
||||
*/
|
||||
export class BitFlags<T extends number> {
|
||||
private value: T;
|
||||
|
||||
constructor(initialValue: T | number = 0 as T) {
|
||||
this.value = initialValue as T;
|
||||
}
|
||||
|
||||
/** Get current numeric value */
|
||||
public getValue(): T {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/** Check if a specific flag is set */
|
||||
public has(flag: T): boolean {
|
||||
return (this.value & flag) === flag;
|
||||
}
|
||||
|
||||
/** Add a flag */
|
||||
public add(flag: T): void {
|
||||
this.value = (this.value | flag) as T;
|
||||
}
|
||||
|
||||
/** Remove a flag */
|
||||
public remove(flag: T): void {
|
||||
this.value = (this.value & ~flag) as T;
|
||||
}
|
||||
|
||||
/** Toggle a flag */
|
||||
public toggle(flag: T): void {
|
||||
this.value = (this.value ^ flag) as T;
|
||||
}
|
||||
|
||||
/** Reset all flags */
|
||||
public clear(): void {
|
||||
this.value = 0 as T;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { MiddlewareHandler } from "@hono/hono";
|
||||
import { z } from "zod";
|
||||
import type { HonoEnv } from "./types.ts";
|
||||
import { ZodSchema } from "zod/v4";
|
||||
import { HTTPStatus } from "@oneday/http-status";
|
||||
|
||||
// thanks claude, this hurt my brain!
|
||||
|
||||
@@ -11,9 +12,16 @@ export const typedZValidator = <
|
||||
S extends ZodSchema
|
||||
>(
|
||||
target: T,
|
||||
schema: S
|
||||
schema: S,
|
||||
httpOk?: boolean,
|
||||
recNetError?: string
|
||||
) => {
|
||||
return zValidator(target, schema) as MiddlewareHandler<
|
||||
return zValidator(target, schema, (result, c) => {
|
||||
if (!result.success) {
|
||||
if (recNetError) return c.json({ success: false, error: recNetError }, httpOk == true ? HTTPStatus.OK : HTTPStatus.BadRequest);
|
||||
else return c.json({ success: false, error: "Request validation failed" }, httpOk == true ? HTTPStatus.OK : HTTPStatus.BadRequest);
|
||||
}
|
||||
}) as MiddlewareHandler<
|
||||
HonoEnv,
|
||||
string,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user