├── CHANGELOG.md ├── README.md ├── flow-db-admin-tests.js ├── flow-db-admin.js ├── lib ├── both │ ├── AdminDashboard.coffee │ ├── collections.coffee │ ├── routes.js │ ├── startup.coffee │ └── utils.coffee ├── client │ ├── css │ │ └── admin-custom.less │ ├── html │ │ ├── admin_header.html │ │ ├── admin_sidebar.html │ │ ├── admin_templates.html │ │ ├── admin_widgets.html │ │ └── fadmin_layouts.html │ └── js │ │ ├── admin_layout.js │ │ ├── autoForm.coffee │ │ ├── events.coffee │ │ ├── helpers.coffee │ │ ├── slim_scroll.js │ │ └── templates.coffee └── server │ ├── methods.coffee │ └── publish.coffee └── package.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | 3 | Version 1.0.0 - 3rd Aug 2015 4 | *Update to flow-router 2.0 and blaze-layout Updated from meteorhacks:flow-router, to kadirahq:flow-router (flow-router 2.0) 5 | *Updated from meteorhacks:flow-layout, to kadirahq:blaze-layout Minor bug fixes. 6 | 7 | Version 1.0.3 8 | *Update to less for working with Meteor 1.2 thanks to @CaptainN 9 | 10 | Version 1.1 11 | *Updates to remove modals and fixes for Meteor 1.2 12 | 13 | Version 1.1.5 14 | * Update to add check for admin authorization check , thanks @markoshust 15 | * Updated to autoform version 5.7.1 16 | 17 | Version 1.2 18 | * Updated package transition to collection2-core 19 | * Bumped package versions to latest 20 | * Bumped Meteor version from 1.3 (because newer Tabular requires at least 1.3) 21 | * Added ecmascript to support import/require of Tabular and SimpleSchema -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Meteor Flow DB Admin 2 | ============ 3 | `$ meteor add sach:flow-db-admin` 4 | 5 | A fork of yogiben:admin package to work with flow-router instead of iron:router 6 | 7 | A complete admin dashboard solution for meteor built off the [kadira:flow-router](https://github.com/kadirahq/flow-router), [kadira:blaze-layout](https://github.com/kadirahq/blaze-layout), [alanning:roles](https://github.com/alanning/meteor-roles/) and [aldeed:autoform](https://github.com/aldeed/meteor-autoform) packages and frontend from the open source admin dashboard template, [Admin LTE](https://github.com/almasaeed2010/AdminLTE). 8 | 9 | 10 | ![alt tag](https://raw.githubusercontent.com/yogiben/meteor-admin/master/readme/screenshot1.png) 11 | 12 | ![alt tag](https://raw.githubusercontent.com/yogiben/meteor-admin/master/readme/screenshot2.png) 13 | 14 | ### Getting started ### 15 | 16 | #### 0. Prerequisites#### 17 | This package is designed to work with certain types of projects. Your project should be using and have configured 18 | * Flow Router - `meteor add kadira:flow-router` 19 | * Blaze Layout - `meteor add kadira:blaze-layout` 20 | * Collection Helpers - `meteor add dburles:collection-helpers` 21 | * Collection2 - `meteor add aldeed:collection2-core` 22 | * An accounts system - e.g. `meteor add accounts-base accounts-password` 23 | * Roles - `meteor add alanning:roles` 24 | * Bootstrap 3 - e.g. `meteor add twbs:bootstrap` 25 | * Fontawesome - e.g. `meteor add fortawesome:fontawesome` 26 | 27 | #### 1. Install #### 28 | Download to your packages directory and run `meteor add sach:flow-db-admin` then go to `/admin` to start 29 | 30 | #### 2. Config #### 31 | The simplest possible config with one, 'Posts', collection. 32 | 33 | ##### Server and Client ##### 34 | ```javascript 35 | AdminConfig = { 36 | collections: { 37 | Posts: {} 38 | } 39 | }; 40 | ``` 41 | This config will make the **first user** admin. 42 | 43 | You can also set the adminEmails property which will will override this. 44 | ```javascript 45 | AdminConfig = { 46 | name: 'My App', 47 | adminEmails: ['ben@code2create.com'], 48 | collections: { 49 | Posts: {} 50 | } 51 | }; 52 | ``` 53 | #### 3. Define your data models #### 54 | If you are unfamiliar with [autoform](https://github.com/aldeed/meteor-autoform) or [collection2](https://github.com/aldeed/meteor-collection2) or [collection-helpers](https://github.com/dburles/meteor-collection-helpers) you should check them out now. 55 | 56 | You need to define and attach a schema to the collections that you want to edit via the admin dashboard. Check out the [documentation](https://github.com/aldeed/meteor-collection2). 57 | ```javascript 58 | Schemas = {}; 59 | 60 | Posts = new Meteor.Collection('posts'); 61 | 62 | Schemas.Posts = new SimpleSchema({ 63 | title: { 64 | type: String, 65 | max: 60 66 | }, 67 | content: { 68 | type: String, 69 | autoform: { 70 | rows: 5 71 | } 72 | }, 73 | createdAt: { 74 | type: Date, 75 | label: 'Date', 76 | autoValue: function () { 77 | if (this.isInsert) { 78 | return new Date(); 79 | } 80 | } 81 | }, 82 | owner: { 83 | type: String, 84 | regEx: SimpleSchema.RegEx.Id, 85 | autoValue: function () { 86 | if (this.isInsert) { 87 | return Meteor.userId(); 88 | } 89 | }, 90 | autoform: { 91 | options: function () { 92 | return _.map(Meteor.users.find().fetch(), function (user) { 93 | return { 94 | label: user.emails[0].address, 95 | value: user._id 96 | }; 97 | }); 98 | } 99 | } 100 | } 101 | }); 102 | 103 | Posts.attachSchema(Schemas.Posts) 104 | ``` 105 | 106 | #### Collections #### 107 | `AdminConfig.collections` tells the dashboard which collections to manage based on the global variable name. 108 | ```javascript 109 | AdminConfig = { 110 | collections: { 111 | Posts: { 112 | // collection options 113 | }, 114 | Comments: { 115 | // collection options 116 | } 117 | } 118 | }; 119 | ``` 120 | It is possible to configure the way the collection is managed. 121 | ``` 122 | Comments: { 123 | icon: 'comment', 124 | omitFields: ['updatedAt'], 125 | tableColumns: [ 126 | { label: 'Content', name: 'content' }, 127 | { label: 'Post', name: 'postTitle()' }, 128 | { label: 'User', name: 'owner', template: 'userEmail' } 129 | ] 130 | showEditColumn: true, // Set to false to hide the edit button. True by default. 131 | showDelColumn: true, // Set to false to hide the edit button. True by default. 132 | showWidget: false, 133 | color: 'red' 134 | } 135 | ``` 136 | 137 | ##### Collection options ##### 138 | `icon` is the icon code from [Font Awesome](http://fortawesome.github.io/Font-Awesome/icons/). 139 | 140 | `tableColumns` an array of objects that describe the columns that will appear in the admin dashboard. 141 | 142 | * `{label: 'Content', name:'content'}` will display the `content` property of the mongo doc. 143 | * `{label: 'Post', name: 'postTitle()'}` will use `postTitle` collection helper (see `dburles:collection-helpers` package). 144 | * `{label: 'Joined', name: 'createdAt', template: 'prettyDate'}` will display `createdAt` field using `prettyDate` template. Following object will be set as the context: 145 | ``` 146 | { 147 | value: // current cell value 148 | doc: // current document 149 | } 150 | ``` 151 | 152 | `fields` is an array of field names - set when the form should only show these fields. From [AutoForm](https://github.com/aldeed/meteor-autoform). 153 | 154 | `extraFields` fields to be subscribed but not displayed in the table. Can be used if collection helper depends on the field which is not in the table. 155 | 156 | `omitFields` hides fields that we don't want appearing in the add / edit screens like 'updatedAt' for example. From [AutoForm](https://github.com/aldeed/meteor-autoform). 157 | 158 | `showWidget` when set to false hides the corresponding widget from the dashboard. 159 | 160 | `color` styles the widget. See the [LTE Admin documentation](http://almsaeedstudio.com/preview/). 161 | 162 | #### Users #### 163 | 164 | The Meteor.users collection is automatically added to the admin panel. 165 | You can create, view and delete users. 166 | 167 | If you have attached a schema to the user, it will automatically be used for the edit form. 168 | You can disable this functionality, or customize the schema that is used. 169 | 170 | ```javascript 171 | AdminConfig = { 172 | //... 173 | 174 | // Disable editing of user fields: 175 | userSchema: null, 176 | 177 | // Use a custom SimpleSchema: 178 | userSchema: new SimpleSchema({ 179 | 'profile.gender': { 180 | type: String, 181 | allowedValues: ['male', 'female'] 182 | } 183 | }) 184 | } 185 | ``` 186 | 187 | #### Custom Templates #### 188 | The default admin templates are autoForm instances based on the schemas assigned to the collections. If they don't do the job, you specify a custom template to use for each of the `new`,`edit` and `view` screens for each collection. 189 | ```javascript 190 | AdminConfig = { 191 | // ... 192 | collections: { 193 | Posts: { 194 | templates: { 195 | new: { 196 | name: 'postWYSIGEditor' 197 | }, 198 | edit: { 199 | name: 'postWYSIGEditor', 200 | data: { 201 | post: Meteor.isClient && Session.get('admin_doc') 202 | } 203 | } 204 | } 205 | } 206 | } 207 | }; 208 | ``` 209 | The `/admin/Posts/new` and `/admin/Posts/edit` will not use the `postWYSIGEditor` template that you've defined somewhere in your code. The `edit` view will be rendered with a data context (here the document being edited). 210 | 211 | Custom templates are most used when you need to use an {{#autoForm}} instead of the default {{> quickForm}}. 212 | 213 | #### Autoform #### 214 | ```javascript 215 | AdminConfig = { 216 | // ... 217 | autoForm: 218 | omitFields: ['createdAt', 'updatedAt'] 219 | }; 220 | ``` 221 | Here you can specify globally the fields that should never appear in your `new` and `update` views. This is typically meta information likes dates. 222 | 223 | **Important** don't omit fields unless the schema specifies either an `autoValue` or `optional` is set to `true`. See [autoForm](https://github.com/aldeed/meteor-autoform). 224 | 225 | #### Dashboard #### 226 | Here you can customise the look and feel of the dashboard. 227 | ```javascript 228 | AdminConfig = { 229 | // ... 230 | dashboard: { 231 | homeUrl: '/dashboard', 232 | skin: 'black', 233 | widgets: [ 234 | { 235 | template: 'adminCollectionWidget', 236 | data: { 237 | collection: 'Posts', 238 | class: 'col-lg-3 col-xs-6' 239 | } 240 | }, 241 | { 242 | template: 'adminUserWidget', 243 | data: { 244 | class: 'col-lg-3 col-xs-6' 245 | } 246 | } 247 | ] 248 | } 249 | }; 250 | ``` 251 | `homeUrl` is the `href` property of the 'Home' button. Defaults to `/`. 252 | 253 | `skin` defaults to 'blue'. Available skins: `black black-light blue blue-light green green-light purple purple-light red red-light yellow yellow-light` 254 | 255 | `widgets` is an array of objects specifying template names and data contexts. Make sure to specify the `class` in the data context. If set, the `widgets` property will override the collection widgets which appear by default. 256 | 257 | #### Extending Dashboard #### 258 | There are few things you can do to integrate your package with meteor-admin. Remember to wrap it in Meteor.startup on client. 259 | 260 | ##### Add sidebar item with single link ##### 261 | 262 | ```javascript 263 | AdminDashboard.addSidebarItem('New User', AdminDashboard.path('/Users/new'), { icon: 'plus' }) 264 | ``` 265 | 266 | ##### Add sidebar item with multiple links ##### 267 | 268 | ```javascript 269 | AdminDashboard.addSidebarItem('Analytics', { 270 | icon: 'line-chart', 271 | urls: [ 272 | { title: 'Statistics', url: AdminDashboard.path('/analytics/statistics') }, 273 | { title: 'Settings', url: AdminDashboard.path('/analytics/settings') } 274 | ] 275 | }); 276 | ``` 277 | 278 | ##### Add link to collection item ##### 279 | 280 | This will iterate through all collection items in sidebar and call your function. If you return an object with the `title` and `url` properties the link will be added. Otherwise it will be ignored. 281 | 282 | ```javascript 283 | AdminDashboard.addCollectionItem(function (collection, path) { 284 | if (collection === 'Users') { 285 | return { 286 | title: 'Delete', 287 | url: path + '/delete' 288 | }; 289 | } 290 | }); 291 | ``` 292 | #### Change Log #### 293 | * Version 1.0.0 - 3rd Aug 2015 294 | Update to flow-router 2.0 and blaze-layout 295 | Updated from meteorhacks:flow-router, to kadirahq:flow-router (flow-router 2.0) 296 | Updated from meteorhacks:flow-layout, to kadirahq:blaze-layout 297 | Minor bug fixes. 298 | 299 | * Version 1.0.3 300 | Update to less for working with Meteor 1.2 301 | thanks to @CaptainN 302 | 303 | * Version 1.1 304 | Updates to remove modals and fixes for Meteor 1.2 305 | -------------------------------------------------------------------------------- /flow-db-admin-tests.js: -------------------------------------------------------------------------------- 1 | // Write your tests here! 2 | // Here is an example. 3 | Tinytest.add('example', function (test) { 4 | test.equal(true, true); 5 | }); 6 | 7 | -------------------------------------------------------------------------------- /flow-db-admin.js: -------------------------------------------------------------------------------- 1 | import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; 2 | 3 | checkNpmVersions({ 'simpl-schema': '0.x.x' }, 'sach:flow-db-admin'); 4 | 5 | -------------------------------------------------------------------------------- /lib/both/AdminDashboard.coffee: -------------------------------------------------------------------------------- 1 | SimpleSchema = require('simpl-schema').default; 2 | 3 | AdminDashboard = 4 | schemas: {} 5 | sidebarItems: [] 6 | collectionItems: [] 7 | alertSuccess: (message)-> 8 | Session.set 'adminError', null 9 | Session.set 'adminSuccess', message 10 | alertFailure: (message)-> 11 | Session.set 'adminSuccess', null 12 | Session.set 'adminError', message 13 | 14 | checkAdmin: -> 15 | if not Roles.userIsInRole Meteor.userId(), ['admin'] 16 | Meteor.call 'adminCheckAdmin' 17 | if (typeof AdminConfig?.nonAdminRedirectRoute == "string") 18 | Router.go AdminConfig.nonAdminRedirectRoute 19 | if typeof @.next == 'function' 20 | @next() 21 | adminRoutes: ['adminDashboard','adminDashboardUsersNew','adminDashboardUsersEdit','adminDashboardView','adminDashboardNew','adminDashboardEdit'] 22 | collectionLabel: (collection)-> 23 | if collection == 'Users' 24 | 'Users' 25 | else if collection? and typeof AdminConfig.collections[collection].label == 'string' 26 | AdminConfig.collections[collection].label 27 | else Session.get 'admin_collection_name' 28 | 29 | addSidebarItem: (title, url, options) -> 30 | item = title: title 31 | if _.isObject(url) and typeof options == 'undefined' 32 | item.options = url 33 | else 34 | item.url = url 35 | item.options = options 36 | 37 | @sidebarItems.push item 38 | 39 | extendSidebarItem: (title, urls) -> 40 | if _.isObject(urls) then urls = [urls] 41 | 42 | existing = _.find @sidebarItems, (item) -> item.title == title 43 | if existing 44 | existing.options.urls = _.union existing.options.urls, urls 45 | 46 | addCollectionItem: (fn) -> 47 | @collectionItems.push fn 48 | 49 | path: (s) -> 50 | path = '/admin' 51 | if typeof s == 'string' and s.length > 0 52 | path += (if s[0] == '/' then '' else '/') + s 53 | path 54 | 55 | 56 | AdminDashboard.schemas.newUser = new SimpleSchema 57 | email: 58 | type: String 59 | label: "Email address" 60 | chooseOwnPassword: 61 | type: Boolean 62 | label: 'Let this user choose their own password with an email' 63 | defaultValue: true 64 | password: 65 | type: String 66 | label: 'Password' 67 | optional: true 68 | sendPassword: 69 | type: Boolean 70 | label: 'Send this user their password by email' 71 | optional: true 72 | 73 | AdminDashboard.schemas.sendResetPasswordEmail = new SimpleSchema 74 | _id: 75 | type: String 76 | 77 | AdminDashboard.schemas.changePassword = new SimpleSchema 78 | _id: 79 | type: String 80 | password: 81 | type: String 82 | -------------------------------------------------------------------------------- /lib/both/collections.coffee: -------------------------------------------------------------------------------- 1 | @AdminCollectionsCount = new Mongo.Collection 'adminCollectionsCount' -------------------------------------------------------------------------------- /lib/both/routes.js: -------------------------------------------------------------------------------- 1 | 2 | var fadminRoutes = FlowRouter.group({ 3 | name: "AdminController", 4 | prefix: '/admin', 5 | subscriptions: function() { 6 | this.register('fadminUsers', Meteor.subscribe('adminUsers')); 7 | this.register('fadminUser', Meteor.subscribe('adminUser')); 8 | this.register('fadminCollectionsCount', Meteor.subscribe('adminCollectionsCount')); 9 | }, 10 | triggersEnter: [ 11 | function(context) { 12 | if(!Roles.userIsInRole (Meteor.userId(),['admin'])) 13 | { 14 | Meteor.call('adminCheckAdmin'); 15 | //if (typeof AdminConfig.nonAdminRedirectRoute == 'string') 16 | // FlowRouter.go(AdminController.nonAdminRedirectRoute); 17 | } 18 | }, 19 | function(context) { 20 | Session.set('adminSuccess', null); 21 | Session.set('adminError', null); 22 | Session.set('admin_title', null); 23 | Session.set('admin_subtitle', null); 24 | Session.set('admin_collection_name', null); 25 | Session.set('admin_collection_page', null); 26 | Session.set('admin_id',null); 27 | Session.set('admin_doc', null); 28 | } 29 | ], 30 | triggersExit: [ 31 | function(context) { 32 | /* Force displaying the loading template when a route 33 | * is left in order to prevent the setting of session 34 | * variables, in the Enter Triggers, from rerunning 35 | * computations in the currently displayed template 36 | * (which is going to be evicted anyway) 37 | */ 38 | BlazeLayout.render('fAdminLayout', {main: 'AdminLoading'}); 39 | Tracker.flush(); 40 | } 41 | ] 42 | }); 43 | 44 | fadminRoutes.route('/',{ 45 | name: 'adminDashboard', 46 | triggersEnter: [ 47 | function(context){ 48 | Session.set('admin_title',"Dashboard"); 49 | Session.set('admin_collection_name',""); 50 | Session.set('admin_collection_page',""); 51 | } 52 | ], 53 | action: function () 54 | { 55 | BlazeLayout.render('fAdminLayout', {main: 'AdminDashboard'}); 56 | } 57 | }); 58 | 59 | 60 | fadminRoutes.route('/view/:collectionName',{ 61 | triggersEnter: [ 62 | function(context){ 63 | Session.set('admin_title', context.params.collectionName); 64 | Session.set('admin_subtitle', 'View'); 65 | Session.set('admin_collection_page', 'view'); 66 | Session.set('admin_collection_name', context.params.collectionName); 67 | }], 68 | action: function(params) 69 | { 70 | BlazeLayout.render('fAdminLayout',{main: 'AdminDashboardView'}); 71 | } 72 | }); 73 | 74 | fadminRoutes.route('/new/:collectionName',{ 75 | triggersEnter: [function(context){ 76 | Session.set('admin_title', context.params.collectionName); 77 | Session.set('admin_subtitle', 'Create New'); 78 | Session.set('admin_collection_page', 'new'); 79 | Session.set('admin_collection_name', context.params.collectionName); 80 | }], 81 | action: function(params) 82 | { if(params.collectionName == 'Users') 83 | BlazeLayout.render('fAdminLayout',{main: 'AdminDashboardUsersNew'}); 84 | else 85 | BlazeLayout.render('fAdminLayout',{main: 'AdminDashboardNew'}); 86 | } 87 | }); 88 | 89 | fadminRoutes.route('/edit/:collectionName/:_id',{ 90 | triggersEnter: [function(context){ 91 | Session.set('admin_title', context.params.collectionName); 92 | Session.set('admin_subtitle', 'Edit'); 93 | Session.set('admin_collection_page', 'edit'); 94 | Session.set('admin_collection_name', context.params.collectionName); 95 | if (context.params.collectionName == 'Users') 96 | Session.set('admin_id', context.params._id); 97 | else 98 | Session.set('admin_id', null); 99 | 100 | }], 101 | triggersExit: [ 102 | function(context){ 103 | Session.set('admin_id',null); 104 | } 105 | ], 106 | subscriptions : function(params){ 107 | if (params.collectionName !== 'Users') 108 | this.register('admindoc2edit', Meteor.subscribe('adminCollectionDoc', params.collectionName, parseID(params._id))); 109 | }, 110 | action: function(params) 111 | { 112 | if(params.collectionName == 'Users') 113 | BlazeLayout.render('fAdminLayout',{main: 'AdminDashboardUsersEdit'}); 114 | else 115 | BlazeLayout.render('fAdminLayout',{main: 'AdminDashboardEdit'}); 116 | } 117 | }); 118 | -------------------------------------------------------------------------------- /lib/both/startup.coffee: -------------------------------------------------------------------------------- 1 | `import Tabular from 'meteor/aldeed:tabular'` 2 | 3 | @AdminTables = {} 4 | 5 | adminTablesDom = '<"box"<"box-header"<"box-toolbar"<"pull-left"><"pull-right"p>>><"box-body"t>>' 6 | 7 | adminEditButton = { 8 | data: '_id' 9 | title: 'Edit' 10 | createdCell: (node, cellData, rowData) -> 11 | $(node).html(Blaze.toHTMLWithData Template.adminEditBtn, {_id: cellData}, node) 12 | width: '40px' 13 | orderable: false 14 | } 15 | adminDelButton = { 16 | data: '_id' 17 | title: 'Delete' 18 | createdCell: (node, cellData, rowData) -> 19 | $(node).html(Blaze.toHTMLWithData Template.adminDeleteBtn, {_id: cellData}, node) 20 | width: '40px' 21 | orderable: false 22 | } 23 | 24 | adminEditDelButtons = [ 25 | adminEditButton, 26 | adminDelButton 27 | ] 28 | 29 | defaultColumns = () -> [ 30 | data: '_id', 31 | title: 'ID' 32 | ] 33 | 34 | AdminTables.Users = new Tabular.Table 35 | # Modify selector to allow search by email 36 | changeSelector: (selector, userId) -> 37 | $or = selector['$or'] 38 | $or and selector['$or'] = _.map $or, (exp) -> 39 | if exp.emails?['$regex']? 40 | emails: $elemMatch: address: exp.emails 41 | else 42 | exp 43 | selector 44 | 45 | name: 'Users' 46 | collection: Meteor.users 47 | columns: _.union [ 48 | { 49 | data: '_id' 50 | title: 'Admin' 51 | # TODO: use `tmpl` 52 | createdCell: (node, cellData, rowData) -> 53 | $(node).html(Blaze.toHTMLWithData Template.adminUsersIsAdmin, {_id: cellData}, node) 54 | width: '40px' 55 | } 56 | { 57 | data: 'emails' 58 | title: 'Email' 59 | render: (value) -> 60 | if value then value[0].address else '' 61 | searchable: true 62 | } 63 | { 64 | data: 'emails' 65 | title: 'Mail' 66 | # TODO: use `tmpl` 67 | createdCell: (node, cellData, rowData) -> 68 | $(node).html(Blaze.toHTMLWithData Template.adminUsersMailBtn, {emails: cellData}, node) 69 | width: '40px' 70 | } 71 | { data: 'createdAt', title: 'Joined' } 72 | ], adminEditDelButtons 73 | dom: adminTablesDom 74 | 75 | adminTablePubName = (collection) -> 76 | "admin_tabular_#{collection}" 77 | 78 | adminCreateTables = (collections) -> 79 | _.each AdminConfig?.collections, (collection, name) -> 80 | _.defaults collection, { 81 | showEditColumn: true 82 | showDelColumn: true 83 | } 84 | 85 | columns = _.map collection.tableColumns, (column) -> 86 | if column.template 87 | createdCell = (node, cellData, rowData) -> 88 | $(node).html '' 89 | Blaze.renderWithData(Template[column.template], {value: cellData, doc: rowData}, node) 90 | 91 | data: column.name 92 | title: column.label 93 | createdCell: createdCell 94 | 95 | if columns.length == 0 96 | columns = defaultColumns() 97 | 98 | if collection.showEditColumn 99 | columns.push(adminEditButton) 100 | if collection.showDelColumn 101 | columns.push(adminDelButton) 102 | 103 | AdminTables[name] = new Tabular.Table 104 | name: name 105 | collection: adminCollectionObject(name) 106 | pub: collection.children and adminTablePubName(name) 107 | sub: collection.sub 108 | columns: columns 109 | extraFields: collection.extraFields 110 | dom: adminTablesDom 111 | 112 | 113 | adminPublishTables = (collections) -> 114 | _.each collections, (collection, name) -> 115 | if not collection.children then return undefined 116 | Meteor.publishComposite adminTablePubName(name), (tableName, ids, fields) -> 117 | check tableName, String 118 | check ids, Array 119 | check fields, Match.Optional Object 120 | 121 | extraFields = _.reduce collection.extraFields, (fields, name) -> 122 | fields[name] = 1 123 | fields 124 | , {} 125 | _.extend fields, extraFields 126 | 127 | @unblock() 128 | 129 | find: -> 130 | @unblock() 131 | adminCollectionObject(name).find {_id: {$in: ids}}, {fields: fields} 132 | children: collection.children 133 | 134 | Meteor.startup -> 135 | adminCreateTables AdminConfig?.collections 136 | adminPublishTables AdminConfig?.collections if Meteor.isServer 137 | -------------------------------------------------------------------------------- /lib/both/utils.coffee: -------------------------------------------------------------------------------- 1 | @adminCollectionObject = (collection) -> 2 | if typeof AdminConfig.collections[collection] != 'undefined' and typeof AdminConfig.collections[collection].collectionObject != 'undefined' 3 | AdminConfig.collections[collection].collectionObject 4 | else 5 | lookup collection 6 | 7 | @adminCallback = (name, args, callback) -> 8 | stop = false 9 | if typeof AdminConfig?.callbacks?[name] == 'function' 10 | stop = AdminConfig.callbacks[name](args...) is false 11 | if typeof callback == 'function' 12 | callback args... unless stop 13 | 14 | @lookup = (obj, root, required=true) -> 15 | if typeof root == 'undefined' 16 | root = if Meteor.isServer then global else window 17 | if typeof obj == 'string' 18 | ref = root 19 | arr = obj.split '.' 20 | continue while arr.length and (ref = ref[arr.shift()]) 21 | if not ref and required 22 | throw new Error(obj + ' is not in the ' + root.toString()) 23 | else 24 | return ref 25 | return obj 26 | 27 | @parseID = (id) -> 28 | if typeof id == 'string' 29 | if(id.indexOf("ObjectID") > -1) 30 | return new Mongo.ObjectID(id.slice(id.indexOf('"') + 1,id.lastIndexOf('"'))) 31 | else 32 | return id 33 | else 34 | return id 35 | 36 | @parseIDs = (ids) -> 37 | return _.map ids, (id) -> 38 | parseID id 39 | -------------------------------------------------------------------------------- /lib/client/css/admin-custom.less: -------------------------------------------------------------------------------- 1 | .admin-alert 2 | { 3 | margin-left: 0px !important; 4 | } 5 | 6 | .admin-layout 7 | { 8 | th.admin-sortable 9 | { 10 | cursor: pointer; 11 | } 12 | 13 | .header 14 | { 15 | padding:0; 16 | } 17 | 18 | .dataTables_wrapper { 19 | 20 | table.dataTable { 21 | width: 100% !important; 22 | } 23 | 24 | table.dataTable > tbody > tr > td { 25 | background-color: white; 26 | } 27 | 28 | table.dataTable > thead:first-child > tr:first-child > th, 29 | table.dataTable > tbody > tr:nth-child(even) > td { 30 | background-color: #f3f4f5 !important; 31 | } 32 | 33 | table.dataTable > thead:first-child > tr:first-child > th { 34 | border-bottom: none !important; 35 | padding: 8px 36 | } 37 | 38 | table.dataTable > thead > tr:first-child > th, 39 | table.dataTable > tbody > tr > td { 40 | border-top: 1px solid #ddd !important; 41 | } 42 | 43 | .box-body { 44 | padding: 0; 45 | } 46 | 47 | .box > .box-header { 48 | padding-bottom: 0; 49 | } 50 | 51 | .box-toolbar { 52 | padding: 10px; 53 | 54 | .pull-left { 55 | min-width: 310px; 56 | } 57 | 58 | .dataTables_filter, 59 | .dataTables_length { 60 | float: left; 61 | } 62 | 63 | .dataTables_length { 64 | margin-right: 8px; 65 | } 66 | 67 | .dataTables_filter input { 68 | margin: 0; 69 | } 70 | } 71 | 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/client/html/admin_header.html: -------------------------------------------------------------------------------- 1 | 49 | -------------------------------------------------------------------------------- /lib/client/html/admin_sidebar.html: -------------------------------------------------------------------------------- 1 | 37 | 38 | 48 | 49 | 65 | -------------------------------------------------------------------------------- /lib/client/html/admin_templates.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 49 | 50 | 54 | 55 | 76 | 77 | 93 | 94 | 145 | 146 | 159 | 160 | 163 | 164 | 167 | 168 | 172 | 173 | 177 | -------------------------------------------------------------------------------- /lib/client/html/admin_widgets.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 45 | -------------------------------------------------------------------------------- /lib/client/html/fadmin_layouts.html: -------------------------------------------------------------------------------- 1 | 41 | 42 | 53 | 56 | 81 | -------------------------------------------------------------------------------- /lib/client/js/admin_layout.js: -------------------------------------------------------------------------------- 1 | BlazeLayout.setRoot('body'); 2 | Template.fAdminLayout.created = function () { 3 | var self = this; 4 | 5 | self.minHeight = new ReactiveVar( 6 | $(window).height() - $('.main-header').height()); 7 | 8 | $(window).resize(function () { 9 | self.minHeight.set($(window).height() - $('.main-header').height()); 10 | }); 11 | 12 | $('body').addClass('fixed'); 13 | }; 14 | 15 | Template.fAdminLayout.helpers({ 16 | minHeight: function () { 17 | return Template.instance().minHeight.get() + 'px' 18 | } 19 | }); 20 | 21 | 22 | dataTableOptions = { 23 | "aaSorting": [], 24 | "bPaginate": true, 25 | "bLengthChange": false, 26 | "bFilter": true, 27 | "bSort": true, 28 | "bInfo": true, 29 | "bAutoWidth": false 30 | }; 31 | -------------------------------------------------------------------------------- /lib/client/js/autoForm.coffee: -------------------------------------------------------------------------------- 1 | # Add hooks used by many forms 2 | AutoForm.addHooks [ 3 | 'admin_insert', 4 | 'admin_update', 5 | 'adminNewUser', 6 | 'adminUpdateUser', 7 | 'adminSendResetPasswordEmail', 8 | 'adminChangePassword'], 9 | beginSubmit: -> 10 | $('.btn-primary').addClass('disabled') 11 | endSubmit: -> 12 | $('.btn-primary').removeClass('disabled') 13 | onError: (formType, error)-> 14 | AdminDashboard.alertFailure error.message 15 | 16 | AutoForm.hooks 17 | admin_insert: 18 | onSubmit: (insertDoc, updateDoc, currentDoc)-> 19 | hook = @ 20 | Meteor.call 'adminInsertDoc', insertDoc, Session.get('admin_collection_name'), (e,r)-> 21 | if e 22 | hook.done(e) 23 | else 24 | adminCallback 'onInsert', [Session.get 'admin_collection_name', insertDoc, updateDoc, currentDoc], (collection) -> 25 | hook.done null, collection 26 | return false 27 | onSuccess: (formType, collection)-> 28 | AdminDashboard.alertSuccess 'Successfully created' 29 | FlowRouter.go "/admin/view/#{collection}" 30 | 31 | admin_update: 32 | onSubmit: (insertDoc, updateDoc, currentDoc)-> 33 | hook = @ 34 | Meteor.call 'adminUpdateDoc', updateDoc, Session.get('admin_collection_name'), @docId, (e,r)-> 35 | if e 36 | hook.done(e) 37 | else 38 | adminCallback 'onUpdate', [Session.get 'admin_collection_name', insertDoc, updateDoc, currentDoc], (collection) -> 39 | hook.done null, collection 40 | return false 41 | onSuccess: (formType, collection)-> 42 | AdminDashboard.alertSuccess 'Successfully updated' 43 | 44 | adminNewUser: 45 | onSuccess: (formType, result)-> 46 | AdminDashboard.alertSuccess 'Created user' 47 | 48 | adminUpdateUser: 49 | onSubmit: (insertDoc, updateDoc, currentDoc)-> 50 | Meteor.call 'adminUpdateUser', updateDoc, Session.get('admin_id'), @done 51 | return false 52 | onSuccess: (formType, result)-> 53 | AdminDashboard.alertSuccess 'Updated user' 54 | 55 | adminSendResetPasswordEmail: 56 | onSuccess: (formType, result)-> 57 | AdminDashboard.alertSuccess 'Email sent' 58 | 59 | adminChangePassword: 60 | onSuccess: (operation, result, template)-> 61 | AdminDashboard.alertSuccess 'Password reset' 62 | -------------------------------------------------------------------------------- /lib/client/js/events.coffee: -------------------------------------------------------------------------------- 1 | Template.fAdminLayout.events 2 | 'click .btn-delete': (e,t) -> 3 | _id = $(e.target).attr('doc') 4 | if Session.equals 'admin_collection_name', 'Users' 5 | Session.set 'admin_id', _id 6 | Session.set 'admin_doc', Meteor.users.findOne(_id) 7 | else 8 | Session.set 'admin_id', parseID(_id) 9 | Session.set 'admin_doc', adminCollectionObject(Session.get('admin_collection_name')).findOne(parseID(_id)) 10 | 11 | Template.AdminDashboardUsersEdit.events 12 | 'click .btn-add-role': (e,t) -> 13 | console.log 'adding user to role' 14 | Meteor.call 'adminAddUserToRole', $(e.target).attr('user'), $(e.target).attr('role') 15 | 'click .btn-remove-role': (e,t) -> 16 | console.log 'removing user from role' 17 | Meteor.call 'adminRemoveUserToRole', $(e.target).attr('user'), $(e.target).attr('role') 18 | 19 | Template.AdminHeader.events 20 | 'click .btn-sign-out': () -> 21 | Meteor.logout -> 22 | FlowRouter.go('/') 23 | 24 | Template.adminDeleteWidget.events 25 | 'click #confirm-delete': () -> 26 | collection = FlowRouter.getParam 'collectionName' 27 | _id = FlowRouter.getParam '_id' 28 | Meteor.call 'adminRemoveDoc', collection, _id, (e,r)-> 29 | FlowRouter.go '/admin/view/' + collection 30 | -------------------------------------------------------------------------------- /lib/client/js/helpers.coffee: -------------------------------------------------------------------------------- 1 | Template.registerHelper('AdminTables', AdminTables); 2 | 3 | adminCollections = -> 4 | collections = {} 5 | 6 | if typeof AdminConfig != 'undefined' and typeof AdminConfig.collections == 'object' 7 | collections = AdminConfig.collections 8 | 9 | collections.Users = 10 | collectionObject: Meteor.users 11 | icon: 'user' 12 | label: 'Users' 13 | 14 | _.map collections, (obj, key) -> 15 | obj = _.extend obj, {name: key} 16 | obj = _.defaults obj, {label: key, icon: 'plus', color: 'blue'} 17 | obj = _.extend obj, 18 | viewPath: FlowRouter.path "/admin/view/:coll",{coll: key} 19 | newPath: FlowRouter.path "/admin/new/:coll",{coll: key} 20 | 21 | Template.registerHelper 'AdminConfig', -> 22 | AdminConfig if typeof AdminConfig != 'undefined' 23 | 24 | Template.registerHelper 'admin_skin', -> 25 | AdminConfig?.skin or 'black-light' 26 | 27 | Template.registerHelper 'admin_collections', adminCollections 28 | 29 | Template.registerHelper 'admin_collection_name', -> 30 | Session.get 'admin_collection_name' 31 | 32 | Template.registerHelper 'admin_current_id', -> 33 | Session.get 'admin_id' 34 | 35 | Template.registerHelper 'admin_current_doc', -> 36 | Session.get 'admin_doc' 37 | 38 | Template.registerHelper 'admin_is_users_collection', -> 39 | Session.get('admin_collection_name') == 'Users' 40 | 41 | Template.registerHelper 'admin_sidebar_items', -> 42 | AdminDashboard.sidebarItems 43 | 44 | Template.registerHelper 'admin_collection_items', -> 45 | items = [] 46 | _.each AdminDashboard.collectionItems, (fn) => 47 | item = fn @name, '/admin/' + @name 48 | if item?.title and item?.url 49 | items.push item 50 | items 51 | 52 | Template.registerHelper 'admin_omit_fields', -> 53 | if typeof AdminConfig.autoForm != 'undefined' and typeof AdminConfig.autoForm.omitFields == 'object' 54 | global = AdminConfig.autoForm.omitFields 55 | if not Session.equals('admin_collection_name','Users') and typeof AdminConfig != 'undefined' and typeof AdminConfig.collections[Session.get 'admin_collection_name'].omitFields == 'object' 56 | collection = AdminConfig.collections[Session.get 'admin_collection_name'].omitFields 57 | if typeof global == 'object' and typeof collection == 'object' 58 | _.union global, collection 59 | else if typeof global == 'object' 60 | global 61 | else if typeof collection == 'object' 62 | collection 63 | 64 | Template.registerHelper 'AdminSchemas', -> 65 | AdminDashboard.schemas 66 | 67 | # Template.registerHelper 'adminIsUserInRole', (_id,role)-> 68 | # Roles.userIsInRole _id, role 69 | 70 | Template.registerHelper 'adminGetUsers', -> 71 | Meteor.users 72 | 73 | Template.registerHelper 'adminGetUserSchema', -> 74 | if _.has(AdminConfig, 'userSchema') 75 | schema = AdminConfig.userSchema 76 | else if typeof Meteor.users._c2 == 'object' 77 | schema = Meteor.users.simpleSchema() 78 | 79 | return schema 80 | 81 | Template.registerHelper 'adminCollectionLabel', (collection)-> 82 | AdminDashboard.collectionLabel(collection) if collection? 83 | 84 | Template.registerHelper 'adminCollectionCount', (collection)-> 85 | if collection == 'Users' 86 | Meteor.users.find().count() 87 | else 88 | AdminCollectionsCount.findOne({collection: collection})?.count 89 | 90 | Template.registerHelper 'adminTemplate', (collection, mode)-> 91 | if collection?.toLowerCase() != 'users' && typeof AdminConfig?.collections?[collection]?.templates != 'undefined' 92 | AdminConfig.collections[collection].templates[mode] 93 | 94 | Template.registerHelper 'adminGetCollection', (collection)-> 95 | _.find adminCollections(), (item) -> item.name == collection 96 | 97 | Template.registerHelper 'adminWidgets', -> 98 | if typeof AdminConfig.dashboard != 'undefined' and typeof AdminConfig.dashboard.widgets != 'undefined' 99 | AdminConfig.dashboard.widgets 100 | 101 | # Template.registerHelper 'adminUserEmail', (user) -> 102 | # if user && user.emails && user.emails[0] && user.emails[0].address 103 | # user.emails[0].address 104 | # else if user && user.services && user.services.facebook && user.services.facebook.email 105 | # user.services.facebook.email 106 | # else if user && user.services && user.services.google && user.services.google.email 107 | # user.services.google.email 108 | 109 | Template.registerHelper 'adminViewPath', (collection)-> 110 | FlowRouter.path "/admin/view/:coll",{coll: collection} 111 | 112 | Template.registerHelper 'adminNewPath', (collection)-> 113 | FlowRouter.path "/admin/new/:coll",{coll: collection} 114 | 115 | Template.registerHelper 'AdminDashboardPath', -> 116 | FlowRouter.path 'AdminDashboard' 117 | 118 | Template.registerHelper 'isSubReady', (sub) -> 119 | if sub 120 | FlowRouter.subsReady sub 121 | else 122 | FlowRouter.subsReady 123 | -------------------------------------------------------------------------------- /lib/client/js/slim_scroll.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2011 Piotr Rochala (http://rocha.la) 2 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 3 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 4 | * 5 | * Version: 1.3.0 6 | * 7 | */ 8 | (function(f){jQuery.fn.extend({slimScroll:function(h){var a=f.extend({width:"auto",height:"250px",size:"7px",color:"#000",position:"right",distance:"1px",start:"top",opacity:0.4,alwaysVisible:!1,disableFadeOut:!1,railVisible:!1,railColor:"#333",railOpacity:0.2,railDraggable:!0,railClass:"slimScrollRail",barClass:"slimScrollBar",wrapperClass:"slimScrollDiv",allowPageScroll:!1,wheelStep:20,touchScrollStep:200,borderRadius:"7px",railBorderRadius:"7px"},h);this.each(function(){function r(d){if(s){d=d|| 9 | window.event;var c=0;d.wheelDelta&&(c=-d.wheelDelta/120);d.detail&&(c=d.detail/3);f(d.target||d.srcTarget||d.srcElement).closest("."+a.wrapperClass).is(b.parent())&&m(c,!0);d.preventDefault&&!k&&d.preventDefault();k||(d.returnValue=!1)}}function m(d,f,h){k=!1;var e=d,g=b.outerHeight()-c.outerHeight();f&&(e=parseInt(c.css("top"))+d*parseInt(a.wheelStep)/100*c.outerHeight(),e=Math.min(Math.max(e,0),g),e=0=b.outerHeight()?k=!0:(c.stop(!0,!0).fadeIn("fast"),a.railVisible&&g.stop(!0,!0).fadeIn("fast"))}function p(){a.alwaysVisible||(A=setTimeout(function(){a.disableFadeOut&&s||(x||y)||(c.fadeOut("slow"),g.fadeOut("slow"))},1E3))}var s,x,y,A,z,u,l,B,D=30,k=!1,b=f(this);if(b.parent().hasClass(a.wrapperClass)){var n=b.scrollTop(), 12 | c=b.parent().find("."+a.barClass),g=b.parent().find("."+a.railClass);w();if(f.isPlainObject(h)){if("height"in h&&"auto"==h.height){b.parent().css("height","auto");b.css("height","auto");var q=b.parent().parent().height();b.parent().css("height",q);b.css("height",q)}if("scrollTo"in h)n=parseInt(a.scrollTo);else if("scrollBy"in h)n+=parseInt(a.scrollBy);else if("destroy"in h){c.remove();g.remove();b.unwrap();return}m(n,!1,!0)}}else{a.height="auto"==a.height?b.parent().height():a.height;n=f("
").addClass(a.wrapperClass).css({position:"relative", 13 | overflow:"hidden",width:a.width,height:a.height});b.css({overflow:"hidden",width:a.width,height:a.height});var g=f("
").addClass(a.railClass).css({width:a.size,height:"100%",position:"absolute",top:0,display:a.alwaysVisible&&a.railVisible?"block":"none","border-radius":a.railBorderRadius,background:a.railColor,opacity:a.railOpacity,zIndex:90}),c=f("
").addClass(a.barClass).css({background:a.color,width:a.size,position:"absolute",top:0,opacity:a.opacity,display:a.alwaysVisible? 14 | "block":"none","border-radius":a.borderRadius,BorderRadius:a.borderRadius,MozBorderRadius:a.borderRadius,WebkitBorderRadius:a.borderRadius,zIndex:99}),q="right"==a.position?{right:a.distance}:{left:a.distance};g.css(q);c.css(q);b.wrap(n);b.parent().append(c);b.parent().append(g);a.railDraggable&&c.bind("mousedown",function(a){var b=f(document);y=!0;t=parseFloat(c.css("top"));pageY=a.pageY;b.bind("mousemove.slimscroll",function(a){currTop=t+a.pageY-pageY;c.css("top",currTop);m(0,c.position().top,!1)}); 15 | b.bind("mouseup.slimscroll",function(a){y=!1;p();b.unbind(".slimscroll")});return!1}).bind("selectstart.slimscroll",function(a){a.stopPropagation();a.preventDefault();return!1});g.hover(function(){v()},function(){p()});c.hover(function(){x=!0},function(){x=!1});b.hover(function(){s=!0;v();p()},function(){s=!1;p()});b.bind("touchstart",function(a,b){a.originalEvent.touches.length&&(z=a.originalEvent.touches[0].pageY)});b.bind("touchmove",function(b){k||b.originalEvent.preventDefault();b.originalEvent.touches.length&& 16 | (m((z-b.originalEvent.touches[0].pageY)/a.touchScrollStep,!0),z=b.originalEvent.touches[0].pageY)});w();"bottom"===a.start?(c.css({top:b.outerHeight()-c.outerHeight()}),m(0,!0)):"top"!==a.start&&(m(f(a.start).position().top,null,!0),a.alwaysVisible||c.hide());C()}});return this}});jQuery.fn.extend({slimscroll:jQuery.fn.slimScroll})})(jQuery); -------------------------------------------------------------------------------- /lib/client/js/templates.coffee: -------------------------------------------------------------------------------- 1 | Template.AdminDashboardView.rendered = -> 2 | table = @$('.dataTable').DataTable(); 3 | 4 | Template.AdminDashboardView.helpers 5 | hasDocuments: -> 6 | AdminCollectionsCount.findOne({collection: Session.get 'admin_collection_name'})?.count > 0 7 | newPath: -> 8 | FlowRouter.path "/admin/new/:coll",{coll: Session.get 'admin_collection_name' } 9 | admin_table: -> 10 | AdminTables[Session.get 'admin_collection_name'] 11 | 12 | Template.adminUsersIsAdmin.helpers checkadmin: -> 13 | Roles.userIsInRole @_id, 'admin' 14 | 15 | Template.adminUsersMailBtn.helpers adminUserEmail: -> 16 | user = @ 17 | if user && user.emails && user.emails[0] && user.emails[0].address 18 | user.emails[0].address 19 | else if user && user.services && user.services.facebook && user.services.facebook.email 20 | user.services.facebook.email 21 | else if user && user.services && user.services.google && user.services.google.email 22 | user.services.google.email 23 | else 'null@null.null' 24 | 25 | Template.adminEditBtn.helpers path: -> 26 | FlowRouter.path '/admin/edit/:coll/:_id', 27 | coll: Session.get('admin_collection_name') 28 | _id: @_id 29 | 30 | Template.adminDeleteBtn.helpers path: -> 31 | FlowRouter.path '/admin/edit/:coll/:_id', { 32 | coll: Session.get('admin_collection_name') 33 | _id: @_id 34 | }, action: 'delete' 35 | 36 | Template.AdminHeader.helpers 37 | profilepath: -> FlowRouter.path '/admin/edit/:coll/:_id', 38 | coll: 'Users' 39 | _id: Meteor.userId() 40 | 41 | Template.AdminDashboardEdit.rendered = -> 42 | editcollectionName = FlowRouter.getParam 'collectionName' 43 | editId = FlowRouter.getParam '_id' 44 | Session.set 'admin_doc', adminCollectionObject(editcollectionName).findOne _id : editId 45 | 46 | Template.AdminDashboardEdit.helpers 47 | fadmin_doc: -> 48 | editcollectionName = FlowRouter.getParam 'collectionName' 49 | editId = FlowRouter.getParam '_id' 50 | adminCollectionObject(editcollectionName).findOne _id : editId if editcollectionName && editId 51 | action: -> FlowRouter.getQueryParam 'action' 52 | 53 | 54 | Template.AdminDashboardUsersEdit.rendered = -> 55 | editcollectionName = FlowRouter.getParam 'collectionName' 56 | editId = FlowRouter.getParam '_id' 57 | console.log 'here', adminCollectionObject(editcollectionName).findOne _id : editId 58 | Session.set 'admin_doc', adminCollectionObject(editcollectionName).findOne _id : editId 59 | 60 | Template.AdminDashboardUsersEdit.helpers 61 | user: -> Meteor.users.find(FlowRouter.getParam '_id').fetch() 62 | action: -> FlowRouter.getQueryParam 'action' 63 | roles: -> Roles.getRolesForUser(FlowRouter.getParam '_id') 64 | otherRoles: -> _.difference _.map(Meteor.roles.find().fetch(), (role) -> role.name), Roles.getRolesForUser(FlowRouter.getParam '_id') 65 | -------------------------------------------------------------------------------- /lib/server/methods.coffee: -------------------------------------------------------------------------------- 1 | Meteor.methods 2 | adminInsertDoc: (doc,collection)-> 3 | check arguments, [Match.Any] 4 | if Roles.userIsInRole this.userId, ['admin'] 5 | Future = Npm.require('fibers/future'); 6 | fut = new Future(); 7 | 8 | adminCollectionObject(collection).insert doc, (e,_id)-> 9 | fut['return']( {e:e,_id:_id} ) 10 | return fut.wait() 11 | 12 | adminUpdateDoc: (modifier,collection,_id)-> 13 | check arguments, [Match.Any] 14 | if Roles.userIsInRole this.userId, ['admin'] 15 | Future = Npm.require('fibers/future'); 16 | fut = new Future(); 17 | adminCollectionObject(collection).update {_id:_id},modifier,(e,r)-> 18 | fut['return']( {e:e,r:r} ) 19 | return fut.wait() 20 | 21 | adminRemoveDoc: (collection,_id)-> 22 | check arguments, [Match.Any] 23 | if Roles.userIsInRole this.userId, ['admin'] 24 | if collection == 'Users' 25 | Meteor.users.remove {_id:_id} 26 | else 27 | # global[collection].remove {_id:_id} 28 | adminCollectionObject(collection).remove {_id: _id} 29 | 30 | 31 | adminNewUser: (doc) -> 32 | check arguments, [Match.Any] 33 | if Roles.userIsInRole this.userId, ['admin'] 34 | emails = doc.email.split(',') 35 | _.each emails, (email)-> 36 | user = {} 37 | user.email = email 38 | unless doc.chooseOwnPassword 39 | user.password = doc.password 40 | 41 | _id = Accounts.createUser user 42 | 43 | if doc.sendPassword and AdminConfig.fromEmail? 44 | Email.send 45 | to: user.email 46 | from: AdminConfig.fromEmail 47 | subject: 'Your account has been created' 48 | html: 'You\'ve just had an account created for ' + Meteor.absoluteUrl() + ' with password ' + doc.password 49 | 50 | if not doc.sendPassword 51 | Accounts.sendEnrollmentEmail _id 52 | 53 | adminUpdateUser: (modifier,_id)-> 54 | check arguments, [Match.Any] 55 | if Roles.userIsInRole this.userId, ['admin'] 56 | Future = Npm.require('fibers/future'); 57 | fut = new Future(); 58 | Meteor.users.update {_id:_id},modifier,(e,r)-> 59 | fut['return']( {e:e,r:r} ) 60 | return fut.wait() 61 | 62 | adminSendResetPasswordEmail: (doc)-> 63 | check arguments, [Match.Any] 64 | if Roles.userIsInRole this.userId, ['admin'] 65 | console.log 'Changing password for user ' + doc._id 66 | Accounts.sendResetPasswordEmail(doc._id) 67 | 68 | adminChangePassword: (doc)-> 69 | check arguments, [Match.Any] 70 | if Roles.userIsInRole this.userId, ['admin'] 71 | console.log 'Changing password for user ' + doc._id 72 | Accounts.setPassword(doc._id, doc.password) 73 | label: 'Email user their new password' 74 | 75 | adminCheckAdmin: -> 76 | check arguments, [Match.Any] 77 | user = Meteor.users.findOne(_id:this.userId) 78 | if this.userId and !Roles.userIsInRole(this.userId, ['admin']) and (user.emails.length > 0) 79 | email = user.emails[0].address 80 | if typeof Meteor.settings.adminEmails != 'undefined' 81 | adminEmails = Meteor.settings.adminEmails 82 | if adminEmails.indexOf(email) > -1 83 | console.log 'Adding admin user: ' + email 84 | Roles.addUsersToRoles this.userId, ['admin'], Roles.GLOBAL_GROUP 85 | else if typeof AdminConfig != 'undefined' and typeof AdminConfig.adminEmails == 'object' 86 | adminEmails = AdminConfig.adminEmails 87 | if adminEmails.indexOf(email) > -1 88 | console.log 'Adding admin user: ' + email 89 | Roles.addUsersToRoles this.userId, ['admin'], Roles.GLOBAL_GROUP 90 | else if this.userId == Meteor.users.findOne({},{sort:{createdAt:1}})._id 91 | console.log 'Making first user admin: ' + email 92 | Roles.addUsersToRoles this.userId, ['admin'] 93 | 94 | adminAddUserToRole: (_id,role)-> 95 | check arguments, [Match.Any] 96 | if Roles.userIsInRole this.userId, ['admin'] 97 | Roles.addUsersToRoles _id, role, Roles.GLOBAL_GROUP 98 | 99 | adminRemoveUserToRole: (_id,role)-> 100 | check arguments, [Match.Any] 101 | if Roles.userIsInRole this.userId, ['admin'] 102 | Roles.removeUsersFromRoles _id, role, Roles.GLOBAL_GROUP 103 | 104 | adminSetCollectionSort: (collection, _sort) -> 105 | check arguments, [Match.Any] 106 | global.AdminPages[collection].set 107 | sort: _sort 108 | -------------------------------------------------------------------------------- /lib/server/publish.coffee: -------------------------------------------------------------------------------- 1 | Meteor.publishComposite 'adminCollectionDoc', (collection, id) -> 2 | check collection, String 3 | check id, Match.OneOf(String, Mongo.ObjectID) 4 | if Roles.userIsInRole this.userId, ['admin'] 5 | find: -> 6 | adminCollectionObject(collection).find(id) 7 | children: AdminConfig?.collections?[collection]?.children or [] 8 | else 9 | @ready() 10 | 11 | Meteor.publish 'adminUsers', -> 12 | if Roles.userIsInRole @userId, ['admin'] 13 | Meteor.users.find() 14 | else 15 | @ready() 16 | 17 | Meteor.publish 'adminUser', -> 18 | Meteor.users.find @userId 19 | 20 | Meteor.publish 'adminCollectionsCount', -> 21 | handles = [] 22 | self = @ 23 | 24 | _.each AdminTables, (table, name) -> 25 | id = new Mongo.ObjectID 26 | count = 0 27 | 28 | ready = false 29 | handles.push table.collection.find().observeChanges 30 | added: -> 31 | count += 1 32 | ready and self.changed 'adminCollectionsCount', id, {count: count} 33 | removed: -> 34 | count -= 1 35 | ready and self.changed 'adminCollectionsCount', id, {count: count} 36 | ready = true 37 | 38 | self.added 'adminCollectionsCount', id, {collection: name, count: count} 39 | 40 | self.onStop -> 41 | _.each handles, (handle) -> handle.stop() 42 | self.ready() 43 | 44 | Meteor.publish null, -> 45 | Meteor.roles.find({}) 46 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'sach:flow-db-admin', 3 | version: '1.2.0', 4 | // Brief, one-line summary of the package. 5 | summary: 'Meteor Database Admin package for use with Flow Router', 6 | // URL to the Git repository containing the source code for this package. 7 | git: 'https://github.com/sachinbhutani/flow-db-admin', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | 15 | both = ['client','server'] 16 | 17 | api.use( 18 | [ 19 | 'coffeescript', 20 | 'underscore', 21 | 'reactive-var', 22 | 'meteorhacks:unblock@1.1.0', 23 | 'kadira:flow-router@2.12.1', 24 | 'kadira:blaze-layout@2.3.0', 25 | 'zimme:active-route@2.3.2', 26 | 'reywood:publish-composite@1.5.2', 27 | 'aldeed:collection2-core@2.0.1', 28 | 'aldeed:autoform@6.2.0', 29 | 'aldeed:template-extension@4.1.0', 30 | 'alanning:roles@1.2.14', 31 | 'raix:handlebar-helpers@0.2.5', 32 | 'momentjs:moment@2.18.1', 33 | 'aldeed:tabular@2.1.1', 34 | 'mfactory:admin-lte@0.0.2', 35 | 'tmeasday:check-npm-versions@0.3.1', 36 | 'check', 37 | 'ecmascript' 38 | ], 39 | both); 40 | 41 | api.use(['less@1.0.0 || 2.5.0','session','jquery','templating'],'client') 42 | 43 | api.use(['email'],'server') 44 | 45 | api.add_files([ 46 | 'lib/both/AdminDashboard.coffee', 47 | 'lib/both/routes.js', 48 | 'lib/both/utils.coffee', 49 | 'lib/both/startup.coffee', 50 | 'lib/both/collections.coffee' 51 | ], both); 52 | 53 | api.add_files([ 54 | 'lib/client/html/admin_templates.html', 55 | 'lib/client/html/admin_widgets.html', 56 | 'lib/client/html/fadmin_layouts.html', 57 | 'lib/client/html/admin_sidebar.html', 58 | 'lib/client/html/admin_header.html', 59 | 'lib/client/js/admin_layout.js', 60 | 'lib/client/js/helpers.coffee', 61 | 'lib/client/js/templates.coffee', 62 | 'lib/client/js/events.coffee', 63 | 'lib/client/js/slim_scroll.js', 64 | 'lib/client/js/autoForm.coffee', 65 | 'lib/client/css/admin-custom.less' 66 | ], 'client'); 67 | 68 | api.add_files([ 69 | 'lib/server/publish.coffee', 70 | 'lib/server/methods.coffee' 71 | ], 'server'); 72 | 73 | //api.addAssets(['lib/client/css/admin-custom.css'],'client'); 74 | api.export('AdminDashboard',both) 75 | 76 | }); 77 | 78 | Package.onTest(function(api) { 79 | api.use('tinytest'); 80 | api.use('sach:flow-db-admin'); 81 | api.addFiles('flow-db-admin-tests.js'); 82 | }); 83 | --------------------------------------------------------------------------------