1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-05 10:22:31 +01:00

generate openapi documentation

This commit is contained in:
Flam3rboy 2021-09-21 22:52:30 +02:00
parent eb2f447d96
commit 2a094c603a
19 changed files with 7419 additions and 1052 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,90 +0,0 @@
{
"UserProfileResponse": {
"type": "object",
"properties": {
"user": {
"$ref": "#/definitions/UserPublic"
},
"connected_accounts": {
"$ref": "#/definitions/PublicConnectedAccount"
},
"premium_guild_since": {
"type": "string",
"format": "date-time"
},
"premium_since": {
"type": "string",
"format": "date-time"
}
},
"additionalProperties": false,
"required": [
"connected_accounts",
"user"
],
"definitions": {
"UserPublic": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"discriminator": {
"type": "string"
},
"id": {
"type": "string"
},
"public_flags": {
"type": "string"
},
"avatar": {
"type": "string"
},
"accent_color": {
"type": "integer"
},
"banner": {
"type": "string"
},
"bio": {
"type": "string"
},
"bot": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"bio",
"bot",
"discriminator",
"id",
"public_flags",
"username"
]
},
"PublicConnectedAccount": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"type": {
"type": "string"
},
"verifie": {
"type": "boolean"
}
},
"additionalProperties": false,
"required": [
"name",
"type",
"verifie"
]
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,15 @@
import { traverseDirectory } from "lambert-server";
import path from "path";
import express from "express";
import * as RouteUtility from "../dist/util/route";
import { RouteOptions } from "../dist/util/route";
const { traverseDirectory } = require("lambert-server");
const path = require("path");
const express = require("express");
const RouteUtility = require("../dist/util/route");
const Router = express.Router;
const routes = new Map<string, RouteUtility.RouteOptions>();
/**
* Some documentation.
*
* @type {Map<string, RouteUtility.RouteOptions>}
*/
const routes = new Map();
let currentPath = "";
let currentFile = "";
const methods = ["get", "post", "put", "delete", "patch"];
@ -13,13 +17,13 @@ const methods = ["get", "post", "put", "delete", "patch"];
function registerPath(file, method, prefix, path, ...args) {
const urlPath = prefix + path;
const sourceFile = file.replace("/dist/", "/src/").replace(".js", ".ts");
const opts: RouteOptions = args.find((x) => typeof x === "object");
const opts = args.find((x) => typeof x === "object");
if (opts) {
routes.set(urlPath + "|" + method, opts); // @ts-ignore
opts.file = sourceFile;
// console.log(method, urlPath, opts);
} else {
console.log(`${sourceFile}\nrouter.${method}("${path}") is missing the "route()" description middleware\n`, args);
console.log(`${sourceFile}\nrouter.${method}("${path}") is missing the "route()" description middleware\n`);
}
}
@ -42,7 +46,7 @@ express.Router = (opts) => {
return router;
};
export default function getRouteDescriptions() {
module.exports = function getRouteDescriptions() {
const root = path.join(__dirname, "..", "dist", "routes", "/");
traverseDirectory({ dirname: root, recursive: true }, (file) => {
currentFile = file;
@ -52,7 +56,11 @@ export default function getRouteDescriptions() {
if (path.endsWith("/index")) path = path.slice(0, "/index".length * -1); // delete index from path
currentPath = path;
require(file);
try {
require(file);
} catch (error) {
console.error("error loading file " + file, error);
}
});
return routes;
}
};

View File

@ -4,9 +4,9 @@ import path from "path";
import fs from "fs";
import * as TJS from "typescript-json-schema";
import "missing-native-js-functions";
const schemaPath = path.join(__dirname, "..", "assets", "responses.json");
const schemaPath = path.join(__dirname, "..", "assets", "schemas.json");
const settings: TJS.PartialArgs = {
const settings = {
required: true,
ignoreErrors: true,
excludePrivate: true,
@ -14,10 +14,13 @@ const settings: TJS.PartialArgs = {
noExtraProps: true,
defaultProps: false
};
const compilerOptions: TJS.CompilerOptions = {
const compilerOptions = {
strictNullChecks: true
};
const ExcludedSchemas = [
const Excluded = [
"DefaultSchema",
"Schema",
"EntitySchema",
"ServerResponse",
"Http2ServerResponse",
"global.Express.Response",
@ -32,13 +35,13 @@ function main() {
const generator = TJS.buildGenerator(program, settings);
if (!generator || !program) return;
const schemas = generator.getUserSymbols().filter((x) => x.endsWith("Response") && !ExcludedSchemas.includes(x));
const schemas = generator.getUserSymbols().filter((x) => (x.endsWith("Schema") || x.endsWith("Response")) && !Excluded.includes(x));
console.log(schemas);
var definitions: any = {};
var definitions = {};
for (const name of schemas) {
const part = TJS.generateSchema(program, name, settings, [], generator as TJS.JsonSchemaGenerator);
const part = TJS.generateSchema(program, name, settings, [], generator);
if (!part) continue;
definitions = { ...definitions, [name]: { ...part } };
@ -47,11 +50,10 @@ function main() {
fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4));
}
// #/definitions/
main();
function walk(dir: string) {
var results = [] as string[];
function walk(dir) {
var results = [];
var list = fs.readdirSync(dir);
list.forEach(function (file) {
file = dir + "/" + file;

View File

@ -1,60 +0,0 @@
// https://mermade.github.io/openapi-gui/#
// https://editor.swagger.io/
import path from "path";
import fs from "fs";
import * as TJS from "typescript-json-schema";
import "missing-native-js-functions";
const schemaPath = path.join(__dirname, "..", "assets", "schemas.json");
const settings: TJS.PartialArgs = {
required: true,
ignoreErrors: true,
excludePrivate: true,
defaultNumberType: "integer",
noExtraProps: true,
defaultProps: false
};
const compilerOptions: TJS.CompilerOptions = {
strictNullChecks: true
};
const ExcludedSchemas = ["DefaultSchema", "Schema", "EntitySchema"];
function main() {
const program = TJS.getProgramFromFiles(walk(path.join(__dirname, "..", "src", "routes")), compilerOptions);
const generator = TJS.buildGenerator(program, settings);
if (!generator || !program) return;
const schemas = generator.getUserSymbols().filter((x) => x.endsWith("Schema") && !ExcludedSchemas.includes(x));
console.log(schemas);
var definitions: any = {};
for (const name of schemas) {
const part = TJS.generateSchema(program, name, settings, [], generator as TJS.JsonSchemaGenerator);
if (!part) continue;
definitions = { ...definitions, [name]: { ...part } };
}
fs.writeFileSync(schemaPath, JSON.stringify(definitions, null, 4));
}
// #/definitions/
main();
function walk(dir: string) {
var results = [] as string[];
var list = fs.readdirSync(dir);
list.forEach(function (file) {
file = dir + "/" + file;
var stat = fs.statSync(file);
if (stat && stat.isDirectory()) {
/* Recurse into a subdirectory */
results = results.concat(walk(file));
} else {
if (!file.endsWith(".ts")) return;
results.push(file);
}
});
return results;
}

View File

@ -0,0 +1,127 @@
// https://mermade.github.io/openapi-gui/#
// https://editor.swagger.io/
const getRouteDescriptions = require("../jest/getRouteDescriptions");
const path = require("path");
const fs = require("fs");
require("missing-native-js-functions");
const openapiPath = path.join(__dirname, "..", "assets", "openapi.json");
const SchemaPath = path.join(__dirname, "..", "assets", "schemas.json");
const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
const specification = JSON.parse(fs.readFileSync(openapiPath, { encoding: "utf8" }));
function combineSchemas(schemas) {
var definitions = {};
for (const name in schemas) {
definitions = {
...definitions,
...schemas[name].definitions,
[name]: { ...schemas[name], definitions: undefined, $schema: undefined }
};
}
for (const key in definitions) {
specification.components.schemas[key] = definitions[key];
delete definitions[key].additionalProperties;
delete definitions[key].$schema;
const definition = definitions[key];
if (typeof definition.properties === "object") {
for (const property of Object.values(definition.properties)) {
if (Array.isArray(property.type)) {
if (property.type.includes("null")) {
property.type = property.type.find((x) => x !== "null");
property.nullable = true;
}
}
}
}
}
return definitions;
}
function getTag(key) {
return key.match(/\/([\w-]+)/)[1];
}
function apiRoutes() {
const routes = getRouteDescriptions();
const tags = Array.from(routes.keys()).map((x) => getTag(x));
specification.tags = [...specification.tags.map((x) => x.name), ...tags].unique().map((x) => ({ name: x }));
routes.forEach((route, pathAndMethod) => {
const [p, method] = pathAndMethod.split("|");
const path = p.replace(/:(\w+)/g, "{$1}");
let obj = specification.paths[path]?.[method] || {};
if (!obj.description) {
const permission = route.permission ? `##### Requires the \`\`${route.permission}\`\` permission\n` : "";
const event = route.test?.event ? `##### Fires a \`\`${route.test?.event}\`\` event\n` : "";
obj.description = permission + event;
}
if (route.body) {
obj.requestBody = {
required: true,
content: {
"application/json": {
schema: { $ref: `#/components/schemas/${route.body}` }
}
}
}.merge(obj.requestBody);
}
if (!obj.responses) {
obj.responses = {
default: {
description: "not documented"
}
};
}
if (route.test?.response) {
const status = route.test.response.status || 200;
obj.responses = {
[status]: {
...(route.test.response.body
? {
description: obj.responses[status].description || "",
content: {
"application/json": {
schema: {
$ref: `#/components/schemas/${route.test.response.body}`
}
}
}
}
: {})
}
}.merge(obj.responses);
delete obj.responses.default;
}
if (p.includes(":")) {
obj.parameters = p.match(/:\w+/g)?.map((x) => ({
name: x.replace(":", ""),
in: "path",
required: true,
schema: { type: "string" },
description: x.replace(":", "")
}));
}
obj.tags = [...(obj.tags || []), getTag(p)].unique();
specification.paths[path] = { ...specification.paths[path], [method]: obj };
});
}
function main() {
combineSchemas(schemas);
apiRoutes();
fs.writeFileSync(
openapiPath,
JSON.stringify(specification, null, 4).replaceAll("#/definitions", "#/components/schemas").replaceAll("bigint", "number")
);
}
main();

View File

@ -1,92 +0,0 @@
// https://mermade.github.io/openapi-gui/#
// https://editor.swagger.io/
import path from "path";
import fs from "fs";
import * as TJS from "typescript-json-schema";
import "missing-native-js-functions";
const settings: TJS.PartialArgs = {
required: true,
ignoreErrors: true,
excludePrivate: true,
defaultNumberType: "integer",
noExtraProps: true,
defaultProps: false
};
const compilerOptions: TJS.CompilerOptions = {
strictNullChecks: false
};
const openapiPath = path.join(__dirname, "..", "assets", "openapi.json");
var specification = JSON.parse(fs.readFileSync(openapiPath, { encoding: "utf8" }));
async function utilSchemas() {
const program = TJS.getProgramFromFiles([path.join(__dirname, "..", "..", "util", "src", "index.ts")], compilerOptions);
const generator = TJS.buildGenerator(program, settings);
const schemas = ["UserPublic", "UserPrivate", "PublicConnectedAccount"];
// @ts-ignore
combineSchemas({ schemas, generator, program });
}
function combineSchemas(opts: { program: TJS.Program; generator: TJS.JsonSchemaGenerator; schemas: string[] }) {
var definitions: any = {};
for (const name of opts.schemas) {
const part = TJS.generateSchema(opts.program, name, settings, [], opts.generator as TJS.JsonSchemaGenerator);
if (!part) continue;
definitions = { ...definitions, [name]: { ...part, definitions: undefined, $schema: undefined } };
}
for (const key in definitions) {
specification.components.schemas[key] = definitions[key];
delete definitions[key].additionalProperties;
delete definitions[key].$schema;
}
return definitions;
}
const ExcludedSchemas = [
"DefaultSchema",
"Schema",
"EntitySchema",
"ServerResponse",
"Http2ServerResponse",
"global.Express.Response",
"Response",
"e.Response",
"request.Response",
"supertest.Response"
];
function apiSchemas() {
const program = TJS.getProgramFromFiles([path.join(__dirname, "..", "src", "schema", "index.ts")], compilerOptions);
const generator = TJS.buildGenerator(program, settings);
const schemas = generator
.getUserSymbols()
.filter((x) => x.endsWith("Response") && !ExcludedSchemas.includes(x))
.concat(generator.getUserSymbols().filter((x) => x.endsWith("Schema") && !ExcludedSchemas.includes(x)));
// @ts-ignore
combineSchemas({ schemas, generator, program });
}
function addDefaultResponses() {
Object.values(specification.paths).forEach((path: any) => Object.values(path).forEach((request: any) => {}));
}
function main() {
addDefaultResponses();
utilSchemas();
apiSchemas();
fs.writeFileSync(
openapiPath,
JSON.stringify(specification, null, 4).replaceAll("#/definitions", "#/components/schemas").replaceAll("bigint", "number")
);
}
main();

View File

@ -1,9 +1,7 @@
import { Request, Response, Router } from "express";
import { FieldErrors, route } from "@fosscord/api";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { Config, User } from "@fosscord/util";
import { adjustEmail } from "./register";
import { Config, User, generateToken, adjustEmail } from "@fosscord/util";
const router: Router = Router();
export default router;
@ -68,25 +66,6 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo
res.json({ token, settings: user.settings });
});
export async function generateToken(id: string) {
const iat = Math.floor(Date.now() / 1000);
const algorithm = "HS256";
return new Promise((res, rej) => {
jwt.sign(
{ id: id, iat },
Config.get().security.jwtSecret,
{
algorithm
},
(err, token) => {
if (err) return rej(err);
return res(token);
}
);
});
}
/**
* POST /auth/login
* @argument { login: "email@gmail.com", password: "cleartextpassword", undelete: false, captcha_key: null, login_source: null, gift_code_sku_id: null, }

View File

@ -1,10 +1,8 @@
import { Request, Response, Router } from "express";
import { trimSpecial, User, Snowflake, Config, defaultSettings, Member, Invite } from "@fosscord/util";
import { trimSpecial, User, Snowflake, Config, defaultSettings, generateToken, Invite, adjustEmail } from "@fosscord/util";
import bcrypt from "bcrypt";
import { EMAIL_REGEX, FieldErrors, route } from "@fosscord/api";
import { FieldErrors, route, getIpAdress, IPAnalysis, isProxy } from "@fosscord/api";
import "missing-native-js-functions";
import { generateToken } from "./login";
import { getIpAdress, IPAnalysis, isProxy } from "@fosscord/api";
import { HTTPError } from "lambert-server";
const router: Router = Router();
@ -228,24 +226,6 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
return res.json({ token: await generateToken(user.id) });
});
export function adjustEmail(email: string): string | undefined {
if (!email) return email;
// body parser already checked if it is a valid email
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
// @ts-ignore
if (!parts || parts.length < 5) return undefined;
const domain = parts[5];
const user = parts[1];
// TODO: check accounts with uncommon email domains
if (domain === "gmail.com" || domain === "googlemail.com") {
// replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
return user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
}
return email;
}
export default router;
/**

View File

@ -1,9 +1,10 @@
import { Request, Response, Router } from "express";
import { Channel, ChannelRecipientAddEvent, ChannelType, DiscordApiErrors, DmChannelDTO, emitEvent, PublicUserProjection, Recipient, User } from "@fosscord/util";
import { route } from "@fosscord/api"
const router: Router = Router();
router.put("/:user_id", async (req: Request, res: Response) => {
router.put("/:user_id", route({}), async (req: Request, res: Response) => {
const { channel_id, user_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] });
@ -39,7 +40,7 @@ router.put("/:user_id", async (req: Request, res: Response) => {
}
});
router.delete("/:user_id", async (req: Request, res: Response) => {
router.delete("/:user_id", route({}), async (req: Request, res: Response) => {
const { channel_id, user_id } = req.params;
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] });
if (!(channel.type === ChannelType.GROUP_DM && (channel.owner_id === req.user_id || user_id === req.user_id)))

View File

@ -4,14 +4,14 @@ import { Request, Response, Router } from "express";
const router = Router();
router.delete("/:member_id/roles/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
const { guild_id, role_id, member_id } = req.params;
await Member.removeRole(member_id, guild_id, role_id);
res.sendStatus(204);
});
router.put("/:member_id/roles/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
router.put("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
const { guild_id, role_id, member_id } = req.params;
await Member.addRole(member_id, guild_id, role_id);

View File

@ -1,8 +1,9 @@
import { Request, Response, Router } from "express";
import { route } from "@fosscord/api";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
router.get("/", route({}), async (req: Request, res: Response) => {
//TODO
res.json({
id: "",
@ -15,4 +16,4 @@ router.get("/", async (req: Request, res: Response) => {
}).status(200);
});
export default router;
export default router;

View File

@ -1,10 +1,11 @@
import { Request, Response, Router } from "express";
import { route } from "@fosscord/api";
const router: Router = Router();
router.get("/", async (req: Request, res: Response) => {
router.get("/", route({}), async (req: Request, res: Response) => {
//TODO
res.json({ sticker_packs: [] }).status(200);
});
export default router;
export default router;

View File

@ -1,8 +1,6 @@
import { Request } from "express";
import { ntob } from "./Base64";
import { FieldErrors } from "./FieldError";
export const EMAIL_REGEX =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export function checkLength(str: string, min: number, max: number, key: string, req: Request) {
if (str.length < min || str.length > max) {

View File

@ -9,7 +9,7 @@ import addFormats from "ajv-formats";
import fetch from "node-fetch";
import { User } from "@fosscord/util";
const SchemaPath = join(__dirname, "..", "assets", "responses.json");
const SchemaPath = join(__dirname, "..", "assets", "schemas.json");
const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
export const ajv = new Ajv({
allErrors: true,
@ -64,7 +64,7 @@ describe("Automatic unit tests with route description middleware", () => {
routes.forEach((route, pathAndMethod) => {
const [path, method] = pathAndMethod.split("|");
test(path, async (done) => {
test(`${method.toUpperCase()} ${path}`, async (done) => {
if (!route.test) {
console.log(`${(route as any).file}\nrouter.${method} is missing the test property`);
return done();

20
util/src/util/Email.ts Normal file
View File

@ -0,0 +1,20 @@
export const EMAIL_REGEX =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export function adjustEmail(email: string): string | undefined {
if (!email) return email;
// body parser already checked if it is a valid email
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
// @ts-ignore
if (!parts || parts.length < 5) return undefined;
const domain = parts[5];
const user = parts[1];
// TODO: check accounts with uncommon email domains
if (domain === "gmail.com" || domain === "googlemail.com") {
// replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
return user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
}
return email;
}

View File

@ -1,4 +1,5 @@
import jwt, { VerifyOptions } from "jsonwebtoken";
import { Config } from "./Config";
import { User } from "../entities";
export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
@ -21,3 +22,22 @@ export function checkToken(token: string, jwtSecret: string): Promise<any> {
});
});
}
export async function generateToken(id: string) {
const iat = Math.floor(Date.now() / 1000);
const algorithm = "HS256";
return new Promise((res, rej) => {
jwt.sign(
{ id: id, iat },
Config.get().security.jwtSecret,
{
algorithm,
},
(err, token) => {
if (err) return rej(err);
return res(token);
}
);
});
}

View File

@ -1,11 +1,12 @@
export * from "./ApiError";
export * from "./BitField";
export * from "./checkToken";
export * from "./Token";
export * from "./cdn";
export * from "./Config";
export * from "./Constants";
export * from "./Database";
export * from "./Event";
export * from "./Email";
export * from "./Intents";
export * from "./MessageFlags";
export * from "./Permissions";