oops
This commit is contained in:
parent
3f040b36a4
commit
cfd0a7ca6e
@ -1,15 +0,0 @@
|
||||
import { Collection, Database } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||
import Image from "./models/Image.ts";
|
||||
import User from "./models/User.ts";
|
||||
|
||||
export default class AppDatabase {
|
||||
public readonly db: Database;
|
||||
public readonly users: Collection<User>;
|
||||
public readonly images: Collection<Image>;
|
||||
|
||||
public constructor(db: Database) {
|
||||
this.db = db;
|
||||
this.users = db.collection('users');
|
||||
this.images = db.collection('images');
|
||||
}
|
||||
}
|
24
src/index.ts
24
src/index.ts
@ -1,24 +0,0 @@
|
||||
import { MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||
import { RootRouter } from "./routers/RootRouter.ts";
|
||||
import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts";
|
||||
import AppDatabase from "./AppDatabase.ts";
|
||||
|
||||
export default async function clonegur() {
|
||||
let salt;
|
||||
|
||||
try {
|
||||
salt = new TextDecoder().decode(await Deno.readFile('keys/salt.txt'));
|
||||
}
|
||||
catch {
|
||||
salt = await bcrypt.genSalt();
|
||||
await Deno.writeFile('keys/salt.txt', new TextEncoder().encode(salt));
|
||||
}
|
||||
|
||||
const db = await new MongoClient().connect({
|
||||
db: 'clonegur',
|
||||
servers: [ { host: '127.0.0.1', port: 27017 } ]
|
||||
});
|
||||
return new RootRouter(salt, new AppDatabase(db));
|
||||
}
|
||||
|
||||
(await clonegur()).attach(Deno.listen({ port: 4000, hostname: 'localhost' }));
|
@ -1,15 +0,0 @@
|
||||
import { UUID } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||
|
||||
export enum Visibility {
|
||||
Public,
|
||||
Unlisted,
|
||||
Private,
|
||||
}
|
||||
|
||||
export default interface Image {
|
||||
_id: UUID;
|
||||
author: string;
|
||||
name: string;
|
||||
visibility: Visibility;
|
||||
created: number;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { UUID } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||
|
||||
export default interface User {
|
||||
_id: string;
|
||||
username: string;
|
||||
password: string;
|
||||
images: UUID[];
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import HttpError from "../server/HttpError.ts";
|
||||
import RestRequest from "../server/RestRequest.ts";
|
||||
import RestResponse from "../server/RestResponse.ts";
|
||||
import Router from "../server/Router.ts";
|
||||
|
||||
export default class AppRouter extends Router {
|
||||
public onError(_req: RestRequest, error: unknown): RestResponse | HttpError | Promise<RestResponse | HttpError> {
|
||||
if (error instanceof HttpError) return new HttpError({ error: error.body }, error.status);
|
||||
return super.onError(_req, error);
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
import { UUID } from "https://deno.land/x/web_bson@v0.3.0/mod.js";
|
||||
|
||||
import { auth, body, headers, jwt, page, rest, schema } from "../server/decorators.ts";
|
||||
import { JWTPayload } from "../utils/JWT.ts";
|
||||
import AppRouter from "./AppRouter.ts";
|
||||
import HttpError from "../server/HttpError.ts";
|
||||
import Image, { Visibility } from "../models/Image.ts";
|
||||
import { Page } from "../server/decorators/page.ts";
|
||||
import { Headers } from "../server/RestRequest.ts";
|
||||
import { convert } from "../server/decorators/schema.ts";
|
||||
import { now } from "../utils/utils.ts";
|
||||
import AppDatabase from "../AppDatabase.ts";
|
||||
|
||||
export default class ImageRouter extends AppRouter {
|
||||
public static deserialize(image: Image) {
|
||||
return {
|
||||
author: image.author,
|
||||
created: image.created,
|
||||
name: image.name,
|
||||
visibility: image.visibility,
|
||||
id: image._id,
|
||||
};
|
||||
}
|
||||
|
||||
@rest('GET', '/')
|
||||
async get(@schema('uuid') id: UUID, @jwt(v => v.salt, false) @auth() jwt?: JWTPayload) {
|
||||
const image = await this.db.images.findOne({ _id: new UUID(id) });
|
||||
|
||||
if (
|
||||
!image ||
|
||||
image.visibility === Visibility.Private && image.author !== jwt?.name
|
||||
) throw new HttpError("Image doesn't exist.");
|
||||
|
||||
return ImageRouter.deserialize(image);
|
||||
}
|
||||
@rest('GET', '/feed')
|
||||
async self(@page() page: Page) {
|
||||
const res = await page.apply(this.db.images.find({})).toArray();
|
||||
if (!res) throw new HttpError('User not found.');
|
||||
return res.map(v => ImageRouter.deserialize(v));
|
||||
}
|
||||
|
||||
@rest('POST', '/upload')
|
||||
async upload(@body() body: Blob, @headers() headers: Headers, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
||||
const user = await this.db.users.findOne({ username: jwt.name });
|
||||
if (!user) throw new HttpError("You don't exist.");
|
||||
|
||||
// Parse body
|
||||
const contentType = headers['content-type'] + "";
|
||||
if (!contentType.startsWith('multipart/form-data')) {
|
||||
throw new HttpError("Expected a 'Content-type: multipart/form-data; ...' header.");
|
||||
}
|
||||
const data = await new Request('http://127.0.0.1', {
|
||||
body, headers: [ ['content-type', contentType ] ],
|
||||
method: 'post'
|
||||
}).formData();
|
||||
|
||||
// Clean up data
|
||||
if (!data.has('file')) throw new HttpError("Expected a 'file' entry in form data.");
|
||||
if (!data.has('body')) throw new HttpError("Expected a 'body' entry in form data.");
|
||||
|
||||
let rawFile: File = data.get('file') as File;
|
||||
let rawReq: string = data.get('body') as string;
|
||||
|
||||
if (typeof rawFile === 'string') rawFile = new File([rawFile], 'unknown');
|
||||
if (typeof rawReq !== 'string') rawReq = await (rawReq as Blob).text();
|
||||
if (rawFile.size > (1 << 20) * 2) throw new HttpError("File too large (max 2MB).");
|
||||
|
||||
// Extract (and check) extension
|
||||
const pointI = rawFile.name.lastIndexOf(".");
|
||||
if (pointI < 0) throw new HttpError("Given file has no extension.");
|
||||
const ext = rawFile.name.substring(pointI + 1).trim();
|
||||
if (ext === "") throw new HttpError("Given file has no extension.");
|
||||
|
||||
// Clean up request
|
||||
const req = await convert(rawReq, { name: 'string?', visibility: 'number?' });
|
||||
req.name ??= new UUID().toString();
|
||||
req.visibility = 0;
|
||||
if (req.visibility < 0 || req.visibility > 2) throw new HttpError("body.visibility: Must be 0, 1, or 2");
|
||||
|
||||
// Create file
|
||||
const img: Image = { _id: new UUID(), author: user.username, created: now(), name: req.name!, visibility: req.visibility };
|
||||
await Deno.mkdir('images', { recursive: true });
|
||||
const out = await Deno.open(`images/${img._id}.${ext}`, { write: true, create: true });
|
||||
|
||||
for await (const bit of rawFile.stream()) out.write(bit);
|
||||
out.close();
|
||||
|
||||
// Write to DB
|
||||
try {
|
||||
await this.db.images.insertOne(img);
|
||||
await this.db.users.updateOne({ username: user.username }, { $push: { images: img._id } });
|
||||
return ImageRouter.deserialize(img);
|
||||
}
|
||||
catch (e) {
|
||||
await Deno.remove(`images/${img._id}.${ext}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@rest('POST', '/change')
|
||||
async change(@body() raw: unknown, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
||||
const body = await convert(raw, { id: 'uuid', name: 'string?', visibility: 'number?' });
|
||||
const user = await this.db.users.findOne({ username: jwt.name });
|
||||
if (!user) throw new HttpError("You don't exist.");
|
||||
|
||||
const img = await this.db.images.findOne({ _id: body.id });
|
||||
if (!img) throw new HttpError("Image doesn't exist.");
|
||||
if (user.username !== img.author) throw new HttpError("You don't own the image.");
|
||||
|
||||
await this.db.images.updateOne({ _id: body.id }, { $set: { name: body.name, visibility: body.visibility } });
|
||||
return ImageRouter.deserialize(img);
|
||||
}
|
||||
|
||||
public constructor(private db: AppDatabase) {
|
||||
super();
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import UserRouter from "../routers/UserRouter.ts";
|
||||
import ImageRouter from "./ImageRouter.ts";
|
||||
import AppRouter from "./AppRouter.ts";
|
||||
import RestResponse from "../server/RestResponse.ts";
|
||||
import { rest, route } from "../server/decorators.ts";
|
||||
import { stream } from "../utils/utils.ts";
|
||||
import AppDatabase from "../AppDatabase.ts";
|
||||
|
||||
export class RootRouter extends AppRouter {
|
||||
@route('users/*') users;
|
||||
@route('images/*') images;
|
||||
|
||||
@rest('*', '*')
|
||||
default() {
|
||||
return new RestResponse().body(stream('Page not found :/')).status(404);
|
||||
}
|
||||
|
||||
constructor(salt: string, db: AppDatabase) {
|
||||
super();
|
||||
|
||||
this.users = new UserRouter(salt, db);
|
||||
this.images = new ImageRouter(db);
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts";
|
||||
|
||||
import { auth, body, rest, schema } from "../server/decorators.ts";
|
||||
import HttpError from "../server/HttpError.ts";
|
||||
import User from "../models/User.ts";
|
||||
import AppRouter from "./AppRouter.ts";
|
||||
import jwt from "../server/decorators/jwt.ts";
|
||||
import JWT, { JWTPayload } from "../utils/JWT.ts";
|
||||
import { now } from "../utils/utils.ts";
|
||||
import { convert } from "../server/decorators/schema.ts";
|
||||
import AppDatabase from "../AppDatabase.ts";
|
||||
import { Visibility } from "../models/Image.ts";
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export default class UserRouter extends AppRouter {
|
||||
public deserialize(user: User, self = false) {
|
||||
let images = user.images;
|
||||
if (!self) {
|
||||
images = [];
|
||||
Promise.all(user.images.map(async v => {
|
||||
if ((await this.db.images.findOne({ _id: v }))?.visibility === Visibility.Public) {
|
||||
images.push(v);
|
||||
}
|
||||
}));
|
||||
}
|
||||
return { username: user.username, images };
|
||||
}
|
||||
|
||||
@rest('GET', '/')
|
||||
async get(@schema('string') username: string, @jwt('salt', true) @auth() jwt: JWTPayload) {
|
||||
const res = await this.db.users.findOne({ username });
|
||||
if (res === undefined) throw new HttpError('User not found.');
|
||||
|
||||
return this.deserialize(res, jwt.name === username);
|
||||
}
|
||||
@rest('GET', '/self')
|
||||
async self(@jwt('salt', true) @auth() auth: JWTPayload) {
|
||||
if (auth === undefined) throw new HttpError('You are not logged in.');
|
||||
const res = await this.db.users.findOne({ username: auth.name });
|
||||
|
||||
if (res === undefined) throw new HttpError('User not found.');
|
||||
|
||||
return this.deserialize(res);
|
||||
}
|
||||
|
||||
@rest('POST', '/signup')
|
||||
async signup(@body() raw: unknown) {
|
||||
const body = await convert(raw, {
|
||||
username: 'string',
|
||||
password: 'string',
|
||||
});
|
||||
|
||||
if (await this.db.users.countDocuments({ username: body.username }) > 0) {
|
||||
throw new HttpError('User with the same username already exists.');
|
||||
}
|
||||
|
||||
const password = await bcrypt.hash(body.password, this.salt);
|
||||
|
||||
await this.db.users.insertOne({
|
||||
username: body.username,
|
||||
password: password,
|
||||
images: [],
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
@rest('POST', '/login')
|
||||
async login(@body() raw: unknown) {
|
||||
const body = await convert(raw, {
|
||||
username: 'string',
|
||||
password: 'string'
|
||||
});
|
||||
const res = await this.db.users.findOne({ username: body.username });
|
||||
if (!res) throw new HttpError('Incorrect username or password.');
|
||||
const hashed = await bcrypt.hash(body.password, this.salt);
|
||||
|
||||
if (res.password !== hashed) throw new HttpError('Incorrect username or password.');
|
||||
|
||||
const time = now();
|
||||
return JWT.encode({
|
||||
iat: time,
|
||||
exp: time + 3600 * 12,
|
||||
name: res.username,
|
||||
}, this.salt);
|
||||
}
|
||||
|
||||
public constructor(private salt: string, private db: AppDatabase) {
|
||||
super();
|
||||
db.users.createIndexes({ indexes: [ { key: { username: 1 }, name: 'Username Index' } ] });
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import serialize from "./serialize.ts";
|
||||
|
||||
export default class HttpError extends Error {
|
||||
constructor(public readonly body: unknown, public readonly status = 400) {
|
||||
super();
|
||||
serialize(body).then(v => this.message = v.toString());
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
export type ParamDict = Record<string, string>;
|
||||
export type Headers = Record<string, string | string[] | undefined>;
|
||||
|
||||
function splitUrl(url: string) {
|
||||
return url.split('/').map(v => v.trim()).filter(v => v !== '');
|
||||
}
|
||||
function sanitizeUrl(url: string, forceAbsolute = true) {
|
||||
url = url.trim();
|
||||
if (forceAbsolute || url.startsWith('/')) {
|
||||
return '/' + splitUrl(url).join('/');
|
||||
}
|
||||
else {
|
||||
return '/' + splitUrl(url).join('/');
|
||||
}
|
||||
}
|
||||
|
||||
export default class RestRequest {
|
||||
public readonly body: unknown;
|
||||
public readonly method: string;
|
||||
public readonly url: string;
|
||||
public readonly pathParams: ParamDict;
|
||||
public readonly queryParams: ParamDict;
|
||||
public readonly headers: Headers;
|
||||
|
||||
public get params() {
|
||||
return { ...this.queryParams, ...this.pathParams };
|
||||
}
|
||||
|
||||
public constructor(
|
||||
body: unknown,
|
||||
headers: Headers,
|
||||
method: string,
|
||||
url: string,
|
||||
pathParams: ParamDict = {},
|
||||
queryParams: ParamDict = {}
|
||||
) {
|
||||
this.body = body;
|
||||
this.headers = headers;
|
||||
this.pathParams = { ...pathParams };
|
||||
this.queryParams = { ...queryParams };
|
||||
this.method = method.toLowerCase();
|
||||
this.url = sanitizeUrl(url);
|
||||
|
||||
if (this.url.includes('?')) {
|
||||
const questionIndex = this.url.indexOf('?');
|
||||
this.url = this.url.substring(0, questionIndex);
|
||||
const params = this.url
|
||||
.substring(questionIndex + 1)
|
||||
.split('&')
|
||||
.map(v => v.trim())
|
||||
.filter(v => v !== '');
|
||||
|
||||
for (const rawParam of params) {
|
||||
const i = rawParam.indexOf('=');
|
||||
if (i < 0) continue;
|
||||
|
||||
const name = rawParam.substring(0, i);
|
||||
const val = rawParam.substring(i + 1);
|
||||
|
||||
if (name === '') continue;
|
||||
this.queryParams[name] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public match(predicate: string) {
|
||||
const urlSegments = splitUrl(this.url);
|
||||
const predSegments = splitUrl(predicate);
|
||||
const wildcardIndex = predSegments.indexOf('*');
|
||||
const hasWildcard = wildcardIndex >= 0;
|
||||
const pathParams: ParamDict = { ...this.pathParams };
|
||||
|
||||
if (wildcardIndex >= 0) {
|
||||
if (predSegments.includes('*', wildcardIndex + 1)) throw new Error("A path predicate may not have more than one wildcard.");
|
||||
if (predSegments.splice(wildcardIndex).length > 1) throw new Error("A path predicate must be the last segment.");
|
||||
}
|
||||
|
||||
for (const predSeg of predSegments) {
|
||||
const urlSeg = urlSegments.shift();
|
||||
if (urlSeg === undefined) return undefined;
|
||||
else if (predSeg.startsWith(':')) {
|
||||
const name = predSeg.substring(1);
|
||||
if (name.length === 0) throw new Error('Invalid path predicate - a segment may not be ":".');
|
||||
pathParams[name] = decodeURI(urlSeg);
|
||||
}
|
||||
else if (predSeg === urlSeg) continue;
|
||||
else return undefined;
|
||||
}
|
||||
|
||||
if (!hasWildcard && urlSegments.length > 0) return undefined;
|
||||
|
||||
return new RestRequest(
|
||||
this.body, this.headers, this.method, '/' + urlSegments.join('/'),
|
||||
{ ...this.pathParams, ...pathParams }, this.queryParams
|
||||
);
|
||||
}
|
||||
|
||||
public static fromMessage(msg: Deno.RequestEvent) {
|
||||
const raw = msg.request.body;
|
||||
const headers = {} as Headers;
|
||||
|
||||
for (const entry of msg.request.headers.entries()) {
|
||||
headers[entry[0]] = entry[1];
|
||||
}
|
||||
|
||||
const url = new URL(msg.request.url);
|
||||
const params = {} as ParamDict;
|
||||
for (const entry of url.searchParams.entries()) params[entry[0]] = entry[1];
|
||||
|
||||
return new RestRequest(raw, headers, msg.request.method, url.pathname, {}, params);
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import { stream } from "../utils/utils.ts";
|
||||
import { Headers } from "./RestRequest.ts";
|
||||
|
||||
export default class RestResponse {
|
||||
#status = 200;
|
||||
#statusMsg = '';
|
||||
#body?: ReadableStream<Uint8Array>;
|
||||
|
||||
headers: Headers = {};
|
||||
|
||||
public constructor() { }
|
||||
|
||||
public get statusCode() { return this.#status; }
|
||||
public get statusMessage() { return this.#statusMsg; }
|
||||
public get content() { return this.#body; }
|
||||
|
||||
public header(name: string, val: string | string[]) {
|
||||
this.headers[name] = val;
|
||||
return this;
|
||||
}
|
||||
public status(val: number, message = '') {
|
||||
this.#status = val;
|
||||
this.#statusMsg = message;
|
||||
return this;
|
||||
}
|
||||
public body(val: string | ReadableStream<Uint8Array>) {
|
||||
if (typeof val === 'string') val = stream(val);
|
||||
this.#body = val;
|
||||
return this;
|
||||
}
|
||||
|
||||
public toFetchResponse(): Response {
|
||||
const headers: string[][] = [];
|
||||
for (const key in this.headers) {
|
||||
const val = this.headers[key];
|
||||
if (typeof val === 'string') {
|
||||
headers.push([key, val]);
|
||||
}
|
||||
else if (val instanceof Array) {
|
||||
headers.push([key, ...val]);
|
||||
}
|
||||
else headers.push([key]);
|
||||
}
|
||||
return new Response(this.#body, {
|
||||
headers: headers,
|
||||
status: this.#status,
|
||||
statusText: this.#statusMsg,
|
||||
});
|
||||
}
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
import { Reflect } from "https://deno.land/x/reflect_metadata@v0.1.12/mod.ts";
|
||||
import serialize from "./serialize.ts";
|
||||
import HttpError from "./HttpError.ts";
|
||||
import RestResponse from "./RestResponse.ts";
|
||||
import RestRequest from "./RestRequest.ts";
|
||||
|
||||
export type HandlerRes = Promise<RestResponse | undefined> | RestResponse | undefined;
|
||||
|
||||
export interface Handler {
|
||||
handle(req: RestRequest): HandlerRes;
|
||||
}
|
||||
export interface RestOptions {
|
||||
route?: string;
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
export type ProcessFunc<T extends Router> = (this: T, req: RestRequest, arg: any, name: string) => Promise<unknown> | unknown;
|
||||
|
||||
export interface RouterHandler {
|
||||
path: string;
|
||||
handler: Handler;
|
||||
}
|
||||
export function addMetaQuery<T extends Router>(target: T, ...handlers: ((r: T) => RouterHandler)[]) {
|
||||
let props = Reflect.getOwnMetadata('router:queries', target);
|
||||
if (props === undefined) Reflect.defineMetadata('router:queries', props = [], target);
|
||||
|
||||
props.push(...handlers);
|
||||
}
|
||||
|
||||
export function makeParameterModifier<T extends Router>(func: ProcessFunc<T>) {
|
||||
return (target: T, key: string, index: number) => {
|
||||
let res = Reflect.getOwnMetadata('router:params', target, key);
|
||||
|
||||
if (res === undefined) Reflect.defineMetadata('router:params', res = [], target, key);
|
||||
(res[index] ??= []).push(func);
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class Router {
|
||||
private _handlers?: RouterHandler[];
|
||||
public defaultHandler?: Handler;
|
||||
|
||||
private _init() {
|
||||
if (this._handlers === undefined) {
|
||||
this._handlers = [];
|
||||
|
||||
// why the actual fuck not
|
||||
// deno-lint-ignore no-this-alias
|
||||
for (let proto = this; proto instanceof Router; proto = Object.getPrototypeOf(proto)) {
|
||||
const props = Reflect.getOwnMetadata('router:queries', proto);
|
||||
for (const handler of props ?? []) {
|
||||
this._handlers.push(handler(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._handlers;
|
||||
}
|
||||
|
||||
public async handle(req: RestRequest) {
|
||||
for (const hnd of this._init()) {
|
||||
const _req = req.match(hnd.path);
|
||||
if (_req) {
|
||||
try {
|
||||
const res = await hnd.handler.handle(_req);
|
||||
if (res) return res;
|
||||
}
|
||||
catch (e) {
|
||||
const res = await this.onError(_req, e);
|
||||
if (res instanceof HttpError) return new RestResponse()
|
||||
.body(await serialize(res.body))
|
||||
.status(res.status);
|
||||
else return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.defaultHandler?.handle(req);
|
||||
}
|
||||
public addHandler(path: string, handler: Handler) {
|
||||
this._init().push({ path, handler });
|
||||
return this;
|
||||
}
|
||||
|
||||
public onError(_req: RestRequest, error: unknown): Promise<RestResponse | HttpError> | RestResponse | HttpError {
|
||||
if (error instanceof HttpError) return error;
|
||||
else {
|
||||
console.error(error);
|
||||
try {
|
||||
return new HttpError(`Internal error: ${error}\nSee logs for details`, 500);
|
||||
}
|
||||
catch {
|
||||
return new HttpError('Internal error.\nSee logs for details', 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async attach(server: Deno.Listener) {
|
||||
for await (const conn of server) {
|
||||
for await (const req of Deno.serveHttp(conn)) {
|
||||
const r = await this.handle(RestRequest.fromMessage(req));
|
||||
if (r) req.respondWith(r.toFetchResponse());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import body from "./decorators/body.ts";
|
||||
import rest from "./decorators/rest.ts";
|
||||
import auth from "./decorators/auth.ts";
|
||||
import route from "./decorators/route.ts";
|
||||
import schema from "./decorators/schema.ts";
|
||||
import jwt from "./decorators/jwt.ts";
|
||||
import page from "./decorators/page.ts";
|
||||
import headers from "./decorators/headers.ts";
|
||||
|
||||
export { body, schema, rest, route, auth , jwt, page, headers };
|
@ -1,10 +0,0 @@
|
||||
import { makeParameterModifier } from "../Router.ts";
|
||||
|
||||
export default function auth() {
|
||||
return makeParameterModifier(function (req) {
|
||||
const res = req.headers.authorization;
|
||||
if (typeof res !== 'string') return undefined;
|
||||
if (res.startsWith('Bearer')) return res.substring(6).trimStart();
|
||||
else return undefined;
|
||||
});
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { makeParameterModifier } from "../Router.ts";
|
||||
|
||||
export type BodyType = 'raw' | 'json';
|
||||
|
||||
export default function body() {
|
||||
return makeParameterModifier(req => {
|
||||
return req.body;
|
||||
});
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { makeParameterModifier } from "../Router.ts";
|
||||
|
||||
|
||||
export default function headers() {
|
||||
return makeParameterModifier(req => req.headers);
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
// SHUT THE FUCK UP
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import JWT from "../../utils/JWT.ts";
|
||||
import HttpError from "../HttpError.ts";
|
||||
import Router, { makeParameterModifier } from "../Router.ts";
|
||||
|
||||
export default function jwt<T extends Router = any>(salt: ((self: T) => string) | string, required = false) {
|
||||
return makeParameterModifier<T>(function (_req, val?: string) {
|
||||
if (val === undefined) return undefined;
|
||||
const s = typeof salt === 'function' ? salt(this) : (this as any)[salt] as string;
|
||||
try {
|
||||
const res = JWT.decode(val, s);
|
||||
if (required && res === undefined) throw new HttpError('You are not logged in.');
|
||||
return res;
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof Error && !(e instanceof HttpError)) {
|
||||
throw new HttpError(e.message, 400);
|
||||
}
|
||||
else throw e;
|
||||
}
|
||||
});
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import { makeParameterModifier } from "../Router.ts";
|
||||
import { FindCursor } from "https://deno.land/x/mongo@v0.31.2/src/collection/commands/find.ts";
|
||||
|
||||
export class Page {
|
||||
public size?: number;
|
||||
public index?: number;
|
||||
|
||||
public apply<T>(cursor: FindCursor<T>) {
|
||||
let res = cursor;
|
||||
|
||||
if (this.size !== undefined) {
|
||||
if (this.index !== undefined) res = res.skip(this.index * this.size);
|
||||
res = res.limit(this.size);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public constructor(size?: number);
|
||||
public constructor(size: number, index?: number);
|
||||
public constructor(size?: number, index?: number) {
|
||||
this.size = size;
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
export default function uuid() {
|
||||
return makeParameterModifier(req => {
|
||||
let n: number | undefined = Number.parseInt(req.params.n);
|
||||
let i: number | undefined = Number.parseInt(req.params.i);
|
||||
|
||||
if (isNaN(n) || n < 1) n = undefined;
|
||||
if (isNaN(i) || i < 0) i = undefined;
|
||||
|
||||
if (n === undefined) return new Page();
|
||||
else if (i === undefined) return new Page(n);
|
||||
else return new Page(n, i);
|
||||
});
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
// deno-lint-ignore-file ban-types
|
||||
import { Reflect } from "https://deno.land/x/reflect_metadata@v0.1.12/mod.ts";
|
||||
|
||||
import RestResponse from "../RestResponse.ts";
|
||||
import Router, { ProcessFunc, addMetaQuery } from "../Router.ts";
|
||||
import serialize from "../serialize.ts";
|
||||
|
||||
export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE';
|
||||
|
||||
type Base<KeyT extends string> = Router & { [X in KeyT]: Function; };
|
||||
type ModArray<T extends Router> = ([ProcessFunc<T>, ...ProcessFunc<T>[]] | undefined)[];
|
||||
|
||||
export default function rest<KeyT extends keyof T & string, T extends Base<KeyT>>(method: HttpMethod, route?: string) {
|
||||
return (target: T, key: KeyT) => {
|
||||
const path = route ?? key;
|
||||
|
||||
addMetaQuery(target, (r) => ({
|
||||
path, handler: {
|
||||
async handle(req) {
|
||||
if (method !== '*' && req.method.toUpperCase() !== method) return undefined;
|
||||
|
||||
const params: string[] = [];
|
||||
const args: unknown[] = [];
|
||||
const allMods: ModArray<T> = [];
|
||||
|
||||
let signature = r[key].toString();
|
||||
signature = signature.substring(signature.indexOf('(') + 1, signature.indexOf(')'));
|
||||
params.push(...signature.split(',').map(v => v.trim()).filter(v => v !== ''));
|
||||
|
||||
for (let proto = r; proto instanceof Router; proto = Object.getPrototypeOf(proto)) {
|
||||
const data = Reflect.getOwnMetadata('router:params', proto, key) as ModArray<T>;
|
||||
if (!data) continue;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i] === undefined) continue;
|
||||
|
||||
if (allMods[i]) allMods[i]!.push(...data[i]!);
|
||||
else allMods[i] ??= [...data[i]!];
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const param = params[i];
|
||||
|
||||
let arg: unknown = req.params[param];
|
||||
|
||||
for (const mod of allMods[i] ?? []) {
|
||||
arg = await mod.call(r, req, arg, params[i]);
|
||||
}
|
||||
|
||||
args.push(arg);
|
||||
}
|
||||
|
||||
const res = r[key].apply(r, args);
|
||||
if (res instanceof RestResponse) return res;
|
||||
return new RestResponse().body(await serialize(res));
|
||||
}
|
||||
},
|
||||
}));
|
||||
};
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import Router, { Handler, addMetaQuery } from "../Router.ts";
|
||||
|
||||
export default function route<KeyT extends keyof T & string, T extends Router & { [x in KeyT]: Handler }>(path?: string) {
|
||||
return (target: T, key: KeyT) => {
|
||||
addMetaQuery(target, r => ({
|
||||
handler: r[key],
|
||||
path: path ?? key,
|
||||
}));
|
||||
};
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { UUID } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||
import HttpError from "../HttpError.ts";
|
||||
import { makeParameterModifier } from "../Router.ts";
|
||||
|
||||
export type PrimitiveSchema = number | 'any' | 'undefined' | 'null' | 'string' | 'number' | 'boolean' | 'true' | 'false' | 'uuid' | 'object';
|
||||
export type OptionalSchema<T extends PrimitiveSchema | number = PrimitiveSchema> = `${T}?`;
|
||||
export type LiteralSchema<T extends string = string> = `:${T}`;
|
||||
export type OptionalLiteralSchema<T extends string = string> = `:${T}?`;
|
||||
export type ObjectSchema = { $optional?: boolean; } & { [key: string]: Schema };
|
||||
export type OrSchema = [] | [Schema] | [Schema, Schema] | [Schema, Schema, Schema];
|
||||
export type Schema =
|
||||
PrimitiveSchema | OptionalSchema | OrSchema | ObjectSchema |
|
||||
LiteralSchema | OptionalLiteralSchema;
|
||||
|
||||
export type ObjectSchemaType<T extends ObjectSchema> =
|
||||
({ [x in keyof T]: SchemaType<T[x]> }) |
|
||||
(T["$optional"] extends true ? undefined : never);
|
||||
export type SchemaType<T extends Schema> =
|
||||
T extends 'any' ? any :
|
||||
T extends 'null' ? null :
|
||||
T extends 'undefined' ? undefined :
|
||||
T extends 'string' ? string :
|
||||
T extends 'number' ? number :
|
||||
T extends 'boolean' ? boolean :
|
||||
T extends 'uuid' ? UUID :
|
||||
T extends 'true' ? true :
|
||||
T extends 'false' ? false :
|
||||
T extends number ? T :
|
||||
T extends OptionalSchema<infer T> ? SchemaType<T> | undefined :
|
||||
T extends LiteralSchema<infer T> ? T :
|
||||
T extends OptionalLiteralSchema<infer T> ? T | undefined :
|
||||
T extends [] ? never :
|
||||
T extends [Schema] ? SchemaType<T[0]> :
|
||||
T extends [Schema, Schema] ? SchemaType<T[0]> | SchemaType<T[1]> :
|
||||
T extends [Schema, Schema, Schema] ? SchemaType<T[0]> | SchemaType<T[1]> | SchemaType<T[2]> :
|
||||
T extends ObjectSchema ? ObjectSchemaType<T> : never;
|
||||
|
||||
|
||||
function stringify(desc: Schema): string {
|
||||
if (typeof desc === 'string') return desc;
|
||||
if (typeof desc === 'number') return desc.toString();
|
||||
if (desc instanceof Array) return desc.map(stringify).join(' | ');
|
||||
|
||||
let res = '{ ';
|
||||
|
||||
for (const key in desc) {
|
||||
if (key === '$optional') continue;
|
||||
if (res != '{ ') res += ', ';
|
||||
res += key + ': ';
|
||||
res += stringify(desc[key]);
|
||||
}
|
||||
res += '}';
|
||||
if (desc.$optional) res += '?';
|
||||
|
||||
return res;
|
||||
}
|
||||
async function _convert(path: string[], val: unknown, desc: Schema): Promise<any> {
|
||||
const _path = path.join('.');
|
||||
if (val instanceof Blob) val = await val.text();
|
||||
if (val instanceof ReadableStream) {
|
||||
const res: Uint8Array[] = [];
|
||||
|
||||
for await (const part of val) {
|
||||
if (!(part instanceof Uint8Array)) throw new Error(`${_path}: Invalid stream given.`);
|
||||
res.push(part);
|
||||
}
|
||||
|
||||
val = await new Blob(res).text();
|
||||
}
|
||||
if (desc === 'any') return val as any;
|
||||
|
||||
if (typeof desc === 'string') {
|
||||
let type: string = desc;
|
||||
const opt = desc.endsWith('?');
|
||||
|
||||
if (opt) type = type.substring(0, desc.length - 1);
|
||||
if (opt && val === undefined) return val;
|
||||
|
||||
if (type.startsWith(':')) {
|
||||
if (val !== type.substring(1)) return val;
|
||||
else throw new HttpError(`${_path}: Expected ${type.substring(1)}.`);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "true":
|
||||
if (val === true) return true;
|
||||
throw new HttpError(`${_path}: Expected true.`);
|
||||
case "false":
|
||||
if (val === false) return false;
|
||||
throw new HttpError(`${_path}: Expected false.`);
|
||||
case "null":
|
||||
if (val === null) return null;
|
||||
throw new HttpError(`${_path}: Expected null.`);
|
||||
case "undefined":
|
||||
if (val === undefined) return undefined;
|
||||
throw new HttpError(`${_path}: Expected undefined.`);
|
||||
case "string": return val + "";
|
||||
case "uuid":
|
||||
try { return new UUID(val + ""); }
|
||||
catch { throw new HttpError(`${_path}: Expected an uuid or a value, convertible to an uuid.`); }
|
||||
case "number": {
|
||||
const res = Number.parseFloat(val + "");
|
||||
if (isNaN(res)) throw new HttpError(`${_path}: Expected a number or a value, convertible to a number.`);
|
||||
return res;
|
||||
}
|
||||
case "boolean": {
|
||||
const res = val + "";
|
||||
if (res === 'true') return true;
|
||||
if (res === 'false') return true;
|
||||
throw new HttpError(`${_path}: Expected a boolean or a value, convertible to a boolean.`);
|
||||
}
|
||||
}
|
||||
|
||||
const num = Number.parseFloat(type);
|
||||
|
||||
if (!isNaN(num)) {
|
||||
const res = Number.parseFloat(val + "");
|
||||
if (res !== num) throw new HttpError(`${_path}: Expected ${num}.`);
|
||||
return res;
|
||||
}
|
||||
|
||||
throw new Error(`${_path}: Unknown type ${type}`);
|
||||
}
|
||||
else if (typeof desc === 'number') {
|
||||
if (desc === val) return desc;
|
||||
else throw new HttpError(`${_path}: Expected ${desc}.`);
|
||||
}
|
||||
else if (desc instanceof Array) {
|
||||
for (const type of desc) {
|
||||
try {
|
||||
return await _convert(path, val, type);
|
||||
}
|
||||
catch { /**/ }
|
||||
}
|
||||
throw new HttpError(`${_path}: Expected a ${stringify(desc)}, got ${typeof val} instead.`);
|
||||
}
|
||||
else {
|
||||
if (desc.$optional && val === undefined) return val;
|
||||
if (val === null) throw new HttpError(`${_path}: Expected an object, got null instead.`);
|
||||
|
||||
if (typeof val === 'string') {
|
||||
try {
|
||||
val = JSON.parse(val);
|
||||
}
|
||||
catch (e) {
|
||||
throw new HttpError(`${_path}: Invalid JSON given for object: ${e}`);
|
||||
}
|
||||
}
|
||||
else throw new HttpError(`${_path}: Expected an object or a valid json string.`);
|
||||
|
||||
for (const key in desc) {
|
||||
if (key === '$optional') continue;
|
||||
(val as any)[key] = await _convert([ ...path, key ], (val as any)[key], desc[key]);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
||||
export function convert<T extends Schema>(val: unknown, desc: T, path?: string): Promise<SchemaType<T>> {
|
||||
return _convert(path ? [path] : [], val, desc);
|
||||
}
|
||||
|
||||
|
||||
export default function schema(desc: Schema) {
|
||||
return makeParameterModifier(async (_req, val, name) => await _convert([name], val, desc));
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { stream } from "../utils/utils.ts";
|
||||
|
||||
const undefinedBuff = new Blob([new TextEncoder().encode('undefined')]);
|
||||
const nullBuff = new Blob([new TextEncoder().encode('null')]);
|
||||
|
||||
export default async function serialize(val: unknown, depth = 16): Promise<ReadableStream<Uint8Array>> {
|
||||
while (true) {
|
||||
if (depth <= 0) throw new Error("Call depth exceeded limit.");
|
||||
|
||||
if (val instanceof Promise) val = await val;
|
||||
else if (val instanceof Function) {
|
||||
if (val.length !== 0) throw new Error('Can\'t serialize an argument-accepting function');
|
||||
val = val();
|
||||
}
|
||||
else break;
|
||||
|
||||
depth--;
|
||||
}
|
||||
|
||||
if (val === undefined) return undefinedBuff.stream();
|
||||
if (val === null) return nullBuff.stream();
|
||||
if (typeof val === 'string') return stream(val);
|
||||
if (val instanceof Blob) return val.stream();
|
||||
if (val instanceof ReadableStream) return val;
|
||||
if (val instanceof Uint8Array) return new Blob([val]).stream();
|
||||
|
||||
if (!(val instanceof Array) && val.toString !== Object.prototype.toString && val.toString instanceof Function) {
|
||||
val = val.toString();
|
||||
}
|
||||
|
||||
return stream(JSON.stringify(val));
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
import { hmac } from "https://deno.land/x/hmac@v2.0.1/mod.ts";
|
||||
|
||||
function trimBase64(val: string | Uint8Array) {
|
||||
if (val instanceof Uint8Array) {
|
||||
val = new TextDecoder().decode(val);
|
||||
}
|
||||
|
||||
while (val.endsWith('=')) {
|
||||
val = val.substring(0, val.length - 1);
|
||||
}
|
||||
|
||||
return val.replaceAll('+', '-').replaceAll('/', '_');
|
||||
}
|
||||
function toBase64(val: string) {
|
||||
val = btoa(val);
|
||||
|
||||
while (val.endsWith('=')) {
|
||||
val = val.substring(0, val.length - 1);
|
||||
}
|
||||
|
||||
return val.replaceAll('+', '-').replaceAll('/', '_');
|
||||
}
|
||||
function fromBase64(val: string) {
|
||||
return atob(val.replaceAll('-', '+').replaceAll('_', '/'));
|
||||
}
|
||||
|
||||
export interface JWTPayload {
|
||||
iss?: string;
|
||||
sub?: string;
|
||||
aud?: string;
|
||||
exp?: number;
|
||||
nbf?: number;
|
||||
iat?: number;
|
||||
jti?: string;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
export default {
|
||||
encode(payload: JWTPayload | string, key: string) {
|
||||
if (typeof payload === 'string') return payload;
|
||||
const rawHeader = JSON.stringify({ alg: "HS256", typ: "JWT" });
|
||||
const rawPayload = JSON.stringify(payload);
|
||||
const data = toBase64(rawHeader) + '.' + toBase64(rawPayload);
|
||||
const rawSignature = trimBase64(hmac('sha256', key, data, 'utf8', 'base64'));
|
||||
return data + '.' + rawSignature;
|
||||
},
|
||||
decode(jwt: string | JWTPayload, key?: string) {
|
||||
if (typeof jwt === 'object') return jwt;
|
||||
const segments = jwt.split('.');
|
||||
if (segments.length != 3) throw new Error("Expected JWT to have exactly 2 dots.");
|
||||
const [ rawHeader, rawPayload, givenSig ] = segments;
|
||||
const data = rawHeader + '.' + rawPayload;
|
||||
|
||||
if (key != undefined) {
|
||||
if (JSON.parse(fromBase64(rawHeader))?.alg != "HS256") throw new Error("Invalid JWT algorithm.");
|
||||
const actualSig = trimBase64(hmac('sha256', key, data, 'utf8', 'base64')) as string;
|
||||
if (givenSig != actualSig) throw new Error("Invalid JWT signature.");
|
||||
}
|
||||
|
||||
try { return JSON.parse(fromBase64(rawPayload)) as JWTPayload; }
|
||||
catch { throw new Error("Invalid JWT payload."); }
|
||||
},
|
||||
validate(j: string | JWTPayload, key: string) {
|
||||
if (typeof j === 'object') {
|
||||
j = this.encode(j, key);
|
||||
}
|
||||
const segments = j.split('.');
|
||||
if (segments.length != 3) throw new Error("Expected jwt to have exactly 2 dots.");
|
||||
const [ header, payload, givenSig ] = segments;
|
||||
const data = header + '.' + payload;
|
||||
const actualSig = trimBase64(hmac('sha256', key, data, 'utf8', 'base64')) as string;
|
||||
|
||||
return givenSig != actualSig;
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export function now() {
|
||||
return new Date().getTime() / 1000;
|
||||
}
|
||||
export function stream(...text: string[]) {
|
||||
return new Blob(text).stream();
|
||||
}
|
Loading…
Reference in New Issue
Block a user