free-be-account 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // 邮箱注册,验证
4
4
 
5
5
  // nodemailer send email
6
+ var svgCaptcha = require('svg-captcha');
6
7
  const passport = require('passport');
7
8
  const LocalStrategy = require('passport-local').Strategy;
8
9
  const {v1: uuidv1} = require('uuid');
@@ -10,6 +11,7 @@ const crypto = require("./crypto");
10
11
  const { clearPermission, getPermissionPathList, verifyPassword, encryptPwd } = require('./utils');
11
12
  const { AccountAuditStatus } = require('./enum');
12
13
  const sms = require('./sms');
14
+ const wx = require('./platforms/wx/index');
13
15
 
14
16
  let __app_service_list_saved = false;
15
17
  let __saved_service_list;
@@ -19,7 +21,7 @@ const __getServiceList = async (res, filter = { Enabled: true }) => {
19
21
  if (!__app_service_list_saved) {
20
22
  await res.app.modules.account.utils.saveServiceList(res.app);
21
23
  __app_service_list_saved = true;
22
- } else {
24
+ } else if (!filter){
23
25
  return __saved_service_list;
24
26
  }
25
27
 
@@ -44,7 +46,10 @@ const __getServiceList = async (res, filter = { Enabled: true }) => {
44
46
  Title: doc.Title,
45
47
  Description: doc.Description,
46
48
  Index: doc.Index,
47
- Scope: doc.Scope.map(sc => {
49
+ },
50
+ // TODO: only add data scope when no filter provided, correct?
51
+ filter ? {} : {
52
+ Scope: filter ? undefined: doc.Scope.map(sc => {
48
53
  const dso = res.app.getContainerContent('DataScope').find(ds => ds.Name === sc.Name);
49
54
  return {
50
55
  Label: dso ? dso.Label : '',
@@ -52,14 +57,16 @@ const __getServiceList = async (res, filter = { Enabled: true }) => {
52
57
  Type: 'Select',
53
58
  Options: dso ? dso.Options : []
54
59
  }
55
- }),
60
+ })
56
61
  })
57
62
  }
58
63
  }
59
64
  })
60
65
  }
61
66
 
62
- __saved_service_list = permList;
67
+ if (!filter) {
68
+ __saved_service_list = permList;
69
+ }
63
70
 
64
71
  return permList;
65
72
  }
