User stuff, config structure changes, web panel start, versioncheck fix, api start, recaptcha support for web panel
This commit is contained in:
@@ -4,7 +4,7 @@ import Logging from "@proxnet/undead-logging";
|
||||
|
||||
const log = new Logging('APIUtils');
|
||||
|
||||
interface AppRouter {
|
||||
type AppRouter = {
|
||||
path: string,
|
||||
router: express.Router
|
||||
}
|
||||
@@ -57,10 +57,10 @@ export function checkBodyTypes<T>(typeDef: T) {
|
||||
};
|
||||
}
|
||||
|
||||
export function genericResponseFormat(failure: boolean, msg: string | null = null, data = null) {
|
||||
export function genericResponseFormat(failure: boolean, msg: string | null = null, data: object | null = null) {
|
||||
return { failed: failure, instance: instanceId, message: msg, data: data };
|
||||
}
|
||||
export function genericResponse(failure: boolean, msg: string | null = null, data = null) {
|
||||
export function genericResponse(failure: boolean, msg: string | null = null, data: object | null = null) {
|
||||
return (_rq: express.Request, rs: express.Response) => {
|
||||
rs.json({ failed: failure, instance: instanceId, message: msg, data: data });
|
||||
};
|
||||
@@ -102,4 +102,67 @@ export function statusResponse(code: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export class RateLimiter {
|
||||
|
||||
#intervalId: number
|
||||
|
||||
#hitLimit: number
|
||||
|
||||
#addressHits: Map<string, number> = new Map();
|
||||
|
||||
/**
|
||||
* @param interval In seconds: rate at which hit counts will be cleared
|
||||
* @param limit Number of hits (inclusive) before requests are blocked
|
||||
*/
|
||||
constructor(interval: number, limit: number) {
|
||||
|
||||
this.#hitLimit = limit;
|
||||
|
||||
this.#intervalId = setInterval(() => {
|
||||
this.#addressHits.clear();
|
||||
}, interval * 1000);
|
||||
|
||||
Deno.addSignalListener('SIGINT', () => {
|
||||
this.#close();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
#addressIncrement(address: string) {
|
||||
const hits = this.#addressHits.get(address);
|
||||
if (hits) this.#addressHits.set(address, hits + 1);
|
||||
else this.#addressHits.set(address, 1);
|
||||
}
|
||||
|
||||
#getAddressHits(address: string) {
|
||||
const hits = this.#addressHits.get(address);
|
||||
if (hits) return hits;
|
||||
else {
|
||||
this.#addressHits.set(address, 1);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
middle() {
|
||||
|
||||
return (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
|
||||
const address = getSrcIpDefault(rq);
|
||||
this.#addressIncrement(address);
|
||||
|
||||
const hits = this.#getAddressHits(address);
|
||||
if (hits && hits > this.#hitLimit) {
|
||||
rs.statusCode = 429;
|
||||
rs.json(genericResponseFormat(true, `Rate limit for address ${address} reached. Try again in a moment.`));
|
||||
return;
|
||||
} else nxt();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#close() {
|
||||
clearInterval(this.#intervalId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export * as APIUtils from "./apiutils.ts"
|
||||
@@ -41,13 +41,19 @@ type SecretConfiguration = {
|
||||
authSecret: string
|
||||
}
|
||||
|
||||
type RecaptchaConfiguration = {
|
||||
sitekey: string,
|
||||
secret: string
|
||||
}
|
||||
|
||||
export type GalvanicConfiguration = {
|
||||
redis: RedisConfiguration,
|
||||
web: WebConfiguration,
|
||||
public: PublicConfiguration,
|
||||
logging: LoggingConfiguration,
|
||||
discord: DiscordConfiguration,
|
||||
secrets: SecretConfiguration
|
||||
discord: DiscordConfiguration | null,
|
||||
secrets: SecretConfiguration,
|
||||
recaptcha: RecaptchaConfiguration | null
|
||||
}
|
||||
|
||||
export const defaultConfig: GalvanicConfiguration = {
|
||||
@@ -75,14 +81,11 @@ export const defaultConfig: GalvanicConfiguration = {
|
||||
debug: false,
|
||||
network: false
|
||||
},
|
||||
discord: {
|
||||
token: "replace-me",
|
||||
guildId: "replace-me",
|
||||
clientId: "replace-me"
|
||||
},
|
||||
discord: null,
|
||||
secrets: {
|
||||
authSecret: "CHANGE-ME-PLEASE"
|
||||
}
|
||||
},
|
||||
recaptcha: null
|
||||
}
|
||||
|
||||
/** The current configuration. Read and parsed only during startup. */
|
||||
@@ -100,7 +103,7 @@ export function configurationExists() {
|
||||
return fs.existsSync('./config.json');
|
||||
}
|
||||
|
||||
/** Place the default configuration in the current directory. */
|
||||
/** Place [or overwrite] the [existing] default configuration in the current directory */
|
||||
export function generateDefaultConfig() {
|
||||
fs.writeFileSync('./config.json', JSON.stringify(defaultConfig, undefined, ' '));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Config } from "../config.ts";
|
||||
import { Redis } from "../db.ts";
|
||||
import { Objectives } from "./objectives.ts";
|
||||
|
||||
export type Config = {
|
||||
@@ -10,22 +11,12 @@ export type LevelProgressionItem = {
|
||||
RequiredXp: number
|
||||
}
|
||||
export type PublicConfig = {
|
||||
MessageOfTheDay: string,
|
||||
CdnBaseUri: string,
|
||||
MatchmakingParams: {
|
||||
PreferFullRoomsFrequency: number,
|
||||
PreferEmptyRoomsFrequency: number
|
||||
},
|
||||
ServerMaintenance: {
|
||||
StartsInMinutes: number
|
||||
},
|
||||
LevelProgressionMaps: LevelProgressionItem[],
|
||||
DailyObjectives: Objectives.Objective[][],
|
||||
ConfigTable: Config[],
|
||||
PhotonConfig: {
|
||||
CrcCheckEnabled: boolean,
|
||||
EnableServerTracingAfterDisconnect: boolean
|
||||
}
|
||||
ConfigTable: Config[]
|
||||
}
|
||||
|
||||
export function getConfig() {
|
||||
@@ -41,25 +32,37 @@ export function getConfig() {
|
||||
}
|
||||
|
||||
const conf: PublicConfig = {
|
||||
MessageOfTheDay: config.public.motd,
|
||||
CdnBaseUri: `${config.web.secureNameserverHost ? 'https' : 'http'}://${c.web.nameserverHost}/{0}`,
|
||||
MatchmakingParams: {
|
||||
PreferFullRoomsFrequency: 1,
|
||||
PreferEmptyRoomsFrequency: 0
|
||||
},
|
||||
ServerMaintenance: {
|
||||
StartsInMinutes: 0
|
||||
},
|
||||
LevelProgressionMaps: generateLevelProgressionMap(),
|
||||
DailyObjectives: [],
|
||||
ConfigTable: [],
|
||||
PhotonConfig: {
|
||||
CrcCheckEnabled: false,
|
||||
EnableServerTracingAfterDisconnect: false
|
||||
}
|
||||
ConfigTable: []
|
||||
}
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
export async function getAllGameConfigs() {
|
||||
try {
|
||||
const gameConfigs = new Map<string, string>();
|
||||
const val = await Redis.Database.hgetall(Redis.buildKey(Redis.KeyGroups.Config.Root, Redis.KeyGroups.Config.Game));
|
||||
|
||||
for (const key of Object.keys(val))
|
||||
gameConfigs.set(key, val[key]);
|
||||
|
||||
return gameConfigs;
|
||||
} catch (error) {
|
||||
console.error("Error fetching game configs:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function setGameConfig(key: string, value: string) {
|
||||
return Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Config.Root, Redis.KeyGroups.Config.Game));
|
||||
}
|
||||
export function getGameConfig(key: string) {
|
||||
return Redis.Database.hget(Redis.buildKey(Redis.KeyGroups.Config.Root, Redis.KeyGroups.Config.Game), key);
|
||||
}
|
||||
|
||||
export * as GameConfigs from "./config.ts";
|
||||
@@ -48,14 +48,12 @@ export class ConsumableBuilder {
|
||||
IsActive: boolean;
|
||||
|
||||
constructor(selection: ConsumableSelection, id: number, createdAt: Date, count: number, active: boolean) {
|
||||
|
||||
this.Id = id;
|
||||
this.ConsumableItemDesc = selection.guid;
|
||||
this.CreatedAt = createdAt.toUTCString();
|
||||
this.Count = count;
|
||||
this.UnlockedLevel = 0; // All players have access to every consumable - avatars and equipment are different
|
||||
this.IsActive = active;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
45
src/data/recaptcha.ts
Normal file
45
src/data/recaptcha.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import { Config } from "../config.ts";
|
||||
|
||||
const log = new Logging("ReCAPTCHA");
|
||||
|
||||
type SiteVerifyParams = {
|
||||
secret: string,
|
||||
response: string,
|
||||
remoteip?: string
|
||||
}
|
||||
type SiteVerifyResponse = {
|
||||
success: boolean,
|
||||
challenge_ts: string,
|
||||
hostname: string,
|
||||
"error-codes": string[]
|
||||
}
|
||||
|
||||
class ReCAPTCHABase {
|
||||
|
||||
async siteVerify(response: string, remoteip?: string) {
|
||||
const config = Config.getConfig();
|
||||
if (typeof config == 'undefined') return null;
|
||||
if (config.recaptcha == null) {
|
||||
log.e("Tried to verify ReCAPTCHA, but the config is null!");
|
||||
return null;
|
||||
}
|
||||
|
||||
const body: SiteVerifyParams = {
|
||||
secret: config.recaptcha.secret,
|
||||
response: response,
|
||||
remoteip: remoteip
|
||||
}
|
||||
const res = await fetch('https://google.com/recaptcha/api/siteverify', {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const resBody = await res.json() as SiteVerifyResponse;
|
||||
return resBody.success
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Recaptcha = new ReCAPTCHABase();
|
||||
export default Recaptcha;
|
||||
@@ -1,12 +1,101 @@
|
||||
interface UserInitOptions {
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { Redis } from "../db.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
|
||||
const log = new Logging("UserConstruct");
|
||||
|
||||
type UserInitOptions = {
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
type UserCreatedObj = {
|
||||
user: User,
|
||||
backupcode: string
|
||||
}
|
||||
|
||||
function randomASCII() {
|
||||
const codes = crypto.getRandomValues(new Uint8Array(512));
|
||||
const filteredCodes = codes.filter(val => (val >= 48 && val <= 57) || (val >= 65 && val <= 90) || (val >= 97 && val <= 122) );
|
||||
let str = String.fromCharCode(...filteredCodes);
|
||||
if (str.length < 32) str = randomASCII();
|
||||
return str.substring(0, 32);
|
||||
}
|
||||
|
||||
export class User {
|
||||
|
||||
static init() {
|
||||
static async exists(username: string) {
|
||||
return (await Redis.Database.exists(Redis.buildKey(Redis.KeyGroups.Usernames, username))) == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a user
|
||||
* @returns A `UserCreatedObj` with a reference to the new user if one was created, else `null` if the username already exists.
|
||||
*/
|
||||
static async init(options: UserInitOptions) {
|
||||
if (await User.exists(options.username)) return null;
|
||||
|
||||
const uuid = crypto.randomUUID();
|
||||
const backup = randomASCII();
|
||||
Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Usernames, options.username), uuid);
|
||||
Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Users.Root, uuid, Redis.KeyGroups.Users.Username), options.username);
|
||||
Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Users.Root, uuid, Redis.KeyGroups.Users.BackupCode), backup);
|
||||
Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Users.Root, uuid, Redis.KeyGroups.Users.Password), await bcrypt.hash(options.password));
|
||||
|
||||
const user = new User(uuid);
|
||||
const res: UserCreatedObj = {
|
||||
user: user,
|
||||
backupcode: backup
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a User by their username
|
||||
* @returns A `User` is one was found, else `null`
|
||||
*/
|
||||
static async byName(username: string) {
|
||||
const uuid = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Usernames, username));
|
||||
if (uuid == null) return null;
|
||||
else return new User(uuid);
|
||||
}
|
||||
|
||||
#uuid: string;
|
||||
|
||||
constructor(uuid: string) {
|
||||
this.#uuid = uuid;
|
||||
}
|
||||
|
||||
getUuid() {
|
||||
return this.#uuid;
|
||||
}
|
||||
|
||||
async validatePassword(password: string) {
|
||||
const hash = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#uuid, Redis.KeyGroups.Users.Password));
|
||||
if (hash == null) throw new Error(`Hash for user ${this.#uuid} was not found`);
|
||||
return await bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
async setPassword(password: string) {
|
||||
const hash = await bcrypt.hash(password);
|
||||
Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#uuid, Redis.KeyGroups.Users.Password), hash);
|
||||
}
|
||||
|
||||
async getBackupCode() {
|
||||
return await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#uuid, Redis.KeyGroups.Users.BackupCode));
|
||||
}
|
||||
|
||||
async getAssociatedProfiles() {
|
||||
const list = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#uuid, Redis.KeyGroups.Users.Profiles));
|
||||
return new Set<number>(list.filter(val => !Number.isNaN(parseInt(val, 10))).map(val => parseInt(val, 10)));
|
||||
}
|
||||
|
||||
async removeAssociatedProfile(id: number) {
|
||||
await Redis.Database.srem(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#uuid, Redis.KeyGroups.Users.Profiles), id);
|
||||
}
|
||||
|
||||
async addAssociatedProfile(id: number) {
|
||||
await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Users.Root, this.#uuid, Redis.KeyGroups.Users.Profiles), id);
|
||||
}
|
||||
|
||||
}
|
||||
41
src/db.ts
41
src/db.ts
@@ -1,8 +1,9 @@
|
||||
import { Redis } from "ioredis";
|
||||
import * as Config from "./config.ts";
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import chalk from "npm:chalk@^5.3.0";
|
||||
|
||||
const log = new Logging("RedisDB");
|
||||
const log = new Logging("Redis");
|
||||
|
||||
const config = Config.getConfig();
|
||||
if (typeof config == 'undefined') {
|
||||
@@ -26,10 +27,19 @@ export const Database = new Redis({
|
||||
db: config?.redis.db,
|
||||
lazyConnect: true
|
||||
});
|
||||
export function connectToRedis() {
|
||||
Database.connect();
|
||||
Database.on('connect', async () => {
|
||||
log.i(`Connected to Redis`);
|
||||
}
|
||||
|
||||
if (Deno.args.includes('--db-flush')) await Database.flushall(() => {
|
||||
log.w(`${chalk.inverse('The database was flushed.')}`);
|
||||
});
|
||||
});
|
||||
Database.on('connecting', () => {
|
||||
log.i('Connecting to Redis..');
|
||||
});
|
||||
Database.on('error', (err) => {
|
||||
log.e(`Redis error: ${err.stack}`);
|
||||
});
|
||||
|
||||
export function buildKey(...args: string[]) {
|
||||
return args.join(':');
|
||||
@@ -37,16 +47,21 @@ export function buildKey(...args: string[]) {
|
||||
export const KeyGroups = {
|
||||
Config: {
|
||||
Root: "config",
|
||||
Dynamic: "dynamic"
|
||||
Dynamic: "dynamic",
|
||||
Game: "game"
|
||||
},
|
||||
Accounts: {
|
||||
Root: "accounts",
|
||||
Ids: "ids",
|
||||
Usernames: "usernames",
|
||||
DisplayNames: "displaynames",
|
||||
XP: "scores",
|
||||
Developers: "developers",
|
||||
ProfileImages: "images"
|
||||
Ids: "profile-ids",
|
||||
Profiles: {
|
||||
Root: "profiles"
|
||||
},
|
||||
Usernames: "usernames",
|
||||
Users: {
|
||||
Root: "users",
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
BackupCode: "backupcode",
|
||||
Profiles: "profiles",
|
||||
Meta: "meta"
|
||||
}
|
||||
}
|
||||
export * as Redis from "./db.ts";
|
||||
@@ -19,6 +19,7 @@ client.once(discord.Events.ClientReady, client => {
|
||||
|
||||
let shuttingDown = false;
|
||||
Deno.addSignalListener('SIGINT', () => {
|
||||
if (client.readyTimestamp == null) return;
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
log.n('Disconnecting from Discord');
|
||||
@@ -26,8 +27,12 @@ Deno.addSignalListener('SIGINT', () => {
|
||||
});
|
||||
|
||||
export function login() {
|
||||
if (config?.discord?.token == Config.defaultConfig.discord?.token) {
|
||||
log.i('Discord not configured, ignoring');
|
||||
return;
|
||||
}
|
||||
log.i(`Creating Discord connection..`);
|
||||
client.login(config?.discord.token);
|
||||
client.login(config?.discord?.token);
|
||||
}
|
||||
|
||||
export * as Discord from "./discord.ts";
|
||||
@@ -5,11 +5,11 @@ export enum ResultType {
|
||||
NotFound
|
||||
}
|
||||
|
||||
interface ConfigResult {
|
||||
type ConfigResult = {
|
||||
Status: ResultType,
|
||||
Data: string | null
|
||||
}
|
||||
interface ConfigMResult {
|
||||
type ConfigMResult = {
|
||||
Status: ResultType,
|
||||
Data: (string | null)[] | null
|
||||
}
|
||||
|
||||
20
src/main.ts
20
src/main.ts
@@ -2,8 +2,10 @@ import * as Log from "@proxnet/undead-logging";
|
||||
import * as Config from "./config.ts";
|
||||
// @ts-types = 'npm:@types/express'
|
||||
import express from "express";
|
||||
import { Redis } from "./db.ts";
|
||||
import { Database } from "./db.ts";
|
||||
import { APIUtils } from "./apiutils.ts";
|
||||
import { Discord } from "./discord.ts";
|
||||
import { User } from "./data/users.ts";
|
||||
|
||||
const log = new Log.default("Main");
|
||||
|
||||
@@ -39,14 +41,23 @@ app.use((rq: express.Request, rs: express.Response, nxt: express.NextFunction) =
|
||||
nxt();
|
||||
});
|
||||
|
||||
app.use('/', APIUtils.genericResponse(false, `${config?.public.serverName} - ${config?.public.motd}`));
|
||||
app.get('/', APIUtils.genericResponse(false, `${config?.public.serverName} - ${config?.public.motd}`));
|
||||
|
||||
app.get('/debug', async (_rq, rs) => {
|
||||
const user = await User.init({ username: "testuser123", password: "foopass123" });
|
||||
log.i(String(user == null));
|
||||
|
||||
rs.sendStatus(200);
|
||||
});
|
||||
|
||||
// content routes
|
||||
const nameserverRouter = await import('./routes/nameserver.ts');
|
||||
const apiRouter = await import('./routes/api.ts');
|
||||
const userRouter = await import('./routes/user.ts');
|
||||
|
||||
app.use(nameserverRouter.route.path, nameserverRouter.route.router);
|
||||
app.use(apiRouter.route.path, apiRouter.route.router);
|
||||
app.use(userRouter.route.path, userRouter.route.router);
|
||||
|
||||
app.use((rq: express.Request, rs: express.Response) => {
|
||||
log.e(`${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`);
|
||||
@@ -55,8 +66,7 @@ app.use((rq: express.Request, rs: express.Response) => {
|
||||
});
|
||||
|
||||
try {
|
||||
log.i(`Connecting to Redis..`);
|
||||
Redis.connectToRedis();
|
||||
Database.connect();
|
||||
} catch (err) {
|
||||
log.e(`Cannot start: Redis could not be initialized. ${err}`);
|
||||
Deno.exit(1);
|
||||
@@ -81,4 +91,4 @@ try {
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
//Discord.login(); do not use for now
|
||||
Discord.login();
|
||||
@@ -4,5 +4,5 @@ import { APIUtils } from "../apiutils.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/api');
|
||||
|
||||
route.router.use(VersionCheckRoute.router);
|
||||
route.router.use(ConfigRoute.router);
|
||||
route.router.use(VersionCheckRoute.path, VersionCheckRoute.router);
|
||||
route.router.use(ConfigRoute.path, ConfigRoute.router);
|
||||
@@ -3,7 +3,7 @@ import { GameConfigs } from "../../data/config.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/config');
|
||||
|
||||
route.router.get('/v2', (rq, rs) => {
|
||||
route.router.get('/v2', (_rq, rs) => {
|
||||
const config = GameConfigs.getConfig();
|
||||
if (config == null) rs.sendStatus(500);
|
||||
else rs.json(config);
|
||||
|
||||
7
src/routes/api/gameconfigs.ts
Normal file
7
src/routes/api/gameconfigs.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { APIUtils } from "../../apiutils.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/gameconfigs');
|
||||
|
||||
route.router.get('/v1/all', (_rq, rs) => {
|
||||
rs.json([]);
|
||||
});
|
||||
@@ -4,20 +4,29 @@ export const route = APIUtils.createRouter('/versioncheck');
|
||||
|
||||
const validVersion = '20191120';
|
||||
|
||||
enum VersionStatus {
|
||||
UpdateRequired,
|
||||
ValidForMenu,
|
||||
ValidForPlay
|
||||
}
|
||||
type ValidVersionResponse = {
|
||||
ValidVersion: boolean
|
||||
VersionStatus: VersionStatus
|
||||
}
|
||||
|
||||
route.router.get('/v3', (rq, rs) => {
|
||||
route.router.get('/v4', (rq, rs) => {
|
||||
const requestedVer = rq.query['v'];
|
||||
if (typeof requestedVer !== 'string' || requestedVer !== validVersion) {
|
||||
if (typeof requestedVer == 'undefined') {
|
||||
rs.statusCode = 400;
|
||||
rs.json(APIUtils.genericResponseFormat(true, 'One or more query parameters were not found.'));
|
||||
}
|
||||
else if (requestedVer !== validVersion) {
|
||||
const res: ValidVersionResponse = {
|
||||
ValidVersion: false
|
||||
VersionStatus: VersionStatus.UpdateRequired
|
||||
}
|
||||
rs.json(res);
|
||||
} else {
|
||||
const res: ValidVersionResponse = {
|
||||
ValidVersion: true
|
||||
VersionStatus: VersionStatus.ValidForPlay
|
||||
}
|
||||
rs.json(res);
|
||||
}
|
||||
|
||||
@@ -20,19 +20,18 @@ type NameserverHosts = {
|
||||
Leaderboard: string
|
||||
}
|
||||
|
||||
const path = `${protocol}://${config.web.nameserverHost}`;
|
||||
const nameserver: NameserverHosts = {
|
||||
Auth: path,
|
||||
API: path,
|
||||
WWW: path,
|
||||
Notifications: path,
|
||||
Images: path,
|
||||
CDN: path,
|
||||
Commerce: path,
|
||||
Matchmaking: path,
|
||||
Storage: path,
|
||||
Chat: path,
|
||||
Leaderboard: path
|
||||
Auth: `${protocol}://${config.web.nameserverHost}/auth`,
|
||||
API: `${protocol}://${config.web.nameserverHost}`,
|
||||
WWW: `${protocol}://${config.web.nameserverHost}`,
|
||||
Notifications: `${protocol}://${config.web.nameserverHost}/notify`,
|
||||
Images: `${protocol}://${config.web.nameserverHost}/img`,
|
||||
CDN: `${protocol}://${config.web.nameserverHost}/cdn`,
|
||||
Commerce: `${protocol}://${config.web.nameserverHost}/commerce`,
|
||||
Matchmaking: `${protocol}://${config.web.nameserverHost}/match`,
|
||||
Storage: `${protocol}://${config.web.nameserverHost}/storage`,
|
||||
Chat: `${protocol}://${config.web.nameserverHost}/chat`,
|
||||
Leaderboard: `${protocol}://${config.web.nameserverHost}/leaderboard`
|
||||
}
|
||||
|
||||
route.router.get('*', (_rq, rs) => {
|
||||
|
||||
53
src/routes/user.ts
Normal file
53
src/routes/user.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { APIUtils, getSrcIpDefault } from "../apiutils.ts";
|
||||
// @ts-types = "npm:@types/express"
|
||||
import express from "express";
|
||||
import { User } from "../data/users.ts";
|
||||
import Recaptcha from "../data/recaptcha.ts";
|
||||
|
||||
export const route = APIUtils.createRouter('/user');
|
||||
|
||||
type CreateUserBody = {
|
||||
username: string,
|
||||
password: string,
|
||||
recaptcha: string
|
||||
}
|
||||
|
||||
type CreatedUserResponse = {
|
||||
uuid: string,
|
||||
backupcode: string
|
||||
}
|
||||
|
||||
const rateLimit = new APIUtils.RateLimiter(10, 1);
|
||||
|
||||
route.router.post('/create',
|
||||
|
||||
rateLimit.middle(),
|
||||
express.json(),
|
||||
APIUtils.checkBodyTypes<CreateUserBody>({ username: "test", password: "test", recaptcha: "test" }),
|
||||
|
||||
async (rq, rs) => {
|
||||
const body = rq.body as CreateUserBody;
|
||||
|
||||
const recaptchaStatus = await Recaptcha.siteVerify(body.recaptcha, getSrcIpDefault(rq));
|
||||
if (recaptchaStatus) {
|
||||
const userinit = await User.init({ username: body.username, password: body.password });
|
||||
if (userinit == null) {
|
||||
rs.statusCode = 400;
|
||||
rs.json(APIUtils.genericResponseFormat(true, "Username is already taken"));
|
||||
} else {
|
||||
|
||||
const res: CreatedUserResponse = {
|
||||
uuid: userinit.user.getUuid(),
|
||||
backupcode: userinit.backupcode
|
||||
}
|
||||
rs.json(APIUtils.genericResponseFormat(false, "User created successfully", res));
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
rs.statusCode = 400;
|
||||
rs.json(APIUtils.genericResponseFormat(true, "ReCAPTCHA error"));
|
||||
}
|
||||
}
|
||||
|
||||
);
|
||||
Reference in New Issue
Block a user