Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
JWT_TOKEN_SECRET=HEGbulKGDblAFYskBLml

# Registration
MIN_PASSWORD_LENGTH=8
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
RUN deno cache --unstable --importmap import_map.json app.ts
CMD ["run", "--allow-read", "--allow-net", "--unstable", "--importmap importmap.json", "app.ts"]
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
9 changes: 4 additions & 5 deletions app.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
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<Context>();

app.use(oakCors());
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) });
2 changes: 1 addition & 1 deletion config/config.ts
Original file line number Diff line number Diff line change
@@ -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();
2 changes: 1 addition & 1 deletion db/db.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion helpers/encription.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down
18 changes: 10 additions & 8 deletions helpers/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -13,8 +13,10 @@ const {
JWT_REFRESH_TOKEN_EXP,
} = config;

const JWTAlgorithm = "HS256";

const header: Jose = {
alg: "HS256",
alg: JWTAlgorithm,
typ: "JWT",
};

Expand All @@ -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 });
Expand All @@ -35,20 +37,20 @@ 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 });
};

const getJwtPayload = async (token: string): Promise<any | null> => {
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 };
4 changes: 2 additions & 2 deletions helpers/roles.ts
Original file line number Diff line number Diff line change
@@ -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(",")
Expand Down
12 changes: 12 additions & 0 deletions importmap.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
4 changes: 2 additions & 2 deletions middlewares/error.middleware.ts
Original file line number Diff line number Diff line change
@@ -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<void>) => {
try {
Expand Down
9 changes: 5 additions & 4 deletions middlewares/jwt-auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<any | null> => {
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) {}
Expand Down
2 changes: 1 addition & 1 deletion middlewares/logger.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context } from "./../types.ts";
import type { Context } from "./../types.ts";
const loggerMiddleware = async (ctx: Context, next: () => Promise<void>) => {
await next();
const reqTime = ctx.response.headers.get("X-Response-Time");
Expand Down
2 changes: 1 addition & 1 deletion middlewares/request-id.middleware.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand Down
20 changes: 11 additions & 9 deletions middlewares/request-validator.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion middlewares/timing.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context } from "./../types.ts";
import type { Context } from "./../types.ts";
const timingMiddleware = async (ctx: Context, next: () => Promise<void>) => {
const start = Date.now();
await next();
Expand Down
4 changes: 2 additions & 2 deletions middlewares/user-guard.middleware.ts
Original file line number Diff line number Diff line change
@@ -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";


Expand Down
2 changes: 1 addition & 1 deletion repositories/user.repository.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
30 changes: 16 additions & 14 deletions routes/auth.routes.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
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
* */
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
Expand All @@ -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;
},
Expand All @@ -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 = [
Expand All @@ -55,27 +59,25 @@ 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 */
requestValidator({ bodyRules: refreshTokenSchema }),
/** 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;
},
];

Expand Down
4 changes: 2 additions & 2 deletions routes/routes.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Loading