@estopia/shared 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/broker/eventTypes.d.ts +27 -0
  2. package/dist/broker/eventTypes.d.ts.map +1 -0
  3. package/dist/broker/eventTypes.js +9 -0
  4. package/dist/broker/eventTypes.js.map +1 -0
  5. package/dist/broker/publisher.d.ts +16 -0
  6. package/dist/broker/publisher.d.ts.map +1 -0
  7. package/dist/broker/publisher.js +124 -0
  8. package/dist/broker/publisher.js.map +1 -0
  9. package/dist/broker/subscriber.d.ts +14 -0
  10. package/dist/broker/subscriber.d.ts.map +1 -0
  11. package/dist/broker/subscriber.js +60 -0
  12. package/dist/broker/subscriber.js.map +1 -0
  13. package/dist/database/cockroach.d.ts +11 -0
  14. package/dist/database/cockroach.d.ts.map +1 -0
  15. package/dist/database/cockroach.js +27 -0
  16. package/dist/database/cockroach.js.map +1 -0
  17. package/dist/database/dataTypes.d.ts +99 -0
  18. package/dist/database/dataTypes.d.ts.map +1 -0
  19. package/dist/database/dataTypes.js +7 -0
  20. package/dist/database/dataTypes.js.map +1 -0
  21. package/{src/database/types.ts → dist/database/types.d.ts} +8 -9
  22. package/dist/database/types.d.ts.map +1 -0
  23. package/dist/database/types.js +3 -0
  24. package/dist/database/types.js.map +1 -0
  25. package/dist/helpers/middlware/auth.d.ts +10 -0
  26. package/dist/helpers/middlware/auth.d.ts.map +1 -0
  27. package/dist/helpers/middlware/auth.js +55 -0
  28. package/dist/helpers/middlware/auth.js.map +1 -0
  29. package/dist/helpers/middlware/secure.d.ts +11 -0
  30. package/dist/helpers/middlware/secure.d.ts.map +1 -0
  31. package/dist/helpers/middlware/secure.js +42 -0
  32. package/dist/helpers/middlware/secure.js.map +1 -0
  33. package/dist/helpers/validateJWTToken.d.ts +16 -0
  34. package/dist/helpers/validateJWTToken.d.ts.map +1 -0
  35. package/dist/helpers/validateJWTToken.js +36 -0
  36. package/dist/helpers/validateJWTToken.js.map +1 -0
  37. package/{src/index.ts → dist/index.d.ts} +16 -24
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +24 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/logging/logger.d.ts +13 -0
  42. package/dist/logging/logger.d.ts.map +1 -0
  43. package/dist/logging/logger.js +81 -0
  44. package/dist/logging/logger.js.map +1 -0
  45. package/dist/types/ENUMS.d.ts +32 -0
  46. package/dist/types/ENUMS.d.ts.map +1 -0
  47. package/dist/types/ENUMS.js +41 -0
  48. package/dist/types/ENUMS.js.map +1 -0
  49. package/dist/types/directs.d.ts +16 -0
  50. package/dist/types/directs.d.ts.map +1 -0
  51. package/dist/types/directs.js +3 -0
  52. package/dist/types/directs.js.map +1 -0
  53. package/dist/types/medical.d.ts +32 -0
  54. package/dist/types/medical.d.ts.map +1 -0
  55. package/dist/types/medical.js +3 -0
  56. package/dist/types/medical.js.map +1 -0
  57. package/dist/types/messages.d.ts +24 -0
  58. package/dist/types/messages.d.ts.map +1 -0
  59. package/dist/types/messages.js +3 -0
  60. package/dist/types/messages.js.map +1 -0
  61. package/{src/types/request.ts → dist/types/request.d.ts} +22 -23
  62. package/dist/types/request.d.ts.map +1 -0
  63. package/dist/types/request.js +3 -0
  64. package/dist/types/request.js.map +1 -0
  65. package/dist/types/servers.d.ts +32 -0
  66. package/dist/types/servers.d.ts.map +1 -0
  67. package/dist/types/servers.js +3 -0
  68. package/dist/types/servers.js.map +1 -0
  69. package/dist/types/servicePayloads.d.ts +32 -0
  70. package/dist/types/servicePayloads.d.ts.map +1 -0
  71. package/dist/types/servicePayloads.js +10 -0
  72. package/dist/types/servicePayloads.js.map +1 -0
  73. package/dist/types/ticketsAndReporting.d.ts +29 -0
  74. package/dist/types/ticketsAndReporting.d.ts.map +1 -0
  75. package/dist/types/ticketsAndReporting.js +3 -0
  76. package/dist/types/ticketsAndReporting.js.map +1 -0
  77. package/dist/types/user.d.ts +29 -0
  78. package/dist/types/user.d.ts.map +1 -0
  79. package/dist/types/user.js +3 -0
  80. package/dist/types/user.js.map +1 -0
  81. package/package.json +10 -4
  82. package/.github/workflows/tests.yml +0 -29
  83. package/scripts/migrations/1670000000000-initial.js +0 -107
  84. package/scripts/package.json +0 -20
  85. package/src/broker/eventTypes.ts +0 -23
  86. package/src/broker/publisher.ts +0 -82
  87. package/src/broker/subscriber.ts +0 -51
  88. package/src/database/cockroach.ts +0 -27
  89. package/src/helpers/middlware/auth.ts +0 -55
  90. package/src/helpers/middlware/secure.ts +0 -48
  91. package/src/helpers/validateJWTToken.ts +0 -38
  92. package/src/logging/logger.ts +0 -103
  93. package/src/types/ENUMS.ts +0 -45
  94. package/src/types/directs.ts +0 -18
  95. package/src/types/medical.ts +0 -36
  96. package/src/types/messages.ts +0 -27
  97. package/src/types/servers.ts +0 -36
  98. package/src/types/servicePayloads.ts +0 -40
  99. package/src/types/ticketsAndReporting.ts +0 -32
  100. package/src/types/user.ts +0 -34
  101. package/tests/verifyJWTToken.test.ts +0 -27
  102. package/tsconfig.json +0 -48
  103. package/vitest.config.ts +0 -9
