diff --git a/backend/package.json b/backend/package.json index 86a289be7..20dbb608b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "sub-store", - "version": "2.14.412", + "version": "2.14.413", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "main": "src/main.js", "scripts": { diff --git a/backend/src/restful/index.js b/backend/src/restful/index.js index c863a9697..f1dee5aac 100644 --- a/backend/src/restful/index.js +++ b/backend/src/restful/index.js @@ -10,6 +10,7 @@ import registerSubscriptionRoutes from './subscriptions'; import registerCollectionRoutes from './collections'; import registerArtifactRoutes from './artifacts'; import registerFileRoutes from './file'; +import registerTokenRoutes from './token'; import registerModuleRoutes from './module'; import registerSyncRoutes from './sync'; import registerDownloadRoutes from './download'; @@ -37,6 +38,7 @@ export default function serve() { registerSettingRoutes($app); registerArtifactRoutes($app); registerFileRoutes($app); + registerTokenRoutes($app); registerModuleRoutes($app); registerSyncRoutes($app); registerNodeInfoRoutes($app); diff --git a/backend/src/restful/miscs.js b/backend/src/restful/miscs.js index c7cc868b4..4d90fe668 100644 --- a/backend/src/restful/miscs.js +++ b/backend/src/restful/miscs.js @@ -9,10 +9,6 @@ import { GIST_BACKUP_FILE_NAME, GIST_BACKUP_KEY, SETTINGS_KEY, - TOKENS_KEY, - FILES_KEY, - COLLECTIONS_KEY, - SUBS_KEY, } from '@/constants'; import { InternalServerError, RequestInvalidError } from '@/restful/errors'; import Gist from '@/utils/gist'; @@ -24,7 +20,6 @@ export default function register($app) { $app.get('/api/utils/env', getEnv); // get runtime environment $app.get('/api/utils/backup', gistBackup); // gist backup actions $app.get('/api/utils/refresh', refresh); - $app.post('/api/token', signToken); // Storage management $app.route('/api/storage') @@ -76,145 +71,6 @@ function getEnv(req, res) { success(res, env); } -async function signToken(req, res) { - if (!ENV().isNode) { - return failed( - res, - new RequestInvalidError( - 'INVALID_ENV', - `This endpoint is only available in Node.js environment`, - ), - ); - } - try { - const { payload, options } = req.body; - const ms = eval(`require("ms")`); - let token = payload?.token; - if (token != null) { - if (typeof token !== 'string' || token.length < 1) { - return failed( - res, - new RequestInvalidError( - 'INVALID_CUSTOM_TOKEN', - `Invalid custom token: ${token}`, - ), - ); - } - const tokens = $.read(TOKENS_KEY) || []; - if (tokens.find((t) => t.token === token)) { - return failed( - res, - new RequestInvalidError( - 'DUPLICATE_TOKEN', - `Token ${token} already exists`, - ), - ); - } - } - const type = payload?.type; - const name = payload?.name; - if (!type || !name) - return failed( - res, - new RequestInvalidError( - 'INVALID_PAYLOAD', - `payload type and name are required`, - ), - ); - if (type === 'col') { - const collections = $.read(COLLECTIONS_KEY) || []; - const collection = collections.find((c) => c.name === name); - if (!collection) - return failed( - res, - new RequestInvalidError( - 'INVALID_COLLECTION', - `collection ${name} not found`, - ), - ); - } else if (type === 'file') { - const files = $.read(FILES_KEY) || []; - const file = files.find((f) => f.name === name); - if (!file) - return failed( - res, - new RequestInvalidError( - 'INVALID_FILE', - `file ${name} not found`, - ), - ); - } else if (type === 'sub') { - const subs = $.read(SUBS_KEY) || []; - const sub = subs.find((s) => s.name === name); - if (!sub) - return failed( - res, - new RequestInvalidError( - 'INVALID_SUB', - `sub ${name} not found`, - ), - ); - } else { - return failed( - res, - new RequestInvalidError( - 'INVALID_TYPE', - `type ${name} not supported`, - ), - ); - } - let expiresIn = options?.expiresIn; - if (options?.expiresIn != null) { - expiresIn = ms(options.expiresIn); - if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) { - return failed( - res, - new RequestInvalidError( - 'INVALID_EXPIRES_IN', - `Invalid expiresIn option: ${options.expiresIn}`, - ), - ); - } - } - const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH'); - const nanoid = eval(`require("nanoid")`); - const tokens = $.read(TOKENS_KEY) || []; - // const now = Date.now(); - // for (const key in tokens) { - // const token = tokens[key]; - // if (token.exp != null || token.exp < now) { - // delete tokens[key]; - // } - // } - if (!token) { - do { - token = nanoid.customAlphabet(nanoid.urlAlphabet)(); - } while (tokens.find((t) => t.token === token)); - } - tokens.push({ - ...payload, - token, - createdAt: Date.now(), - expiresIn: expiresIn > 0 ? options?.expiresIn : undefined, - exp: expiresIn > 0 ? Date.now() + expiresIn : undefined, - }); - - $.write(tokens, TOKENS_KEY); - return success(res, { - token, - secret, - }); - } catch (e) { - return failed( - res, - new InternalServerError( - 'TOKEN_SIGN_FAILED', - `Failed to sign token`, - `Reason: ${e.message ?? e}`, - ), - ); - } -} async function refresh(_, res) { // 1. get GitHub avatar and artifact store await updateAvatar(); diff --git a/backend/src/restful/token.js b/backend/src/restful/token.js new file mode 100644 index 000000000..bd03126f8 --- /dev/null +++ b/backend/src/restful/token.js @@ -0,0 +1,181 @@ +import { deleteByName } from '@/utils/database'; +import { ENV } from '@/vendor/open-api'; +import { TOKENS_KEY, SUBS_KEY, FILES_KEY, COLLECTIONS_KEY } from '@/constants'; +import { failed, success } from '@/restful/response'; +import $ from '@/core/app'; +import { RequestInvalidError, InternalServerError } from '@/restful/errors'; + +export default function register($app) { + if (!$.read(TOKENS_KEY)) $.write([], TOKENS_KEY); + + $app.post('/api/token', signToken); + + $app.route('/api/token/:token').delete(deleteToken); + + $app.route('/api/tokens').get(getAllTokens); +} + +function deleteToken(req, res) { + let { token } = req.params; + token = decodeURIComponent(token); + $.info(`正在删除:${token}`); + let allTokens = $.read(TOKENS_KEY); + deleteByName(allTokens, token, 'token'); + $.write(allTokens, TOKENS_KEY); + success(res); +} + +function getAllTokens(req, res) { + const { type, name } = req.query; + const allTokens = $.read(TOKENS_KEY) || []; + success( + res, + type || name + ? allTokens.filter( + (item) => + (type ? item.type === type : true) && + (name ? item.name === name : true), + ) + : allTokens, + ); +} + +async function signToken(req, res) { + if (!ENV().isNode) { + return failed( + res, + new RequestInvalidError( + 'INVALID_ENV', + `This endpoint is only available in Node.js environment`, + ), + ); + } + try { + const { payload, options } = req.body; + const ms = eval(`require("ms")`); + let token = payload?.token; + if (token != null) { + if (typeof token !== 'string' || token.length < 1) { + return failed( + res, + new RequestInvalidError( + 'INVALID_CUSTOM_TOKEN', + `Invalid custom token: ${token}`, + ), + ); + } + const tokens = $.read(TOKENS_KEY) || []; + if (tokens.find((t) => t.token === token)) { + return failed( + res, + new RequestInvalidError( + 'DUPLICATE_TOKEN', + `Token ${token} already exists`, + ), + ); + } + } + const type = payload?.type; + const name = payload?.name; + if (!type || !name) + return failed( + res, + new RequestInvalidError( + 'INVALID_PAYLOAD', + `payload type and name are required`, + ), + ); + if (type === 'col') { + const collections = $.read(COLLECTIONS_KEY) || []; + const collection = collections.find((c) => c.name === name); + if (!collection) + return failed( + res, + new RequestInvalidError( + 'INVALID_COLLECTION', + `collection ${name} not found`, + ), + ); + } else if (type === 'file') { + const files = $.read(FILES_KEY) || []; + const file = files.find((f) => f.name === name); + if (!file) + return failed( + res, + new RequestInvalidError( + 'INVALID_FILE', + `file ${name} not found`, + ), + ); + } else if (type === 'sub') { + const subs = $.read(SUBS_KEY) || []; + const sub = subs.find((s) => s.name === name); + if (!sub) + return failed( + res, + new RequestInvalidError( + 'INVALID_SUB', + `sub ${name} not found`, + ), + ); + } else { + return failed( + res, + new RequestInvalidError( + 'INVALID_TYPE', + `type ${name} not supported`, + ), + ); + } + let expiresIn = options?.expiresIn; + if (options?.expiresIn != null) { + expiresIn = ms(options.expiresIn); + if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) { + return failed( + res, + new RequestInvalidError( + 'INVALID_EXPIRES_IN', + `Invalid expiresIn option: ${options.expiresIn}`, + ), + ); + } + } + const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH'); + const nanoid = eval(`require("nanoid")`); + const tokens = $.read(TOKENS_KEY) || []; + // const now = Date.now(); + // for (const key in tokens) { + // const token = tokens[key]; + // if (token.exp != null || token.exp < now) { + // delete tokens[key]; + // } + // } + if (!token) { + do { + token = nanoid.customAlphabet(nanoid.urlAlphabet)(); + } while (tokens.find((t) => t.token === token)); + } + tokens.push({ + ...payload, + token, + createdAt: Date.now(), + expiresIn: expiresIn > 0 ? options?.expiresIn : undefined, + exp: expiresIn > 0 ? Date.now() + expiresIn : undefined, + }); + + $.write(tokens, TOKENS_KEY); + return success(res, { + token, + secret, + }); + } catch (e) { + return failed( + res, + new InternalServerError( + 'TOKEN_SIGN_FAILED', + `Failed to sign token`, + `Reason: ${e.message ?? e}`, + ), + ); + } +} diff --git a/backend/src/utils/database.js b/backend/src/utils/database.js index 58f04c395..9786046b1 100644 --- a/backend/src/utils/database.js +++ b/backend/src/utils/database.js @@ -1,17 +1,17 @@ -export function findByName(list, name) { - return list.find((item) => item.name === name); +export function findByName(list, name, field = 'name') { + return list.find((item) => item[field] === name); } -export function findIndexByName(list, name) { - return list.findIndex((item) => item.name === name); +export function findIndexByName(list, name, field = 'name') { + return list.findIndex((item) => item[field] === name); } -export function deleteByName(list, name) { - const idx = findIndexByName(list, name); +export function deleteByName(list, name, field = 'name') { + const idx = findIndexByName(list, name, field); list.splice(idx, 1); } -export function updateByName(list, name, newItem) { - const idx = findIndexByName(list, name); +export function updateByName(list, name, newItem, field = 'name') { + const idx = findIndexByName(list, name, field); list[idx] = newItem; }