base backend setup
This commit is contained in:
parent
a83c05aa01
commit
debe0f13a2
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
@ -7,12 +7,16 @@
|
|||||||
"start": "bun run src/index.ts"
|
"start": "bun run src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bogeychan/elysia-etag": "^0.0.6",
|
||||||
"@elysiajs/cors": "^1.1.1",
|
"@elysiajs/cors": "^1.1.1",
|
||||||
|
"@elysiajs/swagger": "^1.1.3",
|
||||||
"@ssr/common": "workspace:common",
|
"@ssr/common": "workspace:common",
|
||||||
"@tqman/nice-logger": "^1.0.1",
|
"@tqman/nice-logger": "^1.0.1",
|
||||||
"elysia": "latest",
|
"elysia": "latest",
|
||||||
"elysia-autoroutes": "^0.5.0",
|
"elysia-autoroutes": "^0.5.0",
|
||||||
"elysia-decorators": "^1.0.2"
|
"elysia-decorators": "^1.0.2",
|
||||||
|
"elysia-helmet": "^2.0.0",
|
||||||
|
"elysia-rate-limit": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bun-types": "latest"
|
"bun-types": "latest"
|
||||||
|
75
projects/backend/src/common/http-codes.ts
Normal file
75
projects/backend/src/common/http-codes.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
export const HttpCode = {
|
||||||
|
// 1xx Informational
|
||||||
|
CONTINUE: { code: 100, message: "Continue" },
|
||||||
|
SWITCHING_PROTOCOLS: { code: 101, message: "Switching Protocols" },
|
||||||
|
PROCESSING: { code: 102, message: "Processing" },
|
||||||
|
EARLY_HINTS: { code: 103, message: "Early Hints" },
|
||||||
|
|
||||||
|
// 2xx Success
|
||||||
|
OK: { code: 200, message: "OK" },
|
||||||
|
CREATED: { code: 201, message: "Created" },
|
||||||
|
ACCEPTED: { code: 202, message: "Accepted" },
|
||||||
|
NON_AUTHORITATIVE_INFORMATION: { code: 203, message: "Non-Authoritative Information" },
|
||||||
|
NO_CONTENT: { code: 204, message: "No Content" },
|
||||||
|
RESET_CONTENT: { code: 205, message: "Reset Content" },
|
||||||
|
PARTIAL_CONTENT: { code: 206, message: "Partial Content" },
|
||||||
|
MULTI_STATUS: { code: 207, message: "Multi-Status" },
|
||||||
|
ALREADY_REPORTED: { code: 208, message: "Already Reported" },
|
||||||
|
IM_USED: { code: 226, message: "IM Used" },
|
||||||
|
|
||||||
|
// 3xx Redirection
|
||||||
|
MULTIPLE_CHOICES: { code: 300, message: "Multiple Choices" },
|
||||||
|
MOVED_PERMANENTLY: { code: 301, message: "Moved Permanently" },
|
||||||
|
FOUND: { code: 302, message: "Found" },
|
||||||
|
SEE_OTHER: { code: 303, message: "See Other" },
|
||||||
|
NOT_MODIFIED: { code: 304, message: "Not Modified" },
|
||||||
|
USE_PROXY: { code: 305, message: "Use Proxy" },
|
||||||
|
TEMPORARY_REDIRECT: { code: 307, message: "Temporary Redirect" },
|
||||||
|
PERMANENT_REDIRECT: { code: 308, message: "Permanent Redirect" },
|
||||||
|
|
||||||
|
// 4xx Client Errors
|
||||||
|
BAD_REQUEST: { code: 400, message: "Bad Request" },
|
||||||
|
UNAUTHORIZED: { code: 401, message: "Unauthorized" },
|
||||||
|
PAYMENT_REQUIRED: { code: 402, message: "Payment Required" },
|
||||||
|
FORBIDDEN: { code: 403, message: "Forbidden" },
|
||||||
|
NOT_FOUND: { code: 404, message: "Not Found" },
|
||||||
|
METHOD_NOT_ALLOWED: { code: 405, message: "Method Not Allowed" },
|
||||||
|
NOT_ACCEPTABLE: { code: 406, message: "Not Acceptable" },
|
||||||
|
PROXY_AUTHENTICATION_REQUIRED: { code: 407, message: "Proxy Authentication Required" },
|
||||||
|
REQUEST_TIMEOUT: { code: 408, message: "Request Timeout" },
|
||||||
|
CONFLICT: { code: 409, message: "Conflict" },
|
||||||
|
GONE: { code: 410, message: "Gone" },
|
||||||
|
LENGTH_REQUIRED: { code: 411, message: "Length Required" },
|
||||||
|
PRECONDITION_FAILED: { code: 412, message: "Precondition Failed" },
|
||||||
|
PAYLOAD_TOO_LARGE: { code: 413, message: "Payload Too Large" },
|
||||||
|
URI_TOO_LONG: { code: 414, message: "URI Too Long" },
|
||||||
|
UNSUPPORTED_MEDIA_TYPE: { code: 415, message: "Unsupported Media Type" },
|
||||||
|
RANGE_NOT_SATISFIABLE: { code: 416, message: "Range Not Satisfiable" },
|
||||||
|
EXPECTATION_FAILED: { code: 417, message: "Expectation Failed" },
|
||||||
|
IM_A_TEAPOT: { code: 418, message: "I'm a teapot" },
|
||||||
|
MISDIRECTED_REQUEST: { code: 421, message: "Misdirected Request" },
|
||||||
|
UNPROCESSABLE_ENTITY: { code: 422, message: "Unprocessable Entity" },
|
||||||
|
LOCKED: { code: 423, message: "Locked" },
|
||||||
|
FAILED_DEPENDENCY: { code: 424, message: "Failed Dependency" },
|
||||||
|
TOO_EARLY: { code: 425, message: "Too Early" },
|
||||||
|
UPGRADE_REQUIRED: { code: 426, message: "Upgrade Required" },
|
||||||
|
PRECONDITION_REQUIRED: { code: 428, message: "Precondition Required" },
|
||||||
|
TOO_MANY_REQUESTS: { code: 429, message: "Too Many Requests" },
|
||||||
|
REQUEST_HEADER_FIELDS_TOO_LARGE: { code: 431, message: "Request Header Fields Too Large" },
|
||||||
|
UNAVAILABLE_FOR_LEGAL_REASONS: { code: 451, message: "Unavailable For Legal Reasons" },
|
||||||
|
|
||||||
|
// 5xx Server Errors
|
||||||
|
INTERNAL_SERVER_ERROR: { code: 500, message: "Internal Server Error" },
|
||||||
|
NOT_IMPLEMENTED: { code: 501, message: "Not Implemented" },
|
||||||
|
BAD_GATEWAY: { code: 502, message: "Bad Gateway" },
|
||||||
|
SERVICE_UNAVAILABLE: { code: 503, message: "Service Unavailable" },
|
||||||
|
GATEWAY_TIMEOUT: { code: 504, message: "Gateway Timeout" },
|
||||||
|
HTTP_VERSION_NOT_SUPPORTED: { code: 505, message: "HTTP Version Not Supported" },
|
||||||
|
VARIANT_ALSO_NEGOTIATES: { code: 506, message: "Variant Also Negotiates" },
|
||||||
|
INSUFFICIENT_STORAGE: { code: 507, message: "Insufficient Storage" },
|
||||||
|
LOOP_DETECTED: { code: 508, message: "Loop Detected" },
|
||||||
|
NOT_EXTENDED: { code: 510, message: "Not Extended" },
|
||||||
|
NETWORK_AUTHENTICATION_REQUIRED: { code: 511, message: "Network Authentication Required" },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type HttpCode = typeof HttpCode[keyof typeof HttpCode];
|
@ -1,9 +1,9 @@
|
|||||||
import { Controller, Get } from "elysia-decorators";
|
import { Controller, Get } from "elysia-decorators";
|
||||||
import { getAppVersion } from "../common/app-utils";
|
import { getAppVersion } from "../common/app-utils";
|
||||||
|
|
||||||
@Controller("/")
|
@Controller()
|
||||||
export default class AppController {
|
export default class AppController {
|
||||||
@Get()
|
@Get("/")
|
||||||
public index() {
|
public index() {
|
||||||
return {
|
return {
|
||||||
app: "backend",
|
app: "backend",
|
11
projects/backend/src/error/rate-limit-error.ts
Normal file
11
projects/backend/src/error/rate-limit-error.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { HttpCode } from "../common/http-codes";
|
||||||
|
|
||||||
|
export class RateLimitError extends Error {
|
||||||
|
constructor(
|
||||||
|
public message: string = 'rate-limited',
|
||||||
|
public detail: string = '',
|
||||||
|
public status: number = HttpCode.TOO_MANY_REQUESTS.code
|
||||||
|
) {
|
||||||
|
super(message)
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,12 @@ import { Elysia } from "elysia";
|
|||||||
import cors from "@elysiajs/cors";
|
import cors from "@elysiajs/cors";
|
||||||
import { decorators } from "elysia-decorators";
|
import { decorators } from "elysia-decorators";
|
||||||
import { logger } from "@tqman/nice-logger";
|
import { logger } from "@tqman/nice-logger";
|
||||||
import AppController from "./controller/app";
|
import { swagger } from '@elysiajs/swagger'
|
||||||
|
import { rateLimit } from 'elysia-rate-limit'
|
||||||
|
import { RateLimitError } from "./error/rate-limit-error";
|
||||||
|
import { helmet } from 'elysia-helmet';
|
||||||
|
import { etag } from '@bogeychan/elysia-etag'
|
||||||
|
import AppController from "./controller/app.controller";
|
||||||
|
|
||||||
const app = new Elysia();
|
const app = new Elysia();
|
||||||
|
|
||||||
@ -23,6 +28,11 @@ app.onError({ as: "global" }, ({ code, error }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable E-Tags
|
||||||
|
*/
|
||||||
|
app.use(etag());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable CORS
|
* Enable CORS
|
||||||
*/
|
*/
|
||||||
@ -37,6 +47,30 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limit (100 requests per minute)
|
||||||
|
*/
|
||||||
|
app.use(rateLimit({
|
||||||
|
scoping: "global",
|
||||||
|
duration: 60 * 1000,
|
||||||
|
max: 100,
|
||||||
|
skip: (request) => {
|
||||||
|
let [ _, path ] = request.url.split("/"); // Get the url parts
|
||||||
|
path === "" || path === undefined && (path = "/"); // If we're on /, the path is undefined, so we set it to /
|
||||||
|
return path === "/"; // ignore all requests to /
|
||||||
|
},
|
||||||
|
errorResponse: new RateLimitError("Too many requests, please try again later"),
|
||||||
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security settings
|
||||||
|
*/
|
||||||
|
app.use(helmet({
|
||||||
|
hsts: false, // Disable HSTS
|
||||||
|
contentSecurityPolicy: false, // Disable CSP
|
||||||
|
dnsPrefetchControl: true, // Enable DNS prefetch
|
||||||
|
}))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controllers
|
* Controllers
|
||||||
*/
|
*/
|
||||||
@ -46,6 +80,11 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swagger Documentation
|
||||||
|
*/
|
||||||
|
app.use(swagger());
|
||||||
|
|
||||||
app.onStart(() => {
|
app.onStart(() => {
|
||||||
console.log("Listening on port http://localhost:8080");
|
console.log("Listening on port http://localhost:8080");
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user