@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,759 @@
1
+ /**
2
+ * Parental Plugin
3
+ *
4
+ * Generic parental/guardian controls for @qwickapps/server.
5
+ * Supports PIN protection, profile restrictions, schedules, and activity logging.
6
+ * Uses adapters for domain-specific behavior (kids, gaming, education).
7
+ *
8
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
9
+ */
10
+
11
+ import { createHash } from 'crypto';
12
+ import type { Request, Response } from 'express';
13
+ import type { Plugin, PluginConfig, PluginRegistry } from '../../core/plugin-registry.js';
14
+ import type {
15
+ ParentalPluginConfig,
16
+ ParentalStore,
17
+ ParentalAdapter,
18
+ GuardianSettings,
19
+ ProfileRestriction,
20
+ ActivityLog,
21
+ AccessCheckResult,
22
+ CreateGuardianSettingsInput,
23
+ UpdateGuardianSettingsInput,
24
+ CreateRestrictionInput,
25
+ LogActivityInput,
26
+ } from './types.js';
27
+
28
+ // Store instances for helper access
29
+ let currentStore: ParentalStore | null = null;
30
+ let currentAdapter: ParentalAdapter | null = null;
31
+ let currentConfig: ParentalPluginConfig | null = null;
32
+
33
+ /**
34
+ * Hash a PIN using SHA-256
35
+ */
36
+ function hashPin(pin: string): string {
37
+ return createHash('sha256').update(pin).digest('hex');
38
+ }
39
+
40
+ /**
41
+ * Check if current time is within allowed schedule
42
+ */
43
+ function isWithinSchedule(schedule: Record<string, { start: string; end: string }>): boolean {
44
+ const now = new Date();
45
+ const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
46
+ const today = dayNames[now.getDay()];
47
+
48
+ const todaySchedule = schedule[today];
49
+ if (!todaySchedule) return true; // No schedule for today = allowed
50
+
51
+ const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
52
+
53
+ return currentTime >= todaySchedule.start && currentTime <= todaySchedule.end;
54
+ }
55
+
56
+ /**
57
+ * Create the Parental plugin
58
+ */
59
+ export function createParentalPlugin(config: ParentalPluginConfig): Plugin {
60
+ const debug = config.debug || false;
61
+ const apiPrefix = config.api?.prefix || '/parental';
62
+ const maxPinAttempts = config.maxPinAttempts || 5;
63
+ const pinLockoutMinutes = config.pinLockoutMinutes || 30;
64
+
65
+ function log(message: string, data?: Record<string, unknown>) {
66
+ if (debug) {
67
+ console.log(`[ParentalPlugin] ${message}`, data || '');
68
+ }
69
+ }
70
+
71
+ return {
72
+ id: 'parental',
73
+ name: 'Parental',
74
+ version: '1.0.0',
75
+
76
+ async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
77
+ log('Starting parental plugin');
78
+
79
+ // Initialize the store (creates tables if needed)
80
+ await config.store.initialize();
81
+ log('Parental store initialized');
82
+
83
+ // Store references for helper access
84
+ currentStore = config.store;
85
+ currentAdapter = config.adapter;
86
+ currentConfig = config;
87
+
88
+ // Register health check
89
+ registry.registerHealthCheck({
90
+ name: 'parental-store',
91
+ type: 'custom',
92
+ check: async () => {
93
+ try {
94
+ return { healthy: true };
95
+ } catch {
96
+ return { healthy: false };
97
+ }
98
+ },
99
+ });
100
+
101
+ // Add API routes if enabled
102
+ if (config.api?.enabled !== false) {
103
+ // ═══════════════════════════════════════════════════════════════════════
104
+ // Guardian Settings Routes
105
+ // ═══════════════════════════════════════════════════════════════════════
106
+
107
+ // Get guardian settings
108
+ registry.addRoute({
109
+ method: 'get',
110
+ path: `${apiPrefix}/settings/:userId`,
111
+ pluginId: 'parental',
112
+ handler: async (req: Request, res: Response) => {
113
+ try {
114
+ const { userId } = req.params;
115
+ const settings = await getGuardianSettings(userId);
116
+
117
+ if (!settings) {
118
+ return res.status(404).json({ error: 'Settings not found' });
119
+ }
120
+
121
+ // Don't expose PIN hash
122
+ const { pin_hash: _pin, ...safeSettings } = settings;
123
+ res.json({ ...safeSettings, has_pin: !!_pin });
124
+ } catch (error) {
125
+ console.error('[ParentalPlugin] Get settings error:', error);
126
+ res.status(500).json({ error: 'Failed to get settings' });
127
+ }
128
+ },
129
+ });
130
+
131
+ // Create guardian settings
132
+ registry.addRoute({
133
+ method: 'post',
134
+ path: `${apiPrefix}/settings`,
135
+ pluginId: 'parental',
136
+ handler: async (req: Request, res: Response) => {
137
+ try {
138
+ const input = req.body as CreateGuardianSettingsInput;
139
+
140
+ // Hash PIN if provided
141
+ const processedInput = {
142
+ ...input,
143
+ adapter_type: config.adapter.name,
144
+ pin: input.pin ? hashPin(input.pin) : undefined,
145
+ };
146
+
147
+ const settings = await createGuardianSettings(processedInput);
148
+ const { pin_hash: _pin, ...safeSettings } = settings;
149
+ res.status(201).json({ ...safeSettings, has_pin: !!_pin });
150
+ } catch (error) {
151
+ console.error('[ParentalPlugin] Create settings error:', error);
152
+ res.status(500).json({ error: 'Failed to create settings' });
153
+ }
154
+ },
155
+ });
156
+
157
+ // Update guardian settings
158
+ registry.addRoute({
159
+ method: 'patch',
160
+ path: `${apiPrefix}/settings/:userId`,
161
+ pluginId: 'parental',
162
+ handler: async (req: Request, res: Response) => {
163
+ try {
164
+ const { userId } = req.params;
165
+ const input = req.body as UpdateGuardianSettingsInput;
166
+
167
+ const settings = await updateGuardianSettings(userId, input);
168
+ if (!settings) {
169
+ return res.status(404).json({ error: 'Settings not found' });
170
+ }
171
+
172
+ const { pin_hash: _pin, ...safeSettings } = settings;
173
+ res.json({ ...safeSettings, has_pin: !!_pin });
174
+ } catch (error) {
175
+ console.error('[ParentalPlugin] Update settings error:', error);
176
+ res.status(500).json({ error: 'Failed to update settings' });
177
+ }
178
+ },
179
+ });
180
+
181
+ // Set PIN
182
+ registry.addRoute({
183
+ method: 'post',
184
+ path: `${apiPrefix}/settings/:userId/pin`,
185
+ pluginId: 'parental',
186
+ handler: async (req: Request, res: Response) => {
187
+ try {
188
+ const { userId } = req.params;
189
+ const { pin, current_pin } = req.body;
190
+
191
+ if (!pin || pin.length < 4) {
192
+ return res.status(400).json({ error: 'PIN must be at least 4 characters' });
193
+ }
194
+
195
+ // If user already has PIN, verify current PIN
196
+ const settings = await getGuardianSettings(userId);
197
+ if (settings?.pin_hash) {
198
+ if (!current_pin) {
199
+ return res.status(400).json({ error: 'Current PIN required to change PIN' });
200
+ }
201
+ const isValid = await verifyPin(userId, current_pin);
202
+ if (!isValid) {
203
+ return res.status(401).json({ error: 'Invalid current PIN' });
204
+ }
205
+ }
206
+
207
+ await setPin(userId, pin);
208
+ res.json({ success: true });
209
+ } catch (error) {
210
+ console.error('[ParentalPlugin] Set PIN error:', error);
211
+ res.status(500).json({ error: 'Failed to set PIN' });
212
+ }
213
+ },
214
+ });
215
+
216
+ // Verify PIN
217
+ registry.addRoute({
218
+ method: 'post',
219
+ path: `${apiPrefix}/settings/:userId/verify-pin`,
220
+ pluginId: 'parental',
221
+ handler: async (req: Request, res: Response) => {
222
+ try {
223
+ const { userId } = req.params;
224
+ const { pin } = req.body;
225
+
226
+ if (!pin) {
227
+ return res.status(400).json({ error: 'PIN required' });
228
+ }
229
+
230
+ const isValid = await verifyPin(userId, pin);
231
+
232
+ if (!isValid) {
233
+ // Increment failed attempts
234
+ const attempts = await incrementFailedPinAttempts(userId);
235
+
236
+ if (attempts >= maxPinAttempts) {
237
+ // Lock the account
238
+ const lockUntil = new Date(Date.now() + pinLockoutMinutes * 60 * 1000);
239
+ return res.status(423).json({
240
+ error: 'Account locked due to too many failed attempts',
241
+ locked_until: lockUntil,
242
+ attempts,
243
+ });
244
+ }
245
+
246
+ return res.status(401).json({
247
+ error: 'Invalid PIN',
248
+ attempts_remaining: maxPinAttempts - attempts,
249
+ });
250
+ }
251
+
252
+ // Reset failed attempts on success
253
+ await resetFailedPinAttempts(userId);
254
+ res.json({ success: true });
255
+ } catch (error) {
256
+ console.error('[ParentalPlugin] Verify PIN error:', error);
257
+ res.status(500).json({ error: 'Failed to verify PIN' });
258
+ }
259
+ },
260
+ });
261
+
262
+ // ═══════════════════════════════════════════════════════════════════════
263
+ // Profile Restrictions Routes
264
+ // ═══════════════════════════════════════════════════════════════════════
265
+
266
+ // Get restrictions for a profile
267
+ registry.addRoute({
268
+ method: 'get',
269
+ path: `${apiPrefix}/restrictions/:profileId`,
270
+ pluginId: 'parental',
271
+ handler: async (req: Request, res: Response) => {
272
+ try {
273
+ const { profileId } = req.params;
274
+ const restrictions = await getRestrictions(profileId);
275
+ res.json(restrictions);
276
+ } catch (error) {
277
+ console.error('[ParentalPlugin] Get restrictions error:', error);
278
+ res.status(500).json({ error: 'Failed to get restrictions' });
279
+ }
280
+ },
281
+ });
282
+
283
+ // Create restriction
284
+ registry.addRoute({
285
+ method: 'post',
286
+ path: `${apiPrefix}/restrictions`,
287
+ pluginId: 'parental',
288
+ handler: async (req: Request, res: Response) => {
289
+ try {
290
+ const input = req.body as CreateRestrictionInput;
291
+
292
+ // Validate with adapter
293
+ if (config.adapter.validateRestriction) {
294
+ const validation = config.adapter.validateRestriction(input);
295
+ if (!validation.valid) {
296
+ return res.status(400).json({ error: 'Invalid restriction', errors: validation.errors });
297
+ }
298
+ }
299
+
300
+ const restriction = await createRestriction(input);
301
+ res.status(201).json(restriction);
302
+ } catch (error) {
303
+ console.error('[ParentalPlugin] Create restriction error:', error);
304
+ res.status(500).json({ error: 'Failed to create restriction' });
305
+ }
306
+ },
307
+ });
308
+
309
+ // Update restriction
310
+ registry.addRoute({
311
+ method: 'patch',
312
+ path: `${apiPrefix}/restrictions/:id`,
313
+ pluginId: 'parental',
314
+ handler: async (req: Request, res: Response) => {
315
+ try {
316
+ const { id } = req.params;
317
+ const updates = req.body as Partial<ProfileRestriction>;
318
+
319
+ const restriction = await updateRestriction(id, updates);
320
+ if (!restriction) {
321
+ return res.status(404).json({ error: 'Restriction not found' });
322
+ }
323
+ res.json(restriction);
324
+ } catch (error) {
325
+ console.error('[ParentalPlugin] Update restriction error:', error);
326
+ res.status(500).json({ error: 'Failed to update restriction' });
327
+ }
328
+ },
329
+ });
330
+
331
+ // Delete restriction
332
+ registry.addRoute({
333
+ method: 'delete',
334
+ path: `${apiPrefix}/restrictions/:id`,
335
+ pluginId: 'parental',
336
+ handler: async (req: Request, res: Response) => {
337
+ try {
338
+ const { id } = req.params;
339
+ const deleted = await deleteRestriction(id);
340
+
341
+ if (!deleted) {
342
+ return res.status(404).json({ error: 'Restriction not found' });
343
+ }
344
+ res.status(204).send();
345
+ } catch (error) {
346
+ console.error('[ParentalPlugin] Delete restriction error:', error);
347
+ res.status(500).json({ error: 'Failed to delete restriction' });
348
+ }
349
+ },
350
+ });
351
+
352
+ // Pause profile
353
+ registry.addRoute({
354
+ method: 'post',
355
+ path: `${apiPrefix}/restrictions/:profileId/pause`,
356
+ pluginId: 'parental',
357
+ handler: async (req: Request, res: Response) => {
358
+ try {
359
+ const { profileId } = req.params;
360
+ const { until, reason } = req.body;
361
+
362
+ await pauseProfile(profileId, until ? new Date(until) : undefined, reason);
363
+ res.json({ success: true, paused: true });
364
+ } catch (error) {
365
+ console.error('[ParentalPlugin] Pause profile error:', error);
366
+ res.status(500).json({ error: 'Failed to pause profile' });
367
+ }
368
+ },
369
+ });
370
+
371
+ // Resume profile
372
+ registry.addRoute({
373
+ method: 'post',
374
+ path: `${apiPrefix}/restrictions/:profileId/resume`,
375
+ pluginId: 'parental',
376
+ handler: async (req: Request, res: Response) => {
377
+ try {
378
+ const { profileId } = req.params;
379
+ await resumeProfile(profileId);
380
+ res.json({ success: true, paused: false });
381
+ } catch (error) {
382
+ console.error('[ParentalPlugin] Resume profile error:', error);
383
+ res.status(500).json({ error: 'Failed to resume profile' });
384
+ }
385
+ },
386
+ });
387
+
388
+ // Check profile access
389
+ registry.addRoute({
390
+ method: 'get',
391
+ path: `${apiPrefix}/restrictions/:profileId/check`,
392
+ pluginId: 'parental',
393
+ handler: async (req: Request, res: Response) => {
394
+ try {
395
+ const { profileId } = req.params;
396
+ const result = await checkProfileAccess(profileId);
397
+ res.json(result);
398
+ } catch (error) {
399
+ console.error('[ParentalPlugin] Check access error:', error);
400
+ res.status(500).json({ error: 'Failed to check access' });
401
+ }
402
+ },
403
+ });
404
+
405
+ // ═══════════════════════════════════════════════════════════════════════
406
+ // Activity Log Routes
407
+ // ═══════════════════════════════════════════════════════════════════════
408
+
409
+ // Log activity
410
+ registry.addRoute({
411
+ method: 'post',
412
+ path: `${apiPrefix}/activity`,
413
+ pluginId: 'parental',
414
+ handler: async (req: Request, res: Response) => {
415
+ try {
416
+ const input = req.body as LogActivityInput;
417
+ const activity = await logActivity({
418
+ ...input,
419
+ adapter_type: config.adapter.name,
420
+ });
421
+ res.status(201).json(activity);
422
+ } catch (error) {
423
+ console.error('[ParentalPlugin] Log activity error:', error);
424
+ res.status(500).json({ error: 'Failed to log activity' });
425
+ }
426
+ },
427
+ });
428
+
429
+ // Get activity log
430
+ registry.addRoute({
431
+ method: 'get',
432
+ path: `${apiPrefix}/activity/:userId`,
433
+ pluginId: 'parental',
434
+ handler: async (req: Request, res: Response) => {
435
+ try {
436
+ const { userId } = req.params;
437
+ const limit = parseInt(req.query.limit as string) || 100;
438
+ const profileId = req.query.profileId as string | undefined;
439
+
440
+ const activities = await getActivityLog(userId, limit, profileId);
441
+
442
+ // Format details with adapter if available
443
+ const formattedActivities = activities.map((activity) => {
444
+ if (config.adapter.formatActivityDetails) {
445
+ return {
446
+ ...activity,
447
+ formatted_details: config.adapter.formatActivityDetails(activity),
448
+ };
449
+ }
450
+ return activity;
451
+ });
452
+
453
+ res.json(formattedActivities);
454
+ } catch (error) {
455
+ console.error('[ParentalPlugin] Get activity error:', error);
456
+ res.status(500).json({ error: 'Failed to get activity log' });
457
+ }
458
+ },
459
+ });
460
+
461
+ // Get adapter info (activity types, defaults)
462
+ registry.addRoute({
463
+ method: 'get',
464
+ path: `${apiPrefix}/adapter-info`,
465
+ pluginId: 'parental',
466
+ handler: async (_req: Request, res: Response) => {
467
+ try {
468
+ res.json({
469
+ name: config.adapter.name,
470
+ activity_types: config.adapter.getActivityTypes(),
471
+ default_daily_limit: config.adapter.getDefaultDailyLimit(),
472
+ });
473
+ } catch (error) {
474
+ console.error('[ParentalPlugin] Get adapter info error:', error);
475
+ res.status(500).json({ error: 'Failed to get adapter info' });
476
+ }
477
+ },
478
+ });
479
+ }
480
+
481
+ log('Parental plugin started');
482
+ },
483
+
484
+ async onStop(): Promise<void> {
485
+ log('Stopping parental plugin');
486
+ await config.store.shutdown();
487
+ currentStore = null;
488
+ currentAdapter = null;
489
+ currentConfig = null;
490
+ log('Parental plugin stopped');
491
+ },
492
+ };
493
+ }
494
+
495
+ // ═══════════════════════════════════════════════════════════════════════════
496
+ // Helper Functions
497
+ // ═══════════════════════════════════════════════════════════════════════════
498
+
499
+ /**
500
+ * Get the current parental store instance
501
+ */
502
+ export function getParentalStore(): ParentalStore | null {
503
+ return currentStore;
504
+ }
505
+
506
+ /**
507
+ * Get the current parental adapter instance
508
+ */
509
+ export function getParentalAdapter(): ParentalAdapter | null {
510
+ return currentAdapter;
511
+ }
512
+
513
+ // ─────────────────────────────────────────────────────────────────────────────
514
+ // Guardian Settings Helpers
515
+ // ─────────────────────────────────────────────────────────────────────────────
516
+
517
+ /**
518
+ * Get guardian settings for a user
519
+ */
520
+ export async function getGuardianSettings(userId: string): Promise<GuardianSettings | null> {
521
+ if (!currentStore) {
522
+ throw new Error('Parental plugin not initialized');
523
+ }
524
+ return currentStore.getSettings(userId);
525
+ }
526
+
527
+ /**
528
+ * Create guardian settings
529
+ */
530
+ export async function createGuardianSettings(input: CreateGuardianSettingsInput): Promise<GuardianSettings> {
531
+ if (!currentStore) {
532
+ throw new Error('Parental plugin not initialized');
533
+ }
534
+ return currentStore.createSettings(input);
535
+ }
536
+
537
+ /**
538
+ * Update guardian settings
539
+ */
540
+ export async function updateGuardianSettings(
541
+ userId: string,
542
+ input: UpdateGuardianSettingsInput
543
+ ): Promise<GuardianSettings | null> {
544
+ if (!currentStore) {
545
+ throw new Error('Parental plugin not initialized');
546
+ }
547
+ return currentStore.updateSettings(userId, input);
548
+ }
549
+
550
+ /**
551
+ * Set PIN for guardian
552
+ */
553
+ export async function setPin(userId: string, pin: string): Promise<void> {
554
+ if (!currentStore) {
555
+ throw new Error('Parental plugin not initialized');
556
+ }
557
+ const pinHash = hashPin(pin);
558
+ return currentStore.setPin(userId, pinHash);
559
+ }
560
+
561
+ /**
562
+ * Verify PIN
563
+ */
564
+ export async function verifyPin(userId: string, pin: string): Promise<boolean> {
565
+ if (!currentStore) {
566
+ throw new Error('Parental plugin not initialized');
567
+ }
568
+ const pinHash = hashPin(pin);
569
+ return currentStore.verifyPin(userId, pinHash);
570
+ }
571
+
572
+ /**
573
+ * Increment failed PIN attempts
574
+ */
575
+ export async function incrementFailedPinAttempts(userId: string): Promise<number> {
576
+ if (!currentStore) {
577
+ throw new Error('Parental plugin not initialized');
578
+ }
579
+ return currentStore.incrementFailedPinAttempts(userId);
580
+ }
581
+
582
+ /**
583
+ * Reset failed PIN attempts
584
+ */
585
+ export async function resetFailedPinAttempts(userId: string): Promise<void> {
586
+ if (!currentStore) {
587
+ throw new Error('Parental plugin not initialized');
588
+ }
589
+ return currentStore.resetFailedPinAttempts(userId);
590
+ }
591
+
592
+ // ─────────────────────────────────────────────────────────────────────────────
593
+ // Profile Restrictions Helpers
594
+ // ─────────────────────────────────────────────────────────────────────────────
595
+
596
+ /**
597
+ * Get restrictions for a profile
598
+ */
599
+ export async function getRestrictions(profileId: string): Promise<ProfileRestriction[]> {
600
+ if (!currentStore) {
601
+ throw new Error('Parental plugin not initialized');
602
+ }
603
+ return currentStore.getRestrictions(profileId);
604
+ }
605
+
606
+ /**
607
+ * Create a restriction
608
+ */
609
+ export async function createRestriction(input: CreateRestrictionInput): Promise<ProfileRestriction> {
610
+ if (!currentStore) {
611
+ throw new Error('Parental plugin not initialized');
612
+ }
613
+ return currentStore.createRestriction(input);
614
+ }
615
+
616
+ /**
617
+ * Update a restriction
618
+ */
619
+ export async function updateRestriction(
620
+ id: string,
621
+ updates: Partial<ProfileRestriction>
622
+ ): Promise<ProfileRestriction | null> {
623
+ if (!currentStore) {
624
+ throw new Error('Parental plugin not initialized');
625
+ }
626
+ return currentStore.updateRestriction(id, updates);
627
+ }
628
+
629
+ /**
630
+ * Delete a restriction (soft delete)
631
+ */
632
+ export async function deleteRestriction(id: string): Promise<boolean> {
633
+ if (!currentStore) {
634
+ throw new Error('Parental plugin not initialized');
635
+ }
636
+ return currentStore.deleteRestriction(id);
637
+ }
638
+
639
+ /**
640
+ * Pause a profile's access
641
+ */
642
+ export async function pauseProfile(profileId: string, until?: Date, reason?: string): Promise<void> {
643
+ if (!currentStore) {
644
+ throw new Error('Parental plugin not initialized');
645
+ }
646
+ return currentStore.pauseProfile(profileId, until, reason);
647
+ }
648
+
649
+ /**
650
+ * Resume a profile's access
651
+ */
652
+ export async function resumeProfile(profileId: string): Promise<void> {
653
+ if (!currentStore) {
654
+ throw new Error('Parental plugin not initialized');
655
+ }
656
+ return currentStore.resumeProfile(profileId);
657
+ }
658
+
659
+ /**
660
+ * Check if a profile has access based on restrictions
661
+ */
662
+ export async function checkProfileAccess(profileId: string): Promise<AccessCheckResult> {
663
+ if (!currentStore) {
664
+ throw new Error('Parental plugin not initialized');
665
+ }
666
+
667
+ const restrictions = await currentStore.getRestrictions(profileId);
668
+
669
+ // Check if any restrictions are paused
670
+ for (const restriction of restrictions) {
671
+ if (restriction.is_paused) {
672
+ // Check if pause has expired
673
+ if (restriction.pause_until && new Date() > restriction.pause_until) {
674
+ // Auto-resume (should be handled by cron, but check here too)
675
+ continue;
676
+ }
677
+ return {
678
+ allowed: false,
679
+ reason: restriction.pause_reason || 'Profile is paused',
680
+ };
681
+ }
682
+
683
+ // Check schedule restrictions
684
+ if (restriction.schedule) {
685
+ if (!isWithinSchedule(restriction.schedule)) {
686
+ // Find when access becomes available
687
+ const now = new Date();
688
+ const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
689
+ const today = dayNames[now.getDay()];
690
+ const todaySchedule = restriction.schedule[today];
691
+
692
+ let availableAt: Date | undefined;
693
+ if (todaySchedule) {
694
+ const [hours, minutes] = todaySchedule.start.split(':').map(Number);
695
+ availableAt = new Date(now);
696
+ availableAt.setHours(hours, minutes, 0, 0);
697
+
698
+ // If we're past today's start, set to tomorrow
699
+ if (now > availableAt) {
700
+ availableAt.setDate(availableAt.getDate() + 1);
701
+ }
702
+ }
703
+
704
+ return {
705
+ allowed: false,
706
+ reason: 'Outside allowed hours',
707
+ available_at: availableAt,
708
+ };
709
+ }
710
+ }
711
+ }
712
+
713
+ // Get time limit restriction and check remaining time
714
+ const timeLimitRestriction = restrictions.find((r) => r.restriction_type === 'time_limit');
715
+ if (timeLimitRestriction?.daily_limit_minutes) {
716
+ // Note: Actual time tracking would be done via usage-plugin
717
+ // This is a placeholder for the check result
718
+ return {
719
+ allowed: true,
720
+ minutes_remaining: timeLimitRestriction.daily_limit_minutes,
721
+ };
722
+ }
723
+
724
+ return { allowed: true };
725
+ }
726
+
727
+ // ─────────────────────────────────────────────────────────────────────────────
728
+ // Activity Log Helpers
729
+ // ─────────────────────────────────────────────────────────────────────────────
730
+
731
+ /**
732
+ * Log an activity
733
+ */
734
+ export async function logActivity(input: LogActivityInput): Promise<ActivityLog> {
735
+ if (!currentStore) {
736
+ throw new Error('Parental plugin not initialized');
737
+ }
738
+
739
+ // Call adapter hook if available
740
+ if (currentAdapter?.onRestrictionViolation && input.activity_type === 'restriction_violation') {
741
+ await currentAdapter.onRestrictionViolation(input.profile_id || '', input.details?.reason as string || 'Unknown');
742
+ }
743
+
744
+ if (currentAdapter?.onDailyLimitReached && input.activity_type === 'time_limit_reached') {
745
+ await currentAdapter.onDailyLimitReached(input.profile_id || '');
746
+ }
747
+
748
+ return currentStore.logActivity(input);
749
+ }
750
+
751
+ /**
752
+ * Get activity log
753
+ */
754
+ export async function getActivityLog(userId: string, limit = 100, profileId?: string): Promise<ActivityLog[]> {
755
+ if (!currentStore) {
756
+ throw new Error('Parental plugin not initialized');
757
+ }
758
+ return currentStore.getActivityLog(userId, limit, profileId);
759
+ }