domma-cms 0.10.0 → 0.12.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.
- package/CLAUDE.md +248 -159
- package/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +7 -3
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/http-interceptor.js +1 -0
- package/admin/js/lib/safe-html.js +1 -0
- package/admin/js/templates/layouts.html +5 -4
- package/admin/js/templates/notifications.html +14 -0
- package/admin/js/templates/plugin-marketplace.html +16 -0
- package/admin/js/templates/plugins.html +17 -5
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/layouts.js +1 -16
- package/admin/js/views/notifications.js +1 -0
- package/admin/js/views/plugin-marketplace.js +1 -0
- package/admin/js/views/plugins.js +16 -16
- package/config/navigation.json +5 -72
- package/config/plugins.json +10 -14
- package/config/presets.json +50 -13
- package/config/site.json +11 -63
- package/package.json +2 -1
- package/plugins/_template/admin/templates/index.html +17 -0
- package/plugins/_template/admin/views/index.js +19 -0
- package/plugins/_template/config.js +8 -0
- package/plugins/_template/plugin.js +23 -0
- package/plugins/_template/plugin.json +34 -0
- package/plugins/analytics/plugin.json +41 -31
- package/plugins/blog/admin/templates/blog.html +22 -0
- package/plugins/blog/admin/templates/categories.html +7 -0
- package/plugins/blog/admin/templates/comments.html +11 -0
- package/plugins/blog/admin/templates/post-editor.html +97 -0
- package/plugins/blog/admin/templates/settings.html +11 -0
- package/plugins/blog/admin/views/blog.js +183 -0
- package/plugins/blog/admin/views/categories.js +235 -0
- package/plugins/blog/admin/views/comments.js +187 -0
- package/plugins/blog/admin/views/post-editor.js +291 -0
- package/plugins/blog/admin/views/settings.js +100 -0
- package/plugins/blog/collections/categories/schema.json +12 -0
- package/plugins/blog/collections/comments/schema.json +16 -0
- package/plugins/blog/collections/posts/schema.json +19 -0
- package/plugins/blog/config.js +8 -0
- package/plugins/blog/plugin.js +352 -0
- package/plugins/blog/plugin.json +96 -0
- package/plugins/blog/roles/blog-author.json +10 -0
- package/plugins/blog/roles/blog-editor.json +12 -0
- package/plugins/blog/templates/author.html +9 -0
- package/plugins/blog/templates/category.html +9 -0
- package/plugins/blog/templates/index.html +9 -0
- package/plugins/blog/templates/post.html +17 -0
- package/plugins/blog/templates/tag.html +9 -0
- package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
- package/plugins/contacts/collections/user-contacts/schema.json +1 -1
- package/plugins/contacts/plugin.js +4 -10
- package/plugins/contacts/plugin.json +13 -3
- package/plugins/notes/collections/user-notes/schema.json +1 -1
- package/plugins/notes/plugin.js +3 -9
- package/plugins/notes/plugin.json +13 -3
- package/plugins/site-search/plugin.json +5 -2
- package/plugins/theme-switcher/plugin.json +1 -1
- package/plugins/todo/collections/todos/schema.json +1 -1
- package/plugins/todo/plugin.js +3 -9
- package/plugins/todo/plugin.json +13 -3
- package/public/css/site.css +1 -1
- package/scripts/build.js +48 -0
- package/scripts/create-plugin.js +113 -0
- package/scripts/fresh.js +6 -7
- package/scripts/gen-instance-secret.js +46 -0
- package/scripts/reset.js +3 -3
- package/scripts/setup.js +31 -13
- package/server/middleware/auth.js +48 -0
- package/server/middleware/managerAuth.js +36 -0
- package/server/routes/api/actions.js +1 -1
- package/server/routes/api/auth.js +4 -3
- package/server/routes/api/layouts.js +173 -49
- package/server/routes/api/notifications.js +155 -0
- package/server/routes/api/plugin-marketplace.js +75 -0
- package/server/routes/api/users.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/public.js +4 -9
- package/server/server.js +32 -3
- package/server/services/actions.js +1 -1
- package/server/services/managerClient.js +182 -0
- package/server/services/permissionRegistry.js +245 -173
- package/server/services/pluginInstaller.js +301 -0
- package/server/services/plugins.js +117 -10
- package/server/services/presetCollections.js +66 -251
- package/server/services/renderer.js +99 -0
- package/server/services/roles.js +191 -39
- package/server/services/users.js +1 -1
- package/server/services/views.js +1 -1
- package/server/templates/page.html +2 -2
- package/plugins/docs/admin/templates/docs.html +0 -69
- package/plugins/docs/admin/views/docs.js +0 -276
- package/plugins/docs/config.js +0 -8
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
- package/plugins/docs/data/folders.json +0 -9
- package/plugins/docs/data/templates.json +0 -1
- package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
- package/plugins/docs/plugin.js +0 -375
- package/plugins/docs/plugin.json +0 -23
- package/plugins/form-builder/data/forms/contacts.json +0 -66
- package/plugins/form-builder/data/forms/enquiries.json +0 -103
- package/plugins/form-builder/data/forms/feedback.json +0 -131
- package/plugins/form-builder/data/forms/notes.json +0 -79
- package/plugins/form-builder/data/forms/to-do.json +0 -100
- package/plugins/form-builder/data/submissions/contacts.json +0 -1
- package/plugins/form-builder/data/submissions/enquiries.json +0 -1
- package/plugins/form-builder/data/submissions/feedback.json +0 -1
- package/plugins/form-builder/data/submissions/notes.json +0 -1
- package/plugins/form-builder/data/submissions/to-do.json +0 -1
- package/plugins/garage/admin/templates/garage.html +0 -111
- package/plugins/garage/admin/views/garage.js +0 -622
- package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
- package/plugins/garage/config.js +0 -18
- package/plugins/garage/data/vehicles.json +0 -70
- package/plugins/garage/plugin.js +0 -398
- package/plugins/garage/plugin.json +0 -33
- package/scripts/seed.js +0 -1996
- package/server/services/userTypes.js +0 -227
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"slug": "garage-vehicles",
|
|
3
|
-
"title": "Garage Vehicles",
|
|
4
|
-
"description": "Vehicle registrations managed by the Garage plugin.",
|
|
5
|
-
"plugin": "garage",
|
|
6
|
-
"fields": [
|
|
7
|
-
{
|
|
8
|
-
"name": "userId",
|
|
9
|
-
"label": "User ID",
|
|
10
|
-
"type": "text",
|
|
11
|
-
"required": true
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"name": "registrationNumber",
|
|
15
|
-
"label": "Registration",
|
|
16
|
-
"type": "text",
|
|
17
|
-
"required": true
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"name": "make",
|
|
21
|
-
"label": "Make",
|
|
22
|
-
"type": "text"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"name": "colour",
|
|
26
|
-
"label": "Colour",
|
|
27
|
-
"type": "text"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"name": "yearOfManufacture",
|
|
31
|
-
"label": "Year",
|
|
32
|
-
"type": "text"
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"name": "fuelType",
|
|
36
|
-
"label": "Fuel Type",
|
|
37
|
-
"type": "text"
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"name": "engineCapacity",
|
|
41
|
-
"label": "Engine Capacity",
|
|
42
|
-
"type": "text"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"name": "co2Emissions",
|
|
46
|
-
"label": "CO2 Emissions",
|
|
47
|
-
"type": "text"
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
"name": "motStatus",
|
|
51
|
-
"label": "MOT Status",
|
|
52
|
-
"type": "text"
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"name": "motExpiryDate",
|
|
56
|
-
"label": "MOT Expiry",
|
|
57
|
-
"type": "text"
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
"name": "taxStatus",
|
|
61
|
-
"label": "Tax Status",
|
|
62
|
-
"type": "text"
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
"name": "taxDueDate",
|
|
66
|
-
"label": "Tax Due",
|
|
67
|
-
"type": "text"
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
"name": "isSaved",
|
|
71
|
-
"label": "Saved",
|
|
72
|
-
"type": "text"
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"name": "lookupDate",
|
|
76
|
-
"label": "Lookup Date",
|
|
77
|
-
"type": "text"
|
|
78
|
-
}
|
|
79
|
-
],
|
|
80
|
-
"api": {
|
|
81
|
-
"create": {
|
|
82
|
-
"enabled": false,
|
|
83
|
-
"access": "admin"
|
|
84
|
-
},
|
|
85
|
-
"read": {
|
|
86
|
-
"enabled": false,
|
|
87
|
-
"access": "admin"
|
|
88
|
-
},
|
|
89
|
-
"update": {
|
|
90
|
-
"enabled": false,
|
|
91
|
-
"access": "admin"
|
|
92
|
-
},
|
|
93
|
-
"delete": {
|
|
94
|
-
"enabled": false,
|
|
95
|
-
"access": "admin"
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
"storage": {
|
|
99
|
-
"adapter": "file"
|
|
100
|
-
}
|
|
101
|
-
}
|
package/plugins/garage/config.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Garage Plugin — Default Configuration
|
|
3
|
-
*
|
|
4
|
-
* Override any of these via config/plugins.json under "garage.settings".
|
|
5
|
-
*/
|
|
6
|
-
export default {
|
|
7
|
-
/** DVLA Vehicle Enquiry Service API key. Obtain from developer-portal.driver-vehicle-licensing.api.gov.uk */
|
|
8
|
-
dvlaApiKey: '',
|
|
9
|
-
/** DVLA API endpoint — do not change unless DVLA updates their URL. */
|
|
10
|
-
dvlaApiUrl: 'https://driver-vehicle-licensing.api.gov.uk/vehicle-enquiry/v1/vehicles',
|
|
11
|
-
/** Per-user rate limiting for the /lookup endpoint. */
|
|
12
|
-
rateLimit: {
|
|
13
|
-
max: 10,
|
|
14
|
-
windowSeconds: 60
|
|
15
|
-
},
|
|
16
|
-
/** Maximum lookup history entries to retain per user (oldest unsaved entries are pruned first). */
|
|
17
|
-
maxHistoryItems: 100
|
|
18
|
-
};
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"id": "d1757874-3a2d-4894-99ae-57fd934a9a8b",
|
|
4
|
-
"userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
|
|
5
|
-
"registrationNumber": "GIL7698",
|
|
6
|
-
"make": "BMW",
|
|
7
|
-
"colour": "GREY",
|
|
8
|
-
"yearOfManufacture": 2017,
|
|
9
|
-
"fuelType": "DIESEL",
|
|
10
|
-
"engineCapacity": 1995,
|
|
11
|
-
"co2Emissions": 134,
|
|
12
|
-
"motStatus": "Valid",
|
|
13
|
-
"motExpiryDate": "2027-03-26",
|
|
14
|
-
"taxStatus": "Taxed",
|
|
15
|
-
"taxDueDate": "2027-03-01",
|
|
16
|
-
"isSaved": true,
|
|
17
|
-
"lookupDate": "2026-03-28T21:32:31.725Z"
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"id": "3503abbc-5bb9-4f96-9b13-1ab389f557b4",
|
|
21
|
-
"userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
|
|
22
|
-
"registrationNumber": "LF16BMV",
|
|
23
|
-
"make": "TESLA",
|
|
24
|
-
"colour": "BLUE",
|
|
25
|
-
"yearOfManufacture": 2016,
|
|
26
|
-
"fuelType": "ELECTRICITY",
|
|
27
|
-
"engineCapacity": 0,
|
|
28
|
-
"co2Emissions": 0,
|
|
29
|
-
"motStatus": "Valid",
|
|
30
|
-
"motExpiryDate": "2026-12-16",
|
|
31
|
-
"taxStatus": "Taxed",
|
|
32
|
-
"taxDueDate": "2026-12-01",
|
|
33
|
-
"isSaved": true,
|
|
34
|
-
"lookupDate": "2026-03-28T21:32:48.814Z"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"id": "c50c6b66-bc48-4bd1-a8fc-7dbc669f61b5",
|
|
38
|
-
"userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
|
|
39
|
-
"registrationNumber": "CN64NFR",
|
|
40
|
-
"make": "FORD",
|
|
41
|
-
"colour": "RED",
|
|
42
|
-
"yearOfManufacture": 2014,
|
|
43
|
-
"fuelType": "PETROL",
|
|
44
|
-
"engineCapacity": 1242,
|
|
45
|
-
"co2Emissions": 115,
|
|
46
|
-
"motStatus": "Valid",
|
|
47
|
-
"motExpiryDate": "2027-01-25",
|
|
48
|
-
"taxStatus": "Taxed",
|
|
49
|
-
"taxDueDate": "2027-01-01",
|
|
50
|
-
"isSaved": true,
|
|
51
|
-
"lookupDate": "2026-03-28T21:32:55.731Z"
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"id": "cfd54f2a-7bab-469d-b929-d682a1313154",
|
|
55
|
-
"userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
|
|
56
|
-
"registrationNumber": "PK75SFO",
|
|
57
|
-
"make": "VOLKSWAGEN",
|
|
58
|
-
"colour": "BLUE",
|
|
59
|
-
"yearOfManufacture": 2025,
|
|
60
|
-
"fuelType": "PETROL",
|
|
61
|
-
"engineCapacity": 999,
|
|
62
|
-
"co2Emissions": 121,
|
|
63
|
-
"motStatus": "No details held by DVLA",
|
|
64
|
-
"motExpiryDate": null,
|
|
65
|
-
"taxStatus": "Taxed",
|
|
66
|
-
"taxDueDate": "2026-10-01",
|
|
67
|
-
"isSaved": true,
|
|
68
|
-
"lookupDate": "2026-03-28T21:49:25.814Z"
|
|
69
|
-
}
|
|
70
|
-
]
|
package/plugins/garage/plugin.js
DELETED
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Garage Plugin — Backend
|
|
3
|
-
*
|
|
4
|
-
* Proxies DVLA vehicle lookups and stores per-user vehicle history via the
|
|
5
|
-
* CMS collections service (garage-vehicles collection). Uses MongoDB when
|
|
6
|
-
* configured, falls back to file-based storage automatically.
|
|
7
|
-
* All routes are mounted at /api/plugins/garage/ by the plugin system.
|
|
8
|
-
*
|
|
9
|
-
* Routes:
|
|
10
|
-
* POST /lookup — Proxy DVLA lookup, upsert into history
|
|
11
|
-
* GET /vehicles — Saved vehicles for the authenticated user
|
|
12
|
-
* GET /history — Full lookup history for the authenticated user
|
|
13
|
-
* PATCH /vehicles/:id/save — Toggle isSaved on a vehicle entry
|
|
14
|
-
* DELETE /vehicles/:id — Remove a vehicle entry from history
|
|
15
|
-
*/
|
|
16
|
-
import fs from 'fs';
|
|
17
|
-
import path from 'path';
|
|
18
|
-
import {fileURLToPath} from 'url';
|
|
19
|
-
import {
|
|
20
|
-
createEntry,
|
|
21
|
-
deleteEntry,
|
|
22
|
-
getEntry,
|
|
23
|
-
listEntries,
|
|
24
|
-
updateEntry
|
|
25
|
-
} from '../../server/services/collections.js';
|
|
26
|
-
import defaultConfig from './config.js';
|
|
27
|
-
|
|
28
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
-
const COLLECTION = 'garage-vehicles';
|
|
30
|
-
const VEHICLES_DATA = path.join(__dirname, '../../content/collections/garage-vehicles/data.json');
|
|
31
|
-
|
|
32
|
-
/** In-memory rate limit tracker: Map<userId, { count: number, windowStart: number }> */
|
|
33
|
-
const rateLimits = new Map();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Flatten a collection entry into the vehicle model the frontend expects.
|
|
38
|
-
*
|
|
39
|
-
* @param {{ id: string, data: object }} entry
|
|
40
|
-
* @returns {object}
|
|
41
|
-
*/
|
|
42
|
-
function toVehicle(entry) {
|
|
43
|
-
return {
|
|
44
|
-
id: entry.id,
|
|
45
|
-
...entry.data,
|
|
46
|
-
isSaved: entry.data.isSaved === true || entry.data.isSaved === 'true'
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Check and increment the per-user rate limit.
|
|
52
|
-
*
|
|
53
|
-
* @param {string} userId
|
|
54
|
-
* @param {{ max: number, windowSeconds: number }} config
|
|
55
|
-
* @returns {{ allowed: boolean, retryAfter: number }} retryAfter is seconds until reset (0 if allowed)
|
|
56
|
-
*/
|
|
57
|
-
function checkRateLimit(userId, config) {
|
|
58
|
-
const now = Date.now();
|
|
59
|
-
const windowMs = config.windowSeconds * 1000;
|
|
60
|
-
const entry = rateLimits.get(userId);
|
|
61
|
-
|
|
62
|
-
if (!entry || now - entry.windowStart >= windowMs) {
|
|
63
|
-
rateLimits.set(userId, {count: 1, windowStart: now});
|
|
64
|
-
return {allowed: true, retryAfter: 0};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (entry.count >= config.max) {
|
|
68
|
-
const retryAfter = Math.ceil((entry.windowStart + windowMs - now) / 1000);
|
|
69
|
-
return {allowed: false, retryAfter};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
entry.count += 1;
|
|
73
|
-
return {allowed: true, retryAfter: 0};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Normalise a registration number: uppercase and strip all spaces.
|
|
78
|
-
*
|
|
79
|
-
* @param {string} reg
|
|
80
|
-
* @returns {string}
|
|
81
|
-
*/
|
|
82
|
-
function normaliseReg(reg) {
|
|
83
|
-
return String(reg).toUpperCase().replace(/\s+/g, '');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Map a DVLA API response to our internal vehicle data shape.
|
|
88
|
-
*
|
|
89
|
-
* @param {string} userId
|
|
90
|
-
* @param {object} dvlaData - Raw response from DVLA API
|
|
91
|
-
* @returns {object}
|
|
92
|
-
*/
|
|
93
|
-
function mapDvlaResponse(userId, dvlaData) {
|
|
94
|
-
return {
|
|
95
|
-
userId,
|
|
96
|
-
registrationNumber: dvlaData.registrationNumber ?? '',
|
|
97
|
-
make: dvlaData.make ?? '',
|
|
98
|
-
colour: dvlaData.colour ?? '',
|
|
99
|
-
yearOfManufacture: dvlaData.yearOfManufacture ?? null,
|
|
100
|
-
fuelType: dvlaData.fuelType ?? '',
|
|
101
|
-
engineCapacity: dvlaData.engineCapacity ?? null,
|
|
102
|
-
co2Emissions: dvlaData.co2Emissions ?? null,
|
|
103
|
-
motStatus: dvlaData.motStatus ?? '',
|
|
104
|
-
motExpiryDate: dvlaData.motExpiryDate ?? null,
|
|
105
|
-
taxStatus: dvlaData.taxStatus ?? '',
|
|
106
|
-
taxDueDate: dvlaData.taxDueDate ?? null,
|
|
107
|
-
isSaved: false,
|
|
108
|
-
lookupDate: new Date().toISOString()
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Escape HTML special characters for safe server-side rendering.
|
|
114
|
-
*
|
|
115
|
-
* @param {*} str
|
|
116
|
-
* @returns {string}
|
|
117
|
-
*/
|
|
118
|
-
function escHtml(str) {
|
|
119
|
-
return String(str ?? '')
|
|
120
|
-
.replace(/&/g, '&')
|
|
121
|
-
.replace(/</g, '<')
|
|
122
|
-
.replace(/>/g, '>')
|
|
123
|
-
.replace(/"/g, '"');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Render a list of saved vehicle objects as a static HTML card grid.
|
|
128
|
-
* Used by the [garage-vehicles /] shortcode for front-end display.
|
|
129
|
-
*
|
|
130
|
-
* @param {object[]} vehicles
|
|
131
|
-
* @returns {string} HTML string
|
|
132
|
-
*/
|
|
133
|
-
function renderVehiclesHtml(vehicles) {
|
|
134
|
-
const cards = vehicles.map(v => {
|
|
135
|
-
const motLv = (v.motStatus || '').toLowerCase();
|
|
136
|
-
const motCls = motLv === 'valid' ? 'badge-success' : motLv ? 'badge-danger' : 'badge-secondary';
|
|
137
|
-
const taxLv = (v.taxStatus || '').toLowerCase();
|
|
138
|
-
const taxCls = taxLv === 'taxed' ? 'badge-success' : taxLv === 'sorn' ? 'badge-warning' : taxLv ? 'badge-danger' : 'badge-secondary';
|
|
139
|
-
|
|
140
|
-
const details = [
|
|
141
|
-
v.colour,
|
|
142
|
-
v.fuelType,
|
|
143
|
-
v.engineCapacity ? `${v.engineCapacity}cc` : null,
|
|
144
|
-
v.co2Emissions ? `${v.co2Emissions}g/km` : null
|
|
145
|
-
].filter(Boolean).join(' · ');
|
|
146
|
-
|
|
147
|
-
const motExpiry = v.motExpiryDate
|
|
148
|
-
? `<span style="font-size:0.78rem;color:var(--dm-text-muted,#888);">Exp ${escHtml(v.motExpiryDate)}</span>` : '';
|
|
149
|
-
const taxDue = v.taxDueDate
|
|
150
|
-
? `<span style="font-size:0.78rem;color:var(--dm-text-muted,#888);">Due ${escHtml(v.taxDueDate)}</span>` : '';
|
|
151
|
-
|
|
152
|
-
return `<div class="card" style="flex:1;min-width:260px;max-width:340px;">
|
|
153
|
-
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;">
|
|
154
|
-
<span style="background:#FFD900;color:#000;font-family:'Arial Black',Arial,sans-serif;font-weight:900;font-size:1rem;letter-spacing:3px;padding:0.15rem 0.5rem;border-radius:4px;border:2px solid #000;">${escHtml(v.registrationNumber || '')}</span>
|
|
155
|
-
<span style="font-size:0.85rem;color:var(--dm-text-muted,#888);">${escHtml(v.make || '')}${v.yearOfManufacture ? ` · ${escHtml(String(v.yearOfManufacture))}` : ''}</span>
|
|
156
|
-
</div>
|
|
157
|
-
<div class="card-body">
|
|
158
|
-
${details ? `<p style="font-size:0.85rem;color:var(--dm-text-muted,#888);margin:0 0 0.6rem;">${escHtml(details)}</p>` : ''}
|
|
159
|
-
<div style="display:flex;gap:0.4rem;flex-wrap:wrap;align-items:center;">
|
|
160
|
-
<span class="badge ${motCls}">MOT: ${escHtml(v.motStatus || 'Unknown')}</span>
|
|
161
|
-
${motExpiry}
|
|
162
|
-
<span class="badge ${taxCls}">Tax: ${escHtml(v.taxStatus || 'Unknown')}</span>
|
|
163
|
-
${taxDue}
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
</div>`;
|
|
167
|
-
}).join('\n');
|
|
168
|
-
|
|
169
|
-
return `<div style="display:flex;flex-wrap:wrap;gap:1rem;">\n${cards}\n</div>`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export default async function garagePlugin(fastify, options) {
|
|
173
|
-
const {authenticate} = options.auth;
|
|
174
|
-
const {registerShortcode} = options.hooks;
|
|
175
|
-
const cfg = {...defaultConfig, ...(options.settings || {})};
|
|
176
|
-
cfg.dvlaApiKey = process.env.DVLA_API_KEY || cfg.dvlaApiKey;
|
|
177
|
-
|
|
178
|
-
// --- [garage-vehicles /] shortcode --------------------------------------
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Renders saved vehicles as a static HTML card grid on public pages.
|
|
182
|
-
* Reads data synchronously so it fits the markdown pipeline.
|
|
183
|
-
*
|
|
184
|
-
* Usage:
|
|
185
|
-
* [garage-vehicles /] — all saved vehicles
|
|
186
|
-
* [garage-vehicles user="uuid" /] — one user's saved vehicles
|
|
187
|
-
*/
|
|
188
|
-
registerShortcode('garage-vehicles', (attrStr, _body, ctx) => {
|
|
189
|
-
const attrs = ctx.parseShortcodeAttrs(attrStr);
|
|
190
|
-
const userId = attrs.user || null;
|
|
191
|
-
|
|
192
|
-
let entries;
|
|
193
|
-
try {
|
|
194
|
-
entries = JSON.parse(fs.readFileSync(VEHICLES_DATA, 'utf8'));
|
|
195
|
-
} catch {
|
|
196
|
-
return '';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
let vehicles = entries
|
|
200
|
-
.filter(e => e.data && (e.data.isSaved === true || e.data.isSaved === 'true'))
|
|
201
|
-
.map(e => ({id: e.id, ...e.data, isSaved: true}));
|
|
202
|
-
|
|
203
|
-
if (userId) vehicles = vehicles.filter(v => v.userId === userId);
|
|
204
|
-
if (!vehicles.length) return '';
|
|
205
|
-
|
|
206
|
-
return renderVehiclesHtml(vehicles);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// --- POST /lookup -------------------------------------------------------
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Proxy a DVLA vehicle lookup and upsert the result into the user's history.
|
|
213
|
-
* Returns 503 if no DVLA API key is configured.
|
|
214
|
-
* Returns 429 if the user has exceeded the rate limit.
|
|
215
|
-
*/
|
|
216
|
-
fastify.post('/lookup', {preHandler: [authenticate]}, async (request, reply) => {
|
|
217
|
-
const userId = request.user.id;
|
|
218
|
-
const {registrationNumber} = request.body || {};
|
|
219
|
-
|
|
220
|
-
if (!registrationNumber || typeof registrationNumber !== 'string') {
|
|
221
|
-
return reply.status(400).send({error: 'registrationNumber is required'});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const reg = normaliseReg(registrationNumber);
|
|
225
|
-
if (!reg) {
|
|
226
|
-
return reply.status(400).send({error: 'Invalid registration number'});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const rl = checkRateLimit(userId, cfg.rateLimit);
|
|
230
|
-
if (!rl.allowed) {
|
|
231
|
-
return reply.status(429).send({
|
|
232
|
-
error: `Rate limit reached. Try again in ${rl.retryAfter} seconds.`,
|
|
233
|
-
retryAfter: rl.retryAfter
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (!cfg.dvlaApiKey) {
|
|
238
|
-
return reply.status(503).send({
|
|
239
|
-
error: 'DVLA API key not configured. Add dvlaApiKey to the Garage plugin settings in config/plugins.json.'
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
let dvlaData;
|
|
244
|
-
try {
|
|
245
|
-
const dvlaRes = await fetch(cfg.dvlaApiUrl, {
|
|
246
|
-
method: 'POST',
|
|
247
|
-
headers: {
|
|
248
|
-
'x-api-key': cfg.dvlaApiKey,
|
|
249
|
-
'Content-Type': 'application/json'
|
|
250
|
-
},
|
|
251
|
-
body: JSON.stringify({registrationNumber: reg})
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
if (dvlaRes.status === 404) {
|
|
255
|
-
return reply.status(404).send({error: 'Vehicle not found. Check the registration number.'});
|
|
256
|
-
}
|
|
257
|
-
if (!dvlaRes.ok) {
|
|
258
|
-
const body = await dvlaRes.text().catch(() => '');
|
|
259
|
-
fastify.log.warn({status: dvlaRes.status, body}, '[garage] DVLA API error');
|
|
260
|
-
return reply.status(502).send({error: 'DVLA API returned an error. Please try again.'});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
dvlaData = await dvlaRes.json();
|
|
264
|
-
} catch (err) {
|
|
265
|
-
fastify.log.error({err}, '[garage] Failed to reach DVLA API');
|
|
266
|
-
return reply.status(502).send({error: 'Could not reach the DVLA API. Check your network connection.'});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Upsert: update existing entry if same reg+user, otherwise create
|
|
270
|
-
const {entries: all} = await listEntries(COLLECTION, {limit: 10000});
|
|
271
|
-
const existing = all.find(e => e.data.userId === userId && e.data.registrationNumber === reg);
|
|
272
|
-
|
|
273
|
-
let vehicle;
|
|
274
|
-
if (existing) {
|
|
275
|
-
const updated = await updateEntry(COLLECTION, existing.id, {
|
|
276
|
-
...mapDvlaResponse(userId, dvlaData),
|
|
277
|
-
isSaved: existing.data.isSaved
|
|
278
|
-
});
|
|
279
|
-
vehicle = toVehicle(updated);
|
|
280
|
-
} else {
|
|
281
|
-
const created = await createEntry(COLLECTION, mapDvlaResponse(userId, dvlaData), {createdBy: userId});
|
|
282
|
-
vehicle = toVehicle(created);
|
|
283
|
-
|
|
284
|
-
// Prune oldest unsaved entries if over the history limit
|
|
285
|
-
const {entries: fresh} = await listEntries(COLLECTION, {limit: 10000});
|
|
286
|
-
const userEntries = fresh.filter(e => e.data.userId === userId);
|
|
287
|
-
if (userEntries.length > cfg.maxHistoryItems) {
|
|
288
|
-
const unsaved = userEntries
|
|
289
|
-
.filter(e => !e.data.isSaved)
|
|
290
|
-
.sort((a, b) => new Date(a.data.lookupDate) - new Date(b.data.lookupDate));
|
|
291
|
-
const toDelete = unsaved.slice(0, userEntries.length - cfg.maxHistoryItems);
|
|
292
|
-
for (const entry of toDelete) {
|
|
293
|
-
await deleteEntry(COLLECTION, entry.id);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return vehicle;
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
// --- GET /vehicles (saved) ----------------------------------------------
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Return all vehicles the authenticated user has saved to their garage.
|
|
305
|
-
* Supports optional ?q= for server-side search across reg/make/colour/year/fuel.
|
|
306
|
-
*/
|
|
307
|
-
fastify.get('/vehicles', {preHandler: [authenticate]}, async (request) => {
|
|
308
|
-
const userId = request.user.id;
|
|
309
|
-
const q = (request.query.q || '').toLowerCase().trim();
|
|
310
|
-
|
|
311
|
-
const {entries} = await listEntries(COLLECTION, {limit: 10000});
|
|
312
|
-
let results = entries
|
|
313
|
-
.filter(e => e.data.userId === userId && (e.data.isSaved === true || e.data.isSaved === 'true'))
|
|
314
|
-
.map(toVehicle);
|
|
315
|
-
|
|
316
|
-
if (q) {
|
|
317
|
-
results = results.filter(v => {
|
|
318
|
-
const haystack = [
|
|
319
|
-
v.registrationNumber, v.make, v.colour,
|
|
320
|
-
v.yearOfManufacture, v.fuelType, v.taxStatus, v.motStatus
|
|
321
|
-
].join(' ').toLowerCase();
|
|
322
|
-
return haystack.includes(q);
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return results;
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// --- GET /history -------------------------------------------------------
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Return the authenticated user's full lookup history, newest first.
|
|
333
|
-
* Supports optional ?q= search.
|
|
334
|
-
*/
|
|
335
|
-
fastify.get('/history', {preHandler: [authenticate]}, async (request) => {
|
|
336
|
-
const userId = request.user.id;
|
|
337
|
-
const q = (request.query.q || '').toLowerCase().trim();
|
|
338
|
-
|
|
339
|
-
const {entries} = await listEntries(COLLECTION, {limit: 10000});
|
|
340
|
-
let results = entries
|
|
341
|
-
.filter(e => e.data.userId === userId)
|
|
342
|
-
.map(toVehicle)
|
|
343
|
-
.sort((a, b) => new Date(b.lookupDate) - new Date(a.lookupDate));
|
|
344
|
-
|
|
345
|
-
if (q) {
|
|
346
|
-
results = results.filter(v => {
|
|
347
|
-
const haystack = [
|
|
348
|
-
v.registrationNumber, v.make, v.colour,
|
|
349
|
-
v.yearOfManufacture, v.fuelType, v.taxStatus, v.motStatus
|
|
350
|
-
].join(' ').toLowerCase();
|
|
351
|
-
return haystack.includes(q);
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return results;
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
// --- PATCH /vehicles/:id/save -------------------------------------------
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Toggle the isSaved flag on a vehicle entry.
|
|
362
|
-
* Returns 404 if the entry does not exist or belongs to another user.
|
|
363
|
-
*/
|
|
364
|
-
fastify.patch('/vehicles/:id/save', {preHandler: [authenticate]}, async (request, reply) => {
|
|
365
|
-
const userId = request.user.id;
|
|
366
|
-
const {id} = request.params;
|
|
367
|
-
|
|
368
|
-
const entry = await getEntry(COLLECTION, id);
|
|
369
|
-
if (!entry || entry.data.userId !== userId) {
|
|
370
|
-
return reply.status(404).send({error: 'Vehicle not found'});
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const updated = await updateEntry(COLLECTION, id, {
|
|
374
|
-
...entry.data,
|
|
375
|
-
isSaved: !(entry.data.isSaved === true || entry.data.isSaved === 'true')
|
|
376
|
-
});
|
|
377
|
-
return toVehicle(updated);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
// --- DELETE /vehicles/:id -----------------------------------------------
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Permanently remove a vehicle entry from the user's history.
|
|
384
|
-
* Returns 404 if the entry does not exist or belongs to another user.
|
|
385
|
-
*/
|
|
386
|
-
fastify.delete('/vehicles/:id', {preHandler: [authenticate]}, async (request, reply) => {
|
|
387
|
-
const userId = request.user.id;
|
|
388
|
-
const {id} = request.params;
|
|
389
|
-
|
|
390
|
-
const entry = await getEntry(COLLECTION, id);
|
|
391
|
-
if (!entry || entry.data.userId !== userId) {
|
|
392
|
-
return reply.status(404).send({error: 'Vehicle not found'});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
await deleteEntry(COLLECTION, id);
|
|
396
|
-
return {ok: true};
|
|
397
|
-
});
|
|
398
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "garage",
|
|
3
|
-
"displayName": "Garage",
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"description": "UK vehicle management with DVLA API lookup, save vehicles, and search history.",
|
|
6
|
-
"author": "Darryl Waterhouse",
|
|
7
|
-
"date": "2026-03-28",
|
|
8
|
-
"icon": "truck",
|
|
9
|
-
"admin": {
|
|
10
|
-
"sidebar": [
|
|
11
|
-
{
|
|
12
|
-
"id": "garage",
|
|
13
|
-
"text": "Garage",
|
|
14
|
-
"icon": "truck",
|
|
15
|
-
"url": "#/plugins/garage",
|
|
16
|
-
"section": "#/plugins/garage"
|
|
17
|
-
}
|
|
18
|
-
],
|
|
19
|
-
"routes": [
|
|
20
|
-
{
|
|
21
|
-
"path": "/plugins/garage",
|
|
22
|
-
"view": "plugin-garage",
|
|
23
|
-
"title": "Garage - Domma CMS"
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
|
-
"views": {
|
|
27
|
-
"plugin-garage": {
|
|
28
|
-
"entry": "garage/admin/views/garage.js?v=4",
|
|
29
|
-
"exportName": "garageView"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|