free-be-account 0.0.2 → 0.0.4

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,29 +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
- }).then((user) => {
609
- if (!user) { return done(null, false); }
610
- if (!verifyPassword(password, user.Password, m.config.pwdEncryptMethod || 'md5')) { return done(null, false); }
611
- return done(null, user);
612
- }).catch((err) => {
613
- return done(err);
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
+ }
614
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
+ }
615
868
  }
616
869
  ));
617
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
+
618
902
  // login with the specified strategy
619
903
  app.post(`${app.config['baseUrl'] || ''}/logedin`,
620
904
  async (req, res) => {
@@ -655,6 +939,31 @@ module.exports = {
655
939
  return next();
656
940
  });
657
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
+
658
967
  async function clear_cache_token_by_user_id (id) {
659
968
  if (!id) return;
660
969
 
@@ -666,18 +975,11 @@ module.exports = {
666
975
  let value = await app.cache.get(k);
667
976
  if (value && value.userId && value.userId === id)
668
977
  await app.cache.del(k);
669
- // cache.del(k);
670
978
  }
671
979
  }
672
-
673
- // cache.keys().forEach(async (k) => {
674
- // let value = await app.cache.get(k);
675
- // if (value && value.userId && value.userId === id)
676
- // cache.del(k);
677
- // });
678
980
  }
679
981
 
680
- async function generate_new_access_token_pwd (userId, oldToken, keepToken = '') {
982
+ async function generate_new_access_token_pwd (userId, oldToken, keepToken = '', isWx = false) {
681
983
  let uuid = keepToken || uuidv1();
682
984
 
683
985
  // remove the old one from cache
@@ -687,7 +989,7 @@ module.exports = {
687
989
 
688
990
  // add the new one to the cache
689
991
 
690
- 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']);
691
993
  // cache.put(uuid, { userId: userId, type: 'pwd' }, app.config['cacheTimeout']);
692
994
 
693
995
  return uuid;
@@ -697,6 +999,21 @@ module.exports = {
697
999
  app.post(`${app.config['baseUrl'] || ''}/login`,
698
1000
  passport.authenticate(m.config['strategy'] || 'local', { session: false }),
699
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
+ }
700
1017
 
701
1018
  // set token into cookie
702
1019
  let access_token = req.cookies.token || req.header('Authorization');
@@ -706,10 +1023,10 @@ module.exports = {
706
1023
  (req.user && req.user.PhoneNumber && m.config['keepTokenAccounts'].indexOf(req.user.PhoneNumber) >= 0)) {
707
1024
  // keep token
708
1025
  const kt = await app.cache.get(`_keep_token_${req.user.id}`);
709
- 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);
710
1027
  app.cache.set(`_keep_token_${req.user.id}`, token);
711
1028
  } else {
712
- 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);
713
1030
  }
714
1031
 
715
1032
  res.cookie('token', token, { maxAge: app.config['cookieTimeout'] });
@@ -718,7 +1035,6 @@ module.exports = {
718
1035
  Name: (req.user.Profile && req.user.Profile.Name) || req.user.PhoneNumber || req.user.UserName || '',
719
1036
  Avatar: req.user.Profile && req.user.Profile.Avatar ? req.user.Profile.Avatar : '',
720
1037
  Status: req.user.Status,
721
- Org: req.user.Org,
722
1038
  }, false);
723
1039
 
724
1040
  return next();
@@ -779,14 +1095,30 @@ module.exports = {
779
1095
  return next('route');
780
1096
  }
781
1097
  const phone = crypto.encoder.desDecode(req.body.PhoneNumber, m.config.desKey);
782
- 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');
783
1115
 
784
1116
  if (!result) {
785
1117
  res.makeError(500, 'Failed to send sms!', m);
786
1118
  return next('route');
787
1119
  }
788
1120
  } catch (ex) {
789
- res.makeError(500, 'Failed to send sms!', m);
1121
+ res.makeError(500, ex.message || 'Failed to send sms!', m);
790
1122
  return next('route');
791
1123
  }
792
1124
 
@@ -850,20 +1182,37 @@ module.exports = {
850
1182
  return next('route');
851
1183
  }
852
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
+
853
1199
  const existPhone = await res.app.models.account.countDocuments({ PhoneNumber: phone });
854
1200
  if (existPhone) {
855
- res.makeError(404, 'The phone use used already!', m);
1201
+ res.makeError(404, 'The phone number was used already!', m);
856
1202
  return next('route');
857
1203
  }
858
1204
 
