@waline/vercel
Version:
vercel server for waline comment system
217 lines (177 loc) • 5.61 kB
JavaScript
const path = require('node:path');
const qs = require('node:querystring');
const jwt = require('jsonwebtoken');
module.exports = class BaseLogic extends think.Logic {
constructor(...args) {
super(...args);
this.modelInstance = this.getModel('Users');
this.resource = this.getResource();
this.id = this.getId();
}
// oxlint-disable-next-line max-statements
async __before() {
const referrerCheckResult = this.referrerCheck();
if (!referrerCheckResult) {
return this.ctx.throw(403);
}
this.ctx.state.userInfo = {};
const { authorization } = this.ctx.req.headers;
const { state } = this.get();
if (!authorization && !state) {
return;
}
const token = state || authorization.replace(/^Bearer /, '');
let userId = '';
try {
userId = jwt.verify(token, think.config('jwtKey'));
} catch (err) {
think.logger.debug(err);
}
if (think.isEmpty(userId) || !think.isString(userId)) {
return;
}
const user = await this.modelInstance.select(
{ objectId: userId, type: ['!=', 'banned'] },
{
field: [
'id',
'email',
'url',
'display_name',
'type',
'avatar',
'2fa',
'label',
...this.ctx.state.oauthServices.map(({ name }) => name),
],
},
);
if (think.isEmpty(user)) {
return;
}
const [userInfo] = user;
let avatarUrl =
userInfo.avatar ||
(await think.service('avatar').stringify({
mail: userInfo.email,
nick: userInfo.display_name,
link: userInfo.url,
}));
const { avatarProxy } = think.config();
if (avatarProxy) {
avatarUrl = `${avatarProxy}?url=${encodeURIComponent(avatarUrl)}`;
}
userInfo.avatar = avatarUrl;
this.ctx.state.userInfo = userInfo;
this.ctx.state.token = token;
}
referrerCheck() {
let { secureDomains } = this.config();
if (!secureDomains) {
return true;
}
const whitelistPath = ['/api/comment/rss'];
if (this.ctx.path && whitelistPath.includes(this.ctx.path)) {
return true;
}
const referrer = this.ctx.referrer(true);
let { origin } = this.ctx;
if (origin) {
try {
const parsedOrigin = new URL(origin);
origin = parsedOrigin.hostname;
} catch (err) {
console.error('Invalid origin format:', origin, err);
}
}
secureDomains = think.isArray(secureDomains) ? secureDomains : [secureDomains];
secureDomains.push('localhost', '127.0.0.1');
secureDomains = [...secureDomains, ...this.ctx.state.oauthServices.map(({ origin }) => origin)];
// 转换可能的正则表达式字符串为正则表达式对象
secureDomains = secureDomains
.map((domain) => {
// 如果是正则表达式字符串,创建一个 RegExp 对象
if (typeof domain === 'string' && domain.startsWith('/') && domain.endsWith('/')) {
try {
return new RegExp(domain.slice(1, -1)); // 去掉斜杠并创建 RegExp 对象
} catch (err) {
console.error('Invalid regex pattern in secureDomains:', domain, err);
return null;
}
}
return domain;
})
.filter(Boolean); // 过滤掉无效的正则表达式
// 有 referrer 检查 referrer,没有则检查 origin
const checking = referrer || origin;
const isSafe = secureDomains.some((domain) =>
think.isFunction(domain.test) ? domain.test(checking) : domain === checking,
);
return isSafe;
}
getResource() {
const filename = this.__filename || __filename;
const last = filename.lastIndexOf(path.sep);
return filename.slice(last + 1, -3);
}
getId() {
const id = this.get('id');
if (id && (think.isString(id) || think.isNumber(id))) {
return id;
}
const last = decodeURIComponent(this.ctx.path.split('/').pop());
if (last !== this.resource && /^([a-z0-9]+,?)*$/i.test(last)) {
return last;
}
return '';
}
async useCaptchaCheck() {
const { RECAPTCHA_V3_SECRET, TURNSTILE_SECRET } = process.env;
const { turnstile, recaptchaV3 } = this.post();
if (TURNSTILE_SECRET) {
return this.useRecaptchaOrTurnstileCheck({
secret: TURNSTILE_SECRET,
token: turnstile,
api: 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
method: 'POST',
});
}
if (RECAPTCHA_V3_SECRET) {
return this.useRecaptchaOrTurnstileCheck({
secret: RECAPTCHA_V3_SECRET,
token: recaptchaV3,
api: 'https://recaptcha.net/recaptcha/api/siteverify',
method: 'GET',
});
}
}
async useRecaptchaOrTurnstileCheck({ secret, token, api, method }) {
if (!secret) {
return;
}
if (!token) {
return this.ctx.throw(403);
}
const query = qs.stringify({
secret,
response: token,
remoteip: this.ctx.ip,
});
const requestUrl = method === 'GET' ? `${api}?${query}` : api;
const options =
method === 'GET'
? {}
: {
method,
headers: {
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
body: query,
};
const response = await fetch(requestUrl, options).then((resp) => resp.json());
if (!response.success) {
think.logger.debug('RecaptchaV3 or Turnstile Result:', JSON.stringify(response, null, '\t'));
return this.ctx.throw(403);
}
}
};