backend: Refactor storage services and remove global preferences service references

This commit is contained in:
Carlos Santos 2025-04-04 11:30:51 +02:00
parent 9e3644ab06
commit 12ef04964c
18 changed files with 182 additions and 133 deletions

View File

@ -1,8 +1,8 @@
import { Container } from 'inversify';
import {
AuthService,
GlobalPreferencesService,
GlobalPreferencesStorageFactory,
MeetStorageService,
StorageFactory,
LiveKitService,
LivekitWebhookService,
LoggerService,
@ -12,7 +12,7 @@ import {
RecordingService,
RedisService,
RoomService,
S3PreferenceStorage,
S3Storage,
S3Service,
SystemEventService,
TaskSchedulerService,
@ -47,11 +47,11 @@ const registerDependencies = () => {
container.bind(RecordingService).toSelf().inSingletonScope();
container.bind(LivekitWebhookService).toSelf().inSingletonScope();
container.bind(GlobalPreferencesService).toSelf().inSingletonScope();
container.bind(MeetStorageService).toSelf().inSingletonScope();
container.bind(ParticipantService).toSelf().inSingletonScope();
container.bind(S3PreferenceStorage).toSelf().inSingletonScope();
container.bind(GlobalPreferencesStorageFactory).toSelf().inSingletonScope();
container.bind(S3Storage).toSelf().inSingletonScope();
container.bind(StorageFactory).toSelf().inSingletonScope();
initializeEagerServices();
};

View File

@ -1,7 +1,7 @@
import { container } from '../../config/dependency-injector.config.js';
import { Request, Response } from 'express';
import { LoggerService } from '../../services/logger.service.js';
import { GlobalPreferencesService } from '../../services/preferences/index.js';
import { MeetStorageService } from '../../services/storage/index.js';
import { OpenViduMeetError } from '../../models/error.model.js';
export const updateRoomPreferences = async (req: Request, res: Response) => {
@ -11,7 +11,7 @@ export const updateRoomPreferences = async (req: Request, res: Response) => {
const { roomId, roomPreferences } = req.body;
try {
const preferenceService = container.get(GlobalPreferencesService);
const preferenceService = container.get(MeetStorageService);
preferenceService.validateRoomPreferences(roomPreferences);
const savedPreferences = await preferenceService.updateOpenViduRoomPreferences(roomId, roomPreferences);
@ -35,7 +35,7 @@ export const getRoomPreferences = async (req: Request, res: Response) => {
try {
const { roomId } = req.params;
const preferenceService = container.get(GlobalPreferencesService);
const preferenceService = container.get(MeetStorageService);
const preferences = await preferenceService.getOpenViduRoomPreferences(roomId);
if (!preferences) {

View File

@ -1,13 +1,13 @@
import { container } from '../../config/dependency-injector.config.js';
import { Request, Response } from 'express';
import { LoggerService } from '../../services/logger.service.js';
import { GlobalPreferencesService } from '../../services/preferences/index.js';
import { MeetStorageService } from '../../services/storage/index.js';
import { OpenViduMeetError } from '../../models/error.model.js';
import { SecurityPreferencesDTO, UpdateSecurityPreferencesDTO } from '@typings-ce';
export const updateSecurityPreferences = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const globalPrefService = container.get(GlobalPreferencesService);
const globalPrefService = container.get(MeetStorageService);
logger.verbose(`Updating security preferences: ${JSON.stringify(req.body)}`);
const securityPreferences = req.body as UpdateSecurityPreferencesDTO;
@ -43,7 +43,7 @@ export const updateSecurityPreferences = async (req: Request, res: Response) =>
export const getSecurityPreferences = async (_req: Request, res: Response) => {
const logger = container.get(LoggerService);
const preferenceService = container.get(GlobalPreferencesService);
const preferenceService = container.get(MeetStorageService);
try {
const preferences = await preferenceService.getGlobalPreferences();

View File

@ -1,13 +1,13 @@
import { container } from '../../config/dependency-injector.config.js';
import { Request, Response } from 'express';
import { LoggerService } from '../../services/logger.service.js';
import { GlobalPreferencesService } from '../../services/preferences/index.js';
import { MeetStorageService } from '../../services/storage/index.js';
import { OpenViduMeetError } from '../../models/error.model.js';
import { WebhookPreferences } from '@typings-ce';
export const updateWebhookPreferences = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const globalPrefService = container.get(GlobalPreferencesService);
const globalPrefService = container.get(MeetStorageService);
logger.verbose(`Updating webhooks preferences: ${JSON.stringify(req.body)}`);
const webhookPreferences = req.body as WebhookPreferences;
@ -31,7 +31,7 @@ export const updateWebhookPreferences = async (req: Request, res: Response) => {
export const getWebhookPreferences = async (_req: Request, res: Response) => {
const logger = container.get(LoggerService);
const preferenceService = container.get(GlobalPreferencesService);
const preferenceService = container.get(MeetStorageService);
try {
const preferences = await preferenceService.getGlobalPreferences();

View File

@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import { AuthMode, ParticipantRole, UserRole, TokenOptions } from '@typings-ce';
import { container } from '../config/dependency-injector.config.js';
import { GlobalPreferencesService, LoggerService, RoomService } from '../services/index.js';
import { MeetStorageService, LoggerService, RoomService } from '../services/index.js';
import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
/**
@ -13,7 +13,7 @@ import { allowAnonymous, tokenAndRoleValidator, withAuth } from './auth.middlewa
*/
export const configureTokenAuth = async (req: Request, res: Response, next: NextFunction) => {
const logger = container.get(LoggerService);
const globalPrefService = container.get(GlobalPreferencesService);
const globalPrefService = container.get(MeetStorageService);
const roomService = container.get(RoomService);
let role: ParticipantRole;

View File

@ -1,7 +1,7 @@
import { container } from '../config/dependency-injector.config.js';
import { NextFunction, Request, Response } from 'express';
import { LoggerService } from '../services/logger.service.js';
import { GlobalPreferencesService } from '../services/index.js';
import { MeetStorageService } from '../services/index.js';
import { allowAnonymous, apiKeyValidator, tokenAndRoleValidator, withAuth } from './auth.middleware.js';
import { AuthMode, ParticipantRole, UserRole } from '@typings-ce';
@ -14,7 +14,7 @@ import { AuthMode, ParticipantRole, UserRole } from '@typings-ce';
*/
export const configureCreateRoomAuth = async (req: Request, res: Response, next: NextFunction) => {
const logger = container.get(LoggerService);
const globalPrefService = container.get(GlobalPreferencesService);
const globalPrefService = container.get(MeetStorageService);
let allowRoomCreation: boolean;
let requireAuthentication: boolean;
@ -70,7 +70,7 @@ export const configureRoomAuthorization = async (req: Request, res: Response, ne
}
const logger = container.get(LoggerService);
const globalPrefService = container.get(GlobalPreferencesService);
const globalPrefService = container.get(MeetStorageService);
let authMode: AuthMode;
try {

View File

@ -25,7 +25,7 @@ import {
recordingRouter,
roomRouter
} from './routes/index.js';
import { GlobalPreferencesService } from './services/index.js';
import { MeetStorageService } from './services/index.js';
import { internalParticipantsRouter } from './routes/participants.routes.js';
import cookieParser from 'cookie-parser';
@ -76,7 +76,7 @@ const createApp = () => {
};
const initializeGlobalPreferences = async () => {
const globalPreferencesService = container.get(GlobalPreferencesService);
const globalPreferencesService = container.get(MeetStorageService);
// TODO: This should be invoked in the constructor of the service
await globalPreferencesService.ensurePreferencesInitialized();
};

View File

@ -2,7 +2,7 @@ import { MEET_ADMIN_SECRET, MEET_ADMIN_USER } from '../environment.js';
import { inject, injectable } from '../config/dependency-injector.config.js';
import { User } from '@typings-ce';
import { UserService } from './user.service.js';
import { GlobalPreferencesService } from './preferences/global-preferences.service.js';
import { MeetStorageService } from './storage/storage.service.js';
import { LoggerService } from './logger.service.js';
import { PasswordHelper } from '../helpers/password.helper.js';
@ -11,7 +11,7 @@ export class AuthService {
constructor(
@inject(LoggerService) protected logger: LoggerService,
@inject(UserService) protected userService: UserService,
@inject(GlobalPreferencesService) protected globalPrefService: GlobalPreferencesService
@inject(MeetStorageService) protected globalPrefService: MeetStorageService
) {}
async authenticate(username: string, password: string): Promise<User | null> {

View File

@ -10,9 +10,9 @@ export * from './openvidu-webhook.service.js';
export * from './system-event.service.js';
export * from './task-scheduler.service.js';
export * from './mutex.service.js';
export * from './preferences/index.js';
export * from './storage/index.js';
export * from './redis.service.js';
export * from './s3.service.js';
export * from './preferences/s3-preferences-storage.js';
export * from './storage/providers/s3-storage.js';
export * from './token.service.js';
export * from './user.service.js';

View File

@ -10,13 +10,13 @@ import {
MeetWebhookPayload,
WebhookPreferences
} from '@typings-ce';
import { GlobalPreferencesService } from './preferences/global-preferences.service.js';
import { MeetStorageService } from './storage/storage.service.js';
@injectable()
export class OpenViduWebhookService {
constructor(
@inject(LoggerService) protected logger: LoggerService,
@inject(GlobalPreferencesService) protected globalPrefService: GlobalPreferencesService
@inject(MeetStorageService) protected globalPrefService: MeetStorageService
) {}
// TODO: Implement Room webhooks

View File

@ -1,31 +0,0 @@
/**
* Factory class to determine and instantiate the appropriate preferences storage
* mechanism (e.g., Database or S3), based on the configuration of the application.
*/
import { PreferencesStorage } from './global-preferences-storage.interface.js';
import { S3PreferenceStorage } from './s3-preferences-storage.js';
import { MEET_PREFERENCES_STORAGE_MODE } from '../../environment.js';
import { inject, injectable } from '../../config/dependency-injector.config.js';
import { LoggerService } from '../logger.service.js';
@injectable()
export class GlobalPreferencesStorageFactory {
constructor(
@inject(S3PreferenceStorage) protected s3PreferenceStorage: S3PreferenceStorage,
@inject(LoggerService) protected logger: LoggerService
) {}
create(): PreferencesStorage {
const storageMode = MEET_PREFERENCES_STORAGE_MODE;
switch (storageMode) {
case 's3':
return this.s3PreferenceStorage;
default:
this.logger.info('No preferences storage mode specified. Defaulting to S3.');
return this.s3PreferenceStorage;
}
}
}

View File

@ -1,3 +0,0 @@
export * from './global-preferences.service.js';
export * from './global-preferences-storage.interface.js';
export * from './global-preferences.factory.js';

View File

@ -0,0 +1,4 @@
export * from './storage.service.js';
export * from './storage.interface.js';
export * from './storage.factory.js';
export * from './providers/s3-storage.js';

View File

@ -1,23 +1,33 @@
/**
* Implements storage for preferences using S3.
* This is used when the application is configured to operate in "s3" mode.
*/
import { GlobalPreferences, MeetRoom } from '@typings-ce';
import { PreferencesStorage } from './global-preferences-storage.interface.js';
import { S3Service } from '../s3.service.js';
import { LoggerService } from '../logger.service.js';
import { RedisService } from '../redis.service.js';
import { OpenViduMeetError } from '../../models/error.model.js';
import { inject, injectable } from '../../config/dependency-injector.config.js';
import { MEET_S3_ROOMS_PREFIX } from '../../environment.js';
import { StorageProvider } from '../storage.interface.js';
import { S3Service } from '../../s3.service.js';
import { LoggerService } from '../../logger.service.js';
import { RedisService } from '../../redis.service.js';
import { OpenViduMeetError } from '../../../models/error.model.js';
import { inject, injectable } from '../../../config/dependency-injector.config.js';
import { MEET_S3_ROOMS_PREFIX } from '../../../environment.js';
// TODO Rename this service to MeetStorageService?
/**
* Implementation of the StorageProvider interface using AWS S3 for persistent storage
* with Redis caching for improved performance.
*
* This class provides operations for storing and retrieving application preferences and room data
* with a two-tiered storage approach:
* - Redis is used as a primary cache for fast access
* - S3 serves as the persistent storage layer and fallback when data is not in Redis
*
* The storage operations are performed in parallel to both systems when writing data,
* with transaction-like rollback behavior if one operation fails.
*
* @template G - Type for global preferences data, defaults to GlobalPreferences
* @template R - Type for room data, defaults to MeetRoom
*
* @implements {StorageProvider}
*/
@injectable()
export class S3PreferenceStorage<
G extends GlobalPreferences = GlobalPreferences,
R extends MeetRoom = MeetRoom
> implements PreferencesStorage
export class S3Storage<G extends GlobalPreferences = GlobalPreferences, R extends MeetRoom = MeetRoom>
implements StorageProvider
{
protected readonly GLOBAL_PREFERENCES_KEY = 'openvidu-meet-preferences';
constructor(
@ -79,7 +89,7 @@ export class S3PreferenceStorage<
}
}
async saveOpenViduRoom(ovRoom: R): Promise<R> {
async saveMeetRoom(ovRoom: R): Promise<R> {
const { roomId } = ovRoom;
const s3Path = `${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`;
const roomStr = JSON.stringify(ovRoom);
@ -122,21 +132,34 @@ export class S3PreferenceStorage<
throw error;
}
async getOpenViduRooms(): Promise<R[]> {
async getMeetRooms(
maxItems: number,
nextPageToken?: string
): Promise<{
rooms: R[];
isTruncated: boolean;
nextPageToken?: string;
}> {
try {
const content = await this.s3Service.listObjects(MEET_S3_ROOMS_PREFIX);
const roomFiles =
content.Contents?.filter(
(file) =>
file?.Key?.endsWith('.json') &&
file.Key !== `${this.GLOBAL_PREFERENCES_KEY}.json`
) ?? [];
const {
Contents: roomFiles,
IsTruncated,
NextContinuationToken
} = await this.s3Service.listObjectsPaginated(MEET_S3_ROOMS_PREFIX, maxItems, nextPageToken);
if (roomFiles.length === 0) {
this.logger.verbose('No OpenVidu rooms found in S3');
return [];
if (!roomFiles) {
this.logger.verbose('No rooms found. Returning an empty array.');
return { rooms: [], isTruncated: false };
}
// const promises: Promise<R>[] = [];
// // Retrieve the data for each room
// roomFiles.forEach((item) => {
// if (item?.Key && item.Key.endsWith('.json')) {
// promises.push(getOpenViduRoom(item.Key) as Promise<R>);
// }
// });
// Extract room names from file paths
const roomIds = roomFiles.map((file) => this.extractRoomId(file.Key)).filter(Boolean) as string[];
// Fetch room preferences in parallel
@ -145,7 +168,7 @@ export class S3PreferenceStorage<
if (!roomId) return null;
try {
return await this.getOpenViduRoom(roomId);
return await this.getMeetRoom(roomId);
} catch (error: any) {
this.logger.warn(`Failed to fetch room "${roomId}": ${error.message}`);
return null;
@ -154,10 +177,11 @@ export class S3PreferenceStorage<
);
// Filter out null values
return rooms.filter(Boolean) as R[];
const roomsResponse = rooms.filter(Boolean) as R[];
return { rooms: roomsResponse, isTruncated: !!IsTruncated, nextPageToken: NextContinuationToken };
} catch (error) {
this.handleError(error, 'Error fetching Room preferences');
return [];
return { rooms: [], isTruncated: false };
}
}
@ -181,7 +205,7 @@ export class S3PreferenceStorage<
return parts[parts.length - 2];
}
async getOpenViduRoom(roomId: string): Promise<R | null> {
async getMeetRoom(roomId: string): Promise<R | null> {
try {
const room: R | null = await this.getFromRedis<R>(roomId);
@ -198,7 +222,7 @@ export class S3PreferenceStorage<
}
}
async deleteOpenViduRoom(roomId: string): Promise<void> {
async deleteMeetRoom(roomId: string): Promise<void> {
try {
await Promise.all([
this.s3Service.deleteObject(`${MEET_S3_ROOMS_PREFIX}/${roomId}/${roomId}.json`),

View File

@ -0,0 +1,32 @@
import { StorageProvider } from './storage.interface.js';
import { S3Storage } from './providers/s3-storage.js';
import { MEET_PREFERENCES_STORAGE_MODE } from '../../environment.js';
import { inject, injectable } from '../../config/dependency-injector.config.js';
import { LoggerService } from '../logger.service.js';
/**
* Factory class responsible for creating the appropriate storage provider based on configuration.
*
* This factory determines which storage implementation to use based on the `MEET_PREFERENCES_STORAGE_MODE`
* environment variable. Currently supports S3 storage, with more providers potentially added in the future.
*/
@injectable()
export class StorageFactory {
constructor(
@inject(S3Storage) protected s3Storage: S3Storage,
@inject(LoggerService) protected logger: LoggerService
) {}
create(): StorageProvider {
const storageMode = MEET_PREFERENCES_STORAGE_MODE;
switch (storageMode) {
case 's3':
return this.s3Storage;
default:
this.logger.info('No preferences storage mode specified. Defaulting to S3.');
return this.s3Storage;
}
}
}

View File

@ -1,13 +1,17 @@
import { GlobalPreferences, MeetRoom } from '@typings-ce';
/**
* Interface for managing global preferences storage.
* An interface that defines the contract for storage providers in the OpenVidu Meet application.
* Storage providers handle persistence of global application preferences and meeting room data.
*
* @template T - The type of global preferences, extending GlobalPreferences
* @template R - The type of room data, extending MeetRoom
*
* Implementations of this interface should handle the persistent storage
* of application settings and room information, which could be backed by
* various storage solutions (database, file system, cloud storage, etc.).
*/
export interface PreferencesStorage<
T extends GlobalPreferences = GlobalPreferences,
R extends MeetRoom = MeetRoom
> {
export interface StorageProvider<T extends GlobalPreferences = GlobalPreferences, R extends MeetRoom = MeetRoom> {
/**
* Initializes the storage with default preferences if they are not already set.
*
@ -31,7 +35,14 @@ export interface PreferencesStorage<
*/
saveGlobalPreferences(preferences: T): Promise<T>;
getOpenViduRooms(): Promise<R[]>;
getMeetRooms(
maxItems?: number,
nextPageToken?: string
): Promise<{
rooms: R[];
isTruncated: boolean;
nextPageToken?: string;
}>;
/**
* Retrieves the {@link MeetRoom}.
@ -39,21 +50,21 @@ export interface PreferencesStorage<
* @param roomId - The name of the room to retrieve.
* @returns A promise that resolves to the OpenVidu Room, or null if not found.
**/
getOpenViduRoom(roomId: string): Promise<R | null>;
getMeetRoom(roomId: string): Promise<R | null>;
/**
* Saves the OpenVidu Room.
* Saves the OpenVidu Meet Room.
*
* @param ovRoom - The OpenVidu Room to save.
* @returns A promise that resolves to the saved
**/
saveOpenViduRoom(ovRoom: R): Promise<R>;
saveMeetRoom(ovRoom: R): Promise<R>;
/**
* Deletes the OpenVidu Room for a given room name.
* Deletes the OpenVidu Meet Room for a given room name.
*
* @param roomId - The name of the room whose should be deleted.
* @returns A promise that resolves when the room have been deleted.
**/
deleteOpenViduRoom(roomId: string): Promise<void>;
deleteMeetRoom(roomId: string): Promise<void>;
}

View File

@ -1,28 +1,29 @@
/**
* Service that provides high-level methods for managing application preferences,
* regardless of the underlying storage mechanism.
*/
import { AuthMode, AuthType, GlobalPreferences, MeetRoom, MeetRoomPreferences } from '@typings-ce';
import { LoggerService } from '../logger.service.js';
import { PreferencesStorage } from './global-preferences-storage.interface.js';
import { GlobalPreferencesStorageFactory } from './global-preferences.factory.js';
import { StorageProvider } from './storage.interface.js';
import { StorageFactory } from './storage.factory.js';
import { errorRoomNotFound, OpenViduMeetError } from '../../models/error.model.js';
import { MEET_NAME_ID, MEET_SECRET, MEET_USER, MEET_WEBHOOK_ENABLED, MEET_WEBHOOK_URL } from '../../environment.js';
import { injectable, inject } from '../../config/dependency-injector.config.js';
import { PasswordHelper } from '../../helpers/password.helper.js';
/**
* A service for managing storage operations related to OpenVidu Meet rooms and preferences.
*
* This service provides an abstraction layer over the underlying storage implementation,
* handling initialization, retrieval, and persistence of global preferences and room data.
*
* @typeParam G - Type for global preferences, extends GlobalPreferences
* @typeParam R - Type for room data, extends MeetRoom
*/
@injectable()
export class GlobalPreferencesService<
G extends GlobalPreferences = GlobalPreferences,
R extends MeetRoom = MeetRoom
> {
protected storage: PreferencesStorage;
export class MeetStorageService<G extends GlobalPreferences = GlobalPreferences, R extends MeetRoom = MeetRoom> {
protected storageProvider: StorageProvider;
constructor(
@inject(LoggerService) protected logger: LoggerService,
@inject(GlobalPreferencesStorageFactory) protected storageFactory: GlobalPreferencesStorageFactory
@inject(StorageFactory) protected storageFactory: StorageFactory
) {
this.storage = this.storageFactory.create();
this.storageProvider = this.storageFactory.create();
}
/**
@ -33,7 +34,7 @@ export class GlobalPreferencesService<
const preferences = await this.getDefaultPreferences();
try {
await this.storage.initialize(preferences);
await this.storageProvider.initialize(preferences);
return preferences as G;
} catch (error) {
this.handleError(error, 'Error initializing default preferences');
@ -46,7 +47,7 @@ export class GlobalPreferencesService<
* @returns {Promise<GlobalPreferences>}
*/
async getGlobalPreferences(): Promise<G> {
const preferences = await this.storage.getGlobalPreferences();
const preferences = await this.storageProvider.getGlobalPreferences();
if (preferences) return preferences as G;
@ -60,16 +61,27 @@ export class GlobalPreferencesService<
*/
async saveGlobalPreferences(preferences: G): Promise<G> {
this.logger.info('Saving global preferences');
return this.storage.saveGlobalPreferences(preferences) as Promise<G>;
return this.storageProvider.saveGlobalPreferences(preferences) as Promise<G>;
}
async saveOpenViduRoom(ovRoom: R): Promise<R> {
this.logger.info(`Saving OpenVidu room ${ovRoom.roomId}`);
return this.storage.saveOpenViduRoom(ovRoom) as Promise<R>;
return this.storageProvider.saveMeetRoom(ovRoom) as Promise<R>;
}
async getOpenViduRooms(): Promise<R[]> {
return this.storage.getOpenViduRooms() as Promise<R[]>;
async getOpenViduRooms(
maxItems?: number,
nextPageToken?: string
): Promise<{
rooms: R[];
isTruncated: boolean;
nextPageToken?: string;
}> {
return this.storageProvider.getMeetRooms(maxItems, nextPageToken) as Promise<{
rooms: R[];
isTruncated: boolean;
nextPageToken?: string;
}>;
}
/**
@ -80,7 +92,7 @@ export class GlobalPreferencesService<
* @throws Error if the room preferences are not found.
*/
async getOpenViduRoom(roomId: string): Promise<R> {
const openviduRoom = await this.storage.getOpenViduRoom(roomId);
const openviduRoom = await this.storageProvider.getMeetRoom(roomId);
if (!openviduRoom) {
this.logger.error(`Room not found for room ${roomId}`);
@ -91,7 +103,7 @@ export class GlobalPreferencesService<
}
async deleteOpenViduRoom(roomId: string): Promise<void> {
return this.storage.deleteOpenViduRoom(roomId);
return this.storageProvider.deleteMeetRoom(roomId);
}
async getOpenViduRoomPreferences(roomId: string): Promise<MeetRoomPreferences> {
@ -177,7 +189,7 @@ export class GlobalPreferencesService<
* @param {any} error
* @param {string} message
*/
protected handleError(error: any, message: string) {
protected handleError(error: OpenViduMeetError | unknown, message: string) {
if (error instanceof OpenViduMeetError) {
this.logger.error(`${message}: ${error.message}`);
} else {

View File

@ -2,13 +2,13 @@ import { MEET_ADMIN_USER } from '../environment.js';
import { inject, injectable } from '../config/dependency-injector.config.js';
import { UserRole, SingleUserAuth, User, SingleUserCredentials } from '@typings-ce';
import { LoggerService } from './logger.service.js';
import { GlobalPreferencesService } from './preferences/global-preferences.service.js';
import { MeetStorageService } from './storage/storage.service.js';
@injectable()
export class UserService {
constructor(
@inject(LoggerService) protected logger: LoggerService,
@inject(GlobalPreferencesService) protected globalPrefService: GlobalPreferencesService
@inject(MeetStorageService) protected globalPrefService: MeetStorageService
) {}
async getUser(username: string): Promise<User | null> {