├── .env ├── 404.ts ├── .deno_plugins └── deno_mongo_8b7daea8b292e42957b4fe724ea309d6.dylib ├── config └── databases.ts ├── util ├── hash.ts └── token.ts ├── WebSocket └── index.ts ├── routes ├── normal.ts └── protected.ts ├── .denon.json ├── middleware └── auth.ts ├── main.ts ├── controllers ├── AuthController.ts └── UserController.ts └── validation.ts /.env: -------------------------------------------------------------------------------- 1 | APP_HOST='http://localhost' -------------------------------------------------------------------------------- /404.ts: -------------------------------------------------------------------------------- 1 | export default ({ response }: { response: any }) => { 2 | response.status = 404; 3 | response.body = { 4 | error: "Not Found", 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /.deno_plugins/deno_mongo_8b7daea8b292e42957b4fe724ea309d6.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfumes/deno-js-with-mongodb-course/HEAD/.deno_plugins/deno_mongo_8b7daea8b292e42957b4fe724ea309d6.dylib -------------------------------------------------------------------------------- /config/databases.ts: -------------------------------------------------------------------------------- 1 | import { init, MongoClient } from "https://deno.land/x/mongo@v0.6.0/mod.ts"; 2 | await init(); 3 | const client = new MongoClient(); 4 | client.connectWithUri("mongodb://localhost:27017"); 5 | const db = client.database("deno"); 6 | export default db; 7 | // const users = db.collection("users"); 8 | // users.insertOne({ name: "Sarthak" }); 9 | -------------------------------------------------------------------------------- /util/hash.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts"; 2 | 3 | export default { 4 | async bcrypt(stringToHash: string): Promise { 5 | const hash = await bcrypt.hash(stringToHash); 6 | return hash; 7 | }, 8 | async verify(hash: string, text: string): Promise { 9 | const result = await bcrypt.compare(text, hash); 10 | return result; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /WebSocket/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | acceptWebSocket, 3 | acceptable, 4 | WebSocket, 5 | } from "https://deno.land/std/ws/mod.ts"; 6 | 7 | export default async (ctx: any, next: () => Promise) => { 8 | const req = ctx.request.serverRequest; 9 | if (acceptable(req)) { 10 | const sock = await ctx.upgrade(); 11 | for await (const ev of sock) { 12 | if (typeof ev === "string") { 13 | // text message 14 | console.log("ws:Text", ev); 15 | await sock.send(ev); 16 | } 17 | } 18 | } else { 19 | await next(); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /routes/normal.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "https://deno.land/x/oak/mod.ts"; 2 | import UserController from "../controllers/UserController.ts"; 3 | import AuthController from "../controllers/AuthController.ts"; 4 | import { oakCors } from "https://deno.land/x/cors/mod.ts"; 5 | const router = new Router(); 6 | 7 | router 8 | .get("/user", UserController.index) 9 | .get("/user/:id", UserController.show) 10 | .post("/user", UserController.store) 11 | .patch("/user/:id", UserController.update) 12 | .delete("/user/:id", UserController.destroy); 13 | 14 | router 15 | .options("/login", oakCors()) 16 | .post("/login", AuthController.login); 17 | 18 | export default router; 19 | -------------------------------------------------------------------------------- /.denon.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "main.ts" 4 | ], 5 | "quiet": false, 6 | "debug": true, 7 | "fullscreen": true, 8 | "extensions": [ 9 | ".js", 10 | ".ts", 11 | ".py", 12 | ".json" 13 | ], 14 | "interval": 2000, 15 | "watch": [ 16 | "/" 17 | ], 18 | "deno_args":[ 19 | "--allow-net", 20 | "--allow-read", 21 | "--unstable", 22 | "--allow-write", 23 | "--allow-plugin" 24 | ], 25 | "execute": { 26 | ".js": ["deno", "run"], 27 | ".ts": ["deno", "run"], 28 | ".py": ["python"] 29 | }, 30 | "fmt": true, 31 | "test": true 32 | } -------------------------------------------------------------------------------- /middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import token from "../util/token.ts"; 2 | export default { 3 | async authorized(ctx: any) { 4 | const authorization = ctx.request.headers.get("authorization"); 5 | if (!authorization) { 6 | ctx.response.status = 401; // unauthorised 7 | ctx.response.body = { error: "Unauthorized" }; 8 | return false; 9 | } 10 | const headerToken = authorization.replace("Bearer ", ""); 11 | // validate token 12 | const isTokenValid = await token.validate(headerToken); 13 | if (!isTokenValid) { 14 | ctx.response.status = 401; // unauthorised 15 | ctx.response.body = { error: "Unauthorized" }; 16 | return false; 17 | } 18 | 19 | return headerToken; 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { Application } from "https://deno.land/x/oak/mod.ts"; 2 | import { oakCors } from "https://deno.land/x/cors/mod.ts"; 3 | import { config } from "https://deno.land/x/dotenv/mod.ts"; 4 | import router from "./routes/normal.ts"; 5 | import protectedRouter from "./routes/protected.ts"; 6 | import notFound from "./404.ts"; 7 | import websocket from "./WebSocket/index.ts"; 8 | import authMiddleware from "./middleware/auth.ts"; 9 | const env = config(); 10 | 11 | const app = new Application(); 12 | const HOST = env.APP_HOST || "http://localhost"; 13 | const PORT = +env.APP_PORT || 4000; 14 | 15 | app.use(websocket); 16 | app.use(oakCors()); 17 | app.use(router.routes()); 18 | app.use(protectedRouter.routes()); 19 | app.use(notFound); 20 | 21 | console.log(`server is started at ${HOST}:${PORT}`); 22 | await app.listen({ port: PORT }); 23 | -------------------------------------------------------------------------------- /util/token.ts: -------------------------------------------------------------------------------- 1 | import { 2 | validateJwt, 3 | parseAndDecode, 4 | validateJwtObject, 5 | } from "https://deno.land/x/djwt/validate.ts"; 6 | import { 7 | makeJwt, 8 | setExpiration, 9 | Jose, 10 | Payload, 11 | } from "https://deno.land/x/djwt/create.ts"; 12 | 13 | const key = "your-secret"; 14 | 15 | const header: Jose = { 16 | alg: "HS256", 17 | typ: "JWT", 18 | }; 19 | 20 | export default { 21 | generate(userId: string): string { 22 | const payload: Payload = { 23 | uid: userId, 24 | exp: setExpiration(new Date().getTime() + 60000 * 60), 25 | }; 26 | return makeJwt({ header, payload, key }); 27 | }, 28 | async validate(token: string) { 29 | return !!await validateJwt(token, key, { isThrowing: false }); 30 | }, 31 | fetchUserId(token: string) { 32 | return validateJwtObject(parseAndDecode(token)).payload; 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /routes/protected.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "https://deno.land/x/oak/mod.ts"; 2 | import { ObjectId } from "https://deno.land/x/mongo@v0.6.0/mod.ts"; 3 | import { oakCors } from "https://deno.land/x/cors/mod.ts"; 4 | const protectedRouter = new Router(); 5 | import token from "../util/token.ts"; 6 | import db from "../config/databases.ts"; 7 | import AuthMiddleware from "../middleware/auth.ts"; 8 | const userCollection = db.collection("users"); 9 | 10 | protectedRouter.options("/me", oakCors()); 11 | protectedRouter.get("/me", async (ctx: any) => { 12 | const headerToken = await AuthMiddleware.authorized(ctx); 13 | if (!headerToken) return; 14 | const payload = token.fetchUserId(headerToken); 15 | if (payload) { 16 | const uid: string = String(payload.uid); 17 | const user = await userCollection.findOne({ _id: ObjectId(uid) }); 18 | ctx.response.body = user; 19 | } 20 | }); 21 | 22 | export default protectedRouter; 23 | -------------------------------------------------------------------------------- /controllers/AuthController.ts: -------------------------------------------------------------------------------- 1 | import db from "../config/databases.ts"; 2 | import { ObjectId } from "https://deno.land/x/mongo@v0.6.0/mod.ts"; 3 | const userCollection = db.collection("users"); 4 | import validation from "../validation.ts"; 5 | import hash from "../util/hash.ts"; 6 | import token from "../util/token.ts"; 7 | 8 | export default { 9 | async login(ctx: any) { 10 | // validation 11 | const value = await validation.validateLogin(ctx); 12 | if (!value) { 13 | return; 14 | } 15 | 16 | // fetch user 17 | const user = await userCollection.findOne({ email: value.email }); 18 | if (!user) { 19 | ctx.response.status = 422; 20 | ctx.response.body = { 21 | errors: { message: "Credentials doesn't match out record" }, 22 | }; 23 | return; 24 | } 25 | 26 | // verify password 27 | const passwordMatched = await hash.verify(user.password, value.password); 28 | if (!passwordMatched) { 29 | ctx.response.body = { error: "Password is incorrect" }; 30 | return; 31 | } 32 | 33 | ctx.response.body = token.generate(user._id.$oid); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /validation.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | async validate(ctx: any) { 3 | let errors = []; 4 | let status; 5 | const { value } = await ctx.request.body(); 6 | if (!value) { 7 | ctx.response.status = 400; // bad request 8 | ctx.response.body = { error: "Please provide the required data" }; 9 | return; 10 | } 11 | 12 | const fields = ["email", "password", "name"]; 13 | for (let index = 0; index < fields.length; index++) { 14 | if (!value[fields[index]]) { 15 | status = 422; // unprocessable entity 16 | errors.push({ [fields[index]]: `${fields[index]} field is required` }); 17 | } 18 | } 19 | 20 | if (status) { 21 | ctx.response.body = { errors }; 22 | return false; 23 | } 24 | 25 | return value; 26 | }, 27 | async validateUpdate(ctx: any) { 28 | const { value } = await ctx.request.body(); 29 | if (!value || Object.keys(value).length === 0) { 30 | ctx.response.status = 400; // bad request 31 | ctx.response.body = { 32 | errors: { message: "Please provide the required data" }, 33 | }; 34 | return false; 35 | } 36 | 37 | return value; 38 | }, 39 | async validateLogin(ctx: any) { 40 | let errors = []; 41 | let status; 42 | const { value } = await ctx.request.body(); 43 | if (!value) { 44 | ctx.response.status = 400; // bad request 45 | ctx.response.body = { 46 | errors: { message: "Please provide the required data" }, 47 | }; 48 | return; 49 | } 50 | 51 | const fields = ["email", "password"]; 52 | for (let index = 0; index < fields.length; index++) { 53 | if (!value[fields[index]]) { 54 | status = 422; // unprocessable entity 55 | errors.push({ [fields[index]]: `${fields[index]} field is required` }); 56 | } 57 | } 58 | 59 | if (status) { 60 | ctx.response.status = status; 61 | ctx.response.body = { errors }; 62 | return false; 63 | } 64 | return value; 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /controllers/UserController.ts: -------------------------------------------------------------------------------- 1 | import db from "../config/databases.ts"; 2 | import { ObjectId } from "https://deno.land/x/mongo@v0.6.0/mod.ts"; 3 | const user = db.collection("users"); 4 | import validation from "../validation.ts"; 5 | import hash from "../util/hash.ts"; 6 | 7 | export default { 8 | async index(ctx: any) { 9 | const data = await user.find(); 10 | ctx.response.body = data; 11 | }, 12 | async show(ctx: any) { 13 | try { 14 | const data = await user.findOne({ _id: ObjectId(ctx.params.id) }); 15 | ctx.response.body = data; 16 | } catch (e) { 17 | ctx.response.status = 404; 18 | ctx.response.body = { error: "User does't exists in our database." }; 19 | } 20 | }, 21 | async store(ctx: any) { 22 | const value = await validation.validate(ctx); 23 | if (value) { 24 | value.created_at = parseInt((new Date().getTime() / 1000).toString()); 25 | value.password = await hash.bcrypt(value.password); 26 | const insertId = await user.insertOne(value); 27 | ctx.response.status = 201; 28 | ctx.response.body = insertId; 29 | } 30 | }, 31 | async update(ctx: any) { 32 | const value = await validation.validateUpdate(ctx); 33 | if (value) { 34 | const data = { 35 | email: value.email, 36 | name: value.name, 37 | password: value.password, 38 | }; 39 | try { 40 | await user.updateOne({ _id: ObjectId(ctx.params.id) }, { $set: data }); 41 | ctx.response.status = 200; 42 | ctx.response.body = { message: "updated" }; 43 | } catch (e) { 44 | ctx.response.status = 404; 45 | ctx.response.body = { error: "User does't exists in our database." }; 46 | } 47 | } 48 | }, 49 | async destroy(ctx: any) { 50 | try { 51 | await user.deleteOne({ _id: ObjectId(ctx.params.id) }); 52 | ctx.response.status = 204; // no content 53 | } catch (e) { 54 | ctx.response.status = 404; 55 | ctx.response.body = { error: "User does't exists in our database." }; 56 | } 57 | }, 58 | }; 59 | --------------------------------------------------------------------------------