Refactor for using koa and dicebear
This commit is contained in:
parent
41f480e2a6
commit
1840e5a786
17 changed files with 976 additions and 1129 deletions
24
src/config/components/general.config.ts
Normal file
24
src/config/components/general.config.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import joi from "joi";
|
||||
import { Config } from "../../types/config";
|
||||
import { server } from "./server.config";
|
||||
|
||||
const envSchema = joi
|
||||
.object({
|
||||
NODE_ENV: joi
|
||||
.string()
|
||||
.allow("development", "production", "test")
|
||||
.required(),
|
||||
})
|
||||
.unknown()
|
||||
.required();
|
||||
|
||||
const { error, value: envVars } = envSchema.validate(process.env);
|
||||
if (error) {
|
||||
throw new Error(`Config validation error: ${error.message}`);
|
||||
}
|
||||
|
||||
export const general: Config = {
|
||||
env: envVars.NODE_ENV,
|
||||
isDevelopment: envVars.NODE_ENV === "development",
|
||||
server,
|
||||
};
|
24
src/config/components/server.config.ts
Normal file
24
src/config/components/server.config.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import joi from "joi";
|
||||
import { ServerConfig } from "../../types/config";
|
||||
|
||||
const envSchema = joi
|
||||
.object({
|
||||
HOST: joi.string().default("localhost"),
|
||||
PORT: joi.number().default(3000),
|
||||
API_VERSION: joi.number().default(1),
|
||||
ALLOWED_ORIGINS: joi.string().default("http://localhost:3000"),
|
||||
})
|
||||
.unknown()
|
||||
.required();
|
||||
|
||||
const { error, value: envVars } = envSchema.validate(process.env);
|
||||
if (error) {
|
||||
throw new Error(`Config validation error: ${error.message}`);
|
||||
}
|
||||
|
||||
export const server: ServerConfig = {
|
||||
host: envVars.HOST,
|
||||
port: envVars.PORT,
|
||||
apiVersion: envVars.API_VERSION,
|
||||
origins: envVars.ALLOWED_ORIGINS.split(","),
|
||||
};
|
3
src/config/index.ts
Normal file
3
src/config/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import "dotenv/config";
|
||||
import { general } from "./components/general.config";
|
||||
export default general;
|
20
src/index.ts
Normal file
20
src/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import Koa from "koa";
|
||||
import logger from "koa-logger";
|
||||
|
||||
import config from "./config";
|
||||
import corsMiddleware from "./middlewares/cors.middleware";
|
||||
import errorMiddleware from "./middlewares/error.middleware";
|
||||
import avatarRoute from "./routes/avatar.route";
|
||||
import { startServerLog } from "./utils/server";
|
||||
|
||||
const { server } = config;
|
||||
const app = new Koa();
|
||||
|
||||
// Middlewares
|
||||
app.use(corsMiddleware());
|
||||
app.use(logger());
|
||||
app.use(errorMiddleware());
|
||||
|
||||
app.use(avatarRoute.routes()).use(avatarRoute.allowedMethods());
|
||||
|
||||
app.listen(server.port, server.host, startServerLog(server.port));
|
20
src/middlewares/cors.middleware.ts
Normal file
20
src/middlewares/cors.middleware.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import cors from "@koa/cors";
|
||||
import { Context } from "koa";
|
||||
import Router from "koa-router";
|
||||
|
||||
import config from "../config";
|
||||
|
||||
const { server } = config;
|
||||
|
||||
const origin = (ctx: Context) => {
|
||||
const [defaultOrigin] = server.origins;
|
||||
if (ctx.headers.origin && server.origins.indexOf(ctx.headers.origin) > -1) {
|
||||
return ctx.header.origin || defaultOrigin;
|
||||
}
|
||||
return defaultOrigin;
|
||||
};
|
||||
|
||||
const corsMiddleware: () => Router.IMiddleware<any, {}> = () =>
|
||||
cors({ origin });
|
||||
|
||||
export default corsMiddleware;
|
24
src/middlewares/error.middleware.ts
Normal file
24
src/middlewares/error.middleware.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Router from "koa-router";
|
||||
import { isValidationError } from "../utils/type-guards";
|
||||
|
||||
const errorMiddleware: () => Router.IMiddleware<any, {}> =
|
||||
() => async (ctx, next) => {
|
||||
try {
|
||||
await next();
|
||||
} catch (err) {
|
||||
console.log("error middleware", err);
|
||||
if (err && (err as any).status >= 500) console.log("Error handler:", err);
|
||||
if (isValidationError(err)) {
|
||||
ctx.status = 400;
|
||||
} else {
|
||||
ctx.status = (err as any).status || 500;
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
status: "failed",
|
||||
message: (err as any).message || "Internal server error",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default errorMiddleware;
|
63
src/routes/avatar.route.ts
Normal file
63
src/routes/avatar.route.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import Router from "koa-router";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { createAvatar, Style } from "@dicebear/core";
|
||||
import * as openPeeps from "@dicebear/open-peeps";
|
||||
import * as pixelArt from "@dicebear/pixel-art";
|
||||
import * as thumbs from "@dicebear/thumbs";
|
||||
import sharp from "sharp";
|
||||
|
||||
import { isValue, isSize, isStyle } from "../utils/type-guards";
|
||||
|
||||
const DENSITY = 72;
|
||||
const avatars: Record<string, Style<any> & { defaultSize: number }> = {
|
||||
"open-peeps": { ...openPeeps, defaultSize: 704 },
|
||||
"pixel-art": { ...pixelArt, defaultSize: 16 },
|
||||
thumbs: { ...thumbs, defaultSize: 100 },
|
||||
};
|
||||
|
||||
const validStyles = Object.keys(avatars);
|
||||
const isValidStyle = isStyle(validStyles);
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.get("/list", async (ctx) => {
|
||||
const styles = validStyles.map((key) => ({ ...avatars[key].meta, key }));
|
||||
ctx.body = { styles };
|
||||
});
|
||||
|
||||
router.use(async (ctx, next) => {
|
||||
ctx.set({
|
||||
Expires: new Date(Date.now() + 604800000).toUTCString(),
|
||||
});
|
||||
ctx.type = "image/png";
|
||||
await next();
|
||||
});
|
||||
|
||||
router.get("/random", async (ctx) => {
|
||||
const { style = "thumbs", size = "100" } = ctx.query;
|
||||
if (!isValidStyle(style) || !isSize(size)) {
|
||||
throw Error("Invalid query");
|
||||
}
|
||||
const seed = uuidv4();
|
||||
const density = (DENSITY * parseInt(size)) / avatars[style].defaultSize;
|
||||
ctx.body = sharp(
|
||||
Buffer.from(createAvatar(avatars[style], { seed }).toString()),
|
||||
{ density }
|
||||
).png();
|
||||
});
|
||||
|
||||
router.get("/:seed", async (ctx) => {
|
||||
const { seed, style = "thumbs", size = "100" } = ctx.query;
|
||||
if (!isValue(seed) || !isValidStyle(style) || !isSize(size)) {
|
||||
throw Error("Invalid query");
|
||||
}
|
||||
const density = (DENSITY * parseInt(size)) / avatars[style].defaultSize;
|
||||
ctx.body = sharp(
|
||||
Buffer.from(createAvatar(avatars[style], { seed }).toString()),
|
||||
{
|
||||
density,
|
||||
}
|
||||
).png();
|
||||
});
|
||||
|
||||
export default router;
|
12
src/types/config.ts
Normal file
12
src/types/config.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export type ServerConfig = {
|
||||
host: string;
|
||||
port: number;
|
||||
origins: ReadonlyArray<string>;
|
||||
apiVersion: number;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
env: string;
|
||||
isDevelopment: boolean;
|
||||
server: ServerConfig;
|
||||
};
|
20
src/utils/server.ts
Normal file
20
src/utils/server.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { NetworkInterfaceInfo, networkInterfaces } from "os";
|
||||
|
||||
export const startServerLog = (port: number) => async () => {
|
||||
const net = Object.values(networkInterfaces())
|
||||
.flat()
|
||||
.filter((v) => v?.family === "IPv4")
|
||||
.sort((v) => (v!.internal ? -1 : 1)) as Array<NetworkInterfaceInfo>;
|
||||
|
||||
console.info("Server started successfully!");
|
||||
console.info("You can now use the service.");
|
||||
|
||||
net.forEach(({ internal, address }) =>
|
||||
console.info(
|
||||
`\t${(internal ? "Local:" : "On Your Network:").padEnd(
|
||||
20,
|
||||
" "
|
||||
)}http://${address}:${port}`
|
||||
)
|
||||
);
|
||||
};
|
18
src/utils/type-guards.ts
Normal file
18
src/utils/type-guards.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { ValidationError } from "joi";
|
||||
|
||||
export const isValidationError = (error: unknown): error is ValidationError =>
|
||||
error instanceof ValidationError;
|
||||
|
||||
export const isValue = <T>(value: T | Array<T> | undefined): value is T =>
|
||||
!!value && !Array.isArray(value);
|
||||
|
||||
export const isSize = (value: unknown): value is string =>
|
||||
!Array.isArray(value) &&
|
||||
!isNaN(parseInt(value as string)) &&
|
||||
parseInt(value as string) <= 512 &&
|
||||
parseInt(value as string) >= 16;
|
||||
|
||||
export const isStyle =
|
||||
(validStyles: string[]) =>
|
||||
<T>(value: T | Array<T> | undefined): value is T =>
|
||||
!!value && !Array.isArray(value) && validStyles.includes(value as string);
|
Loading…
Add table
Add a link
Reference in a new issue