@qwickapps/server 1.4.0 → 1.5.0

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 (271) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +12 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -1
  6. package/dist/plugins/bans/bans-plugin.js +12 -3
  7. package/dist/plugins/bans/bans-plugin.js.map +1 -1
  8. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts +11 -0
  9. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts.map +1 -0
  10. package/dist/plugins/devices/__tests__/devices-plugin.test.js +410 -0
  11. package/dist/plugins/devices/__tests__/devices-plugin.test.js.map +1 -0
  12. package/dist/plugins/devices/__tests__/token-utils.test.d.ts +7 -0
  13. package/dist/plugins/devices/__tests__/token-utils.test.d.ts.map +1 -0
  14. package/dist/plugins/devices/__tests__/token-utils.test.js +197 -0
  15. package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -0
  16. package/dist/plugins/devices/adapters/compute-adapter.d.ts +36 -0
  17. package/dist/plugins/devices/adapters/compute-adapter.d.ts.map +1 -0
  18. package/dist/plugins/devices/adapters/compute-adapter.js +100 -0
  19. package/dist/plugins/devices/adapters/compute-adapter.js.map +1 -0
  20. package/dist/plugins/devices/adapters/index.d.ts +12 -0
  21. package/dist/plugins/devices/adapters/index.d.ts.map +1 -0
  22. package/dist/plugins/devices/adapters/index.js +10 -0
  23. package/dist/plugins/devices/adapters/index.js.map +1 -0
  24. package/dist/plugins/devices/adapters/mobile-adapter.d.ts +41 -0
  25. package/dist/plugins/devices/adapters/mobile-adapter.d.ts.map +1 -0
  26. package/dist/plugins/devices/adapters/mobile-adapter.js +131 -0
  27. package/dist/plugins/devices/adapters/mobile-adapter.js.map +1 -0
  28. package/dist/plugins/devices/devices-plugin.d.ts +70 -0
  29. package/dist/plugins/devices/devices-plugin.d.ts.map +1 -0
  30. package/dist/plugins/devices/devices-plugin.js +453 -0
  31. package/dist/plugins/devices/devices-plugin.js.map +1 -0
  32. package/dist/plugins/devices/index.d.ts +18 -0
  33. package/dist/plugins/devices/index.d.ts.map +1 -0
  34. package/dist/plugins/devices/index.js +18 -0
  35. package/dist/plugins/devices/index.js.map +1 -0
  36. package/dist/plugins/devices/stores/index.d.ts +9 -0
  37. package/dist/plugins/devices/stores/index.d.ts.map +1 -0
  38. package/dist/plugins/devices/stores/index.js +9 -0
  39. package/dist/plugins/devices/stores/index.js.map +1 -0
  40. package/dist/plugins/devices/stores/postgres-store.d.ts +26 -0
  41. package/dist/plugins/devices/stores/postgres-store.d.ts.map +1 -0
  42. package/dist/plugins/devices/stores/postgres-store.js +199 -0
  43. package/dist/plugins/devices/stores/postgres-store.js.map +1 -0
  44. package/dist/plugins/devices/token-utils.d.ts +100 -0
  45. package/dist/plugins/devices/token-utils.d.ts.map +1 -0
  46. package/dist/plugins/devices/token-utils.js +162 -0
  47. package/dist/plugins/devices/token-utils.js.map +1 -0
  48. package/dist/plugins/devices/types.d.ts +307 -0
  49. package/dist/plugins/devices/types.d.ts.map +1 -0
  50. package/dist/plugins/devices/types.js +10 -0
  51. package/dist/plugins/devices/types.js.map +1 -0
  52. package/dist/plugins/index.d.ts +14 -2
  53. package/dist/plugins/index.d.ts.map +1 -1
  54. package/dist/plugins/index.js +13 -1
  55. package/dist/plugins/index.js.map +1 -1
  56. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts +5 -0
  57. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts.map +1 -0
  58. package/dist/plugins/notifications/__tests__/notifications-manager.test.js +470 -0
  59. package/dist/plugins/notifications/__tests__/notifications-manager.test.js.map +1 -0
  60. package/dist/plugins/notifications/index.d.ts +71 -0
  61. package/dist/plugins/notifications/index.d.ts.map +1 -0
  62. package/dist/plugins/notifications/index.js +72 -0
  63. package/dist/plugins/notifications/index.js.map +1 -0
  64. package/dist/plugins/notifications/notifications-manager.d.ts +182 -0
  65. package/dist/plugins/notifications/notifications-manager.d.ts.map +1 -0
  66. package/dist/plugins/notifications/notifications-manager.js +610 -0
  67. package/dist/plugins/notifications/notifications-manager.js.map +1 -0
  68. package/dist/plugins/notifications/notifications-plugin.d.ts +83 -0
  69. package/dist/plugins/notifications/notifications-plugin.d.ts.map +1 -0
  70. package/dist/plugins/notifications/notifications-plugin.js +337 -0
  71. package/dist/plugins/notifications/notifications-plugin.js.map +1 -0
  72. package/dist/plugins/notifications/types.d.ts +164 -0
  73. package/dist/plugins/notifications/types.d.ts.map +1 -0
  74. package/dist/plugins/notifications/types.js +9 -0
  75. package/dist/plugins/notifications/types.js.map +1 -0
  76. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts +12 -0
  77. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts.map +1 -0
  78. package/dist/plugins/parental/__tests__/parental-plugin.test.js +349 -0
  79. package/dist/plugins/parental/__tests__/parental-plugin.test.js.map +1 -0
  80. package/dist/plugins/parental/adapters/index.d.ts +8 -0
  81. package/dist/plugins/parental/adapters/index.d.ts.map +1 -0
  82. package/dist/plugins/parental/adapters/index.js +7 -0
  83. package/dist/plugins/parental/adapters/index.js.map +1 -0
  84. package/dist/plugins/parental/adapters/kids-adapter.d.ts +24 -0
  85. package/dist/plugins/parental/adapters/kids-adapter.d.ts.map +1 -0
  86. package/dist/plugins/parental/adapters/kids-adapter.js +174 -0
  87. package/dist/plugins/parental/adapters/kids-adapter.js.map +1 -0
  88. package/dist/plugins/parental/index.d.ts +14 -0
  89. package/dist/plugins/parental/index.d.ts.map +1 -0
  90. package/dist/plugins/parental/index.js +15 -0
  91. package/dist/plugins/parental/index.js.map +1 -0
  92. package/dist/plugins/parental/parental-plugin.d.ts +88 -0
  93. package/dist/plugins/parental/parental-plugin.d.ts.map +1 -0
  94. package/dist/plugins/parental/parental-plugin.js +666 -0
  95. package/dist/plugins/parental/parental-plugin.js.map +1 -0
  96. package/dist/plugins/parental/stores/index.d.ts +7 -0
  97. package/dist/plugins/parental/stores/index.d.ts.map +1 -0
  98. package/dist/plugins/parental/stores/index.js +7 -0
  99. package/dist/plugins/parental/stores/index.js.map +1 -0
  100. package/dist/plugins/parental/stores/postgres-store.d.ts +10 -0
  101. package/dist/plugins/parental/stores/postgres-store.d.ts.map +1 -0
  102. package/dist/plugins/parental/stores/postgres-store.js +209 -0
  103. package/dist/plugins/parental/stores/postgres-store.js.map +1 -0
  104. package/dist/plugins/parental/types.d.ts +154 -0
  105. package/dist/plugins/parental/types.d.ts.map +1 -0
  106. package/dist/plugins/parental/types.js +10 -0
  107. package/dist/plugins/parental/types.js.map +1 -0
  108. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts +11 -0
  109. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts.map +1 -0
  110. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js +243 -0
  111. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js.map +1 -0
  112. package/dist/plugins/profiles/index.d.ts +12 -0
  113. package/dist/plugins/profiles/index.d.ts.map +1 -0
  114. package/dist/plugins/profiles/index.js +13 -0
  115. package/dist/plugins/profiles/index.js.map +1 -0
  116. package/dist/plugins/profiles/profiles-plugin.d.ts +71 -0
  117. package/dist/plugins/profiles/profiles-plugin.d.ts.map +1 -0
  118. package/dist/plugins/profiles/profiles-plugin.js +481 -0
  119. package/dist/plugins/profiles/profiles-plugin.js.map +1 -0
  120. package/dist/plugins/profiles/stores/index.d.ts +9 -0
  121. package/dist/plugins/profiles/stores/index.d.ts.map +1 -0
  122. package/dist/plugins/profiles/stores/index.js +9 -0
  123. package/dist/plugins/profiles/stores/index.js.map +1 -0
  124. package/dist/plugins/profiles/stores/postgres-store.d.ts +18 -0
  125. package/dist/plugins/profiles/stores/postgres-store.d.ts.map +1 -0
  126. package/dist/plugins/profiles/stores/postgres-store.js +310 -0
  127. package/dist/plugins/profiles/stores/postgres-store.js.map +1 -0
  128. package/dist/plugins/profiles/types.d.ts +289 -0
  129. package/dist/plugins/profiles/types.d.ts.map +1 -0
  130. package/dist/plugins/profiles/types.js +10 -0
  131. package/dist/plugins/profiles/types.js.map +1 -0
  132. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts +11 -0
  133. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts.map +1 -0
  134. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js +305 -0
  135. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js.map +1 -0
  136. package/dist/plugins/subscriptions/index.d.ts +12 -0
  137. package/dist/plugins/subscriptions/index.d.ts.map +1 -0
  138. package/dist/plugins/subscriptions/index.js +13 -0
  139. package/dist/plugins/subscriptions/index.js.map +1 -0
  140. package/dist/plugins/subscriptions/stores/index.d.ts +9 -0
  141. package/dist/plugins/subscriptions/stores/index.d.ts.map +1 -0
  142. package/dist/plugins/subscriptions/stores/index.js +9 -0
  143. package/dist/plugins/subscriptions/stores/index.js.map +1 -0
  144. package/dist/plugins/subscriptions/stores/postgres-store.d.ts +14 -0
  145. package/dist/plugins/subscriptions/stores/postgres-store.d.ts.map +1 -0
  146. package/dist/plugins/subscriptions/stores/postgres-store.js +359 -0
  147. package/dist/plugins/subscriptions/stores/postgres-store.js.map +1 -0
  148. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts +82 -0
  149. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -0
  150. package/dist/plugins/subscriptions/subscriptions-plugin.js +449 -0
  151. package/dist/plugins/subscriptions/subscriptions-plugin.js.map +1 -0
  152. package/dist/plugins/subscriptions/types.d.ts +308 -0
  153. package/dist/plugins/subscriptions/types.d.ts.map +1 -0
  154. package/dist/plugins/subscriptions/types.js +10 -0
  155. package/dist/plugins/subscriptions/types.js.map +1 -0
  156. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts +11 -0
  157. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts.map +1 -0
  158. package/dist/plugins/usage/__tests__/usage-plugin.test.js +218 -0
  159. package/dist/plugins/usage/__tests__/usage-plugin.test.js.map +1 -0
  160. package/dist/plugins/usage/index.d.ts +12 -0
  161. package/dist/plugins/usage/index.d.ts.map +1 -0
  162. package/dist/plugins/usage/index.js +13 -0
  163. package/dist/plugins/usage/index.js.map +1 -0
  164. package/dist/plugins/usage/stores/index.d.ts +9 -0
  165. package/dist/plugins/usage/stores/index.d.ts.map +1 -0
  166. package/dist/plugins/usage/stores/index.js +9 -0
  167. package/dist/plugins/usage/stores/index.js.map +1 -0
  168. package/dist/plugins/usage/stores/postgres-store.d.ts +14 -0
  169. package/dist/plugins/usage/stores/postgres-store.d.ts.map +1 -0
  170. package/dist/plugins/usage/stores/postgres-store.js +146 -0
  171. package/dist/plugins/usage/stores/postgres-store.js.map +1 -0
  172. package/dist/plugins/usage/types.d.ts +195 -0
  173. package/dist/plugins/usage/types.d.ts.map +1 -0
  174. package/dist/plugins/usage/types.js +10 -0
  175. package/dist/plugins/usage/types.js.map +1 -0
  176. package/dist/plugins/usage/usage-plugin.d.ts +51 -0
  177. package/dist/plugins/usage/usage-plugin.d.ts.map +1 -0
  178. package/dist/plugins/usage/usage-plugin.js +412 -0
  179. package/dist/plugins/usage/usage-plugin.js.map +1 -0
  180. package/dist/plugins/users/__tests__/postgres-store.test.d.ts +10 -0
  181. package/dist/plugins/users/__tests__/postgres-store.test.d.ts.map +1 -0
  182. package/dist/plugins/users/__tests__/postgres-store.test.js +229 -0
  183. package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -0
  184. package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
  185. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
  186. package/dist/plugins/users/index.d.ts +2 -2
  187. package/dist/plugins/users/index.d.ts.map +1 -1
  188. package/dist/plugins/users/index.js +1 -1
  189. package/dist/plugins/users/index.js.map +1 -1
  190. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
  191. package/dist/plugins/users/stores/postgres-store.js +76 -0
  192. package/dist/plugins/users/stores/postgres-store.js.map +1 -1
  193. package/dist/plugins/users/types.d.ts +74 -6
  194. package/dist/plugins/users/types.d.ts.map +1 -1
  195. package/dist/plugins/users/users-plugin.d.ts +15 -1
  196. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  197. package/dist/plugins/users/users-plugin.js +29 -0
  198. package/dist/plugins/users/users-plugin.js.map +1 -1
  199. package/dist-ui/assets/index-CynOqPkb.js +469 -0
  200. package/dist-ui/assets/index-CynOqPkb.js.map +1 -0
  201. package/dist-ui/index.html +1 -1
  202. package/dist-ui-lib/api/controlPanelApi.d.ts +46 -0
  203. package/dist-ui-lib/components/StatCard.d.ts +16 -0
  204. package/dist-ui-lib/dashboard/widgets/NotificationsStatsWidget.d.ts +12 -0
  205. package/dist-ui-lib/dashboard/widgets/index.d.ts +1 -0
  206. package/dist-ui-lib/index.js +1822 -1611
  207. package/dist-ui-lib/index.js.map +1 -1
  208. package/dist-ui-lib/pages/NotificationsPage.d.ts +9 -0
  209. package/dist-ui-lib/utils/formatters.d.ts +19 -0
  210. package/package.json +1 -1
  211. package/src/index.ts +178 -0
  212. package/src/plugins/bans/bans-plugin.ts +15 -3
  213. package/src/plugins/devices/__tests__/devices-plugin.test.ts +551 -0
  214. package/src/plugins/devices/__tests__/token-utils.test.ts +264 -0
  215. package/src/plugins/devices/adapters/compute-adapter.ts +139 -0
  216. package/src/plugins/devices/adapters/index.ts +13 -0
  217. package/src/plugins/devices/adapters/mobile-adapter.ts +179 -0
  218. package/src/plugins/devices/devices-plugin.ts +538 -0
  219. package/src/plugins/devices/index.ts +69 -0
  220. package/src/plugins/devices/stores/index.ts +9 -0
  221. package/src/plugins/devices/stores/postgres-store.ts +304 -0
  222. package/src/plugins/devices/token-utils.ts +213 -0
  223. package/src/plugins/devices/types.ts +351 -0
  224. package/src/plugins/index.ts +218 -0
  225. package/src/plugins/notifications/__tests__/notifications-manager.test.ts +637 -0
  226. package/src/plugins/notifications/index.ts +91 -0
  227. package/src/plugins/notifications/notifications-manager.ts +773 -0
  228. package/src/plugins/notifications/notifications-plugin.ts +398 -0
  229. package/src/plugins/notifications/types.ts +207 -0
  230. package/src/plugins/parental/__tests__/parental-plugin.test.ts +465 -0
  231. package/src/plugins/parental/adapters/index.ts +8 -0
  232. package/src/plugins/parental/adapters/kids-adapter.ts +206 -0
  233. package/src/plugins/parental/index.ts +55 -0
  234. package/src/plugins/parental/parental-plugin.ts +759 -0
  235. package/src/plugins/parental/stores/index.ts +7 -0
  236. package/src/plugins/parental/stores/postgres-store.ts +304 -0
  237. package/src/plugins/parental/types.ts +180 -0
  238. package/src/plugins/profiles/__tests__/profiles-plugin.test.ts +321 -0
  239. package/src/plugins/profiles/index.ts +49 -0
  240. package/src/plugins/profiles/profiles-plugin.ts +546 -0
  241. package/src/plugins/profiles/stores/index.ts +9 -0
  242. package/src/plugins/profiles/stores/postgres-store.ts +439 -0
  243. package/src/plugins/profiles/types.ts +338 -0
  244. package/src/plugins/subscriptions/__tests__/subscriptions-plugin.test.ts +404 -0
  245. package/src/plugins/subscriptions/index.ts +51 -0
  246. package/src/plugins/subscriptions/stores/index.ts +9 -0
  247. package/src/plugins/subscriptions/stores/postgres-store.ts +482 -0
  248. package/src/plugins/subscriptions/subscriptions-plugin.ts +530 -0
  249. package/src/plugins/subscriptions/types.ts +355 -0
  250. package/src/plugins/usage/__tests__/usage-plugin.test.ts +288 -0
  251. package/src/plugins/usage/index.ts +39 -0
  252. package/src/plugins/usage/stores/index.ts +9 -0
  253. package/src/plugins/usage/stores/postgres-store.ts +213 -0
  254. package/src/plugins/usage/types.ts +222 -0
  255. package/src/plugins/usage/usage-plugin.ts +484 -0
  256. package/src/plugins/users/__tests__/postgres-store.test.ts +326 -0
  257. package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
  258. package/src/plugins/users/index.ts +6 -0
  259. package/src/plugins/users/stores/postgres-store.ts +104 -0
  260. package/src/plugins/users/types.ts +82 -6
  261. package/src/plugins/users/users-plugin.ts +37 -0
  262. package/ui/src/App.tsx +5 -1
  263. package/ui/src/api/controlPanelApi.ts +103 -6
  264. package/ui/src/components/StatCard.tsx +58 -0
  265. package/ui/src/dashboard/builtInWidgets.tsx +3 -1
  266. package/ui/src/dashboard/widgets/NotificationsStatsWidget.tsx +167 -0
  267. package/ui/src/dashboard/widgets/index.ts +1 -0
  268. package/ui/src/pages/NotificationsPage.tsx +417 -0
  269. package/ui/src/utils/formatters.ts +33 -0
  270. package/dist-ui/assets/index-D7DoZ9rL.js +0 -478
  271. 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
+ };