859
1205
  // only create with specified fields
860
1206
  res.locals.body = {
1207
+ Saved: true,
861
1208
  PhoneNumber: phone,
862
1209
  Password: encryptPwd(password, m.config.pwdEncryptMethod || 'md5')
863
1210
  }
864
1211
 
865
1212
  if (!m.config.accountRequireAudit) {
866
1213
  res.locals.body.Status = AccountAuditStatus.Passed;
1214
+ } else {
1215
+ res.locals.body.Status = AccountAuditStatus.Auditing;
867
1216
  }
868
1217
 
869
1218
  const defaultPerm = Object.assign({}, m.config.accountDefaultPermissions);
@@ -882,6 +1231,20 @@ module.exports = {
882
1231
  res.makeError(400, 'Please provide phone number, sms code and the password!', m);
883
1232
  return next('route');
884
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
+
885
1248
  const phone = crypto.encoder.desDecode(req.body.PhoneNumber, m.config.desKey);
886
1249
  const password = crypto.encoder.desDecode(req.body.Password, m.config.desKey);
887
1250
 
@@ -919,9 +1282,7 @@ module.exports = {
919
1282
  let filter;
920
1283
  if (req.user.Permission !== '*') {
921
1284
  const permPathList = getPermissionPathList(req.user.Permission);
922
- filter = { Path: { $in: permPathList }, Enabled: true };
923
- } else {
924
- filter = { Enabled: true };
1285
+ filter = { Path: { $in: permPathList } };
925
1286
  }
926
1287
 
927
1288
  res.addData(await __getServiceList(res, filter));
@@ -970,6 +1331,10 @@ module.exports = {
970
1331
  for (let j = 0; j < sList.length; j += 1) {
971
1332
  const service = sList[j];
972
1333
 
1334
+ if(ds.OnlyFor && ds.OnlyFor.findIndex(dsof => {
1335
+ return (dsof.startsWith('/') ? dsof : '/dsof').startsWith(service.Path);
1336
+ }) < 0) continue;
1337
+
973
1338
  if (service.Path) {
974
1339
  insertDataScopeMW(service.Path, `${service.Path.replace(/\//g, '_')}_${ds.Name}`, ds.Func(ds, service.Scope.find(ss => ss.Name === ds.Name), service.Path));
975
1340
  }
@@ -987,6 +1352,9 @@ module.exports = {
987
1352
  }
988
1353
 
989
1354
  await m.models.account.create({
1355
+ Saved: true,
1356
+ Enabled: true,
1357
+ Deleted: false,
990
1358
  UserName: m.config.defaultAccountName || 'admin',
991
1359
  Password: crypto.MD5(m.config.defaultAccountPwd) || 'f6fdffe48c908deb0f4c3bd36c032e72',
992
1360
  Permission: perms,
@@ -997,7 +1365,10 @@ module.exports = {
997
1365
  });
998
1366
  }
999
1367
 
1368
+ // clear all cached permission control (user permissions)
1369
+ m.clearCachedPermission(app);
1370
+
1000
1371
  // TODO: remove service list which are in the white list
1001
1372
  }
1002
1373
  }
1003
- }
1374
+ })
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "free-be-account",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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
+ "axios": "^1.3.4",
8
9
  "bcrypt": "^5.0.1",
9
- "crypto-js": "^4.0.0",
10
+ "crypto-js": "^4.1.1",
10
11
  "js-md5": "^0.7.3",
12
+ "nodemailer": "^6.9.1",
11
13
  "passport": "^0.5.0",
12
14
  "passport-local": "^1.0.0",
15
+ "svg-captcha": "^1.4.0",
13
16
  "uuid": "^8.3.2"
14
17
  }
15
18
  }
@@ -0,0 +1,18 @@
1
+ const path = require('path');
2
+ const axios = require('axios');
3
+
4
+ const { wx } = require(path.resolve('./global'));
5
+
6
+ const wxAgent = axios.create({
7
+ baseURL: 'https://api.weixin.qq.com',
8
+ timeout: 30 * 1000,
9
+ });
10
+
11
+ module.exports = {
12
+ code2session: (code, mp) => {
13
+ const appid = (mp && wx[mp] && wx[mp].appid) || wx.appid;
14
+ const secret = (mp && wx[mp] && wx[mp].secret) || wx.secret;
15
+
16
+ return wxAgent.get(`/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code`);
17
+ },
18
+ }
@@ -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,20 +59,28 @@ 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];
@@ -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);
@@ -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