feat: allow logging in with aliases

This commit is contained in:
Viren070
2025-11-05 14:03:08 +00:00
parent 4580e252dc
commit 0eb7de41f3
6 changed files with 77 additions and 17 deletions
+5 -5
View File
@@ -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<string, { uuid: string; password: string }> = {};
const parsed = x.split(',').map((x) => {
const aliases: Map<string, { uuid: string; password: string }> = 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({
@@ -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 () => {
+35
View File
@@ -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();
}
+14 -5
View File
@@ -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];
+15 -4
View File
@@ -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;
+1 -1
View File
@@ -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);
}