mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 07:39:02 +08:00
feat: 支持管理 token
This commit is contained in:
parent
c8c22c3901
commit
2b60c515cd
@ -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": {
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
181
backend/src/restful/token.js
Normal file
181
backend/src/restful/token.js
Normal 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}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user