From 9413c442dfaa76969a9a7bcb912e9b97ff7f6427 Mon Sep 17 00:00:00 2001 From: Ian Mustafa Date: Fri, 28 Nov 2025 15:45:51 +0700 Subject: [PATCH] WIP: auth user creation --- bun.lock | 47 ++++++++++++++++++- drizzle.config.ts | 2 +- package.json | 2 + .../1764313178_flimsy_tarantula.sql | 4 +- .../migrations/meta/1764313178_snapshot.json | 10 ++-- .../db/migrations}/meta/_journal.json | 4 +- src/helpers/signature.ts | 6 ++- src/middlewares/apiKeyAuth.ts | 33 +++++++++++++ 8 files changed, 95 insertions(+), 13 deletions(-) rename drizzle/1759370311_heavy_gressill.sql => src/db/migrations/1764313178_flimsy_tarantula.sql (76%) rename drizzle/meta/1759370311_snapshot.json => src/db/migrations/meta/1764313178_snapshot.json (94%) rename {drizzle => src/db/migrations}/meta/_journal.json (66%) create mode 100644 src/middlewares/apiKeyAuth.ts diff --git a/bun.lock b/bun.lock index 4a32064..d0f5f4d 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "backend-node-mbg", @@ -35,7 +36,9 @@ "devDependencies": { "@eslint/js": "^9.35.0", "@stylistic/eslint-plugin": "^5.3.1", + "@types/bun": "^1.3.3", "@types/lodash-es": "^4.17.12", + "@types/node": "^24.10.1", "bun-types": "latest", "eslint": "^9.35.0", "globals": "^16.4.0", @@ -539,6 +542,8 @@ "@types/aws-lambda": ["@types/aws-lambda@8.10.152", "", {}, "sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw=="], + "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], + "@types/bunyan": ["@types/bunyan@1.8.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], @@ -555,7 +560,7 @@ "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], "@types/oracledb": ["@types/oracledb@6.5.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ=="], @@ -1047,7 +1052,7 @@ "uint8array-extras": ["uint8array-extras@1.4.1", "", {}, "sha512-+NWHrac9dvilNgme+gP4YrBSumsaMZP0fNBtXXFIf33RLLKEcBUKaQZ7ULUbS0sBfcjxIZ4V96OTRkCbM7hxpw=="], - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -1191,12 +1196,30 @@ "@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], + "@types/bun/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + + "@types/bunyan/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/connect/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/memcached/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/mysql/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/oracledb/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/pg/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/tedious/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], + "bun-types/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "eslint/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1205,6 +1228,8 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "protobufjs/@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + "protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -1301,12 +1326,30 @@ "@scalar/themes/@scalar/types/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@types/bunyan/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/connect/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/memcached/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/mysql/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/oracledb/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/pg/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@types/tedious/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "eslint/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "protobufjs/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], diff --git a/drizzle.config.ts b/drizzle.config.ts index ea6d2de..bc49f8c 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -3,7 +3,7 @@ import { dbDsn } from '~/config' export default defineConfig({ schema: './src/db/schema/*', - out: './drizzle', + out: './src/db/migrations', dialect: 'postgresql', migrations: { prefix: 'unix', diff --git a/package.json b/package.json index 14371db..63b70f2 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,9 @@ "devDependencies": { "@eslint/js": "^9.35.0", "@stylistic/eslint-plugin": "^5.3.1", + "@types/bun": "^1.3.3", "@types/lodash-es": "^4.17.12", + "@types/node": "^24.10.1", "bun-types": "latest", "eslint": "^9.35.0", "globals": "^16.4.0", diff --git a/drizzle/1759370311_heavy_gressill.sql b/src/db/migrations/1764313178_flimsy_tarantula.sql similarity index 76% rename from drizzle/1759370311_heavy_gressill.sql rename to src/db/migrations/1764313178_flimsy_tarantula.sql index 2ee544d..296a37a 100644 --- a/drizzle/1759370311_heavy_gressill.sql +++ b/src/db/migrations/1764313178_flimsy_tarantula.sql @@ -1,6 +1,6 @@ -CREATE TYPE "public"."role" AS ENUM('admin', 'visitor', 'user');--> statement-breakpoint +CREATE TYPE "public"."role" AS ENUM('admin', 'visitor', 'user', 'volunteer');--> statement-breakpoint CREATE TABLE "users" ( - "id" "ulid" PRIMARY KEY DEFAULT gen_ulid() NOT NULL, + "id" serial PRIMARY KEY NOT NULL, "name" varchar(255) NOT NULL, "email" varchar(255), "phone" varchar(255), diff --git a/drizzle/meta/1759370311_snapshot.json b/src/db/migrations/meta/1764313178_snapshot.json similarity index 94% rename from drizzle/meta/1759370311_snapshot.json rename to src/db/migrations/meta/1764313178_snapshot.json index a75119f..eddbc89 100644 --- a/drizzle/meta/1759370311_snapshot.json +++ b/src/db/migrations/meta/1764313178_snapshot.json @@ -1,5 +1,5 @@ { - "id": "16065bc5-5f72-44e1-9a45-2f939dd8866a", + "id": "7f01d0a9-3f79-476c-b885-b83b3191bdf8", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -10,10 +10,9 @@ "columns": { "id": { "name": "id", - "type": "ulid", + "type": "serial", "primaryKey": true, - "notNull": true, - "default": "gen_ulid()" + "notNull": true }, "name": { "name": "name", @@ -110,7 +109,8 @@ "values": [ "admin", "visitor", - "user" + "user", + "volunteer" ] } }, diff --git a/drizzle/meta/_journal.json b/src/db/migrations/meta/_journal.json similarity index 66% rename from drizzle/meta/_journal.json rename to src/db/migrations/meta/_journal.json index 28c5ffb..aa0bf7b 100644 --- a/drizzle/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1759370311281, - "tag": "1759370311_heavy_gressill", + "when": 1764313178750, + "tag": "1764313178_flimsy_tarantula", "breakpoints": true } ] diff --git a/src/helpers/signature.ts b/src/helpers/signature.ts index 4f1981d..3981bca 100644 --- a/src/helpers/signature.ts +++ b/src/helpers/signature.ts @@ -1,4 +1,4 @@ -import { createHmac, timingSafeEqual } from 'crypto' +import { createHmac, timingSafeEqual } from 'node:crypto' /** * Canonicalizes an object by recursively removing 'signature' properties, @@ -8,6 +8,7 @@ import { createHmac, timingSafeEqual } from 'crypto' * converting Dates to ISO strings, BigInt to strings, * and non-finite numbers (NaN/Infinity/-Infinity) to null. * Throws on circular references or unsupported objects. + * * @param data The data to canonicalize. * @returns The canonicalized, signature-free structure. */ @@ -96,6 +97,7 @@ export function canonicalizeObject(data: T): unknown { /** * Returns the canonical JSON string of the data. + * * @param data The data to canonicalize. * @returns The canonical JSON string. */ @@ -105,6 +107,7 @@ export function canonicalize(data: unknown): string { /** * Signs the data using HMAC-SHA256 and returns the signature as lowercase hex. + * * @param data The data to sign. * @param secretKey The secret key. If not provided, uses process.env.SECRET_KEY. * @returns The signature. @@ -125,6 +128,7 @@ export function sign(data: unknown, secretKey?: string): string { /** * Verifies the signature of the data using HMAC-SHA256 with constant-time comparison. + * * @param data The data to verify. * @param signature The expected signature. * @param secretKey The secret key. If not provided, uses process.env.SECRET_KEY. diff --git a/src/middlewares/apiKeyAuth.ts b/src/middlewares/apiKeyAuth.ts new file mode 100644 index 0000000..85e7614 --- /dev/null +++ b/src/middlewares/apiKeyAuth.ts @@ -0,0 +1,33 @@ +// Clock skew window: ±5 minutes +// const CLOCK_SKEW_MS = 5 * 60 * 1000 + +export const deviceGuard = () => ({ + beforeHandle(context: unknown) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { headers, set } = context as any + // Validate X-API-Key + const apiKey = headers['x-api-key'] + if (!apiKey || typeof apiKey !== 'string' || apiKey.trim() === '') { + set.status = 401 + throw new Error('invalid_api_key') + } + + // Validate X-Timestamp + // const timestampStr = headers['x-timestamp'] + // if (!timestampStr) { + // set.status = 403 + // throw new Error('replay_attack_detected') + // } + // const timestamp = new Date(timestampStr) + // if (isNaN(timestamp.getTime()) || timestampStr !== timestamp.toISOString()) { + // set.status = 403 + // throw new Error('replay_attack_detected') + // } + // const now = new Date() + // const diff = Math.abs(now.getTime() - timestamp.getTime()) + // if (diff > CLOCK_SKEW_MS) { + // set.status = 403 + // throw new Error('replay_attack_detected') + // } + }, +})