├── .eslintrc.json ├── .github └── workflows │ ├── main.yml │ └── publish.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── README.md ├── bun.lockb ├── bunfig.toml ├── docker-compose.yml ├── drizzle.config.ts ├── drizzle ├── 0000_silky_nightcrawler.sql └── meta │ ├── 0000_snapshot.json │ └── _journal.json ├── package.json ├── renovate.json ├── src ├── currentUrlAndMethodIsAllowed.ts ├── elysia-auth-plugin.ts ├── index.ts ├── type.ts └── utils.ts ├── test ├── access-token-value.test.ts ├── config-url.test.ts ├── context.test.ts ├── currentUrlAndMethodIsAllowed.test.ts ├── default-answer.test.ts ├── public-private-page.test.ts ├── user-validation.test.ts ├── utils.test.ts └── utils │ ├── db.ts │ ├── migrate.ts │ ├── schema.ts │ ├── server.test.ts │ ├── server.ts │ └── utils.ts ├── tsconfig.json └── tsup.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root":true, 3 | "parser":"@typescript-eslint/parser", 4 | "parserOptions":{ 5 | "ecmaVersion":2020, 6 | "sourceType":"module" 7 | }, 8 | "plugins":[ 9 | "simple-import-sort", 10 | "prettier" 11 | ], 12 | "env":{ 13 | "amd":true, 14 | "node":true 15 | }, 16 | "extends":[ 17 | "eslint:recommended", 18 | "plugin:@typescript-eslint/eslint-recommended", 19 | "plugin:@typescript-eslint/recommended", 20 | "prettier" 21 | ], 22 | "rules":{ 23 | "prettier/prettier":"error", 24 | "@typescript-eslint/no-unused-vars":"error", 25 | "simple-import-sort/imports":"error", 26 | "simple-import-sort/exports":"error", 27 | "@typescript-eslint/no-var-requires":"off" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on latest Bun 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | 12 | services: 13 | postgres: 14 | image: postgres 15 | env: 16 | POSTGRES_USER: docker 17 | POSTGRES_PASSWORD: docker 18 | POSTGRES_DB: project 19 | options: >- 20 | --health-cmd pg_isready 21 | --health-interval 10s 22 | --health-timeout 5s 23 | --health-retries 5 24 | ports: 25 | - 5432:5432 26 | 27 | steps: 28 | - name: Checkout repo 29 | uses: actions/checkout@v4 30 | 31 | - uses: oven-sh/setup-bun@v2 32 | 33 | - run: bun install 34 | 35 | - name: Migration Up 36 | run: bun run migration:up 37 | 38 | - name: Lint 39 | run: bun lint 40 | 41 | - name: Test 42 | run: bun test 43 | 44 | - name: Build 45 | run: bun run build -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | types: [closed] 7 | 8 | jobs: 9 | build: 10 | name: Publish 11 | 12 | if: startsWith(github.head_ref, 'renovate/') 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest] 17 | 18 | services: 19 | postgres: 20 | image: postgres 21 | env: 22 | POSTGRES_USER: docker 23 | POSTGRES_PASSWORD: docker 24 | POSTGRES_DB: project 25 | options: >- 26 | --health-cmd pg_isready 27 | --health-interval 10s 28 | --health-timeout 5s 29 | --health-retries 5 30 | ports: 31 | - 5432:5432 32 | 33 | steps: 34 | - uses: dorny/paths-filter@v3 35 | id: changes 36 | with: 37 | filters: | 38 | src: 39 | - 'src/**' 40 | - 'package.json' 41 | - name: Checkout repo 42 | if: steps.changes.outputs.src == 'true' 43 | uses: actions/checkout@v4 44 | 45 | - if: steps.changes.outputs.src == 'true' 46 | run: | 47 | git config user.name "${{ secrets.USER_NAME }}" 48 | git config user.email "${{ secrets.USER_EMAIL }}" 49 | 50 | - uses: oven-sh/setup-bun@v2 51 | 52 | - run: bun install 53 | 54 | - name: Migration Up 55 | run: bun run migration:up 56 | 57 | - name: Lint 58 | run: bun lint 59 | 60 | - name: Test 61 | run: bun test 62 | 63 | - name: Build 64 | run: bun run build 65 | 66 | - name: Bump version 67 | if: steps.changes.outputs.src == 'true' 68 | uses: qzb/standard-version-action@v1.0.13 69 | 70 | - if: steps.changes.outputs.src == 'true' 71 | run: | 72 | git config --global user.name "${{ secrets.USER_NAME }}" 73 | git config --global user.email "${{ secrets.USER_EMAIL }}" 74 | git push --follow-tags origin master 75 | - if: steps.changes.outputs.src == 'true' 76 | uses: JS-DevTools/npm-publish@v3 77 | with: 78 | token: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | coverage 6 | docker -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "trailingComma": "all", 5 | "singleQuote": true, 6 | "endOfLine":"auto" 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### 1.3.3 (2025-05-28) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **deps:** update dependency drizzle-orm to ^0.44.0 ([#119](https://github.com/qlaffont/elysia-auth-drizzle/issues/119)) ([007d00e](https://github.com/qlaffont/elysia-auth-drizzle/commit/007d00e9508a1fe4cdb597cb50fee796e6c0cacd)) 11 | 12 | ### 1.3.2 (2025-05-13) 13 | 14 | ### 1.2.92 (2025-04-28) 15 | 16 | ### 1.2.91 (2025-04-24) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * **deps:** update dependency drizzle-orm to ^0.43.0 ([#116](https://github.com/qlaffont/elysia-auth-drizzle/issues/116)) ([30b17ae](https://github.com/qlaffont/elysia-auth-drizzle/commit/30b17ae59deb975013271436d0ebb81ebc81cbb1)) 22 | 23 | ### 1.2.90 (2025-04-21) 24 | 25 | ### 1.2.89 (2025-04-16) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * **deps:** update dependency drizzle-orm to ^0.42.0 ([#114](https://github.com/qlaffont/elysia-auth-drizzle/issues/114)) ([f578be4](https://github.com/qlaffont/elysia-auth-drizzle/commit/f578be4105402e9e0430335092f467dea15899e7)) 31 | 32 | ### 1.2.88 (2025-04-15) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * **deps:** update dependency drizzle-kit to ^0.31.0 ([#113](https://github.com/qlaffont/elysia-auth-drizzle/issues/113)) ([954143c](https://github.com/qlaffont/elysia-auth-drizzle/commit/954143c2b7225970136eeb2fb7610f4b8cebb519)) 38 | 39 | ### 1.2.87 (2025-04-14) 40 | 41 | ### 1.2.86 (2025-04-08) 42 | 43 | ### 1.2.85 (2025-04-01) 44 | 45 | ### 1.2.84 (2025-03-26) 46 | 47 | ### 1.2.83 (2025-03-21) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * **deps:** update dependency drizzle-orm to ^0.41.0 ([#108](https://github.com/qlaffont/elysia-auth-drizzle/issues/108)) ([4c2fa63](https://github.com/qlaffont/elysia-auth-drizzle/commit/4c2fa6342e653c1bd80fe4c32a7b95ae7fcdd4a8)) 53 | 54 | ### 1.2.82 (2025-03-19) 55 | 56 | ### 1.2.81 (2025-03-17) 57 | 58 | ### 1.2.80 (2025-03-13) 59 | 60 | ### 1.2.79 (2025-03-10) 61 | 62 | ### 1.2.78 (2025-03-07) 63 | 64 | ### 1.2.77 (2025-03-06) 65 | 66 | ### 1.2.76 (2025-02-27) 67 | 68 | ### 1.2.75 (2025-02-26) 69 | 70 | ### 1.2.74 (2025-02-26) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * **deps:** update dependency drizzle-orm to ^0.40.0 ([#99](https://github.com/qlaffont/elysia-auth-drizzle/issues/99)) ([ebf34fe](https://github.com/qlaffont/elysia-auth-drizzle/commit/ebf34fe7ee802e51bcdd4a20f2bf23d04a767bc9)) 76 | 77 | ### 1.2.73 (2025-02-25) 78 | 79 | ### 1.2.72 (2025-02-17) 80 | 81 | ### 1.2.71 (2025-02-14) 82 | 83 | ### 1.2.70 (2025-02-11) 84 | 85 | ### 1.2.69 (2025-02-07) 86 | 87 | ### 1.2.68 (2025-02-04) 88 | 89 | ### 1.2.67 (2025-02-03) 90 | 91 | ### 1.2.66 (2025-01-27) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * **deps:** update dependency drizzle-orm to ^0.39.0 ([#91](https://github.com/qlaffont/elysia-auth-drizzle/issues/91)) ([67fdb47](https://github.com/qlaffont/elysia-auth-drizzle/commit/67fdb4757b65dafc821fb27e22c43a35f9409d73)) 97 | 98 | ### 1.2.65 (2025-01-14) 99 | 100 | ### 1.2.64 (2025-01-06) 101 | 102 | ### 1.2.63 (2024-12-29) 103 | 104 | ### 1.2.62 (2024-12-20) 105 | 106 | ### 1.2.61 (2024-12-09) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * **deps:** update dependency drizzle-orm to ^0.38.0 ([#86](https://github.com/qlaffont/elysia-auth-drizzle/issues/86)) ([d7e1c6e](https://github.com/qlaffont/elysia-auth-drizzle/commit/d7e1c6ee4361b08bb924545b316ceaf1fd098bbc)) 112 | 113 | ### 1.2.60 (2024-12-09) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * **deps:** update dependency drizzle-kit to ^0.30.0 ([#85](https://github.com/qlaffont/elysia-auth-drizzle/issues/85)) ([e6d6b63](https://github.com/qlaffont/elysia-auth-drizzle/commit/e6d6b63e183c41a831803b04b3922195c882d7b4)) 119 | 120 | ### 1.2.59 (2024-12-06) 121 | 122 | ### 1.2.58 (2024-12-04) 123 | 124 | 125 | ### Bug Fixes 126 | 127 | * **deps:** update dependency drizzle-orm to ^0.37.0 ([#83](https://github.com/qlaffont/elysia-auth-drizzle/issues/83)) ([5ef233b](https://github.com/qlaffont/elysia-auth-drizzle/commit/5ef233b0009cf3dc08f50b9dd89cc9aeb1a9e2e5)) 128 | 129 | ### 1.2.57 (2024-12-03) 130 | 131 | 132 | ### Bug Fixes 133 | 134 | * **deps:** update dependency drizzle-kit to ^0.29.0 ([#82](https://github.com/qlaffont/elysia-auth-drizzle/issues/82)) ([2b82a62](https://github.com/qlaffont/elysia-auth-drizzle/commit/2b82a62d5e82f779b967f1148b25de689755a1e6)) 135 | 136 | ### 1.2.56 (2024-11-29) 137 | 138 | ### 1.2.55 (2024-11-28) 139 | 140 | ### 1.2.54 (2024-11-20) 141 | 142 | ### 1.2.53 (2024-11-20) 143 | 144 | ### 1.2.52 (2024-11-15) 145 | 146 | ### 1.2.51 (2024-11-14) 147 | 148 | ### 1.2.50 (2024-11-13) 149 | 150 | ### 1.2.49 (2024-11-06) 151 | 152 | 153 | ### Bug Fixes 154 | 155 | * **deps:** update dependency drizzle-kit to ^0.28.0 ([a18cb3c](https://github.com/qlaffont/elysia-auth-drizzle/commit/a18cb3ce6e2ab0b5cddb33b94ff46c78a26effd1)) 156 | 157 | ### 1.2.48 (2024-10-30) 158 | 159 | 160 | ### Bug Fixes 161 | 162 | * **deps:** update dependency drizzle-orm to ^0.36.0 ([8333ef8](https://github.com/qlaffont/elysia-auth-drizzle/commit/8333ef8cf3430b9ca0aae010aafaba46da47e671)) 163 | 164 | ### 1.2.47 (2024-10-30) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * **deps:** update dependency drizzle-kit to ^0.27.0 ([994225f](https://github.com/qlaffont/elysia-auth-drizzle/commit/994225ff25ec0b58c7a3e53cba3bf3c01843423e)) 170 | 171 | ### 1.2.46 (2024-10-28) 172 | 173 | ### 1.2.45 (2024-10-16) 174 | 175 | ### 1.2.44 (2024-10-16) 176 | 177 | 178 | ### Bug Fixes 179 | 180 | * **deps:** update dependency drizzle-orm to ^0.35.0 ([8ed5aed](https://github.com/qlaffont/elysia-auth-drizzle/commit/8ed5aed1b65e27c31b3dc1d307f046456d6853a1)) 181 | 182 | ### 1.2.43 (2024-10-15) 183 | 184 | 185 | ### Bug Fixes 186 | 187 | * **deps:** update dependency drizzle-kit to ^0.26.0 ([bd8b5f7](https://github.com/qlaffont/elysia-auth-drizzle/commit/bd8b5f7f5e08d2a0bc839cb299b28522be5558db)) 188 | 189 | ### 1.2.40 (2024-10-08) 190 | 191 | 192 | ### Bug Fixes 193 | 194 | * **deps:** update dependency drizzle-orm to ^0.34.0 ([94b1327](https://github.com/qlaffont/elysia-auth-drizzle/commit/94b13271e19176cb899e7146bf1e122aca847352)) 195 | 196 | ### 1.2.39 (2024-10-07) 197 | 198 | 199 | ### Bug Fixes 200 | 201 | * **deps:** update dependency drizzle-kit to ^0.25.0 ([d675ce1](https://github.com/qlaffont/elysia-auth-drizzle/commit/d675ce18b543550c5dfa34f357e3a21b48618d0f)) 202 | 203 | ### 1.2.38 (2024-10-07) 204 | 205 | ### 1.2.37 (2024-10-02) 206 | 207 | ### 1.2.36 (2024-09-20) 208 | 209 | ### 1.2.35 (2024-09-09) 210 | 211 | ### 1.2.34 (2024-08-26) 212 | 213 | ### 1.2.33 (2024-08-22) 214 | 215 | ### 1.2.32 (2024-08-14) 216 | 217 | ### 1.2.31 (2024-08-08) 218 | 219 | 220 | ### Bug Fixes 221 | 222 | * **deps:** update dependency drizzle-orm to ^0.33.0 ([2202b01](https://github.com/qlaffont/elysia-auth-drizzle/commit/2202b017c2d8cb55e8fc46c9a799afdd606fa349)) 223 | 224 | ### 1.2.30 (2024-08-08) 225 | 226 | 227 | ### Bug Fixes 228 | 229 | * **deps:** update dependency drizzle-kit to ^0.24.0 ([d9da1d4](https://github.com/qlaffont/elysia-auth-drizzle/commit/d9da1d42e39fbd3a6cb610cb3346c0fabc627673)) 230 | 231 | ### 1.2.29 (2024-08-07) 232 | 233 | ### 1.2.28 (2024-07-22) 234 | 235 | ### 1.2.27 (2024-07-13) 236 | 237 | ### 1.2.26 (2024-07-11) 238 | 239 | 240 | ### Bug Fixes 241 | 242 | * **deps:** update dependency @bogeychan/elysia-logger to ^0.1.0 ([8e0dd0a](https://github.com/qlaffont/elysia-auth-drizzle/commit/8e0dd0a857dcb736f86f56a45cac8af14eb070c5)) 243 | 244 | ### 1.2.25 (2024-07-10) 245 | 246 | ### 1.2.24 (2024-07-10) 247 | 248 | ### 1.2.23 (2024-07-08) 249 | 250 | ### 1.2.22 (2024-07-08) 251 | 252 | ### 1.2.21 (2024-07-08) 253 | 254 | ### 1.2.20 (2024-07-08) 255 | 256 | ### 1.2.19 (2024-06-25) 257 | 258 | ### 1.2.18 (2024-06-18) 259 | 260 | ### 1.2.17 (2024-06-17) 261 | 262 | ### 1.2.16 (2024-06-11) 263 | 264 | ### 1.2.15 (2024-06-10) 265 | 266 | ### 1.2.14 (2024-06-10) 267 | 268 | ### 1.2.13 (2024-06-05) 269 | 270 | ### 1.2.12 (2024-06-05) 271 | 272 | ### 1.2.8 (2024-06-03) 273 | 274 | ### 1.2.7 (2024-06-01) 275 | 276 | ### 1.2.6 (2024-06-01) 277 | 278 | ### 1.2.5 (2024-06-01) 279 | 280 | ### 1.2.4 (2024-05-31) 281 | 282 | ### 1.2.3 (2024-05-31) 283 | 284 | ### 1.1.20 (2024-05-24) 285 | 286 | 287 | ### Bug Fixes 288 | 289 | * **deps:** update dependency @bogeychan/elysia-logger to ^0.0.22 ([b9b70c1](https://github.com/qlaffont/elysia-auth-drizzle/commit/b9b70c17bcb4325a0bbd38e0a41a22e9209aec6e)) 290 | 291 | ### 1.1.19 (2024-05-24) 292 | 293 | ### 1.1.18 (2024-05-24) 294 | 295 | ### 1.1.17 (2024-05-16) 296 | 297 | ### 1.1.16 (2024-05-13) 298 | 299 | ### 1.1.15 (2024-05-10) 300 | 301 | ### 1.1.14 (2024-05-07) 302 | 303 | ### 1.1.13 (2024-04-30) 304 | 305 | ### 1.1.12 (2024-04-24) 306 | 307 | ### 1.1.11 (2024-04-23) 308 | 309 | ### 1.1.10 (2024-04-22) 310 | 311 | ### 1.1.9 (2024-04-18) 312 | 313 | ### 1.1.8 (2024-04-17) 314 | 315 | ### 1.1.7 (2024-04-16) 316 | 317 | ### 1.1.6 (2024-04-15) 318 | 319 | ### 1.1.5 (2024-04-15) 320 | 321 | ### 1.1.4 (2024-04-10) 322 | 323 | ### 1.1.3 (2024-04-09) 324 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elysia-auth-drizzle 2 | 3 | Library who handle authentification (Header/Cookie/QueryParam). 4 | 5 | ## Usage 6 | 7 | ```typescript 8 | import { elysiaAuthDrizzlePlugin } from 'elysia-auth-drizzle'; 9 | 10 | export const app = new Elysia() 11 | .use( 12 | elysiaAuthDrizzlePlugin({ 13 | config: [ 14 | { 15 | url: '/public', 16 | method: 'GET', 17 | }, 18 | ], 19 | jwtSecret: 'test', 20 | drizzle: { 21 | db: db, 22 | usersSchema: users, 23 | tokensSchema: tokens, 24 | }, 25 | }), 26 | ) 27 | ``` 28 | 29 | ## Plugin options 30 | 31 | | name | default | description | 32 | | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | 33 | | jwtSecret | undefined | Secret used to sign JWT | 34 | | drizzle | undefined | Contain drizzle db + users schema + tokens schemas ({db, userSchemas, tokenSchemas} / Token Schemas is optional if you use verifyAccessTokenOnlyInJWT) | 35 | | config | [] | Array who contain url with method allowed in public | 36 | | cookieSecret | undefined | (optional) Secret used to sign cookie value | 37 | | verifyAccessTokenOnlyInJWT | false | (optional) Check only JWT expiration not token validity in DB | 38 | | userValidation | undefined | (optional) (user) => void or `Promise` / Allow to make more check regarding user (ex: check if user is banned) | 39 | 40 | ## Tests 41 | 42 | To execute jest tests (all errors, type integrity test) 43 | 44 | ``` 45 | bun test 46 | ``` 47 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlaffont/elysia-auth-drizzle/078d62cb3d9b7cc1da5945e708c79675997b72a1/bun.lockb -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | 3 | # always enable coverage 4 | coverage = true -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | postgres: 5 | image: postgres 6 | environment: 7 | POSTGRES_USER: docker 8 | POSTGRES_PASSWORD: docker 9 | POSTGRES_DB: project 10 | PGDATA: /data/postgres 11 | volumes: 12 | - ./docker/postgres:/data/postgres 13 | ports: 14 | - "5432:5432" -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'drizzle-kit'; 2 | 3 | export default { 4 | schema: './test/utils/schema.ts', 5 | out: './drizzle', 6 | driver: 'pg', // 'pg' | 'mysql2' | 'better-sqlite' | 'libsql' | 'turso' 7 | dbCredentials: { 8 | connectionString: 'postgres://docker:docker@127.0.0.1:5432/project', 9 | }, 10 | } satisfies Config; 11 | -------------------------------------------------------------------------------- /drizzle/0000_silky_nightcrawler.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "tokens" ( 2 | "id" uuid DEFAULT gen_random_uuid() NOT NULL, 3 | "owner_id" uuid NOT NULL, 4 | "access_token" text NOT NULL, 5 | "refresh_token" text NOT NULL, 6 | "created_at" timestamp DEFAULT now() NOT NULL 7 | ); 8 | --> statement-breakpoint 9 | CREATE TABLE IF NOT EXISTS "users" ( 10 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 11 | "created_at" timestamp DEFAULT now() NOT NULL, 12 | "updated_at" timestamp DEFAULT now() NOT NULL 13 | ); 14 | --> statement-breakpoint 15 | CREATE INDEX IF NOT EXISTS "tokens_id_idx" ON "tokens" ("id");--> statement-breakpoint 16 | CREATE INDEX IF NOT EXISTS "user_id_idx" ON "users" ("id"); 17 | 18 | INSERT INTO "users" ("id", "created_at", "updated_at") VALUES ('04319e04-08d4-452f-9a46-9c1f9e79e2f0', '2022-08-18 11:58:05.698', '2022-08-18 11:58:05.699'); -------------------------------------------------------------------------------- /drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "d583e08d-a1f3-456a-86d8-ff7d57110e46", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "5", 5 | "dialect": "pg", 6 | "tables": { 7 | "tokens": { 8 | "name": "tokens", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "uuid", 14 | "primaryKey": false, 15 | "notNull": true, 16 | "default": "gen_random_uuid()" 17 | }, 18 | "owner_id": { 19 | "name": "owner_id", 20 | "type": "uuid", 21 | "primaryKey": false, 22 | "notNull": true 23 | }, 24 | "access_token": { 25 | "name": "access_token", 26 | "type": "text", 27 | "primaryKey": false, 28 | "notNull": true 29 | }, 30 | "refresh_token": { 31 | "name": "refresh_token", 32 | "type": "text", 33 | "primaryKey": false, 34 | "notNull": true 35 | }, 36 | "created_at": { 37 | "name": "created_at", 38 | "type": "timestamp", 39 | "primaryKey": false, 40 | "notNull": true, 41 | "default": "now()" 42 | } 43 | }, 44 | "indexes": { 45 | "tokens_id_idx": { 46 | "name": "tokens_id_idx", 47 | "columns": [ 48 | "id" 49 | ], 50 | "isUnique": false 51 | } 52 | }, 53 | "foreignKeys": {}, 54 | "compositePrimaryKeys": {}, 55 | "uniqueConstraints": {} 56 | }, 57 | "users": { 58 | "name": "users", 59 | "schema": "", 60 | "columns": { 61 | "id": { 62 | "name": "id", 63 | "type": "uuid", 64 | "primaryKey": true, 65 | "notNull": true, 66 | "default": "gen_random_uuid()" 67 | }, 68 | "created_at": { 69 | "name": "created_at", 70 | "type": "timestamp", 71 | "primaryKey": false, 72 | "notNull": true, 73 | "default": "now()" 74 | }, 75 | "updated_at": { 76 | "name": "updated_at", 77 | "type": "timestamp", 78 | "primaryKey": false, 79 | "notNull": true, 80 | "default": "now()" 81 | } 82 | }, 83 | "indexes": { 84 | "user_id_idx": { 85 | "name": "user_id_idx", 86 | "columns": [ 87 | "id" 88 | ], 89 | "isUnique": false 90 | } 91 | }, 92 | "foreignKeys": {}, 93 | "compositePrimaryKeys": {}, 94 | "uniqueConstraints": {} 95 | } 96 | }, 97 | "enums": {}, 98 | "schemas": {}, 99 | "_meta": { 100 | "columns": {}, 101 | "schemas": {}, 102 | "tables": {} 103 | } 104 | } -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1711548142506, 9 | "tag": "0000_silky_nightcrawler", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elysia-auth-drizzle", 3 | "version": "1.3.3", 4 | "main": "./dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "scripts": { 7 | "test": "bun test --coverage", 8 | "test:watch": "bun test --watch", 9 | "lint": "eslint src test", 10 | "build": "tsup src/index.ts", 11 | "migration:make": "drizzle-kit generate:pg", 12 | "migration:up": "bun test/utils/migrate.ts", 13 | "migration:studio": "drizzle-kit studio" 14 | }, 15 | "files": [ 16 | "dist", 17 | "src" 18 | ], 19 | "peerDependencies": { 20 | "elysia": "^1.1.22" 21 | }, 22 | "dependencies": { 23 | "@bogeychan/elysia-logger": "^0.1.4", 24 | "jsonwebtoken": "^9.0.2", 25 | "drizzle-kit": "^0.31.0", 26 | "drizzle-orm": "^0.44.0", 27 | "unify-errors": "^1.2.227" 28 | }, 29 | "devDependencies": { 30 | "@elysiajs/eden": "^1.1.3", 31 | "@types/jsonwebtoken": "^9.0.6", 32 | "@typescript-eslint/eslint-plugin": "7.16.0", 33 | "@typescript-eslint/parser": "7.16.0", 34 | "bun-types": "latest", 35 | "eslint": "8.57.0", 36 | "eslint-config-prettier": "9.1.0", 37 | "eslint-plugin-import": "2.29.1", 38 | "eslint-plugin-prettier": "5.1.3", 39 | "eslint-plugin-simple-import-sort": "12.1.1", 40 | "pg": "^8.11.3", 41 | "postgres": "^3.4.4", 42 | "prettier": "3.3.3", 43 | "tsup": "^8.0.2", 44 | "unify-elysia": "^1.1.15" 45 | }, 46 | "module": "dist/index.mjs", 47 | "packageManager": "pnpm@10.11.0" 48 | } 49 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "automergeType": "pr", 7 | "automerge": true, 8 | "ignoreDeps": [ 9 | "pg", 10 | "postgres", 11 | "@typescript-eslint/eslint-plugin", 12 | "@typescript-eslint/parser", 13 | "eslint", 14 | "eslint-config-prettier", 15 | "eslint-plugin-import", 16 | "eslint-plugin-prettier", 17 | "eslint-plugin-simple-import-sort", 18 | "prettier", 19 | "tsup" 20 | ] 21 | } -------------------------------------------------------------------------------- /src/currentUrlAndMethodIsAllowed.ts: -------------------------------------------------------------------------------- 1 | import { HTTPMethods, UrlConfig } from './type'; 2 | 3 | const methods: HTTPMethods[] = [ 4 | 'DELETE', 5 | 'GET', 6 | 'HEAD', 7 | 'PATCH', 8 | 'POST', 9 | 'PUT', 10 | 'OPTIONS', 11 | 'PROPFIND', 12 | 'PROPPATCH', 13 | 'MKCOL', 14 | 'COPY', 15 | 'MOVE', 16 | 'LOCK', 17 | 'UNLOCK', 18 | 'TRACE', 19 | 'SEARCH', 20 | ]; 21 | 22 | const addUrl = ( 23 | u: string, 24 | m: HTTPMethods | '*', 25 | urls: UrlConfig[], 26 | ): UrlConfig[] => { 27 | if (m === '*') { 28 | for (const HTTPMethod of methods) { 29 | urls.push({ url: u, method: HTTPMethod }); 30 | } 31 | } else { 32 | urls.push({ url: u, method: m as HTTPMethods }); 33 | } 34 | 35 | return urls; 36 | }; 37 | 38 | export const currentUrlAndMethodIsAllowed = ( 39 | url: string, 40 | method: HTTPMethods, 41 | config: UrlConfig[], 42 | ): boolean => { 43 | let urlsConfig: UrlConfig[] = []; 44 | let result = false; 45 | 46 | for (let i = 0, len = config.length; i < len; i += 1) { 47 | const val = config[i]; 48 | urlsConfig = addUrl(val.url, val.method, urlsConfig); 49 | } 50 | 51 | let currentUrl = url; 52 | 53 | // Remove Query Params 54 | currentUrl = currentUrl.split('?')[0]; 55 | 56 | // Remove last slash to be sure to have same url 57 | if (currentUrl !== '/' && currentUrl.slice(-1) === '/') { 58 | currentUrl = currentUrl.slice(0, -1); 59 | } 60 | 61 | for (let index = 0; index < urlsConfig.length; index += 1) { 62 | const urlConfig: UrlConfig = urlsConfig[index]; 63 | 64 | //If url ends with *, just check that url is similar without * 65 | if (urlConfig.url.endsWith('/*')) { 66 | if (currentUrl.startsWith(urlConfig.url.replace('/*', ''))) { 67 | result = true; 68 | break; 69 | } 70 | } 71 | 72 | // Check current url and current method are in config 73 | if (currentUrl === urlConfig.url && method === urlConfig.method) { 74 | result = true; 75 | break; 76 | } 77 | 78 | // Ignore dynamic paremeters and check 79 | if (urlConfig.url.indexOf('/:') !== -1) { 80 | const splitUrl = currentUrl.split('/'); 81 | const splitConfigUrl = urlConfig.url.split('/'); 82 | 83 | if ( 84 | splitUrl.length === splitConfigUrl.length && 85 | method === urlConfig.method 86 | ) { 87 | let similar = true; 88 | for (let j = 0; j < splitUrl.length; j += 1) { 89 | if ( 90 | splitConfigUrl[j].indexOf(':') === -1 && 91 | splitUrl[j] !== splitConfigUrl[j] 92 | ) { 93 | similar = false; 94 | break; 95 | } 96 | } 97 | 98 | // If Everything is similar (with parameters and method) 99 | if (similar) { 100 | result = true; 101 | break; 102 | } 103 | } 104 | } 105 | } 106 | 107 | return result; 108 | }; 109 | -------------------------------------------------------------------------------- /src/elysia-auth-plugin.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { eq } from 'drizzle-orm'; 3 | import { Elysia } from 'elysia'; 4 | import { verify } from 'jsonwebtoken'; 5 | import { Unauthorized } from 'unify-errors'; 6 | 7 | import { currentUrlAndMethodIsAllowed } from './currentUrlAndMethodIsAllowed'; 8 | import { HTTPMethods, UrlConfig } from './type'; 9 | 10 | //REF: https://github.com/elysiajs/elysia/blob/main/src/utils.ts 11 | const encoder = new TextEncoder(); 12 | function removeTrailingEquals(digest: string): string { 13 | let trimmedDigest = digest; 14 | while (trimmedDigest.endsWith('=')) { 15 | trimmedDigest = trimmedDigest.slice(0, -1); 16 | } 17 | return trimmedDigest; 18 | } 19 | 20 | export const signCookie = async (val: string, secret: string | null) => { 21 | if (typeof val !== 'string') 22 | throw new TypeError('Cookie value must be provided as a string.'); 23 | 24 | if (secret === null) throw new TypeError('Secret key must be provided.'); 25 | 26 | const secretKey = await crypto.subtle.importKey( 27 | 'raw', 28 | encoder.encode(secret), 29 | { name: 'HMAC', hash: 'SHA-256' }, 30 | false, 31 | ['sign'], 32 | ); 33 | const hmacBuffer = await crypto.subtle.sign( 34 | 'HMAC', 35 | secretKey, 36 | encoder.encode(val), 37 | ); 38 | 39 | return ( 40 | val + '.' + removeTrailingEquals(Buffer.from(hmacBuffer).toString('base64')) 41 | ); 42 | }; 43 | export const unsignCookie = async (input: string, secret: string | null) => { 44 | if (typeof input !== 'string') 45 | throw new TypeError('Signed cookie string must be provided.'); 46 | 47 | if (null === secret) throw new TypeError('Secret key must be provided.'); 48 | 49 | const tentativeValue = input.slice(0, input.lastIndexOf('.')); 50 | const expectedInput = await signCookie(tentativeValue, secret); 51 | 52 | return expectedInput === input ? tentativeValue : false; 53 | }; 54 | 55 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 56 | export interface Options { 57 | jwtSecret: string; 58 | cookieSecret?: string; 59 | drizzle: { 60 | //@ts-ignore 61 | db; 62 | //@ts-ignore 63 | tokensSchema?; 64 | //@ts-ignore 65 | usersSchema; 66 | }; 67 | config?: UrlConfig[]; 68 | userValidation?: (user: T) => void | Promise; 69 | verifyAccessTokenOnlyInJWT?: boolean; 70 | prefix?: string; 71 | } 72 | 73 | export const getAccessTokenFromRequest = async ( 74 | req: { 75 | cookie?: Record; 76 | query?: Record; 77 | headers: Record; 78 | }, 79 | cookieSecret?: string, 80 | ) => { 81 | let token: string | undefined; 82 | 83 | if ( 84 | req.cookie && 85 | req.cookie['authorization'] && 86 | req.cookie['authorization'].value 87 | ) { 88 | if (cookieSecret) { 89 | const result = await unsignCookie( 90 | req.cookie['authorization'].value, 91 | cookieSecret, 92 | ); 93 | 94 | if (result === false) { 95 | throw new Unauthorized({ 96 | error: 'Token is not valid', 97 | }); 98 | } else { 99 | token = result; 100 | } 101 | } else { 102 | token = req.cookie['authorization'].value; 103 | } 104 | } 105 | 106 | if ((req.query as { access_token: string }).access_token) { 107 | token = (req.query as { access_token: string }).access_token; 108 | } 109 | 110 | if (req.headers.authorization) { 111 | token = (req.headers.authorization as string).trim().split(' ')[1]; 112 | } 113 | 114 | return token; 115 | }; 116 | 117 | export const checkTokenValidity = 118 | ( 119 | options: Options, 120 | currentUrl: string, 121 | currentMethod: HTTPMethods, 122 | cookieManager: { [x: string]: { remove: () => void } }, 123 | ) => 124 | async ( 125 | tokenValue?: string, 126 | ): Promise<{ connectedUser: T; isConnected: true } | void> => { 127 | //Check if token existing 128 | if (tokenValue) { 129 | let userId; 130 | 131 | try { 132 | const tokenData = verify(tokenValue, options.jwtSecret); 133 | 134 | if (!options.verifyAccessTokenOnlyInJWT) { 135 | const result = await options.drizzle.db 136 | .select() 137 | .from(options.drizzle.tokensSchema) 138 | .where(eq(options.drizzle.tokensSchema.accessToken, tokenValue)) 139 | .limit(1); 140 | 141 | if (result.length !== 1) { 142 | throw 'Token not valid in DB'; 143 | } else { 144 | userId = result[0].ownerId; 145 | } 146 | } else { 147 | //@ts-ignore 148 | userId = tokenData.id; 149 | } 150 | } catch (error) { 151 | //If token is not valid and If user is not connected and url is not public 152 | if ( 153 | !currentUrlAndMethodIsAllowed( 154 | currentUrl, 155 | currentMethod as HTTPMethods, 156 | options.config!, 157 | ) 158 | ) { 159 | if (cookieManager && cookieManager['authorization']) { 160 | cookieManager['authorization'].remove(); 161 | } 162 | 163 | throw new Unauthorized({ 164 | error: 'Token is not valid', 165 | }); 166 | } 167 | 168 | return; 169 | } 170 | 171 | const result = await options.drizzle.db 172 | .select() 173 | .from(options.drizzle.usersSchema) 174 | .where(eq(options.drizzle.usersSchema.id, userId)) 175 | .limit(1); 176 | 177 | const user = result[0]; 178 | 179 | options.userValidation && (await options.userValidation(user)); 180 | 181 | return { 182 | connectedUser: user as T, 183 | isConnected: true, 184 | }; 185 | } 186 | }; 187 | 188 | export const elysiaAuthDrizzlePlugin = (userOptions?: Options) => { 189 | const defaultOptions: Omit< 190 | Required>, 191 | 'jwtSecret' | 'cookieSecret' | 'drizzle' | 'prefix' 192 | > = { 193 | config: [], 194 | userValidation: () => {}, 195 | verifyAccessTokenOnlyInJWT: false, 196 | }; 197 | 198 | const options = { 199 | ...defaultOptions, 200 | ...userOptions, 201 | } as Required>; 202 | 203 | return new Elysia({ name: 'elysia-auth-drizzle' }).derive( 204 | { as: 'global' }, 205 | async ({ headers, query, cookie, request }) => { 206 | let isConnected = false; 207 | let connectedUser: T | undefined; 208 | 209 | const req = { 210 | headers, 211 | query, 212 | cookie, 213 | url: new URL(request.url).pathname, 214 | method: request.method as HTTPMethods, 215 | }; 216 | 217 | const tokenValue: string | undefined = await getAccessTokenFromRequest( 218 | req, 219 | options?.cookieSecret, 220 | ); 221 | 222 | const res = await checkTokenValidity( 223 | options as Options, 224 | req.url, 225 | req.method, 226 | req.cookie, 227 | )(tokenValue); 228 | 229 | if (res) { 230 | connectedUser = res.connectedUser; 231 | isConnected = res.isConnected; 232 | } 233 | 234 | // If user is not connected and url is not public 235 | if ( 236 | !isConnected && 237 | (options.prefix ? req.url.startsWith(options.prefix) : true) && 238 | !currentUrlAndMethodIsAllowed( 239 | req.url, 240 | req.method as HTTPMethods, 241 | options.config!, 242 | ) 243 | ) { 244 | throw new Unauthorized({ 245 | error: 'Page is not public', 246 | }); 247 | } 248 | 249 | return { 250 | isConnected, 251 | connectedUser, 252 | }; 253 | }, 254 | ); 255 | }; 256 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | checkTokenValidity as CheckTokenValidity, 3 | elysiaAuthDrizzlePlugin as plugin, 4 | getAccessTokenFromRequest as GetAccessTokenFromRequest, 5 | Options, 6 | } from './elysia-auth-plugin'; 7 | import { UrlConfig } from './type'; 8 | import { 9 | createUserToken as CreateUserToken, 10 | refreshUserToken as RefreshUserToken, 11 | removeAllUserTokens as RemoveAllUserTokens, 12 | removeUserToken as RemoveUserToken, 13 | } from './utils'; 14 | 15 | export const createUserToken = CreateUserToken; 16 | export const refreshUserToken = RefreshUserToken; 17 | export const removeAllUserTokens = RemoveAllUserTokens; 18 | export const removeUserToken = RemoveUserToken; 19 | export const elysiaAuthDrizzlePlugin = plugin; 20 | export const getAccessTokenFromRequest = GetAccessTokenFromRequest; 21 | export type ElysiaUrlConfig = UrlConfig; 22 | export type ElysiaAuthDrizzlePluginConfig = Options; 23 | export const checkTokenValidity = CheckTokenValidity; 24 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | export type HTTPMethods = 2 | | 'DELETE' 3 | | 'GET' 4 | | 'HEAD' 5 | | 'PATCH' 6 | | 'POST' 7 | | 'PUT' 8 | | 'OPTIONS' 9 | | 'PROPFIND' 10 | | 'PROPPATCH' 11 | | 'MKCOL' 12 | | 'COPY' 13 | | 'MOVE' 14 | | 'LOCK' 15 | | 'UNLOCK' 16 | | 'TRACE' 17 | | 'SEARCH'; 18 | 19 | export interface UrlConfig { 20 | url: string; 21 | method: HTTPMethods | '*'; 22 | } 23 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { eq } from 'drizzle-orm'; 4 | import { sign, verify } from 'jsonwebtoken'; 5 | import { BadRequest, NotFound } from 'unify-errors'; 6 | 7 | export const createUserToken = 8 | ({ 9 | db, 10 | usersSchema, 11 | tokensSchema, 12 | }: { 13 | //@ts-ignore 14 | db; 15 | //@ts-ignore 16 | usersSchema; 17 | //@ts-ignore 18 | tokensSchema?; 19 | }) => 20 | async ( 21 | userId: string, 22 | { 23 | secret, 24 | refreshSecret, 25 | accessTokenTime, 26 | refreshTokenTime, 27 | }: { 28 | secret: string; 29 | refreshSecret?: string; 30 | accessTokenTime: string; 31 | refreshTokenTime: string; 32 | }, 33 | ) => { 34 | let user; 35 | 36 | try { 37 | user = await db 38 | .select() 39 | .from(usersSchema) 40 | .where(eq(usersSchema.id, userId)) 41 | .limit(1); 42 | 43 | if (user.length === 0) { 44 | throw new NotFound({ error: 'User not found' }); 45 | } 46 | } catch (error) { 47 | throw new NotFound({ error: 'User not found' }); 48 | } 49 | 50 | const accessToken = sign({ id: userId }, secret, { 51 | expiresIn: accessTokenTime, 52 | }); 53 | 54 | const refreshToken = sign( 55 | { id: userId, date: new Date().getTime }, 56 | refreshSecret || secret, 57 | { 58 | expiresIn: refreshTokenTime, 59 | }, 60 | ); 61 | 62 | if (tokensSchema) { 63 | await db.insert(tokensSchema).values({ 64 | accessToken, 65 | refreshToken, 66 | ownerId: userId, 67 | }); 68 | } 69 | 70 | return { 71 | accessToken, 72 | refreshToken, 73 | }; 74 | }; 75 | 76 | export const removeUserToken = 77 | //@ts-ignore 78 | 79 | 80 | ({ db, tokensSchema }) => 81 | async (accessToken: string) => { 82 | await db 83 | .delete(tokensSchema) 84 | .where(eq(tokensSchema.accessToken, accessToken)); 85 | }; 86 | 87 | export const removeAllUserTokens = 88 | //@ts-ignore 89 | 90 | 91 | ({ db, tokensSchema }) => 92 | async (ownerId: string) => { 93 | await db.delete(tokensSchema).where(eq(tokensSchema.ownerId, ownerId)); 94 | }; 95 | 96 | export const refreshUserToken = 97 | //@ts-ignore 98 | 99 | 100 | ({ 101 | db, 102 | tokensSchema, 103 | }: { 104 | //@ts-ignore 105 | db; 106 | //@ts-ignore 107 | tokensSchema?; 108 | }) => 109 | async ( 110 | refreshToken: string, 111 | { 112 | secret, 113 | refreshSecret, 114 | accessTokenTime, 115 | }: { 116 | secret: string; 117 | refreshSecret?: string; 118 | accessTokenTime: string; 119 | }, 120 | ) => { 121 | let content; 122 | try { 123 | content = verify(refreshToken, refreshSecret || secret) as { 124 | id: string; 125 | }; 126 | } catch (error) { 127 | if (tokensSchema) { 128 | await db 129 | .delete(tokensSchema) 130 | .where(eq(tokensSchema.refreshToken, refreshToken)); 131 | } 132 | 133 | throw new BadRequest({ 134 | error: 'Token expired', 135 | }); 136 | } 137 | 138 | let token; 139 | if (tokensSchema) { 140 | const result = await db 141 | .select() 142 | .from(tokensSchema) 143 | .where(eq(tokensSchema.refreshToken, refreshToken)) 144 | .limit(1); 145 | 146 | if (result.length === 0) { 147 | throw new NotFound({ 148 | error: 'Token not found', 149 | }); 150 | } else { 151 | token = result[0]; 152 | } 153 | } else { 154 | //Get Data from expired Token 155 | } 156 | 157 | // Renew Token 158 | const accessToken = sign({ id: token?.ownerId || content?.id }, secret, { 159 | expiresIn: accessTokenTime, 160 | }); 161 | 162 | if (tokensSchema) { 163 | await db 164 | .update(tokensSchema) 165 | .set({ 166 | accessToken, 167 | }) 168 | .where(eq(tokensSchema.id, token.id)); 169 | } 170 | 171 | return { accessToken, refreshToken }; 172 | }; 173 | -------------------------------------------------------------------------------- /test/access-token-value.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'bun:test'; 2 | 3 | import { signCookie } from '../src/elysia-auth-plugin'; 4 | import { app } from './utils/server.test'; 5 | import { cleanToken, generateToken, testRoute } from './utils/utils'; 6 | 7 | let server; 8 | 9 | describe('Access Token value', () => { 10 | it('should be able to access token from authorization header', async () => { 11 | server = await app(); 12 | 13 | const token = await generateToken(); 14 | 15 | await testRoute( 16 | server, 17 | '/not-public-success', 18 | 'GET', 19 | { 20 | headers: { 21 | authorization: `Bearer ${token.accessToken}`, 22 | }, 23 | }, 24 | { supposedStatus: 200 }, 25 | ); 26 | 27 | await cleanToken(token); 28 | }); 29 | 30 | it('should be able to access token from Authorization header', async () => { 31 | server = await app(); 32 | 33 | const token = await generateToken(); 34 | 35 | await testRoute( 36 | server, 37 | '/not-public-success', 38 | 'GET', 39 | { 40 | headers: { 41 | Authorization: `Bearer ${token.accessToken}`, 42 | }, 43 | }, 44 | { supposedStatus: 200 }, 45 | ); 46 | 47 | await cleanToken(token); 48 | }); 49 | 50 | it('should be able to access token from access_token query param', async () => { 51 | server = await app(); 52 | 53 | const token = await generateToken(); 54 | 55 | await testRoute( 56 | server, 57 | `/not-public-success?access_token=${encodeURIComponent( 58 | token.accessToken, 59 | )}`, 60 | 'GET', 61 | { 62 | headers: { 63 | authorization: `Bearer ${token.accessToken}`, 64 | }, 65 | }, 66 | { supposedStatus: 200 }, 67 | ); 68 | 69 | await cleanToken(token); 70 | }); 71 | 72 | it('should be able to access token from Authorization cookie', async () => { 73 | server = await app(); 74 | 75 | const token = await generateToken(); 76 | 77 | await testRoute( 78 | server, 79 | `/not-public-success`, 80 | 'GET', 81 | { 82 | headers: { 83 | Cookie: `authorization=${token.accessToken}`, 84 | }, 85 | }, 86 | { supposedStatus: 200 }, 87 | ); 88 | 89 | await cleanToken(token); 90 | }); 91 | 92 | it('should be able to access token from Authorization cookie (SIGNED)', async () => { 93 | server = await app({ 94 | cookieSecret: 'test', 95 | }); 96 | 97 | const token = await generateToken(); 98 | 99 | await testRoute( 100 | server, 101 | `/not-public-success`, 102 | 'GET', 103 | { 104 | headers: { 105 | Cookie: `authorization=${await signCookie(token.accessToken, 'test')}`, 106 | }, 107 | }, 108 | { supposedStatus: 200 }, 109 | ); 110 | 111 | await cleanToken(token); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /test/config-url.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'bun:test'; 2 | 3 | import { app, methods } from './utils/server.test'; 4 | import { testRoute } from './utils/utils'; 5 | 6 | let server; 7 | 8 | describe('Config Url', () => { 9 | it('should validate on url and method specified', async () => { 10 | server = await app({ config: [{ url: '/public-success', method: 'GET' }] }); 11 | 12 | await testRoute( 13 | server, 14 | '/public-success', 15 | 'GET', 16 | {}, 17 | { supposedStatus: 200 }, 18 | ); 19 | }); 20 | 21 | it('should validate on url and method not specified', async () => { 22 | server = await app({ config: [{ url: '/public-success', method: '*' }] }); 23 | 24 | await testRoute( 25 | server, 26 | '/public-success', 27 | 'GET', 28 | {}, 29 | { supposedStatus: 200 }, 30 | ); 31 | }); 32 | 33 | it('should validate on url and for each method', async () => { 34 | for (const method of methods) { 35 | server = await app({ 36 | config: [{ url: `/public-${method.toLowerCase()}`, method: method }], 37 | }); 38 | 39 | await testRoute( 40 | server, 41 | `/public-${method.toLowerCase()}`, 42 | method, 43 | {}, 44 | { supposedStatus: 200 }, 45 | ); 46 | } 47 | }); 48 | 49 | it('should validate on url who contain parameter', async () => { 50 | server = await app({ 51 | config: [ 52 | { 53 | url: `/public-success/:variable1/tuto/:variable3/get`, 54 | method: 'GET', 55 | }, 56 | ], 57 | }); 58 | 59 | await testRoute( 60 | server, 61 | `/public-success/testvariable/tuto/test/get`, 62 | 'GET', 63 | {}, 64 | { supposedStatus: 200 }, 65 | ); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/context.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'bun:test'; 2 | 3 | import { app, userData } from './utils/server.test'; 4 | import { cleanToken, generateToken, testRoute } from './utils/utils'; 5 | 6 | let server; 7 | 8 | describe('Context', () => { 9 | describe('connectedUser', () => { 10 | it('should be able to get user if connected', async () => { 11 | server = await app(); 12 | 13 | const token = await generateToken(); 14 | 15 | await testRoute( 16 | server, 17 | `/get-req-user`, 18 | 'GET', 19 | { 20 | headers: { 21 | authorization: `Bearer ${token.accessToken}`, 22 | }, 23 | }, 24 | { supposedStatus: 200, supposedMessage: { id: userData.id } }, 25 | ); 26 | 27 | await cleanToken(token); 28 | }); 29 | 30 | it('should be able to get nothing if not connected', async () => { 31 | server = await app({ config: [{ url: '/get-req-user', method: 'GET' }] }); 32 | 33 | await testRoute( 34 | server, 35 | `/get-req-user`, 36 | 'GET', 37 | {}, 38 | { supposedStatus: 200, supposedMessage: undefined }, 39 | ); 40 | }); 41 | }); 42 | 43 | describe('isConnected', () => { 44 | it('should be able to return true if connected', async () => { 45 | server = await app(); 46 | 47 | const token = await generateToken(); 48 | 49 | await testRoute( 50 | server, 51 | `/get-req-isConnected`, 52 | 'GET', 53 | { 54 | headers: { 55 | authorization: `Bearer ${token.accessToken}`, 56 | }, 57 | }, 58 | { supposedStatus: 200, supposedMessage: { isConnected: true } }, 59 | ); 60 | 61 | await cleanToken(token); 62 | }); 63 | 64 | it('should be able to return false if not connected', async () => { 65 | server = await app({ 66 | config: [{ url: '/get-req-isConnected', method: 'GET' }], 67 | }); 68 | 69 | await testRoute( 70 | server, 71 | `/get-req-isConnected`, 72 | 'GET', 73 | {}, 74 | { supposedStatus: 200, supposedMessage: { isConnected: false } }, 75 | ); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/currentUrlAndMethodIsAllowed.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import { describe, expect, it } from 'bun:test'; 6 | 7 | import { ElysiaAuthDrizzlePluginConfig } from '../src'; 8 | import { currentUrlAndMethodIsAllowed } from '../src/currentUrlAndMethodIsAllowed'; 9 | 10 | describe('currentUrlAndMethodIsAllowed', () => { 11 | it('should validate url with method', async () => { 12 | const config: ElysiaAuthDrizzlePluginConfig['config'] = [ 13 | { 14 | url: '/test', 15 | method: 'GET', 16 | }, 17 | ]; 18 | 19 | expect(currentUrlAndMethodIsAllowed('/test', 'GET', config)).toBe(true); 20 | }); 21 | 22 | it('should validate url with slash at the end', async () => { 23 | const config: ElysiaAuthDrizzlePluginConfig['config'] = [ 24 | { 25 | url: '/test', 26 | method: 'GET', 27 | }, 28 | ]; 29 | 30 | expect(currentUrlAndMethodIsAllowed('/test/', 'GET', config)).toBe(true); 31 | }); 32 | 33 | it('should validate url with any at the end', async () => { 34 | const config: ElysiaAuthDrizzlePluginConfig['config'] = [ 35 | { 36 | url: '/test/*', 37 | method: 'GET', 38 | }, 39 | ]; 40 | 41 | expect( 42 | currentUrlAndMethodIsAllowed('/test/ceciestyntest', 'GET', config), 43 | ).toBe(true); 44 | }); 45 | 46 | it('should validate with several param', async () => { 47 | const config: ElysiaAuthDrizzlePluginConfig['config'] = [ 48 | { 49 | url: '/test/:test2/:test3/test', 50 | method: 'GET', 51 | }, 52 | ]; 53 | 54 | expect( 55 | currentUrlAndMethodIsAllowed('/test/:test2/:test3/test', 'GET', config), 56 | ).toBe(true); 57 | 58 | expect( 59 | currentUrlAndMethodIsAllowed('/test/:test2/:test3/wrong', 'GET', config), 60 | ).toBe(false); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/default-answer.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'bun:test'; 2 | 3 | import { app } from './utils/server.test'; 4 | import { cleanToken, generateToken, testRoute } from './utils/utils'; 5 | 6 | let server; 7 | 8 | describe('Default', () => { 9 | it('should return Unauthorized, if url is not in config and no token', async () => { 10 | server = await app(); 11 | 12 | await testRoute( 13 | server, 14 | `/public-success`, 15 | 'GET', 16 | {}, 17 | { 18 | supposedStatus: 401, 19 | supposedMessage: { 20 | error: 'Unauthorized', 21 | context: { error: 'Page is not public' }, 22 | }, 23 | }, 24 | ); 25 | }); 26 | 27 | it('should return page, if url is not in config and token', async () => { 28 | server = await app(); 29 | 30 | const token = await generateToken(); 31 | 32 | await testRoute( 33 | server, 34 | `/not-public-success`, 35 | 'GET', 36 | { 37 | headers: { 38 | authorization: `Bearer ${token.accessToken}`, 39 | }, 40 | }, 41 | { 42 | supposedStatus: 200, 43 | }, 44 | ); 45 | 46 | await cleanToken(token); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/public-private-page.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'bun:test'; 2 | import { eq } from 'drizzle-orm'; 3 | import { sign } from 'jsonwebtoken'; 4 | 5 | import { db } from './utils/db'; 6 | import { tokens } from './utils/schema'; 7 | import { app, expiredTokenValue, userData } from './utils/server.test'; 8 | import { cleanToken, generateToken, testRoute } from './utils/utils'; 9 | 10 | let server; 11 | 12 | describe('Public/Private page', () => { 13 | describe('Public', () => { 14 | it('should be able to access page without token', async () => { 15 | server = await app({ 16 | config: [{ url: '/public-success', method: 'GET' }], 17 | }); 18 | 19 | await testRoute( 20 | server, 21 | `/public-success`, 22 | 'GET', 23 | {}, 24 | { supposedStatus: 200 }, 25 | ); 26 | }); 27 | 28 | it('should be able to access page with token', async () => { 29 | server = await app({ 30 | config: [{ url: '/public-success', method: 'GET' }], 31 | }); 32 | 33 | const token = await generateToken(); 34 | 35 | await testRoute( 36 | server, 37 | `/public-success`, 38 | 'GET', 39 | { 40 | headers: { authorization: `Bearer ${token.accessToken}` }, 41 | }, 42 | { supposedStatus: 200 }, 43 | ); 44 | 45 | await cleanToken(token); 46 | }); 47 | 48 | it('should be able to access page with invalid token', async () => { 49 | server = await app({ 50 | config: [{ url: '/public-success', method: 'GET' }], 51 | }); 52 | 53 | await testRoute( 54 | server, 55 | `/public-success`, 56 | 'GET', 57 | { 58 | headers: { authorization: `Bearer thisisawrongtoken` }, 59 | }, 60 | { supposedStatus: 200 }, 61 | ); 62 | }); 63 | 64 | it('should be able to access page with expired token', async () => { 65 | server = await app({ 66 | config: [{ url: '/public-success', method: 'GET' }], 67 | }); 68 | 69 | const token = await db 70 | .insert(tokens) 71 | .values({ 72 | accessToken: expiredTokenValue, 73 | refreshToken: 'test', 74 | ownerId: userData.id, 75 | }) 76 | .returning(); 77 | 78 | await testRoute( 79 | server, 80 | `/public-success`, 81 | 'GET', 82 | { 83 | headers: { authorization: `Bearer ${expiredTokenValue}` }, 84 | }, 85 | { supposedStatus: 200 }, 86 | ); 87 | 88 | await db.delete(tokens).where(eq(tokens.id, token[0].id)); 89 | }); 90 | }); 91 | 92 | describe('Private', () => { 93 | it('should return unauthorized if no token', async () => { 94 | server = await app(); 95 | 96 | await testRoute( 97 | server, 98 | `/not-public-success`, 99 | 'GET', 100 | {}, 101 | { 102 | supposedStatus: 401, 103 | supposedMessage: { 104 | error: 'Unauthorized', 105 | context: { error: 'Page is not public' }, 106 | }, 107 | }, 108 | ); 109 | }); 110 | 111 | it('should return page if token is valid', async () => { 112 | server = await app(); 113 | 114 | const token = await generateToken(); 115 | 116 | await testRoute( 117 | server, 118 | `/not-public-success`, 119 | 'GET', 120 | { 121 | headers: { 122 | authorization: `Bearer ${token.accessToken}`, 123 | }, 124 | }, 125 | { 126 | supposedStatus: 200, 127 | }, 128 | ); 129 | 130 | await cleanToken(token); 131 | }); 132 | 133 | it('should return page if token is valid and server is in JWT ONLY', async () => { 134 | server = await app({ verifyAccessTokenOnlyInJWT: true }); 135 | 136 | const jwtAccessToken = await sign({ id: userData.id }, 'test', { 137 | expiresIn: '1d', 138 | }); 139 | await testRoute( 140 | server, 141 | `/not-public-success`, 142 | 'GET', 143 | { 144 | headers: { 145 | authorization: `Bearer ${jwtAccessToken}`, 146 | }, 147 | }, 148 | { 149 | supposedStatus: 200, 150 | }, 151 | ); 152 | }); 153 | 154 | it('should not return page if token is valid and server is not in JWT ONLY', async () => { 155 | server = await app(); 156 | 157 | const jwtAccessToken = await sign({ id: userData.id }, 'test', { 158 | expiresIn: '1d', 159 | }); 160 | await testRoute( 161 | server, 162 | `/not-public-success`, 163 | 'GET', 164 | { 165 | headers: { 166 | authorization: `Bearer ${jwtAccessToken}`, 167 | }, 168 | }, 169 | { 170 | supposedStatus: 401, 171 | }, 172 | ); 173 | }); 174 | 175 | it('should return unauthorized token is invalid', async () => { 176 | server = await app(); 177 | 178 | await testRoute( 179 | server, 180 | `/not-public-success`, 181 | 'GET', 182 | { 183 | headers: { 184 | authorization: `Bearer thisIsAWrongToken`, 185 | }, 186 | }, 187 | { 188 | supposedStatus: 401, 189 | supposedMessage: { 190 | error: 'Unauthorized', 191 | context: { error: 'Token is not valid' }, 192 | }, 193 | }, 194 | ); 195 | }); 196 | 197 | it('should return unauthorized token is expired', async () => { 198 | server = await app(); 199 | 200 | const token = await db 201 | .insert(tokens) 202 | .values({ 203 | accessToken: expiredTokenValue, 204 | refreshToken: 'test', 205 | ownerId: userData.id, 206 | }) 207 | .returning(); 208 | 209 | await testRoute( 210 | server, 211 | `/not-public-success`, 212 | 'GET', 213 | { 214 | headers: { 215 | authorization: `Bearer ${expiredTokenValue}`, 216 | }, 217 | }, 218 | { 219 | supposedStatus: 401, 220 | supposedMessage: { 221 | error: 'Unauthorized', 222 | context: { error: 'Token is not valid' }, 223 | }, 224 | }, 225 | ); 226 | 227 | await db.delete(tokens).where(eq(tokens.id, token[0].id)); 228 | }); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /test/user-validation.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test'; 2 | 3 | import { app, userData } from './utils/server.test'; 4 | import { cleanToken, generateToken, testRoute } from './utils/utils'; 5 | 6 | let server; 7 | 8 | describe('User validation', () => { 9 | it('should execute user validation', async () => { 10 | let connectedUserId: string = 'wrongid'; 11 | 12 | server = await app({ 13 | userValidation: async (user) => { 14 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 15 | //@ts-ignore 16 | connectedUserId = user.id; 17 | }, 18 | }); 19 | 20 | const token = await generateToken(); 21 | 22 | await testRoute( 23 | server, 24 | `/not-public-success`, 25 | 'GET', 26 | { 27 | headers: { 28 | authorization: `Bearer ${token.accessToken}`, 29 | }, 30 | }, 31 | { 32 | supposedStatus: 200, 33 | }, 34 | ); 35 | 36 | expect(connectedUserId).toBe(userData.id); 37 | 38 | await cleanToken(token); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'bun:test'; 2 | import { and, eq } from 'drizzle-orm'; 3 | import { sign } from 'jsonwebtoken'; 4 | import { BadRequest, NotFound } from 'unify-errors'; 5 | 6 | import { 7 | createUserToken, 8 | refreshUserToken, 9 | removeAllUserTokens, 10 | removeUserToken, 11 | } from '../src'; 12 | import { db } from './utils/db'; 13 | import { tokens, users } from './utils/schema'; 14 | import { expiredTokenValue, userData } from './utils/server.test'; 15 | 16 | const secret = 'test'; 17 | const refreshSecret = 'testrefresh'; 18 | 19 | describe('Utils function', () => { 20 | describe('createUserToken', () => { 21 | it('should be able to create token for existing user', async () => { 22 | const result = await createUserToken({ 23 | db, 24 | usersSchema: users, 25 | tokensSchema: tokens, 26 | })(userData.id, { 27 | secret, 28 | accessTokenTime: '1d', 29 | refreshTokenTime: '7d', 30 | }); 31 | 32 | const token = await db.query.tokens.findFirst({ 33 | where: and( 34 | eq(tokens.accessToken, result.accessToken), 35 | eq(tokens.refreshToken, result.refreshToken), 36 | ), 37 | }); 38 | 39 | expect(token).toMatchObject({ 40 | accessToken: result.accessToken, 41 | refreshToken: result.refreshToken, 42 | }); 43 | 44 | await db.delete(tokens).where(eq(tokens.id, token!.id)); 45 | }); 46 | 47 | it('should return NotFound if user is not found', async () => { 48 | try { 49 | await createUserToken({ 50 | db, 51 | usersSchema: users, 52 | tokensSchema: tokens, 53 | })('wrongid', { 54 | secret, 55 | accessTokenTime: '1d', 56 | refreshTokenTime: '7d', 57 | }); 58 | } catch (error) { 59 | expect(error).toStrictEqual(new NotFound({ error: 'User not found' })); 60 | } 61 | }); 62 | }); 63 | 64 | describe('removeUserToken', () => { 65 | it('should remove token ', async () => { 66 | const result = await createUserToken({ 67 | db, 68 | usersSchema: users, 69 | tokensSchema: tokens, 70 | })(userData.id, { 71 | secret, 72 | accessTokenTime: '1d', 73 | refreshTokenTime: '7d', 74 | }); 75 | 76 | await removeUserToken({ 77 | db, 78 | tokensSchema: tokens, 79 | })(result.accessToken); 80 | 81 | const token = await db.query.tokens.findFirst({ 82 | where: and( 83 | eq(tokens.accessToken, result.accessToken), 84 | eq(tokens.refreshToken, result.refreshToken), 85 | ), 86 | }); 87 | 88 | expect(token as undefined).toBe(undefined); 89 | }); 90 | }); 91 | 92 | describe('removeAllUserTokens', () => { 93 | it('should remove token ', async () => { 94 | const result = await createUserToken({ 95 | db, 96 | usersSchema: users, 97 | tokensSchema: tokens, 98 | })(userData.id, { 99 | secret, 100 | accessTokenTime: '1d', 101 | refreshTokenTime: '7d', 102 | }); 103 | 104 | await removeAllUserTokens({ 105 | db, 106 | tokensSchema: tokens, 107 | })(userData.id); 108 | 109 | const token = await db.query.tokens.findFirst({ 110 | where: and( 111 | eq(tokens.accessToken, result.accessToken), 112 | eq(tokens.refreshToken, result.refreshToken), 113 | ), 114 | }); 115 | 116 | expect(token as undefined).toBe(undefined); 117 | }); 118 | }); 119 | 120 | describe('refreshUserToken', () => { 121 | it('should be able to refresh user token ', async () => { 122 | const result = await createUserToken({ 123 | db, 124 | usersSchema: users, 125 | tokensSchema: tokens, 126 | })(userData.id, { 127 | secret, 128 | refreshSecret, 129 | accessTokenTime: '1d', 130 | refreshTokenTime: '7d', 131 | }); 132 | 133 | const resultRefresh = await refreshUserToken({ 134 | db, 135 | tokensSchema: tokens, 136 | })(result.refreshToken, { 137 | secret, 138 | refreshSecret, 139 | accessTokenTime: '2d', 140 | }); 141 | 142 | expect(resultRefresh?.accessToken).not.toBe(result.accessToken); 143 | }); 144 | 145 | it('should return a Bad request is token is expired ', async () => { 146 | try { 147 | await refreshUserToken({ 148 | db, 149 | tokensSchema: tokens, 150 | })(expiredTokenValue, { 151 | secret, 152 | accessTokenTime: '1d', 153 | }); 154 | } catch (error) { 155 | expect(error).toStrictEqual(new BadRequest({ error: 'Token expired' })); 156 | } 157 | }); 158 | 159 | it("should return a Not Found if token doesn't exist", async () => { 160 | try { 161 | await refreshUserToken({ 162 | db, 163 | tokensSchema: tokens, 164 | })(sign({ userId: 'test' }, secret), { 165 | secret, 166 | accessTokenTime: '1d', 167 | }); 168 | } catch (error) { 169 | expect(error).toStrictEqual(new NotFound({ error: 'Token not found' })); 170 | } 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /test/utils/db.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/postgres-js'; 2 | import postgres from 'postgres'; 3 | 4 | import * as schema from './schema'; 5 | 6 | export const connection = postgres( 7 | 'postgres://docker:docker@127.0.0.1:5432/project', 8 | ); 9 | 10 | export const db = drizzle(connection, { schema }); 11 | -------------------------------------------------------------------------------- /test/utils/migrate.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from 'drizzle-orm/postgres-js/migrator'; 2 | 3 | import { connection, db } from './db'; 4 | 5 | (async () => { 6 | // This will run migrations on the database, skipping the ones already applied 7 | await migrate(db, { migrationsFolder: './drizzle' }); 8 | 9 | // Don't forget to close the connection, otherwise the script will hang 10 | await connection.end(); 11 | process.exit(0); 12 | })(); 13 | -------------------------------------------------------------------------------- /test/utils/schema.ts: -------------------------------------------------------------------------------- 1 | import { relations } from 'drizzle-orm'; 2 | import { index, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; 3 | 4 | /** 5 | * Users 6 | */ 7 | 8 | export const users = pgTable( 9 | 'users', 10 | { 11 | id: uuid('id').primaryKey().notNull().defaultRandom(), 12 | 13 | createdAt: timestamp('created_at').notNull().defaultNow(), 14 | updatedAt: timestamp('updated_at') 15 | .notNull() 16 | .defaultNow() 17 | .$onUpdate(() => { 18 | return new Date(); 19 | }), 20 | }, 21 | (users) => ({ 22 | idIdx: index('user_id_idx').on(users.id), 23 | }), 24 | ); 25 | 26 | export const usersRelations = relations(users, ({ many }) => ({ 27 | tokens: many(tokens), 28 | })); 29 | 30 | /** 31 | * Tokens 32 | */ 33 | 34 | export const tokens = pgTable( 35 | 'tokens', 36 | { 37 | id: uuid('id').notNull().defaultRandom(), 38 | 39 | ownerId: uuid('owner_id').notNull(), 40 | 41 | accessToken: text('access_token').notNull(), 42 | refreshToken: text('refresh_token').notNull(), 43 | 44 | createdAt: timestamp('created_at').notNull().defaultNow(), 45 | }, 46 | (tokens) => ({ 47 | idIdx: index('tokens_id_idx').on(tokens.id), 48 | }), 49 | ); 50 | 51 | export const tokensRelations = relations(tokens, ({ one }) => ({ 52 | owner: one(users, { 53 | fields: [tokens.ownerId], 54 | references: [users.id], 55 | }), 56 | })); 57 | -------------------------------------------------------------------------------- /test/utils/server.test.ts: -------------------------------------------------------------------------------- 1 | import { Elysia } from 'elysia'; 2 | import { pluginUnifyElysia } from 'unify-elysia'; 3 | 4 | import { 5 | elysiaAuthDrizzlePlugin, 6 | ElysiaAuthDrizzlePluginConfig, 7 | } from '../../src'; 8 | import { HTTPMethods } from '../../src/type'; 9 | import { db } from './db'; 10 | import { tokens, users } from './schema'; 11 | 12 | export const userData = { 13 | id: '04319e04-08d4-452f-9a46-9c1f9e79e2f0', 14 | }; 15 | 16 | export const methods = [ 17 | 'DELETE', 18 | 'GET', 19 | 'HEAD', 20 | 'PATCH', 21 | 'POST', 22 | 'PUT', 23 | 'OPTIONS', 24 | 'PROPFIND', 25 | 'PROPPATCH', 26 | 'MKCOL', 27 | 'COPY', 28 | 'MOVE', 29 | 'LOCK', 30 | 'UNLOCK', 31 | 'TRACE', 32 | 'SEARCH', 33 | ] as HTTPMethods[]; 34 | 35 | export const expiredTokenValue = 36 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjA0MzE5ZTA0LTA4ZDQtNDUyZi05YTQ2LTljMWY5ZTc5ZTJmMCIsImlhdCI6MTY2MDgyNDg2NSwiZXhwIjoxNjYwODI0ODY2fQ.aZOpXfb-1l-TlYzlaMBo-00J99I_NTP4ELuXpSgS6Lg'; 37 | 38 | export const defaultSuccess = () => { 39 | return { success: true }; 40 | }; 41 | 42 | export const app = (config?: Partial>) => { 43 | const server = new Elysia() 44 | .use(pluginUnifyElysia({})) 45 | .use( 46 | elysiaAuthDrizzlePlugin({ 47 | config: [ 48 | { 49 | url: '/public', 50 | method: 'GET', 51 | }, 52 | ], 53 | jwtSecret: 'test', 54 | drizzle: { 55 | db: db, 56 | usersSchema: users, 57 | tokensSchema: tokens, 58 | }, 59 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 60 | //@ts-ignore 61 | userValidation: (user) => { 62 | user; 63 | }, 64 | ...config, 65 | }), 66 | ) 67 | .get('/public-success', defaultSuccess) 68 | .get('/public-success/:variable1/:variable2/:variable3/get', defaultSuccess) 69 | .get('/not-public-success', defaultSuccess) 70 | .get('/get-req-user', ({ connectedUser }) => { 71 | return connectedUser; 72 | }) 73 | .get('/get-req-isConnected', ({ isConnected }) => { 74 | return { isConnected }; 75 | }); 76 | 77 | for (const method of methods) { 78 | server.route(method, `/public-${method.toLowerCase()}`, defaultSuccess); 79 | } 80 | 81 | return server; 82 | }; 83 | -------------------------------------------------------------------------------- /test/utils/server.ts: -------------------------------------------------------------------------------- 1 | import { app } from './server.test'; 2 | 3 | app().listen(3000, () => console.log('Server started on port 3000')); 4 | -------------------------------------------------------------------------------- /test/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | import { expect } from 'bun:test'; 4 | import { eq } from 'drizzle-orm'; 5 | import { sign } from 'jsonwebtoken'; 6 | 7 | import { HTTPMethods } from '../../src/type'; 8 | import { db } from './db'; 9 | import { tokens } from './schema'; 10 | import { userData } from './server.test'; 11 | 12 | export const generateToken = async () => { 13 | const jwtAccessToken = await sign({ id: userData.id }, 'test', { 14 | expiresIn: '1d', 15 | }); 16 | 17 | const jwtRefreshToken = await sign({ id: userData.id }, 'test', { 18 | expiresIn: '1d', 19 | }); 20 | 21 | return await db 22 | .insert(tokens) 23 | .values({ 24 | accessToken: jwtAccessToken, 25 | refreshToken: jwtRefreshToken, 26 | ownerId: userData.id, 27 | }) 28 | .returning() 29 | .then((res) => res[0]); 30 | }; 31 | 32 | export const cleanToken = async (token: typeof tokens.$inferSelect) => { 33 | await db.delete(tokens).where(eq(tokens.id, token.id)); 34 | }; 35 | 36 | export const testRoute = async ( 37 | server: any, 38 | routePath: string, 39 | method: HTTPMethods, 40 | data: { 41 | headers?: Record; 42 | cookies?: Record; 43 | }, 44 | validation: { 45 | supposedStatus: number; 46 | supposedMessage?: string | Record; 47 | }, 48 | ): Promise => { 49 | let status: number; 50 | let content: Record | string | undefined | null; 51 | let json = false; 52 | 53 | await server 54 | .handle( 55 | new Request(`http://localhost${routePath}`, { 56 | headers: data?.headers || {}, 57 | method: method, 58 | }), 59 | ) 60 | .then( 61 | async (res: { 62 | status: number; 63 | json: () => 64 | | string 65 | | Record 66 | | PromiseLike | null | undefined> 67 | | null 68 | | undefined; 69 | text: () => 70 | | string 71 | | Record 72 | | PromiseLike | null | undefined> 73 | | null 74 | | undefined; 75 | }) => { 76 | status = res.status; 77 | 78 | try { 79 | content = await res.json(); 80 | json = true; 81 | return; 82 | // eslint-disable-next-line no-empty 83 | } catch (error) {} 84 | 85 | try { 86 | content = await res.text(); 87 | return; 88 | // eslint-disable-next-line no-empty 89 | } catch (error) {} 90 | 91 | return; 92 | }, 93 | ); 94 | 95 | if (validation.supposedStatus) { 96 | expect(status!).toBe(validation.supposedStatus); 97 | } 98 | 99 | if (validation.supposedMessage) { 100 | if (json) { 101 | //@ts-ignore 102 | expect(content).toMatchObject(validation.supposedMessage); 103 | } else { 104 | //@ts-ignore 105 | expect(content).toEqual(validation.supposedMessage); 106 | } 107 | } 108 | 109 | return; 110 | }; 111 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "ES2022", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | 78 | /* Type Checking */ 79 | "strict": true, /* Enable all strict type-checking options. */ 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | splitting: false, 6 | sourcemap: false, 7 | clean: true, 8 | dts: true, 9 | treeshake: true, 10 | format: ['cjs', 'esm'], 11 | }); 12 | --------------------------------------------------------------------------------