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 ,
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' ,
2019-02-28 14:52:14 -08:00
2021-04-19 20:52:10 -07:00
alert ,
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' ) ,
2019-10-11 19:31:03 -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' ) ,
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' ) ,
2019-07-26 10:49:29 -07:00
settings = require ( './settings.js' ) ,
2021-05-28 14:34:18 -07:00
users = require ( './users.js' ) ,
util = require ( 'util' ) ;
const NOTIFICATION _FIELDS = [ 'id' , 'eventId' , 'title' , 'message' , 'creationTime' , 'acknowledged' ] ;
function postProcess ( result ) {
assert . strictEqual ( typeof result , 'object' ) ;
result . id = String ( result . id ) ;
2018-12-17 16:37:19 +01:00
2021-05-28 14:34:18 -07:00
// convert to boolean
result . acknowledged = ! ! result . acknowledged ;
return result ;
}
async function add ( eventId , title , message ) {
2019-02-06 16:18:12 +01:00
assert ( typeof eventId === 'string' || eventId === null ) ;
2018-12-17 16:37:19 +01:00
assert . strictEqual ( typeof title , 'string' ) ;
assert . strictEqual ( typeof message , 'string' ) ;
2021-05-28 14:34:18 -07:00
const query = 'INSERT INTO notifications (eventId, title, message, acknowledged) VALUES (?, ?, ?, ?)' ;
const args = [ eventId , title , message , false ] ;
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' ) ;
2021-05-28 14:34:18 -07:00
const result = await database . query ( 'SELECT ' + NOTIFICATION _FIELDS + ' FROM notifications WHERE id = ?' , [ id ] ) ;
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
2021-05-28 14:34:18 -07:00
const results = await database . query ( 'SELECT ' + NOTIFICATION _FIELDS + ' from notifications WHERE title = ? ORDER BY creationTime LIMIT 1' , [ title ] ) ;
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
2021-05-28 14:34:18 -07:00
let args = [ ] ;
let fields = [ ] ;
for ( let k in data ) {
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
2021-05-28 14:34:18 -07:00
let args = [ ] ;
2019-01-07 14:56:43 +01:00
2021-05-28 14:34:18 -07:00
let where = [ ] ;
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-05-28 14:34:18 -07:00
async function oomEvent ( eventId , app , addon , containerId /*, 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 app , 'object' ) ;
assert . strictEqual ( typeof addon , 'object' ) ;
assert . strictEqual ( typeof containerId , 'string' ) ;
2019-01-07 12:57:57 +01:00
2021-01-02 11:07:44 -08:00
assert ( app || addon ) ;
let title , message ;
2019-03-06 11:54:37 -08:00
if ( app ) {
2020-09-03 13:26:51 -07:00
title = ` The application at ${ app . fqdn } ran out of memory. ` ;
2021-05-05 12:29:04 -07:00
message = ` The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit]( ${ settings . dashboardOrigin ( ) } /#/app/ ${ app . id } /resources) ` ;
2019-03-06 11:54:37 -08:00
} else if ( addon ) {
title = ` The ${ addon . name } service ran out of memory ` ;
2021-05-05 12:29:04 -07:00
message = ` The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit]( ${ settings . dashboardOrigin ( ) } /#/services) ` ;
2019-03-06 11:54:37 -08:00
}
2021-05-28 14:34:18 -07:00
await add ( eventId , title , message ) ;
2019-01-07 12:57:57 +01:00
}
2019-01-07 13:01:27 +01:00
2021-05-28 14:34:18 -07:00
async function appUpdated ( eventId , app ) {
2019-05-07 12:04:43 +02:00
assert . strictEqual ( typeof eventId , 'string' ) ;
assert . strictEqual ( typeof app , 'object' ) ;
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
2019-08-02 20:56:25 -07:00
const tmp = app . manifest . description . match ( /<upstream>(.*)<\/upstream>/i ) ;
const upstreamVersion = ( tmp && tmp [ 1 ] ) ? tmp [ 1 ] : '' ;
const title = upstreamVersion ? ` ${ app . manifest . title } at ${ app . fqdn } updated to ${ upstreamVersion } (package version ${ app . manifest . version } ) `
: ` ${ app . manifest . title } at ${ app . fqdn } updated to package version ${ app . manifest . version } ` ;
2021-05-28 14:34:18 -07:00
await add ( eventId , title , ` The application installed at https:// ${ app . fqdn } was updated. \n \n Changelog: \n ${ app . manifest . changelog } \n ` ) ;
2019-05-07 12:04:43 +02: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 ( '' ) ;
2021-05-28 14:34:18 -07:00
await add ( eventId , ` Cloudron updated to v ${ newVersion } ` , ` Cloudron was updated from v ${ oldVersion } to v ${ newVersion } . \n \n Changelog: \n ${ changelogMarkdown } \n ` ) ;
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' ) ;
2021-05-28 14:34:18 -07:00
await add ( eventId , 'Cloudron update failed' , ` Failed to update Cloudron: ${ errorMessage } . ` ) ;
2019-08-03 13:59:11 -07:00
}
2021-05-28 14:34:18 -07:00
async function certificateRenewalError ( eventId , vhost , errorMessage ) {
2019-03-04 14:50:56 -08:00
assert . strictEqual ( typeof eventId , 'string' ) ;
assert . strictEqual ( typeof vhost , 'string' ) ;
assert . strictEqual ( typeof errorMessage , 'string' ) ;
2021-05-28 14:34:18 -07:00
const getAdmins = util . callbackify ( users . getAdmins ) ;
const admins = await getAdmins ( ) ;
for ( const admin of admins ) {
2019-03-10 15:53:20 -07:00
mailer . certificateRenewalError ( admin . email , vhost , errorMessage ) ;
2021-05-28 14:34:18 -07:00
}
await add ( eventId , ` Certificate renewal of ${ vhost } failed ` , ` Failed to renew certs of ${ vhost } : ${ errorMessage } . Renewal will be retried in 12 hours ` ) ;
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' ) ;
2021-05-28 14:34:18 -07:00
const getSuperAdmins = util . callbackify ( users . getSuperAdmins ) ;
const superAdmins = await getSuperAdmins ( ) ;
for ( const superAdmin of superAdmins ) {
mailer . backupFailed ( superAdmin . email , errorMessage , ` ${ settings . dashboardOrigin ( ) } /logs.html?taskId= ${ taskId } ` ) ;
}
await add ( eventId , 'Backup failed' , ` Backup failed: ${ errorMessage } . Logs are available [here](/logs.html?taskId= ${ taskId } ). ` ) ;
2019-03-04 15:00:23 -08:00
}
2021-06-23 22:44:30 -07:00
// id is unused but nice to search code
2021-05-28 14:34:18 -07:00
async function alert ( id , title , message ) {
2019-04-30 16:26:16 +02:00
assert . strictEqual ( typeof id , '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' ) ;
2021-05-28 14:34:18 -07:00
const result = await getByTitle ( title ) ;
2021-06-23 22:44:30 -07:00
if ( message ) {
if ( ! result ) {
return await add ( null /* eventId */ , title , message ) ;
} else {
await update ( result , {
eventId : null ,
title ,
message ,
acknowledged : false ,
creationTime : new Date ( )
} ) ;
return result . id ;
}
2021-05-28 14:34:18 -07:00
} else {
2021-06-23 22:44:30 -07:00
await database . query ( 'DELETE FROM notifications WHERE title = ?' , [ title ] ) ;
return null ;
2021-05-28 14:34:18 -07:00
}
2019-02-06 15:47:31 +01:00
}
2019-02-27 16:10:54 -08:00
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-08-29 23:07:55 +02:00
// external ldap syncer does not generate notifications - FIXME username might be an issue here
2021-05-28 14:34:18 -07:00
if ( source . username === auditSource . EXTERNAL _LDAP _TASK . username ) return ;
2019-08-29 23:07:55 +02:00
2019-03-01 15:14:35 -08:00
switch ( action ) {
2019-03-25 14:02:23 -07:00
case eventlog . ACTION _APP _OOM :
2021-05-28 14:34:18 -07:00
return await oomEvent ( id , data . app , data . addon , data . containerId , 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-05-28 14:34:18 -07:00
return await appUpdated ( id , data . app ) ;
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-01 09:09:16 -07:00
if ( ! data . notAfter || ( data . notAfter - new Date ( ) >= ( 60 * 60 * 24 * 10 * 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 ;
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
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
}
}