2018-12-17 16:37:19 +01:00
'use strict' ;
exports = module . exports = {
2020-12-29 12:43:53 -08:00
get ,
2021-04-21 12:00:07 -07:00
update ,
list ,
2021-05-28 14:34:18 -07:00
del ,
2018-12-17 17:35:19 +01:00
2020-12-29 12:43:53 -08:00
onEvent ,
2023-09-26 12:58:19 +02:00
// these are notification types, keep in sync with client.js
2023-09-22 17:58:13 +02:00
ALERT _CLOUDRON _INSTALLED : 'cloudronInstalled' ,
ALERT _CLOUDRON _UPDATED : 'cloudronUpdated' ,
ALERT _CLOUDRON _UPDATE _FAILED : 'cloudronUpdateFailed' ,
ALERT _CERTIFICATE _RENEWAL _FAILED : 'certificateRenewalFailed' ,
2019-02-28 14:52:14 -08:00
ALERT _BACKUP _CONFIG : 'backupConfig' ,
ALERT _DISK _SPACE : 'diskSpace' ,
ALERT _MAIL _STATUS : 'mailStatus' ,
ALERT _REBOOT : 'reboot' ,
2019-03-07 13:34:46 -08:00
ALERT _BOX _UPDATE : 'boxUpdate' ,
2021-05-18 14:37:11 -07:00
ALERT _UPDATE _UBUNTU : 'ubuntuUpdate' ,
2022-08-10 12:19:42 +02:00
ALERT _MANUAL _APP _UPDATE : 'manualAppUpdate' ,
2023-09-22 17:58:13 +02:00
ALERT _APP _OOM : 'appOutOfMemory' ,
ALERT _APP _UPDATED : 'appUpdated' ,
ALERT _BACKUP _FAILED : 'backupFailed' ,
2019-02-28 14:52:14 -08:00
2021-04-19 20:52:10 -07:00
alert ,
2023-03-26 14:18:37 +02:00
clearAlert ,
2019-03-02 19:23:39 -08:00
// exported for testing
_add : add
2018-12-17 16:37:19 +01:00
} ;
2021-05-28 14:34:18 -07:00
const assert = require ( 'assert' ) ,
2021-09-30 09:50:30 -07:00
AuditSource = require ( './auditsource.js' ) ,
2019-10-22 12:59:26 -07:00
BoxError = require ( './boxerror.js' ) ,
2019-08-03 13:59:11 -07:00
changelog = require ( './changelog.js' ) ,
2022-04-01 13:44:46 -07:00
constants = require ( './constants.js' ) ,
2023-08-11 19:41:05 +05:30
dashboard = require ( './dashboard.js' ) ,
2021-05-28 14:34:18 -07:00
database = require ( './database.js' ) ,
2019-02-27 16:10:54 -08:00
eventlog = require ( './eventlog.js' ) ,
2018-12-17 17:35:19 +01:00
mailer = require ( './mailer.js' ) ,
2021-06-30 17:19:30 +02:00
users = require ( './users.js' ) ;
2021-05-28 14:34:18 -07:00
2023-09-22 17:58:13 +02:00
const NOTIFICATION _FIELDS = [ 'id' , 'eventId' , 'type' , 'title' , 'message' , 'creationTime' , 'acknowledged' ] ;
2021-05-28 14:34:18 -07:00
function postProcess ( result ) {
assert . strictEqual ( typeof result , 'object' ) ;
result . id = String ( result . id ) ;
2023-03-26 14:42:38 +02:00
result . acknowledged = ! ! result . acknowledged ; // convert to boolean
2021-05-28 14:34:18 -07:00
return result ;
}
2023-09-22 17:58:13 +02:00
async function add ( type , title , message , data ) {
assert . strictEqual ( typeof type , 'string' ) ;
2018-12-17 16:37:19 +01:00
assert . strictEqual ( typeof title , 'string' ) ;
assert . strictEqual ( typeof message , 'string' ) ;
2023-03-26 14:42:38 +02:00
assert . strictEqual ( typeof data , 'object' ) ;
2021-05-28 14:34:18 -07:00
2023-09-22 17:58:13 +02:00
const query = 'INSERT INTO notifications (type, title, message, acknowledged, eventId) VALUES (?, ?, ?, ?, ?)' ;
const args = [ type , title , message , false , data ? . eventId || null ] ;
2021-05-28 14:34:18 -07:00
const result = await database . query ( query , args ) ;
return String ( result . insertId ) ;
2018-12-17 16:37:19 +01:00
}
2021-05-28 14:34:18 -07:00
async function get ( id ) {
2018-12-17 16:37:19 +01:00
assert . strictEqual ( typeof id , 'string' ) ;
2023-03-26 14:42:38 +02:00
const result = await database . query ( ` SELECT ${ NOTIFICATION _FIELDS } FROM notifications WHERE id = ? ` , [ id ] ) ;
2021-05-28 14:34:18 -07:00
if ( result . length === 0 ) return null ;
2018-12-17 16:37:19 +01:00
2021-05-28 14:34:18 -07:00
return postProcess ( result [ 0 ] ) ;
2018-12-17 16:37:19 +01:00
}
2021-05-28 14:34:18 -07:00
async function getByTitle ( title ) {
assert . strictEqual ( typeof title , 'string' ) ;
2018-12-17 16:37:19 +01:00
2023-03-26 14:42:38 +02:00
const results = await database . query ( ` SELECT ${ NOTIFICATION _FIELDS } from notifications WHERE title = ? ORDER BY creationTime LIMIT 1 ` , [ title ] ) ;
2021-05-28 14:34:18 -07:00
if ( results . length === 0 ) return null ;
2018-12-17 16:37:19 +01:00
2021-05-28 14:34:18 -07:00
return postProcess ( results [ 0 ] ) ;
2018-12-17 16:37:19 +01:00
}
2021-05-28 14:34:18 -07:00
async function update ( notification , data ) {
assert . strictEqual ( typeof notification , 'object' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
2018-12-17 16:37:19 +01:00
2023-03-26 14:42:38 +02:00
const args = [ ] ;
const fields = [ ] ;
for ( const k in data ) {
2021-05-28 14:34:18 -07:00
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ) ;
}
args . push ( notification . id ) ;
2018-12-17 16:37:19 +01:00
2021-05-28 14:34:18 -07:00
const result = await database . query ( 'UPDATE notifications SET ' + fields . join ( ', ' ) + ' WHERE id = ?' , args ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Notification not found' ) ;
2018-12-17 16:37:19 +01:00
}
2018-12-17 17:35:19 +01:00
2021-05-28 14:34:18 -07:00
async function del ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
2019-01-07 14:56:43 +01:00
2021-05-28 14:34:18 -07:00
const result = await database . query ( 'DELETE FROM notifications WHERE id = ?' , [ id ] ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Notification not found' ) ;
}
2019-01-17 13:36:54 +01:00
2021-05-28 14:34:18 -07:00
async function list ( filters , page , perPage ) {
assert . strictEqual ( typeof filters , 'object' ) ;
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
2019-01-17 13:36:54 +01:00
2023-03-26 14:42:38 +02:00
const args = [ ] ;
2019-01-07 14:56:43 +01:00
2023-03-26 14:42:38 +02:00
const where = [ ] ;
2021-05-28 14:34:18 -07:00
if ( 'acknowledged' in filters ) {
where . push ( 'acknowledged=?' ) ;
args . push ( filters . acknowledged ) ;
}
2021-04-19 20:52:10 -07:00
2021-05-28 14:34:18 -07:00
let query = ` SELECT ${ NOTIFICATION _FIELDS } FROM notifications ` ;
if ( where . length ) query += ' WHERE ' + where . join ( ' AND ' ) ;
query += ' ORDER BY creationTime DESC LIMIT ?,?' ;
2021-04-19 20:52:10 -07:00
2021-05-28 14:34:18 -07:00
args . push ( ( page - 1 ) * perPage ) ;
args . push ( perPage ) ;
2021-04-19 20:52:10 -07:00
2021-05-28 14:34:18 -07:00
const results = await database . query ( query , args ) ;
results . forEach ( postProcess ) ;
return results ;
2021-04-19 20:52:10 -07:00
}
2021-09-19 17:32:48 -07:00
async function oomEvent ( eventId , containerId , app , addonName /*, event*/ ) {
2019-01-19 13:22:29 +01:00
assert . strictEqual ( typeof eventId , 'string' ) ;
2019-03-06 11:54:37 -08:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-09-19 17:32:48 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
assert ( addonName === null || typeof addonName === 'string' ) ;
2019-01-07 12:57:57 +01:00
2021-09-19 17:32:48 -07:00
assert ( app || addonName ) ;
2021-01-02 11:07:44 -08:00
2023-08-11 19:41:05 +05:30
const { fqdn : dashboardFqdn } = await dashboard . getLocation ( ) ;
2021-01-02 11:07:44 -08:00
let title , message ;
2021-09-19 17:32:48 -07:00
if ( addonName ) {
if ( app ) {
title = ` The ${ addonName } service of the app at ${ app . fqdn } ran out of memory ` ;
} else {
title = ` The ${ addonName } service ran out of memory ` ;
}
2023-08-11 19:41:05 +05:30
message = ` The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https:// ${ dashboardFqdn } /#/services) ` ;
2021-09-19 17:32:48 -07:00
} else if ( app ) {
title = ` The app at ${ app . fqdn } ran out of memory. ` ;
2023-08-11 19:41:05 +05:30
message = ` The app has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https:// ${ dashboardFqdn } /#/app/ ${ app . id } /resources) ` ;
2019-03-06 11:54:37 -08:00
}
2023-09-22 17:58:13 +02:00
await add ( exports . ALERT _APP _OOM , title , message , { eventId } ) ;
2019-01-07 12:57:57 +01:00
}
2019-01-07 13:01:27 +01:00
2021-07-30 14:30:08 +02:00
async function appUpdated ( eventId , app , fromManifest , toManifest ) {
2019-05-07 12:04:43 +02:00
assert . strictEqual ( typeof eventId , 'string' ) ;
assert . strictEqual ( typeof app , 'object' ) ;
2021-07-30 14:30:08 +02:00
assert . strictEqual ( typeof fromManifest , 'object' ) ;
assert . strictEqual ( typeof toManifest , 'object' ) ;
2019-05-07 12:04:43 +02:00
2021-05-28 14:34:18 -07:00
if ( ! app . appStoreId ) return ; // skip notification of dev apps
2020-09-03 13:26:51 -07:00
2021-07-30 14:30:08 +02:00
const tmp = toManifest . description . match ( /<upstream>(.*)<\/upstream>/i ) ;
2019-08-02 20:56:25 -07:00
const upstreamVersion = ( tmp && tmp [ 1 ] ) ? tmp [ 1 ] : '' ;
2021-07-30 14:30:08 +02:00
const title = upstreamVersion ? ` ${ toManifest . title } at ${ app . fqdn } updated to ${ upstreamVersion } (package version ${ toManifest . version } ) `
: ` ${ toManifest . title } at ${ app . fqdn } updated to package version ${ toManifest . version } ` ;
2019-08-02 20:56:25 -07:00
2023-09-22 17:58:13 +02:00
await add ( exports . ALERT _APP _UPDATED , title , ` The application installed at https:// ${ app . fqdn } was updated. \n \n Changelog: \n ${ toManifest . changelog } \n ` , { eventId } ) ;
2019-05-07 12:04:43 +02:00
}
2022-04-01 13:44:46 -07:00
async function boxInstalled ( eventId , version ) {
assert . strictEqual ( typeof eventId , 'string' ) ;
assert . strictEqual ( typeof version , 'string' ) ;
const changes = changelog . getChanges ( version . replace ( /\.([^.]*)$/ , '.0' ) ) ; // last .0 release
const changelogMarkdown = changes . map ( ( m ) => ` * ${ m } \n ` ) . join ( '' ) ;
2023-09-22 17:58:13 +02:00
await add ( exports . ALERT _CLOUDRON _INSTALLED , ` Cloudron v ${ version } installed ` , ` Cloudron v ${ version } was installed. \n \n Please join our community at ${ constants . FORUM _URL } . \n \n Changelog: \n ${ changelogMarkdown } \n ` , { eventId } ) ;
2022-04-01 13:44:46 -07:00
}
2021-05-28 14:34:18 -07:00
async function boxUpdated ( eventId , oldVersion , newVersion ) {
2019-10-14 09:30:20 -07:00
assert . strictEqual ( typeof eventId , 'string' ) ;
2019-08-03 13:59:11 -07:00
assert . strictEqual ( typeof oldVersion , 'string' ) ;
assert . strictEqual ( typeof newVersion , 'string' ) ;
const changes = changelog . getChanges ( newVersion ) ;
const changelogMarkdown = changes . map ( ( m ) => ` * ${ m } \n ` ) . join ( '' ) ;
2023-09-22 17:58:13 +02:00
await add ( exports . ALERT _CLOUDRON _UPDATED , ` Cloudron updated to v ${ newVersion } ` , ` Cloudron was updated from v ${ oldVersion } to v ${ newVersion } . \n \n Changelog: \n ${ changelogMarkdown } \n ` , { eventId } ) ;
2019-10-14 09:30:20 -07:00
}
2021-05-28 14:34:18 -07:00
async function boxUpdateError ( eventId , errorMessage ) {
2019-10-14 09:30:20 -07:00
assert . strictEqual ( typeof eventId , 'string' ) ;
assert . strictEqual ( typeof errorMessage , 'string' ) ;
2023-09-22 17:58:13 +02:00
await add ( exports . ALERT _CLOUDRON _UPDATE _FAILED , 'Cloudron update failed' , ` Failed to update Cloudron: ${ errorMessage } . ` , { eventId } ) ;
2019-08-03 13:59:11 -07:00
}
2022-07-13 09:26:27 +05:30
async function certificateRenewalError ( eventId , fqdn , errorMessage ) {
2019-03-04 14:50:56 -08:00
assert . strictEqual ( typeof eventId , 'string' ) ;
2022-07-13 09:26:27 +05:30
assert . strictEqual ( typeof fqdn , 'string' ) ;
2019-03-04 14:50:56 -08:00
assert . strictEqual ( typeof errorMessage , 'string' ) ;
2023-09-22 17:58:13 +02:00
await add ( exports . ALERT _CERTIFICATE _RENEWAL _FAILED , ` Certificate renewal of ${ fqdn } failed ` , ` Failed to renew certs of ${ fqdn } : ${ errorMessage } . Renewal will be retried in 12 hours. ` , { eventId } ) ;
2021-05-28 14:34:18 -07:00
2021-07-15 09:50:11 -07:00
const admins = await users . getAdmins ( ) ;
for ( const admin of admins ) {
2022-07-13 09:26:27 +05:30
await mailer . certificateRenewalError ( admin . email , fqdn , errorMessage ) ;
2021-07-15 09:50:11 -07:00
}
2019-03-04 14:50:56 -08:00
}
2021-05-28 14:34:18 -07:00
async function backupFailed ( eventId , taskId , errorMessage ) {
2019-03-04 15:00:23 -08:00
assert . strictEqual ( typeof eventId , 'string' ) ;
2019-03-04 17:52:31 -08:00
assert . strictEqual ( typeof taskId , 'string' ) ;
2019-03-04 15:00:23 -08:00
assert . strictEqual ( typeof errorMessage , 'string' ) ;
2023-09-22 17:58:13 +02:00
await add ( exports . ALERT _BACKUP _FAILED , 'Backup failed' , ` Backup failed: ${ errorMessage } . Logs are available [here](/frontend/logs.html?taskId= ${ taskId } ). ` , { eventId } ) ;
2021-06-23 16:50:19 -07:00
// only send mail if the past 3 automated backups failed
2021-08-20 11:27:35 -07:00
const backupEvents = await eventlog . listPaged ( [ eventlog . ACTION _BACKUP _FINISH ] , null /* search */ , 1 , 20 ) ;
2021-06-23 16:50:19 -07:00
let count = 0 ;
for ( const event of backupEvents ) {
if ( ! event . data . errorMessage ) return ; // successful backup (manual or cron)
2021-09-30 09:50:30 -07:00
if ( event . source . username === AuditSource . CRON . username && ++ count === 3 ) break ; // last 3 consecutive crons have failed
2021-06-23 16:50:19 -07:00
}
if ( count !== 3 ) return ; // less than 3 failures
2023-08-11 19:41:05 +05:30
const { fqdn : dashboardFqdn } = await dashboard . getLocation ( ) ;
2021-07-15 09:50:11 -07:00
const superadmins = await users . getSuperadmins ( ) ;
for ( const superadmin of superadmins ) {
2023-08-26 09:35:55 +02:00
await mailer . backupFailed ( superadmin . email , errorMessage , ` https:// ${ dashboardFqdn } /frontend/logs.html?taskId= ${ taskId } ` ) ;
2021-07-15 09:50:11 -07:00
}
2019-03-04 15:00:23 -08:00
}
2023-09-22 17:58:13 +02:00
// type must be one of ALERT_*
async function alert ( type , title , message , options ) {
assert . strictEqual ( typeof type , 'string' ) ;
2019-02-28 14:52:14 -08:00
assert . strictEqual ( typeof title , 'string' ) ;
2019-02-06 15:47:31 +01:00
assert . strictEqual ( typeof message , 'string' ) ;
2023-03-26 15:12:00 +02:00
assert . strictEqual ( typeof options , 'object' ) ;
2019-02-06 15:47:31 +01:00
2021-05-28 14:34:18 -07:00
const result = await getByTitle ( title ) ;
2023-09-22 17:58:13 +02:00
if ( ! result ) return await add ( type , title , message , { eventId : null } ) ;
2023-03-26 15:12:00 +02:00
if ( ! options . persist ) return result . id ;
await update ( result , {
id : result . id ,
eventId : null ,
2023-09-26 14:14:09 +02:00
type : type ,
2023-03-26 15:12:00 +02:00
title ,
message ,
acknowledged : false ,
creationTime : new Date ( )
} ) ;
return result . id ;
2019-02-06 15:47:31 +01:00
}
2019-02-27 16:10:54 -08:00
2023-03-26 14:18:37 +02:00
// id is unused but nice to search code
2023-09-22 17:58:13 +02:00
async function clearAlert ( type ) {
assert . strictEqual ( typeof type , 'string' ) ;
2023-03-26 14:18:37 +02:00
2023-09-22 17:58:13 +02:00
await database . query ( 'DELETE FROM notifications WHERE type = ?' , [ type ] ) ;
2023-03-26 14:18:37 +02:00
return null ;
}
2021-05-28 14:34:18 -07:00
async function onEvent ( id , action , source , data ) {
2019-02-27 16:10:54 -08:00
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof action , 'string' ) ;
assert . strictEqual ( typeof source , 'object' ) ;
assert . strictEqual ( typeof data , 'object' ) ;
2019-03-01 15:14:35 -08:00
switch ( action ) {
2019-03-25 14:02:23 -07:00
case eventlog . ACTION _APP _OOM :
2021-09-19 17:32:48 -07:00
return await oomEvent ( id , data . containerId , data . app , data . addonName , data . event ) ;
2019-03-25 14:02:23 -07:00
2019-05-07 12:04:43 +02:00
case eventlog . ACTION _APP _UPDATE _FINISH :
2021-09-30 09:50:30 -07:00
if ( source . username !== AuditSource . CRON . username ) return ; // updated by user
2021-07-13 13:29:42 -07:00
if ( data . errorMessage ) return ; // the update indicator will still appear, so no need to notify user
2021-07-30 14:30:08 +02:00
return await appUpdated ( id , data . app , data . fromManifest , data . toManifest ) ;
2019-05-07 12:04:43 +02:00
2019-03-04 14:50:56 -08:00
case eventlog . ACTION _CERTIFICATE _RENEWAL :
case eventlog . ACTION _CERTIFICATE _NEW :
2021-05-28 14:34:18 -07:00
if ( ! data . errorMessage ) return ;
2021-06-24 00:48:54 -07:00
if ( ! data . notAfter || ( data . notAfter - new Date ( ) >= ( 10 * 24 * 60 * 60 * 1000 ) ) ) return ; // more than 10 days left to expire
2021-05-28 14:34:18 -07:00
return await certificateRenewalError ( id , data . domain , data . errorMessage ) ;
2019-03-04 14:50:56 -08:00
2019-03-25 14:02:23 -07:00
case eventlog . ACTION _BACKUP _FINISH :
2021-05-28 14:34:18 -07:00
if ( ! data . errorMessage ) return ;
2021-09-30 09:50:30 -07:00
if ( source . username !== AuditSource . CRON . username && ! data . timedOut ) return ; // manual stop by user
2019-10-11 19:35:21 -07:00
2021-05-28 14:34:18 -07:00
return await backupFailed ( id , data . taskId , data . errorMessage ) ; // only notify for automated backups or timedout
2019-03-04 15:00:23 -08:00
2022-04-01 13:44:46 -07:00
case eventlog . ACTION _INSTALL _FINISH :
return await boxInstalled ( id , data . version ) ;
2019-08-03 13:59:11 -07:00
case eventlog . ACTION _UPDATE _FINISH :
2021-05-28 14:34:18 -07:00
if ( ! data . errorMessage ) return await boxUpdated ( id , data . oldVersion , data . newVersion ) ;
if ( data . timedOut ) return await boxUpdateError ( id , data . errorMessage ) ;
return ;
2019-08-03 13:59:11 -07:00
2019-03-25 14:02:23 -07:00
default :
2021-05-28 14:34:18 -07:00
return ;
2019-02-27 16:10:54 -08:00
}
}