Admin panel:

Only invited user can register in strict mode,
Set mail server in admin panel,
Switch strict mode in admin panel,
Invite people to system in admin panel
This commit is contained in:
lkisme 2017-02-24 22:10:38 +08:00
parent 29fdfb9c88
commit 1dfb6ef477
20 changed files with 638 additions and 10 deletions

View file

@ -119,6 +119,8 @@
"allowIsBoardMember": true,
"allowIsBoardMemberByCard": true,
"Emoji": true,
"Checklists": true
"Checklists": true,
"Settings": true,
"InvitationCodes": true
}
}

View file

@ -1,4 +1,6 @@
Meteor.subscribe('boards');
Meteor.subscribe('setting');
Meteor.subscribe('user-admin');
BlazeLayout.setRoot('body');

View file

@ -0,0 +1,5 @@
template(name='invitationCode')
.at-input#invitationcode
label(for='at-field-code') {{_ 'invitation-code'}}
input#at-field-invitationcode(type="text" name='at-field-invitationcode' placeholder="{{_ 'invitation-code'}}")

View file

@ -0,0 +1,6 @@
Template.invitationCode.onRendered(() => {
const strict = Settings.findOne().strict;
if(!strict){
$('#invitationcode').hide();
}
});

View file

@ -0,0 +1,72 @@
template(name="setting")
.setting-content
.content-title
span Settings
.content-body
.side-menu
ul
li.active
a.js-setting-menu(data-id="general-setting") System
li
a.js-setting-menu(data-id="email-setting") Email
.main-body
if loading.get
+spinner
else if generalSetting.get
+general
else if emailSetting.get
+email
template(name="general")
ul#general-setting.setting-detail
li
a.flex.js-toggle-strict-mode
.materialCheckBox(class="{{#if currentSetting.strict}}is-checked{{/if}}")
span Use Strict Mode
li
.invite-people(class="{{#if currentSetting.strict}}{{else}}hide{{/if}}")
ul
li
.title Invite People
textarea#email-to-invite.form-control(rows='5', placeholder="Email Adresses")
li
.title To board(s)
.bg-white
each boards
a.option.flex.js-toggle-board-choose(id= _id)
.materialCheckBox(data-id= _id)
span= title
li
button.js-email-invite.primary Invite
template(name='email')
ul#email-setting.setting-detail
li.smtp-form
.title SMTP Host {{currentSetting.mailServer.port}}
.description The address of the SMTP server that handles your emails.
.form-group
input.form-control#mail-server-host(type="text", placeholder="smtp.domain.com" value="{{currentSetting.mailServer.host}}")
li.smtp-form
.title SMTP Port
.description The port your SMTP server uses for outgoing emails.
.form-group
input.form-control#mail-server-port(type="text", placeholder="25" value="{{currentSetting.mailServer.port}}")
li.smtp-form
.title SMTP user name
.form-group
input.form-control#mail-server-username(type="text", placeholder="user name" value="{{currentSetting.mailServer.username}}")
li.smtp-form
.title SMTP password
.form-group
input.form-control#mail-server-password(type="text", placeholder="password" value="{{currentSetting.mailServer.password}}")
li.smtp-form
.title From
.Email address you want to use to send emails.
.form-group
input.form-control#mail-server-from(type="email", placeholder="no-reply@domain.com" value="{{currentSetting.mailServer.from}}")
li
button.js-save.primary Save

View file

