diff --git a/app.ts b/app.ts index de7765a..fd064d4 100644 --- a/app.ts +++ b/app.ts @@ -1,12 +1,11 @@ +import {bold, yellow} from "fmt/colors.ts"; +import {Application} from "oak"; +import {oakCors} from "cors"; import { - Application, - bold, - ConnectionOptions, - ConnectionOptionsReader, - createConnection, - oakCors, - yellow, -} from "./deps.ts"; + ConnectionOptions, + ConnectionOptionsReader, + createConnection, +} from "typeorm"; import {config} from "./config/config.ts"; import router from "./routes.ts"; import {authorizationGuard, errorHandler, logger, notFound, oakSnelm, timing} from "./middleware/mod.ts"; @@ -15,7 +14,7 @@ import {authorizationGuard, errorHandler, logger, notFound, oakSnelm, timing} fr * Reads connection options stored in ormconfig configuration file. */ async function getConnectionOptions(connectionName: string = "default"): Promise { - return new ConnectionOptionsReader({root: "."}).get(connectionName); + return new ConnectionOptionsReader({root: "."}).get(connectionName); } const {APP_HOST, APP_PORT, CORS_ORIGIN, CORS_METHODS, CORS_ALLOWED_HEADERS} = config; @@ -25,9 +24,9 @@ const connectionOptions: ConnectionOptions = await getConnectionOptions(); await createConnection(connectionOptions); const cors = oakCors({ - origin: CORS_ORIGIN, - methods: CORS_METHODS, - allowedHeaders: CORS_ALLOWED_HEADERS + origin: CORS_ORIGIN, + methods: CORS_METHODS, + allowedHeaders: CORS_ALLOWED_HEADERS }); const app = new Application(); @@ -44,11 +43,11 @@ app.use(router.allowedMethods()); app.use(notFound); app.addEventListener("listen", ({hostname, port}) => { - console.log(bold("Start listening on ") + yellow(`${hostname}:${port}`)); + console.log(bold("Start listening on ") + yellow(`${hostname}:${port}`)); }); if (import.meta.main) { - await app.listen(`${APP_HOST}:${APP_PORT}`); + await app.listen(`${APP_HOST}:${APP_PORT}`); } export {app}; \ No newline at end of file diff --git a/app_test.ts b/app_test.ts index 90eb529..874170d 100644 --- a/app_test.ts +++ b/app_test.ts @@ -1,106 +1,106 @@ -import {superoak} from "./deps.ts"; +import {superoak} from "superoak"; import {app} from "./app.ts"; import {makeAccessToken} from "./util/jwt.ts"; import {User} from "./entity/user.ts"; import {Authority, AuthorityName} from "./entity/authority.ts"; async function generateAdminToken() { - const user = new User(); - user.username = "admin"; - user.email = "admin@itrunner.org"; + const user = new User(); + user.username = "admin"; + user.email = "admin@itrunner.org"; - const roleAdmin = new Authority(); - roleAdmin.name = AuthorityName.ADMIN; - const roleUser = new Authority(); + const roleAdmin = new Authority(); + roleAdmin.name = AuthorityName.ADMIN; + const roleUser = new Authority(); - user.authorities = [roleAdmin, roleUser]; + user.authorities = [roleAdmin, roleUser]; - return await makeAccessToken(user); + return await makeAccessToken(user); } async function generateUserToken() { - const user = new User(); - user.username = "user"; - user.email = "user@itrunner.org"; + const user = new User(); + user.username = "user"; + user.email = "user@itrunner.org"; - const roleUser = new Authority(); + const roleUser = new Authority(); - user.authorities = [roleUser]; + user.authorities = [roleUser]; - return await makeAccessToken(user); + return await makeAccessToken(user); } const adminToken = await generateAdminToken(); const userToken = await generateUserToken(); Deno.test("Unauthorized Error", async () => { - const request = await superoak(app); - await request.get("/api/heroes").expect(401); + const request = await superoak(app); + await request.get("/api/heroes").expect(401); }); Deno.test("Forbidden Error", async () => { - const request = await superoak(app); - await request.post("/api/heroes").auth(userToken, {type: "bearer"}).send({name: "Jack"}) - .expect(403); + const request = await superoak(app); + await request.post("/api/heroes").auth(userToken, {type: "bearer"}).send({name: "Jack"}) + .expect(403); }); Deno.test("should get token successfully", async () => { - const request = await superoak(app); - await request.post("/api/auth").send({username: "admin", password: "admin"}) - .expect('Content-Type', 'application/json; charset=utf-8') - .expect(/{"token":.*,"refresh_token":.*}/i); + const request = await superoak(app); + await request.post("/api/auth").send({username: "admin", password: "admin"}) + .expect('Content-Type', 'application/json; charset=utf-8') + .expect(/{"token":.*,"refresh_token":.*}/i); }); Deno.test("should provide correct credential", async () => { - const request = await superoak(app); - await request.post("/api/auth").send({username: "admin", password: "111111"}) - .expect(401); + const request = await superoak(app); + await request.post("/api/auth").send({username: "admin", password: "111111"}) + .expect(401); }); Deno.test("crud should be executed successfully", async () => { - // add hero - let request = await superoak(app); - await request.post("/api/heroes").auth(adminToken, {type: "bearer"}).send({name: "Jack"}) - .expect(200).expect(/{"name":"Jack","id":11,.*}/); - - // update hero - request = await superoak(app); - await request.post("/api/heroes").auth(adminToken, {type: "bearer"}).send({name: "Jacky", id: 11}) - .expect(200).expect(/{"name":"Jacky","id":11,.*}/); - - // find heroes by name - request = await superoak(app); - await request.get("/api/heroes/?name=m").auth(adminToken, {type: "bearer"}) - .expect(200); - - // find heroes by page - request = await superoak(app); - await request.get("/api/heroes").auth(adminToken, {type: "bearer"}).send({page: 0, size: 10}) - .expect(200).expect(/{.*"totalElements":11.*}/); - - // get hero by id - request = await superoak(app); - await request.get("/api/heroes/11").auth(adminToken, {type: "bearer"}) - .expect(200).expect(/{"id":11,"name":"Jacky",.*}/); - - // delete hero - request = await superoak(app); - await request.delete("/api/heroes/11").auth(adminToken, {type: "bearer"}).expect(200); + // add hero + let request = await superoak(app); + await request.post("/api/heroes").auth(adminToken, {type: "bearer"}).send({name: "Jack"}) + .expect(200).expect(/{"name":"Jack","id":11,.*}/); + + // update hero + request = await superoak(app); + await request.post("/api/heroes").auth(adminToken, {type: "bearer"}).send({name: "Jacky", id: 11}) + .expect(200).expect(/{"name":"Jacky","id":11,.*}/); + + // find heroes by name + request = await superoak(app); + await request.get("/api/heroes/?name=m").auth(adminToken, {type: "bearer"}) + .expect(200); + + // find heroes by page + request = await superoak(app); + await request.get("/api/heroes").auth(adminToken, {type: "bearer"}).send({page: 0, size: 10}) + .expect(200).expect(/{.*"totalElements":11.*}/); + + // get hero by id + request = await superoak(app); + await request.get("/api/heroes/11").auth(adminToken, {type: "bearer"}) + .expect(200).expect(/{"id":11,"name":"Jacky",.*}/); + + // delete hero + request = await superoak(app); + await request.delete("/api/heroes/11").auth(adminToken, {type: "bearer"}).expect(200); }); Deno.test("validation failed", async () => { - // add hero - const request = await superoak(app); - await request.post("/api/heroes").auth(adminToken, {type: "bearer"}).send({name: "J"}) - .expect(400).expect(/.*,"message":"name characters length must be between 3-30; ",.*/i); + // add hero + const request = await superoak(app); + await request.post("/api/heroes").auth(adminToken, {type: "bearer"}).send({name: "J"}) + .expect(400).expect(/.*,"message":"name characters length must be between 3-30; ",.*/i); }); Deno.test("Path not found", async () => { - const request = await superoak(app); - await request.get("/api/test").auth(adminToken, {type: "bearer"}).expect(404); + const request = await superoak(app); + await request.get("/api/test").auth(adminToken, {type: "bearer"}).expect(404); }); Deno.test("Entity not found", async () => { - const request = await superoak(app); - await request.get("/api/test/9999").auth(adminToken, {type: "bearer"}).expect(404); + const request = await superoak(app); + await request.get("/api/test/9999").auth(adminToken, {type: "bearer"}).expect(404); }); diff --git a/config/config.ts b/config/config.ts index cca24a2..8782d4f 100644 --- a/config/config.ts +++ b/config/config.ts @@ -1,3 +1,3 @@ -import { config as loadConfig } from "../deps.ts"; +import {config as loadConfig} from "dotenv"; -export const config = loadConfig({ safe: true }); +export const config = loadConfig({safe: true}); diff --git a/controller/auth_controller.ts b/controller/auth_controller.ts index d8da6e2..5d31c71 100644 --- a/controller/auth_controller.ts +++ b/controller/auth_controller.ts @@ -1,17 +1,18 @@ -import {Request, required, Response} from "../deps.ts"; +import {Request, Response} from "oak"; +import {required} from "validasaur"; import {LoginCredential} from "../interfaces/LoginCredential.ts"; import * as authService from "../service/user_service.ts"; import {validateRequest} from "../util/validator.ts"; -async function login({request, response}: { request: Request; response: Response }) { - const credential = (await request.body().value) as LoginCredential; - await validateCredential(credential); - response.body = await authService.authenticate(credential); +async function login({request, response}: { request: Request; response: Response }): Promise { + const credential = (await request.body().value) as LoginCredential; + await validateCredential(credential); + response.body = await authService.authenticate(credential); } async function validateCredential(credential: LoginCredential): Promise { - const credentialSchema = {username: [required], password: [required]}; - await validateRequest(credential, credentialSchema); + const credentialSchema = {username: [required], password: [required]}; + await validateRequest(credential, credentialSchema); } export {login}; diff --git a/controller/hero_controller.ts b/controller/hero_controller.ts index 023d3f1..72bfcaf 100644 --- a/controller/hero_controller.ts +++ b/controller/hero_controller.ts @@ -1,83 +1,84 @@ -import {lengthBetween, match, oakHelpers, required, RouterContext, Status} from "../deps.ts"; +import {helpers as oakHelpers, RouterContext, Status} from "oak"; +import {lengthBetween, match, required} from "validasaur"; import {deleteHeroById, findHeroesByName, getHeroById, getHeroesByPage, saveHero} from "../service/hero_service.ts"; import {Hero} from "../entity/hero.ts"; import {parsePageQuery} from "../util/pages.ts"; import {validateRequest} from "../util/validator.ts"; // get heroes -async function getHeroes(context: RouterContext) { - const query = oakHelpers.getQuery(context); - context.response.body = await getHeroesByPage(parsePageQuery(query)); +async function getHeroes(context: RouterContext): Promise { + const query = oakHelpers.getQuery(context); + context.response.body = await getHeroesByPage(parsePageQuery(query)); } // get hero by id -async function getHero(context: RouterContext) { - const {id} = context.params; - await validateId(id); +async function getHero(context: RouterContext): Promise { + const {id} = context.params; + await validateId(id); - if (id) { - context.response.body = await getHeroById(parseInt(id)); - } + if (id) { + context.response.body = await getHeroById(parseInt(id)); + } } // search heroes by name -async function searchHeroes(context: RouterContext) { - const {name} = oakHelpers.getQuery(context); - await validateName(name); - context.response.body = await findHeroesByName(name); +async function searchHeroes(context: RouterContext): Promise { + const {name} = oakHelpers.getQuery(context); + await validateName(name); + context.response.body = await findHeroesByName(name); } // add a new hero -async function addHero(context: RouterContext) { - const hero: Hero = await context.request.body().value; - await validateHero(hero); - context.response.body = await saveHero(hero); +async function addHero(context: RouterContext): Promise { + const hero: Hero = await context.request.body().value; + await validateHero(hero); + context.response.body = await saveHero(hero); } // update a hero information -async function updateHero(context: RouterContext) { - const hero: Hero = await context.request.body().value; - await validateHero(hero); - context.response.body = await saveHero(hero); +async function updateHero(context: RouterContext): Promise { + const hero: Hero = await context.request.body().value; + await validateHero(hero); + context.response.body = await saveHero(hero); } // Delete a hero by id -async function deleteHero(context: RouterContext) { - const {id} = context.params; - await validateId(id); +async function deleteHero(context: RouterContext): Promise { + const {id} = context.params; + await validateId(id); - if (id) { - await deleteHeroById(parseInt(id)); - context.response.status = Status.OK; - } + if (id) { + await deleteHeroById(parseInt(id)); + context.response.status = Status.OK; + } } async function validateHero(hero: Hero): Promise { - const heroSchema = {name: [required, lengthBetween(3, 30)]}; - await validateRequest(hero, heroSchema); + const heroSchema = {name: [required, lengthBetween(3, 30)]}; + await validateRequest(hero, heroSchema); } async function validateId(id: string | undefined): Promise { - await validateRequest( - {id}, - {id: [match(/^\d*$/)]}, - {messages: {"id": "id must be a number"}} - ); + await validateRequest( + {id}, + {id: [match(/^\d*$/)]}, + {messages: {"id": "id must be a number"}} + ); } async function validateName(name: string | undefined): Promise { - await validateRequest( - {name}, - {name: [required]}, - {messages: {"name.required": "Required parameter 'name' is not present"}} - ); + await validateRequest( + {name}, + {name: [required]}, + {messages: {"name.required": "Required parameter 'name' is not present"}} + ); } export { - addHero, - deleteHero, - getHero, - getHeroes, - searchHeroes, - updateHero, + addHero, + deleteHero, + getHero, + getHeroes, + searchHeroes, + updateHero, }; diff --git a/deps.ts b/deps.ts deleted file mode 100644 index a4b75e2..0000000 --- a/deps.ts +++ /dev/null @@ -1,76 +0,0 @@ -export { - bold, - cyan, - green, - yellow, -} from "https://deno.land/std@0.95.0/fmt/colors.ts"; -export * as bcrypt from "https://deno.land/x/bcrypt@v0.2.4/mod.ts"; -export { oakCors } from "https://deno.land/x/cors@v1.2.1/mod.ts"; -export { - create, - getNumericDate, - verify, -} from "https://deno.land/x/djwt@v2.2/mod.ts"; -export type { Header, Payload } from "https://deno.land/x/djwt@v2.2/mod.ts"; -export { config } from "https://deno.land/x/dotenv@v2.0.0/mod.ts"; -export { - Application, - Context, - helpers as oakHelpers, - HttpError, - httpErrors, - isHttpError, - Request, - Response, - Router, - Status, -} from "https://deno.land/x/oak@v7.3.0/mod.ts"; -export type { - HTTPMethods, - RouteParams, - RouterContext, -} from "https://deno.land/x/oak@v7.3.0/mod.ts"; -export { - Column, - ConnectionOptionsReader, - createConnection, - CreateDateColumn, - DeleteResult, - Entity, - EntityRepository, - getConnection, - getCustomRepository, - getRepository, - JoinTable, - ManyToMany, - PrimaryGeneratedColumn, - Repository, - UpdateDateColumn, -} from "https://deno.land/x/typeorm@v0.2.23-rc10/mod.ts"; -export type { - ConnectionOptions, - MigrationInterface, - QueryRunner, -} from "https://deno.land/x/typeorm@v0.2.23-rc10/mod.ts"; -export { Snelm } from "https://deno.land/x/snelm@1.3.0/mod.ts"; -export { - firstMessages, - isEmail, - lengthBetween, - match, - required, - validate, -} from "https://deno.land/x/validasaur@v0.15.0/mod.ts"; -export type { - InputData, - ValidationErrors, - ValidationOptions, - ValidationRules, -} from "https://deno.land/x/validasaur@v0.15.0/mod.ts"; -export { - assert, - assertEquals, - assertThrows, - assertThrowsAsync, -} from "https://deno.land/std@0.95.0/testing/asserts.ts"; -export {superoak} from "https://deno.land/x/superoak@4.2.0/mod.ts"; diff --git a/entity/authority.ts b/entity/authority.ts index 738a5e4..279ace3 100644 --- a/entity/authority.ts +++ b/entity/authority.ts @@ -1,21 +1,21 @@ -import {Column, Entity, ManyToMany, PrimaryGeneratedColumn} from "../deps.ts"; +import {Column, Entity, ManyToMany, PrimaryGeneratedColumn} from "typeorm"; import {User} from "./user.ts"; enum AuthorityName { - USER = "ROLE_USER", - ADMIN = "ROLE_ADMIN" + USER = "ROLE_USER", + ADMIN = "ROLE_ADMIN" } @Entity() class Authority { - @PrimaryGeneratedColumn() - id!: number; + @PrimaryGeneratedColumn() + id!: number; - @Column({type: String, name: "authority_name", length: 10}) - name: AuthorityName = AuthorityName.USER; + @Column({type: String, name: "authority_name", length: 10}) + name: AuthorityName = AuthorityName.USER; - @ManyToMany((type) => User, (user) => user.authorities) - users: User[]; + @ManyToMany((type) => User, (user) => user.authorities) + users: User[]; } export {AuthorityName, Authority}; diff --git a/entity/hero.ts b/entity/hero.ts index ab26d60..4457ec9 100644 --- a/entity/hero.ts +++ b/entity/hero.ts @@ -4,19 +4,19 @@ import { Entity, PrimaryGeneratedColumn, UpdateDateColumn, -} from "../deps.ts"; +} from "typeorm"; -@Entity({ orderBy: { id: "ASC" } }) +@Entity({orderBy: {id: "ASC"}}) export class Hero { @PrimaryGeneratedColumn() id!: number; - @Column("varchar", { name: "hero_name", length: 50, unique: true }) + @Column("varchar", {name: "hero_name", length: 50, unique: true}) name: string; - @CreateDateColumn({ name: "created_date" }) + @CreateDateColumn({name: "created_date"}) createdDate: Date; - @UpdateDateColumn({ name: "last_modified_date" }) + @UpdateDateColumn({name: "last_modified_date"}) lastModifiedDate: Date; } diff --git a/entity/user.ts b/entity/user.ts index bf2daa2..7cc1002 100644 --- a/entity/user.ts +++ b/entity/user.ts @@ -4,27 +4,27 @@ import { JoinTable, ManyToMany, PrimaryGeneratedColumn, -} from "../deps.ts"; -import { Authority } from "./authority.ts"; +} from "typeorm"; +import {Authority} from "./authority.ts"; @Entity("users") export class User { @PrimaryGeneratedColumn() id!: number; - @Column({ type: String, length: 50, unique: true }) + @Column({type: String, length: 50, unique: true}) username: string; - @Column({ type: String, length: 100 }) + @Column({type: String, length: 100}) password: string; - @Column({ type: String, length: 50, unique: true }) + @Column({type: String, length: 50, unique: true}) email: string; - @Column({ type: Boolean }) + @Column({type: Boolean}) enabled: boolean = true; - @ManyToMany((type) => Authority, { eager: true }) + @ManyToMany((type) => Authority, {eager: true}) @JoinTable({ name: "user_authority", joinColumn: { diff --git a/import_map.json b/import_map.json new file mode 100644 index 0000000..5be4d13 --- /dev/null +++ b/import_map.json @@ -0,0 +1,15 @@ +{ + "imports": { + "fmt/": "https://deno.land/std@0.95.0/fmt/", + "bcrypt": "https://deno.land/x/bcrypt@v0.2.4/mod.ts", + "cors": "https://deno.land/x/cors@v1.2.2/mod.ts", + "djwt": "https://deno.land/x/djwt@v2.2/mod.ts", + "dotenv": "https://deno.land/x/dotenv@v3.0.0/mod.ts", + "oak": "https://deno.land/x/oak@v7.3.0/mod.ts", + "snelm": "https://deno.land/x/snelm@1.3.0/mod.ts", + "typeorm": "https://deno.land/x/typeorm@v0.2.23-rc10/mod.ts", + "validasaur": "https://deno.land/x/validasaur@v0.15.0/mod.ts", + "testing/": "https://deno.land/std@0.95.0/testing/", + "superoak": "https://deno.land/x/superoak@4.2.0/mod.ts" + } +} diff --git a/middleware/authority_guard.ts b/middleware/authority_guard.ts index 4427994..b0178ea 100644 --- a/middleware/authority_guard.ts +++ b/middleware/authority_guard.ts @@ -1,31 +1,31 @@ -import {httpErrors, RouterContext, Status} from "../deps.ts"; +import {httpErrors, RouterContext, Status} from "oak"; /** * Authority Guard Middleware */ -async function authorityGuard(context: RouterContext, next: () => Promise, roles: string[]) { - context.assert(roles, Status.BadRequest); - context.assert(context.state.user, Status.Unauthorized); +async function authorityGuard(context: RouterContext, next: () => Promise, roles: string[]) { + context.assert(roles, Status.BadRequest); + context.assert(context.state.user, Status.Unauthorized); - if (hasAnyRole(context.state.user.authorities, roles)) { - await next(); - } else { - throw new httpErrors.Forbidden(); - } + if (hasAnyRole(context.state.user.authorities, roles)) { + await next(); + } else { + throw new httpErrors.Forbidden(); + } } function hasAnyRole(authorities: string[], roles: string[]) { - let hasRole = false; + let hasRole = false; - // @ts-ignore - for (const role of roles) { - if (authorities.indexOf(`ROLE_${role}`) >= 0) { - hasRole = true; - break; - } + // @ts-ignore + for (const role of roles) { + if (authorities.indexOf(`ROLE_${role}`) >= 0) { + hasRole = true; + break; } + } - return hasRole; + return hasRole; } export {authorityGuard}; \ No newline at end of file diff --git a/middleware/authorization_guard.ts b/middleware/authorization_guard.ts index 62ad483..a3abaac 100644 --- a/middleware/authorization_guard.ts +++ b/middleware/authorization_guard.ts @@ -1,4 +1,4 @@ -import {Context, httpErrors, Request} from "../deps.ts"; +import {Context, httpErrors, Request} from "oak"; import {getPayload} from "../util/jwt.ts"; import {config} from "../config/config.ts"; @@ -7,32 +7,32 @@ const ignorePaths = config.IGNORE_PATHS.split(","); /** * Authorization Guard Middleware */ -async function authorizationGuard(context: Context, next: () => Promise) { - if (!isIgnore(context.request)) { - await parseBearerToken(context); +async function authorizationGuard(context: Context, next: () => Promise) { + if (!isIgnore(context.request)) { + await parseBearerToken(context); - if (!context.state.user) { - throw new httpErrors.Unauthorized(); - } + if (!context.state.user) { + throw new httpErrors.Unauthorized(); } + } - await next(); + await next(); } function isIgnore(request: Request) { - return request.method === "OPTIONS" || ignorePaths.indexOf(request.url.pathname) >= 0; + return request.method === "OPTIONS" || ignorePaths.indexOf(request.url.pathname) >= 0; } async function parseBearerToken(context: Context): Promise { - const authHeader = context.request.headers.get("Authorization"); - if (authHeader) { - const token = authHeader.replace(/^bearer/i, "").trim(); - const user = await getPayload(token); - - if (user) { - context.state.user = user; - } + const authHeader = context.request.headers.get("Authorization"); + if (authHeader) { + const token = authHeader.replace(/^bearer/i, "").trim(); + const user = await getPayload(token); + + if (user) { + context.state.user = user; } + } } export {authorizationGuard}; \ No newline at end of file diff --git a/middleware/error_handler.ts b/middleware/error_handler.ts index b814397..13f7cdb 100644 --- a/middleware/error_handler.ts +++ b/middleware/error_handler.ts @@ -1,25 +1,25 @@ -import {Context, isHttpError, Status} from "../deps.ts"; +import {Context, isHttpError, Status} from "oak"; -export async function errorHandler(context: Context, next: () => Promise) { - try { - await next(); - } catch (err) { - console.log(err); +export async function errorHandler(context: Context, next: () => Promise) { + try { + await next(); + } catch (err) { + console.log(err); - if (isHttpError(err)) { - context.response.status = err.status; - } else { - switch (err.name) { - case "EntityNotFound": - context.response.status = Status.NotFound; - break; - default: - // handle other statuses - context.response.status = Status.BadRequest; - } - } - - context.response.body = {error: err.name, message: err.message, timestamp: Date.now()}; - context.response.type = "json"; + if (isHttpError(err)) { + context.response.status = err.status; + } else { + switch (err.name) { + case "EntityNotFound": + context.response.status = Status.NotFound; + break; + default: + // handle other statuses + context.response.status = Status.BadRequest; + } } + + context.response.body = {error: err.name, message: err.message, timestamp: Date.now()}; + context.response.type = "json"; + } } diff --git a/middleware/logger.ts b/middleware/logger.ts index e3c7b17..55dc1ae 100644 --- a/middleware/logger.ts +++ b/middleware/logger.ts @@ -1,10 +1,11 @@ -import {bold, Context, cyan, green} from "../deps.ts"; +import {Context} from "oak"; +import {bold, cyan, green} from "fmt/colors.ts"; -async function logger(ctx: Context, next: () => Promise) { - await next(); - const rt = ctx.response.headers.get("X-Response-Time"); - console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`); - console.log(`${green(ctx.request.method)} ${cyan(decodeURIComponent(ctx.request.url.pathname))} - ${bold(String(rt))}`); +async function logger(ctx: Context, next: () => Promise): Promise { + await next(); + const rt = ctx.response.headers.get("X-Response-Time"); + console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`); + console.log(`${green(ctx.request.method)} ${cyan(decodeURIComponent(ctx.request.url.pathname))} - ${bold(String(rt))}`); } export {logger}; \ No newline at end of file diff --git a/middleware/not_found.ts b/middleware/not_found.ts index e9171f9..3647781 100644 --- a/middleware/not_found.ts +++ b/middleware/not_found.ts @@ -1,8 +1,8 @@ -import {Context, Status} from "../deps.ts"; +import {Context, Status} from "oak"; function notFound(context: Context) { - context.response.status = Status.NotFound; - context.response.body = {error: "NotFound", message: `Path ${context.request.url} not found`}; + context.response.status = Status.NotFound; + context.response.body = {error: "NotFound", message: `Path ${context.request.url} not found`}; } export {notFound}; \ No newline at end of file diff --git a/middleware/snelm.ts b/middleware/snelm.ts index fb064d2..00ca544 100644 --- a/middleware/snelm.ts +++ b/middleware/snelm.ts @@ -1,21 +1,22 @@ -import {Context, Snelm} from "../deps.ts"; +import {Context} from "oak"; +import {Snelm} from "snelm"; // referrerPolicy config const referrerPolicyConfig = { - policy: 'same-origin' + policy: 'same-origin' }; // hidePoweredBy config const hidePoweredByConfig = { - setTo: 'iTRunner' + setTo: 'iTRunner' }; const snelm = new Snelm("oak", { - referrerPolicy: referrerPolicyConfig, - hidePoweredBy: hidePoweredByConfig + referrerPolicy: referrerPolicyConfig, + hidePoweredBy: hidePoweredByConfig }); -export async function oakSnelm(ctx: Context, next: () => Promise) { - ctx.response = snelm.snelm(ctx.request, ctx.response); - await next(); +export async function oakSnelm(ctx: Context, next: () => Promise) { + ctx.response = snelm.snelm(ctx.request, ctx.response); + await next(); } \ No newline at end of file diff --git a/middleware/timing.ts b/middleware/timing.ts index b3133ab..f41f9fa 100644 --- a/middleware/timing.ts +++ b/middleware/timing.ts @@ -1,10 +1,10 @@ -import {Context} from "../deps.ts"; +import {Context} from "oak"; -async function timing(ctx: Context, next: () => Promise) { - const start = Date.now(); - await next(); - const ms = Date.now() - start; - ctx.response.headers.set("X-Response-Time", `${ms}ms`); +async function timing(ctx: Context, next: () => Promise) { + const start = Date.now(); + await next(); + const ms = Date.now() - start; + ctx.response.headers.set("X-Response-Time", `${ms}ms`); } export {timing}; diff --git a/migration/1598262813255-InitialData.ts b/migration/1598262813255-InitialData.ts index afef96f..42331a5 100644 --- a/migration/1598262813255-InitialData.ts +++ b/migration/1598262813255-InitialData.ts @@ -1,4 +1,4 @@ -import { MigrationInterface, QueryRunner } from "../deps.ts"; +import {MigrationInterface, QueryRunner} from "typeorm"; export class InitialData1598262813255 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { diff --git a/repository/HeroRepository.ts b/repository/HeroRepository.ts index 384a870..3016624 100644 --- a/repository/HeroRepository.ts +++ b/repository/HeroRepository.ts @@ -1,6 +1,6 @@ -import { Hero } from "../entity/hero.ts"; -import { EntityRepository, Repository } from "../deps.ts"; -import { Page, Pageable } from "../util/pages.ts"; +import {EntityRepository, Repository} from "typeorm"; +import {Hero} from "../entity/hero.ts"; +import {Page, Pageable} from "../util/pages.ts"; @EntityRepository(Hero) export class HeroRepository extends Repository { @@ -12,17 +12,17 @@ export class HeroRepository extends Repository { } return queryBuilder.skip(pageable.page * pageable.size).take(pageable.size) - .getManyAndCount().then((entitiesWithCount) => { - return { - content: entitiesWithCount[0], - totalElements: entitiesWithCount[1], - }; - }); + .getManyAndCount().then((entitiesWithCount) => { + return { + content: entitiesWithCount[0], + totalElements: entitiesWithCount[1], + }; + }); } async findByName(name: string): Promise { return this.createQueryBuilder("hero") - .where("hero.name like :name", { name: `%${name}%` }) - .getMany(); + .where("hero.name like :name", {name: `%${name}%`}) + .getMany(); } } diff --git a/repository/UserRepository.ts b/repository/UserRepository.ts index e100e0a..805ba8d 100644 --- a/repository/UserRepository.ts +++ b/repository/UserRepository.ts @@ -1,9 +1,9 @@ -import { EntityRepository, Repository } from "../deps.ts"; -import { User } from "../entity/user.ts"; +import {EntityRepository, Repository} from "typeorm"; +import {User} from "../entity/user.ts"; @EntityRepository(User) export class UserRepository extends Repository { findByUsername(username: string) { - return this.findOne({ username, enabled: true }); + return this.findOne({username, enabled: true}); } } diff --git a/routes.ts b/routes.ts index a8515ce..370eb60 100644 --- a/routes.ts +++ b/routes.ts @@ -1,4 +1,4 @@ -import {Router} from "./deps.ts"; +import {Router} from "oak"; import {addHero, deleteHero, getHero, getHeroes, searchHeroes, updateHero} from "./controller/hero_controller.ts"; import {login} from "./controller/auth_controller.ts"; import {authorityGuard} from "./middleware/authority_guard.ts"; diff --git a/service/hero_service.ts b/service/hero_service.ts index 2c8e82b..e98ff39 100644 --- a/service/hero_service.ts +++ b/service/hero_service.ts @@ -1,26 +1,27 @@ -import {DeleteResult, getCustomRepository} from "../deps.ts"; +import {getCustomRepository} from "typeorm"; +import type {DeleteResult} from "typeorm"; import {HeroRepository} from "../repository/HeroRepository.ts"; import {Page, Pageable} from "../util/pages.ts"; import {Hero} from "../entity/hero.ts"; async function getHeroById(id: number): Promise { - return await getCustomRepository(HeroRepository).findOneOrFail(id); + return await getCustomRepository(HeroRepository).findOneOrFail(id); } async function getHeroesByPage(pageable: Pageable): Promise> { - return await getCustomRepository(HeroRepository).findAll(pageable); + return await getCustomRepository(HeroRepository).findAll(pageable); } async function findHeroesByName(name: string): Promise { - return await getCustomRepository(HeroRepository).findByName(name); + return await getCustomRepository(HeroRepository).findByName(name); } async function saveHero(hero: Hero): Promise { - return await getCustomRepository(HeroRepository).save(hero); + return await getCustomRepository(HeroRepository).save(hero); } async function deleteHeroById(id: number): Promise { - return await getCustomRepository(HeroRepository).delete(id); + return await getCustomRepository(HeroRepository).delete(id); } export {getHeroById, getHeroesByPage, findHeroesByName, saveHero, deleteHeroById}; \ No newline at end of file diff --git a/service/user_service.ts b/service/user_service.ts index 072ff7e..cf19772 100644 --- a/service/user_service.ts +++ b/service/user_service.ts @@ -1,22 +1,24 @@ +import {getCustomRepository} from "typeorm"; +import {httpErrors} from "oak"; +import {compare} from "bcrypt"; import {LoginCredential} from "../interfaces/LoginCredential.ts"; -import {bcrypt, getCustomRepository, httpErrors} from "../deps.ts"; import {makeAccessToken, makeRefreshToken} from "../util/jwt.ts"; import {UserRepository} from "../repository/UserRepository.ts"; export async function authenticate(credential: LoginCredential) { - const {username, password} = credential; - const user = await getCustomRepository(UserRepository).findByUsername(username); + const {username, password} = credential; + const user = await getCustomRepository(UserRepository).findByUsername(username); - if (user) { - const passHash = user.password; - const isValidPass = await bcrypt.compare(password, passHash); - if (isValidPass) { - return { - "token": await makeAccessToken(user), - "refresh_token": await makeRefreshToken(user) - }; - } + if (user) { + const passHash = user.password; + const isValidPass = await compare(password, passHash); + if (isValidPass) { + return { + "token": await makeAccessToken(user), + "refresh_token": await makeRefreshToken(user) + }; } + } - throw new httpErrors.Unauthorized("Wrong credential"); + throw new httpErrors.Unauthorized("Wrong credential"); } diff --git a/util/jwt.ts b/util/jwt.ts index 4e72c67..1e607c3 100644 --- a/util/jwt.ts +++ b/util/jwt.ts @@ -1,45 +1,45 @@ -import type {Header, Payload} from "../deps.ts"; -import {create, getNumericDate, verify} from "../deps.ts"; +import type {Header, Payload} from "djwt"; +import {create, getNumericDate, verify} from "djwt"; import {config} from "../config/config.ts"; const { - JWT_SECRET, - JWT_ACCESS_TOKEN_EXP, - JWT_REFRESH_TOKEN_EXP, - JWT_ISSUER, + JWT_SECRET, + JWT_ACCESS_TOKEN_EXP, + JWT_REFRESH_TOKEN_EXP, + JWT_ISSUER, } = config; const header: Header = { - alg: "HS512", - typ: "JWT", + alg: "HS512", + typ: "JWT", }; async function makeAccessToken(user: any): Promise { - const payload: Payload = { - iss: JWT_ISSUER, - sub: user.username, - email: user.email, - authorities: user.authorities.map((auth: { name: string }) => { - return auth.name; - }), - exp: getNumericDate(parseInt(JWT_ACCESS_TOKEN_EXP)), - }; - - return await create(header, payload, JWT_SECRET); + const payload: Payload = { + iss: JWT_ISSUER, + sub: user.username, + email: user.email, + authorities: user.authorities.map((auth: { name: string }) => { + return auth.name; + }), + exp: getNumericDate(parseInt(JWT_ACCESS_TOKEN_EXP)), + }; + + return await create(header, payload, JWT_SECRET); } async function makeRefreshToken(user: any): Promise { - const payload: Payload = { - iss: JWT_ISSUER, - sub: user.username, - exp: getNumericDate(parseInt(JWT_REFRESH_TOKEN_EXP)), - }; + const payload: Payload = { + iss: JWT_ISSUER, + sub: user.username, + exp: getNumericDate(parseInt(JWT_REFRESH_TOKEN_EXP)), + }; - return await create(header, payload, JWT_SECRET); + return await create(header, payload, JWT_SECRET); } async function getPayload(jwt: string): Promise { - return await verify(jwt, JWT_SECRET, "HS512"); + return await verify(jwt, JWT_SECRET, "HS512"); } export {makeAccessToken, makeRefreshToken, getPayload}; \ No newline at end of file diff --git a/util/pages.ts b/util/pages.ts index c32b6b0..7d8be89 100644 --- a/util/pages.ts +++ b/util/pages.ts @@ -4,32 +4,32 @@ const DEFAULT_SIZE = 10; export type Order = "ASC" | "DESC"; export interface Pageable { - page: number; - size: number; - sort?: { property: string; order: Order }; + page: number; + size: number; + sort?: { property: string; order: Order }; } export interface Page { - content: T[]; - totalElements: number; + content: T[]; + totalElements: number; } export function parsePageQuery(query: Record): Pageable { - const {page: pageString, size: sizeString, sort: sortString} = query; + const {page: pageString, size: sizeString, sort: sortString} = query; - const page = parseInt(pageString); - const size = parseInt(sizeString); + const page = parseInt(pageString); + const size = parseInt(sizeString); - const pageable: Pageable = { - page: isNaN(page) ? DEFAULT_PAGE : page, - size: isNaN(size) ? DEFAULT_SIZE : size - }; + const pageable: Pageable = { + page: isNaN(page) ? DEFAULT_PAGE : page, + size: isNaN(size) ? DEFAULT_SIZE : size + }; - if (sortString) { - const sort = sortString.split(","); - const [property, order] = sort; - pageable.sort = {property, order: order ? order : "ASC"}; - } + if (sortString) { + const sort = sortString.split(","); + const [property, order] = sort; + pageable.sort = {property, order: order ? order : "ASC"}; + } - return pageable; + return pageable; } diff --git a/util/pages_test.ts b/util/pages_test.ts index aa743b0..dba8eff 100644 --- a/util/pages_test.ts +++ b/util/pages_test.ts @@ -1,14 +1,14 @@ -import {assertEquals} from "../deps.ts"; +import {assertEquals} from "testing/asserts.ts"; import {parsePageQuery} from "./pages.ts"; Deno.test("default pageable parameter", () => { - assertEquals(parsePageQuery({}), {page: 0, size: 10}); - assertEquals(parsePageQuery({page: "page", size: "size"}), {page: 0, size: 10}); - assertEquals(parsePageQuery({sort: "field"}), {page: 0, size: 10, sort: {property: "field", order: "ASC"}}); + assertEquals(parsePageQuery({}), {page: 0, size: 10}); + assertEquals(parsePageQuery({page: "page", size: "size"}), {page: 0, size: 10}); + assertEquals(parsePageQuery({sort: "field"}), {page: 0, size: 10, sort: {property: "field", order: "ASC"}}); }); Deno.test("normal pageable parameter", () => { - assertEquals(parsePageQuery({page: "2", size: "10"}), {page: 2, size: 10}); - assertEquals(parsePageQuery({page: "2", size: "10", sort: "field,DESC"}), - {page: 2, size: 10, sort: {property: "field", order: "DESC"}}); + assertEquals(parsePageQuery({page: "2", size: "10"}), {page: 2, size: 10}); + assertEquals(parsePageQuery({page: "2", size: "10", sort: "field,DESC"}), + {page: 2, size: 10, sort: {property: "field", order: "DESC"}}); }); \ No newline at end of file diff --git a/util/validator.ts b/util/validator.ts index 7c0d832..236e1bd 100644 --- a/util/validator.ts +++ b/util/validator.ts @@ -1,32 +1,33 @@ import { - firstMessages, - httpErrors, - InputData, - validate, - ValidationErrors, - ValidationOptions, - ValidationRules -} from "../deps.ts"; + firstMessages, + InputData, + validate, + ValidationErrors, + ValidationOptions, + ValidationRules +} from "validasaur"; + +import {httpErrors} from "oak"; function getMessages(errors: ValidationErrors): string { - const firstErrors = firstMessages(errors); + const firstErrors = firstMessages(errors); - let messages = ""; - for (let field in firstErrors) { - messages += firstErrors[field] + "; "; - } + let messages = ""; + for (let field in firstErrors) { + messages += firstErrors[field] + "; "; + } - return messages; + return messages; } async function validateRequest(input: InputData, rules: ValidationRules, options?: ValidationOptions) { - // @ts-ignore - const [isValid, errors] = await validate(input, rules, options); + // @ts-ignore + const [isValid, errors] = await validate(input, rules, options); - if (!isValid) { - const message = getMessages(errors); - throw new httpErrors.BadRequest(message); - } + if (!isValid) { + const message = getMessages(errors); + throw new httpErrors.BadRequest(message); + } } export {validateRequest}; \ No newline at end of file diff --git a/util/validator_test.ts b/util/validator_test.ts index d3cb279..55e7a2a 100644 --- a/util/validator_test.ts +++ b/util/validator_test.ts @@ -1,24 +1,26 @@ -import {assertThrowsAsync, HttpError, isEmail, lengthBetween, required} from "../deps.ts"; +import {HttpError} from "oak"; +import {isEmail, lengthBetween, required} from "validasaur"; +import {assertThrowsAsync} from "testing/asserts.ts"; import {validateRequest} from "./validator.ts"; const userSchema = { - username: [required, lengthBetween(3, 30)], - password: [required, lengthBetween(10, 20)], - email: [required, isEmail] + username: [required, lengthBetween(3, 30)], + password: [required, lengthBetween(10, 20)], + email: [required, isEmail] } Deno.test("validate user information", async () => { - assertThrowsAsync(async () => { - return await validateRequest({}, userSchema); - }, - HttpError, - "username is required; password is required; email is required;"); + assertThrowsAsync(async () => { + return await validateRequest({}, userSchema); + }, + HttpError, + "username is required; password is required; email is required;"); - assertThrowsAsync(async () => { - return await validateRequest({username: "A", password: "123456", email: "test"}, userSchema); - }, - HttpError, - "username characters length must be between 3-30; " + - "password characters length must be between 10-20; " + - "email is not a valid email address;"); + assertThrowsAsync(async () => { + return await validateRequest({username: "A", password: "123456", email: "test"}, userSchema); + }, + HttpError, + "username characters length must be between 3-30; " + + "password characters length must be between 10-20; " + + "email is not a valid email address;"); }); \ No newline at end of file