WIP: auth user creation

This commit is contained in:
ian
2025-11-28 15:45:51 +07:00
parent a0687d26cc
commit 9413c442df
8 changed files with 95 additions and 13 deletions
@@ -0,0 +1,16 @@
CREATE TYPE "public"."role" AS ENUM('admin', 'visitor', 'user', 'volunteer');--> statement-breakpoint
CREATE TABLE "users" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(255) NOT NULL,
"email" varchar(255),
"phone" varchar(255),
"password" varchar(127),
"image" text,
"google_id" varchar(255),
"role" "role" DEFAULT 'user' NOT NULL,
"status" boolean DEFAULT true NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone NOT NULL,
"deleted_at" timestamp with time zone,
CONSTRAINT "users_email_unique" UNIQUE("email")
);
@@ -0,0 +1,127 @@
{
"id": "7f01d0a9-3f79-476c-b885-b83b3191bdf8",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"phone": {
"name": "phone",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "varchar(127)",
"primaryKey": false,
"notNull": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"google_id": {
"name": "google_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "role",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'user'"
},
"status": {
"name": "status",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {
"public.role": {
"name": "role",
"schema": "public",
"values": [
"admin",
"visitor",
"user",
"volunteer"
]
}
},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1764313178750,
"tag": "1764313178_flimsy_tarantula",
"breakpoints": true
}
]
}
+5 -1
View File
@@ -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<T = unknown>(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.
+33
View File
@@ -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')
// }
},
})