@ -0,0 +1,126 @@
Meteor.subscribe('setting');
Meteor.subscribe('mailServer');
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
this.generalSetting = new ReactiveVar(true);
this.emailSetting = new ReactiveVar(false);
},
setError(error) {
this.error.set(error);
},
setLoading(w) {
this.loading.set(w);
},
checkField(selector) {
const value = $(selector).val();
if(!value || value.trim() === ''){
$(selector).parents('li.smtp-form').addClass('has-error');
throw Error('blank field');
} else {
return value;
}
},
currentSetting(){
return Settings.findOne();
},
boards() {
return Boards.find({
archived: false,
'members.userId': Meteor.userId(),
'members.isAdmin': true,
}, {
sort: ['title'],
});
},
toggleStrictMode(){
this.setLoading(true);
const isStrictMode = this.currentSetting().strict;
Settings.update(Settings.findOne()._id, {$set:{strict: !isStrictMode}});
this.setLoading(false);
if(isStrictMode){
$('.invite-people').slideUp();
}else{
$('.invite-people').slideDown();
}
},
switchMenu(event){
const target = $(event.target);
if(!target.hasClass('active')){
$('.side-menu li.active').removeClass('active');
target.parent().addClass('active');
const targetID = target.data('id');
this.generalSetting.set('general-setting' === targetID);
this.emailSetting.set('email-setting' === targetID);
}
},
checkBoard(event){
let target = $(event.target);
if(!target.hasClass('js-toggle-board-choose')){
target = target.parent();
}
const checkboxId = target.attr('id');
$(`#${checkboxId} .materialCheckBox`).toggleClass('is-checked');
$(`#${checkboxId}`).toggleClass('is-checked');
},
inviteThroughEmail(){
this.setLoading(true);
const emails = $('#email-to-invite').val().trim().split('\n').join(',').split(',');
const boardsToInvite = [];
$('.js-toggle-board-choose .materialCheckBox.is-checked').each(function () {
boardsToInvite.push($(this).data('id'));
});
const validEmails = [];
emails.forEach((email) => {
if (email && SimpleSchema.RegEx.Email.test(email.trim())) {
validEmails.push(email.trim());
}
});
Meteor.call('sendInvitation', validEmails, boardsToInvite, () => {
// if (!err) {
// TODO - show more info to user
// }
this.setLoading(false);
});
},
saveMailServerInfo(){
this.setLoading(true);
$('li').removeClass('has-error');
try{
const host = this.checkField('#mail-server-host');
const port = this.checkField('#mail-server-port');
const username = this.checkField('#mail-server-username');
const password = this.checkField('#mail-server-password');
const from = this.checkField('#mail-server-from');
Settings.update(Settings.findOne()._id, {$set:{'mailServer.host':host, 'mailServer.port': port, 'mailServer.username': username,
'mailServer.password': password, 'mailServer.from': from}});
} catch (e) {
return;
} finally {
this.setLoading(false);
}
},
events(){
return [{
'click a.js-toggle-strict-mode': this.toggleStrictMode,
'click a.js-setting-menu': this.switchMenu,
'click a.js-toggle-board-choose': this.checkBoard,
'click button.js-email-invite': this.inviteThroughEmail,
'click button.js-save': this.saveMailServerInfo,
}];
},
}).register('setting');

View file

@ -0,0 +1,112 @@
.flex
display: -webkit-box
display: -moz-box
display: -webkit-flex
display: -moz-flex
display: -ms-flexbox
display: flex
.setting-content
padding 30px
color: #727479
background: #dedede
width 100%
height 100%
position: absolute;
.content-title
font-size 20px
.content-body
display flex
padding-top 15px
height 100%
.side-menu
background-color: #f7f7f7;
border: 1px solid #f0f0f0;
border-radius: 4px;
width: 250px;
box-shadow: inset -1px -1px 3px rgba(0,0,0,.05);
ul
li
margin: 0.1rem 0.2rem;
&.active
background #fff
box-shadow 0 1px 2px rgba(0,0,0,0.15);
&:hover
background #fff
box-shadow 0 1px 2px rgba(0,0,0,0.15);
a
@extends .flex
padding: 1rem 0 1rem 1rem
width: 100% - 5rem
span
font-size: 13px
.main-body
padding: 0.1em 1em
ul
li
padding: 0.5rem 0.5rem;
a
.is-checked
border-bottom: 2px solid #2980b9;
border-right: 2px solid #2980b9;
span
padding: 0 0.5rem
.invite-people
padding-left 20px;
li
min-width: 500px;
ul.no-margin-bottom
margin-bottom: 0;
.bg-white
a
background #f7f7f7
&.is-checked
background #fff
.option
@extends .flex
-webkit-border-radius: 3px;
border-radius: 3px;
background: #fff;
text-decoration: none;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.2);
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
margin-top: 5px;
padding: 5px;
.title
font-weight 700;
margin-bottom 0.5rem;
.description
margin-bottom 0.5rem;
.bg-white
background #f9fbfc;
.form-control.has-error
border-color: #a94442;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
li.has-error
color #a94442
.form-group
.form-control
border-color: #a94442;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);

