diff --git a/.env.example b/.env.example index 27602d4..ee80da2 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,18 @@ +# App +PORT=8000 + DB_NAME=deno_api_db DB_HOST=db DB_PASS=example DB_USER=root ENV=dev -# Access token validity in ms -JWT_ACCESS_TOKEN_EXP=600000 -# Refresh token validity in ms -JWT_REFRESH_TOKEN_EXP=3600000 +# Access token validity in seconds +JWT_ACCESS_TOKEN_EXP=600 +# Refresh token validity in seconds +JWT_REFRESH_TOKEN_EXP=3600 # Secret secuirity string -JWT_TOKEN_SECRET=HEGbulKGDblAFYskBLml \ No newline at end of file +JWT_TOKEN_SECRET=HEGbulKGDblAFYskBLml + +# Registration +MIN_PASSWORD_LENGTH=8 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c6e3930..05f9322 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,5 +7,5 @@ WORKDIR /usr/src/app COPY . . USER deno -RUN deno cache app.ts -CMD ["run", "--allow-read", "--allow-net", "--unstable", "app.ts"] \ No newline at end of file +RUN deno cache --unstable --importmap import_map.json app.ts +CMD ["run", "--allow-read", "--allow-net", "--unstable", "--importmap importmap.json", "app.ts"] \ No newline at end of file diff --git a/README.md b/README.md index f6b7b4f..5d965b6 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ This is a starter project to create Deno RESTful API using oak. [oak](https://github.com/oakserver/oak) is a middleware framework and router middleware for Deno, inspired by popular Node.js framework [Koa](https://koajs.com/) and [@koa/router](https://github.com/koajs/router/). +Note: Only Deno 1.4.2 is supported at the moment (work is ongoing to support newer versions). + This project covers - Swagger Open API doc - Docker container environment @@ -50,7 +52,7 @@ We can run the project **with/ without Docker**. - For non-docker run API server with Deno run time ``` - $ deno run --allow-read --allow-net app.ts + $ deno run --allow-read --allow-net --unstable --importmap importmap.json app.ts ``` - **API** - Browse `API` at [http://localhost:8000](http://localhost:8000) @@ -77,12 +79,12 @@ deno run --allow-net --allow-read --allow-write https://deno.land/x/nessie@v1.0. | Package | Purpose | | ---------|---------| -|[oak@v5.0.0](https://deno.land/x/oak@v5.0.0)| Deno middleware framework| +|[oak@v6.2.0](https://deno.land/x/oak@v6.2.0)| Deno middleware framework| |[dotenv@v0.4.2](https://deno.land/x/dotenv@v0.4.2)| Read env variables| |[mysql@2.2.0](https://deno.land/x/mysql@2.2.0)|MySQL driver for Deno| |[nessie@v1.0.0-rc3](https://deno.land/x/nessie@v1.0.0-rc3)| DB migration tool for Deno| |[validasaur@v0.7.0](https://deno.land/x/validasaur@v0.7.0)| validation library| -|[djwt@v0.9.0](https://deno.land/x/djwt@v0.9.0)| JWT token encoding| +|[djwt@v1.4](https://deno.land/x/djwt@v1.4)| JWT token encoding| |[bcrypt@v0.2.1](https://deno.land/x/bcrypt@v0.2.1)| bcrypt encription lib| ### Project Layout diff --git a/app.ts b/app.ts index 05b7337..d91b0b1 100644 --- a/app.ts +++ b/app.ts @@ -1,11 +1,10 @@ -import { Application } from "https://deno.land/x/oak@v5.0.0/mod.ts"; +import { Application } from "https://deno.land/x/oak@v6.2.0/mod.ts"; import * as middlewares from "./middlewares/middlewares.ts"; import { oakCors } from "https://deno.land/x/cors/mod.ts"; import { router } from "./routes/routes.ts"; -import { Context } from "./types.ts"; +import type { Context } from "./types.ts"; import { config } from "./config/config.ts"; -const port = 8000; const app = new Application(); app.use(oakCors()); @@ -13,11 +12,11 @@ app.use(middlewares.loggerMiddleware); app.use(middlewares.errorMiddleware); app.use(middlewares.timingMiddleware); -const { JWT_TOKEN_SECRET } = config; +const { PORT, JWT_TOKEN_SECRET } = config; app.use(middlewares.JWTAuthMiddleware(JWT_TOKEN_SECRET)); app.use(middlewares.requestIdMiddleware); app.use(router.routes()); app.use(router.allowedMethods()); -await app.listen({ port }); +await app.listen({ port: Number.parseInt(PORT) }); diff --git a/config/config.ts b/config/config.ts index d71ac8e..0a6e94b 100644 --- a/config/config.ts +++ b/config/config.ts @@ -1,2 +1,2 @@ -import { config as loadConfig } from "https://deno.land/x/dotenv@v0.4.2/mod.ts"; +import { config as loadConfig } from "https://deno.land/x/dotenv@v0.4.3/mod.ts"; export const config = loadConfig(); diff --git a/db/db.ts b/db/db.ts index aa4ff55..c03ff5d 100644 --- a/db/db.ts +++ b/db/db.ts @@ -1,4 +1,4 @@ -import { Client } from "https://deno.land/x/mysql@2.2.0/mod.ts"; +import { Client } from "https://deno.land/x/mysql@v2.7.0/mod.ts"; import { config } from "./../config/config.ts"; const port = config.DB_PORT ? parseInt(config.DB_PORT || "") : undefined; diff --git a/helpers/encription.ts b/helpers/encription.ts index 633bd1c..6215cd2 100644 --- a/helpers/encription.ts +++ b/helpers/encription.ts @@ -1,4 +1,4 @@ -import * as bcrypt from "https://deno.land/x/bcrypt@v0.2.1/mod.ts"; +import * as bcrypt from "https://deno.land/x/bcrypt@v0.2.3/mod.ts"; /** * encript given string */ diff --git a/helpers/jwt.ts b/helpers/jwt.ts index 3c6d6e4..a1c5b87 100644 --- a/helpers/jwt.ts +++ b/helpers/jwt.ts @@ -3,8 +3,8 @@ import { Payload, makeJwt, setExpiration, -} from "https://deno.land/x/djwt@v0.9.0/create.ts"; -import { validateJwt } from "https://deno.land/x/djwt@v0.9.0/validate.ts"; +} from "https://deno.land/x/djwt@v1.4/create.ts"; +import { validateJwt } from "https://deno.land/x/djwt@v1.4/validate.ts"; import { config } from "./../config/config.ts"; const { @@ -13,8 +13,10 @@ const { JWT_REFRESH_TOKEN_EXP, } = config; +const JWTAlgorithm = "HS256"; + const header: Jose = { - alg: "HS256", + alg: JWTAlgorithm, typ: "JWT", }; @@ -25,7 +27,7 @@ const getAuthToken = (user: any) => { name: user.name, email: user.email, roles: user.roles, - exp: setExpiration(new Date().getTime() + parseInt(JWT_ACCESS_TOKEN_EXP)), + exp: setExpiration((Date.now() / 1000) + parseInt(JWT_ACCESS_TOKEN_EXP)), }; return makeJwt({ header, payload, key: JWT_TOKEN_SECRET }); @@ -35,7 +37,7 @@ const getRefreshToken = (user: any) => { const payload: Payload = { iss: "deno-api", id: user.id, - exp: setExpiration(new Date().getTime() + parseInt(JWT_REFRESH_TOKEN_EXP)), + exp: setExpiration((Date.now() / 1000) + parseInt(JWT_REFRESH_TOKEN_EXP)), }; return makeJwt({ header, payload, key: JWT_TOKEN_SECRET }); @@ -43,12 +45,12 @@ const getRefreshToken = (user: any) => { const getJwtPayload = async (token: string): Promise => { try { - const jwtObject = await validateJwt(token, JWT_TOKEN_SECRET); - if (jwtObject && jwtObject.payload) { + const jwtObject = await validateJwt({jwt: token, key: JWT_TOKEN_SECRET, algorithm: [JWTAlgorithm], critHandlers: {}}); + if (jwtObject.isValid) { return jwtObject.payload; } } catch (err) {} return null; }; -export { getAuthToken, getRefreshToken, getJwtPayload }; +export { getAuthToken, getRefreshToken, getJwtPayload, JWTAlgorithm }; diff --git a/helpers/roles.ts b/helpers/roles.ts index f61891d..5177a89 100644 --- a/helpers/roles.ts +++ b/helpers/roles.ts @@ -1,5 +1,5 @@ -import { AuthUser } from "../types.ts"; -import { UserRole } from "../types/user/user-role.ts"; +import type { AuthUser } from "../types.ts"; +import type { UserRole } from "../types/user/user-role.ts"; const hasUserRole = (user: AuthUser, roles: UserRole | UserRole[]) => { const userRoles = user.roles.split(",") diff --git a/importmap.json b/importmap.json new file mode 100644 index 0000000..dc466c1 --- /dev/null +++ b/importmap.json @@ -0,0 +1,12 @@ +{ + "imports": { + "https://deno.land/std@v0.61.0/encoding/hex.ts": "https://deno.land/std@0.61.0/encoding/hex.ts", + "https://deno.land/std@v0.61.0/encoding/base64url.ts": "https://deno.land/std@0.61.0/encoding/base64url.ts", + "https://deno.land/std@v0.61.0/hash/sha256.ts": "https://deno.land/std@0.61.0/hash/sha256.ts", + "https://deno.land/std@v0.61.0/hash/sha512.ts": "https://deno.land/std@0.61.0/hash/sha512.ts", + "https://deno.land/std@0.53.0/path/posix.ts": "https://deno.land/std@0.61.0/path/posix.ts", + "https://deno.land/std@0.53.0/path/win32.ts": "https://deno.land/std@0.61.0/path/win32.ts", + "https://deno.land/std@0.56.0/path/posix.ts": "https://deno.land/std@0.61.0/path/posix.ts", + "https://deno.land/std@0.56.0/path/win32.ts": "https://deno.land/std@0.61.0/path/win32.ts" + } +} \ No newline at end of file diff --git a/middlewares/error.middleware.ts b/middlewares/error.middleware.ts index d8b46dd..a8ef15f 100644 --- a/middlewares/error.middleware.ts +++ b/middlewares/error.middleware.ts @@ -1,9 +1,9 @@ import { isHttpError, Status, -} from "https://deno.land/x/oak@v5.0.0/mod.ts"; +} from "https://deno.land/x/oak@v6.2.0/mod.ts"; import { config } from "./../config/config.ts"; -import { Context } from "./../types.ts"; +import type { Context } from "./../types.ts"; const errorMiddleware = async (ctx: Context, next: () => Promise) => { try { diff --git a/middlewares/jwt-auth.middleware.ts b/middlewares/jwt-auth.middleware.ts index 85ba541..578474e 100644 --- a/middlewares/jwt-auth.middleware.ts +++ b/middlewares/jwt-auth.middleware.ts @@ -1,5 +1,6 @@ -import { Context, AuthUser } from "./../types.ts"; -import { validateJwt } from "https://deno.land/x/djwt@v0.9.0/validate.ts"; +import type { Context, AuthUser } from "./../types.ts"; +import { validateJwt } from "https://deno.land/x/djwt@v1.4/validate.ts"; +import { JWTAlgorithm } from "./../helpers/jwt.ts"; /** * Decode token and returns payload @@ -8,8 +9,8 @@ import { validateJwt } from "https://deno.land/x/djwt@v0.9.0/validate.ts"; */ const getJwtPayload = async (token: string, secret: string): Promise => { try { - const jwtObject = await validateJwt(token, secret); - if (jwtObject && jwtObject.payload) { + const jwtObject = await validateJwt({jwt: token, key: secret, algorithm: [JWTAlgorithm], critHandlers: {}}); + if (jwtObject.isValid) { return jwtObject.payload; } } catch (err) {} diff --git a/middlewares/logger.middleware.ts b/middlewares/logger.middleware.ts index f7e9f8b..97ee985 100644 --- a/middlewares/logger.middleware.ts +++ b/middlewares/logger.middleware.ts @@ -1,4 +1,4 @@ -import { Context } from "./../types.ts"; +import type { Context } from "./../types.ts"; const loggerMiddleware = async (ctx: Context, next: () => Promise) => { await next(); const reqTime = ctx.response.headers.get("X-Response-Time"); diff --git a/middlewares/request-id.middleware.ts b/middlewares/request-id.middleware.ts index d673914..83eee46 100644 --- a/middlewares/request-id.middleware.ts +++ b/middlewares/request-id.middleware.ts @@ -1,4 +1,4 @@ -import { Context } from "./../types.ts"; +import type { Context } from "./../types.ts"; import { v4 as uuid } from "https://deno.land/std@0.62.0/uuid/mod.ts"; /** diff --git a/middlewares/request-validator.middleware.ts b/middlewares/request-validator.middleware.ts index 144e8a7..a3c1434 100644 --- a/middlewares/request-validator.middleware.ts +++ b/middlewares/request-validator.middleware.ts @@ -2,9 +2,9 @@ import { validate, ValidationErrors, ValidationRules, -} from "https://deno.land/x/validasaur@v0.7.0/src/mod.ts"; -import { httpErrors } from "https://deno.land/x/oak@v5.0.0/mod.ts"; -import { Context } from "./../types.ts"; +} from "https://deno.land/x/validasaur@v0.15.0/mod.ts"; +import { httpErrors } from "https://deno.land/x/oak@v6.2.0/mod.ts"; +import type { Context } from "./../types.ts"; /** * get single error message from errors @@ -30,12 +30,14 @@ const requestValidator = ({ bodyRules }: { bodyRules: ValidationRules }) => { const request = ctx.request; const body = (await request.body()).value; - /** check rules */ - const [isValid, errors] = await validate(body, bodyRules); - if (!isValid) { - /** if error found, throw bad request error */ - const message = getErrorMessage(errors); - throw new httpErrors.BadRequest(message); + if (body) { + /** check rules */ + const [isValid, errors] = await validate(body, bodyRules); + if (!isValid) { + /** if error found, throw bad request error */ + const message = getErrorMessage(errors); + throw new httpErrors.BadRequest(message); + } } await next(); diff --git a/middlewares/timing.middleware.ts b/middlewares/timing.middleware.ts index e2d3df1..5369fea 100644 --- a/middlewares/timing.middleware.ts +++ b/middlewares/timing.middleware.ts @@ -1,4 +1,4 @@ -import { Context } from "./../types.ts"; +import type { Context } from "./../types.ts"; const timingMiddleware = async (ctx: Context, next: () => Promise) => { const start = Date.now(); await next(); diff --git a/middlewares/user-guard.middleware.ts b/middlewares/user-guard.middleware.ts index 1ec0b44..bf3a2bd 100644 --- a/middlewares/user-guard.middleware.ts +++ b/middlewares/user-guard.middleware.ts @@ -1,5 +1,5 @@ -import { httpErrors } from "https://deno.land/x/oak@v5.0.0/mod.ts"; -import { Context, UserRole } from "./../types.ts"; +import { httpErrors } from "https://deno.land/x/oak@v6.2.0/mod.ts"; +import type { Context, UserRole } from "./../types.ts"; import { hasUserRole } from "../helpers/roles.ts"; diff --git a/repositories/user.repository.ts b/repositories/user.repository.ts index 897d5da..2a99d6c 100644 --- a/repositories/user.repository.ts +++ b/repositories/user.repository.ts @@ -1,5 +1,5 @@ import { db } from "./../db/db.ts"; -import { UserInfo } from "../types.ts"; +import type { UserInfo } from "../types.ts"; /** * Get all users list diff --git a/routes/auth.routes.ts b/routes/auth.routes.ts index fe157b7..a9ec28b 100644 --- a/routes/auth.routes.ts +++ b/routes/auth.routes.ts @@ -1,18 +1,22 @@ -import { +import type { Context, CreateUser, - RefreshToken, LoginCredential, + RefreshToken, } from "./../types.ts"; import { required, isEmail, - lengthBetween, -} from "https://deno.land/x/validasaur@v0.7.0/src/rules.ts"; + minLength, +} from "https://deno.land/x/validasaur@v0.15.0/mod.ts"; import * as authService from "./../services/auth.service.ts"; import { requestValidator } from "./../middlewares/request-validator.middleware.ts"; +import { config } from "./../config/config.ts"; + +const { MIN_PASSWORD_LENGTH } = config; + /** * request body schema * for user create/update @@ -20,7 +24,7 @@ import { requestValidator } from "./../middlewares/request-validator.middleware. const registrationSchema = { name: [required], email: [required, isEmail], - password: [required, lengthBetween(6, 12)], + password: [required, minLength(Number.parseInt(MIN_PASSWORD_LENGTH))], }; //todo: add validation alphanumeric, spechal char @@ -34,7 +38,7 @@ const register = [ /** router handler */ async (ctx: Context) => { const request = ctx.request; - const userData = (await request.body()).value as CreateUser; + const userData = await request.body().value as CreateUser; const user = await authService.registerUser(userData); ctx.response.body = user; }, @@ -46,7 +50,7 @@ const register = [ * */ const loginSchema = { email: [required, isEmail], - password: [required, lengthBetween(6, 12)], + password: [required, minLength(Number.parseInt(MIN_PASSWORD_LENGTH))], }; const login = [ @@ -55,14 +59,14 @@ const login = [ /** router handler */ async (ctx: Context) => { const request = ctx.request; - const credential = (await request.body()).value as LoginCredential; + const credential = await request.body().value as LoginCredential; const token = await authService.loginUser(credential); ctx.response.body = token; }, ]; const refreshTokenSchema = { - refresh_token: [required], + value: [required], }; const refreshToken = [ /** request validation middleware */ @@ -70,12 +74,10 @@ const refreshToken = [ /** router handler */ async (ctx: Context) => { const request = ctx.request; - const data = (await request.body()).value as RefreshToken; + const token = await request.body().value as RefreshToken; - const token = await authService.refreshToken( - data["refresh_token"], - ); - ctx.response.body = token; + const auth = await authService.jwtAuth(token); + ctx.response.body = auth; }, ]; diff --git a/routes/routes.ts b/routes/routes.ts index 0d08d07..5db542e 100644 --- a/routes/routes.ts +++ b/routes/routes.ts @@ -1,5 +1,5 @@ -import { Router } from "https://deno.land/x/oak@v5.0.0/mod.ts"; -import { Context } from "./../types.ts"; +import { Router } from "https://deno.land/x/oak@v6.2.0/mod.ts"; +import type { Context } from "./../types.ts"; import * as authRoutes from "./auth.routes.ts"; import * as userRoutes from "./user.routes.ts"; diff --git a/routes/user.routes.ts b/routes/user.routes.ts index 152ca60..7d7c980 100644 --- a/routes/user.routes.ts +++ b/routes/user.routes.ts @@ -2,14 +2,15 @@ import { helpers, Status, httpErrors, -} from "https://deno.land/x/oak@v5.0.0/mod.ts"; +} from "https://deno.land/x/oak@v6.2.0/mod.ts"; import { required, isEmail, -} from "https://deno.land/x/validasaur@v0.7.0/src/rules.ts"; +} from "https://deno.land/x/validasaur@v0.15.0/mod.ts"; import * as userService from "./../services/user.service.ts"; import { requestValidator, userGuard } from "./../middlewares/middlewares.ts"; -import { Context, UserRole } from "./../types.ts"; +import type { Context} from "./../types.ts"; +import { UserRole} from "./../types.ts"; import { hasUserRole } from "../helpers/roles.ts"; /** request body schema for user create/update */ diff --git a/services/auth.service.ts b/services/auth.service.ts index b6ec166..c260127 100644 --- a/services/auth.service.ts +++ b/services/auth.service.ts @@ -1,11 +1,12 @@ import * as userRepo from "./../repositories/user.repository.ts"; -import { httpErrors } from "https://deno.land/x/oak@v5.0.0/mod.ts"; +import { httpErrors } from "https://deno.land/x/oak@v6.2.0/mod.ts"; import * as encription from "../helpers/encription.ts"; import * as jwt from "../helpers/jwt.ts"; import { CreateUser, UserRole, UserInfo, + RefreshToken, LoginCredential, } from "../types.ts"; @@ -60,10 +61,10 @@ export const loginUser = async (credential: LoginCredential) => { throw new httpErrors.Unauthorized("Wrong credential"); }; -export const refreshToken = async (token: string) => { +export const jwtAuth = async (token: RefreshToken) => { try { // todo: check token intention - const payload = await jwt.getJwtPayload(token); + const payload = await jwt.getJwtPayload(token.value); if (payload) { /** get user from token */ const id = payload.id as number; diff --git a/services/user.service.ts b/services/user.service.ts index 556236a..2a7b03d 100644 --- a/services/user.service.ts +++ b/services/user.service.ts @@ -1,5 +1,5 @@ import * as userRepo from "./../repositories/user.repository.ts"; -import { httpErrors } from "https://deno.land/x/oak@v5.0.0/mod.ts"; +import { httpErrors } from "https://deno.land/x/oak@v6.2.0/mod.ts"; import { encript } from "../helpers/encription.ts"; /** diff --git a/types.ts b/types.ts index 9a3f930..ba08b7b 100644 --- a/types.ts +++ b/types.ts @@ -3,7 +3,6 @@ export * from "./types/auth/auth-user.ts"; export * from "./types/auth/login-credential.ts"; export * from "./types/auth/refresh-token.ts"; - export * from "./types/user/user-role.ts"; export * from "./types/user/create-user.ts"; export * from "./types/user/user-info.ts"; diff --git a/types/auth/refresh-token.ts b/types/auth/refresh-token.ts index fb808b4..3e0e3ca 100644 --- a/types/auth/refresh-token.ts +++ b/types/auth/refresh-token.ts @@ -1,5 +1,5 @@ /** Token refresh request body */ export type RefreshToken = { /** refresh token */ - refresh_token: string; + value: string; }; diff --git a/types/core/context.ts b/types/core/context.ts index 19cd774..37dec47 100644 --- a/types/core/context.ts +++ b/types/core/context.ts @@ -1,5 +1,5 @@ -import { Context as OakContext } from "https://deno.land/x/oak@v5.0.0/mod.ts"; -import { AuthUser } from "./../auth/auth-user.ts"; +import { Context as OakContext } from "https://deno.land/x/oak@v6.2.0/mod.ts"; +import type { AuthUser } from "./../auth/auth-user.ts"; /** * Custom appilication context diff --git a/types/user/user-info.ts b/types/user/user-info.ts index 5e2a3d5..1f6f619 100644 --- a/types/user/user-info.ts +++ b/types/user/user-info.ts @@ -1,5 +1,5 @@ -import { CreateUser } from "./create-user.ts"; -import { UserRole } from "./user-role.ts"; +import type { CreateUser } from "./create-user.ts"; +import type { UserRole } from "./user-role.ts"; /** Request body to create user */ export type UserInfo = CreateUser & {