pockethost 1.0.0-rc.1
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/LICENSE.md +16 -0
- package/dist/index.mjs +3156 -0
- package/dist/instance-app/migrations/1699198734_pockethost_defaults.js +39 -0
- package/dist/instance-app/pb_hooks/_ph_admin_sync.pb.js +71 -0
- package/dist/instance-app/pb_hooks/_ph_lib.js +19 -0
- package/dist/instance-app/tsconfig.json +8 -0
- package/dist/instance-app/types/goja.d.ts +58 -0
- package/dist/instance-app/types/lib.d.ts +5 -0
- package/dist/instance-app/types/types.d.ts +19573 -0
- package/dist/mothership-app/migrations/1705062576_collections_snapshot.js +1863 -0
- package/dist/mothership-app/migrations/1705083826_updated_stats.js +721 -0
- package/dist/mothership-app/migrations/1706060446_updated_instances.js +44 -0
- package/dist/mothership-app/pb_hooks/src/instances-create.pb.js +65 -0
- package/dist/mothership-app/pb_hooks/src/instances-delete.pb.js +69 -0
- package/dist/mothership-app/pb_hooks/src/instances-migrate-versions.pb.js +33 -0
- package/dist/mothership-app/pb_hooks/src/instances-notify-discord-after-create.pb.js +31 -0
- package/dist/mothership-app/pb_hooks/src/instances-notify-on-maintenance-mode.pb.js +52 -0
- package/dist/mothership-app/pb_hooks/src/instances-reset-idle.pb.js +7 -0
- package/dist/mothership-app/pb_hooks/src/instances-update.pb.js +121 -0
- package/dist/mothership-app/pb_hooks/src/instances-validate-before-create.pb.js +16 -0
- package/dist/mothership-app/pb_hooks/src/instances-validate-before-update.pb.js +43 -0
- package/dist/mothership-app/pb_hooks/src/lib.js +171 -0
- package/dist/mothership-app/pb_hooks/src/ls.pb.js +159 -0
- package/dist/mothership-app/pb_hooks/src/mail.pb.js +49 -0
- package/dist/mothership-app/pb_hooks/src/notifications.pb.js +62 -0
- package/dist/mothership-app/pb_hooks/src/random-words.js +2085 -0
- package/dist/mothership-app/pb_hooks/src/signup.pb.js +214 -0
- package/dist/mothership-app/pb_hooks/src/sns.pb.js +133 -0
- package/dist/mothership-app/pb_hooks/src/unsubscribe.pb.js +33 -0
- package/dist/mothership-app/pb_hooks/src/user-token.pb.js +33 -0
- package/dist/mothership-app/pb_hooks/src/user-welcome-msg.pb.js +32 -0
- package/dist/mothership-app/pb_hooks/src/versions-route.pb.js +12 -0
- package/dist/mothership-app/pb_hooks/src/versions.js +0 -0
- package/dist/mothership-app/pb_hooks/tsconfig.json +8 -0
- package/dist/mothership-app/pb_hooks/types/global.d.ts +1 -0
- package/dist/mothership-app/pb_hooks/types/goja.d.ts +58 -0
- package/dist/mothership-app/pb_hooks/types/lemon.d.ts +12 -0
- package/dist/mothership-app/pb_hooks/types/lib.d.ts +55 -0
- package/dist/mothership-app/pb_hooks/types/types.d.ts +19573 -0
- package/dist/mothership-app/ph_app/index.js +11 -0
- package/dist/mothership-app/ph_app/package-lock.json +657 -0
- package/dist/mothership-app/ph_app/package.json +14 -0
- package/package.json +144 -0
- package/readme.md +168 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/// <reference path="../pb_data/types.d.ts" />
|
|
2
|
+
migrate((db) => {
|
|
3
|
+
const dao = new Dao(db)
|
|
4
|
+
const collection = dao.findCollectionByNameOrId("etae8tuiaxl6xfv")
|
|
5
|
+
|
|
6
|
+
// update
|
|
7
|
+
collection.schema.addField(new SchemaField({
|
|
8
|
+
"system": false,
|
|
9
|
+
"id": "5oz9huwg",
|
|
10
|
+
"name": "cname",
|
|
11
|
+
"type": "text",
|
|
12
|
+
"required": false,
|
|
13
|
+
"presentable": false,
|
|
14
|
+
"unique": false,
|
|
15
|
+
"options": {
|
|
16
|
+
"min": null,
|
|
17
|
+
"max": null,
|
|
18
|
+
"pattern": "^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,24}$"
|
|
19
|
+
}
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
return dao.saveCollection(collection)
|
|
23
|
+
}, (db) => {
|
|
24
|
+
const dao = new Dao(db)
|
|
25
|
+
const collection = dao.findCollectionByNameOrId("etae8tuiaxl6xfv")
|
|
26
|
+
|
|
27
|
+
// update
|
|
28
|
+
collection.schema.addField(new SchemaField({
|
|
29
|
+
"system": false,
|
|
30
|
+
"id": "5oz9huwg",
|
|
31
|
+
"name": "cname",
|
|
32
|
+
"type": "text",
|
|
33
|
+
"required": false,
|
|
34
|
+
"presentable": false,
|
|
35
|
+
"unique": false,
|
|
36
|
+
"options": {
|
|
37
|
+
"min": null,
|
|
38
|
+
"max": null,
|
|
39
|
+
"pattern": "^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,6}$"
|
|
40
|
+
}
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
return dao.saveCollection(collection)
|
|
44
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
{
|
|
3
|
+
"subdomain": "foo"
|
|
4
|
+
}
|
|
5
|
+
*/
|
|
6
|
+
routerAdd(
|
|
7
|
+
'POST',
|
|
8
|
+
'/api/instance',
|
|
9
|
+
(c) => {
|
|
10
|
+
const dao = $app.dao()
|
|
11
|
+
const { audit, mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
|
|
12
|
+
|
|
13
|
+
const log = mkLog(`POST:instance`)
|
|
14
|
+
|
|
15
|
+
const authRecord = /** @type {models.Record} */ (c.get('authRecord')) // empty if not authenticated as regular auth record
|
|
16
|
+
log(`***authRecord`, JSON.stringify(authRecord))
|
|
17
|
+
|
|
18
|
+
if (!authRecord) {
|
|
19
|
+
throw new Error(`Expected authRecord here`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
log(`***TOP OF POST`)
|
|
23
|
+
let data = /** @type{ {subdomain?: string} } */ (
|
|
24
|
+
new DynamicModel({
|
|
25
|
+
subdomain: '',
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
log(`***before bind`)
|
|
30
|
+
|
|
31
|
+
c.bind(data)
|
|
32
|
+
|
|
33
|
+
log(`***after bind`)
|
|
34
|
+
|
|
35
|
+
// This is necessary for destructuring to work correctly
|
|
36
|
+
data = JSON.parse(JSON.stringify(data))
|
|
37
|
+
|
|
38
|
+
const { subdomain } = data
|
|
39
|
+
|
|
40
|
+
log(`***vars`, JSON.stringify({ subdomain }))
|
|
41
|
+
|
|
42
|
+
if (!subdomain) {
|
|
43
|
+
throw new BadRequestError(
|
|
44
|
+
`Subdomain is required when creating an instance.`,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { versions } = require(`${__hooks}/versions.js`)
|
|
49
|
+
|
|
50
|
+
const collection = dao.findCollectionByNameOrId('instances')
|
|
51
|
+
const record = new Record(collection)
|
|
52
|
+
record.set('uid', authRecord.getId())
|
|
53
|
+
record.set('subdomain', subdomain)
|
|
54
|
+
record.set('status', 'idle')
|
|
55
|
+
record.set('version', versions[0])
|
|
56
|
+
record.set('syncAdmin', true)
|
|
57
|
+
record.set('notifyMaintenanceMode', true)
|
|
58
|
+
|
|
59
|
+
const form = new RecordUpsertForm($app, record)
|
|
60
|
+
form.submit()
|
|
61
|
+
|
|
62
|
+
return c.json(200, { instance: record })
|
|
63
|
+
},
|
|
64
|
+
$apis.requireRecordAuth(),
|
|
65
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
{
|
|
3
|
+
"id": "kz4ngg77eaw1ho0",
|
|
4
|
+
"fields": {
|
|
5
|
+
"maintenance": true
|
|
6
|
+
"name": '',
|
|
7
|
+
"version": ''
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
*/
|
|
11
|
+
routerAdd(
|
|
12
|
+
'DELETE',
|
|
13
|
+
'/api/instance/:id',
|
|
14
|
+
(c) => {
|
|
15
|
+
const dao = $app.dao()
|
|
16
|
+
const { audit, mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
|
|
17
|
+
|
|
18
|
+
const log = mkLog(`DELETE:instance`)
|
|
19
|
+
|
|
20
|
+
log(`TOP OF DELETE`)
|
|
21
|
+
let data = new DynamicModel({
|
|
22
|
+
id: '',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
c.bind(data)
|
|
26
|
+
log(`After bind`)
|
|
27
|
+
|
|
28
|
+
// This is necessary for destructuring to work correctly
|
|
29
|
+
data = JSON.parse(JSON.stringify(data))
|
|
30
|
+
|
|
31
|
+
const id = c.pathParam('id')
|
|
32
|
+
|
|
33
|
+
log(
|
|
34
|
+
`vars`,
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
id,
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const authRecord = /** @type {models.Record} */ (c.get('authRecord')) // empty if not authenticated as regular auth record
|
|
41
|
+
log(`authRecord`, JSON.stringify(authRecord))
|
|
42
|
+
|
|
43
|
+
if (!authRecord) {
|
|
44
|
+
throw new BadRequestError(`Expected authRecord here`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const record = dao.findRecordById('instances', id)
|
|
48
|
+
if (!record) {
|
|
49
|
+
throw new BadRequestError(`Instance ${id} not found.`)
|
|
50
|
+
}
|
|
51
|
+
if (record.get('uid') !== authRecord.id) {
|
|
52
|
+
throw new BadRequestError(`Not authorized`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (record.getString('status').toLowerCase() !== 'idle') {
|
|
56
|
+
throw new BadRequestError(`Instance must be shut down first.`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const path = [$os.getenv('DATA_ROOT'), id].join('/')
|
|
60
|
+
log(`path ${path}`)
|
|
61
|
+
const res = $os.removeAll(path)
|
|
62
|
+
log(`res`, res)
|
|
63
|
+
|
|
64
|
+
dao.deleteRecord(record)
|
|
65
|
+
|
|
66
|
+
return c.json(200, { status: 'ok' })
|
|
67
|
+
},
|
|
68
|
+
$apis.requireRecordAuth(),
|
|
69
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Migrate version numbers */
|
|
2
|
+
onAfterBootstrap((e) => {
|
|
3
|
+
const dao = $app.dao()
|
|
4
|
+
const { audit, mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
|
|
5
|
+
|
|
6
|
+
const log = mkLog(`bootstrap`)
|
|
7
|
+
|
|
8
|
+
const records = dao.findRecordsByFilter(`instances`, '1=1')
|
|
9
|
+
const { versions } = require(`${__hooks}/versions.js`)
|
|
10
|
+
const unrecognized = []
|
|
11
|
+
records.forEach((record) => {
|
|
12
|
+
const v = record.get('version').trim()
|
|
13
|
+
if (versions.includes(v)) return
|
|
14
|
+
const newVersion = (() => {
|
|
15
|
+
if (v.startsWith(`~`)) {
|
|
16
|
+
const [major, minor] = v.slice(1).split('.')
|
|
17
|
+
const newVersion = [major, minor, '*'].join('.')
|
|
18
|
+
return newVersion
|
|
19
|
+
} else {
|
|
20
|
+
if (v === `^0` || v === `0` || v === '1') {
|
|
21
|
+
return versions[0]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})()
|
|
25
|
+
if (versions.includes(newVersion)) {
|
|
26
|
+
record.set(`version`, newVersion)
|
|
27
|
+
dao.saveRecord(record)
|
|
28
|
+
} else {
|
|
29
|
+
unrecognized.push(v)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
log({ unrecognized })
|
|
33
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/// <reference path="../types/types.d.ts" />
|
|
2
|
+
|
|
3
|
+
onModelAfterCreate((e) => {
|
|
4
|
+
const dao = e.dao || $app.dao()
|
|
5
|
+
const { audit, mkLog } = /** @type {Lib} */ (require(`${__hooks}/lib.js`))
|
|
6
|
+
|
|
7
|
+
const log = mkLog(`instances:create:discord:notify`)
|
|
8
|
+
|
|
9
|
+
const webhookUrl = $os.getenv('DISCORD_POCKETSTREAM_URL')
|
|
10
|
+
if (!webhookUrl) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
const version = e.model.get('version')
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const res = $http.send({
|
|
17
|
+
url: webhookUrl,
|
|
18
|
+
method: 'POST',
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
content: `Someone just created an app running PocketBase v${version}`,
|
|
21
|
+
}),
|
|
22
|
+
headers: { 'content-type': 'application/json' },
|
|
23
|
+
timeout: 5, // in seconds
|
|
24
|
+
})
|
|
25
|
+
} catch (e) {
|
|
26
|
+
audit(`ERROR`, `Instance creation discord notify failed with ${e}`, {
|
|
27
|
+
log,
|
|
28
|
+
dao,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}, 'instances')
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
onModelAfterUpdate((e) => {
|
|
2
|
+
const dao = e.dao || $app.dao()
|
|
3
|
+
|
|
4
|
+
const newModel = /** @type {models.Record} */ (e.model)
|
|
5
|
+
const oldModel = newModel.originalCopy()
|
|
6
|
+
|
|
7
|
+
const { mkLog, enqueueNotification, audit } = /** @type {Lib} */ (
|
|
8
|
+
require(`${__hooks}/lib.js`)
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const log = mkLog(`maintenance-mode`)
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Bail out if we aren't in maintenance mode
|
|
15
|
+
const isMaintenance = newModel.get('maintenance')
|
|
16
|
+
// log({ isMaintenance })
|
|
17
|
+
if (!isMaintenance) return
|
|
18
|
+
|
|
19
|
+
// Bail out if the maintenance mode flag has not changed
|
|
20
|
+
if (isMaintenance === oldModel.get(`maintenance`)) return
|
|
21
|
+
|
|
22
|
+
log(`switched`)
|
|
23
|
+
const uid = newModel.get(`uid`)
|
|
24
|
+
const user = dao.findRecordById('users', uid)
|
|
25
|
+
|
|
26
|
+
// Bail out if the user has notifications disabled globally or for the instance
|
|
27
|
+
const shouldNotify =
|
|
28
|
+
user.getBool(`notifyMaintenanceMode`) &&
|
|
29
|
+
newModel.getBool(`notifyMaintenanceMode`)
|
|
30
|
+
log({ shouldNotify })
|
|
31
|
+
if (!shouldNotify) return
|
|
32
|
+
|
|
33
|
+
// Send the email warning about maintenance mode
|
|
34
|
+
const instanceId = newModel.getId()
|
|
35
|
+
const subdomain = newModel.getString(`subdomain`)
|
|
36
|
+
const address = user.getString(`email`)
|
|
37
|
+
log({ instanceId, subdomain, address })
|
|
38
|
+
enqueueNotification(`email`, `maintenance-mode`, uid, {
|
|
39
|
+
log,
|
|
40
|
+
dao,
|
|
41
|
+
message_template_vars: {
|
|
42
|
+
subdomain,
|
|
43
|
+
instanceId,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
} catch (e) {
|
|
47
|
+
audit('ERROR', `Failed to enqueue template with ${e}`, {
|
|
48
|
+
log,
|
|
49
|
+
dao,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}, 'instances')
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/// <reference path="../types/types.d.ts" />
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
{
|
|
5
|
+
"id": "kz4ngg77eaw1ho0",
|
|
6
|
+
"fields": {
|
|
7
|
+
"maintenance": true
|
|
8
|
+
"name": '',
|
|
9
|
+
"version": ''
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
*/
|
|
13
|
+
routerAdd(
|
|
14
|
+
'PUT',
|
|
15
|
+
'/api/instance/:id',
|
|
16
|
+
(c) => {
|
|
17
|
+
const dao = $app.dao()
|
|
18
|
+
const { mkLog, audit, removeEmptyKeys } = /** @type {Lib} */ (
|
|
19
|
+
require(`${__hooks}/lib.js`)
|
|
20
|
+
)
|
|
21
|
+
const log = mkLog(`PUT:instance`)
|
|
22
|
+
|
|
23
|
+
log(`TOP OF PUT`)
|
|
24
|
+
|
|
25
|
+
let data =
|
|
26
|
+
/**
|
|
27
|
+
* @type {{
|
|
28
|
+
* id: string
|
|
29
|
+
* fields: {
|
|
30
|
+
* subdomain: string | null
|
|
31
|
+
* maintenance: boolean | null
|
|
32
|
+
* version: string | null
|
|
33
|
+
* secrets: StringKvLookup | null
|
|
34
|
+
* syncAdmin: boolean | null
|
|
35
|
+
* dev: boolean | null
|
|
36
|
+
* cname: string | null
|
|
37
|
+
* notifyMaintenanceMode: boolean | null
|
|
38
|
+
* }
|
|
39
|
+
* }} }
|
|
40
|
+
*/
|
|
41
|
+
(
|
|
42
|
+
new DynamicModel({
|
|
43
|
+
id: '',
|
|
44
|
+
fields: {
|
|
45
|
+
subdomain: null,
|
|
46
|
+
maintenance: null,
|
|
47
|
+
version: null,
|
|
48
|
+
secrets: null,
|
|
49
|
+
syncAdmin: null,
|
|
50
|
+
dev: null,
|
|
51
|
+
cname: null,
|
|
52
|
+
notifyMaintenanceMode: null,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
c.bind(data)
|
|
58
|
+
log(`After bind`)
|
|
59
|
+
|
|
60
|
+
// This is necessary for destructuring to work correctly
|
|
61
|
+
data = JSON.parse(JSON.stringify(data))
|
|
62
|
+
|
|
63
|
+
const id = c.pathParam('id')
|
|
64
|
+
const {
|
|
65
|
+
fields: {
|
|
66
|
+
subdomain,
|
|
67
|
+
maintenance,
|
|
68
|
+
version,
|
|
69
|
+
secrets,
|
|
70
|
+
syncAdmin,
|
|
71
|
+
dev,
|
|
72
|
+
cname,
|
|
73
|
+
notifyMaintenanceMode,
|
|
74
|
+
},
|
|
75
|
+
} = data
|
|
76
|
+
|
|
77
|
+
log(
|
|
78
|
+
`vars`,
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
id,
|
|
81
|
+
subdomain,
|
|
82
|
+
maintenance,
|
|
83
|
+
version,
|
|
84
|
+
secrets,
|
|
85
|
+
syncAdmin,
|
|
86
|
+
dev,
|
|
87
|
+
cname,
|
|
88
|
+
notifyMaintenanceMode,
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const record = dao.findRecordById('instances', id)
|
|
93
|
+
const authRecord = /** @type {models.Record} */ (c.get('authRecord')) // empty if not authenticated as regular auth record
|
|
94
|
+
log(`authRecord`, JSON.stringify(authRecord))
|
|
95
|
+
|
|
96
|
+
if (!authRecord) {
|
|
97
|
+
throw new Error(`Expected authRecord here`)
|
|
98
|
+
}
|
|
99
|
+
if (record.get('uid') !== authRecord.id) {
|
|
100
|
+
throw new BadRequestError(`Not authorized`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sanitized = removeEmptyKeys({
|
|
104
|
+
subdomain,
|
|
105
|
+
version,
|
|
106
|
+
maintenance,
|
|
107
|
+
secrets,
|
|
108
|
+
syncAdmin,
|
|
109
|
+
dev,
|
|
110
|
+
cname,
|
|
111
|
+
notifyMaintenanceMode,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const form = new RecordUpsertForm($app, record)
|
|
115
|
+
form.loadData(sanitized)
|
|
116
|
+
form.submit()
|
|
117
|
+
|
|
118
|
+
return c.json(200, { status: 'ok' })
|
|
119
|
+
},
|
|
120
|
+
$apis.requireRecordAuth(),
|
|
121
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/// <reference path="../types/types.d.ts" />
|
|
2
|
+
|
|
3
|
+
/** Validate instance version */
|
|
4
|
+
onModelBeforeCreate((e) => {
|
|
5
|
+
const dao = e.dao || $app.dao()
|
|
6
|
+
const { versions } = require(`${__hooks}/versions.js`)
|
|
7
|
+
|
|
8
|
+
const version = e.model.get('version')
|
|
9
|
+
if (!versions.includes(version)) {
|
|
10
|
+
throw new BadRequestError(
|
|
11
|
+
`Invalid version ${version}. Version must be one of: ${versions.join(
|
|
12
|
+
', ',
|
|
13
|
+
)}`,
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}, 'instances')
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/// <reference path="../types/types.d.ts" />
|
|
2
|
+
|
|
3
|
+
/** Validate instance version */
|
|
4
|
+
onModelBeforeUpdate((e) => {
|
|
5
|
+
const dao = e.dao || $app.dao()
|
|
6
|
+
const { versions } = require(`${__hooks}/versions.js`)
|
|
7
|
+
|
|
8
|
+
const version = e.model.get('version')
|
|
9
|
+
if (!versions.includes(version)) {
|
|
10
|
+
throw new BadRequestError(
|
|
11
|
+
`Invalid version ${version}. Version must be one of: ${versions.join(
|
|
12
|
+
', ',
|
|
13
|
+
)}`,
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const id = e.model.getId()
|
|
18
|
+
const cname = e.model.get('cname')
|
|
19
|
+
if (cname.length > 0) {
|
|
20
|
+
const result = new DynamicModel({
|
|
21
|
+
id: '',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const inUse = (() => {
|
|
25
|
+
try {
|
|
26
|
+
dao
|
|
27
|
+
.db()
|
|
28
|
+
.newQuery(
|
|
29
|
+
`select id from instances where cname='${cname}' and id <> '${id}'`,
|
|
30
|
+
)
|
|
31
|
+
.one(result)
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.log(`*** cname OK ${cname}`)
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
return true
|
|
37
|
+
})()
|
|
38
|
+
|
|
39
|
+
if (inUse) {
|
|
40
|
+
throw new BadRequestError(`Custom domain already in use.`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}, 'instances')
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/** @type {Lib['audit']} */
|
|
2
|
+
const audit = (event, note, { log, dao, extra = {} }) => {
|
|
3
|
+
log(`AUDIT:${event}: ${note}`, extra)
|
|
4
|
+
dao.saveRecord(
|
|
5
|
+
new Record(dao.findCollectionByNameOrId('audit'), {
|
|
6
|
+
event,
|
|
7
|
+
note,
|
|
8
|
+
...extra,
|
|
9
|
+
}),
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @type {Lib['mkLog']} */
|
|
14
|
+
const mkLog =
|
|
15
|
+
(namespace) =>
|
|
16
|
+
/**
|
|
17
|
+
* @param {...any} s
|
|
18
|
+
* @returns
|
|
19
|
+
*/
|
|
20
|
+
(...s) =>
|
|
21
|
+
console.log(
|
|
22
|
+
`[${namespace}]`,
|
|
23
|
+
...s.map((p) => {
|
|
24
|
+
if (typeof p === 'object') return JSON.stringify(p, null, 2)
|
|
25
|
+
return p
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {...any} args
|
|
31
|
+
* @returns
|
|
32
|
+
*/
|
|
33
|
+
const dbg = (...args) => console.log(args)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} template
|
|
37
|
+
* @param {{ [_: string]: string }} dict
|
|
38
|
+
* @returns
|
|
39
|
+
*/
|
|
40
|
+
function interpolateString(template, dict) {
|
|
41
|
+
return template.replace(/\{\$(\w+)\}/g, (match, key) => {
|
|
42
|
+
dbg({ match, key })
|
|
43
|
+
const lowerKey = key.toLowerCase()
|
|
44
|
+
return dict.hasOwnProperty(lowerKey) ? dict[lowerKey] || '' : match
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @type {Lib['enqueueNotification']} */
|
|
49
|
+
const enqueueNotification = (
|
|
50
|
+
channel,
|
|
51
|
+
template,
|
|
52
|
+
user_id,
|
|
53
|
+
{ log, dao, message_template_vars = {} },
|
|
54
|
+
) => {
|
|
55
|
+
log({ channel, template, user_id })
|
|
56
|
+
const emailTemplate = dao.findFirstRecordByData(
|
|
57
|
+
'message_templates',
|
|
58
|
+
`slug`,
|
|
59
|
+
template,
|
|
60
|
+
)
|
|
61
|
+
log(`got email template`, emailTemplate)
|
|
62
|
+
if (!emailTemplate) throw new Error(`Template ${template} not found`)
|
|
63
|
+
const emailNotification = new Record(
|
|
64
|
+
dao.findCollectionByNameOrId('notifications'),
|
|
65
|
+
{
|
|
66
|
+
user: user_id,
|
|
67
|
+
channel,
|
|
68
|
+
message_template: emailTemplate.getId(),
|
|
69
|
+
message_template_vars,
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
log(`built notification record`, emailNotification)
|
|
73
|
+
dao.saveRecord(emailNotification)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @type {Lib['processNotification']} */
|
|
77
|
+
const processNotification = (notificationRec, { log, dao, test = false }) => {
|
|
78
|
+
log({ notificationRec })
|
|
79
|
+
|
|
80
|
+
const channel = notificationRec.getString(`channel`)
|
|
81
|
+
dao.expandRecord(notificationRec, ['message_template', 'user'], null)
|
|
82
|
+
|
|
83
|
+
const messageTemplateRec = notificationRec.expandedOne('message_template')
|
|
84
|
+
if (!messageTemplateRec) {
|
|
85
|
+
throw new Error(`Missing message template`)
|
|
86
|
+
}
|
|
87
|
+
const userRec = notificationRec.expandedOne('user')
|
|
88
|
+
if (!userRec) {
|
|
89
|
+
throw new Error(`Missing user record`)
|
|
90
|
+
}
|
|
91
|
+
const vars = JSON.parse(notificationRec.getString(`message_template_vars`))
|
|
92
|
+
const to = userRec.email()
|
|
93
|
+
const subject = interpolateString(
|
|
94
|
+
messageTemplateRec.getString(`subject`),
|
|
95
|
+
vars,
|
|
96
|
+
)
|
|
97
|
+
const html = interpolateString(
|
|
98
|
+
messageTemplateRec.getString(`body_html`),
|
|
99
|
+
vars,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
log({ channel, messageTemplateRec, userRec, vars, to, subject, html })
|
|
103
|
+
switch (channel) {
|
|
104
|
+
case `email`:
|
|
105
|
+
/** @type {Partial<mailer.Message_In>} */
|
|
106
|
+
const msgArgs = {
|
|
107
|
+
from: {
|
|
108
|
+
address: $app.settings().meta.senderAddress,
|
|
109
|
+
name: $app.settings().meta.senderName,
|
|
110
|
+
},
|
|
111
|
+
to: [{ address: to }],
|
|
112
|
+
bcc: [{ address: `pockethost+notifications@benallfree.com` }],
|
|
113
|
+
subject,
|
|
114
|
+
html,
|
|
115
|
+
}
|
|
116
|
+
if (test) {
|
|
117
|
+
msgArgs.to = [{ address: `ben@benallfree.com` }]
|
|
118
|
+
msgArgs.subject = `***TEST ${to} *** ${msgArgs.subject}`
|
|
119
|
+
}
|
|
120
|
+
log({ msgArgs })
|
|
121
|
+
// @ts-ignore
|
|
122
|
+
const msg = new MailerMessage(msgArgs)
|
|
123
|
+
$app.newMailClient().send(msg)
|
|
124
|
+
log(`email sent`)
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
case `lemonbot`:
|
|
128
|
+
const res = $http.send({
|
|
129
|
+
url: test
|
|
130
|
+
? `https://discord.com/api/webhooks/1194056858516869213/pwT-ymSxiPmLN5-M2a7FsesvrbtdmwUsKXWIKqebROJEfyP0E3-aSseUjDg4ojBkWV4D` // Test channel
|
|
131
|
+
: `https://discord.com/api/webhooks/1193619183594901575/JVDfdUz2HPEUk-nG1RfI3BK2Czyx5vw1YmeH7cNfgvXbHNGPH0oJncOYqxMA_u5b2u57`, // Stream channel
|
|
132
|
+
method: 'POST',
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
content: subject,
|
|
135
|
+
}),
|
|
136
|
+
headers: { 'content-type': 'application/json' },
|
|
137
|
+
timeout: 5, // in seconds
|
|
138
|
+
})
|
|
139
|
+
log(`discord sent`)
|
|
140
|
+
break
|
|
141
|
+
default:
|
|
142
|
+
throw new Error(`Unsupported channel: ${channel}`)
|
|
143
|
+
}
|
|
144
|
+
if (!test) {
|
|
145
|
+
notificationRec.set(`delivered`, new DateTime())
|
|
146
|
+
dao.saveRecord(notificationRec)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @template T
|
|
152
|
+
* @param {T} obj
|
|
153
|
+
* @returns T
|
|
154
|
+
*/
|
|
155
|
+
function removeEmptyKeys(obj) {
|
|
156
|
+
const sanitized = Object.entries(obj).reduce((acc, [key, value]) => {
|
|
157
|
+
if (value !== null && value !== undefined) {
|
|
158
|
+
acc[key] = value
|
|
159
|
+
}
|
|
160
|
+
return acc
|
|
161
|
+
}, /** @type {T} */ ({}))
|
|
162
|
+
return sanitized
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
audit,
|
|
167
|
+
processNotification,
|
|
168
|
+
mkLog,
|
|
169
|
+
enqueueNotification,
|
|
170
|
+
removeEmptyKeys,
|
|
171
|
+
}
|