diff --git a/src/AppDatabase.ts b/src/AppDatabase.ts deleted file mode 100644 index e281539..0000000 --- a/src/AppDatabase.ts +++ /dev/null @@ -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; - public readonly images: Collection; - - public constructor(db: Database) { - this.db = db; - this.users = db.collection('users'); - this.images = db.collection('images'); - } -} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 2c55d88..0000000 --- a/src/index.ts +++ /dev/null @@ -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' })); diff --git a/src/models/Image.ts b/src/models/Image.ts deleted file mode 100644 index ff12467..0000000 --- a/src/models/Image.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/src/models/User.ts b/src/models/User.ts deleted file mode 100644 index 58a14b3..0000000 --- a/src/models/User.ts +++ /dev/null @@ -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[]; -} diff --git a/src/routers/AppRouter.ts b/src/routers/AppRouter.ts deleted file mode 100644 index d6852aa..0000000 --- a/src/routers/AppRouter.ts +++ /dev/null @@ -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 { - if (error instanceof HttpError) return new HttpError({ error: error.body }, error.status); - return super.onError(_req, error); - } -} \ No newline at end of file diff --git a/src/routers/ImageRouter.ts b/src/routers/ImageRouter.ts deleted file mode 100644 index c94ff20..0000000 --- a/src/routers/ImageRouter.ts +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/src/routers/RootRouter.ts b/src/routers/RootRouter.ts deleted file mode 100644 index 3b17667..0000000 --- a/src/routers/RootRouter.ts +++ /dev/null @@ -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); - } -} diff --git a/src/routers/UserRouter.ts b/src/routers/UserRouter.ts deleted file mode 100644 index 2a1b99c..0000000 --- a/src/routers/UserRouter.ts +++ /dev/null @@ -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' } ] }); - } -} \ No newline at end of file diff --git a/src/server/HttpError.ts b/src/server/HttpError.ts deleted file mode 100644 index 8dd1549..0000000 --- a/src/server/HttpError.ts +++ /dev/null @@ -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()); - } -} diff --git a/src/server/RestRequest.ts b/src/server/RestRequest.ts deleted file mode 100644 index 1bb566b..0000000 --- a/src/server/RestRequest.ts +++ /dev/null @@ -1,112 +0,0 @@ -export type ParamDict = Record; -export type Headers = Record; - -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); - } -} diff --git a/src/server/RestResponse.ts b/src/server/RestResponse.ts deleted file mode 100644 index 03f6647..0000000 --- a/src/server/RestResponse.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { stream } from "../utils/utils.ts"; -import { Headers } from "./RestRequest.ts"; - -export default class RestResponse { - #status = 200; - #statusMsg = ''; - #body?: ReadableStream; - - 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) { - 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, - }); - } -} \ No newline at end of file diff --git a/src/server/Router.ts b/src/server/Router.ts deleted file mode 100644 index 661d74e..0000000 --- a/src/server/Router.ts +++ /dev/null @@ -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; - -export interface Handler { - handle(req: RestRequest): HandlerRes; -} -export interface RestOptions { - route?: string; -} - -// deno-lint-ignore no-explicit-any -export type ProcessFunc = (this: T, req: RestRequest, arg: any, name: string) => Promise | unknown; - -export interface RouterHandler { - path: string; - handler: Handler; -} -export function addMetaQuery(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(func: ProcessFunc) { - 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 { - 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()); - } - } - } -} diff --git a/src/server/decorators.ts b/src/server/decorators.ts deleted file mode 100644 index b2011a1..0000000 --- a/src/server/decorators.ts +++ /dev/null @@ -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 }; \ No newline at end of file diff --git a/src/server/decorators/auth.ts b/src/server/decorators/auth.ts deleted file mode 100644 index 5abbe14..0000000 --- a/src/server/decorators/auth.ts +++ /dev/null @@ -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; - }); -} \ No newline at end of file diff --git a/src/server/decorators/body.ts b/src/server/decorators/body.ts deleted file mode 100644 index 2ddaab5..0000000 --- a/src/server/decorators/body.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { makeParameterModifier } from "../Router.ts"; - -export type BodyType = 'raw' | 'json'; - -export default function body() { - return makeParameterModifier(req => { - return req.body; - }); -} \ No newline at end of file diff --git a/src/server/decorators/headers.ts b/src/server/decorators/headers.ts deleted file mode 100644 index 4b3251d..0000000 --- a/src/server/decorators/headers.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { makeParameterModifier } from "../Router.ts"; - - -export default function headers() { - return makeParameterModifier(req => req.headers); -} diff --git a/src/server/decorators/jwt.ts b/src/server/decorators/jwt.ts deleted file mode 100644 index 8c9986e..0000000 --- a/src/server/decorators/jwt.ts +++ /dev/null @@ -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(salt: ((self: T) => string) | string, required = false) { - return makeParameterModifier(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; - } - }); -} \ No newline at end of file diff --git a/src/server/decorators/page.ts b/src/server/decorators/page.ts deleted file mode 100644 index 1418a1b..0000000 --- a/src/server/decorators/page.ts +++ /dev/null @@ -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(cursor: FindCursor) { - 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); - }); -} \ No newline at end of file diff --git a/src/server/decorators/rest.ts b/src/server/decorators/rest.ts deleted file mode 100644 index 703661e..0000000 --- a/src/server/decorators/rest.ts +++ /dev/null @@ -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 = Router & { [X in KeyT]: Function; }; -type ModArray = ([ProcessFunc, ...ProcessFunc[]] | undefined)[]; - -export default function rest>(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 = []; - - 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; - 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)); - } - }, - })); - }; -} \ No newline at end of file diff --git a/src/server/decorators/route.ts b/src/server/decorators/route.ts deleted file mode 100644 index e1e35a6..0000000 --- a/src/server/decorators/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Router, { Handler, addMetaQuery } from "../Router.ts"; - -export default function route(path?: string) { - return (target: T, key: KeyT) => { - addMetaQuery(target, r => ({ - handler: r[key], - path: path ?? key, - })); - }; -} \ No newline at end of file diff --git a/src/server/decorators/schema.ts b/src/server/decorators/schema.ts deleted file mode 100644 index 8ad8f1a..0000000 --- a/src/server/decorators/schema.ts +++ /dev/null @@ -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}?`; -export type LiteralSchema = `:${T}`; -export type OptionalLiteralSchema = `:${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 = - ({ [x in keyof T]: SchemaType }) | - (T["$optional"] extends true ? undefined : never); -export type SchemaType = - 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 ? SchemaType | undefined : - T extends LiteralSchema ? T : - T extends OptionalLiteralSchema ? T | undefined : - T extends [] ? never : - T extends [Schema] ? SchemaType : - T extends [Schema, Schema] ? SchemaType | SchemaType : - T extends [Schema, Schema, Schema] ? SchemaType | SchemaType | SchemaType : - T extends ObjectSchema ? ObjectSchemaType : 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 { - 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(val: unknown, desc: T, path?: string): Promise> { - return _convert(path ? [path] : [], val, desc); -} - - -export default function schema(desc: Schema) { - return makeParameterModifier(async (_req, val, name) => await _convert([name], val, desc)); -} diff --git a/src/server/serialize.ts b/src/server/serialize.ts deleted file mode 100644 index 44699a6..0000000 --- a/src/server/serialize.ts +++ /dev/null @@ -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> { - 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)); -} diff --git a/src/utils/JWT.ts b/src/utils/JWT.ts deleted file mode 100644 index 48294db..0000000 --- a/src/utils/JWT.ts +++ /dev/null @@ -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; - } -} diff --git a/src/utils/utils.ts b/src/utils/utils.ts deleted file mode 100644 index 7002f99..0000000 --- a/src/utils/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function now() { - return new Date().getTime() / 1000; -} -export function stream(...text: string[]) { - return new Blob(text).stream(); -} \ No newline at end of file