@qwickapps/server 1.4.0 → 1.5.1
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/CHANGELOG.md +507 -0
- package/README.md +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/bans/bans-plugin.d.ts.map +1 -1
- package/dist/plugins/bans/bans-plugin.js +12 -3
- package/dist/plugins/bans/bans-plugin.js.map +1 -1
- package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts +11 -0
- package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts.map +1 -0
- package/dist/plugins/devices/__tests__/devices-plugin.test.js +410 -0
- package/dist/plugins/devices/__tests__/devices-plugin.test.js.map +1 -0
- package/dist/plugins/devices/__tests__/token-utils.test.d.ts +7 -0
- package/dist/plugins/devices/__tests__/token-utils.test.d.ts.map +1 -0
- package/dist/plugins/devices/__tests__/token-utils.test.js +197 -0
- package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -0
- package/dist/plugins/devices/adapters/compute-adapter.d.ts +36 -0
- package/dist/plugins/devices/adapters/compute-adapter.d.ts.map +1 -0
- package/dist/plugins/devices/adapters/compute-adapter.js +100 -0
- package/dist/plugins/devices/adapters/compute-adapter.js.map +1 -0
- package/dist/plugins/devices/adapters/index.d.ts +12 -0
- package/dist/plugins/devices/adapters/index.d.ts.map +1 -0
- package/dist/plugins/devices/adapters/index.js +10 -0
- package/dist/plugins/devices/adapters/index.js.map +1 -0
- package/dist/plugins/devices/adapters/mobile-adapter.d.ts +41 -0
- package/dist/plugins/devices/adapters/mobile-adapter.d.ts.map +1 -0
- package/dist/plugins/devices/adapters/mobile-adapter.js +131 -0
- package/dist/plugins/devices/adapters/mobile-adapter.js.map +1 -0
- package/dist/plugins/devices/devices-plugin.d.ts +70 -0
- package/dist/plugins/devices/devices-plugin.d.ts.map +1 -0
- package/dist/plugins/devices/devices-plugin.js +453 -0
- package/dist/plugins/devices/devices-plugin.js.map +1 -0
- package/dist/plugins/devices/index.d.ts +18 -0
- package/dist/plugins/devices/index.d.ts.map +1 -0
- package/dist/plugins/devices/index.js +18 -0
- package/dist/plugins/devices/index.js.map +1 -0
- package/dist/plugins/devices/stores/index.d.ts +9 -0
- package/dist/plugins/devices/stores/index.d.ts.map +1 -0
- package/dist/plugins/devices/stores/index.js +9 -0
- package/dist/plugins/devices/stores/index.js.map +1 -0
- package/dist/plugins/devices/stores/postgres-store.d.ts +26 -0
- package/dist/plugins/devices/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/devices/stores/postgres-store.js +199 -0
- package/dist/plugins/devices/stores/postgres-store.js.map +1 -0
- package/dist/plugins/devices/token-utils.d.ts +100 -0
- package/dist/plugins/devices/token-utils.d.ts.map +1 -0
- package/dist/plugins/devices/token-utils.js +162 -0
- package/dist/plugins/devices/token-utils.js.map +1 -0
- package/dist/plugins/devices/types.d.ts +307 -0
- package/dist/plugins/devices/types.d.ts.map +1 -0
- package/dist/plugins/devices/types.js +10 -0
- package/dist/plugins/devices/types.js.map +1 -0
- package/dist/plugins/index.d.ts +14 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +13 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts +5 -0
- package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts.map +1 -0
- package/dist/plugins/notifications/__tests__/notifications-manager.test.js +470 -0
- package/dist/plugins/notifications/__tests__/notifications-manager.test.js.map +1 -0
- package/dist/plugins/notifications/index.d.ts +71 -0
- package/dist/plugins/notifications/index.d.ts.map +1 -0
- package/dist/plugins/notifications/index.js +72 -0
- package/dist/plugins/notifications/index.js.map +1 -0
- package/dist/plugins/notifications/notifications-manager.d.ts +182 -0
- package/dist/plugins/notifications/notifications-manager.d.ts.map +1 -0
- package/dist/plugins/notifications/notifications-manager.js +610 -0
- package/dist/plugins/notifications/notifications-manager.js.map +1 -0
- package/dist/plugins/notifications/notifications-plugin.d.ts +83 -0
- package/dist/plugins/notifications/notifications-plugin.d.ts.map +1 -0
- package/dist/plugins/notifications/notifications-plugin.js +337 -0
- package/dist/plugins/notifications/notifications-plugin.js.map +1 -0
- package/dist/plugins/notifications/types.d.ts +164 -0
- package/dist/plugins/notifications/types.d.ts.map +1 -0
- package/dist/plugins/notifications/types.js +9 -0
- package/dist/plugins/notifications/types.js.map +1 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts +12 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts.map +1 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.js +349 -0
- package/dist/plugins/parental/__tests__/parental-plugin.test.js.map +1 -0
- package/dist/plugins/parental/adapters/index.d.ts +8 -0
- package/dist/plugins/parental/adapters/index.d.ts.map +1 -0
- package/dist/plugins/parental/adapters/index.js +7 -0
- package/dist/plugins/parental/adapters/index.js.map +1 -0
- package/dist/plugins/parental/adapters/kids-adapter.d.ts +24 -0
- package/dist/plugins/parental/adapters/kids-adapter.d.ts.map +1 -0
- package/dist/plugins/parental/adapters/kids-adapter.js +174 -0
- package/dist/plugins/parental/adapters/kids-adapter.js.map +1 -0
- package/dist/plugins/parental/index.d.ts +14 -0
- package/dist/plugins/parental/index.d.ts.map +1 -0
- package/dist/plugins/parental/index.js +15 -0
- package/dist/plugins/parental/index.js.map +1 -0
- package/dist/plugins/parental/parental-plugin.d.ts +88 -0
- package/dist/plugins/parental/parental-plugin.d.ts.map +1 -0
- package/dist/plugins/parental/parental-plugin.js +666 -0
- package/dist/plugins/parental/parental-plugin.js.map +1 -0
- package/dist/plugins/parental/stores/index.d.ts +7 -0
- package/dist/plugins/parental/stores/index.d.ts.map +1 -0
- package/dist/plugins/parental/stores/index.js +7 -0
- package/dist/plugins/parental/stores/index.js.map +1 -0
- package/dist/plugins/parental/stores/postgres-store.d.ts +10 -0
- package/dist/plugins/parental/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/parental/stores/postgres-store.js +209 -0
- package/dist/plugins/parental/stores/postgres-store.js.map +1 -0
- package/dist/plugins/parental/types.d.ts +154 -0
- package/dist/plugins/parental/types.d.ts.map +1 -0
- package/dist/plugins/parental/types.js +10 -0
- package/dist/plugins/parental/types.js.map +1 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts +11 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts.map +1 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.js +243 -0
- package/dist/plugins/profiles/__tests__/profiles-plugin.test.js.map +1 -0
- package/dist/plugins/profiles/index.d.ts +12 -0
- package/dist/plugins/profiles/index.d.ts.map +1 -0
- package/dist/plugins/profiles/index.js +13 -0
- package/dist/plugins/profiles/index.js.map +1 -0
- package/dist/plugins/profiles/profiles-plugin.d.ts +71 -0
- package/dist/plugins/profiles/profiles-plugin.d.ts.map +1 -0
- package/dist/plugins/profiles/profiles-plugin.js +481 -0
- package/dist/plugins/profiles/profiles-plugin.js.map +1 -0
- package/dist/plugins/profiles/stores/index.d.ts +9 -0
- package/dist/plugins/profiles/stores/index.d.ts.map +1 -0
- package/dist/plugins/profiles/stores/index.js +9 -0
- package/dist/plugins/profiles/stores/index.js.map +1 -0
- package/dist/plugins/profiles/stores/postgres-store.d.ts +18 -0
- package/dist/plugins/profiles/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/profiles/stores/postgres-store.js +310 -0
- package/dist/plugins/profiles/stores/postgres-store.js.map +1 -0
- package/dist/plugins/profiles/types.d.ts +289 -0
- package/dist/plugins/profiles/types.d.ts.map +1 -0
- package/dist/plugins/profiles/types.js +10 -0
- package/dist/plugins/profiles/types.js.map +1 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts +11 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts.map +1 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js +305 -0
- package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js.map +1 -0
- package/dist/plugins/subscriptions/index.d.ts +12 -0
- package/dist/plugins/subscriptions/index.d.ts.map +1 -0
- package/dist/plugins/subscriptions/index.js +13 -0
- package/dist/plugins/subscriptions/index.js.map +1 -0
- package/dist/plugins/subscriptions/stores/index.d.ts +9 -0
- package/dist/plugins/subscriptions/stores/index.d.ts.map +1 -0
- package/dist/plugins/subscriptions/stores/index.js +9 -0
- package/dist/plugins/subscriptions/stores/index.js.map +1 -0
- package/dist/plugins/subscriptions/stores/postgres-store.d.ts +14 -0
- package/dist/plugins/subscriptions/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/subscriptions/stores/postgres-store.js +359 -0
- package/dist/plugins/subscriptions/stores/postgres-store.js.map +1 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.d.ts +82 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.js +449 -0
- package/dist/plugins/subscriptions/subscriptions-plugin.js.map +1 -0
- package/dist/plugins/subscriptions/types.d.ts +308 -0
- package/dist/plugins/subscriptions/types.d.ts.map +1 -0
- package/dist/plugins/subscriptions/types.js +10 -0
- package/dist/plugins/subscriptions/types.js.map +1 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts +11 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts.map +1 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.js +218 -0
- package/dist/plugins/usage/__tests__/usage-plugin.test.js.map +1 -0
- package/dist/plugins/usage/index.d.ts +12 -0
- package/dist/plugins/usage/index.d.ts.map +1 -0
- package/dist/plugins/usage/index.js +13 -0
- package/dist/plugins/usage/index.js.map +1 -0
- package/dist/plugins/usage/stores/index.d.ts +9 -0
- package/dist/plugins/usage/stores/index.d.ts.map +1 -0
- package/dist/plugins/usage/stores/index.js +9 -0
- package/dist/plugins/usage/stores/index.js.map +1 -0
- package/dist/plugins/usage/stores/postgres-store.d.ts +14 -0
- package/dist/plugins/usage/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/usage/stores/postgres-store.js +146 -0
- package/dist/plugins/usage/stores/postgres-store.js.map +1 -0
- package/dist/plugins/usage/types.d.ts +195 -0
- package/dist/plugins/usage/types.d.ts.map +1 -0
- package/dist/plugins/usage/types.js +10 -0
- package/dist/plugins/usage/types.js.map +1 -0
- package/dist/plugins/usage/usage-plugin.d.ts +51 -0
- package/dist/plugins/usage/usage-plugin.d.ts.map +1 -0
- package/dist/plugins/usage/usage-plugin.js +412 -0
- package/dist/plugins/usage/usage-plugin.js.map +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.d.ts +10 -0
- package/dist/plugins/users/__tests__/postgres-store.test.d.ts.map +1 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js +229 -0
- package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
- package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
- package/dist/plugins/users/index.d.ts +2 -2
- package/dist/plugins/users/index.d.ts.map +1 -1
- package/dist/plugins/users/index.js +1 -1
- package/dist/plugins/users/index.js.map +1 -1
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
- package/dist/plugins/users/stores/postgres-store.js +76 -0
- package/dist/plugins/users/stores/postgres-store.js.map +1 -1
- package/dist/plugins/users/types.d.ts +74 -6
- package/dist/plugins/users/types.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.d.ts +15 -1
- package/dist/plugins/users/users-plugin.d.ts.map +1 -1
- package/dist/plugins/users/users-plugin.js +29 -0
- package/dist/plugins/users/users-plugin.js.map +1 -1
- package/dist-ui/assets/index-CynOqPkb.js +469 -0
- package/dist-ui/assets/index-CynOqPkb.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +46 -0
- package/dist-ui-lib/components/StatCard.d.ts +16 -0
- package/dist-ui-lib/dashboard/widgets/NotificationsStatsWidget.d.ts +12 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +1 -0
- package/dist-ui-lib/index.js +1822 -1611
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/pages/NotificationsPage.d.ts +9 -0
- package/dist-ui-lib/utils/formatters.d.ts +19 -0
- package/package.json +3 -2
- package/src/index.ts +178 -0
- package/src/plugins/bans/bans-plugin.ts +15 -3
- package/src/plugins/devices/__tests__/devices-plugin.test.ts +551 -0
- package/src/plugins/devices/__tests__/token-utils.test.ts +264 -0
- package/src/plugins/devices/adapters/compute-adapter.ts +139 -0
- package/src/plugins/devices/adapters/index.ts +13 -0
- package/src/plugins/devices/adapters/mobile-adapter.ts +179 -0
- package/src/plugins/devices/devices-plugin.ts +538 -0
- package/src/plugins/devices/index.ts +69 -0
- package/src/plugins/devices/stores/index.ts +9 -0
- package/src/plugins/devices/stores/postgres-store.ts +304 -0
- package/src/plugins/devices/token-utils.ts +213 -0
- package/src/plugins/devices/types.ts +351 -0
- package/src/plugins/index.ts +218 -0
- package/src/plugins/notifications/__tests__/notifications-manager.test.ts +637 -0
- package/src/plugins/notifications/index.ts +91 -0
- package/src/plugins/notifications/notifications-manager.ts +773 -0
- package/src/plugins/notifications/notifications-plugin.ts +398 -0
- package/src/plugins/notifications/types.ts +207 -0
- package/src/plugins/parental/__tests__/parental-plugin.test.ts +465 -0
- package/src/plugins/parental/adapters/index.ts +8 -0
- package/src/plugins/parental/adapters/kids-adapter.ts +206 -0
- package/src/plugins/parental/index.ts +55 -0
- package/src/plugins/parental/parental-plugin.ts +759 -0
- package/src/plugins/parental/stores/index.ts +7 -0
- package/src/plugins/parental/stores/postgres-store.ts +304 -0
- package/src/plugins/parental/types.ts +180 -0
- package/src/plugins/profiles/__tests__/profiles-plugin.test.ts +321 -0
- package/src/plugins/profiles/index.ts +49 -0
- package/src/plugins/profiles/profiles-plugin.ts +546 -0
- package/src/plugins/profiles/stores/index.ts +9 -0
- package/src/plugins/profiles/stores/postgres-store.ts +439 -0
- package/src/plugins/profiles/types.ts +338 -0
- package/src/plugins/subscriptions/__tests__/subscriptions-plugin.test.ts +404 -0
- package/src/plugins/subscriptions/index.ts +51 -0
- package/src/plugins/subscriptions/stores/index.ts +9 -0
- package/src/plugins/subscriptions/stores/postgres-store.ts +482 -0
- package/src/plugins/subscriptions/subscriptions-plugin.ts +530 -0
- package/src/plugins/subscriptions/types.ts +355 -0
- package/src/plugins/usage/__tests__/usage-plugin.test.ts +288 -0
- package/src/plugins/usage/index.ts +39 -0
- package/src/plugins/usage/stores/index.ts +9 -0
- package/src/plugins/usage/stores/postgres-store.ts +213 -0
- package/src/plugins/usage/types.ts +222 -0
- package/src/plugins/usage/usage-plugin.ts +484 -0
- package/src/plugins/users/__tests__/postgres-store.test.ts +326 -0
- package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
- package/src/plugins/users/index.ts +6 -0
- package/src/plugins/users/stores/postgres-store.ts +104 -0
- package/src/plugins/users/types.ts +82 -6
- package/src/plugins/users/users-plugin.ts +37 -0
- package/ui/src/App.tsx +5 -1
- package/ui/src/api/controlPanelApi.ts +103 -6
- package/ui/src/components/StatCard.tsx +58 -0
- package/ui/src/dashboard/builtInWidgets.tsx +3 -1
- package/ui/src/dashboard/widgets/NotificationsStatsWidget.tsx +167 -0
- package/ui/src/dashboard/widgets/index.ts +1 -0
- package/ui/src/pages/NotificationsPage.tsx +417 -0
- package/ui/src/utils/formatters.ts +33 -0
- package/dist-ui/assets/index-D7DoZ9rL.js +0 -478
- package/dist-ui/assets/index-D7DoZ9rL.js.map +0 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Device Store
|
|
3
|
+
*
|
|
4
|
+
* Device storage implementation using PostgreSQL.
|
|
5
|
+
* Supports multi-tenant isolation via org_id.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
DeviceStore,
|
|
12
|
+
Device,
|
|
13
|
+
CreateDeviceInput,
|
|
14
|
+
UpdateDeviceInput,
|
|
15
|
+
DeviceSearchParams,
|
|
16
|
+
DeviceListResponse,
|
|
17
|
+
PostgresDeviceStoreConfig,
|
|
18
|
+
} from '../types.js';
|
|
19
|
+
|
|
20
|
+
// Pool interface (from pg package)
|
|
21
|
+
interface PgPool {
|
|
22
|
+
query(text: string, values?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a PostgreSQL device store
|
|
27
|
+
*
|
|
28
|
+
* @param config Configuration including a pg Pool instance
|
|
29
|
+
* @returns DeviceStore implementation
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { Pool } from 'pg';
|
|
34
|
+
* import { postgresDeviceStore } from '@qwickapps/server';
|
|
35
|
+
*
|
|
36
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
37
|
+
* const store = postgresDeviceStore({ pool });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function postgresDeviceStore(config: PostgresDeviceStoreConfig): DeviceStore {
|
|
41
|
+
const {
|
|
42
|
+
pool: poolOrFn,
|
|
43
|
+
tableName = 'devices',
|
|
44
|
+
schema = 'public',
|
|
45
|
+
autoCreateTables = true,
|
|
46
|
+
} = config;
|
|
47
|
+
|
|
48
|
+
// Helper to get pool (supports lazy initialization via function)
|
|
49
|
+
const getPool = (): PgPool => {
|
|
50
|
+
const pool = typeof poolOrFn === 'function' ? poolOrFn() : poolOrFn;
|
|
51
|
+
return pool as PgPool;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const devicesTableFull = `"${schema}"."${tableName}"`;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name: 'postgres',
|
|
58
|
+
|
|
59
|
+
async initialize(): Promise<void> {
|
|
60
|
+
if (!autoCreateTables) return;
|
|
61
|
+
|
|
62
|
+
// Create devices table
|
|
63
|
+
await getPool().query(`
|
|
64
|
+
CREATE TABLE IF NOT EXISTS ${devicesTableFull} (
|
|
65
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
66
|
+
org_id UUID,
|
|
67
|
+
user_id UUID,
|
|
68
|
+
adapter_type VARCHAR(50) NOT NULL,
|
|
69
|
+
name VARCHAR(255) NOT NULL,
|
|
70
|
+
token_hash VARCHAR(64) NOT NULL,
|
|
71
|
+
token_prefix VARCHAR(12),
|
|
72
|
+
token_expires_at TIMESTAMPTZ NOT NULL,
|
|
73
|
+
last_seen_at TIMESTAMPTZ,
|
|
74
|
+
last_ip INET,
|
|
75
|
+
is_active BOOLEAN DEFAULT true,
|
|
76
|
+
metadata JSONB DEFAULT '{}',
|
|
77
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
78
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
79
|
+
deleted_at TIMESTAMPTZ
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_org ON ${devicesTableFull}(org_id) WHERE deleted_at IS NULL;
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_user ON ${devicesTableFull}(user_id) WHERE deleted_at IS NULL;
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_token ON ${devicesTableFull}(token_hash) WHERE is_active = true AND deleted_at IS NULL;
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_adapter ON ${devicesTableFull}(adapter_type);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_expires ON ${devicesTableFull}(token_expires_at) WHERE deleted_at IS NULL;
|
|
87
|
+
`);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async getById(id: string): Promise<Device | null> {
|
|
91
|
+
const result = await getPool().query(
|
|
92
|
+
`SELECT * FROM ${devicesTableFull} WHERE id = $1 AND deleted_at IS NULL`,
|
|
93
|
+
[id]
|
|
94
|
+
);
|
|
95
|
+
return (result.rows[0] as Device) || null;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async getByTokenHash(tokenHash: string): Promise<Device | null> {
|
|
99
|
+
const result = await getPool().query(
|
|
100
|
+
`SELECT * FROM ${devicesTableFull}
|
|
101
|
+
WHERE token_hash = $1
|
|
102
|
+
AND is_active = true
|
|
103
|
+
AND deleted_at IS NULL
|
|
104
|
+
AND token_expires_at > NOW()`,
|
|
105
|
+
[tokenHash]
|
|
106
|
+
);
|
|
107
|
+
return (result.rows[0] as Device) || null;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async create(
|
|
111
|
+
input: CreateDeviceInput & {
|
|
112
|
+
tokenHash: string;
|
|
113
|
+
tokenPrefix: string;
|
|
114
|
+
tokenExpiresAt: Date;
|
|
115
|
+
adapterType: string;
|
|
116
|
+
}
|
|
117
|
+
): Promise<Device> {
|
|
118
|
+
const result = await getPool().query(
|
|
119
|
+
`INSERT INTO ${devicesTableFull}
|
|
120
|
+
(org_id, user_id, adapter_type, name, token_hash, token_prefix, token_expires_at, metadata)
|
|
121
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
122
|
+
RETURNING *`,
|
|
123
|
+
[
|
|
124
|
+
input.org_id || null,
|
|
125
|
+
input.user_id || null,
|
|
126
|
+
input.adapterType,
|
|
127
|
+
input.name,
|
|
128
|
+
input.tokenHash,
|
|
129
|
+
input.tokenPrefix,
|
|
130
|
+
input.tokenExpiresAt,
|
|
131
|
+
JSON.stringify(input.metadata || {}),
|
|
132
|
+
]
|
|
133
|
+
);
|
|
134
|
+
return result.rows[0] as Device;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async update(id: string, input: UpdateDeviceInput): Promise<Device | null> {
|
|
138
|
+
const updates: string[] = [];
|
|
139
|
+
const values: unknown[] = [];
|
|
140
|
+
let paramIndex = 1;
|
|
141
|
+
|
|
142
|
+
if (input.name !== undefined) {
|
|
143
|
+
updates.push(`name = $${paramIndex++}`);
|
|
144
|
+
values.push(input.name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (input.is_active !== undefined) {
|
|
148
|
+
updates.push(`is_active = $${paramIndex++}`);
|
|
149
|
+
values.push(input.is_active);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (input.metadata !== undefined) {
|
|
153
|
+
updates.push(`metadata = $${paramIndex++}`);
|
|
154
|
+
values.push(JSON.stringify(input.metadata));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (updates.length === 0) {
|
|
158
|
+
return this.getById(id);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
updates.push(`updated_at = NOW()`);
|
|
162
|
+
values.push(id);
|
|
163
|
+
|
|
164
|
+
const result = await getPool().query(
|
|
165
|
+
`UPDATE ${devicesTableFull}
|
|
166
|
+
SET ${updates.join(', ')}
|
|
167
|
+
WHERE id = $${paramIndex} AND deleted_at IS NULL
|
|
168
|
+
RETURNING *`,
|
|
169
|
+
values
|
|
170
|
+
);
|
|
171
|
+
return (result.rows[0] as Device) || null;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
async delete(id: string): Promise<boolean> {
|
|
175
|
+
// Soft delete
|
|
176
|
+
const result = await getPool().query(
|
|
177
|
+
`UPDATE ${devicesTableFull}
|
|
178
|
+
SET deleted_at = NOW(), is_active = false, updated_at = NOW()
|
|
179
|
+
WHERE id = $1 AND deleted_at IS NULL`,
|
|
180
|
+
[id]
|
|
181
|
+
);
|
|
182
|
+
return (result.rowCount ?? 0) > 0;
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
async search(params: DeviceSearchParams): Promise<DeviceListResponse> {
|
|
186
|
+
const {
|
|
187
|
+
org_id,
|
|
188
|
+
user_id,
|
|
189
|
+
adapter_type,
|
|
190
|
+
is_active,
|
|
191
|
+
query,
|
|
192
|
+
page = 1,
|
|
193
|
+
limit = 20,
|
|
194
|
+
sortBy = 'created_at',
|
|
195
|
+
sortOrder = 'desc',
|
|
196
|
+
} = params;
|
|
197
|
+
|
|
198
|
+
const conditions: string[] = ['deleted_at IS NULL'];
|
|
199
|
+
const values: unknown[] = [];
|
|
200
|
+
let paramIndex = 1;
|
|
201
|
+
|
|
202
|
+
if (org_id) {
|
|
203
|
+
conditions.push(`org_id = $${paramIndex++}`);
|
|
204
|
+
values.push(org_id);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (user_id) {
|
|
208
|
+
conditions.push(`user_id = $${paramIndex++}`);
|
|
209
|
+
values.push(user_id);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (adapter_type) {
|
|
213
|
+
conditions.push(`adapter_type = $${paramIndex++}`);
|
|
214
|
+
values.push(adapter_type);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (is_active !== undefined) {
|
|
218
|
+
conditions.push(`is_active = $${paramIndex++}`);
|
|
219
|
+
values.push(is_active);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (query) {
|
|
223
|
+
conditions.push(`LOWER(name) LIKE $${paramIndex++}`);
|
|
224
|
+
values.push(`%${query.toLowerCase()}%`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
228
|
+
|
|
229
|
+
// Validate sort column to prevent SQL injection
|
|
230
|
+
const validSortColumns = ['name', 'created_at', 'last_seen_at'];
|
|
231
|
+
const sortColumn = validSortColumns.includes(sortBy) ? sortBy : 'created_at';
|
|
232
|
+
const sortDir = sortOrder === 'asc' ? 'ASC' : 'DESC';
|
|
233
|
+
|
|
234
|
+
const offset = (page - 1) * limit;
|
|
235
|
+
|
|
236
|
+
// Get total count
|
|
237
|
+
const countResult = await getPool().query(
|
|
238
|
+
`SELECT COUNT(*) FROM ${devicesTableFull} ${whereClause}`,
|
|
239
|
+
values
|
|
240
|
+
);
|
|
241
|
+
const countRow = countResult.rows[0] as { count: string } | undefined;
|
|
242
|
+
const total = countRow ? parseInt(countRow.count, 10) : 0;
|
|
243
|
+
|
|
244
|
+
// Get devices
|
|
245
|
+
const result = await getPool().query(
|
|
246
|
+
`SELECT * FROM ${devicesTableFull} ${whereClause}
|
|
247
|
+
ORDER BY ${sortColumn} ${sortDir}
|
|
248
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
|
249
|
+
[...values, limit, offset]
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
devices: result.rows as Device[],
|
|
254
|
+
total,
|
|
255
|
+
page,
|
|
256
|
+
limit,
|
|
257
|
+
totalPages: Math.ceil(total / limit),
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
async updateLastSeen(id: string, ip?: string): Promise<void> {
|
|
262
|
+
if (ip) {
|
|
263
|
+
await getPool().query(
|
|
264
|
+
`UPDATE ${devicesTableFull} SET last_seen_at = NOW(), last_ip = $1 WHERE id = $2`,
|
|
265
|
+
[ip, id]
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
await getPool().query(
|
|
269
|
+
`UPDATE ${devicesTableFull} SET last_seen_at = NOW() WHERE id = $1`,
|
|
270
|
+
[id]
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
async updateToken(
|
|
276
|
+
id: string,
|
|
277
|
+
tokenHash: string,
|
|
278
|
+
tokenPrefix: string,
|
|
279
|
+
expiresAt: Date
|
|
280
|
+
): Promise<boolean> {
|
|
281
|
+
const result = await getPool().query(
|
|
282
|
+
`UPDATE ${devicesTableFull}
|
|
283
|
+
SET token_hash = $1, token_prefix = $2, token_expires_at = $3, updated_at = NOW()
|
|
284
|
+
WHERE id = $4 AND deleted_at IS NULL`,
|
|
285
|
+
[tokenHash, tokenPrefix, expiresAt, id]
|
|
286
|
+
);
|
|
287
|
+
return (result.rowCount ?? 0) > 0;
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
async cleanupExpired(): Promise<number> {
|
|
291
|
+
// Deactivate expired tokens
|
|
292
|
+
const result = await getPool().query(
|
|
293
|
+
`UPDATE ${devicesTableFull}
|
|
294
|
+
SET is_active = false, updated_at = NOW()
|
|
295
|
+
WHERE token_expires_at < NOW() AND is_active = true AND deleted_at IS NULL`
|
|
296
|
+
);
|
|
297
|
+
return result.rowCount ?? 0;
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
async shutdown(): Promise<void> {
|
|
301
|
+
// Pool is managed externally, nothing to do here
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Token Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for generating, hashing, and verifying device tokens.
|
|
5
|
+
* Adapted from QwickForge's device-tokens implementation.
|
|
6
|
+
*
|
|
7
|
+
* Copyright (c) 2025 QwickApps.com. All rights reserved.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
// Types
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
export interface DeviceTokenPair {
|
|
17
|
+
/** Raw token to return to client (store securely!) */
|
|
18
|
+
token: string;
|
|
19
|
+
/** Hashed token to store in database */
|
|
20
|
+
hash: string;
|
|
21
|
+
/** First 8 characters for identification */
|
|
22
|
+
prefix: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TokenVerificationResult {
|
|
26
|
+
/** Whether the token is valid */
|
|
27
|
+
valid: boolean;
|
|
28
|
+
/** Error message if invalid */
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
|
+
// Token Generation
|
|
34
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate a cryptographically secure device token
|
|
38
|
+
*
|
|
39
|
+
* Returns both the raw token (to send to client) and the hash (to store in DB).
|
|
40
|
+
* The raw token should be shown to the user ONCE and never stored server-side.
|
|
41
|
+
*
|
|
42
|
+
* Token format: `<prefix>_<32 bytes base64url>`
|
|
43
|
+
*
|
|
44
|
+
* @param prefix - Token prefix for identification (e.g., 'qwf_dev', 'qwb_mob')
|
|
45
|
+
* @returns Token pair with raw token, hash, and display prefix
|
|
46
|
+
*/
|
|
47
|
+
export async function generateDeviceToken(prefix: string): Promise<DeviceTokenPair> {
|
|
48
|
+
// Generate 32 bytes of random data
|
|
49
|
+
const randomBytes = crypto.randomBytes(32);
|
|
50
|
+
|
|
51
|
+
// Convert to base64url (URL-safe, no padding)
|
|
52
|
+
const tokenSecret = randomBytes.toString('base64url');
|
|
53
|
+
|
|
54
|
+
// Combine prefix with secret
|
|
55
|
+
const token = `${prefix}_${tokenSecret}`;
|
|
56
|
+
|
|
57
|
+
// Hash the token for storage
|
|
58
|
+
const hash = await hashToken(token);
|
|
59
|
+
|
|
60
|
+
// Get first 8 chars of full token for display
|
|
61
|
+
const displayPrefix = token.substring(0, 8);
|
|
62
|
+
|
|
63
|
+
return { token, hash, prefix: displayPrefix };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate a short-lived pairing code for device registration
|
|
68
|
+
*
|
|
69
|
+
* Used for device pairing flow. User enters this code in web/mobile UI.
|
|
70
|
+
* Format: 6 uppercase alphanumeric characters (e.g., 'A1B2C3')
|
|
71
|
+
*
|
|
72
|
+
* @returns 6-character pairing code
|
|
73
|
+
*/
|
|
74
|
+
export function generatePairingCode(): string {
|
|
75
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Avoid confusing chars (0, O, I, 1)
|
|
76
|
+
let code = '';
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < 6; i++) {
|
|
79
|
+
const randomIndex = crypto.randomInt(0, chars.length);
|
|
80
|
+
code += chars[randomIndex];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return code;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
87
|
+
// Token Hashing
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Hash a token using SHA-256
|
|
92
|
+
*
|
|
93
|
+
* We use SHA-256 instead of bcrypt for tokens because:
|
|
94
|
+
* 1. Tokens are high-entropy (32 random bytes)
|
|
95
|
+
* 2. No need for slow hashing (not user passwords)
|
|
96
|
+
* 3. Faster verification for high-throughput API calls
|
|
97
|
+
*
|
|
98
|
+
* @param token - Raw token to hash
|
|
99
|
+
* @returns Hex-encoded SHA-256 hash
|
|
100
|
+
*/
|
|
101
|
+
export async function hashToken(token: string): Promise<string> {
|
|
102
|
+
const hash = crypto.createHash('sha256').update(token).digest('hex');
|
|
103
|
+
return hash;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Verify a token against its stored hash
|
|
108
|
+
*
|
|
109
|
+
* Constant-time comparison to prevent timing attacks.
|
|
110
|
+
*
|
|
111
|
+
* @param token - Raw token from client
|
|
112
|
+
* @param storedHash - Hash from database
|
|
113
|
+
* @returns Verification result
|
|
114
|
+
*/
|
|
115
|
+
export async function verifyToken(
|
|
116
|
+
token: string,
|
|
117
|
+
storedHash: string
|
|
118
|
+
): Promise<TokenVerificationResult> {
|
|
119
|
+
try {
|
|
120
|
+
// Hash the provided token
|
|
121
|
+
const tokenHash = await hashToken(token);
|
|
122
|
+
|
|
123
|
+
// Constant-time comparison
|
|
124
|
+
const valid = crypto.timingSafeEqual(
|
|
125
|
+
Buffer.from(tokenHash, 'hex'),
|
|
126
|
+
Buffer.from(storedHash, 'hex')
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (!valid) {
|
|
130
|
+
return { valid: false, error: 'Invalid token' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { valid: true };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
valid: false,
|
|
137
|
+
error: error instanceof Error ? error.message : 'Token verification failed',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
143
|
+
// Token Validation
|
|
144
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate token format (prefix and length)
|
|
148
|
+
*
|
|
149
|
+
* Checks if token matches expected format before attempting verification.
|
|
150
|
+
* Useful for fast rejection of malformed tokens.
|
|
151
|
+
*
|
|
152
|
+
* @param token - Token to validate
|
|
153
|
+
* @param expectedPrefix - Expected token prefix
|
|
154
|
+
* @returns True if format is valid
|
|
155
|
+
*/
|
|
156
|
+
export function isValidTokenFormat(token: string, expectedPrefix: string): boolean {
|
|
157
|
+
// Check prefix
|
|
158
|
+
if (!token.startsWith(`${expectedPrefix}_`)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Extract secret part
|
|
163
|
+
const secret = token.slice(expectedPrefix.length + 1);
|
|
164
|
+
|
|
165
|
+
// Validate length (32 bytes base64url = 43 characters)
|
|
166
|
+
if (secret.length !== 43) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Validate characters (base64url: A-Za-z0-9_-)
|
|
171
|
+
const base64urlPattern = /^[A-Za-z0-9_-]+$/;
|
|
172
|
+
if (!base64urlPattern.test(secret)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if token is expired
|
|
181
|
+
*
|
|
182
|
+
* @param expiresAt - Expiration timestamp
|
|
183
|
+
* @returns True if token is expired
|
|
184
|
+
*/
|
|
185
|
+
export function isTokenExpired(expiresAt: Date): boolean {
|
|
186
|
+
return new Date() > expiresAt;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Calculate token expiration date
|
|
191
|
+
*
|
|
192
|
+
* @param daysValid - Number of days token should be valid (default: 90)
|
|
193
|
+
* @returns Expiration timestamp
|
|
194
|
+
*/
|
|
195
|
+
export function getTokenExpiration(daysValid: number = 90): Date {
|
|
196
|
+
const expiresAt = new Date();
|
|
197
|
+
expiresAt.setDate(expiresAt.getDate() + daysValid);
|
|
198
|
+
return expiresAt;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
202
|
+
// Exports
|
|
203
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
204
|
+
|
|
205
|
+
export const DeviceTokens = {
|
|
206
|
+
generate: generateDeviceToken,
|
|
207
|
+
generatePairingCode,
|
|
208
|
+
hash: hashToken,
|
|
209
|
+
verify: verifyToken,
|
|
210
|
+
isValidFormat: isValidTokenFormat,
|
|
211
|
+
isExpired: isTokenExpired,
|
|
212
|
+
getExpiration: getTokenExpiration,
|
|
213
|
+
};
|