@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.
- package/dist/broker/eventTypes.d.ts +27 -0
- package/dist/broker/eventTypes.d.ts.map +1 -0
- package/dist/broker/eventTypes.js +9 -0
- package/dist/broker/eventTypes.js.map +1 -0
- package/dist/broker/publisher.d.ts +16 -0
- package/dist/broker/publisher.d.ts.map +1 -0
- package/dist/broker/publisher.js +124 -0
- package/dist/broker/publisher.js.map +1 -0
- package/dist/broker/subscriber.d.ts +14 -0
- package/dist/broker/subscriber.d.ts.map +1 -0
- package/dist/broker/subscriber.js +60 -0
- package/dist/broker/subscriber.js.map +1 -0
- package/dist/database/cockroach.d.ts +11 -0
- package/dist/database/cockroach.d.ts.map +1 -0
- package/dist/database/cockroach.js +27 -0
- package/dist/database/cockroach.js.map +1 -0
- package/dist/database/dataTypes.d.ts +99 -0
- package/dist/database/dataTypes.d.ts.map +1 -0
- package/dist/database/dataTypes.js +7 -0
- package/dist/database/dataTypes.js.map +1 -0
- package/{src/database/types.ts → dist/database/types.d.ts} +8 -9
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +3 -0
- package/dist/database/types.js.map +1 -0
- package/dist/helpers/middlware/auth.d.ts +10 -0
- package/dist/helpers/middlware/auth.d.ts.map +1 -0
- package/dist/helpers/middlware/auth.js +55 -0
- package/dist/helpers/middlware/auth.js.map +1 -0
- package/dist/helpers/middlware/secure.d.ts +11 -0
- package/dist/helpers/middlware/secure.d.ts.map +1 -0
- package/dist/helpers/middlware/secure.js +42 -0
- package/dist/helpers/middlware/secure.js.map +1 -0
- package/dist/helpers/validateJWTToken.d.ts +16 -0
- package/dist/helpers/validateJWTToken.d.ts.map +1 -0
- package/dist/helpers/validateJWTToken.js +36 -0
- package/dist/helpers/validateJWTToken.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +16 -24
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.d.ts +13 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +81 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/types/ENUMS.d.ts +32 -0
- package/dist/types/ENUMS.d.ts.map +1 -0
- package/dist/types/ENUMS.js +41 -0
- package/dist/types/ENUMS.js.map +1 -0
- package/dist/types/directs.d.ts +16 -0
- package/dist/types/directs.d.ts.map +1 -0
- package/dist/types/directs.js +3 -0
- package/dist/types/directs.js.map +1 -0
- package/dist/types/medical.d.ts +32 -0
- package/dist/types/medical.d.ts.map +1 -0
- package/dist/types/medical.js +3 -0
- package/dist/types/medical.js.map +1 -0
- package/dist/types/messages.d.ts +24 -0
- package/dist/types/messages.d.ts.map +1 -0
- package/dist/types/messages.js +3 -0
- package/dist/types/messages.js.map +1 -0
- package/{src/types/request.ts → dist/types/request.d.ts} +22 -23
- package/dist/types/request.d.ts.map +1 -0
- package/dist/types/request.js +3 -0
- package/dist/types/request.js.map +1 -0
- package/dist/types/servers.d.ts +32 -0
- package/dist/types/servers.d.ts.map +1 -0
- package/dist/types/servers.js +3 -0
- package/dist/types/servers.js.map +1 -0
- package/dist/types/servicePayloads.d.ts +32 -0
- package/dist/types/servicePayloads.d.ts.map +1 -0
- package/dist/types/servicePayloads.js +10 -0
- package/dist/types/servicePayloads.js.map +1 -0
- package/dist/types/ticketsAndReporting.d.ts +29 -0
- package/dist/types/ticketsAndReporting.d.ts.map +1 -0
- package/dist/types/ticketsAndReporting.js +3 -0
- package/dist/types/ticketsAndReporting.js.map +1 -0
- package/dist/types/user.d.ts +29 -0
- package/dist/types/user.d.ts.map +1 -0
- package/dist/types/user.js +3 -0
- package/dist/types/user.js.map +1 -0
- package/package.json +10 -4
- package/.github/workflows/tests.yml +0 -29
- package/scripts/migrations/1670000000000-initial.js +0 -107
- package/scripts/package.json +0 -20
- package/src/broker/eventTypes.ts +0 -23
- package/src/broker/publisher.ts +0 -82
- package/src/broker/subscriber.ts +0 -51
- package/src/database/cockroach.ts +0 -27
- package/src/helpers/middlware/auth.ts +0 -55
- package/src/helpers/middlware/secure.ts +0 -48
- package/src/helpers/validateJWTToken.ts +0 -38
- package/src/logging/logger.ts +0 -103
- package/src/types/ENUMS.ts +0 -45
- package/src/types/directs.ts +0 -18
- package/src/types/medical.ts +0 -36
- package/src/types/messages.ts +0 -27
- package/src/types/servers.ts +0 -36
- package/src/types/servicePayloads.ts +0 -40
- package/src/types/ticketsAndReporting.ts +0 -32
- package/src/types/user.ts +0 -34
- package/tests/verifyJWTToken.test.ts +0 -27
- package/tsconfig.json +0 -48
- 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 @@
|
|
|
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.
|
|
4
|
-
"main": "
|
|
5
|
-
"types": "
|
|
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
|
-
|
|
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
|
-
};
|
package/scripts/package.json
DELETED
|
@@ -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
|
-
}
|
package/src/broker/eventTypes.ts
DELETED
|
@@ -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;
|
package/src/broker/publisher.ts
DELETED
|
@@ -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
|
-
}
|
package/src/broker/subscriber.ts
DELETED
|
@@ -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;
|
package/src/logging/logger.ts
DELETED
|
@@ -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 };
|