665 lines
17 KiB
JavaScript
665 lines
17 KiB
JavaScript
import { Sequelize, DataTypes, Model, Op } from 'sequelize';
|
|
import require$$0 from 'node:fs';
|
|
import require$$1 from 'node:path';
|
|
import require$$2 from 'node:os';
|
|
import require$$3 from 'node:crypto';
|
|
import { App } from '@kevisual/router';
|
|
import { Redis } from 'ioredis';
|
|
import { useContextKey } from '@kevisual/use-config/context';
|
|
import { SimpleRouter } from '@kevisual/router/simple';
|
|
|
|
function getDefaultExportFromCjs (x) {
|
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
}
|
|
|
|
var main = {exports: {}};
|
|
|
|
var version = "16.4.7";
|
|
var require$$4 = {
|
|
version: version};
|
|
|
|
var hasRequiredMain;
|
|
|
|
function requireMain () {
|
|
if (hasRequiredMain) return main.exports;
|
|
hasRequiredMain = 1;
|
|
const fs = require$$0;
|
|
const path = require$$1;
|
|
const os = require$$2;
|
|
const crypto = require$$3;
|
|
const packageJson = require$$4;
|
|
|
|
const version = packageJson.version;
|
|
|
|
const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
|
|
|
|
// Parse src into an Object
|
|
function parse (src) {
|
|
const obj = {};
|
|
|
|
// Convert buffer to string
|
|
let lines = src.toString();
|
|
|
|
// Convert line breaks to same format
|
|
lines = lines.replace(/\r\n?/mg, '\n');
|
|
|
|
let match;
|
|
while ((match = LINE.exec(lines)) != null) {
|
|
const key = match[1];
|
|
|
|
// Default undefined or null to empty string
|
|
let value = (match[2] || '');
|
|
|
|
// Remove whitespace
|
|
value = value.trim();
|
|
|
|
// Check if double quoted
|
|
const maybeQuote = value[0];
|
|
|
|
// Remove surrounding quotes
|
|
value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2');
|
|
|
|
// Expand newlines if double quoted
|
|
if (maybeQuote === '"') {
|
|
value = value.replace(/\\n/g, '\n');
|
|
value = value.replace(/\\r/g, '\r');
|
|
}
|
|
|
|
// Add to object
|
|
obj[key] = value;
|
|
}
|
|
|
|
return obj
|
|
}
|
|
|
|
function _parseVault (options) {
|
|
const vaultPath = _vaultPath(options);
|
|
|
|
// Parse .env.vault
|
|
const result = DotenvModule.configDotenv({ path: vaultPath });
|
|
if (!result.parsed) {
|
|
const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
|
|
err.code = 'MISSING_DATA';
|
|
throw err
|
|
}
|
|
|
|
// handle scenario for comma separated keys - for use with key rotation
|
|
// example: DOTENV_KEY="dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod"
|
|
const keys = _dotenvKey(options).split(',');
|
|
const length = keys.length;
|
|
|
|
let decrypted;
|
|
for (let i = 0; i < length; i++) {
|
|
try {
|
|
// Get full key
|
|
const key = keys[i].trim();
|
|
|
|
// Get instructions for decrypt
|
|
const attrs = _instructions(result, key);
|
|
|
|
// Decrypt
|
|
decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
|
|
|
|
break
|
|
} catch (error) {
|
|
// last key
|
|
if (i + 1 >= length) {
|
|
throw error
|
|
}
|
|
// try next key
|
|
}
|
|
}
|
|
|
|
// Parse decrypted .env string
|
|
return DotenvModule.parse(decrypted)
|
|
}
|
|
|
|
function _log (message) {
|
|
console.log(`[dotenv@${version}][INFO] ${message}`);
|
|
}
|
|
|
|
function _warn (message) {
|
|
console.log(`[dotenv@${version}][WARN] ${message}`);
|
|
}
|
|
|
|
function _debug (message) {
|
|
console.log(`[dotenv@${version}][DEBUG] ${message}`);
|
|
}
|
|
|
|
function _dotenvKey (options) {
|
|
// prioritize developer directly setting options.DOTENV_KEY
|
|
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
|
|
return options.DOTENV_KEY
|
|
}
|
|
|
|
// secondary infra already contains a DOTENV_KEY environment variable
|
|
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
|
return process.env.DOTENV_KEY
|
|
}
|
|
|
|
// fallback to empty string
|
|
return ''
|
|
}
|
|
|
|
function _instructions (result, dotenvKey) {
|
|
// Parse DOTENV_KEY. Format is a URI
|
|
let uri;
|
|
try {
|
|
uri = new URL(dotenvKey);
|
|
} catch (error) {
|
|
if (error.code === 'ERR_INVALID_URL') {
|
|
const err = new Error('INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development');
|
|
err.code = 'INVALID_DOTENV_KEY';
|
|
throw err
|
|
}
|
|
|
|
throw error
|
|
}
|
|
|
|
// Get decrypt key
|
|
const key = uri.password;
|
|
if (!key) {
|
|
const err = new Error('INVALID_DOTENV_KEY: Missing key part');
|
|
err.code = 'INVALID_DOTENV_KEY';
|
|
throw err
|
|
}
|
|
|
|
// Get environment
|
|
const environment = uri.searchParams.get('environment');
|
|
if (!environment) {
|
|
const err = new Error('INVALID_DOTENV_KEY: Missing environment part');
|
|
err.code = 'INVALID_DOTENV_KEY';
|
|
throw err
|
|
}
|
|
|
|
// Get ciphertext payload
|
|
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
|
|
const ciphertext = result.parsed[environmentKey]; // DOTENV_VAULT_PRODUCTION
|
|
if (!ciphertext) {
|
|
const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
|
|
err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT';
|
|
throw err
|
|
}
|
|
|
|
return { ciphertext, key }
|
|
}
|
|
|
|
function _vaultPath (options) {
|
|
let possibleVaultPath = null;
|
|
|
|
if (options && options.path && options.path.length > 0) {
|
|
if (Array.isArray(options.path)) {
|
|
for (const filepath of options.path) {
|
|
if (fs.existsSync(filepath)) {
|
|
possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault`;
|
|
}
|
|
}
|
|
} else {
|
|
possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault`;
|
|
}
|
|
} else {
|
|
possibleVaultPath = path.resolve(process.cwd(), '.env.vault');
|
|
}
|
|
|
|
if (fs.existsSync(possibleVaultPath)) {
|
|
return possibleVaultPath
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function _resolveHome (envPath) {
|
|
return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
|
|
}
|
|
|
|
function _configVault (options) {
|
|
_log('Loading env from encrypted .env.vault');
|
|
|
|
const parsed = DotenvModule._parseVault(options);
|
|
|
|
let processEnv = process.env;
|
|
if (options && options.processEnv != null) {
|
|
processEnv = options.processEnv;
|
|
}
|
|
|
|
DotenvModule.populate(processEnv, parsed, options);
|
|
|
|
return { parsed }
|
|
}
|
|
|
|
function configDotenv (options) {
|
|
const dotenvPath = path.resolve(process.cwd(), '.env');
|
|
let encoding = 'utf8';
|
|
const debug = Boolean(options && options.debug);
|
|
|
|
if (options && options.encoding) {
|
|
encoding = options.encoding;
|
|
} else {
|
|
if (debug) {
|
|
_debug('No encoding is specified. UTF-8 is used by default');
|
|
}
|
|
}
|
|
|
|
let optionPaths = [dotenvPath]; // default, look for .env
|
|
if (options && options.path) {
|
|
if (!Array.isArray(options.path)) {
|
|
optionPaths = [_resolveHome(options.path)];
|
|
} else {
|
|
optionPaths = []; // reset default
|
|
for (const filepath of options.path) {
|
|
optionPaths.push(_resolveHome(filepath));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build the parsed data in a temporary object (because we need to return it). Once we have the final
|
|
// parsed data, we will combine it with process.env (or options.processEnv if provided).
|
|
let lastError;
|
|
const parsedAll = {};
|
|
for (const path of optionPaths) {
|
|
try {
|
|
// Specifying an encoding returns a string instead of a buffer
|
|
const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }));
|
|
|
|
DotenvModule.populate(parsedAll, parsed, options);
|
|
} catch (e) {
|
|
if (debug) {
|
|
_debug(`Failed to load ${path} ${e.message}`);
|
|
}
|
|
lastError = e;
|
|
}
|
|
}
|
|
|
|
let processEnv = process.env;
|
|
if (options && options.processEnv != null) {
|
|
processEnv = options.processEnv;
|
|
}
|
|
|
|
DotenvModule.populate(processEnv, parsedAll, options);
|
|
|
|
if (lastError) {
|
|
return { parsed: parsedAll, error: lastError }
|
|
} else {
|
|
return { parsed: parsedAll }
|
|
}
|
|
}
|
|
|
|
// Populates process.env from .env file
|
|
function config (options) {
|
|
// fallback to original dotenv if DOTENV_KEY is not set
|
|
if (_dotenvKey(options).length === 0) {
|
|
return DotenvModule.configDotenv(options)
|
|
}
|
|
|
|
const vaultPath = _vaultPath(options);
|
|
|
|
// dotenvKey exists but .env.vault file does not exist
|
|
if (!vaultPath) {
|
|
_warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`);
|
|
|
|
return DotenvModule.configDotenv(options)
|
|
}
|
|
|
|
return DotenvModule._configVault(options)
|
|
}
|
|
|
|
function decrypt (encrypted, keyStr) {
|
|
const key = Buffer.from(keyStr.slice(-64), 'hex');
|
|
let ciphertext = Buffer.from(encrypted, 'base64');
|
|
|
|
const nonce = ciphertext.subarray(0, 12);
|
|
const authTag = ciphertext.subarray(-16);
|
|
ciphertext = ciphertext.subarray(12, -16);
|
|
|
|
try {
|
|
const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce);
|
|
aesgcm.setAuthTag(authTag);
|
|
return `${aesgcm.update(ciphertext)}${aesgcm.final()}`
|
|
} catch (error) {
|
|
const isRange = error instanceof RangeError;
|
|
const invalidKeyLength = error.message === 'Invalid key length';
|
|
const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data';
|
|
|
|
if (isRange || invalidKeyLength) {
|
|
const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)');
|
|
err.code = 'INVALID_DOTENV_KEY';
|
|
throw err
|
|
} else if (decryptionFailed) {
|
|
const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY');
|
|
err.code = 'DECRYPTION_FAILED';
|
|
throw err
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
// Populate process.env with parsed values
|
|
function populate (processEnv, parsed, options = {}) {
|
|
const debug = Boolean(options && options.debug);
|
|
const override = Boolean(options && options.override);
|
|
|
|
if (typeof parsed !== 'object') {
|
|
const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate');
|
|
err.code = 'OBJECT_REQUIRED';
|
|
throw err
|
|
}
|
|
|
|
// Set process.env
|
|
for (const key of Object.keys(parsed)) {
|
|
if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
|
|
if (override === true) {
|
|
processEnv[key] = parsed[key];
|
|
}
|
|
|
|
if (debug) {
|
|
if (override === true) {
|
|
_debug(`"${key}" is already defined and WAS overwritten`);
|
|
} else {
|
|
_debug(`"${key}" is already defined and was NOT overwritten`);
|
|
}
|
|
}
|
|
} else {
|
|
processEnv[key] = parsed[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
const DotenvModule = {
|
|
configDotenv,
|
|
_configVault,
|
|
_parseVault,
|
|
config,
|
|
decrypt,
|
|
parse,
|
|
populate
|
|
};
|
|
|
|
main.exports.configDotenv = DotenvModule.configDotenv;
|
|
main.exports._configVault = DotenvModule._configVault;
|
|
main.exports._parseVault = DotenvModule._parseVault;
|
|
main.exports.config = DotenvModule.config;
|
|
main.exports.decrypt = DotenvModule.decrypt;
|
|
main.exports.parse = DotenvModule.parse;
|
|
main.exports.populate = DotenvModule.populate;
|
|
|
|
main.exports = DotenvModule;
|
|
return main.exports;
|
|
}
|
|
|
|
var mainExports = requireMain();
|
|
var dotenv = /*@__PURE__*/getDefaultExportFromCjs(mainExports);
|
|
|
|
const config = dotenv.config().parsed;
|
|
config.PORT || 4005;
|
|
|
|
if (!config.POSTGRES_PASSWORD || !config.POSTGRES_USER) {
|
|
console.error("postgres config is required password and user");
|
|
process.exit(1);
|
|
}
|
|
const postgresConfig = {
|
|
username: config.POSTGRES_USER,
|
|
password: config.POSTGRES_PASSWORD,
|
|
host: config.POSTGRES_HOST || "localhost",
|
|
port: parseInt(config.POSTGRES_PORT || "5432"),
|
|
database: config.POSTGRES_DB || "postgres"
|
|
};
|
|
const sequelize$1 = new Sequelize({
|
|
dialect: "postgres",
|
|
...postgresConfig
|
|
// logging: false,
|
|
});
|
|
|
|
var VipLevel = /* @__PURE__ */ ((VipLevel2) => {
|
|
VipLevel2["FREE"] = "free";
|
|
VipLevel2["LOVE"] = "love";
|
|
VipLevel2["VIP"] = "vip";
|
|
return VipLevel2;
|
|
})(VipLevel || {});
|
|
var VipCategory = /* @__PURE__ */ ((VipCategory2) => {
|
|
VipCategory2["CENTER"] = "center";
|
|
VipCategory2["AI_CHAT"] = "AI Chat";
|
|
return VipCategory2;
|
|
})(VipCategory || {});
|
|
class VipModel extends Model {
|
|
}
|
|
VipModel.init(
|
|
{
|
|
id: {
|
|
type: DataTypes.UUID,
|
|
primaryKey: true,
|
|
defaultValue: DataTypes.UUIDV4
|
|
},
|
|
userId: {
|
|
type: DataTypes.UUID,
|
|
allowNull: false
|
|
},
|
|
title: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false,
|
|
defaultValue: ""
|
|
},
|
|
description: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: false,
|
|
defaultValue: ""
|
|
},
|
|
level: {
|
|
type: DataTypes.STRING,
|
|
defaultValue: "free" /* FREE */
|
|
},
|
|
category: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false
|
|
},
|
|
startDate: {
|
|
type: DataTypes.DATE,
|
|
defaultValue: DataTypes.NOW
|
|
},
|
|
endDate: {
|
|
type: DataTypes.DATE,
|
|
allowNull: true
|
|
},
|
|
data: {
|
|
type: DataTypes.JSONB,
|
|
defaultValue: {}
|
|
}
|
|
},
|
|
{
|
|
sequelize: sequelize$1,
|
|
tableName: "kv_vip",
|
|
paranoid: true
|
|
}
|
|
);
|
|
VipModel.sync({ alter: true, logging: false }).catch((e) => {
|
|
console.error("VipModel sync", e);
|
|
});
|
|
|
|
const init$1 = () => new Redis({
|
|
host: "localhost",
|
|
// Redis 服务器的主机名或 IP 地址
|
|
port: 6379,
|
|
// Redis 服务器的端口号
|
|
// password: 'your_password', // Redis 的密码 (如果有)
|
|
db: 0,
|
|
// 要使用的 Redis 数据库索引 (0-15)
|
|
keyPrefix: "",
|
|
// key 前缀
|
|
retryStrategy(times) {
|
|
return Math.min(times * 50, 2e3);
|
|
},
|
|
maxRetriesPerRequest: null
|
|
// 允许请求重试的次数 (如果需要无限次重试)
|
|
});
|
|
const redis = useContextKey("redis", init$1);
|
|
redis.on("connect", () => {
|
|
console.log("Redis \u8FDE\u63A5\u6210\u529F");
|
|
});
|
|
redis.on("error", (err) => {
|
|
console.error("Redis \u8FDE\u63A5\u9519\u8BEF", err);
|
|
});
|
|
|
|
useContextKey("router", () => new SimpleRouter());
|
|
useContextKey("redis", () => redis);
|
|
const sequelize = useContextKey("sequelize", () => sequelize$1);
|
|
const init = () => {
|
|
return new App({
|
|
serverOptions: {
|
|
cors: {
|
|
origin: "*"
|
|
}
|
|
// httpType: 'https',
|
|
},
|
|
io: true,
|
|
routerContext: {
|
|
sequelize
|
|
}
|
|
});
|
|
};
|
|
const app = useContextKey("app", init);
|
|
|
|
app.route({
|
|
path: "vip",
|
|
key: "me-vip-list",
|
|
middleware: ["auth"]
|
|
}).define(async (ctx) => {
|
|
const tokenUser = ctx.state.tokenUser;
|
|
const vip = await VipModel.findAll({
|
|
where: {
|
|
userId: tokenUser.id
|
|
}
|
|
});
|
|
ctx.body = {
|
|
list: vip
|
|
};
|
|
}).addTo(app);
|
|
app.route({
|
|
path: "vip",
|
|
key: "me",
|
|
middleware: ["auth"]
|
|
}).define(async (ctx) => {
|
|
const category = ctx.query.category;
|
|
const tokenUser = ctx.state.tokenUser;
|
|
if (!category) {
|
|
ctx.throw(400, "category is required");
|
|
}
|
|
const vip = await VipModel.findOrCreate({
|
|
where: {
|
|
category
|
|
},
|
|
defaults: {
|
|
userId: tokenUser.id,
|
|
category
|
|
}
|
|
});
|
|
ctx.body = vip;
|
|
}).addTo(app);
|
|
|
|
app.route({
|
|
path: "vip",
|
|
key: "list",
|
|
middleware: ["auth-admin"]
|
|
}).define(async (ctx) => {
|
|
ctx.state.tokenUser;
|
|
const { page = 1, pageSize = 20, search, sort = "DESC", level, category, userId } = ctx.query;
|
|
const whereCondition = {};
|
|
if (userId) {
|
|
whereCondition.userId = userId;
|
|
}
|
|
if (level) {
|
|
whereCondition.level = level;
|
|
}
|
|
if (category) {
|
|
whereCondition.category = category;
|
|
}
|
|
if (search) {
|
|
whereCondition[Op.or] = [{ title: { [Op.like]: `%${search}%` } }];
|
|
}
|
|
const { rows: vips, count } = await VipModel.findAndCountAll({
|
|
where: whereCondition,
|
|
offset: (page - 1) * pageSize,
|
|
limit: pageSize,
|
|
order: [["updatedAt", sort]]
|
|
});
|
|
ctx.body = {
|
|
list: vips,
|
|
pagination: {
|
|
page,
|
|
current: page,
|
|
pageSize,
|
|
total: count
|
|
},
|
|
levels: Object.values(VipLevel),
|
|
categories: Object.values(VipCategory)
|
|
};
|
|
}).addTo(app);
|
|
app.route({
|
|
path: "vip",
|
|
key: "update",
|
|
middleware: ["auth-admin"]
|
|
}).define(async (ctx) => {
|
|
ctx.state.tokenUser;
|
|
const { id, data, updatedAt: _clear, createdAt: _clear2, ...rest } = ctx.query.data;
|
|
let vip;
|
|
let isNew = false;
|
|
if (id) {
|
|
vip = await VipModel.findByPk(id);
|
|
} else {
|
|
vip = await VipModel.create({
|
|
data: data || {},
|
|
...rest
|
|
// userId: tokenUser.uid,
|
|
});
|
|
isNew = true;
|
|
}
|
|
if (!vip) {
|
|
ctx.throw(404, "VIP record not found");
|
|
}
|
|
if (!isNew) {
|
|
vip = await vip.update({
|
|
data: { ...vip.data, ...data || {} },
|
|
...rest
|
|
});
|
|
}
|
|
ctx.body = vip;
|
|
}).addTo(app);
|
|
app.route({
|
|
path: "vip",
|
|
key: "delete",
|
|
middleware: ["auth-admin"]
|
|
}).define(async (ctx) => {
|
|
const { id, force = false } = ctx.query.data || {};
|
|
if (!id) {
|
|
ctx.throw(400, "id is required");
|
|
}
|
|
const vip = await VipModel.findByPk(id);
|
|
await vip.destroy({ force });
|
|
ctx.body = vip;
|
|
}).addTo(app);
|
|
app.route({
|
|
path: "vip",
|
|
key: "get",
|
|
middleware: ["auth-admin"]
|
|
}).define(async (ctx) => {
|
|
const tokenUser = ctx.state.tokenUser;
|
|
const { id } = ctx.query.data || {};
|
|
if (!id) {
|
|
ctx.throw(400, "id is required");
|
|
}
|
|
const vip = await VipModel.findByPk(id);
|
|
if (!vip || vip.userId !== tokenUser.uid) {
|
|
ctx.throw(403, "No permission");
|
|
}
|
|
ctx.body = vip;
|
|
}).addTo(app);
|
|
app.route({
|
|
path: "vip",
|
|
key: "config",
|
|
middleware: ["auth-admin"]
|
|
}).define(async (ctx) => {
|
|
ctx.body = {
|
|
levels: Object.values(VipLevel),
|
|
categories: Object.values(VipCategory)
|
|
};
|
|
}).addTo(app);
|