feat: 支持管理 token

This commit is contained in:
xream 2024-11-04 13:59:57 +08:00
parent c8c22c3901
commit 2b60c515cd
No known key found for this signature in database
GPG Key ID: 1D2C5225471789F9
5 changed files with 192 additions and 153 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.412", "version": "2.14.413",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {

View File

@ -10,6 +10,7 @@ import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections'; import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts'; import registerArtifactRoutes from './artifacts';
import registerFileRoutes from './file'; import registerFileRoutes from './file';
import registerTokenRoutes from './token';
import registerModuleRoutes from './module'; import registerModuleRoutes from './module';
import registerSyncRoutes from './sync'; import registerSyncRoutes from './sync';
import registerDownloadRoutes from './download'; import registerDownloadRoutes from './download';
@ -37,6 +38,7 @@ export default function serve() {
registerSettingRoutes($app); registerSettingRoutes($app);
registerArtifactRoutes($app); registerArtifactRoutes($app);
registerFileRoutes($app); registerFileRoutes($app);
registerTokenRoutes($app);
registerModuleRoutes($app); registerModuleRoutes($app);
registerSyncRoutes($app); registerSyncRoutes($app);
registerNodeInfoRoutes($app); registerNodeInfoRoutes($app);

View File

@ -9,10 +9,6 @@ import {
GIST_BACKUP_FILE_NAME, GIST_BACKUP_FILE_NAME,
GIST_BACKUP_KEY, GIST_BACKUP_KEY,
SETTINGS_KEY, SETTINGS_KEY,
TOKENS_KEY,
FILES_KEY,
COLLECTIONS_KEY,
SUBS_KEY,
} from '@/constants'; } from '@/constants';
import { InternalServerError, RequestInvalidError } from '@/restful/errors'; import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist'; 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/env', getEnv); // get runtime environment
$app.get('/api/utils/backup', gistBackup); // gist backup actions $app.get('/api/utils/backup', gistBackup); // gist backup actions
$app.get('/api/utils/refresh', refresh); $app.get('/api/utils/refresh', refresh);
$app.post('/api/token', signToken);
// Storage management // Storage management
$app.route('/api/storage') $app.route('/api/storage')
@ -76,145 +71,6 @@ function getEnv(req, res) {
success(res, env); 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) { async function refresh(_, res) {
// 1. get GitHub avatar and artifact store // 1. get GitHub avatar and artifact store
await updateAvatar(); await updateAvatar();

View File

@ -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}`,
),
);
}
}

View File

@ -1,17 +1,17 @@
export function findByName(list, name) { export function findByName(list, name, field = 'name') {
return list.find((item) => item.name === name); return list.find((item) => item[field] === name);
} }
export function findIndexByName(list, name) { export function findIndexByName(list, name, field = 'name') {
return list.findIndex((item) => item.name === name); return list.findIndex((item) => item[field] === name);
} }
export function deleteByName(list, name) { export function deleteByName(list, name, field = 'name') {
const idx = findIndexByName(list, name); const idx = findIndexByName(list, name, field);
list.splice(idx, 1); list.splice(idx, 1);
} }
export function updateByName(list, name, newItem) { export function updateByName(list, name, newItem, field = 'name') {
const idx = findIndexByName(list, name); const idx = findIndexByName(list, name, field);
list[idx] = newItem; list[idx] = newItem;
} }