Skip to content

Commit

Permalink
Added API endpoints for ED database
Browse files Browse the repository at this point in the history
  • Loading branch information
SwanX1 committed Oct 13, 2022
1 parent aecbcac commit 4d9cbbf
Show file tree
Hide file tree
Showing 11 changed files with 369 additions and 5 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "infernalstudios.org",
"version": "1.2.5",
"version": "1.3.0",
"private": true,
"description": "Source code for <https://infernalstudios.org>!",
"bugs": "https://github.com/infernalexp/infernalstudios.org/issues",
Expand Down
4 changes: 2 additions & 2 deletions scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"terser": "^5.10.0"
},
"engines": {
"node": "^16",
"yarn": "^1.22.0"
"node": ">=16",
"yarn": ">=1.22.0"
}
}
2 changes: 2 additions & 0 deletions spec/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,5 @@ paths:
$ref: "./paths/users_id.yml"
/users/self:
$ref: "./paths/users_self.yml"
/everythingdescriptions/contributions:
$ref: "./paths/everythingdescriptions_contributions.yml"
13 changes: 13 additions & 0 deletions spec/components/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,16 @@ user:
id: "peasant"
permissions: []
passwordChangeRequested: true

edcontributions:
summary: ED Contributions
value:
"everydesc.minecraft:coal":
- value: "Cool furnace thing!"
user: "discord_user_id"
isDiscord: true
id: "abcdefg"
- value: "Cool furnace thing but worded differently!"
user: "user_id"
isDiscord: false
id: "3125476980"
42 changes: 42 additions & 0 deletions spec/components/schemas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,48 @@ initial_user:
password:
$ref: "#/user/properties/password"

edcontribution:
type: object
required: [id, value, user, isDiscord]
properties:
id:
type: string
value:
type: string
user:
type: string
isDiscord:
type: boolean

initial_edcontribution:
type: object
required: [id, value, user, isDiscord]
properties:
value:
type: string
user:
type: string
isDiscord:
type: boolean

edcontributions:
type: object
properties:
a:
type: array
items:
type:
$ref: "#/edcontribution"

add_edcontributions:
type: object
properties:
a:
type: array
items:
type:
$ref: "#/initial_edcontribution"

error:
type: array
items:
Expand Down
82 changes: 82 additions & 0 deletions spec/paths/everythingdescriptions_contributions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
get:
summary: Get ED Contributions
description: |
Returns all contributions.
The contributions aren't sorted, they're returned in the order they were returned by the database.
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/edcontributions"
examples:
contributions:
$ref: "#/components/examples/edcontributions"
"500":
$ref: "#/components/responses/internal_server_error"
post:
summary: Add ED Contribution
description: |
Adds ED contributions.
This endpoint is rate-limited to 15 calls/min (0.25/sec) in order to prevent abuse.
security:
- token
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/initial_edcontributions"
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/edcontributions"
examples:
contributions:
$ref: "#/components/examples/edcontributions"
"400":
$ref: "#/components/responses/zod_error"
"403":
$ref: "#/components/responses/forbidden"
"419":
$ref: "#/components/responses/too_many_requests"
"500":
$ref: "#/components/responses/internal_server_error"
delete:
summary: Delete ED Contribution
security:
- token
parameters:
- name: id
in: path
required: true
schema:
type: string
description: The contribution's unique ID.
responses:
"204":
description: No Content
"400":
$ref: "#/components/responses/zod_error"
"401":
$ref: "#/components/responses/unauthorized"
"404":
description: Not Found
content:
application/json:
schema:
$ref: "#/components/schemas/error"
examples:
contribution_not_found:
summary: Contribution not found
value: { "errors": ["Contribution not found"] }
"419":
$ref: "#/components/responses/too_many_requests"
"500":
$ref: "#/components/responses/internal_server_error"
2 changes: 2 additions & 0 deletions src/api/APIManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getAuthAPI } from "./AuthAPI";
import { getModAPI } from "./ModAPI";
import { getRedirectAPI } from "./RedirectAPI";
import { StatusAPI } from "./StatusAPI";
import { getTempEDAPI } from "./TempEDAPI";
import { getUserAPI } from "./UserAPI";

export function getAPI(database: Database): Router {
Expand Down Expand Up @@ -64,6 +65,7 @@ export function getAPI(database: Database): Router {
api.use("/redirects", getRedirectAPI(database));
api.use("/mods", getModAPI(database));
api.use("/users", getUserAPI(database));
api.use("/everythingdescriptions", getTempEDAPI(database));

return api;
}
104 changes: 104 additions & 0 deletions src/api/TempEDAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) 2022 Infernal Studios, All Rights Reserved unless otherwise explicitly stated.
import express, { Router } from "express";
import { RateLimiterMemory } from "rate-limiter-flexible";
import { z } from "zod";
import { Database } from "../database/Database";
import { EDContribution } from "../database/EDContribution";
import { getAuthMiddleware, randomString } from "../util/Util";

