mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-28 03:25:13 +02:00
feat: Backend implementation for source plugin usage and configuration
This commit is contained in:
31
backend/src/users/user-sources/user-source.dto.ts
Normal file
31
backend/src/users/user-sources/user-source.dto.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
ApiProperty,
|
||||
IntersectionType,
|
||||
OmitType,
|
||||
PartialType,
|
||||
PickType,
|
||||
} from '@nestjs/swagger';
|
||||
import { MediaSource } from './user-source.entity';
|
||||
import { Type } from '@nestjs/common';
|
||||
|
||||
const PickAndPartial = <T, K extends keyof T>(
|
||||
clazz: Type<T>,
|
||||
pick: K[] = [],
|
||||
partial: K[] = [],
|
||||
) =>
|
||||
IntersectionType(
|
||||
OmitType(PickType(clazz, pick), partial),
|
||||
PickType(PartialType(clazz), partial),
|
||||
);
|
||||
|
||||
export class SourceDto extends PickAndPartial(
|
||||
MediaSource,
|
||||
['id', 'userId', 'adminControlled', 'enabled'],
|
||||
['pluginSettings'],
|
||||
) {}
|
||||
|
||||
export class CreateSourceDto extends PickAndPartial(
|
||||
MediaSource,
|
||||
['pluginSettings', 'id'],
|
||||
['enabled', 'adminControlled'],
|
||||
) {}
|
||||
33
backend/src/users/user-sources/user-source.entity.ts
Normal file
33
backend/src/users/user-sources/user-source.entity.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { PluginSettings } from 'plugins/plugin-types';
|
||||
import { User } from 'src/users/user.entity';
|
||||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
|
||||
|
||||
@Entity()
|
||||
export class MediaSource {
|
||||
@ApiProperty({ required: true, type: 'string' })
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@PrimaryColumn()
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ required: true, type: 'string' })
|
||||
@ManyToOne(() => User, (user) => user.mediaSources)
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User;
|
||||
|
||||
@ApiProperty({ required: false, type: 'string', default: true })
|
||||
@Column({ default: true })
|
||||
enabled: boolean;
|
||||
|
||||
@ApiProperty({ required: false, type: 'boolean', default: false })
|
||||
@Column({ default: false })
|
||||
adminControlled: boolean;
|
||||
|
||||
@ApiProperty({ required: false, type: 'object' })
|
||||
@Column('json', { default: '{}' })
|
||||
pluginSettings: PluginSettings = {};
|
||||
// Add other fields as necessary
|
||||
}
|
||||
75
backend/src/users/user-sources/user-sources.controller.ts
Normal file
75
backend/src/users/user-sources/user-sources.controller.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Put,
|
||||
UnauthorizedException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthGuard, GetUser } from 'src/auth/auth.guard';
|
||||
import { UsersService } from '../users.service';
|
||||
import { User } from '../user.entity';
|
||||
import { UserDto } from '../user.dto';
|
||||
import { CreateSourceDto } from './user-source.dto';
|
||||
import {
|
||||
UserSourcesService,
|
||||
UserSourcesServiceError,
|
||||
} from './user-sources.service';
|
||||
|
||||
@ApiTags('users')
|
||||
@Controller('users/:userId/sources')
|
||||
@UseGuards(AuthGuard)
|
||||
export class UsersSourcesController {
|
||||
constructor(
|
||||
private usersService: UsersService,
|
||||
private userSourcesService: UserSourcesService,
|
||||
) {}
|
||||
|
||||
@Put(':sourceId')
|
||||
@ApiOkResponse({ description: 'Source updated', type: UserDto })
|
||||
async updateSource(
|
||||
@GetUser() callerUser: User,
|
||||
@Param('sourceId') sourceId: string,
|
||||
@Param('userId') userId: string,
|
||||
@Body() sourceDto: CreateSourceDto,
|
||||
): Promise<UserDto> {
|
||||
const user = await this.usersService.findOne(userId);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const updatedUser = await this.userSourcesService
|
||||
.updateUserSource(user, sourceId, sourceDto, callerUser)
|
||||
.catch((e) => {
|
||||
if (e === UserSourcesServiceError.Unauthorized) {
|
||||
throw new UnauthorizedException();
|
||||
} else {
|
||||
throw new InternalServerErrorException('Failed to update source');
|
||||
}
|
||||
});
|
||||
|
||||
if (!updatedUser) {
|
||||
throw new InternalServerErrorException('Failed to update source');
|
||||
}
|
||||
|
||||
return UserDto.fromEntity(updatedUser);
|
||||
}
|
||||
|
||||
@Delete(':sourceId')
|
||||
@ApiOkResponse({ description: 'Source deleted', type: UserDto })
|
||||
async deleteSource(
|
||||
@GetUser() callerUser: User,
|
||||
@Param('sourceId') sourceId: string,
|
||||
@Param('userId') userId: string,
|
||||
): Promise<UserDto> {
|
||||
const updatedUser = await this.userSourcesService.deleteUserSource(userId, sourceId, callerUser);
|
||||
|
||||
return UserDto.fromEntity(updatedUser);
|
||||
}
|
||||
}
|
||||
96
backend/src/users/user-sources/user-sources.service.ts
Normal file
96
backend/src/users/user-sources/user-sources.service.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from '../user.entity';
|
||||
import { CreateSourceDto } from './user-source.dto';
|
||||
import { USER_REPOSITORY, USER_SOURCE_REPOSITORY } from '../user.providers';
|
||||
import { MediaSource } from './user-source.entity';
|
||||
import { UsersService } from '../users.service';
|
||||
|
||||
export enum UserSourcesServiceError {
|
||||
SourceNotFound = 'SourceNotFound',
|
||||
Unauthorized = 'Unauthorized',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class UserSourcesService {
|
||||
constructor(
|
||||
private readonly userService: UsersService,
|
||||
@Inject(USER_SOURCE_REPOSITORY)
|
||||
private readonly userSourceRepository: Repository<MediaSource>,
|
||||
) {}
|
||||
|
||||
private async findUserSource(
|
||||
userId: string,
|
||||
sourceId: string,
|
||||
): Promise<MediaSource> {
|
||||
const source = await this.userSourceRepository.findOne({
|
||||
where: {
|
||||
id: sourceId,
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
async deleteUserSource(
|
||||
userId: string,
|
||||
sourceId: string,
|
||||
callerUser: User,
|
||||
): Promise<User> {
|
||||
if (!callerUser.isAdmin || callerUser.id !== userId) {
|
||||
throw UserSourcesServiceError.Unauthorized;
|
||||
}
|
||||
|
||||
const source = await this.findUserSource(userId, sourceId);
|
||||
|
||||
if (!source) {
|
||||
throw UserSourcesServiceError.SourceNotFound;
|
||||
}
|
||||
|
||||
await this.userSourceRepository.remove(source);
|
||||
return this.userService.findOne(userId);
|
||||
}
|
||||
|
||||
async updateUserSource(
|
||||
user: User,
|
||||
sourceId: string,
|
||||
sourceDto: CreateSourceDto,
|
||||
callerUser: User = user,
|
||||
): Promise<User> {
|
||||
if (!callerUser.isAdmin || callerUser.id !== user.id) {
|
||||
throw UserSourcesServiceError.Unauthorized;
|
||||
}
|
||||
|
||||
let source = await this.findUserSource(user.id, sourceId);
|
||||
|
||||
// Create new if doesn't exist
|
||||
if (!source) {
|
||||
source = new MediaSource();
|
||||
source.user = user;
|
||||
source.id = sourceId;
|
||||
source.adminControlled = false;
|
||||
}
|
||||
|
||||
// Check for unauthorized access
|
||||
if (source.adminControlled && !callerUser.isAdmin) {
|
||||
throw UserSourcesServiceError.Unauthorized;
|
||||
} else if (sourceDto.adminControlled !== undefined && !callerUser.isAdmin) {
|
||||
throw UserSourcesServiceError.Unauthorized;
|
||||
}
|
||||
|
||||
source.adminControlled =
|
||||
sourceDto.adminControlled ?? source.adminControlled;
|
||||
source.enabled = sourceDto.enabled ?? source.enabled;
|
||||
console.log('Test defaults, enabled', new MediaSource().enabled);
|
||||
source.pluginSettings = sourceDto.pluginSettings ?? source.pluginSettings;
|
||||
|
||||
await this.userSourceRepository.save(source);
|
||||
return this.userService.findOne(user.id);
|
||||
}
|
||||
|
||||
getSourceSettings(user: User, sourceId: string) {
|
||||
return user.mediaSources?.find((source) => source.id === sourceId)
|
||||
?.pluginSettings;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export class UserDto extends OmitType(User, [
|
||||
@ApiProperty({ type: 'string' })
|
||||
profilePicture: string | null;
|
||||
|
||||
static fromEntity(entity: User): UserDto {
|
||||
static fromEntity(entity: User, caller: User = entity): UserDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
@@ -17,6 +17,8 @@ export class UserDto extends OmitType(User, [
|
||||
onboardingDone: entity.onboardingDone,
|
||||
profilePicture:
|
||||
'data:image;base64,' + entity.profilePicture?.toString('base64'),
|
||||
// pluginSettings: entity.pluginSettings,
|
||||
mediaSources: entity.mediaSources,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -37,6 +39,7 @@ export class UpdateUserDto extends PartialType(
|
||||
'name',
|
||||
'password',
|
||||
'isAdmin',
|
||||
// 'pluginSettings',
|
||||
] as const),
|
||||
) {
|
||||
@ApiProperty({ type: 'string', required: false })
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryColumn,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { MediaSource } from 'src/users/user-sources/user-source.entity';
|
||||
|
||||
export class SonarrSettings {
|
||||
@ApiProperty({ required: true })
|
||||
@@ -127,4 +135,11 @@ export class User {
|
||||
@ApiProperty({ required: true, type: Settings })
|
||||
@Column('json', { default: JSON.stringify(DEFAULT_SETTINGS) })
|
||||
settings = DEFAULT_SETTINGS;
|
||||
|
||||
// @ApiProperty({ required: false, type: 'object' })
|
||||
// @Column('json', { default: '{}' })
|
||||
// pluginSettings: PluginSettings = {};
|
||||
|
||||
@OneToMany(() => MediaSource, (mediaSource) => mediaSource.user)
|
||||
mediaSources: MediaSource[];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { DATA_SOURCE } from '../database/database.providers';
|
||||
import { MediaSource } from './user-sources/user-source.entity';
|
||||
|
||||
export const USER_REPOSITORY = 'USER_REPOSITORY';
|
||||
export const USER_SOURCE_REPOSITORY = 'USER_SOURCE_REPOSITORY';
|
||||
|
||||
export const userProviders = [
|
||||
{
|
||||
@@ -10,4 +12,10 @@ export const userProviders = [
|
||||
useFactory: (dataSource: DataSource) => dataSource.getRepository(User),
|
||||
inject: [DATA_SOURCE],
|
||||
},
|
||||
{
|
||||
provide: USER_SOURCE_REPOSITORY,
|
||||
useFactory: (dataSource: DataSource) =>
|
||||
dataSource.getRepository(MediaSource),
|
||||
inject: [DATA_SOURCE],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -47,7 +47,7 @@ export class UsersController {
|
||||
type: UserDto,
|
||||
isArray: true,
|
||||
})
|
||||
async findAll(@GetUser() callerUser: User): Promise<UserDto[]> {
|
||||
async findAllUsers(@GetUser() callerUser: User): Promise<UserDto[]> {
|
||||
if (!callerUser.isAdmin) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export class UsersController {
|
||||
@Get(':id')
|
||||
@ApiOkResponse({ description: 'User found', type: UserDto })
|
||||
@ApiException(() => NotFoundException, { description: 'User not found' })
|
||||
async findById(
|
||||
async findUserById(
|
||||
@Param('id') id: string,
|
||||
@GetUser() callerUser: User,
|
||||
): Promise<UserDto> {
|
||||
@@ -90,7 +90,7 @@ export class UsersController {
|
||||
@ApiOkResponse({ description: 'User created', type: UserDto })
|
||||
@ApiException(() => UnauthorizedException, { description: 'Unauthorized' })
|
||||
@ApiException(() => BadRequestException)
|
||||
async create(
|
||||
async createUser(
|
||||
@Body()
|
||||
userCreateDto: CreateUserDto,
|
||||
@GetUser() callerUser: User | undefined,
|
||||
@@ -113,7 +113,7 @@ export class UsersController {
|
||||
@Put(':id')
|
||||
@ApiOkResponse({ description: 'User updated', type: UserDto })
|
||||
@ApiException(() => NotFoundException, { description: 'User not found' })
|
||||
async update(
|
||||
async updateUser(
|
||||
@Param('id') id: string,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
@GetUser() callerUser: User,
|
||||
@@ -121,6 +121,7 @@ export class UsersController {
|
||||
if ((!callerUser.isAdmin && callerUser.id !== id) || !id) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
const user = await this.usersService.findOne(id);
|
||||
|
||||
const updated = await this.usersService
|
||||
|
||||
@@ -5,11 +5,12 @@ import { UsersController } from './users.controller';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { LibraryModule } from './library/library.module';
|
||||
import { PlayStateModule } from './play-state/play-state.module';
|
||||
import { UserSourcesService } from './user-sources/user-sources.service';
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule, LibraryModule, PlayStateModule],
|
||||
providers: [...userProviders, UsersService],
|
||||
providers: [...userProviders, UsersService, UserSourcesService],
|
||||
controllers: [UsersController],
|
||||
exports: [UsersService],
|
||||
exports: [UsersService, UserSourcesService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
||||
@@ -16,18 +16,34 @@ export class UsersService {
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
// Finds
|
||||
async findAll(): Promise<User[]> {
|
||||
return this.userRepository.find();
|
||||
return this.userRepository.find({
|
||||
relations: {
|
||||
mediaSources: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<User> {
|
||||
return this.userRepository.findOne({ where: { id } });
|
||||
return this.userRepository.findOne({
|
||||
where: { id },
|
||||
relations: {
|
||||
mediaSources: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOneByName(name: string): Promise<User> {
|
||||
return this.userRepository.findOne({ where: { name } });
|
||||
return this.userRepository.findOne({
|
||||
where: { name },
|
||||
relations: {
|
||||
mediaSources: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// The rest
|
||||
async create(userCreateDto: CreateUserDto): Promise<User> {
|
||||
if (!userCreateDto.name) throw UserServiceError.UsernameRequired;
|
||||
|
||||
@@ -46,7 +62,8 @@ export class UsersService {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return this.userRepository.save(user);
|
||||
await this.userRepository.save(user);
|
||||
return this.findOne(user.id);
|
||||
}
|
||||
|
||||
async update(
|
||||
@@ -60,15 +77,24 @@ export class UsersService {
|
||||
if (
|
||||
updateUserDto.password !== undefined &&
|
||||
updateUserDto.oldPassword !== user.password
|
||||
)
|
||||
) {
|
||||
throw UserServiceError.PasswordMismatch;
|
||||
else if (updateUserDto.password !== undefined)
|
||||
} else if (updateUserDto.password !== undefined) {
|
||||
user.password = updateUserDto.password;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateUserDto.settings) user.settings = updateUserDto.settings;
|
||||
|
||||
// if (updateUserDto.pluginSettings) {
|
||||
// for (const key of Object.keys(updateUserDto.pluginSettings)) {
|
||||
// user.pluginSettings[key] = updateUserDto.pluginSettings[key];
|
||||
// }
|
||||
// }
|
||||
|
||||
if (updateUserDto.onboardingDone)
|
||||
user.onboardingDone = updateUserDto.onboardingDone;
|
||||
|
||||
if (updateUserDto.profilePicture) {
|
||||
try {
|
||||
user.profilePicture = Buffer.from(
|
||||
@@ -79,14 +105,17 @@ export class UsersService {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateUserDto.isAdmin !== undefined && callerUser.isAdmin)
|
||||
user.isAdmin = updateUserDto.isAdmin;
|
||||
|
||||
return this.userRepository.save(user);
|
||||
await this.userRepository.save(user);
|
||||
|
||||
return this.findOne(user.id);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.userRepository.delete(id);
|
||||
async remove(id: string) {
|
||||
return await this.userRepository.delete(id);
|
||||
}
|
||||
|
||||
async noPreviousAdmins(): Promise<boolean> {
|
||||
|
||||
Reference in New Issue
Block a user