mirror of
https://github.com/agregarr/agregarr.git
synced 2026-03-30 00:02:40 +08:00
722 lines
24 KiB
TypeScript
722 lines
24 KiB
TypeScript
import OverseerrAPI, { type OverseerrUser } from '@server/api/overseerr';
|
|
import { getRepository } from '@server/datasource';
|
|
import { User } from '@server/entity/User';
|
|
import { getSettings } from '@server/lib/settings';
|
|
import logger from '@server/logger';
|
|
import { AxiosError } from 'axios';
|
|
import { randomUUID } from 'crypto';
|
|
|
|
/**
|
|
* Extract error details from axios errors or regular errors for logging
|
|
*/
|
|
function getErrorDetails(error: unknown): {
|
|
status?: number;
|
|
data?: unknown;
|
|
message: string;
|
|
stack?: string;
|
|
} {
|
|
if (error instanceof AxiosError) {
|
|
return {
|
|
status: error.response?.status,
|
|
data: error.response?.data,
|
|
message: error.message,
|
|
stack: error.stack,
|
|
};
|
|
}
|
|
if (error instanceof Error) {
|
|
return {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
};
|
|
}
|
|
return {
|
|
message: String(error),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Configuration for creating service users
|
|
*/
|
|
export interface ServiceUserConfig {
|
|
username: string;
|
|
displayName: string;
|
|
email: string;
|
|
permissions: number;
|
|
avatar?: string;
|
|
description?: string;
|
|
}
|
|
|
|
/**
|
|
* Service types for user creation
|
|
*/
|
|
export type ServiceType =
|
|
| 'trakt'
|
|
| 'tmdb'
|
|
| 'imdb'
|
|
| 'mdblist'
|
|
| 'letterboxd'
|
|
| 'networks'
|
|
| 'originals'
|
|
| 'tautulli'
|
|
| 'overseerr'
|
|
| 'anilist'
|
|
| 'myanimelist'
|
|
| 'multi-source';
|
|
|
|
/**
|
|
* Generate service user configuration dynamically
|
|
*/
|
|
export function generateServiceUserConfig(
|
|
serviceType: ServiceType,
|
|
collectionType?: string,
|
|
userCreationMode: 'single' | 'per-service' | 'granular' = 'per-service'
|
|
): ServiceUserConfig {
|
|
const serviceInfo = {
|
|
trakt: { name: 'Trakt' },
|
|
tmdb: { name: 'TMDB' },
|
|
imdb: { name: 'IMDb' },
|
|
mdblist: { name: 'MDBList' },
|
|
letterboxd: { name: 'Letterboxd' },
|
|
networks: { name: 'Networks' },
|
|
originals: { name: 'Originals' },
|
|
tautulli: { name: 'Tautulli' },
|
|
overseerr: { name: 'Overseerr' },
|
|
anilist: { name: 'AniList' },
|
|
myanimelist: { name: 'MyAnimeList' },
|
|
'multi-source': { name: 'MultiSource' },
|
|
}[serviceType];
|
|
|
|
let username: string;
|
|
let displayName: string;
|
|
let email: string;
|
|
let avatar: string;
|
|
let description: string;
|
|
|
|
switch (userCreationMode) {
|
|
case 'single':
|
|
// Single mode: Everything goes to "Agregarr"
|
|
username = 'Agregarr';
|
|
displayName = 'Agregarr';
|
|
email = 'donotchangeme@agregarr.invalid';
|
|
avatar = '/os_icon.svg';
|
|
description = 'Virtual service user for all Agregarr collection requests';
|
|
break;
|
|
|
|
case 'granular':
|
|
if (collectionType) {
|
|
// Granular mode: TraktTrendingAgregarr, TMDbPopularAgregarr, etc.
|
|
const collectionName =
|
|
collectionType.charAt(0).toUpperCase() + collectionType.slice(1);
|
|
username = `${serviceInfo.name}${collectionName}Agregarr`;
|
|
displayName = username;
|
|
email = `donotchangeme@${serviceType.toLowerCase()}.${collectionType.toLowerCase()}.agregarr.invalid`;
|
|
avatar = '/os_icon.svg';
|
|
description = `Virtual service user for ${serviceInfo.name} ${collectionName} collection requests`;
|
|
} else {
|
|
// Fallback to per-service if no collection type
|
|
username = `${serviceInfo.name}Agregarr`;
|
|
displayName = username;
|
|
email = `donotchangeme@${serviceType.toLowerCase()}.agregarr.invalid`;
|
|
avatar = '/os_icon.svg';
|
|
description = `Virtual service user for ${serviceInfo.name} collection requests`;
|
|
}
|
|
break;
|
|
|
|
case 'per-service':
|
|
default:
|
|
// Per-service mode: TraktAgregarr, TMDbAgregarr, etc.
|
|
username = `${serviceInfo.name}Agregarr`;
|
|
displayName = username;
|
|
email = `donotchangeme@${serviceType.toLowerCase()}.agregarr.invalid`;
|
|
avatar = '/os_icon.svg';
|
|
description = `Virtual service user for ${serviceInfo.name} collection requests`;
|
|
break;
|
|
}
|
|
|
|
return {
|
|
username,
|
|
displayName,
|
|
email,
|
|
permissions: 32, // Start with manual approval permissions (will be changed dynamically)
|
|
avatar,
|
|
description,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Service User Manager for creating and managing virtual users
|
|
*
|
|
* Handles creation, retrieval, and management of service users used by
|
|
* collection sync processes for auto-requests and other automated operations.
|
|
*/
|
|
export class ServiceUserManager {
|
|
private userRepository = getRepository(User);
|
|
|
|
/**
|
|
* Get Overseerr API client with current settings
|
|
*/
|
|
private getOverseerrAPI(): OverseerrAPI {
|
|
const settings = getSettings();
|
|
if (!settings.overseerr.hostname || !settings.overseerr.apiKey) {
|
|
throw new Error(
|
|
'External Overseerr not configured for service user management'
|
|
);
|
|
}
|
|
// Create fresh client with current settings
|
|
return new OverseerrAPI(settings.overseerr);
|
|
}
|
|
|
|
/**
|
|
* Get or create a service user based on configuration
|
|
*
|
|
* @param config - Service user configuration
|
|
* @returns Promise resolving to the service user
|
|
*/
|
|
public async getOrCreateServiceUser(
|
|
config: ServiceUserConfig
|
|
): Promise<User> {
|
|
// Try to find existing service user by email (unique identifier)
|
|
let serviceUser = await this.userRepository.findOne({
|
|
where: { email: config.email },
|
|
});
|
|
|
|
// If not found with new format, try old format (migration path)
|
|
if (!serviceUser && config.email.endsWith('.invalid')) {
|
|
const oldEmail = config.email.replace('.invalid', '');
|
|
serviceUser = await this.userRepository.findOne({
|
|
where: { email: oldEmail },
|
|
});
|
|
|
|
// Migrate to new email format
|
|
if (serviceUser) {
|
|
logger.info(
|
|
`Migrating service user email from old format: ${oldEmail} → ${config.email}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
username: config.username,
|
|
}
|
|
);
|
|
|
|
// Update internal user email
|
|
serviceUser.email = config.email;
|
|
serviceUser.updatedAt = new Date();
|
|
|
|
// Recreate external Overseerr user with new email (email is read-only, can't be updated)
|
|
if (serviceUser.externalOverseerrId) {
|
|
try {
|
|
const overseerrAPI = this.getOverseerrAPI();
|
|
|
|
// Create new external user with correct email
|
|
const password = this.generateSecurePassword();
|
|
const newExternalUser = await overseerrAPI.createUser({
|
|
username: config.username,
|
|
email: config.email,
|
|
password: password,
|
|
displayName: config.displayName,
|
|
});
|
|
|
|
// Set appropriate permissions
|
|
const overseerrPermissions = this.mapToOverseerrPermissions(
|
|
config.permissions
|
|
);
|
|
await overseerrAPI.updateUserPermissions(
|
|
newExternalUser.id,
|
|
overseerrPermissions
|
|
);
|
|
await overseerrAPI.disableUserNotifications(newExternalUser.id);
|
|
|
|
logger.info(
|
|
`Recreated external Overseerr user with new email for: ${config.displayName}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
oldExternalUserId: serviceUser.externalOverseerrId,
|
|
newExternalUserId: newExternalUser.id,
|
|
oldEmail: oldEmail,
|
|
newEmail: config.email,
|
|
}
|
|
);
|
|
|
|
// Update internal user to point to new external user
|
|
serviceUser.externalOverseerrId = newExternalUser.id;
|
|
} catch (error) {
|
|
logger.warn(
|
|
`Failed to recreate external Overseerr user for ${config.displayName}, will retry on next sync`,
|
|
{
|
|
label: 'Service User Manager',
|
|
externalUserId: serviceUser.externalOverseerrId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
await this.userRepository.save(serviceUser);
|
|
}
|
|
}
|
|
|
|
if (!serviceUser) {
|
|
// Create new service user (both internal and external)
|
|
serviceUser = await this.createServiceUser(config);
|
|
|
|
logger.info(`Created virtual service user: ${config.displayName}`, {
|
|
label: 'Service User Manager',
|
|
username: config.username,
|
|
email: config.email,
|
|
permissions: config.permissions,
|
|
externalOverseerrId: serviceUser.externalOverseerrId,
|
|
});
|
|
} else {
|
|
// Ensure user exists in external Overseerr and has correct permissions
|
|
await this.ensureExternalUser(serviceUser, config);
|
|
|
|
// Update existing service user if permissions have changed
|
|
const hasPermissionChanges =
|
|
serviceUser.permissions !== config.permissions;
|
|
const hasDisplayNameChanges =
|
|
serviceUser.displayName !== config.displayName;
|
|
|
|
if (hasPermissionChanges || hasDisplayNameChanges) {
|
|
serviceUser.permissions = config.permissions;
|
|
serviceUser.displayName = config.displayName;
|
|
serviceUser.updatedAt = new Date();
|
|
|
|
await this.userRepository.save(serviceUser);
|
|
|
|
// Also update permissions in external Overseerr if they changed
|
|
if (hasPermissionChanges && serviceUser.externalOverseerrId) {
|
|
try {
|
|
const overseerrAPI = this.getOverseerrAPI();
|
|
const overseerrPermissions = this.mapToOverseerrPermissions(
|
|
config.permissions
|
|
);
|
|
await overseerrAPI.updateUserPermissions(
|
|
serviceUser.externalOverseerrId,
|
|
overseerrPermissions
|
|
);
|
|
await overseerrAPI.disableUserNotifications(
|
|
serviceUser.externalOverseerrId
|
|
);
|
|
|
|
logger.info(
|
|
`Updated external Overseerr permissions for: ${config.displayName}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
externalUserId: serviceUser.externalOverseerrId,
|
|
newPermissions: overseerrPermissions,
|
|
}
|
|
);
|
|
} catch (error) {
|
|
// If permission update fails (likely due to stale user ID), re-ensure external user
|
|
logger.warn(
|
|
`Permission update failed for external user ${serviceUser.externalOverseerrId}, re-ensuring user: ${config.displayName}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
externalUserId: serviceUser.externalOverseerrId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
}
|
|
);
|
|
|
|
// Clear stale external user ID and re-ensure external user
|
|
// Set to undefined in memory only - will be saved by ensureExternalUser if successful
|
|
serviceUser.externalOverseerrId = undefined;
|
|
await this.ensureExternalUser(serviceUser, config);
|
|
// ensureExternalUser will set the new ID and save if successful
|
|
// If it fails, user still has undefined but that's handled by ensureExternalUser on next run
|
|
}
|
|
}
|
|
|
|
logger.info(`Updated virtual service user: ${config.displayName}`, {
|
|
label: 'Service User Manager',
|
|
username: config.username,
|
|
permissionsChanged: hasPermissionChanges,
|
|
displayNameChanged: hasDisplayNameChanges,
|
|
});
|
|
}
|
|
}
|
|
|
|
return serviceUser;
|
|
}
|
|
|
|
/**
|
|
* Get or create service user by type with settings consideration
|
|
*/
|
|
public async getOrCreateServiceUserByType(
|
|
serviceType: ServiceType,
|
|
collectionType?: string
|
|
): Promise<User> {
|
|
const settings = getSettings();
|
|
const userCreationMode =
|
|
settings.serviceUser?.userCreationMode ?? 'per-service';
|
|
|
|
const config = generateServiceUserConfig(
|
|
serviceType,
|
|
collectionType,
|
|
userCreationMode
|
|
);
|
|
return this.getOrCreateServiceUser(config);
|
|
}
|
|
|
|
/**
|
|
* Get or create service user with dynamic permissions
|
|
*/
|
|
public async getOrCreateServiceUserForRequest(
|
|
serviceType: ServiceType,
|
|
collectionType: string | undefined,
|
|
autoApprove: boolean
|
|
): Promise<User> {
|
|
const settings = getSettings();
|
|
const userCreationMode =
|
|
settings.serviceUser?.userCreationMode ?? 'per-service';
|
|
|
|
// Generate config
|
|
const config = generateServiceUserConfig(
|
|
serviceType,
|
|
collectionType,
|
|
userCreationMode
|
|
);
|
|
|
|
// Override permissions based on auto-approve setting
|
|
config.permissions = autoApprove ? 928 : 32; // 928 = auto-approve, 32 = manual
|
|
|
|
return this.getOrCreateServiceUser(config);
|
|
}
|
|
|
|
// Note: Virtual user creation removed - not needed since collection functions
|
|
// ignore user parameter when custom titles and global collection flags are used
|
|
|
|
/**
|
|
* Clean up orphaned service users
|
|
*
|
|
* Removes service users that are no longer needed or have been replaced
|
|
* by newer configurations.
|
|
*/
|
|
public async cleanupOrphanedServiceUsers(): Promise<number> {
|
|
// Get all service users (emails starting with donotchangeme@)
|
|
const allServiceUsers = await this.userRepository
|
|
.createQueryBuilder('user')
|
|
.where('user.email LIKE :pattern', { pattern: 'donotchangeme@%' })
|
|
.getMany();
|
|
|
|
if (allServiceUsers.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
// For now, don't auto-cleanup users since the new system is dynamic
|
|
// Users should manually clean up old users after transitioning
|
|
logger.info(`Found ${allServiceUsers.length} service users`, {
|
|
label: 'Service User Manager',
|
|
users: allServiceUsers.map((u) => ({
|
|
email: u.email,
|
|
displayName: u.displayName,
|
|
})),
|
|
});
|
|
|
|
return 0; // No cleanup performed automatically
|
|
}
|
|
|
|
/**
|
|
* List all active service users
|
|
*/
|
|
public async listServiceUsers(): Promise<User[]> {
|
|
return this.userRepository
|
|
.createQueryBuilder('user')
|
|
.where('user.email LIKE :pattern', { pattern: 'donotchangeme@%' })
|
|
.getMany();
|
|
}
|
|
|
|
/**
|
|
* Create a new service user (both internal and external)
|
|
*/
|
|
private async createServiceUser(config: ServiceUserConfig): Promise<User> {
|
|
const overseerrAPI = this.getOverseerrAPI();
|
|
|
|
// First, check if user already exists in external Overseerr
|
|
let externalUser = await this.findExistingUserByEmail(config.email);
|
|
|
|
if (externalUser) {
|
|
// User exists, update permissions
|
|
const overseerrPermissions = this.mapToOverseerrPermissions(
|
|
config.permissions
|
|
);
|
|
await overseerrAPI.updateUserPermissions(
|
|
externalUser.id,
|
|
overseerrPermissions
|
|
);
|
|
await overseerrAPI.disableUserNotifications(externalUser.id);
|
|
|
|
logger.debug(
|
|
`Found existing external Overseerr user: ${config.username}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
externalUserId: externalUser.id,
|
|
permissions: overseerrPermissions,
|
|
}
|
|
);
|
|
} else {
|
|
// User doesn't exist, create new one
|
|
try {
|
|
const password = this.generateSecurePassword();
|
|
externalUser = await overseerrAPI.createUser({
|
|
username: config.username,
|
|
email: config.email,
|
|
password: password,
|
|
displayName: config.displayName,
|
|
});
|
|
|
|
// Set appropriate permissions
|
|
const overseerrPermissions = this.mapToOverseerrPermissions(
|
|
config.permissions
|
|
);
|
|
await overseerrAPI.updateUserPermissions(
|
|
externalUser.id,
|
|
overseerrPermissions
|
|
);
|
|
await overseerrAPI.disableUserNotifications(externalUser.id);
|
|
|
|
logger.debug(`Created external Overseerr user: ${config.username}`, {
|
|
label: 'Service User Manager',
|
|
externalUserId: externalUser.id,
|
|
permissions: overseerrPermissions,
|
|
});
|
|
} catch (error) {
|
|
const errorDetails = getErrorDetails(error);
|
|
logger.error(
|
|
`Failed to create external Overseerr user: ${config.username}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
username: config.username,
|
|
email: config.email,
|
|
displayName: config.displayName,
|
|
errorStatus: errorDetails.status,
|
|
errorData: errorDetails.data,
|
|
errorMessage: errorDetails.message,
|
|
}
|
|
);
|
|
throw new Error(`Failed to create external Overseerr user: ${error}`);
|
|
}
|
|
}
|
|
|
|
// Create internal user with external ID mapping
|
|
const serviceUser = new User({
|
|
email: config.email,
|
|
username: config.username,
|
|
displayName: config.displayName,
|
|
plexUsername: config.username,
|
|
plexTitle: config.displayName,
|
|
permissions: config.permissions,
|
|
userType: 1, // LOCAL user type
|
|
externalOverseerrId: externalUser.id,
|
|
avatar: '/os_icon.svg', // Default Agregarr icon for service users
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
return await this.userRepository.save(serviceUser);
|
|
}
|
|
|
|
/**
|
|
* Ensure user exists in external Overseerr and has correct permissions
|
|
*/
|
|
private async ensureExternalUser(
|
|
user: User,
|
|
config: ServiceUserConfig
|
|
): Promise<void> {
|
|
const overseerrAPI = this.getOverseerrAPI();
|
|
|
|
if (!user.externalOverseerrId) {
|
|
// User doesn't have external ID, check if user exists in external Overseerr
|
|
let externalUser = await this.findExistingUserByEmail(config.email);
|
|
|
|
if (externalUser) {
|
|
// Found existing external user, link it
|
|
user.externalOverseerrId = externalUser.id;
|
|
await this.userRepository.save(user);
|
|
|
|
logger.info(
|
|
`Linked existing service user to external Overseerr: ${config.username}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
externalUserId: externalUser.id,
|
|
}
|
|
);
|
|
} else {
|
|
// External user doesn't exist, create it
|
|
try {
|
|
const password = this.generateSecurePassword();
|
|
externalUser = await overseerrAPI.createUser({
|
|
username: config.username,
|
|
email: config.email,
|
|
password: password,
|
|
displayName: config.displayName,
|
|
});
|
|
|
|
// Update internal user with external ID
|
|
user.externalOverseerrId = externalUser.id;
|
|
await this.userRepository.save(user);
|
|
|
|
logger.info(
|
|
`Created and linked external user for existing service user: ${config.username}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
externalUserId: externalUser.id,
|
|
}
|
|
);
|
|
} catch (error) {
|
|
const errorDetails = getErrorDetails(error);
|
|
logger.error(
|
|
`Failed to create external Overseerr user: ${config.username}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
username: config.username,
|
|
email: config.email,
|
|
displayName: config.displayName,
|
|
errorStatus: errorDetails.status,
|
|
errorData: errorDetails.data,
|
|
errorMessage: errorDetails.message,
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Set appropriate permissions for the external user (new or existing)
|
|
try {
|
|
const overseerrPermissions = this.mapToOverseerrPermissions(
|
|
config.permissions
|
|
);
|
|
await overseerrAPI.updateUserPermissions(
|
|
externalUser.id,
|
|
overseerrPermissions
|
|
);
|
|
await overseerrAPI.disableUserNotifications(externalUser.id);
|
|
} catch (error) {
|
|
logger.error(
|
|
`Failed to set permissions for external user: ${config.username}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
externalUserId: externalUser.id,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
}
|
|
);
|
|
}
|
|
} else {
|
|
// User exists externally, ensure permissions are correct
|
|
try {
|
|
const overseerrPermissions = this.mapToOverseerrPermissions(
|
|
config.permissions
|
|
);
|
|
await overseerrAPI.updateUserPermissions(
|
|
user.externalOverseerrId,
|
|
overseerrPermissions
|
|
);
|
|
await overseerrAPI.disableUserNotifications(user.externalOverseerrId);
|
|
} catch (error) {
|
|
// If permission update fails (likely due to stale user ID), clear and recreate
|
|
logger.warn(
|
|
`Permission update failed for external user ${user.externalOverseerrId}, recreating user: ${config.username}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
externalUserId: user.externalOverseerrId,
|
|
}
|
|
);
|
|
|
|
// Try to find existing user by email first, or create new one
|
|
let externalUser = await this.findExistingUserByEmail(config.email);
|
|
|
|
if (!externalUser) {
|
|
// External user doesn't exist, create it
|
|
const password = this.generateSecurePassword();
|
|
|
|
try {
|
|
externalUser = await overseerrAPI.createUser({
|
|
username: config.username,
|
|
email: config.email,
|
|
password: password,
|
|
displayName: config.displayName,
|
|
});
|
|
} catch (createError) {
|
|
const createErrorDetails = getErrorDetails(createError);
|
|
logger.error(
|
|
`Failed to recreate external Overseerr user during recovery: ${config.username}`,
|
|
{
|
|
label: 'Service User Manager',
|
|
username: config.username,
|
|
email: config.email,
|
|
displayName: config.displayName,
|
|
errorStatus: createErrorDetails.status,
|
|
errorData: createErrorDetails.data,
|
|
errorMessage: createErrorDetails.message,
|
|
}
|
|
);
|
|
throw createError; // Re-throw to prevent saving undefined
|
|
}
|
|
}
|
|
|
|
// Only clear and save the undefined value AFTER successfully getting a new external user
|
|
// This prevents storing undefined permanently if the above operations fail
|
|
user.externalOverseerrId = externalUser.id;
|
|
await this.userRepository.save(user);
|
|
|
|
// Set appropriate permissions for the external user
|
|
const overseerrPermissions = this.mapToOverseerrPermissions(
|
|
config.permissions
|
|
);
|
|
await overseerrAPI.updateUserPermissions(
|
|
externalUser.id,
|
|
overseerrPermissions
|
|
);
|
|
await overseerrAPI.disableUserNotifications(externalUser.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find existing user in external Overseerr by email
|
|
*/
|
|
private async findExistingUserByEmail(
|
|
email: string
|
|
): Promise<OverseerrUser | null> {
|
|
try {
|
|
const overseerrAPI = this.getOverseerrAPI();
|
|
|
|
// Get all users and find by email (Overseerr doesn't have direct email search)
|
|
const usersResponse = await overseerrAPI.getUsers({ take: 1000 }); // Get a large batch
|
|
const existingUser = usersResponse.results.find(
|
|
(user) => user.email === email
|
|
);
|
|
|
|
return existingUser || null;
|
|
} catch (error) {
|
|
logger.warn(`Failed to search for existing user by email: ${email}`, {
|
|
label: 'Service User Manager',
|
|
email,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map internal permissions to Overseerr permission values
|
|
*/
|
|
private mapToOverseerrPermissions(internalPermissions: number): number {
|
|
// Based on your examples:
|
|
// Manual approval (REQUEST only): 32
|
|
// Auto approval (REQUEST + AUTO_APPROVE + AUTO_APPROVE_MOVIE + AUTO_APPROVE_TV): 160
|
|
|
|
const hasAutoApprove = (internalPermissions & 896) > 0; // Check for auto-approve permissions (128+256+512)
|
|
|
|
return hasAutoApprove ? 160 : 32;
|
|
}
|
|
|
|
/**
|
|
* Generate a secure password for service users
|
|
*/
|
|
private generateSecurePassword(): string {
|
|
return randomUUID() + randomUUID().replace(/-/g, '');
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const serviceUserManager = new ServiceUserManager();
|