View file

@ -0,0 +1,21 @@
template(name="settingHeaderBar")
h1.header-setting-menu
span {{_ 'admin-panel'}}
.setting-header-btns.left
unless isMiniScreen
unless isSandstorm
if currentUser
a.setting-header-btn.settings.active
i.fa(class="fa-cog")
span {{_ 'option-setting'}}
//TODO
// a.setting-header-btn.people
// i.fa(class="fa-users")
// span {{_ 'option-people'}}
else
a.setting-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
span {{_ 'log-in'}}

View file

@ -0,0 +1,25 @@
#header #header-main-bar .setting-header-btn
&.active,
&:hover:not(.is-disabled)
background: rgba(0, 0, 0, .15)
color: darken(white, 5%)
margin-left: 20px;
padding-right: 10px;
height: 28px;
font-size: 13px;
float: left;
overflow: hidden;
line-height: @height;
margin: 0 2px;
i.fa
float: left
display: block
line-height: 28px
color: darken(white, 5%)
margin: 0 10px
+ span
display: inline-block
margin-top: 1px
margin-right: 10px

View file

@ -17,6 +17,8 @@ template(name="memberMenuPopup")
li: a.js-change-password {{_ 'changePasswordPopup-title'}}
li: a.js-change-language {{_ 'changeLanguagePopup-title'}}
li: a.js-edit-notification {{_ 'editNotificationPopup-title'}}
if currentUser.isAdmin
li: a.js-go-setting(href='/setting') {{_ 'admin-panel'}}
hr
ul.pop-over-list
li: a.js-logout {{_ 'log-out'}}

View file

@ -15,6 +15,9 @@ Template.memberMenuPopup.events({
AccountsTemplates.logout();
},
'click .js-go-setting'() {
Popup.close();
},
});
Template.editProfilePopup.events({

View file

@ -1,12 +1,21 @@
const passwordField = AccountsTemplates.removeField('password');
const emailField = AccountsTemplates.removeField('email');
AccountsTemplates.addFields([{
_id: 'username',
type: 'text',
displayName: 'username',
required: true,
minLength: 2,
}, emailField, passwordField]);
}, emailField, passwordField, {
_id: 'invitationcode',
type: 'text',
displayName: 'Invitation Code',
required: false,
minLength: 6,
errStr: 'Invitation code doesn\'t exist',
template: 'invitationCode',
}]);
AccountsTemplates.configure({
defaultLayout: 'userFormsLayout',
@ -48,9 +57,6 @@ AccountsTemplates.configureRoute('changePwd', {
});
if (Meteor.isServer) {
if (process.env.MAIL_FROM) {
Accounts.emailTemplates.from = process.env.MAIL_FROM;
}
['resetPassword-subject', 'resetPassword-text', 'verifyEmail-subject', 'verifyEmail-text', 'enrollAccount-subject', 'enrollAccount-text'].forEach((str) => {
const [templateName, field] = str.split('-');
@ -63,3 +69,4 @@ if (Meteor.isServer) {
};
});
}

View file

@ -99,6 +99,16 @@ FlowRouter.route('/import', {
},
});
FlowRouter.route('/setting', {
name: 'setting',
action() {
BlazeLayout.render('defaultLayout', {
headerBar: 'settingHeaderBar',
content: 'setting',
});
},
});
FlowRouter.notFound = {
action() {
BlazeLayout.render('defaultLayout', { content: 'notFound' });

View file

@ -323,5 +323,13 @@
"welcome-board": "Welcome Board",
"welcome-list1": "Basics",
"welcome-list2": "Advanced",
"what-to-do": "What do you want to do?"
}
"what-to-do": "What do you want to do?",
"admin-panel": "Admin Panel",
"system-setting": "System Setting",
"option-setting": "Settings",
"option-people": "People",
"invitation-code": "Invitation Code",
"email-invite-register-subject": "__inviter__ sent you an invitation",
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to Wekan for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.\n",
"error-invitation-code-not-exist": "Invitation code doesn't exist"
}

View file

@ -322,5 +322,12 @@
"welcome-board": "“欢迎”看板",
"welcome-list1": "基本",
"welcome-list2": "高阶",
"what-to-do": "要做什么?"
}
"what-to-do": "要做什么?",
"system-setting": "系统设置",
"option-setting": "设置",
"option-people": "成员",
"invitation-code": "邀请码",
"email-invite-register-subject": "__inviter__ 向您发出邀请",
"email-invite-register-text": "尊敬的 __user__,\n\n__inviter__ 邀请您加入看板参与协作。\n\n请点击下面的链接访问进行注册\n\n__url__\n您的邀请码是 __icode__\n\n谢谢。\n",
"error-invitation-code-not-exist": "验证码不存在"
}