@@ -0,0 +1,29 @@
1
+ export interface UserDto {
2
+ id: string;
3
+ username: string;
4
+ password: string | null;
5
+ icon: string;
6
+ isAdmin: boolean;
7
+ }
8
+ export interface PasskeyDto {
9
+ id: string;
10
+ name: string;
11
+ transports: number;
12
+ }
13
+ export interface AuthAppDto {
14
+ id: string;
15
+ name: string;
16
+ }
17
+ export interface NotificationSettingsDto {
18
+ id: string;
19
+ userId: number;
20
+ directMessage: boolean;
21
+ groupDMMessage: boolean;
22
+ groupDMMention: boolean;
23
+ addedToDM: boolean;
24
+ serverEveryoneMention: boolean;
25
+ serverMention: boolean;
26
+ serverReply: boolean;
27
+ directCall: boolean;
28
+ }
29
+ //# sourceMappingURL=user.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/types/user.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CAEpB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CAEd;AAED,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;CACrB"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=user.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user.js","sourceRoot":"","sources":["../../src/types/user.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@estopia/shared",
3
- "version": "1.0.0",
4
- "main": "src/index.ts",
5
- "types": "src/index.d.ts",
3
+ "version": "1.0.2",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
6
  "description": "",
7
7
  "author": "",
8
8
  "license": "ISC",
9
9
  "scripts": {
10
- "build": "tsc -p tsconfig.json",
10
+ "build": "tsc -p tsconfig.json",
11
+ "build:dist": "tsc -p tsconfig.build.json",
12
+ "prepare": "npm run build:dist",
11
13
  "test": "vitest",
12
14
  "test:watch": "vitest --watch",
13
15
  "db:types": "kysely-codegen --out-file ./src/database/dataTypes.ts"
@@ -23,6 +25,10 @@
23
25
  "pino": "^10.0.0",
24
26
  "vitest": "^3.2.4"
25
27
  },
