1
0
Эх сурвалжийг харах

add passwords, login, jwt essentials

Aneurin Barker Snook 1 жил өмнө
parent
commit
daeef304fd
10 өөрчлөгдсөн 313 нэмэгдсэн , 31 устгасан
  1. 101 6
      package-lock.json
  2. 2 0
      package.json
  3. 69 20
      src/account/api.ts
  4. 40 5
      src/account/model.ts
  5. 6 0
      src/account/types.ts
  6. 54 0
      src/auth.ts
  7. 11 0
      src/http.ts
  8. 17 0
      src/index.ts
  9. 5 0
      src/main.ts
  10. 8 0
      src/types.ts

+ 101 - 6
package-lock.json

@@ -11,10 +11,12 @@
       "dependencies": {
         "dotenv": "^16.3.1",
         "express": "^4.18.2",
+        "jsonwebtoken": "^9.0.2",
         "mongodb": "^6.3.0"
       },
       "devDependencies": {
         "@types/express": "^4.17.21",
+        "@types/jsonwebtoken": "^9.0.5",
         "@types/node": "^20.10.3",
         "@typescript-eslint/eslint-plugin": "^6.13.2",
         "@typescript-eslint/parser": "^6.13.2",
@@ -281,6 +283,15 @@
       "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
       "dev": true
     },
+    "node_modules/@types/jsonwebtoken": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
+      "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/mime": {
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -751,6 +762,11 @@
         "node": ">=16.20.1"
       }
     },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+    },
     "node_modules/bytes": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -1009,6 +1025,14 @@
         "url": "https://github.com/motdotla/dotenv?sponsor=1"
       }
     },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
     "node_modules/ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -1759,6 +1783,46 @@
       "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
       "dev": true
     },
+    "node_modules/jsonwebtoken": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+      "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+      "dependencies": {
+        "jws": "^3.2.2",
+        "lodash.includes": "^4.3.0",
+        "lodash.isboolean": "^3.0.3",
+        "lodash.isinteger": "^4.0.4",
+        "lodash.isnumber": "^3.0.3",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.isstring": "^4.0.1",
+        "lodash.once": "^4.0.0",
+        "ms": "^2.1.1",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+      "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+      "dependencies": {
+        "buffer-equal-constant-time": "1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+      "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+      "dependencies": {
+        "jwa": "^1.4.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
     "node_modules/keyv": {
       "version": "4.5.4",
       "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -1796,17 +1860,51 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/lodash.includes": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+      "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+    },
+    "node_modules/lodash.isboolean": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+      "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+    },
+    "node_modules/lodash.isinteger": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+      "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+    },
+    "node_modules/lodash.isnumber": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+      "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+    },
+    "node_modules/lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+    },
+    "node_modules/lodash.isstring": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+      "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
+    },
     "node_modules/lodash.merge": {
       "version": "4.6.2",
       "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
       "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
       "dev": true
     },
+    "node_modules/lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
+    },
     "node_modules/lru-cache": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
       "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
       "dependencies": {
         "yallist": "^4.0.0"
       },