45
models/invitationCodes.js Normal file
View file

@ -0,0 +1,45 @@
InvitationCodes = new Mongo.Collection('invitation_codes');
InvitationCodes.attachSchema(new SimpleSchema({
code: {
type: String,
},
email: {
type: String,
unique: true,
regEx: SimpleSchema.RegEx.Email,
},
createdAt: {
type: Date,
denyUpdate: false,
},
// always be the admin if only one admin
authorId: {
type: String,
},
boardsToBeInvited: {
type: [String],
optional: true,
},
valid: {
type: Boolean,
defaultValue: true,
},
}));
InvitationCodes.helpers({
author(){
return Users.findOne(this.authorId);
},
});
// InvitationCodes.before.insert((userId, doc) => {
// doc.createdAt = new Date();
// doc.authorId = userId;
// });
if (Meteor.isServer) {
Boards.deny({
fetch: ['members'],
});
}

111
models/settings.js Normal file
View file

@ -0,0 +1,111 @@
Settings = new Mongo.Collection('settings');
Settings.attachSchema(new SimpleSchema({
strict: {
type: Boolean,
},
'mailServer.username': {
type: String,
},
'mailServer.password': {
type: String,
},
'mailServer.host': {
type: String,
},
'mailServer.port': {
type: String,
},
'mailServer.from': {
type: String,
defaultValue: 'Kanban',
},
createdAt: {
type: Date,
denyUpdate: true,
},
modifiedAt: {
type: Date,
},
}));
Settings.helpers({
mailUrl () {
const mailUrl = `smtp://${this.mailServer.username}:${this.mailServer.password}@${this.mailServer.host}:${this.mailServer.port}/`;
return mailUrl;
},
});
Settings.allow({
update(userId) {
const user = Users.findOne(userId);
return user && user.isAdmin;
},
});
Settings.before.update((userId, doc, fieldNames, modifier) => {
modifier.$set = modifier.$set || {};
modifier.$set.modifiedAt = new Date();
});
if (Meteor.isServer) {
Meteor.startup(() => {
const setting = Settings.findOne({});
if(!setting){
const now = new Date();
const defaultSetting = {strict: false, mailServer: {
username: '', password:'', host: '', port:'', from: '',
}, createdAt: now, modifiedAt: now};
Settings.insert(defaultSetting);
}
const newSetting = Settings.findOne();
process.env.MAIL_URL = newSetting.mailUrl();
Accounts.emailTemplates.from = newSetting.mailServer.from;
});
function getRandomNum (min, max) {
const range = max - min;
const rand = Math.random();
return (min + Math.round(rand * range));
}
function sendInvitationEmail (_id){
const icode = InvitationCodes.findOne(_id);
const author = Users.findOne(Meteor.userId());
try {
const params = {
email: icode.email,
inviter: Users.findOne(icode.authorId).username,
user: icode.email.split('@')[0],
icode: icode.code,
url: FlowRouter.url('sign-up'),
};
const lang = author.getLanguage();
Email.send({
to: icode.email,
from: Accounts.emailTemplates.from,
subject: TAPi18n.__('email-invite-register-subject', params, lang),
text: TAPi18n.__('email-invite-register-text', params, lang),
});
} catch (e) {
throw new Meteor.Error('email-fail', e.message);
}
}
Meteor.methods({
sendInvitation(emails, boards) {
check(emails, [String]);
check(boards, [String]);
const user = Users.findOne(Meteor.userId());
if(!user.isAdmin){
throw new Meteor.Error('not-allowed');
}
emails.forEach((email) => {
if (email && SimpleSchema.RegEx.Email.test(email)) {
const code = getRandomNum(100000, 999999);
InvitationCodes.insert({code, email, boardsToBeInvited: boards, createdAt: new Date(), authorId: Meteor.userId()}, function(err, _id){
if(!err && _id) sendInvitationEmail(_id);
});
}
});
},
});
}

