diff --git a/app/assets/app/dash/NavBar.component.js b/app/assets/app/dash/NavBar.component.js index e5eb349..1729c5c 100644 --- a/app/assets/app/dash/NavBar.component.js +++ b/app/assets/app/dash/NavBar.component.js @@ -38,6 +38,7 @@ const template = ` My Profile API Tokens + System Announcements Sign-Out of {{ app_name }} @@ -64,6 +65,7 @@ export default class NavBarComponent extends Component { async vue_on_create() { this.can.api_tokens = await session.check_permissions('v1:reflect:tokens:list') + this.can.messages = await session.check_permissions('v1:message:banners:create') this.$forceUpdate() } diff --git a/app/assets/app/resource/system/Announcement.resource.js b/app/assets/app/resource/system/Announcement.resource.js new file mode 100644 index 0000000..869b315 --- /dev/null +++ b/app/assets/app/resource/system/Announcement.resource.js @@ -0,0 +1,98 @@ +import CRUDBase from '../CRUDBase.js' + +class AnnouncementResource extends CRUDBase { + endpoint = '/api/v1/system/announcements' + required_fields = ['user_ids', 'group_ids', 'title', 'message', 'type'] + permission_base = 'v1:system:announcements' + + item = 'System Announcement' + plural = 'System Announcements' + + listing_definition = { + display: ` + System announcements are administrative messages that you want all or some of your users to see. These messages can be delivered via e-mail, as a message after login, or as a system banner announcement. + `, + columns: [ + { + name: 'Title', + field: 'title', + }, + { + name: 'Message', + field: 'message', + renderer: (message) => String(message).slice(0, 150), + }, + ], + actions: [ + { + type: 'resource', + position: 'main', + action: 'insert', + text: 'Create New', + color: 'success', + }, + { + type: 'resource', + position: 'row', + action: 'delete', + icon: 'fa fa-times', + color: 'danger', + confirm: true, + }, + ], + } + + form_definition = { + fields: [ + { + name: 'Title', + field: 'title', + type: 'text', + }, + { + name: 'Message', + field: 'message', + type: 'textarea', + }, + { + name: 'Users', + field: 'user_ids', + type: 'select.dynamic.multiple', + options: { + resource: 'auth/User', + display: (user) => `${user.last_name}, ${user.first_name} (${user.uid})`, + value: 'id', + }, + }, + { + name: 'Groups', + field: 'group_ids', + type: 'select.dynamic.multiple', + options: { + resource: 'auth/Group', + display: (group) => `${group.name}`, + value: 'id', + }, + }, + { + name: 'Type', + field: 'type', + type: 'select', + options: [ + { display: 'Login Intercept', value: 'login' }, + { display: 'E-Mail', value: 'email' }, + { display: 'System Banner', value: 'banner' }, + ], + }, + ], + handlers: { + insert: { + action: 'redirect', + next: '/dash/c/listing/system/Announcement', + }, + } + } +} + +const system_announcement = new AnnouncementResource() +export { system_announcement } diff --git a/app/controllers/api/v1/Message.controller.js b/app/controllers/api/v1/Message.controller.js index 20cc2ec..7f665ba 100644 --- a/app/controllers/api/v1/Message.controller.js +++ b/app/controllers/api/v1/Message.controller.js @@ -40,6 +40,26 @@ class MessageController extends Controller { await message.dismiss() return res.api() } + + async create_banner(req, res, next) { + // expires, display_type = info, message, user_id? + const expires = req.body.expires + const display_type = req.body.display_type || 'info' + const message = req.body.message + const user_ids = req.body.user_ids + + if ( !expires ) + return res.status(400) + .message(`${req.T('api.missing_field')} expires`) + .api() + + if ( !message ) + return res.status(400) + .message(`${req.T('api.missing_field')} message`) + .api() + + + } } module.exports = exports = MessageController diff --git a/app/controllers/api/v1/System.controller.js b/app/controllers/api/v1/System.controller.js new file mode 100644 index 0000000..699f448 --- /dev/null +++ b/app/controllers/api/v1/System.controller.js @@ -0,0 +1,84 @@ +const { Controller } = require('libflitter') + +class ReflectController extends Controller { + static get services() { + return [...super.services, 'routers', 'models', 'activity'] + } + + async get_announcements(req, res, next) { + const Announcement = this.models.get('system:Announcement') + const announcements = await Announcement.find() + + const data = [] + for ( const announcement of announcements ) { + data.push(await announcement.to_api()) + } + + return res.api(data) + } + + async get_announcement(req, res, next) { + const Announcement = this.models.get('system:Announcement') + const announcement = await Announcement.findById(req.params.id) + + if ( !announcement ) + return res.status(404) + .message(req.T('api.announcement_not_found')) + .api() + + return res.api(await announcement.to_api()) + } + + async create_announcement(req, res, next) { + const Announcement = this.models.get('system:Announcement') + + const required_fields = ['title', 'message', 'user_ids', 'group_ids', 'type'] + for ( const field of required_fields ) { + if ( !req.body[field] ) + return res.status(400) + .message(`${req.T('api.missing_field')} ${field}`) + .api() + } + + if ( !Array.isArray(req.body.user_ids) ) + return res.status(400) + .message(`${req.T('api.improper_field')} user_ids`) + .api() + + if ( !Array.isArray(req.body.group_ids) ) + return res.status(400) + .message(`${req.T('api.improper_field')} group_ids`) + .api() + + if ( !['email', 'login', 'banner'].includes(req.body.type) ) + return res.status(400) + .message(`${req.T('api.improper_field')} type`) + .api() + + const announcement = new Announcement({ + title: req.body.title, + message: req.body.message, + user_ids: req.body.user_ids, + group_ids: req.body.group_ids, + type: req.body.type, + }) + + await announcement.save() + return res.api(await announcement.to_api()) + } + + async delete_announcement(req, res, next) { + const Announcement = this.models.get('system:Announcement') + const announcement = await Announcement.findById(req.params.id) + + if ( !announcement ) + return res.status(404) + .message(req.T('api.announcement_not_found')) + .api() + + await announcement.delete() + return res.api() + } +} + +module.exports = exports = ReflectController diff --git a/app/models/oauth/Client.model.js b/app/models/oauth/Client.model.js index 9aa8ee7..df4b48b 100644 --- a/app/models/oauth/Client.model.js +++ b/app/models/oauth/Client.model.js @@ -24,10 +24,6 @@ class ClientModel extends Model { } } - can(scope) { - return this.api_scopes.includes() - } - async application() { const Application = this.models.get('Application') return Application.findOne({ active: true, oauth_client_ids: this.id }) diff --git a/app/models/system/Announcement.model.js b/app/models/system/Announcement.model.js new file mode 100644 index 0000000..abceb38 --- /dev/null +++ b/app/models/system/Announcement.model.js @@ -0,0 +1,26 @@ +const { Model } = require('flitter-orm') + +class AnnouncementModel extends Model { + static get schema() { + return { + title: String, + message: String, + user_ids: [String], + group_ids: [String], + type: String, // login | email | banner + } + } + + async to_api() { + return { + id: this.id, + title: this.title, + message: this.message, + user_ids: this.user_ids, + group_ids: this.group_ids, + type: this.type, + } + } +} + +module.exports = exports = AnnouncementModel diff --git a/app/routing/routers/api/v1/message.routes.js b/app/routing/routers/api/v1/message.routes.js index bb2f674..e2d9b79 100644 --- a/app/routing/routers/api/v1/message.routes.js +++ b/app/routing/routers/api/v1/message.routes.js @@ -17,6 +17,10 @@ const message_routes = { ['middleware::api:Permission', { check: 'v1:message:banners:update' }], 'controller::api:v1:Message.read_banner', ], + '/banners': [ + ['middleware::api:Permission', { check: 'v1:message:banners:create' }], + 'controller::api:v1:Message.create_banner', + ], }, } diff --git a/app/routing/routers/api/v1/system.routes.js b/app/routing/routers/api/v1/system.routes.js new file mode 100644 index 0000000..cbbd2ae --- /dev/null +++ b/app/routing/routers/api/v1/system.routes.js @@ -0,0 +1,34 @@ +const system_routes = { + prefix: '/api/v1/system', + + middleware: [ + 'auth:APIRoute' + ], + + get: { + '/announcements': [ + ['middleware::api:Permission', { check: 'v1:system:announcements:list' }], + 'controller::api:v1:System.get_announcements', + ], + '/announcements/:id': [ + ['middleware::api:Permission', { check: 'v1:system:announcements:get' }], + 'controller::api:v1:System.get_announcement', + ], + }, + + post: { + '/announcements': [ + ['middleware::api:Permission', { check: 'v1:system:announcements:create'}], + 'controller::api:v1:System.create_announcement', + ], + }, + + delete: { + '/announcements/:id': [ + ['middleware::api:Permission', { check: 'v1:system:announcements:delete' }], + 'controller::api:v1:System.delete_announcement', + ], + }, +} + +module.exports = exports = system_routes diff --git a/locale/en_US/api.locale.js b/locale/en_US/api.locale.js index 7e8732e..3e68299 100644 --- a/locale/en_US/api.locale.js +++ b/locale/en_US/api.locale.js @@ -21,6 +21,8 @@ module.exports = exports = { app_pw_not_found: 'App password not found with that UUID.', + announcement_not_found: 'Announcement not found with that ID.', + invalid_ldap_client_id: 'Invalid ldap_client_id:', invalid_oauth_client_id: 'Invalid oauth_client_id:', invalid_saml_service_provider_id: 'Invalid saml_service_provider_id:',