diff --git a/packages/core/src/utils/env.ts b/packages/core/src/utils/env.ts index 15d4944d..85b478fe 100644 --- a/packages/core/src/utils/env.ts +++ b/packages/core/src/utils/env.ts @@ -181,10 +181,10 @@ const userAgent = makeValidator((x) => { }); // comma separated list of alias:uuid -const aliasedUUIDs = makeValidator((x) => { +const aliasedUUIDs = makeExactValidator((x) => { try { - const aliases: Record = {}; - const parsed = x.split(',').map((x) => { + const aliases: Map = new Map(); + x.split(',').forEach((x) => { const [alias, uuid, password] = x.split(':'); if (!alias || !uuid || !password) { throw new Error('Invalid alias:uuid:password pair'); @@ -195,7 +195,7 @@ const aliasedUUIDs = makeValidator((x) => { ) { throw new Error('Invalid UUID'); } - aliases[alias] = { uuid, password }; + aliases.set(alias, { uuid, password }); }); return aliases; } catch (e) { @@ -393,7 +393,7 @@ export const Env = cleanEnv(process.env, { desc: 'Mapping of URLs to another, converts requests to the original URL to the mapped URL', }), ALIASED_CONFIGURATIONS: aliasedUUIDs({ - default: {}, + default: new Map(), desc: 'Comma separated list of alias:uuid:encryptedPassword pairs. Can then access at /stremio/u/alias/manifest.json ', }), TRUSTED_UUIDS: str({ diff --git a/packages/frontend/src/components/menu/save-install.tsx b/packages/frontend/src/components/menu/save-install.tsx index 6c3cfd41..4a627cd0 100644 --- a/packages/frontend/src/components/menu/save-install.tsx +++ b/packages/frontend/src/components/menu/save-install.tsx @@ -300,8 +300,13 @@ function Content() { toast.error('Failed to export configuration'); } }; - - const manifestUrl = `${baseUrl}/stremio/${uuid}/${encryptedPassword}/manifest.json`; + const uuidRegex = + /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i; + const manifestUrl = uuid + ? uuidRegex.test(uuid) + ? `${baseUrl}/stremio/${uuid}/${encryptedPassword}/manifest.json` + : `${baseUrl}/stremio/u/${uuid}/manifest.json` + : ''; const encodedManifest = encodeURIComponent(manifestUrl); const copyManifestUrl = async () => { diff --git a/packages/server/src/middlewares/alias.ts b/packages/server/src/middlewares/alias.ts new file mode 100644 index 00000000..7bb269cc --- /dev/null +++ b/packages/server/src/middlewares/alias.ts @@ -0,0 +1,35 @@ +import { Request, Response, NextFunction } from 'express'; +import { Env } from '@aiostreams/core'; + +// Resolves alias to UUID for user API routes. +// If the provided value is not a UUID and matches a known alias, replaces it with the real UUID. +export function resolveUuidAliasForUserApi( + req: Request, + res: Response, + next: NextFunction +) { + const uuidRegex = + /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i; + + const method = req.method.toUpperCase(); + + if (method === 'GET' || method === 'HEAD') { + const value = req.query.uuid; + if (typeof value === 'string' && !uuidRegex.test(value)) { + const configuration = Env.ALIASED_CONFIGURATIONS.get(value); + if (configuration?.uuid) { + req.uuid = configuration?.uuid; + } + } + } else if (method === 'PUT' || method === 'DELETE') { + const value = (req.body ?? {}).uuid; + if (typeof value === 'string' && !uuidRegex.test(value)) { + const configuration = Env.ALIASED_CONFIGURATIONS.get(value); + if (configuration?.uuid) { + req.uuid = configuration.uuid; + } + } + } + + next(); +} diff --git a/packages/server/src/middlewares/userData.ts b/packages/server/src/middlewares/userData.ts index a75b3643..35bc9a37 100644 --- a/packages/server/src/middlewares/userData.ts +++ b/packages/server/src/middlewares/userData.ts @@ -8,6 +8,7 @@ import { Resource, StremioTransformer, UserRepository, + Env, } from '@aiostreams/core'; const logger = createLogger('server'); @@ -20,10 +21,10 @@ export const userDataMiddleware = async ( res: Response, next: NextFunction ) => { - const { uuid, encryptedPassword } = req.params; + const { uuid: uuidOrAlias, encryptedPassword } = req.params; // Both uuid and encryptedPassword should be present since we mounted the router on this path - if (!uuid || !encryptedPassword) { + if (!uuidOrAlias || !encryptedPassword) { next(new APIError(constants.ErrorCode.USER_INVALID_DETAILS)); return; } @@ -37,11 +38,19 @@ export const userDataMiddleware = async ( } // Second check - validate UUID format (simpler regex that just checks UUID format) + let uuid: string | undefined; const uuidRegex = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i; - if (!uuidRegex.test(uuid)) { - next(new APIError(constants.ErrorCode.USER_INVALID_DETAILS)); - return; + if (!uuidRegex.test(uuidOrAlias)) { + const alias = Env.ALIASED_CONFIGURATIONS.get(uuidOrAlias); + if (alias) { + uuid = alias.uuid; + } else { + next(new APIError(constants.ErrorCode.USER_INVALID_DETAILS)); + return; + } + } else { + uuid = uuidOrAlias; } const resource = resourceMatch[1]; diff --git a/packages/server/src/routes/api/user.ts b/packages/server/src/routes/api/user.ts index a97103c9..6595ca84 100644 --- a/packages/server/src/routes/api/user.ts +++ b/packages/server/src/routes/api/user.ts @@ -7,16 +7,18 @@ import { UserRepository, } from '@aiostreams/core'; import { userApiRateLimiter } from '../../middlewares/ratelimit.js'; +import { resolveUuidAliasForUserApi } from '../../middlewares/alias.js'; import { createResponse } from '../../utils/responses.js'; const router: Router = Router(); const logger = createLogger('server'); router.use(userApiRateLimiter); +router.use(resolveUuidAliasForUserApi); // checking existence of a user router.head('/', async (req, res, next) => { - const { uuid } = req.query; + const uuid = req.uuid || req.query.uuid; if (typeof uuid !== 'string') { next( new APIError( @@ -55,7 +57,10 @@ router.head('/', async (req, res, next) => { // getting user details router.get('/', async (req, res, next) => { - const { uuid, password } = req.query; + const { uuid, password } = { + uuid: req.uuid || req.query.uuid, + password: req.query.password, + }; if (typeof uuid !== 'string' || typeof password !== 'string') { next( new APIError( @@ -144,7 +149,10 @@ router.post('/', async (req, res, next) => { // updating user details router.put('/', async (req, res, next) => { - const { uuid, password, config } = req.body; + const { uuid, password, config } = { + ...req.body, + uuid: req.uuid || req.body.uuid, + }; if (!uuid || !password || !config) { next( new APIError( @@ -180,7 +188,10 @@ router.put('/', async (req, res, next) => { }); router.delete('/', async (req, res, next) => { - const { uuid, password } = req.body; + const { uuid, password } = { + ...req.body, + uuid: req.uuid || req.body.uuid, + }; if (!uuid || !password) { next(new APIError(constants.ErrorCode.MISSING_REQUIRED_FIELDS)); return; diff --git a/packages/server/src/routes/stremio/alias.ts b/packages/server/src/routes/stremio/alias.ts index a9aaf554..2fd402f7 100644 --- a/packages/server/src/routes/stremio/alias.ts +++ b/packages/server/src/routes/stremio/alias.ts @@ -18,7 +18,7 @@ router.get( wildcardPath = wildcardPath.join('/'); } - const configuration = Env.ALIASED_CONFIGURATIONS[alias]; + const configuration = Env.ALIASED_CONFIGURATIONS.get(alias); if (!configuration || !configuration.uuid || !configuration.password) { throw new APIError(constants.ErrorCode.USER_INVALID_DETAILS); }