export function getTempEDAPI(database: Database): Router {
const api = Router();

api.get("/contributions", async (_req, res) => {
res.status(200);
res.json(await database.temp_ed.getAllClean());
return res.end();
});

const postContributionsSchema = z.record(
z.string().refine(s => /^everydesc.[a-zA-Z0-9_-]+:[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$/.test(s)),
z.array(
z
.object({
value: z.string().max(2047),
user: z.string(),
isDiscord: z.boolean(),
})
.strict()
)
);

const rateLimit = new RateLimiterMemory({
points: 15,
duration: 60,
});

api.post("/contributions", (req, res, next) => {
rateLimit
.consume(req.ip)
.then(rateLimiterRes => {
res.setHeader("X-RateLimit-Limit", rateLimit.points);
res.setHeader("X-RateLimit-Remaining", rateLimiterRes.remainingPoints);
res.setHeader("X-RateLimit-Reset", Math.ceil(rateLimiterRes.msBeforeNext / 1000));
next();
})
.catch(rateLimiterRes => {
rateLimit.block(req.ip, Math.min(rateLimiterRes.msBeforeNext / 1000 + 12, 600));
res.setHeader("X-RateLimit-Limit", rateLimit.points);
res.setHeader("X-RateLimit-Remaining", 0);
res.setHeader("X-RateLimit-Reset", Math.ceil(rateLimiterRes.msBeforeNext / 1000));
res.status(429);
res.json({
errors: ["Too many requests"],
});
return res.end();
});
});

api.post("/contributions", express.json());
api.post("/contributions", getAuthMiddleware(database));
api.post("/contributions", async (req, res) => {
res.status(200);
const contributions = postContributionsSchema.parse(req.body);

const createContributionsPromises: Promise<EDContribution>[] = [];
for (const key in contributions) {
if (Object.prototype.hasOwnProperty.call(contributions, key)) {
const value = contributions[key];
for (const contribution of value) {
const promise = database.temp_ed.create({
id: randomString(),
key,
value: contribution.value,
user: contribution.user,
isDiscord: contribution.isDiscord,
});
createContributionsPromises.push(promise);
}
}
}

await Promise.all(createContributionsPromises);

res.json(await database.temp_ed.getAllClean());

return res.end();
});

api.delete("/contributions", getAuthMiddleware(database));
api.delete("/contributions", async (req, res) => {
if (typeof req.query.id !== "string") {
res.status(400);
res.json({
errors: [`Invalid id parameter`],
});
return res.end();
}

await database.temp_ed.delete(req.query.id);

return res.end();
});

return api;
}
33 changes: 31 additions & 2 deletions src/database/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import chalk from "chalk";
import knex, { Knex } from "knex";
import { coloredIdentifier, Logger } from "logerian";
import { createKnexLogger } from "../util/Util";
import { ModManager, RedirectManager, TokenManager, UserManager } from "./DatabaseManager";
import { EDContributionManager, ModManager, RedirectManager, TokenManager, UserManager } from "./DatabaseManager";
import { EDContributionSchema } from "./EDContribution";
import { ModSchema } from "./Mod";
import { RedirectSchema } from "./Redirect";
import { TokenSchema } from "./Token";
Expand All @@ -13,6 +14,7 @@ import { SQLVersionSchema } from "./Version";
declare module "knex/types/tables" {
interface Tables {
mods: ModSchema;
temp_ed: EDContributionSchema;
redirects: RedirectSchema;
tokens: TokenSchema;
users: UserSchema;
Expand All @@ -34,6 +36,7 @@ export class Database {
public redirects: RedirectManager = new RedirectManager(this);
public tokens: TokenManager = new TokenManager(this);
public users: UserManager = new UserManager(this);
public temp_ed: EDContributionManager = new EDContributionManager(this);

public constructor(options: DatabaseOptions) {
this.options = options;
Expand All @@ -58,6 +61,7 @@ export class Database {
await this.redirects.getAllJSON();
await this.tokens.getAllJSON();
await this.users.getAllJSON();
await this.temp_ed.getAllJSON();

if ((await this.users.getAll()).length === 0) {
this.logger.warn("No users found in database");
Expand Down Expand Up @@ -98,7 +102,19 @@ export class Database {
this.logger.debug(chalk`Table {green 'tokens'} setup`);
});

await Promise.all([modsPromise, versionsPromise, redirectsPromise, usersPromise, tokensPromise]);
this.logger.debug(chalk`Setting up table {green 'temp_ed'}...`);
const edContributionsPromise = this.setupTempEDTable().then(() =>
this.logger.debug(chalk`Table {green 'temp_ed'} setup`)
);

await Promise.all([
modsPromise,
versionsPromise,
redirectsPromise,
usersPromise,
tokensPromise,
edContributionsPromise,
]);

this.logger.info("Database setup complete!");
}
Expand Down Expand Up @@ -171,4 +187,17 @@ export class Database {
});
}
}

private async setupTempEDTable(): Promise<void> {
if (!(await this.sql.schema.hasTable("temp_ed"))) {
this.logger.debug("Table temp_ed doesn't exist, creating...");
await this.sql.schema.createTable("temp_ed", table => {
table.string("id", 32).index().primary().notNullable();
table.string("key", 255).notNullable();
table.string("value", 2047).notNullable();
table.string("user", 255).notNullable();
table.boolean("isDiscord").notNullable().defaultTo(false);
});
}
}
}
Loading

0 comments on commit 4d9cbbf

Please sign in to comment.