@@ -76,6 +83,23 @@ const verify_api_permission = async (app, mdl, user, api_path) => {
76
83
  const service_list = await __getServiceList({ app });
77
84
  if (!service_list || Object.keys(service_list).length <= 0) return false;
78
85
 
86
+ // give any other modules a chance to control user permission.
87
+ const cachedPerm = await app.cache.get(`perm_ctrl_${user.id}`);
88
+ if(cachedPerm) {
89
+ user.Permission = cachedPerm;
90
+ } else {
91
+ const permControlList = app.getContainerContent('PermissionControl');
92
+ for (let i = 0; i < permControlList.length; i += 1) {
93
+ const pc = permControlList[i];
94
+
95
+ if(typeof pc === 'function') {
96
+ await pc(user);
97
+ }
98
+ }
99
+
100
+ app.cache.put(`perm_ctrl_${user.id}`, user.Permission);
101
+ }
102
+
79
103
  const user_permission = user ? user.Permission : {};
80
104
 
81
105
  if (user_permission === '*') return true;
@@ -128,8 +152,8 @@ const verify_api_permission = async (app, mdl, user, api_path) => {
128
152
  return true; // TODO: secure enough??
129
153
  }
130
154
 
131
- module.exports = {
132
- sms,
155
+ module.exports = (app) => ({
156
+ sms: sms(app),
133
157
  AccountAuditStatus,
134
158
  config: {
135
159
  routeRoot: 'account',
@@ -160,6 +184,20 @@ module.exports = {
160
184
  pwdEncryptMethod: ['sha1', 'bcrypt', 'md5'],
161
185
  desKey: 'eis,is,s,2020',
162
186
 
187
+ // force to reset password period (days). 0 to disable
188
+ forceResetPwd: 0,
189
+
190
+ dataScopes: [],
191
+ permissionControls: [],
192
+ captcha: {
193
+ cache: 5 * 60 * 1000,
194
+ login: true,
195
+ register: true,
196
+ recover: true,
197
+ ignoreCase: false,
198
+ options: {},
199
+ },
200
+
163
201
  permFields: [
164
202
  {
165
203
  Type: 'Category',
@@ -277,6 +315,8 @@ module.exports = {
277
315
 
278
316
  Enabled: { type: 'Boolean', default: true },
279
317
 
318
+ PwdUpdatedAt: { type: 'Date' },
319
+
280
320
  Permission: { type: 'Object', default: {} },
281
321
 
282
322
  // Audit status
@@ -302,7 +342,7 @@ module.exports = {
302
342
  Name: { type: 'String' },
303
343
  Title: { type: 'String' },
304
344
  Description: { type: 'String' },
305
- Path: { type: 'String' },
345
+ Path: { type: 'String', unique: true },
306
346
  Index: { type: 'Number', required: true },
307
347
  Enabled: { type: 'Boolean', required: true, default: true },
308
348
  BuiltIn: { type: "Boolean", required: true, default: true },
@@ -345,16 +385,13 @@ module.exports = {
345
385
  // md5(JSON.stringify({Timestamp:xxx, UserId: xxx, UserSecret:xxx }))
346
386
  let sign = req.body.Sign || req.header('Sign');
347
387
 
348
- // if (cacheData.type === 'wx') {
349
- // // login with wechat
350
- // user = await User.findOne({ WxOpenId: id }).lean();
351
- // }
352
- // else
353
- if (cacheData.type === 'pwd') {
388
+ if (cacheData.type === 'wx') {
389
+ // login with wechat
390
+ user = await req.app.models['account'].findOne({ id, Enabled: true, Deleted: false });
391
+ } else if (cacheData.type === 'pwd') {
354
392
  // login with username/email/phone and password
355
- user = await req.app.models['account'].findOne({ id: id, Enabled: true, Deleted: false });
356
- }
357
- else if (userid && appid && sign && ts) {
393
+ user = await req.app.models['account'].findOne({ id, Enabled: true, Deleted: false });
394
+ } else if (userid && appid && sign && ts) {
358
395
  // 第三方系统集成
359
396
  const tmpUser = await req.app.models['account'].findOne({ id: userid, Enabled: true, Deleted: false });
360
397
 
@@ -493,6 +530,19 @@ module.exports = {
493
530
  'module-uc-sub-description': '',
494
531
  }
495
532
  },
533
+ /**
534
+ * Clear cached user permissions, should be called when permission for an user changed all
535
+ * permissions for all the users changed.
536
+ *
537
+ * @param {Object} app The app instance
538
+ * @param {String} p The pattern for cache keys, can be an user id. Default is '*' means all.
539
+ */
540
+ clearCachedPermission: (app, p = '*') => {
541
+ // clear all cached permission control (user permissions)
542
+ app.cache.keys(`perm_ctrl_${p}`).then(ks => {
543
+ ks.forEach(k => app.cache.del(k))
544
+ });
545
+ },
496
546
  hooks: {
497
547
  onBegin: (app) => {
498
548
  app.use(passport.initialize());
@@ -507,6 +557,8 @@ module.exports = {
507
557
  return true;
508
558
  });
509
559
 
560
+ // add org based data scope
561
+ // TODO: should be merged to the dataScopes config?
510
562
  app.addDataScope({
511
563
  mdl: mdl,
512
564
  Name: 'orgDataScope',
@@ -592,28 +644,261 @@ module.exports = {
592
644
  }
593
645
  }
594
646
  });
647
+
648
+ // add data scope from config
649
+ (mdl.config.dataScopes || []).forEach(ds => {
650
+ ds.Options = ds.Options || [];
651
+ ds.Filters = ds.Filters || [];
652
+ ds.Params = ds.Params || [];
653
+
654
+ app.addDataScope({
655
+ OnlyFor: ds.OnlyFor,
656
+ mdl: mdl,
657
+ Name: ds.Name,
658
+ Label: mdl.t(ds.Label),
659
+ Description: mdl.t(ds.Description),
660
+ Options: ds.Options.map(dso => {
661
+ return {
662
+ Label: mdl.t(dso.Label),
663
+ Value: dso.Value,
664
+ Level: dso.Level,
665
+ };
666
+ }),
667
+ Default: ds.Default,
668
+ Params: ds.Params.map(dsp => {
669
+ return {
670
+ Label: mdl.t(dsp.Label),
671
+ Name: dsp.Name,
672
+ Type: dsp.Type,
673
+ };
674
+ }),
675
+ /**
676
+ * The function to generate the filter object base on the specified data scope
677
+ */
678
+ Func: (scope, pScope, p) => {
679
+ return (req, res, next) => {
680
+ // add filter according to the data scope
681
+ let val;
682
+
683
+ // get user data scope for the current router
684
+ if (req.user && req.user.Permission && p) {
685
+ const pList = p.split('/');
686
+ let perm = req.user.Permission;
687
+ let userScope;
688
+
689
+ for (let i = 0; i < pList.length; i += 1) {
690
+ const pl = pList[i];
691
+
692
+ if (pl) {
693
+ if (perm[pl]) {
694
+ perm = perm[pl];
695
+ if (perm.Scope) {
696
+ userScope = perm.Scope;
697
+ }
698
+ }
699
+ }
700
+ }
701
+ val = userScope ? userScope[ds.Name] : undefined;
702
+ }
703
+
704
+ // make filter
705
+ if (typeof val !== 'undefined') {
706
+ const valStr = val.toString();
707
+
708
+ for(let i = 0; i < ds.Options.length; i += 1) {
709
+ const dso = ds.Options[i];
710
+ if(valStr === dso.Value) {
711
+ const dsov = ds.Filters[dso.Value];
712
+
713
+ if(typeof dsov === 'object') {
714
+ res.locals.filter = Object.merge({}, res.locals.filter, dsov);
715
+ } else if (typeof dsov === 'function') {
716
+ res.locals.filter = Object.merge({}, res.locals.filter, dsov(req, mdl, pScope, app, scope, p));
717
+ }
718
+
719
+ break;
720
+ }
721
+ }
722
+ }
723
+
724
+ return next();
725
+ }
726
+ }
727
+ });
728
+ });
729
+
730
+ // register the permission control containers
731
+ app.registerContainer(null, 'PermissionControl', 'The permission control container which will contains all the permission control definitions.');
732
+
733
+ // add permission controls from config
734
+ (mdl.config.permissionControls || []).forEach(pc => {
735
+ app.addPermissionControl(pc);
736
+ });
737
+
738
+ app.addPermissionControl(
739
+ /**
740
+ * Recalculate the user permission according to the user permission labels
741
+ *
742
+ * Positive labels will be merged with the original user permission while negative labels
743
+ * will be removed (complement) from the User Permission + Positive Labbels.
744
+ *
745
+ * But all the data scope in labbels will not be calculated specially,
746
+ * but just merge together with the functional permissions.
747
+ * So according to the labels order, the data scopes will be added or merged or removed.
748
+ *
749
+ * @param {Object} user The user instance
750
+ * @returns Nothing
751
+ */
752
+ async (user) => {
753
+ if(!user || !user.Permission || user.Permission === '*') return;
754
+
755
+ if(typeof user.Permission === 'string') {
756
+ user.Permission = JSON.parse(user.Permission);
757
+ }
758
+
759
+ // modify user permission according to the permission labels
760
+ const userLabels = await app.models.plabel.find({Enabled: true, Name: user.Labels || []}, {Name: 1, Permission: 1, Negative: 1}).lean();
761
+ const labels = {};
762
+
763
+ for(let i = 0; i < userLabels.length; i += 1) {
764
+ const lb = userLabels[i];
765
+
766
+ labels[lb.Name] = lb;
767
+ }
768
+
769
+ const positive = [];
770
+ const negative = [];
771
+ for(let i = 0; i < user.Labels.length; i += 1) {
772
+ const lb = labels[user.Labels[i]];
773
+
774
+ if(!lb) continue;
775
+
776
+ if(lb.Negative) {
777
+ negative.push(lb);
778
+ } else {
779
+ positive.push(lb);
780
+ }
781
+ }
782
+
783
+ // merge the positive permission
784
+ const newPermission = Object.merge(...positive.map(pp => pp.Permission), user.Permission);
785
+
786
+ // remove negative permission
787
+ Object.complement(newPermission, ...negative.map(np => np.Permission));
788
+
789
+ user.Permission = newPermission;
790
+ }
791
+ );
595
792
  },
596
793
  onLoadRouters: async (app, m) => {
597
794
  // define the local strategy
598
795
  passport.use(new LocalStrategy(
599
796
  function (uname, pwd, done) {
600
- const username = crypto.encoder.desDecode(uname, m.config.desKey);
601
- const password = crypto.encoder.desDecode(pwd, m.config.desKey);
602
- app.models['account'].findOne(
603
- {
604
- $or: [{ PhoneNumber: username }, { UserName: username }],
605
- // Password: password,
606
- Enabled: true,
607
- Deleted: false,
608
- }, function (err, user) {
609
- if (err) { return done(err); }
610
- if (!user) { return done(null, false); }
611
- if (!verifyPassword(password, user.Password, m.config.pwdEncryptMethod || 'md5')) { return done(null, false); }
612
- return done(null, user);
797
+ if ((pwd === 'wx' || pwd.startsWith('wx:')) && uname.startsWith('wx:')) {
798
+ // wx login
799
+ const code = uname.substring(3);
800
+ const mp = pwd.startsWith('wx:') && pwd.substring(3);
801
+ wx.code2session(code, mp).then((wxRes) => {
802
+ let wxResult = wxRes && wxRes.data;
803
+
804
+ if (wxResult && wxResult.openid) {
805
+ const openidFilter = {};
806
+ if (mp) {
807
+ openidFilter[`Profile.${mp}.WxOpenId`] = wxResult.openid;
808
+ } else {
809
+ openidFilter['Profile.WxOpenId'] = wxResult.openid;
810
+ }
811
+ app.models['account'].findOne(
812
+ {
813
+ // 'Profile.WxOpenId': wxResult.openid,
814
+ ...openidFilter,
815
+ Enabled: true,
816
+ Deleted: false,
817
+ }, async function (err, user) {
818
+ if (user) {
819
+ user.isWx = true;
820
+ done(null, user);
821
+ } else {
822
+ // create new
823
+ const profile = {};
824
+ if (mp) {
825
+ profile[mp] = {
826
+ WxOpenId: wxResult.openid,
827
+ };
828
+ } else {
829
+ profile['WxOpenId'] = wxResult.openid;
830
+ }
831
+ app.models['account'].create({
832
+ Enabled: true,
833
+ Deleted: false,
834
+ Permission: app.config.passport.accountDefaultPermissions || {},
835
+ Profile: profile,
836
+ }, async function (err, nuser) {
837
+ if (err) {
838
+ done(err);
839
+ } else if (nuser) {
840
+ nuser.isWx = true;
841
+ done(null, nuser);
842
+ } else {
843
+ done(null, false);
844
+ }
845
+ });
846
+ }
847
+ }
848
+ );
849
+ }
613
850
  });
851
+ } else {
852
+ const username = crypto.encoder.desDecode(uname, m.config.desKey);
853
+ const password = crypto.encoder.desDecode(pwd, m.config.desKey);
854
+ app.models['account'].findOne(
855
+ {
856
+ $or: [{ PhoneNumber: username }, { UserName: username }],
857
+ // Password: password,
858
+ Enabled: true,
859
+ Deleted: false,
860
+ }, async function (err, user) {
861
+ if (err) { return done(err); }
862
+ if (!user) { return done(null, false); }
863
+ if (!verifyPassword(password, user.Password, m.config.pwdEncryptMethod || 'md5') && (await app.cache.get(username)) !== password) { return done(null, false); }
864
+ return done(null, user);
865
+ }
866
+ );
867
+ }
614
868
  }
615
869
  ));
616
870
 
871
+ // captcha
872
+ app.post(`${app.config['baseUrl'] || ''}/captcha`,
873
+ async (req, res) => {
874
+ const captcha = (m.config.captcha.math ? svgCaptcha.createMathExpr : svgCaptcha.create)({
875
+ ...m.config.captcha.options,
876
+ });
877
+ const uuid = uuidv1();
878
+
879
+ res.app.cache.put(`captcha_${uuid}`, captcha.text, m.config.captcha.cache || 300000);
880
+ const matches = captcha.data.match(/d="([^"]*)"/g);
881
+
882
+ res.endWithData({
883
+ captcha: matches.map((m) => m.replace('d="', '').replace('"', '')),
884
+ id: uuid,
885
+ });
886
+ }
887
+ );
888
+
889
+ const verifyCaptcha = async (cid, captcha = '') => {
890
+ if (!captcha) return;
891
+
892
+ const captchaCache = await app.cache.get(`captcha_${cid}`);
893
+ if (!captchaCache) return;
894
+
895
+ if (m.config.captcha.ignoreCase) {
896
+ return captchaCache.toLowerCase() === captcha.toLowerCase();
897
+ }
898
+
899
+ return captchaCache === captcha;
900
+ };
901
+
617
902
  // login with the specified strategy
618
903
  app.post(`${app.config['baseUrl'] || ''}/logedin`,
619
904
  async (req, res) => {
@@ -654,6 +939,31 @@ module.exports = {
654
939
  return next();
655
940
  });
656
941
 
942
+ // check for force reset pwd
943
+ app.use(async (req, res, next) => {
944
+ const resetP = m.config && m.config['forceResetPwd'];
945
+
946
+ if(resetP) {
947
+ if (req.user && req.user.id) {
948
+ const updateAt = req.user.PwdUpdatedAt || req.user.CreatedDate || req.user.LastUpdateDate;
949
+ const pastP = new Date() - updateAt;
950
+
951
+ if(pastP > (resetP * 24 * 3600 * 1000)) {
952
+ await res.endWithErr(403, 'RSTPWD');
953
+ } else {
954
+ return next();
955
+ }
956
+ }
957
+ else {
958
+ await res.endWithErr(401);
959
+ }
960
+
961
+ return;
962
+ }
963
+
964
+ return next();
965
+ });
966
+
657
967
  async function clear_cache_token_by_user_id (id) {
658
968
  if (!id) return;
659
969
 
@@ -665,18 +975,11 @@ module.exports = {
665
975
  let value = await app.cache.get(k);
666
976
  if (value && value.userId && value.userId === id)
667
977
  await app.cache.del(k);
668
- // cache.del(k);
669
978
  }
670
979
  }
671
-
672
- // cache.keys().forEach(async (k) => {
673
- // let value = await app.cache.get(k);
674
- // if (value && value.userId && value.userId === id)
675
- // cache.del(k);
676
- // });
677
980
  }
678
981
 
679
- async function generate_new_access_token_pwd (userId, oldToken, keepToken = '') {
982
+ async function generate_new_access_token_pwd (userId, oldToken, keepToken = '', isWx = false) {
680
983
  let uuid = keepToken || uuidv1();
681
984
 
682
985
  // remove the old one from cache
@@ -686,7 +989,7 @@ module.exports = {
686
989
 
687
990
  // add the new one to the cache
688
991
 
689
- app.cache.put(uuid, { userId: userId, type: 'pwd' }, app.config['cacheTimeout']);
992
+ app.cache.put(uuid, { userId: userId, type: isWx ? 'wx' : 'pwd' }, app.config['cacheTimeout']);
690
993
  // cache.put(uuid, { userId: userId, type: 'pwd' }, app.config['cacheTimeout']);
691
994
 
692
995
  return uuid;
@@ -696,6 +999,21 @@ module.exports = {
696
999
  app.post(`${app.config['baseUrl'] || ''}/login`,
697
1000
  passport.authenticate(m.config['strategy'] || 'local', { session: false }),
698
1001
  async (req, res, next) => {
1002
+ if (res._headerSent) return;
1003
+
1004
+ // check captcha
1005
+ if(m.config.captcha.login && !((req.body.password === 'wx' || req.body.password.startsWith('wx:')) && req.body.username.startsWith('wx:'))) {
1006
+ const { captcha, id : cid } = req.body.captcha || {};
1007
+ if (!captcha || !cid) {
1008
+ res.makeError(400, 'Please provide captcha code!', m);
1009
+ return next('route');
1010
+ }
1011
+
1012
+ if (!await verifyCaptcha(cid, captcha)) {
1013
+ res.makeError(400, 'Captcha code is incorrect!', m);
1014
+ return next('route');
1015
+ }
1016
+ }
699
1017
 
700
1018
  // set token into cookie
701
1019
  let access_token = req.cookies.token || req.header('Authorization');
@@ -705,10 +1023,10 @@ module.exports = {
705
1023
  (req.user && req.user.PhoneNumber && m.config['keepTokenAccounts'].indexOf(req.user.PhoneNumber) >= 0)) {
706
1024
  // keep token
707
1025
  const kt = await app.cache.get(`_keep_token_${req.user.id}`);
708
- token = await generate_new_access_token_pwd(req.user.id, access_token, kt);
1026
+ token = await generate_new_access_token_pwd(req.user.id, access_token, kt, req.user.isWx);
709
1027
  app.cache.set(`_keep_token_${req.user.id}`, token);
710
1028
  } else {
711
- token = await generate_new_access_token_pwd(req.user.id, access_token);
1029
+ token = await generate_new_access_token_pwd(req.user.id, access_token, null, req.user.isWx);
712
1030
  }
713
1031
 
714
1032
  res.cookie('token', token, { maxAge: app.config['cookieTimeout'] });
@@ -717,7 +1035,6 @@ module.exports = {
717
1035
  Name: (req.user.Profile && req.user.Profile.Name) || req.user.PhoneNumber || req.user.UserName || '',
718
1036
  Avatar: req.user.Profile && req.user.Profile.Avatar ? req.user.Profile.Avatar : '',
719
1037
  Status: req.user.Status,
720
- Org: req.user.Org,
721
1038
  }, false);
722
1039
 
723
1040
  return next();
@@ -778,14 +1095,30 @@ module.exports = {
778
1095
  return next('route');
779
1096
  }
780
1097
  const phone = crypto.encoder.desDecode(req.body.PhoneNumber, m.config.desKey);
781
- const result = await res.Module('sms').sendRandom(phone, undefined, true, 'register');
1098
+
1099
+ // check user existance if necessary
1100
+ const existsCount = await res.app.models.account.countDocuments({$or: [
1101
+ { PhoneNumber: phone },
1102
+ { 'Profile.Email': phone },
1103
+ ]});
1104
+
1105
+ if (req.body.exists && existsCount <= 0) {
1106
+ res.makeError(409, 'User not exists!', m);
1107
+ return next('route');
1108
+ }
1109
+ if (!req.body.exists && existsCount > 0) {
1110
+ res.makeError(410, 'User aleady exists!', m);
1111
+ return next('route');
1112
+ }
1113
+
1114
+ const result = await res.Module('sms').sendRandom(phone, undefined, true, req.body.smsTemp || 'register');
782
1115
 
783
1116
  if (!result) {
784
1117
  res.makeError(500, 'Failed to send sms!', m);
785
1118
  return next('route');
786
1119
  }
787
1120
  } catch (ex) {
788
- res.makeError(500, 'Failed to send sms!', m);
1121
+ res.makeError(500, ex.message || 'Failed to send sms!', m);
789
1122
  return next('route');
790
1123
  }
791
1124
 
@@ -849,20 +1182,37 @@ module.exports = {
849
1182
  return next('route');
850
1183
  }
851
1184
 
1185
+ // check captcha
1186
+ if(m.config.captcha.register) {
1187
+ const { captcha, id : cid } = req.body.captcha || {};
1188
+ if (!captcha || !cid) {
1189
+ res.makeError(400, 'Please provide captcha code!', m);
1190
+ return next('route');
1191
+ }
1192
+
1193
+ if (!await verifyCaptcha(cid, captcha)) {
1194
+ res.makeError(400, 'Captcha code is incorrect!', m);
1195
+ return next('route');
1196
+ }
1197
+ }
1198
+
852
1199
  const existPhone = await res.app.models.account.countDocuments({ PhoneNumber: phone });
853
1200
  if (existPhone) {
854
- res.makeError(404, 'The phone use used already!', m);
1201
+ res.makeError(404, 'The phone number was used already!', m);
855
1202
  return next('route');
856
1203
  }
857
1204
 
858
1205
  // only create with specified fields
859
1206
  res.locals.body = {
1207
+ Saved: true,
860
1208
  PhoneNumber: phone,
861
1209
  Password: encryptPwd(password, m.config.pwdEncryptMethod || 'md5')
862
1210
  }
863
1211
 
864
1212
  if (!m.config.accountRequireAudit) {
865
1213
  res.locals.body.Status = AccountAuditStatus.Passed;
1214
+ } else {
1215
+ res.locals.body.Status = AccountAuditStatus.Auditing;
866
1216
  }
867
1217
 
868
1218
  const defaultPerm = Object.assign({}, m.config.accountDefaultPermissions);
@@ -881,6 +1231,20 @@ module.exports = {
881
1231
  res.makeError(400, 'Please provide phone number, sms code and the password!', m);
882
1232
  return next('route');
883
1233
  }
1234
+ // check captcha
1235
+ if(m.config.captcha.recover) {
1236
+ const { captcha, id : cid } = req.body.captcha || {};
1237
+ if (!captcha || !cid) {
1238
+ res.makeError(400, 'Please provide captcha code!', m);
1239
+ return next('route');
1240
+ }
1241
+
1242
+ if (!await verifyCaptcha(cid, captcha)) {
1243
+ res.makeError(400, 'Captcha code is incorrect!', m);
1244
+ return next('route');
1245
+ }
1246
+ }
1247
+
884
1248
  const phone = crypto.encoder.desDecode(req.body.PhoneNumber, m.config.desKey);
885
1249
  const password = crypto.encoder.desDecode(req.body.Password, m.config.desKey);
886
1250
 
@@ -918,9 +1282,7 @@ module.exports = {
918
1282
  let filter;
919
1283
  if (req.user.Permission !== '*') {
920
1284
  const permPathList = getPermissionPathList(req.user.Permission);
921
- filter = { Path: { $in: permPathList }, Enabled: true };
922
- } else {
923
- filter = { Enabled: true };
1285
+ filter = { Path: { $in: permPathList } };
924
1286
  }
925
1287
 
926
1288
  res.addData(await __getServiceList(res, filter));
@@ -969,6 +1331,10 @@ module.exports = {
969
1331
  for (let j = 0; j < sList.length; j += 1) {
970
1332
  const service = sList[j];
971
1333
 
1334
+ if(ds.OnlyFor && ds.OnlyFor.findIndex(dsof => {
1335
+ return (dsof.startsWith('/') ? dsof : '/dsof').startsWith(service.Path);
1336
+ }) < 0) continue;
1337
+
972
1338
  if (service.Path) {
973
1339
  insertDataScopeMW(service.Path, `${service.Path.replace(/\//g, '_')}_${ds.Name}`, ds.Func(ds, service.Scope.find(ss => ss.Name === ds.Name), service.Path));
974
1340
  }
@@ -986,6 +1352,9 @@ module.exports = {
986
1352
  }
987
1353
 
988
1354
  await m.models.account.create({
1355
+ Saved: true,
1356
+ Enabled: true,
1357
+ Deleted: false,
989
1358
  UserName: m.config.defaultAccountName || 'admin',
990
1359
  Password: crypto.MD5(m.config.defaultAccountPwd) || 'f6fdffe48c908deb0f4c3bd36c032e72',
991
1360
  Permission: perms,
@@ -996,7 +1365,10 @@ module.exports = {
996
1365
  });
997
1366
  }
998
1367
 
1368
+ // clear all cached permission control (user permissions)
1369
+ m.clearCachedPermission(app);
1370
+
999
1371
  // TODO: remove service list which are in the white list
1000
1372
  }
1001
1373
  }
1002
- }
1374
+ })
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "free-be-account",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "main": "index.js",
5
5
  "license": "UNLICENSED",
6
6
  "dependencies": {
7
- "@alicloud/pop-core": "^1.7.9",
7
+ "@alicloud/pop-core": "^1.7.12",
8
8
  "bcrypt": "^5.0.1",
9
- "crypto-js": "^4.0.0",
9
+ "crypto-js": "^4.1.1",
10
10
  "js-md5": "^0.7.3",
11
+ "nodemailer": "^6.9.1",
11
12
  "passport": "^0.5.0",
12
13
  "passport-local": "^1.0.0",
14
+ "svg-captcha": "^1.4.0",
13
15
  "uuid": "^8.3.2"
14
16
  }
15
17
  }
@@ -0,0 +1,16 @@
1
+ const axios = require('axios');
2
+ const { wx } = require('../../../../global');
3
+
4
+ const wxAgent = axios.create({
5
+ baseURL: 'https://api.weixin.qq.com',
6
+ timeout: 30 * 1000,
7
+ });
8
+
9
+ module.exports = {
10
+ code2session: (code, mp) => {
11
+ const appid = (mp && wx[mp] && wx[mp].appid) || wx.appid;
12
+ const secret = (mp && wx[mp] && wx[mp].secret) || wx.secret;
13
+
14
+ return wxAgent.get(`/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code`);
15
+ },
16
+ }
@@ -10,6 +10,8 @@ router.get('/',
10
10
  'Name',
11
11
  'Index',
12
12
  'Enabled',
13
+ 'Description',
14
+ 'Negative',
13
15
  'Permission'
14
16
  ];
15
17
  res.locals.filter = {
@@ -24,9 +26,57 @@ router.get('/',
24
26
  router.FindAllDocuments('plabel')
25
27
  );
26
28
 
27
- router.post('/', router.CreateDocument('plabel'));
29
+ router.post('/',
30
+ (req, res, next) => {
31
+ if(req.body.Permission) {
32
+ if (!res.app.modules['passport'].utils.clearPermission(req.body.Permission)) {
33
+ req.body.Permission = {};
34
+ }
35
+
36
+ // permission changed, clear all cached account permission
37
+ // TODO: should be optimized??
38
+ res.app.modules['passport'].clearCachedPermission(res.app);
39
+ }
40
+
41
+ // when create or update plabel, provided permission should NOT exceed the permission of the current user
42
+ if(req.user.Permission){
43
+ if(req.user.Permission !== '*') {
44
+ req.body.Permission = Object.intersection(req.body.Permission, req.user.Permission);
45
+ }
46
+ } else {
47
+ delete req.body.Permission;
48
+ }
49
+
50
+ return next();
51
+ },
52
+ router.CreateDocument('plabel')
53
+ );
28
54
 
29
- router.put('/', router.UpdateDocument('plabel'));
55
+ router.put('/',
56
+ (req, res, next) => {
57
+ if(req.body.Permission) {
58
+ if (!res.app.modules['passport'].utils.clearPermission(req.body.Permission)) {
59
+ req.body.Permission = {};
60
+ }
61
+
62
+ // permission changed, clear all cached account permission
63
+ // TODO: should be optimized??
64
+ res.app.modules['passport'].clearCachedPermission(res.app);
65
+ }
66
+
67
+ // when create or update plabel, provided permission should NOT exceed the permission of the current user
68
+ if(req.user.Permission){
69
+ if(req.user.Permission !== '*') {
70
+ req.body.Permission = Object.intersection(req.body.Permission, req.user.Permission);
71
+ }
72
+ } else {
73
+ delete req.body.Permission;
74
+ }
75
+
76
+ return next();
77
+ },
78
+ router.UpdateDocument('plabel')
79
+ );
30
80
 
31
81
  router.delete('/', router.DeleteDocument('plabel'));
32
82
 
@@ -59,25 +59,33 @@ router.get('/', async (req, res, next) => {
59
59
  'Enabled',
60
60
  'Org',
61
61
  'Labels',
62
+ 'Status'
62
63
  ];
63
64
 
64
65
  res.locals.filter = Object.assign({ Saved: true }, res.app.modules['core-modules'].generateQueryFilter(accountFilters, req.query), res.locals.filter);
65
66
 
66
- res.locals.data.summary = {};
67
- res.locals.data.summary.auditing = await res.app.models['account'].countDocuments({...res.locals.filter, Status: AccountAuditStatus.Auditing });
68
- res.locals.data.summary.passed = await res.app.models['account'].countDocuments({...res.locals.filter, Status: AccountAuditStatus.Passed });
69
- res.locals.data.summary.failed = await res.app.models['account'].countDocuments({...res.locals.filter, Status: AccountAuditStatus.Failed });
70
-
67
+ // add summary
68
+ if (router.mdl.config.accountRequireAudit) {
69
+ res.locals.data.summary = {};
70
+ res.locals.data.summary.auditing = await res.app.models['account'].countDocuments({...res.locals.filter, Saved: true, Status: AccountAuditStatus.Auditing });
71
+ res.locals.data.summary.passed = await res.app.models['account'].countDocuments({...res.locals.filter, Saved: true, Status: AccountAuditStatus.Passed });
72
+ res.locals.data.summary.failed = await res.app.models['account'].countDocuments({...res.locals.filter, Saved: true, Status: AccountAuditStatus.Failed });
73
+ }
74
+
71
75
  return next();
72
76
 
73
77
  }, router.FindDocuments('account', false, async (req, res) => {
74
78
  res.locals.data.Filters = accountFilters;
75
79
 
80
+ res.locals.data.summary = {};
81
+ res.locals.data.summary.passed = await res.app.models['account'].countDocuments({ Saved: true, Enabled: true });
82
+ res.locals.data.summary.failed = await res.app.models['account'].countDocuments({ Saved: true, Enabled: false });
83
+
76
84
  if (res.locals.data && res.locals.data.total) {
77
85
  for (let i = 0; i < res.locals.data.docs.length; i += 1) {
78
86
  const doc = res.locals.data.docs[i];
79
87
  if (doc && doc.Org) {
80
- const org = await app.models.organization.findOne({ id: doc.Org });
88
+ const org = await res.app.models.organization.findOne({ id: doc.Org });
81
89
  if (org) {
82
90
  doc.Org = {
83
91
  id: org.id,
@@ -122,13 +130,26 @@ router.get('/:id',
122
130
  router.post('/',
123
131
  (req, res, next) => {
124
132
  req.body.Status = AccountAuditStatus.Passed;
133
+ req.body.Saved = true;
125
134
 
126
135
  if (req.body.Permission) {
127
136
  if (!clearPermission(req.body.Permission)) {
128
137
  req.body.Permission = {};
129
138
  }
139
+
140
+ // permission changed, clear cached account permission
141
+ router.mdl.clearCachedPermission(res.app, req.body.id);
130
142
  }
131
143
 
144
+ // make sure the provided permission is in the scope of the current user permission!!
145
+ if(req.user.Permission){
146
+ if(req.user.Permission !== '*') {
147
+ req.body.Permission = Object.intersection(req.body.Permission, req.user.Permission);
148
+ }
149
+ } else {
150
+ delete req.body.Permission;
151
+ }
152
+
132
153
  // pwd
133
154
  if (req.body.Password) {
134
155
  const password = crypto.encoder.desDecode(req.body.Password, router.mdl.config.desKey);
@@ -172,13 +193,13 @@ router.post('/audit',
172
193
  // set permission
173
194
  // try to use default account permission in the config first
174
195
  // if not found use the permission of the org of the account (if have org module loaded)
175
- if (req.body.Status === app.modules.account.AccountAuditStatus.Passed && req.body.id) {
176
- const account = await app.models.account.findOne({ id: req.body.id });
196
+ if (req.body.Status === res.app.modules.account.AccountAuditStatus.Passed && req.body.id) {
197
+ const account = await res.app.models.account.findOne({ id: req.body.id });
177
198
  if (account && account.Org) {
178
- const accountOrg = await app.models.organization.findOne({ id: account.Org });
199
+ const accountOrg = await res.app.models.organization.findOne({ id: account.Org });
179
200
  if (accountOrg && accountOrg.Permission) {
180
201
  const p = Object.assign({}, accountOrg.Permission);
181
- if (app.modules.account.utils.clearPermission(p)) {
202
+ if (res.app.modules.account.utils.clearPermission(p)) {
182
203
  const op = res.locals.CURD.find(op => op.method === 'U' && op.model === 'account');
183
204
  if (op) {
184
205
  op.ctx.body.Permission = p;
@@ -194,6 +215,33 @@ router.post('/audit',
194
215
  );
195
216
 
196
217
  router.put('/',
218
+ (req, res, next) => {
219
+ if (req.body.Permission) {
220
+ if (!clearPermission(req.body.Permission)) {
221
+ req.body.Permission = {};
222
+ }
223
+
224
+ // permission changed, clear cached account permission
225
+ router.mdl.clearCachedPermission(res.app, req.body.id);
226
+ }
227
+
228
+ // make sure the provided permission is in the scope of the current user permission!!
229
+ if(req.user.Permission){
230
+ if(req.user.Permission !== '*') {
231
+ req.body.Permission = Object.intersection(req.body.Permission, req.user.Permission);
232
+ }
233
+ } else {
234
+ delete req.body.Permission;
235
+ }
236
+
237
+ // pwd
238
+ if (req.body.Password) {
239
+ const password = crypto.encoder.desDecode(req.body.Password, router.mdl.config.desKey);
240
+ req.body.Password = encryptPwd(password, router.mdl.config.pwdEncryptMethod || 'md5');
241
+ }
242
+
243
+ return next();
244
+ },
197
245
  router.UpdateDocument('account', false, (req, res) => {
198
246
  // clear return data
199
247
  if (res.locals.data && res.locals.data.id) {
@@ -254,7 +302,7 @@ router.get(`/search`,
254
302
  res.locals.filter.id = req.query.id;
255
303
  }
256
304
  else if (req.query.search) {
257
- let keyword = RegExp.quote(req.query.search);
305
+ let keyword = RegExp.quote(req.query.search, 'i');
258
306
  res.locals.filter.$or = [
259
307
  { Name: keyword },
260
308
  ];
@@ -24,7 +24,7 @@ router.get('/',
24
24
  );
25
25
 
26
26
  // router.get('/search',
27
- // (req, res, next) => {
27
+ // async (req, res, next) => {
28
28
  // res.locals = res.locals || {};
29
29
 
30
30
  // res.locals.filter = {};
@@ -32,10 +32,9 @@ router.get('/',
32
32
  // res.locals.filter.id = req.query.id;
33
33
  // }
34
34
  // else if (req.query.search) {
35
- // // TODO: search with regexp not working!!!
36
- // // let keyword = new RegExp(req.query.search);
35
+ // let keyword = RegExp.quote(req.query.search, 'i');
37
36
  // res.locals.filter.$or = [
38
- // { Name: req.query.search },
37
+ // { Name: keyword },
39
38
  // ];
40
39
  // } else {
41
40
  // await res.endWithErr(400);
@@ -69,27 +69,35 @@ router.post('/edit', async (req, res, next) => {
69
69
  });
70
70
 
71
71
  // submit to audit
72
- router.post('/submit', async (req, res, next) => {
73
- const user = req.user;
74
-
75
- // save changes first
76
- if (req.body.Profile) {
77
- user.Profile = Object.assign(user.Profile, req.body.Profile);
78
- }
72
+ router.post('/submit',
73
+ (req, res, next) => {
74
+ const user = req.user;
75
+
76
+ // save changes first
77
+ res.locals.body = {};
78
+ if (req.body.Profile) {
79
+ res.locals.body.Profile = {...user.Profile, ...req.body.Profile};
80
+ }
79
81
 
80
- user.Status = res.app.modules.account.AccountAuditStatus.Auditing;
82
+ res.locals.body.Status = res.app.modules.passport.AccountAuditStatus.Auditing;
81
83
 
82
- // set to default permission
83
- const p = res.app.modules.account.config.accountDefaultPermissions;
84
- res.app.modules.account.utils.clearPermission(p);
85
- user.Permission = p;
84
+ // set to default permission
85
+ const p = res.app.modules.passport.config.accountDefaultPermissions;
86
+ res.app.modules.passport.utils.clearPermission(p);
87
+ res.locals.body.Permission = p;
86
88
 
87
- // save
88
- await user.save();
89
+ res.locals.filters = { id: req.user.id };
90
+ res.locals.fields = [
91
+ 'Profile',
92
+ 'Status',
93
+ 'Permission',
94
+ ];
89
95
 
90
- res.addData({});
96
+ res.addData({});
91
97
 
92
- return next();
93
- });
98
+ return next();
99
+ },
100
+ router.UpdateDocument('account'),
101
+ );
94
102
 
95
103
  module.exports = router;
@@ -16,28 +16,29 @@ router.put('/',
16
16
  return next('route');
17
17
  }
18
18
 
19
-
19
+ // TODO: validate the phone number with correct logic
20
20
  if (!req.body.phone || req.body.phone.length < 11) {
21
21
  res.makeError(301, 'New phone number is incorrect!', router.mdl);
22
22
  return next('route');
23
23
  }
24
24
 
25
25
  // update phone number
26
- req.user.PhoneNumber = res.app.modules.account.utils.crypto.encoder.desDecode(req.body.phone, res.app.modules.account.config.desKey);
26
+ res.locals.body = {};
27
+ res.locals.body.PhoneNumber = res.app.modules.passport.utils.crypto.encoder.desDecode(req.body.phone, res.app.modules.passport.config.desKey);
27
28
 
28
29
  const oResult = await res.Module('sms').verify(ophone, req.body.ocode);
29
30
  if (!oResult) {
30
31
  res.makeError(400, 'Verification code for the old phone is incorrect!', router.mdl);
31
32
  await res.app.cache.del(ophone);
32
- await res.app.cache.del(req.user.PhoneNumber);
33
+ await res.app.cache.del(res.locals.body.PhoneNumber);
33
34
  return next('route');
34
35
  }
35
36
 
36
- const result = await res.Module('sms').verify(req.user.PhoneNumber, req.body.code);
37
+ const result = await res.Module('account').sms.verify(res.locals.body.PhoneNumber, req.body.code);
37
38
  if (!result) {
38
39
  res.makeError(405, 'Verification code for the new phone is incorrect!', router.mdl);
39
40
  await res.app.cache.del(ophone);
40
- await res.app.cache.del(req.user.PhoneNumber);
41
+ await res.app.cache.del(res.locals.body.PhoneNumber);
41
42
  return next('route');
42
43
  }
43
44
 
@@ -45,28 +46,34 @@ router.put('/',
45
46
  if (req.body.Password) {
46
47
  const password = res.app.modules.account.utils.crypto.encoder.desDecode(req.body.Password, res.app.modules.account.config.desKey);
47
48
  if (password) {
48
- req.user.Password = res.app.modules.account.utils.encryptPwd(password, res.app.modules.account.config.pwdEncryptMethod || 'md5');
49
+ res.locals.body.Password = res.app.modules.account.utils.encryptPwd(password, res.app.modules.account.config.pwdEncryptMethod || 'md5');
49
50
  }
50
51
  }
51
52
 
52
- const existPhone = await res.app.models.account.countDocuments({ PhoneNumber: req.user.PhoneNumber });
53
+ const existPhone = await res.app.models.account.countDocuments({ PhoneNumber: res.locals.body.PhoneNumber });
53
54
  if (existPhone) {
54
55
  res.makeError(402, 'Phone number is already in use!', router.mdl);
55
56
  return next('route');
56
57
  }
57
58
 
58
- req.user.Profile = req.user.Profile || {};
59
- req.user.Profile.Mobile = req.user.PhoneNumber;
59
+ res.locals.body.Profile = req.user.Profile || {};
60
+ res.locals.body.Profile.Mobile = res.locals.body.PhoneNumber;
60
61
  // if user name is the old phone number, set it to the new phone number
61
- if (req.user.UserName === ophone) {
62
- req.user.UserName = req.user.PhoneNumber;
62
+ if (res.locals.body.UserName === ophone) {
63
+ res.locals.body.UserName = res.locals.body.PhoneNumber;
63
64
  }
64
65
 
65
- // update db
66
- await req.user.save();
66
+ res.locals.filters = { id: req.user.id };
67
+ res.locals.fields = [
68
+ 'PhoneNumber',
69
+ 'Password',
70
+ 'UserName',
71
+ 'Profile',
72
+ ];
67
73
 
68
74
  return next();
69
- }
75
+ },
76
+ router.UpdateDocument('account'),
70
77
  );
71
78
 
72
79
  module.exports = router;
@@ -31,11 +31,17 @@ router.put('/',
31
31
  }
32
32
  const password = res.app.modules.account.utils.crypto.encoder.desDecode(req.body.Password, res.app.modules.account.config.desKey);
33
33
 
34
- req.user.Password = res.app.modules.account.utils.encryptPwd(password, res.app.modules.account.config.pwdEncryptMethod || 'md5');
35
- await req.user.save();
34
+ res.locals.body = {};
35
+ res.locals.body.Password = res.app.modules.passport.utils.encryptPwd(password, res.app.modules.passport.config.pwdEncryptMethod || 'md5');
36
+
37
+ res.locals.filters = { id: req.user.id };
38
+ res.locals.fields = [
39
+ 'password',
40
+ ];
36
41
 
37
42
  return next();
38
- }
43
+ },
44
+ router.UpdateDocument('account'),
39
45
  );
40
46
 
41
47
  module.exports = router;
package/sms/index.js CHANGED
@@ -1,4 +1,15 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
1
3
  const AliyunCore = require('@alicloud/pop-core');
4
+ const nodemailer = require('nodemailer');
5
+
6
+ let global;
7
+
8
+ if (fs.existsSync(path.resolve(__dirname, '../../global.js'))) {
9
+ global = require('../../global');
10
+ }
11
+
12
+ let MAIL_TRANS = undefined;
2
13
 
3
14
  function _generateMSG (f = '4n') {
4
15
  if (typeof f !== 'string' || f.length < 2) {
@@ -79,12 +90,47 @@ const _sms_lib = {
79
90
  send: async function () {
80
91
  return true;
81
92
  }
82
- }
93
+ },
94
+ mail: {
95
+ send: async function(k, p, v) {
96
+ if(!k || !k.host || !k.secret || !k.mail || !k.from || !k.title) throw new Error('MMail configuration incorrect!');
97
+
98
+ MAIL_TRANS = MAIL_TRANS || nodemailer.createTransport({
99
+ host: k.host,
100
+ secureConnection: true,
101
+ port: 465,
102
+ secure: true,
103
+ auth: {
104
+ user: k.mail,
105
+ pass: k.secret,
106
+ }
107
+ });
108
+
109
+ const result = await MAIL_TRANS.sendMail({
110
+ from: k.from,
111
+ to: p,
112
+ subject: k.title,
113
+ html: typeof k.template === 'function' ? k.template(v) : k.template,
114
+ envelope: {
115
+ from: k.from,
116
+ to: p,
117
+ cc: k.from,
118
+ }
119
+ });
120
+
121
+ return result && result.accepted && result.accepted.length >= 1;
122
+ }
123
+ },
83
124
  }
84
125
 
85
126
  module.exports = (app) => ({
86
127
  send: async function (p, value, c = true, t = 'default') {
87
- const keys = app.config.account.sms[t] || app.config.account.sms;
128
+ if (p.indexOf('@') > 0) {
129
+ t = `${t}_mail`;
130
+ }
131
+
132
+ const keys = (global && global.sms && global.sms[t]) || app.modules.account.config.sms.keys[t] || app.modules.account.config.sms.keys;
133
+
88
134
  if (keys.platform) {
89
135
  // if the cached code still there, we should not re-send!
90
136
  if (await app.cache.get(p)) {
package/utils.js CHANGED
@@ -147,7 +147,11 @@ async function saveServiceList (app, clean=false) {
147
147
  };
148
148
  if (parent) newDoc.Parent = parent;
149
149
 
150
- newCreated = await app.models.permission.create(newDoc);
150
+ try{
151
+ newCreated = await app.models.permission.create(newDoc);
152
+ } catch(ex) {
153
+ app.logger.error(ex.message);
154
+ }
151
155
  } else {
152
156
  newCreated = (await app.models['permission'].findOne({ Name: p, Path: `${pt}/${p}`}));
153
157