28
+ "files": [
29
+ "dist",
30
+ "src/database/dataTypes.ts"
31
+ ],
26
32
  "devDependencies": {
27
33
  "@types/amqplib": "^0.10.7",
28
34
  "@types/express": "^5.0.3",
@@ -1,29 +0,0 @@
1
- name: Tests
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - uses: actions/checkout@v3
15
-
16
- - name: Use Node.js
17
- uses: actions/setup-node@v3
18
- with:
19
- node-version: '18'
20
-
21
- - name: Install package dependencies
22
- run: |
23
- npm install
24
- shell: bash
25
-
26
- - name: Run tests
27
- run: |
28
- npm test
29
-
@@ -1,107 +0,0 @@
1
- /* eslint-disable no-unused-vars */
2
- 'use strict';
3
-
4
- exports.up = (pgm) => {
5
- // 1. Create Custom Enum Types
6
- pgm.createType('verification_type', ['email', 'phone', 'password_reset']);
7
- pgm.createType('factor_auth_type', ['whatsapp', 'email']);
8
- pgm.createType('app_type', ['medIOS', 'medAndroid', 'web']);
9
-
10
- // 2. Create Core User Table
11
- pgm.createTable('users', {
12
- id: 'id',
13
- username: { type: 'varchar(50)', notNull: true, unique: true },
14
- password_hash: { type: 'text', notNull: true },
15
- disabled: { type: 'boolean', default: false },
16
- is_admin: { type: 'boolean', default: false },
17
- icon_url: { type: 'text' },
18
- created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
19
- });
20
-
21
- // 3. Create Related Authentication & Settings Tables
22
- pgm.createTable('notification_settings', {
23
- id: 'id',
24
- user_id: { type: 'integer', notNull: true, references: 'users(id)', onDelete: 'CASCADE', unique: true },
25
- direct_message: { type: 'boolean', default: true },
26
- group_dm_message: { type: 'boolean', default: true },
27
- group_dm_mention: { type: 'boolean', default: true },
28
- added_to_dm: { type: 'boolean', default: true },
29
- server_everyone_mention: { type: 'boolean', default: true },
30
- server_mention: { type: 'boolean', default: true },
31
- server_reply: { type: 'boolean', default: true },
32
- direct_call: { type: 'boolean', default: true },
33
- });
34
-
35
- pgm.createTable('user_verifications', {
36
- id: 'id',
37
- user_id: { type: 'integer', notNull: true, references: 'users(id)', onDelete: 'CASCADE' },
38
- ownership_code: { type: 'text', notNull: true },
39
- is_verified: { type: 'boolean', default: false },
40
- type: { type: 'verification_type', notNull: true },
41
- data: { type: 'text' },
42
- expiry_at: { type: 'timestamptz', notNull: true },
43
- created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
44
- });
45
-
46
- pgm.createTable('passkeys', {
47
- id: 'id',
48
- user_id: { type: 'integer', notNull: true, references: 'users(id)', onDelete: 'CASCADE' },
49
- key_id: { type: 'bytea', notNull: true, unique: true },
50
- public_key: { type: 'bytea', notNull: true },
51
- counter: { type: 'bigint', notNull: true },
52
- transports: { type: 'varchar(50)[]' },
53
- name: { type: 'text' },
54
- });
55
-
56
- pgm.createTable('auth_apps', {
57
- id: 'id',
58
- user_id: { type: 'integer', notNull: true, references: 'users(id)', onDelete: 'CASCADE' },
59
- secret: { type: 'text', notNull: true },
60
- name: { type: 'varchar(100)', notNull: true },
61
- });
62
-
63
- pgm.createTable('factor_authentication', {
64
- id: 'id',
65
- user_id: { type: 'integer', notNull: true, references: 'users(id)', onDelete: 'CASCADE' },
66
- type: { type: 'factor_auth_type', notNull: true },
67
- data: { type: 'text', notNull: true },
68
- code: { type: 'text', notNull: true },
69
- is_verified: { type: 'boolean', default: false },
70
- expiry_at: { type: 'timestamptz', notNull: true },
71
- });
72
-
73
- pgm.createTable('tokens', {
74
- id: 'id',
75
- user_id: { type: 'integer', notNull: true, references: 'users(id)', onDelete: 'CASCADE' },
76
- refresh_token: { type: 'uuid', notNull: true, unique: true },
77
- fcm_token: { type: 'text' },
78
- app: { type: 'app_type', notNull: false },
79
- expiry_at: { type: 'timestamptz', notNull: true },
80
- created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
81
- });
82
-
83
- pgm.createTable('secure_tokens', {
84
- id: 'id',
85
- user_id: { type: 'integer', notNull: true, references: 'users(id)', onDelete: 'CASCADE' },
86
- token_uuid: { type: 'uuid', notNull: true, unique: true },
87
- expiry_at: { type: 'timestamptz', notNull: true },
88
- created_at: { type: 'timestamptz', notNull: true, default: pgm.func('now()') },
89
- });
90
- };
91
-
92
- exports.down = (pgm) => {
93
- // Drop tables in reverse order of creation due to dependencies
94
- pgm.dropTable('secure_tokens');
95
- pgm.dropTable('tokens');
96
- pgm.dropTable('factor_authentication');
97
- pgm.dropTable('auth_apps');
98
- pgm.dropTable('passkeys');
99
- pgm.dropTable('user_verifications');
100
- pgm.dropTable('notification_settings');
101
- pgm.dropTable('users');
102
-
103
- // Drop types
104
- pgm.dropType('app_type');
105
- pgm.dropType('factor_auth_type');
106
- pgm.dropType('verification_type');
107
- };
@@ -1,20 +0,0 @@
1
- {
2
- "name": "@estopia/scripts",
3
- "version": "1.0.0",
4
- "main": "index.ts",
5
- "types": "src/index.d.ts",
6
- "description": "",
7
- "author": "",
8
- "license": "ISC",
9
- "scripts": {
10
- "migrate:up": "dotenv -e .env -- node-pg-migrate up -u \"$DATABASE_URL\"",
11
- "migrate:down": "dotenv -e .env -- node-pg-migrate down -u \"$DATABASE_URL\""
12
- },
13
- "dependencies": {
14
- "dotenv": "^17.2.3",
15
- "node-pg-migrate": "^8.0.3"
16
- },
17
- "devDependencies": {
18
- "dotenv-cli": "^10.0.0"
19
- }
20
- }
@@ -1,23 +0,0 @@
1
- export interface UserRegisteredEvent {
2
- userId: string;
3
- email: string;
4
- verificationCode: string;
5
- }
6
-
7
- export interface PasswordResetEvent {
8
- userId: string;
9
- email: string;
10
- resetToken: string;
11
- }
12
-
13
- export type EventPayloads = UserRegisteredEvent | PasswordResetEvent;
14
-
15
- export interface EventMap {
16
- USER_REGISTERED: UserRegisteredEvent;
17
- PASSWORD_RESET: PasswordResetEvent;
18
- }
19
-
20
- export const EVENTS = {
21
- USER_REGISTERED: 'user.registered',
22
- PASSWORD_RESET: 'password.reset',
23
- } as const;
@@ -1,82 +0,0 @@
1
- import * as amqplib from 'amqplib';
2
- import { EventPayloads, EventMap, EVENTS } from './eventTypes';
3
-
4
- export class EventPublisher {
5
- private connection?: any;
6
- private channel?: any;
7
- private exchange = 'estopia.events';
8
-
9
- constructor(private url: string) {}
10
-
11
- async connect() {
12
- if (this.connection && this.channel) return; // already connected
13
- this.connection = await amqplib.connect(this.url);
14
- this.channel = await this.connection.createChannel();
15
- await this.channel.assertExchange(this.exchange, 'topic', { durable: true });
16
- }
17
-
18
- /**
19
- * Publish an event. The payload type is keyed by the same keys used in your EventPayloads map.
20
- * Ensures the routing key exists in EVENTS and that the publisher is connected.
21
- */
22
- async publish<K extends keyof EventMap>(eventName: K, payload: EventMap[K]) {
23
- // Auto-connect if needed
24
- if (!this.channel) {
25
- await this.connect();
26
- }
27
-
28
- const routingKey = EVENTS[eventName as keyof typeof EVENTS];
29
- if (!routingKey || typeof routingKey !== 'string') {
30
- throw new Error(`No routing key configured for event ${String(eventName)}`);
31
- }
32
-
33
- let body: Buffer;
34
- try {
35
- body = Buffer.from(JSON.stringify(payload));
36
- } catch (err) {
37
- throw new Error('Failed to serialize payload for publish');
38
- }
39
-
40
- // Basic retry with exponential backoff
41
- const maxRetries = 3;
42
- const baseBackoffMs = 200;
43
-
44
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
45
- try {
46
- // channel.publish is synchronous; wrap in try/catch in case of closed channel
47
- this.channel.publish(this.exchange, routingKey, body, { persistent: true });
48
- return;
49
- } catch (err) {
50
- // if last attempt, rethrow
51
- if (attempt === maxRetries) throw err;
52
- // try reconnect + backoff
53
- try {
54
- // reset connection and attempt reconnect
55
- await this.close();
56
- } catch (_) {}
57
- try {
58
- await this.connect();
59
- } catch (_) {
60
- // failed to reconnect, will backoff and retry loop
61
- }
62
- const backoff = baseBackoffMs * Math.pow(2, attempt);
63
- await new Promise((res) => setTimeout(res, backoff));
64
- }
65
- }
66
- }
67
-
68
- async close() {
69
- try {
70
- if (this.channel) await this.channel.close();
71
- } catch (e) {
72
- // ignore
73
- }
74
- try {
75
- if (this.connection) await this.connection.close();
76
- } catch (e) {
77
- // ignore
78
- }
79
- this.channel = undefined;
80
- this.connection = undefined;
81
- }
82
- }
@@ -1,51 +0,0 @@
1
- const amqplib = require('amqplib');
2
- import { EVENTS, EventMap } from './eventTypes';
3
-
4
- type EventHandler<T> = (payload: T) => Promise<void> | void;
5
-
6
- export class EventSubscriber {
7
- private connection?: any;
8
- private channel?: any;
9
- private exchange = 'estopia.events';
10
-
11
- constructor(private url: string) {}
12
-
13
- async connect() {
14
- if (this.connection && this.channel) return;
15
- this.connection = await amqplib.connect(this.url);
16
- this.channel = await this.connection.createChannel();
17
- await this.channel.assertExchange(this.exchange, 'topic', { durable: true });
18
- }
19
-
20
- async subscribe<K extends keyof EventMap>(eventName: K, handler: EventHandler<EventMap[K]>) {
21
- if (!this.channel) throw new Error('Subscriber not connected');
22
-
23
- const q = await this.channel.assertQueue('', { exclusive: true });
24
- const routing = EVENTS[eventName as keyof typeof EVENTS];
25
- await this.channel.bindQueue(q.queue, this.exchange, routing);
26
-
27
- await this.channel.consume(q.queue, (msg: any | null) => {
28
- if (msg) {
29
- let payload: EventMap[K];
30
- try {
31
- payload = JSON.parse(msg.content.toString());
32
- } catch (err) {
33
- // malformed message -> nack and requeue false
34
- this.channel?.nack(msg, false, false);
35
- return;
36
- }
37
-
38
- Promise.resolve(handler(payload))
39
- .then(() => this.channel?.ack(msg))
40
- .catch(() => this.channel?.nack(msg, false, true));
41
- }
42
- });
43
- }
44
-
45
- async close() {
46
- try { if (this.channel) await this.channel.close(); } catch (e) { }
47
- try { if (this.connection) await this.connection.close(); } catch (e) { }
48
- this.channel = undefined;
49
- this.connection = undefined;
50
- }
51
- }
@@ -1,27 +0,0 @@
1
- import { Pool } from 'pg';
2
- import { CockroachConfig } from './types';
3
- import { Kysely, PostgresDialect } from 'kysely';
4
- import { DB } from './dataTypes';
5
-
6
- /**
7
- * Creates and tests a new connection pool for a CockroachDB cluster.
8
- * * @param config - The connection configuration for CockroachDB.
9
- * @returns A promise that resolves to a connected Pool instance.
10
- * @throws Will throw an error if the connection fails.
11
- */
12
- export async function createCockroachDBConnection(config: CockroachConfig): Promise<Kysely<DB>> {
13
- try {
14
-
15
- const db = new Kysely<DB>({
16
- dialect: new PostgresDialect({
17
- pool: new Pool(config)
18
- })
19
- });
20
-
21
- console.log('Successfully connected to CockroachDB.');
22
- return db;
23
- } catch (error) {
24
- console.error('Failed to connect to CockroachDB:', error);
25
- throw error;
26
- }
27
- }
@@ -1,55 +0,0 @@
1
- import { EstopiaRequest } from '../../types/request';
2
- import { validateJWTToken } from '../validateJWTToken';
3
-
4
- /**
5
- * Middleware to require authentication.
6
- * - Supports Authorization: Bearer <token> header (case-insensitive)
7
- * - Falls back to cookies.auth_token
8
- * - On success: sets req.auth = { ...payload, token } and calls next()
9
- * - On failure: responds with 401 and an appropriate error message
10
- */
11
- export function requireAuth(req: EstopiaRequest, res: any, next: any) {
12
- // Get Authorization header (many frameworks provide case-insensitive header lookup)
13
- const authHeader = typeof req.header === 'function' ? req.header('Authorization') : undefined;
14
-
15
- // Extract token from "Bearer <token>" or use header value directly
16
- let token: string | undefined;
17
- if (typeof authHeader === 'string' && authHeader.trim() !== '') {
18
- const bearerMatch = authHeader.match(/^\s*Bearer\s+(.+)\s*$/i);
19
- token = bearerMatch ? bearerMatch[1] : authHeader.trim();
20
- }
21
-
22
- // Fallback to cookie
23
- if (!token && req && req.cookies && typeof req.cookies.auth_token === 'string') {
24
- token = req.cookies.auth_token;
25
- }
26
-
27
- if (!token) {
28
- return res.status(401).json({ error: 'Missing authorization token' });
29
- }
30
-
31
- try {
32
- const payload = validateJWTToken(token);
33
- if (!payload) {
34
- return res.status(401).json({ error: 'Invalid token' });
35
- }
36
-
37
- // Attach auth info to request
38
- req.auth = { ...payload, token };
39
- return next();
40
- } catch (err: any) {
41
- // Log warning if logger available, otherwise fallback to console
42
- try {
43
- if (req && req.logger && typeof req.logger.warn === 'function') {
44
- req.logger.warn('Auth verification failed', err);
45
- } else {
46
- // Keep message concise for logs
47
- // eslint-disable-next-line no-console
48
- console.warn('Auth verification failed', err);
49
- }
50
- } catch {
51
- // swallow logging errors
52
- }
53
- return res.status(401).json({ error: 'Invalid authorization' });
54
- }
55
- }
@@ -1,48 +0,0 @@
1
- import { Response } from 'express';
2
- import { EstopiaRequest } from '../../types/request';
3
- import type { Kysely } from 'kysely';
4
- import type { DB } from '../../database/dataTypes';
5
-
6
- /**
7
- * Middleware to require authentication.
8
- * - Supports Authorization: Bearer <token> header (case-insensitive)
9
- * - Falls back to cookies.auth_token
10
- * - On success: sets req.auth = { ...payload, token } and calls next()
11
- * - On failure: responds with 401 and an appropriate error message
12
- */
13
- export async function requireSecure(req: EstopiaRequest, res: Response, next: any) {
14
- // This middleware only validates the Secure-Token cookie against the DB.
15
- // It does NOT require a valid JWT — per request: "it doesn't matter if the JWT token exists".
16
-
17
- // Read secure token from cookie
18
- const secureToken = req && req.cookies['Secure-Token'];
19
- if (!secureToken) {
20
- return res.status(403).json({ error: 'Missing secure token' });
21
- }
22
-
23
- // Expect a Kysely DB instance to be available on app.locals.db
24
- const cockroachPool = req.cockroachPool;
25
-
26
- try {
27
- const db = cockroachPool as Kysely<DB>;
28
- // Query secure_tokens table by token_uuid
29
- const row = await db.selectFrom('secure_tokens').selectAll().where('token_uuid', '=', secureToken).executeTakeFirst();
30
-
31
- if (!row) {
32
- return res.status(403).json({ error: 'Secure token not found' });
33
- }
34
-
35
- // expiry_at may be a Date or string depending on driver; normalize
36
- const expiry = row.expiry_at ? new Date(row.expiry_at as any) : null;
37
- if (!expiry || expiry.getTime() <= Date.now()) {
38
- return res.status(403).json({ error: 'Secure token expired' });
39
- }
40
-
41
- // Attach minimal auth context to request (no JWT required)
42
- req.auth = { secureToken, userId: row.user_id.toString() };
43
- return next();
44
- } catch (err: any) {
45
- console.error('Failed to validate secure token', err?.message || err);
46
- return res.status(500).json({ error: 'Failed to validate secure token' });
47
- }
48
- }
@@ -1,38 +0,0 @@
1
- import jwt from 'jsonwebtoken';
2
-
3
- export interface DecodedJWT {
4
- userId: string;
5
- jti?: string;
6
- iat?: number;
7
- exp?: number;
8
- }
9
-
10
- // Keep verification options minimal to allow tokens signed without an explicit issuer in tests.
11
- const jwtVerifyOptions = {};
12
-
13
- /**
14
- * Verify a JWT and return the decoded payload. Throws on invalid/expired token.
15
- */
16
- export function validateJWTToken(token: string): DecodedJWT {
17
- const secret = process.env.JWT_SECRET;
18
- if (!secret) throw new Error('JWT secret not configured');
19
-
20
- const payload = jwt.verify(token, secret, jwtVerifyOptions) as any;
21
- const userId = payload?.sub ?? payload?.userId ?? payload?.id ?? null;
22
- if (!userId) throw new Error('token missing subject');
23
-
24
- return { userId, jti: payload.jti ?? payload.jti ?? undefined, iat: payload.iat, exp: payload.exp } as DecodedJWT;
25
- }
26
-
27
- /**
28
- * Try to verify a token and return the decoded payload, or null if invalid.
29
- */
30
- export function tryValidateJWTToken(token: string): DecodedJWT | null {
31
- try {
32
- return validateJWTToken(token);
33
- } catch (e) {
34
- return null;
35
- }
36
- }
37
-
38
- export default validateJWTToken;
@@ -1,103 +0,0 @@
1
- import pino from 'pino';
2
- import axios from 'axios';
3
- import { Logger } from '../types/request';
4
-
5
- interface LoggerOptions {
6
- serviceName: string;
7
- logServiceUrl?: string;
8
- environment?: "development" | "production";
9
- sendLogs?: boolean; // default true
10
- }
11
-
12
- interface LogMeta {
13
- [key: string]: any;
14
- }
15
-
16
- export function createLogger(options: LoggerOptions): Logger {
17
- const {
18
- serviceName,
19
- logServiceUrl = process.env.LOG_SERVICE_URL || "http://localhost:4000/logs",
20
- environment = process.env.NODE_ENV as "development" | "production" || "development",
21
- sendLogs = true,
22
- } = options;
23
-
24
- let requestId: string | null = null;
25
- let requestURL: string | null = null;
26
- let requestMethod: string | null = null;
27
- let requestUserId: string | null = null;
28
- let requesterType: 'user' | 'internal' | null = null;
29
-
30
- const logger =
31
- environment === "development"
32
- ? pino({ name: serviceName, level: 'debug' } as any)
33
- : pino({
34
- name: serviceName,
35
- level: 'info',
36
- });
37
-
38
- const sendLog = async (level: string, message: string, meta: LogMeta = {}) => {
39
- if (!sendLogs) return;
40
- try {
41
- if (requestId) {
42
- meta.requestId = requestId;
43
- }
44
- if (requestURL) {
45
- meta.requestURL = requestURL;
46
- }
47
- if (requestMethod) {
48
- meta.requestMethod = requestMethod;
49
- }
50
- if (requestUserId) {
51
- meta.requestUserId = requestUserId;
52
- }
53
- if (requesterType) {
54
- meta.requesterType = requesterType;
55
- }
56
-
57
- await axios.post(
58
- logServiceUrl,
59
- {
60
- service: serviceName,
61
- level,
62
- message,
63
- meta,
64
- timestamp: new Date().toISOString(),
65
- },
66
- { timeout: 2000 }
67
- );
68
- } catch (err) {
69
- // Only warn locally, don’t crash
70
- logger.warn({ err }, "Failed to send log to remote service");
71
- }
72
- };
73
-
74
- const logWrapper: Logger = {
75
- info: (msg: string, meta?: LogMeta) => {
76
- logger.info(meta, msg);
77
- sendLog("info", msg, meta);
78
- },
79
- error: (msg: string, meta?: LogMeta) => {
80
- logger.error(meta, msg);
81
- sendLog("error", msg, meta);
82
- },
83
- warn: (msg: string, meta?: LogMeta) => {
84
- logger.warn(meta, msg);
85
- sendLog("warn", msg, meta);
86
- },
87
- debug: (msg: string, meta?: LogMeta) => {
88
- logger.debug(meta, msg);
89
- sendLog("debug", msg, meta);
90
- },
91
- setRequest: (requesterId: string | null, url: string | null, method: string | null, userId: string | null, type: 'user' | 'internal' | null) => {
92
- requestId = requesterId;
93
- requestURL = url;
94
- requestMethod = method;
95
- requestUserId = userId;
96
- requesterType = type;
97
- }
98
- };
99
-
100
- return logWrapper;
101
- }
102
-
103
- export type { LoggerOptions, LogMeta };