Fix merge conflict.

pull/901/head
Lauri Ojansivu 8 years ago
commit 7c9a30d8fe
  1. 4
      .eslintrc.json
  2. 2
      client/components/main/layouts.js
  3. 5
      client/components/settings/invitationCode.jade
  4. 6
      client/components/settings/invitationCode.js
  5. 71
      client/components/settings/settingBody.jade
  6. 128
      client/components/settings/settingBody.js
  7. 112
      client/components/settings/settingBody.styl
  8. 21
      client/components/settings/settingHeader.jade
  9. 25
      client/components/settings/settingHeader.styl
  10. 2
      client/components/users/userHeader.jade
  11. 3
      client/components/users/userHeader.js
  12. 15
      config/accounts.js
  13. 20
      config/router.js
  14. 25
      i18n/en.i18n.json
  15. 45
      models/invitationCodes.js
  16. 116
      models/settings.js
  17. 45
      models/users.js
  18. 13
      server/publications/settings.js
  19. 8
      server/publications/users.js

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

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

@ -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'}}")

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

@ -0,0 +1,71 @@
template(name="setting")
.setting-content
.content-title
span {{_ 'settings'}}
.content-body
.side-menu
ul
li.active
a.js-setting-menu(data-id="registration-setting") {{_ 'registration'}}
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#registration-setting.setting-detail
li
a.flex.js-toggle-registration
.materialCheckBox(class="{{#if currentSetting.disableRegistration}}is-checked{{/if}}")
span {{_ 'disable-self-registration'}}
li
.invite-people(class="{{#if currentSetting.disableRegistration}}{{else}}hide{{/if}}")
ul
li
.title {{_ 'invite-people'}}
textarea#email-to-invite.form-control(rows='5', placeholder="{{_ 'email-addresses'}}")
li
.title {{_ 'to-boards'}}
.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'}}
.description {{_ 'smtp-host-description'}}
.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 {{_ 'smtp-port-description'}}
.form-group
input.form-control#mail-server-port(type="text", placeholder="25" value="{{currentSetting.mailServer.port}}")
li.smtp-form
.title {{_ 'smtp-username'}}
.form-group
input.form-control#mail-server-username(type="text", placeholder="{{_ 'username'}}" 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 {{_ 'send-from'}}
.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

@ -0,0 +1,128 @@
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'],
});
},
toggleRegistration(){
this.setLoading(true);
const registrationClosed = this.currentSetting().disableRegistration;
Settings.update(Settings.findOne()._id, {$set:{disableRegistration: !registrationClosed}});
this.setLoading(false);
if(registrationClosed){
$('.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('registration-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(){
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());
}
});
if (validEmails.length) {
this.setLoading(true);
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-registration': this.toggleRegistration,
'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');

@ -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);

@ -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 {{_ 'settings'}}
//TODO
// a.setting-header-btn.people
// i.fa(class="fa-users")
// span {{_ 'people'}}
else
a.setting-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
span {{_ 'log-in'}}

@ -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

@ -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'}}

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

@ -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) {
};
});
}

@ -99,6 +99,26 @@ FlowRouter.route('/import', {
},
});
FlowRouter.route('/setting', {
name: 'setting',
triggersEnter: [
AccountsTemplates.ensureSignedIn,
() => {
Session.set('currentBoard', null);
Session.set('currentCard', null);
Filter.reset();
EscapeActions.executeAll();
},
],
action() {
BlazeLayout.render('defaultLayout', {
headerBar: 'settingHeaderBar',
content: 'setting',
});
},
});
FlowRouter.notFound = {
action() {
BlazeLayout.render('defaultLayout', { content: 'notFound' });

@ -323,5 +323,26 @@
"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",
"settings": "Settings",
"people": "People",
"registration": "Registration",
"disable-self-registration": "Disable Self-Registration",
"invite": "Invite",
"invite-people": "Invite People",
"to-boards": "To board(s)",
"email-addresses":"Email Addresses",
"smtp-host-description": "The address of the SMTP server that handles your emails.",
"smtp-port-description": "The port your SMTP server uses for outgoing emails.",
"smtp-host": "SMTP Host",
"smtp-port": "SMTP Port",
"smtp-username": "Username",
"smtp-password": "Password",
"send-from": "From",
"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"
}

@ -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'],
});
}

@ -0,0 +1,116 @@
Settings = new Mongo.Collection('settings');
Settings.attachSchema(new SimpleSchema({
disableRegistration: {
type: Boolean,
},
'mailServer.username': {
type: String,
optional: true,
},
'mailServer.password': {
type: String,
optional: true,
},
'mailServer.host': {
type: String,
optional: true,
},
'mailServer.port': {
type: String,
optional: true,
},
'mailServer.from': {
type: String,
optional: true,
defaultValue: 'Wekan',
},
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 = {disableRegistration: 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);
});
}
});
},
});
}

@ -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().disableRegistration) throw new Meteor.Error('error-user-notCreated');
// Set in lowercase email before creating account
const email = username.toLowerCase();
username = email.substring(0, posAt);
@ -390,6 +390,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 disableRegistration = Settings.findOne().disableRegistration;
if (!disableRegistration) {
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) {
@ -459,4 +481,25 @@ if (Meteor.isServer) {
});
});
}
Users.after.insert((userId, doc) => {
//invite user to corresponding boards
const disableRegistration = Settings.findOne().disableRegistration;
if (disableRegistration) {
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}});
}
}
});
}

@ -0,0 +1,13 @@
Meteor.publish('setting', () => {
return Settings.find({}, {fields:{disableRegistration: 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 [];
});

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

Loading…
Cancel
Save