@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,546 @@
1
+ /**
2
+ * Profiles Plugin
3
+ *
4
+ * Generic multi-profile management plugin for @qwickapps/server.
5
+ * Supports age-based categorization and content filtering.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import type { Request, Response } from 'express';
11
+ import type { Plugin, PluginConfig, PluginRegistry } from '../../core/plugin-registry.js';
12
+ import type {
13
+ ProfilesPluginConfig,
14
+ ProfileStore,
15
+ Profile,
16
+ CreateProfileInput,
17
+ UpdateProfileInput,
18
+ ProfileSearchParams,
19
+ AgeGroup,
20
+ ContentFilterLevel,
21
+ TimeRestrictionResult,
22
+ } from './types.js';
23
+
24
+ // Store instance for helper access
25
+ let currentStore: ProfileStore | null = null;
26
+ let currentConfig: ProfilesPluginConfig | null = null;
27
+
28
+ /**
29
+ * Calculate age from birth date
30
+ */
31
+ function calculateAge(birthDate: Date): number {
32
+ const today = new Date();
33
+ const birth = new Date(birthDate);
34
+ let age = today.getFullYear() - birth.getFullYear();
35
+ const monthDiff = today.getMonth() - birth.getMonth();
36
+ if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
37
+ age--;
38
+ }
39
+ return age;
40
+ }
41
+
42
+ /**
43
+ * Check if current time is within allowed hours
44
+ */
45
+ function isWithinAllowedHours(start?: string, end?: string): boolean {
46
+ if (!start || !end) {
47
+ return true; // No restriction
48
+ }
49
+
50
+ const now = new Date();
51
+ const currentTime = now.getHours() * 60 + now.getMinutes();
52
+
53
+ const [startHour, startMin] = start.split(':').map(Number);
54
+ const [endHour, endMin] = end.split(':').map(Number);
55
+
56
+ const startTime = startHour * 60 + startMin;
57
+ const endTime = endHour * 60 + endMin;
58
+
59
+ if (startTime <= endTime) {
60
+ // Normal range (e.g., 08:00 - 20:00)
61
+ return currentTime >= startTime && currentTime <= endTime;
62
+ } else {
63
+ // Overnight range (e.g., 20:00 - 08:00)
64
+ return currentTime >= startTime || currentTime <= endTime;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Create the Profiles plugin
70
+ */
71
+ export function createProfilesPlugin(config: ProfilesPluginConfig): Plugin {
72
+ const debug = config.debug || false;
73
+ const maxProfilesPerUser = config.maxProfilesPerUser || 10;
74
+ const defaultFilterLevel = config.defaultFilterLevel || 'moderate';
75
+ const apiPrefix = config.api?.prefix || '/profiles';
76
+
77
+ function log(message: string, data?: Record<string, unknown>) {
78
+ if (debug) {
79
+ console.log(`[ProfilesPlugin] ${message}`, data || '');
80
+ }
81
+ }
82
+
83
+ return {
84
+ id: 'profiles',
85
+ name: 'Profiles',
86
+ version: '1.0.0',
87
+
88
+ async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
89
+ log('Starting profiles plugin');
90
+
91
+ // Initialize the store (creates tables if needed)
92
+ await config.store.initialize();
93
+ log('Profiles plugin migrations complete');
94
+
95
+ // Store references for helper access
96
+ currentStore = config.store;
97
+ currentConfig = config;
98
+
99
+ // Register health check
100
+ registry.registerHealthCheck({
101
+ name: 'profiles-store',
102
+ type: 'custom',
103
+ check: async () => {
104
+ try {
105
+ await config.store.search({ limit: 1 });
106
+ return {
107
+ healthy: true,
108
+ details: {
109
+ maxProfilesPerUser,
110
+ defaultFilterLevel,
111
+ },
112
+ };
113
+ } catch {
114
+ return { healthy: false };
115
+ }
116
+ },
117
+ });
118
+
119
+ // Add API routes if enabled
120
+ if (config.api?.crud !== false) {
121
+ // List/Search profiles
122
+ registry.addRoute({
123
+ method: 'get',
124
+ path: apiPrefix,
125
+ pluginId: 'profiles',
126
+ handler: async (req: Request, res: Response) => {
127
+ try {
128
+ const params: ProfileSearchParams = {
129
+ org_id: req.query.org_id as string,
130
+ user_id: req.query.user_id as string,
131
+ age_group: req.query.age_group as AgeGroup,
132
+ is_active: req.query.is_active === 'true' ? true : req.query.is_active === 'false' ? false : undefined,
133
+ query: req.query.q as string,
134
+ page: parseInt(req.query.page as string) || 1,
135
+ limit: Math.min(parseInt(req.query.limit as string) || 20, 100),
136
+ sortBy: (req.query.sortBy as ProfileSearchParams['sortBy']) || 'created_at',
137
+ sortOrder: (req.query.sortOrder as ProfileSearchParams['sortOrder']) || 'desc',
138
+ };
139
+
140
+ const result = await config.store.search(params);
141
+ res.json(result);
142
+ } catch (error) {
143
+ console.error('[ProfilesPlugin] Search error:', error);
144
+ res.status(500).json({ error: 'Failed to search profiles' });
145
+ }
146
+ },
147
+ });
148
+
149
+ // Get profile by ID
150
+ registry.addRoute({
151
+ method: 'get',
152
+ path: `${apiPrefix}/:id`,
153
+ pluginId: 'profiles',
154
+ handler: async (req: Request, res: Response) => {
155
+ try {
156
+ const profile = await config.store.getById(req.params.id);
157
+ if (!profile) {
158
+ return res.status(404).json({ error: 'Profile not found' });
159
+ }
160
+ res.json(profile);
161
+ } catch (error) {
162
+ console.error('[ProfilesPlugin] Get profile error:', error);
163
+ res.status(500).json({ error: 'Failed to get profile' });
164
+ }
165
+ },
166
+ });
167
+
168
+ // Create profile
169
+ registry.addRoute({
170
+ method: 'post',
171
+ path: apiPrefix,
172
+ pluginId: 'profiles',
173
+ handler: async (req: Request, res: Response) => {
174
+ try {
175
+ const input: CreateProfileInput = {
176
+ org_id: req.body.org_id,
177
+ user_id: req.body.user_id,
178
+ name: req.body.name,
179
+ avatar: req.body.avatar,
180
+ birth_date: req.body.birth_date ? new Date(req.body.birth_date) : undefined,
181
+ age: req.body.age,
182
+ content_filter_level: req.body.content_filter_level || defaultFilterLevel,
183
+ daily_time_limit_minutes: req.body.daily_time_limit_minutes,
184
+ allowed_hours_start: req.body.allowed_hours_start,
185
+ allowed_hours_end: req.body.allowed_hours_end,
186
+ is_default: req.body.is_default,
187
+ metadata: req.body.metadata,
188
+ };
189
+
190
+ // Validate required fields
191
+ if (!input.user_id) {
192
+ return res.status(400).json({ error: 'user_id is required' });
193
+ }
194
+ if (!input.name || input.name.trim().length === 0) {
195
+ return res.status(400).json({ error: 'name is required' });
196
+ }
197
+
198
+ // Check profile limit
199
+ const currentCount = await config.store.getProfileCount(input.user_id);
200
+ if (currentCount >= maxProfilesPerUser) {
201
+ return res.status(400).json({
202
+ error: `Maximum profiles (${maxProfilesPerUser}) reached for this user`,
203
+ });
204
+ }
205
+
206
+ const profile = await config.store.create(input);
207
+ res.status(201).json(profile);
208
+ } catch (error) {
209
+ console.error('[ProfilesPlugin] Create profile error:', error);
210
+ res.status(500).json({ error: 'Failed to create profile' });
211
+ }
212
+ },
213
+ });
214
+
215
+ // Update profile
216
+ registry.addRoute({
217
+ method: 'put',
218
+ path: `${apiPrefix}/:id`,
219
+ pluginId: 'profiles',
220
+ handler: async (req: Request, res: Response) => {
221
+ try {
222
+ const input: UpdateProfileInput = {
223
+ name: req.body.name,
224
+ avatar: req.body.avatar,
225
+ birth_date: req.body.birth_date !== undefined
226
+ ? (req.body.birth_date ? new Date(req.body.birth_date) : null)
227
+ : undefined,
228
+ age: req.body.age,
229
+ content_filter_level: req.body.content_filter_level,
230
+ daily_time_limit_minutes: req.body.daily_time_limit_minutes,
231
+ allowed_hours_start: req.body.allowed_hours_start,
232
+ allowed_hours_end: req.body.allowed_hours_end,
233
+ is_active: req.body.is_active,
234
+ is_default: req.body.is_default,
235
+ metadata: req.body.metadata,
236
+ };
237
+
238
+ const profile = await config.store.update(req.params.id, input);
239
+ if (!profile) {
240
+ return res.status(404).json({ error: 'Profile not found' });
241
+ }
242
+ res.json(profile);
243
+ } catch (error) {
244
+ console.error('[ProfilesPlugin] Update profile error:', error);
245
+ res.status(500).json({ error: 'Failed to update profile' });
246
+ }
247
+ },
248
+ });
249
+
250
+ // Delete profile
251
+ registry.addRoute({
252
+ method: 'delete',
253
+ path: `${apiPrefix}/:id`,
254
+ pluginId: 'profiles',
255
+ handler: async (req: Request, res: Response) => {
256
+ try {
257
+ const deleted = await config.store.delete(req.params.id);
258
+ if (!deleted) {
259
+ return res.status(404).json({ error: 'Profile not found' });
260
+ }
261
+ res.status(204).send();
262
+ } catch (error) {
263
+ console.error('[ProfilesPlugin] Delete profile error:', error);
264
+ res.status(500).json({ error: 'Failed to delete profile' });
265
+ }
266
+ },
267
+ });
268
+
269
+ // List profiles for a user
270
+ registry.addRoute({
271
+ method: 'get',
272
+ path: `${apiPrefix}/user/:userId`,
273
+ pluginId: 'profiles',
274
+ handler: async (req: Request, res: Response) => {
275
+ try {
276
+ const profiles = await config.store.listByUser(req.params.userId);
277
+ res.json({ profiles });
278
+ } catch (error) {
279
+ console.error('[ProfilesPlugin] List user profiles error:', error);
280
+ res.status(500).json({ error: 'Failed to list profiles' });
281
+ }
282
+ },
283
+ });
284
+
285
+ // Get default profile for a user
286
+ registry.addRoute({
287
+ method: 'get',
288
+ path: `${apiPrefix}/user/:userId/default`,
289
+ pluginId: 'profiles',
290
+ handler: async (req: Request, res: Response) => {
291
+ try {
292
+ const profile = await config.store.getDefaultProfile(req.params.userId);
293
+ if (!profile) {
294
+ return res.status(404).json({ error: 'No default profile found' });
295
+ }
296
+ res.json(profile);
297
+ } catch (error) {
298
+ console.error('[ProfilesPlugin] Get default profile error:', error);
299
+ res.status(500).json({ error: 'Failed to get default profile' });
300
+ }
301
+ },
302
+ });
303
+
304
+ // Set default profile
305
+ registry.addRoute({
306
+ method: 'post',
307
+ path: `${apiPrefix}/:id/set-default`,
308
+ pluginId: 'profiles',
309
+ handler: async (req: Request, res: Response) => {
310
+ try {
311
+ const profile = await config.store.getById(req.params.id);
312
+ if (!profile) {
313
+ return res.status(404).json({ error: 'Profile not found' });
314
+ }
315
+
316
+ const success = await config.store.setDefaultProfile(req.params.id, profile.user_id);
317
+ if (!success) {
318
+ return res.status(500).json({ error: 'Failed to set default profile' });
319
+ }
320
+
321
+ const updated = await config.store.getById(req.params.id);
322
+ res.json(updated);
323
+ } catch (error) {
324
+ console.error('[ProfilesPlugin] Set default profile error:', error);
325
+ res.status(500).json({ error: 'Failed to set default profile' });
326
+ }
327
+ },
328
+ });
329
+
330
+ // Check time restrictions
331
+ registry.addRoute({
332
+ method: 'get',
333
+ path: `${apiPrefix}/:id/time-check`,
334
+ pluginId: 'profiles',
335
+ handler: async (req: Request, res: Response) => {
336
+ try {
337
+ const profile = await config.store.getById(req.params.id);
338
+ if (!profile) {
339
+ return res.status(404).json({ error: 'Profile not found' });
340
+ }
341
+
342
+ const result = checkTimeRestrictions(profile);
343
+ res.json(result);
344
+ } catch (error) {
345
+ console.error('[ProfilesPlugin] Time check error:', error);
346
+ res.status(500).json({ error: 'Failed to check time restrictions' });
347
+ }
348
+ },
349
+ });
350
+ }
351
+
352
+ log('Profiles plugin started');
353
+ },
354
+
355
+ async onStop(): Promise<void> {
356
+ log('Stopping profiles plugin');
357
+ await config.store.shutdown();
358
+ currentStore = null;
359
+ currentConfig = null;
360
+ log('Profiles plugin stopped');
361
+ },
362
+ };
363
+ }
364
+
365
+ // ═══════════════════════════════════════════════════════════════════════════
366
+ // Helper Functions
367
+ // ═══════════════════════════════════════════════════════════════════════════
368
+
369
+ /**
370
+ * Get the current profile store instance
371
+ */
372
+ export function getProfileStore(): ProfileStore | null {
373
+ return currentStore;
374
+ }
375
+
376
+ /**
377
+ * Create a new profile
378
+ */
379
+ export async function createProfile(input: CreateProfileInput): Promise<Profile> {
380
+ if (!currentStore || !currentConfig) {
381
+ throw new Error('Profiles plugin not initialized');
382
+ }
383
+
384
+ // Check profile limit
385
+ const currentCount = await currentStore.getProfileCount(input.user_id);
386
+ const maxProfiles = currentConfig.maxProfilesPerUser || 10;
387
+ if (currentCount >= maxProfiles) {
388
+ throw new Error(`Maximum profiles (${maxProfiles}) reached for this user`);
389
+ }
390
+
391
+ // Apply default filter level
392
+ if (!input.content_filter_level) {
393
+ input.content_filter_level = currentConfig.defaultFilterLevel || 'moderate';
394
+ }
395
+
396
+ return currentStore.create(input);
397
+ }
398
+
399
+ /**
400
+ * Get a profile by ID
401
+ */
402
+ export async function getProfileById(id: string): Promise<Profile | null> {
403
+ if (!currentStore) {
404
+ throw new Error('Profiles plugin not initialized');
405
+ }
406
+ return currentStore.getById(id);
407
+ }
408
+
409
+ /**
410
+ * Update a profile
411
+ */
412
+ export async function updateProfile(id: string, input: UpdateProfileInput): Promise<Profile | null> {
413
+ if (!currentStore) {
414
+ throw new Error('Profiles plugin not initialized');
415
+ }
416
+ return currentStore.update(id, input);
417
+ }
418
+
419
+ /**
420
+ * Delete a profile
421
+ */
422
+ export async function deleteProfile(id: string): Promise<boolean> {
423
+ if (!currentStore) {
424
+ throw new Error('Profiles plugin not initialized');
425
+ }
426
+ return currentStore.delete(id);
427
+ }
428
+
429
+ /**
430
+ * List profiles for a user
431
+ */
432
+ export async function listUserProfiles(userId: string): Promise<Profile[]> {
433
+ if (!currentStore) {
434
+ throw new Error('Profiles plugin not initialized');
435
+ }
436
+ return currentStore.listByUser(userId);
437
+ }
438
+
439
+ /**
440
+ * Get the default profile for a user
441
+ */
442
+ export async function getDefaultProfile(userId: string): Promise<Profile | null> {
443
+ if (!currentStore) {
444
+ throw new Error('Profiles plugin not initialized');
445
+ }
446
+ return currentStore.getDefaultProfile(userId);
447
+ }
448
+
449
+ /**
450
+ * Set a profile as the default
451
+ */
452
+ export async function setDefaultProfile(profileId: string, userId: string): Promise<boolean> {
453
+ if (!currentStore) {
454
+ throw new Error('Profiles plugin not initialized');
455
+ }
456
+ return currentStore.setDefaultProfile(profileId, userId);
457
+ }
458
+
459
+ /**
460
+ * Get profiles by age group
461
+ */
462
+ export async function getProfilesByAgeGroup(userId: string, ageGroup: AgeGroup): Promise<Profile[]> {
463
+ if (!currentStore) {
464
+ throw new Error('Profiles plugin not initialized');
465
+ }
466
+ return currentStore.getByAgeGroup(userId, ageGroup);
467
+ }
468
+
469
+ /**
470
+ * Get child profiles (age_group = 'child')
471
+ */
472
+ export async function getChildProfiles(userId: string): Promise<Profile[]> {
473
+ return getProfilesByAgeGroup(userId, 'child');
474
+ }
475
+
476
+ /**
477
+ * Get the current age for a profile
478
+ */
479
+ export function getProfileAge(profile: Profile): number | null {
480
+ if (profile.birth_date) {
481
+ return calculateAge(profile.birth_date);
482
+ }
483
+ return profile.age || null;
484
+ }
485
+
486
+ /**
487
+ * Check time restrictions for a profile
488
+ */
489
+ export function checkTimeRestrictions(profile: Profile): TimeRestrictionResult {
490
+ // Check allowed hours
491
+ const withinHours = isWithinAllowedHours(
492
+ profile.allowed_hours_start,
493
+ profile.allowed_hours_end
494
+ );
495
+
496
+ if (!withinHours) {
497
+ // Calculate when access will be available
498
+ const now = new Date();
499
+ let availableAt: Date | undefined;
500
+
501
+ if (profile.allowed_hours_start) {
502
+ const [hour, min] = profile.allowed_hours_start.split(':').map(Number);
503
+ availableAt = new Date(now);
504
+ availableAt.setHours(hour, min, 0, 0);
505
+ if (availableAt <= now) {
506
+ availableAt.setDate(availableAt.getDate() + 1);
507
+ }
508
+ }
509
+
510
+ return {
511
+ allowed: false,
512
+ reason: 'Outside allowed hours',
513
+ available_at: availableAt,
514
+ };
515
+ }
516
+
517
+ // Note: Daily time limit tracking requires usage plugin integration
518
+ // For now, just return allowed if within hours
519
+
520
+ return {
521
+ allowed: true,
522
+ minutes_remaining: profile.daily_time_limit_minutes || undefined,
523
+ };
524
+ }
525
+
526
+ /**
527
+ * Get content filter level for a profile
528
+ */
529
+ export function getContentFilterLevel(profile: Profile): ContentFilterLevel {
530
+ return profile.content_filter_level;
531
+ }
532
+
533
+ /**
534
+ * Check if profile can access content based on filter level
535
+ */
536
+ export function canAccessContent(
537
+ profile: Profile,
538
+ requiredLevel: ContentFilterLevel
539
+ ): boolean {
540
+ const levels: ContentFilterLevel[] = ['strict', 'moderate', 'minimal', 'none'];
541
+ const profileLevelIndex = levels.indexOf(profile.content_filter_level);
542
+ const requiredLevelIndex = levels.indexOf(requiredLevel);
543
+
544
+ // Profile level must be >= required level (stricter or equal)
545
+ return profileLevelIndex <= requiredLevelIndex;
546
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Profile Stores
3
+ *
4
+ * Export all profile storage implementations.
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ export { postgresProfileStore } from './postgres-store.js';