free-be-account 0.0.25 → 0.0.27
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 +170 -3
- package/package.json +1 -1
- package/sms/index.js +3 -0
- package/sms/platforms/submail.js +42 -2
package/index.js
CHANGED
|
@@ -451,6 +451,16 @@ module.exports = (app) => ({
|
|
|
451
451
|
Read: { type: 'Boolean', default: false },
|
|
452
452
|
Category: { type: 'String' },
|
|
453
453
|
},
|
|
454
|
+
|
|
455
|
+
// 静态资源访问权限控制
|
|
456
|
+
staticResourcePermissionControl: {
|
|
457
|
+
User: { type: 'String', refer: 'account', required: true }, // 创建用户
|
|
458
|
+
ResourcePath: { type: 'String', index: true, required: true, unique: true }, // 资源路径
|
|
459
|
+
|
|
460
|
+
Users: { type: 'String' }, // 逗号分割的字符串,每一个代表一个允许访问的用户ID, self代表资源创建者本人
|
|
461
|
+
Referers: { type: 'String' }, // 逗号分割的字符串,每一个代表一个允许访问的来源
|
|
462
|
+
Permissions: { type: 'String' }, // 逗号分割的字符串,每一个代表一个api路径
|
|
463
|
+
},
|
|
454
464
|
},
|
|
455
465
|
utils: {
|
|
456
466
|
verify_api_permission,
|
|
@@ -959,7 +969,7 @@ module.exports = (app) => ({
|
|
|
959
969
|
}).then(async (user) => {
|
|
960
970
|
if (!user) {
|
|
961
971
|
// auto create new user
|
|
962
|
-
if (m.config.autoCreateNewUser && await app.modules['account'].verify(username, password)) {
|
|
972
|
+
if (m.config.autoCreateNewUser && await app.modules['account'].sms.verify(username, password)) {
|
|
963
973
|
const valid_phone = (d) => {
|
|
964
974
|
return /^(0|86|17951)?(13[0-9]|14[0-9]|15[0-9]|16[0-9]|17[0-9]|18[0-9]|19[0-9])[0-9]{8}$/.test(d);
|
|
965
975
|
};
|
|
@@ -1106,7 +1116,12 @@ module.exports = (app) => ({
|
|
|
1106
1116
|
// update token in cookies
|
|
1107
1117
|
const token = req.cookies.token;
|
|
1108
1118
|
if (token) {
|
|
1109
|
-
res.cookie('token', token, {
|
|
1119
|
+
res.cookie('token', token, {
|
|
1120
|
+
httpOnly: true, // 防止 XSS 读取
|
|
1121
|
+
secure: true, // 仅 HTTPS 传输
|
|
1122
|
+
sameSite: 'strict', // CSRF 防护
|
|
1123
|
+
maxAge: app.config['cookieTimeout'],
|
|
1124
|
+
});
|
|
1110
1125
|
}
|
|
1111
1126
|
|
|
1112
1127
|
// check for force reset pwd
|
|
@@ -1159,7 +1174,12 @@ module.exports = (app) => ({
|
|
|
1159
1174
|
token = await generate_new_access_token_pwd(app, req.user.id, access_token, null, req.user.isWx);
|
|
1160
1175
|
}
|
|
1161
1176
|
|
|
1162
|
-
res.cookie('token', token, {
|
|
1177
|
+
res.cookie('token', token, {
|
|
1178
|
+
httpOnly: true, // 防止 XSS 读取
|
|
1179
|
+
secure: true, // 仅 HTTPS 传输
|
|
1180
|
+
sameSite: 'strict', // CSRF 防护
|
|
1181
|
+
maxAge: app.config['cookieTimeout'],
|
|
1182
|
+
});
|
|
1163
1183
|
|
|
1164
1184
|
res.addData({
|
|
1165
1185
|
Name: (req.user.Profile && req.user.Profile.Name) || req.user.PhoneNumber || req.user.UserName || '',
|
|
@@ -1550,6 +1570,153 @@ module.exports = (app) => ({
|
|
|
1550
1570
|
}
|
|
1551
1571
|
},
|
|
1552
1572
|
onRoutersReady: async (app, m) => {
|
|
1573
|
+
// file uploads
|
|
1574
|
+
app.post(
|
|
1575
|
+
`${app.config.baseUrl}/upload`,
|
|
1576
|
+
async (req, res, next) => {
|
|
1577
|
+
if (!res.locals.data?.id) return next();
|
|
1578
|
+
if (!req.body?.perms && !req.body?.refs && !req.body?.users) return next();
|
|
1579
|
+
|
|
1580
|
+
// save the permission control info
|
|
1581
|
+
const assetsFilePath = res.locals.data?.id.split(/[\\|/]/g).slice(-2).join('/');
|
|
1582
|
+
await app.models.staticResourcePermissionControl.create({
|
|
1583
|
+
User: req.user?.id,
|
|
1584
|
+
ResourcePath: assetsFilePath,
|
|
1585
|
+
|
|
1586
|
+
Users: req.body?.users || '',
|
|
1587
|
+
Permissions: req.body?.perms || '',
|
|
1588
|
+
Referers: req.body?.refs || '',
|
|
1589
|
+
})
|
|
1590
|
+
|
|
1591
|
+
return next();
|
|
1592
|
+
}
|
|
1593
|
+
);
|
|
1594
|
+
// multiple files upload
|
|
1595
|
+
app.post(
|
|
1596
|
+
`${app.config.baseUrl}/uploads`,
|
|
1597
|
+
async (req, res, next) => {
|
|
1598
|
+
if (!res.locals.data?.length) return next();
|
|
1599
|
+
if (!req.body?.perms && !req.body?.refs && !req.body?.users) return next();
|
|
1600
|
+
|
|
1601
|
+
// save the permission control info
|
|
1602
|
+
for (let i = 0; i < res.loals.data.length; i += 1) {
|
|
1603
|
+
const resi = res.locals.data[i];
|
|
1604
|
+
if (!resi?.id) continue;
|
|
1605
|
+
|
|
1606
|
+
const assetsFilePath = resi?.id.split(/[\\|/]/g).slice(-2).join('/');
|
|
1607
|
+
await app.models.staticResourcePermissionControl.create({
|
|
1608
|
+
User: req.user?.id,
|
|
1609
|
+
ResourcePath: assetsFilePath,
|
|
1610
|
+
|
|
1611
|
+
Users: req.body?.users || '',
|
|
1612
|
+
Permissions: req.body?.perms || '',
|
|
1613
|
+
Referers: req.body?.refs || '',
|
|
1614
|
+
})
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
return next();
|
|
1618
|
+
}
|
|
1619
|
+
);
|
|
1620
|
+
|
|
1621
|
+
// 静态资源身份验证
|
|
1622
|
+
async function checkPerm (req) {
|
|
1623
|
+
// 验证是静态资源请求
|
|
1624
|
+
const assetsPath = req.originalUrl;
|
|
1625
|
+
if (!assetsPath || !assetsPath.startsWith(app.config.assetsUrlPrefix)) {
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// 获取当前资源的权限控制
|
|
1630
|
+
const assetsFilePath = assetsPath.replace(app.config.assetsUrlPrefix, '').split(/[\\|/]/g).slice(-2).join('/');
|
|
1631
|
+
const assetsPerm = await app.models.staticResourcePermissionControl.findOne({ ResourcePath: assetsFilePath }).lean();
|
|
1632
|
+
|
|
1633
|
+
// 如果没有保存对应的权限控制,说明是公开资源
|
|
1634
|
+
if (!assetsPerm || (!assetsPerm.Users && !assetsPerm.Permissions && !assetsPerm.Referers)) {
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// 验证referer。并不完全可靠,但能防一部分无脑盗链
|
|
1639
|
+
// 配置可能太复杂,所以先不需要
|
|
1640
|
+
const neededReferer = assetsPerm.Referers || '';
|
|
1641
|
+
|
|
1642
|
+
// check referrer
|
|
1643
|
+
const referer = req.get('Referer') || '';
|
|
1644
|
+
let refererPath = '/';
|
|
1645
|
+
try {
|
|
1646
|
+
const urlObj = new URL(referer);
|
|
1647
|
+
refererPath = urlObj.pathname;
|
|
1648
|
+
} catch (ex) {
|
|
1649
|
+
refererPath = '/';
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
if (!refererPath || refererPath === '/') {
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const refererMatched = /^\/pdfjs(_.+)?\/web\/viewer.html$/.test(refererPath) || neededReferer.split(/,|,/).map((nr) => new RegExp(nr)).some((reg) => reg.test(refererPath));
|
|
1657
|
+
|
|
1658
|
+
if (!refererMatched) {
|
|
1659
|
+
return false;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// 验证host。并不完全可靠,但能防一部分无脑盗链
|
|
1663
|
+
const host = req.get('Host') || '';
|
|
1664
|
+
|
|
1665
|
+
if (!host || (app.config.host !== host && app.config.host !== `https://${host}` && app.config.host !== `http://${host}`)) {
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// 验证用户
|
|
1670
|
+
const neededUsers = assetsPerm.Users || '';
|
|
1671
|
+
if (neededUsers) {
|
|
1672
|
+
if (!req.user?.id) return false;
|
|
1673
|
+
|
|
1674
|
+
const userList = neededUsers.split(/,|,/) || [];
|
|
1675
|
+
let userMatched = false;
|
|
1676
|
+
for (let i = 0; i < userList.length; i += 1) {
|
|
1677
|
+
const u = userList[i];
|
|
1678
|
+
if ((u.toLowerCase() === 'self' && req.user.id === assetsPerm.User.toString()) || req.user.id === u) {
|
|
1679
|
+
userMatched = true;
|
|
1680
|
+
break;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (!userMatched) {
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// 验证权限
|
|
1690
|
+
const neededPerms = assetsPerm.Permissions || '';
|
|
1691
|
+
const permList = neededPerms.split(/,|,/) || [];
|
|
1692
|
+
if (permList.length <= 0) {
|
|
1693
|
+
return true;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// 验证权限列表
|
|
1697
|
+
if (!req.user?.id) {
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
let hasPerm = false;
|
|
1702
|
+
for (let i = 0; i < permList.length; i += 1) {
|
|
1703
|
+
const p = permList[i];
|
|
1704
|
+
if (verify_api_permission(req.app, m, req.user, p)) {
|
|
1705
|
+
hasPerm = true;
|
|
1706
|
+
break;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
return hasPerm;
|
|
1711
|
+
}
|
|
1712
|
+
app.use(`/assets`, async (req, res, next) => {
|
|
1713
|
+
if (await checkPerm(req)) {
|
|
1714
|
+
return next()
|
|
1715
|
+
} else {
|
|
1716
|
+
res.status(404).end();
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1553
1720
|
// create default user if it's an empty db
|
|
1554
1721
|
if (await m.models['account'].countDocuments({}) <= 0) {
|
|
1555
1722
|
// let perms = app.ctx.serviceList()
|
package/package.json
CHANGED
package/sms/index.js
CHANGED
package/sms/platforms/submail.js
CHANGED
|
@@ -39,11 +39,14 @@ module.exports = {
|
|
|
39
39
|
codeAndValue[k.templateParamName] = v;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const tsResponse = await client.get('/service/timestamp');
|
|
43
|
+
const ts = (tsResponse && tsResponse.data && tsResponse.data.timestamp) || Math.floor(Date.now() / 1000);
|
|
44
|
+
|
|
42
45
|
const requestBody = {
|
|
43
46
|
appid: k.appid, // 在 SUBMAIL 应用集成中创建的短信应用 ID
|
|
44
47
|
to: p, // 收件人手机号码
|
|
45
48
|
project: k.templateCode, // 模版 ID
|
|
46
|
-
timestamp:
|
|
49
|
+
timestamp: `${ts}`, // Timestamp UNIX 时间戳
|
|
47
50
|
sign_type: 'md5', // md5 or sha1 or normal
|
|
48
51
|
sign_version: 2, // signature 加密计算方式(当 sign_version 传 2 时,vars 参数不参与加密计算)
|
|
49
52
|
};
|
|
@@ -66,5 +69,42 @@ module.exports = {
|
|
|
66
69
|
}).catch(() => {
|
|
67
70
|
return false;
|
|
68
71
|
});
|
|
69
|
-
}
|
|
72
|
+
},
|
|
73
|
+
sendMail: async function (k, p, v) {
|
|
74
|
+
if (!k || !k.appid || !k.appkey) {
|
|
75
|
+
throw new Error('Email parameters not configured correctly for platform (Submail)');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const tsResponse = await client.get('/service/timestamp');
|
|
79
|
+
const ts = (tsResponse && tsResponse.data && tsResponse.data.timestamp) || Math.floor(Date.now() / 1000);
|
|
80
|
+
|
|
81
|
+
const requestBody = {
|
|
82
|
+
appid: k.appid, // 在 SUBMAIL 应用集成中创建的邮件应用 ID
|
|
83
|
+
from: k.from, // 发件人邮箱地址
|
|
84
|
+
to: p, // 收件人邮箱地址
|
|
85
|
+
timestamp: `${ts}`, // Timestamp UNIX 时间戳
|
|
86
|
+
sign_type: 'md5', // md5 or sha1 or normal
|
|
87
|
+
sign_version: 2, // signature 加密计算方式(当 sign_version 传 2 时,vars 参数不参与加密计算)
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const signature = sign(k.appid, k.appkey, requestBody);
|
|
91
|
+
|
|
92
|
+
return await client.post('/mail/send', {
|
|
93
|
+
...requestBody,
|
|
94
|
+
subject: typeof k.title === 'function' ? k.title(v) : k.title, // 邮件标题
|
|
95
|
+
html: typeof k.template === 'function' ? k.template(v) : k.template, // 邮件 HTML 内容
|
|
96
|
+
signature, // 应用密匙或数字签名
|
|
97
|
+
}).then(({data}) => {
|
|
98
|
+
if (data.status === 'success') {
|
|
99
|
+
return true;
|
|
100
|
+
} else {
|
|
101
|
+
console.error('Email send error:', data.msg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}).catch((error) => {
|
|
106
|
+
console.error('Email send exception:', error);
|
|
107
|
+
return false;
|
|
108
|
+
});
|
|
109
|
+
},
|
|
70
110
|
};
|