View file

@ -348,7 +348,7 @@ if (Meteor.isServer) {
if (user._id === inviter._id) throw new Meteor.Error('error-user-notAllowSelf');
} else {
if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
if (Settings.findOne().strict) throw new Meteor.Error('error-user-notCreated');
const email = username;
username = email.substring(0, posAt);
const newUserId = Accounts.createUser({ username, email });
@ -389,6 +389,28 @@ if (Meteor.isServer) {
return { username: user.username, email: user.emails[0].address };
},
});
Accounts.onCreateUser((options, user) => {
const userCount = Users.find().count();
if (userCount === 0){
user.isAdmin = true;
return user;
}
const strict = Settings.findOne().strict;
if (!strict) {
return user;
}
const iCode = options.profile.invitationcode | '';
const invitationCode = InvitationCodes.findOne({code: iCode, valid:true});
if (!invitationCode) {
throw new Meteor.Error('error-invitation-code-not-exist');
}else{
user.profile = {icode: options.profile.invitationcode};
}
return user;
});
}
if (Meteor.isServer) {
@ -458,4 +480,25 @@ if (Meteor.isServer) {
});
});
}
Users.after.insert((userId, doc) => {
//invite user to corresponding boards
const strict = Settings.findOne().strict;
if (strict) {
const user = Users.findOne(doc._id);
const invitationCode = InvitationCodes.findOne({code: user.profile.icode, valid:true});
if (!invitationCode) {
throw new Meteor.Error('error-user-notCreated');
}else{
invitationCode.boardsToBeInvited.forEach((boardId) => {
const board = Boards.findOne(boardId);
board.addMember(doc._id);
});
user.profile = {invitedBoards: invitationCode.boardsToBeInvited};
InvitationCodes.update(invitationCode._id, {$set: {valid:false}});
}
}
});
}

View file

@ -0,0 +1,13 @@
Meteor.publish('setting', () => {
return Settings.find({}, {fields:{strict: 1}});
});
Meteor.publish('mailServer', function () {
if (!Match.test(this.userId, String))
return [];
const user = Users.findOne(this.userId);
if(user && user.isAdmin){
return Settings.find({}, {fields: {mailServer: 1}});
}
return [];
});

View file

@ -9,3 +9,11 @@ Meteor.publish('user-miniprofile', function(userId) {
},
});
});
Meteor.publish('user-admin', function() {
return Meteor.users.find(this.userId, {
fields: {
isAdmin: 1,
},
});
});