diff --git a/README.md b/README.md index cac3944..0de49d8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,154 @@ # hookSettings-for-PocketBase -A PocketBase js hook and module to bring config modules to the WebUI +A PocketBase Hook and JS Module to bring [config modules](https://pocketbase.io/docs/js-overview/#handlers-scope) for other JS Hooks to the WebUI + +![Collection Image](/images/collection.png) + +## Usage + +When first run this module creates a new collection `pbHookSettings` within your PocketBase UI. + +This collection has two fields `setting` and `value`. +The `setting` field is a string and the `value` field is JSON data. + +It's advised to use the module `hookSettings.js` to create a new settings record. + +Those methods can be used within the [handler scope](https://pocketbase.io/docs/js-overview/#handlers-scope) to create a new settings record if it does not exist. +```javascript +require('hookSettings.js').Setting('sampleSetting', defaultValue) +``` +This will create a new setting in the interface and return a new `Setting` object. + +A `Setting` object has the following methods: +- `get()` - Returns the value of the setting +- `set(value)` - Sets the value of the setting + +### Tiny example + + +```javascript +const defaultValue = { + name: 'John Doe', age: 25, +}; +// Entry into pbHookSettings collection will automatically be created +const settings = require('hookSettings.js').Setting('settingName', defaultValue); + +settings.get().name; +settings.get().age; + +settings.set({name: 'Jane Doe', age: 26}); +``` + + +# Install + +### Standard hooks folder location + +1. Download this repository +2. Unzip the downloaded file +3. Copy the `pb_hooks` folder to your PocketBase executable folder +4. Run PocketBase + +### Changed hooks folder location + +1. Download this repository +2. Unzip the downloaded file +3. Copy the contents of the `pb_hooks` folder to the location you have set in your PocketBase settings +4. Run PocketBase + +# Caveats +- It's currently not possible to use settings `onBeforeBootstrap` +- It's currently not possible to use settings in `cronAdd(NOT POSSIBLE HERE, NOT POSSIBLE HERE, () => {POSSIBLE HERE})` since this is executed before the db is running + + +# Example + +This is an example of how to use the `hookSettings.js` module to create a hook that deletes unverified users +after a set time. The time and mail settings are stored in the `pbHookSettings` collection and can be changed +at any time without having to change the code/restarting from the User Interface. + +### Exception +It is possible that `deleteUnverified.pb.js` is executed before `hookSettings.pb.js` which will cause an error, since the `pbHookSettings` collection does not exist yet. This can be fixed by renaming `hookSettings.pb.js` to `0_hookSettings.pb.js` and restarting PocketBase. + + +### Structure +```text +example_folder +├── pb_data +├── pb_migrations +├── pb_hooks +│ ├── hookSettings.js +│ ├── hookSettings.pb.js +│ └── deleteUnverified.pb.js +├── CHANGELOG.md +├── LICENSE.md +└── pocketbase.exe +``` + +### deleteUnverified.pb.js +```javascript +onAfterBootstrap((e) => { + const config = require(`${__hooks}/hookSettings.js`).Setting("deleteUnverified", { + time: 10, // Time in minutes + mail: true, + mailSubject: "Your account has been deleted", + mailBody: "Your account has been deleted due not being verified within the set time limit." + }) + + $app.logger().debug("Initialized deleteUnverified.pb.js with time: " + config.get().time + " minutes", "type", "hook", + "file", "deleteUnverified.pb.js") +}) + +cronAdd("deleteUnverified", "*/" + 2 + " * * * *", () => { + // Default value is empty since the db entry is already created before this code is executed + const config = require(`${__hooks}/hookSettings.js`).Setting("deleteUnverified", {}) + + const result = arrayOf(new DynamicModel({ + "id": "", + "email": "", + })) + + $app.dao().db() + .select("id", "email") + .from("users") + .andWhere($dbx.hashExp({verified: false})) + .all(result) + + + result.forEach((row) => { + const e = $app.dao().findRecordById("users", row?.id); + const s = $app.dao().findRecordById("users", row?.id).getCreated(); + if (s && (new Date().getTime() - s.time().unixMilli()) > config.get().time * 60 * 1000) { + $app.logger().info("CLEAN Deleting aged unverified user " + row?.id + " with mail " + row?.email, "type", "hook", + "file", "deleteUnverified.pb.js") + $app.dao().deleteRecord(e) + + if (!config.get().mail) { + return + } + + function replaceTemplates(value) { + value = value + .replace(/{id}/g, e.id) + .replace(/{email}/g, e.email()) + .replace(/{username}/g, e.username()) + .replace(/{verified}/g, e.verified()) + return value + } + + const message = new MailerMessage({ + from: { + address: $app.settings().meta.senderAddress, + name: $app.settings().meta.senderName, + }, + to: [{address: e.email()}], + subject: replaceTemplates(config.get().mailSubject), + html: replaceTemplates(config.get().mailBody), + }) + + $app.newMailClient().send(message) + } + }) +}) +``` + +![Example Image](/images/WithEntry.png) \ No newline at end of file diff --git a/images/Collection.png b/images/Collection.png new file mode 100644 index 0000000..4bc0fba Binary files /dev/null and b/images/Collection.png differ diff --git a/images/Plain.png b/images/Plain.png new file mode 100644 index 0000000..5d6b08f Binary files /dev/null and b/images/Plain.png differ diff --git a/images/WithEntry.png b/images/WithEntry.png new file mode 100644 index 0000000..7ddf0ff Binary files /dev/null and b/images/WithEntry.png differ diff --git a/pb_hooks/hookSettings.js b/pb_hooks/hookSettings.js new file mode 100644 index 0000000..37a3a38 --- /dev/null +++ b/pb_hooks/hookSettings.js @@ -0,0 +1,60 @@ +module.exports = { + Setting: (setting, defaultValue) => { + class HookSetting { + constructor(setting, defaultValue) { + this.setting = setting + this.defaultValue = defaultValue + + try { + if (this.recordNotExists()) { + this.set(this.defaultValue) + } + } catch (unused) { + console.log("------------------") + console.log("") + console.log("An error occurred in hookSettings.js!") + console.log("A possible cause of this is that the pbHookSettings collection does not yet exist.") + console.log("This can happen if hookSettings.js is called before hookSettings.pb.js (only needs") + console.log("to be called once before hookSettings.js). There are two ways to fix this:") + console.log("1. Try to start PocketBase until hookSettings.pb.js is called before hookSettings.js") + console.log("2. (recommended) Temporarily rename hookSettings.pb.js to hookSettings.pb.js") + console.log("3. (not recommended) Manually create the pbHookSettings collection in the database") + console.log("") + console.log("------------------") + } + } + + + possibleRecords() { + return $app.dao().findRecordsByExpr("pbHookSettings", $dbx.hashExp({setting: this.setting})) + } + + recordNotExists() { + return this.possibleRecords().length === 0 + } + + get() { + let result = this.possibleRecords() + if (result.length > 0) { + return JSON.parse(result[0].get("state")) + } + return this.defaultValue + } + + set(value) { + if (this.recordNotExists()) { + let record = new Record($app.dao().findCollectionByNameOrId("pbHookSettings"), { + setting: this.setting, + state: defaultValue + }) + $app.dao().saveRecord(record) + } + let result = $app.dao().findRecordsByExpr("pbHookSettings", $dbx.hashExp({setting: this.setting}))[0] + result.set("state", JSON.stringify(value)) + $app.dao().saveRecord(result) + } + } + + return new HookSetting(setting, defaultValue) + } +} \ No newline at end of file diff --git a/pb_hooks/hookSettings.pb.js b/pb_hooks/hookSettings.pb.js new file mode 100644 index 0000000..0bc894a --- /dev/null +++ b/pb_hooks/hookSettings.pb.js @@ -0,0 +1,46 @@ +onAfterBootstrap((e) => { + /// Initialize the hook settings collection + /// I know using a try catch block is not the best way to do this, but it works for now + $app.logger().debug( + "Initializing hookSettings.pb.js", + "type", "hook", + "file", "hookSettings.pb.js" + ) + try { + /// Try to find the collection (fails if it doesn't exist) + !$app.dao().findCollectionByNameOrId("pbHookSettings") + $app.logger().debug( + "Hook settings collection found, skipping initialization", + "type", "hook", + "file", "hookSettings.pb.js" + ) + } catch (ignored) { + /// create collection + $app.logger().info( + "Hook settings collection not found, creating!", + "type", "hook", + "file", "hookSettings.pb.js" + ) + const form = new CollectionUpsertForm($app, new Collection()) + form.name = "pbHookSettings" + form.type = "base" + form.schema.addField(new SchemaField({ + name: "setting", + type: "text", + required: true, + presentable: true, + options: { + maxSize: 999, + } + })) + form.schema.addField(new SchemaField({ + name: "state", + type: "json", + required: true, + options: { + maxSize: 999 + } + })) + form.submit() + } +}) \ No newline at end of file