@@ -1967,8 +2065,7 @@
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "node_modules/natural-compare": {
       "version": "1.4.0",
@@ -2393,7 +2490,6 @@
       "version": "7.5.4",
       "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
       "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
-      "dev": true,
       "dependencies": {
         "lru-cache": "^6.0.0"
       },
@@ -2833,8 +2929,7 @@
     "node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
     },
     "node_modules/yn": {
       "version": "3.1.1",

+ 2 - 0
package.json

@@ -15,6 +15,7 @@
   "license": "SEE LICENSE IN LICENSE.md",
   "devDependencies": {
     "@types/express": "^4.17.21",
+    "@types/jsonwebtoken": "^9.0.5",
     "@types/node": "^20.10.3",
     "@typescript-eslint/eslint-plugin": "^6.13.2",
     "@typescript-eslint/parser": "^6.13.2",
@@ -26,6 +27,7 @@
   "dependencies": {
     "dotenv": "^16.3.1",
     "express": "^4.18.2",
+    "jsonwebtoken": "^9.0.2",
     "mongodb": "^6.3.0"
   }
 }

+ 69 - 20
src/account/api.ts

@@ -1,14 +1,15 @@
 import * as v from '../validate'
-import type { Account } from './types'
+import type { AuthRequestHandler } from '../auth'
 import type { Context } from '../types'
 import { ObjectId } from 'mongodb'
 import type { RequestHandler } from 'express'
 import type { WithId } from 'mongodb'
-import { sendBadRequest, sendNotFound } from '../http'
+import type { Account, AccountCreate, AccountUpdate } from './types'
+import { sendBadRequest, sendForbidden, sendNotFound, sendUnauthorized } from '../http'
 
 export function createAccount({ model }: Context): RequestHandler {
   interface RequestData {
-    account: Account
+    account: AccountCreate
   }
 
   interface ResponseData {
@@ -18,6 +19,7 @@ export function createAccount({ model }: Context): RequestHandler {
   const readRequestData = v.validate<RequestData>({
     account: {
       email: v.email,
+      password: v.seq(v.minLength(8)),
     },
   })
 
@@ -26,6 +28,7 @@ export function createAccount({ model }: Context): RequestHandler {
       const input = readRequestData(req.body)
 
       const account = await model.account.create(input.account)
+      if (!account) return sendNotFound(res, next, { reason: 'unexpectedly failed to get new account' })
 
       const output: ResponseData = { account }
       res.send(output)
@@ -41,15 +44,17 @@ export function createAccount({ model }: Context): RequestHandler {
   }
 }
 
-export function deleteAccount({ model }: Context): RequestHandler {
+export function deleteAccount({ model }: Context): AuthRequestHandler {
   interface ResponseData {
     account: WithId<Account>
   }
 
   return async function (req, res, next) {
-    /** @todo get ID from authentication */
-    const id = req.params.id
+    if (!req.verified) return sendUnauthorized(res, next)
+
+    const id = req.params.id || req.accountId
     if (!id) return sendBadRequest(res, next, { reason: 'no ID' })
+    if (!req.accountId?.equals(id)) return sendForbidden(res, next)
 
     try {
       /** @todo delete related data */
@@ -65,15 +70,17 @@ export function deleteAccount({ model }: Context): RequestHandler {
   }
 }
 
-export function getAccount({ model }: Context): RequestHandler {
+export function getAccount({ model }: Context): AuthRequestHandler {
   interface ResponseData {
     account: WithId<Account>
   }
 
   return async function (req, res, next) {
-    /** @todo get ID from authentication */
-    const id = req.params.id
+    if (!req.verified) return sendUnauthorized(res, next)
+
+    const id = req.params.id || req.accountId
     if (!id) return sendBadRequest(res, next, { reason: 'no ID' })
+    if (!req.accountId?.equals(id)) return sendForbidden(res, next)
 
     try {
       const account = await model.account.collection.findOne({ _id: new ObjectId(id) })
@@ -88,9 +95,52 @@ export function getAccount({ model }: Context): RequestHandler {
   }
 }
 
-export function updateAccount({ model }: Context): RequestHandler {
+export function loginAccount({ auth, model }: Context): RequestHandler {
   interface RequestData {
-    account: Partial<Account>
+    account: Pick<Account, 'email' | 'password'>
+  }
+
+  interface ResponseData {
+    token: string
+    account: WithId<Account>
+  }
+
+  const readRequestData = v.validate<RequestData>({
+    account: {
+      email: v.email,
+      password: v.seq(v.minLength(8)),
+    },
+  })
+
+  return async function (req, res, next) {
+    try {
+      const input = readRequestData(req.body)
+
+      const account = await model.account.collection.findOne({ email: input.account.email })
+      if (!account) return sendNotFound(res, next)
+
+      const password = model.account.hashPassword(input.account.password, account.passwordSalt)
+      if (password !== account.password) return sendBadRequest(res, next, { reason: 'invalid password' })
+
+      const token = await auth.sign(account._id)
+
+      const output: ResponseData = { token, account }
+      res.send(output)
+      next()
+    } catch (err) {
+      const name = (err as Error).name
+      if (name === 'ValidateError') {
+        const ve = err as v.ValidateError
+        return sendBadRequest(res, next, { param: ve.param, reason: ve.message })
+      }
+      return next(err)
+    }
+  }
+}
+
+export function updateAccount({ model }: Context): AuthRequestHandler {
+  interface RequestData {
+    account: AccountUpdate
   }
 
   interface ResponseData {
@@ -100,25 +150,24 @@ export function updateAccount({ model }: Context): RequestHandler {
   const readRequestData = v.validate<RequestData>({
     account: {
       email: v.seq(v.optional, v.email),
+      password: v.seq(v.optional, v.minLength(8)),
     },
   })
 
   return async function (req, res, next) {
-    /** @todo get ID from authentication */
-    const id = req.params.id
+    if (!req.verified) return sendUnauthorized(res, next)
+
+    const id = req.params.id || req.accountId
     if (!id) return sendBadRequest(res, next, { reason: 'no ID' })
+    if (!req.accountId?.equals(id)) return sendForbidden(res, next)
 
     try {
       const input = readRequestData(req.body)
-      if (!input.account.email) {
-        return sendBadRequest(res, next, { reason: 'no data' })
+      if (!input.account.email && !input.account.password) {
+        return sendBadRequest(res, next, { reason: 'no changes' })
       }
 
-      const account = await model.account.collection.findOneAndUpdate(
-        { _id: new ObjectId(id) },
-        { $set: input.account },
-        { returnDocument: 'after' },
-      )
+      const account = await model.account.update(id, input.account)
       if (!account) return sendNotFound(res, next)
 
       const output: ResponseData = { account }

+ 40 - 5
src/account/model.ts

@@ -1,15 +1,32 @@
-import type { Account } from './types'
 import type { Context } from '../types'
-import type { WithId } from 'mongodb'
+import { ObjectId } from 'mongodb'
+import crypto from 'crypto'
+import type { Account, AccountCreate, AccountUpdate } from './types'
 
 export type AccountModel = Awaited<ReturnType<typeof createAccountModel>>
 
 async function createAccountModel(ctx: Context) {
   const collection = ctx.db.collection<Account>('account')
 
-  async function create(input: Account): Promise<WithId<Account>> {
-    const result = await collection.insertOne(input)
-    return { ...input, _id: result.insertedId }
+  async function create(input: AccountCreate) {
+    const passwordSalt = generateSalt()
+    const password = hashPassword(input.password, passwordSalt)
+
+    const result = await collection.insertOne({
+      email: input.email,
+      password,
+      passwordSalt,
+    })
+
+    return await collection.findOne({ _id: result.insertedId })
+  }
+
+  function generateSalt() {
+    return crypto.randomBytes(32).toString('hex')
+  }
+
+  function hashPassword(password: string, salt: string) {
+    return crypto.createHmac('sha256', salt).update(password).digest('hex')
   }
 
   async function init() {
@@ -19,12 +36,30 @@ async function createAccountModel(ctx: Context) {
     }
   }
 
+  async function update(id: ObjectId | string, input: AccountUpdate) {
+    const changes = <Partial<Account>>{}
+    if (input.email) changes.email = input.email
+    if (input.password) {
+      changes.passwordSalt = crypto.randomBytes(32).toString('hex')
+      changes.password = crypto.createHmac('sha256', changes.passwordSalt).update(input.password).digest('hex')
+    }
+
+    return collection.findOneAndUpdate(
+      { _id: new ObjectId(id) },
+      { $set: changes },
+      { returnDocument: 'after' },
+    )
+  }
+
   await init()
 
   return {
     collection,
     create,
+    generateSalt,
+    hashPassword,
     init,
+    update,
   }
 }
 

+ 6 - 0
src/account/types.ts

@@ -1,3 +1,9 @@
 export interface Account {
   email: string
+  password: string
+  passwordSalt: string
 }
+
+export type AccountCreate = Pick<Account, 'email' | 'password'>
+
+export type AccountUpdate = Partial<Pick<Account, 'email' | 'password'>>

+ 54 - 0
src/auth.ts

@@ -0,0 +1,54 @@
+import type { Context } from './types'
+import { ObjectId } from 'mongodb'
+import jwt from 'jsonwebtoken'
+import type { NextFunction, Request, Response } from 'express'
+
+export type Auth = ReturnType<typeof createAuth>
+
+export type AuthRequest = Request & {
+  accountId?: ObjectId
+  verified?: boolean
+}
+
+export type AuthRequestHandler = (req: AuthRequest, res: Response, next: NextFunction) => void | Promise<void>
+
+function createAuth(ctx: Context) {
+  const { expiresIn, secret } = ctx.config.auth.jwt
+
+  function sign(accountId: ObjectId | string): Promise<string> {
+    return new Promise((res, rej) => {
+      jwt.sign({ _id: accountId.toString() }, secret, { expiresIn }, (err, enc) => {
+        if (err) return rej(err)
+        if (enc) return res(enc)
+        rej(new Error('no result'))
+      })
+    })
+  }
+
+  function verify(token: string): Promise<ObjectId> {
+    return new Promise((res, rej) => {
+      jwt.verify(token, secret, (err, dec) => {
+        if (err) return rej(err)
+        try {
+          const id = new ObjectId((dec as { _id: string })._id)
+          res(id)
+        } catch (err) {
+          rej(err)
+        }
+      })
+    })
+  }
+
+  async function verifyRequest(req: Request): Promise<ObjectId | undefined> {
+    const header = req.header('authorization')
+    if (!header) return
+    if (!/^[Bb]earer /.test(header)) return
+    const token = header.substring(7);
+    (req as AuthRequest).accountId = await verify(token);
+    (req as AuthRequest).verified = true
+  }
+
+  return { sign, verify, verifyRequest }
+}
+
+export default createAuth

+ 11 - 0
src/http.ts

@@ -8,6 +8,15 @@ export function createExpress(ctx: Context) {
 
   app.use(express.json())
 
+  app.use(async (req, res, next) => {
+    try {
+      await ctx.auth.verifyRequest(req)
+    } catch (err) {
+      ctx.log.warn('Failed to verify request', err)
+    }
+    next()
+  })
+
   const prefix = ctx.config.api.prefix
 
   app.post(`${prefix}/account`, account.createAccount(ctx))
@@ -15,6 +24,8 @@ export function createExpress(ctx: Context) {
   app.put(`${prefix}/account/:id?`, account.updateAccount(ctx))
   app.delete(`${prefix}/account/:id?`, account.deleteAccount(ctx))
 
+  app.post(`${prefix}/login/account`, account.loginAccount(ctx))
+
   // Handle errors passed to next function
   const catchError: ErrorRequestHandler = (err, req, res, next) => {
     if (!res.headersSent) {

+ 17 - 0
src/index.ts

@@ -1,12 +1,29 @@
+import crypto from 'crypto'
 import dotenv from 'dotenv'
 import main from './main'
 
 dotenv.config()
 
+const dynamicConfig: Record<string, unknown> = {}
+
 main({
   api: {
     prefix: process.env.API_PREFIX || '/api',
   },
+  auth: {
+    jwt: {
+      expiresIn: parseInt(process.env.AUTH_JWT_EXPIRES_IN || '86400'),
+      get secret() {
+        if (process.env.AUTH_JWT_SECRET) return process.env.AUTH_JWT_SECRET
+        if (!dynamicConfig.authJwtSecret) {
+          const secret = crypto.randomBytes(64).toString('hex')
+          console.warn('AUTH_JWT_SECRET not set. Generated a JWT secret for this run only:', secret)
+          dynamicConfig.authJwtSecret = secret
+        }
+        return dynamicConfig.authJwtSecret as string
+      },
+    },
+  },
   http: {
     host: process.env.HTTP_HOST || '',
     port: parseInt(process.env.HTTP_PORT || '5001'),

+ 5 - 0
src/main.ts

@@ -1,4 +1,5 @@
 import type { SignalConstants } from 'os'
+import createAuth from './auth'
 import createDatabase from './db'
 import { createExpress } from './http'
 import createLogger from './log'
@@ -16,6 +17,10 @@ async function main(config: Config): Promise<void> {
   const log = createLogger(ctx)
   ctx.log = log
 
+  // Initialize auth
+  const auth = createAuth(ctx)
+  ctx.auth = auth
+
   // Initialize database connection
   const { mongo, db, model } = await createDatabase(ctx)
   ctx.mongo = mongo

+ 8 - 0
src/types.ts

@@ -1,3 +1,4 @@
+import type { Auth } from './auth'
 import type { Logger } from './log'
 import type { Models } from './db'
 import type { Db, MongoClient } from 'mongodb'
@@ -6,6 +7,12 @@ export interface Config {
   api: {
     prefix: string
   }
+  auth: {
+    jwt: {
+      expiresIn: number
+      secret: string
+    }
+  }
   http: {
     host: string
     port: number
@@ -21,6 +28,7 @@ export interface Config {
 }
 
 export interface Context {
+  auth: Auth
   config: Config
   db: Db
   log: Logger