Skip to content

Commit b462aa8

Browse files
authored
Merge branch 'main' into fix/sso-enabled
2 parents 5f14f64 + 038c582 commit b462aa8

File tree

26 files changed

+917
-162
lines changed

26 files changed

+917
-162
lines changed

api/generated-schema.graphql

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1779,17 +1779,8 @@ input ConnectSignInInput {
17791779
"""The API key for authentication"""
17801780
apiKey: String!
17811781

1782-
"""The ID token for authentication"""
1783-
idToken: String
1784-
17851782
"""User information for the sign-in"""
17861783
userInfo: ConnectUserInfoInput
1787-
1788-
"""The access token for authentication"""
1789-
accessToken: String
1790-
1791-
"""The refresh token for authentication"""
1792-
refreshToken: String
17931784
}
17941785

17951786
input ConnectUserInfoInput {

api/src/unraid-api/config/api-config.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ConfigService, registerAs } from '@nestjs/config';
44
import type { ApiConfig } from '@unraid/shared/services/api-config.js';
55
import { csvStringToArray } from '@unraid/shared/util/data.js';
66
import { fileExists } from '@unraid/shared/util/file.js';
7-
import { bufferTime, debounceTime } from 'rxjs/operators';
7+
import { bufferTime } from 'rxjs/operators';
88

99
import { API_VERSION } from '@app/environment.js';
1010
import { ApiStateConfig } from '@app/unraid-api/config/factory/api-state.model.js';

api/src/unraid-api/graph/resolvers/rclone/rclone-api.service.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,23 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
3737
process.env.RCLONE_PASSWORD || crypto.randomBytes(24).toString('base64');
3838
constructor() {}
3939

40+
/**
41+
* Returns whether the RClone service is initialized and ready to use
42+
*/
43+
get initialized(): boolean {
44+
return this.isInitialized;
45+
}
46+
4047
async onModuleInit(): Promise<void> {
4148
try {
49+
// Check if rclone binary is available first
50+
const isBinaryAvailable = await this.checkRcloneBinaryExists();
51+
if (!isBinaryAvailable) {
52+
this.logger.warn('RClone binary not found on system, skipping initialization');
53+
this.isInitialized = false;
54+
return;
55+
}
56+
4257
const { getters } = await import('@app/store/index.js');
4358
// Check if Rclone Socket is running, if not, start it.
4459
this.rcloneSocketPath = getters.paths()['rclone-socket'];
@@ -198,16 +213,38 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
198213
* Checks if the RClone socket is running
199214
*/
200215
private async checkRcloneSocketRunning(): Promise<boolean> {
201-
// Use the API check instead of execa('rclone', ['about']) as rclone might not be in PATH
202-
// or configured correctly for the execa environment vs the rcd environment.
203216
try {
204217
// A simple API call to check if the daemon is responsive
205218
await this.callRcloneApi('core/pid');
206219
this.logger.debug('RClone socket is running and responsive.');
207220
return true;
208221
} catch (error: unknown) {
209-
// Log less verbosely during checks
210-
// this.logger.error(`Error checking RClone socket: ${error}`);
222+
// Silently handle socket connection errors during checks
223+
if (error instanceof Error) {
224+
if (error.message.includes('ENOENT') || error.message.includes('ECONNREFUSED')) {
225+
this.logger.debug('RClone socket not accessible - daemon likely not running');
226+
} else {
227+
this.logger.debug(`RClone socket check failed: ${error.message}`);
228+
}
229+
}
230+
return false;
231+
}
232+
}
233+
234+
/**
235+
* Checks if the RClone binary is available on the system
236+
*/
237+
private async checkRcloneBinaryExists(): Promise<boolean> {
238+
try {
239+
await execa('rclone', ['version']);
240+
this.logger.debug('RClone binary is available on the system.');
241+
return true;
242+
} catch (error: unknown) {
243+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
244+
this.logger.warn('RClone binary not found in PATH.');
245+
} else {
246+
this.logger.error(`Error checking RClone binary: ${error}`);
247+
}
211248
return false;
212249
}
213250
}

api/src/unraid-api/graph/resolvers/rclone/rclone.service.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,13 @@ export class RCloneService {
4848
*/
4949
async onModuleInit(): Promise<void> {
5050
try {
51-
await this.loadProviderInfo();
51+
if (!this.rcloneApiService.initialized) {
52+
this.logger.warn(
53+
'RClone API service is not initialized, skipping provider info loading'
54+
);
55+
} else {
56+
await this.loadProviderInfo();
57+
}
5258
} catch (error) {
5359
this.logger.error(`Failed to initialize RcloneBackupSettingsService: ${error}`);
5460
}

packages/unraid-api-plugin-connect/justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
default:
55
@just --list
66

7+
# Watch for changes in src files and run clean + build
8+
watch:
9+
watchexec -r -e ts,tsx -w src -- pnpm build
10+
711
# Count TypeScript lines in src directory, excluding test and generated files
812
count-lines:
913
#!/usr/bin/env bash

packages/unraid-api-plugin-connect/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"description": "Unraid Connect plugin for Unraid API",
2626
"devDependencies": {
2727
"@apollo/client": "^3.11.8",
28+
"@faker-js/faker": "^9.8.0",
2829
"@graphql-codegen/cli": "^5.0.3",
2930
"@graphql-typed-document-node/core": "^3.2.0",
3031
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
@@ -46,6 +47,7 @@
4647
"class-transformer": "^0.5.1",
4748
"class-validator": "^0.14.1",
4849
"execa": "^9.5.1",
50+
"fast-check": "^4.1.1",
4951
"got": "^14.4.6",
5052
"graphql": "^16.9.0",
5153
"graphql-scalars": "^1.23.0",

packages/unraid-api-plugin-connect/src/model/connect-config.model.ts

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { UsePipes, ValidationPipe } from '@nestjs/common';
22
import { registerAs } from '@nestjs/config';
33
import { Field, InputType, ObjectType } from '@nestjs/graphql';
4+
import { ValidateIf } from 'class-validator';
45

56
import { URL_TYPE } from '@unraid/shared/network.model.js';
67
import { plainToInstance } from 'class-transformer';
@@ -58,9 +59,11 @@ export class MyServersConfig {
5859
localApiKey!: string;
5960

6061
// User Information
61-
@Field(() => String)
62+
@Field(() => String, { nullable: true })
63+
@IsOptional()
64+
@ValidateIf((o) => o.email !== undefined && o.email !== null && o.email !== '')
6265
@IsEmail()
63-
email!: string;
66+
email?: string | null;
6467

6568
@Field(() => String)
6669
@IsString()
@@ -74,19 +77,6 @@ export class MyServersConfig {
7477
@IsString()
7578
regWizTime!: string;
7679

77-
// Authentication Tokens
78-
@Field(() => String)
79-
@IsString()
80-
accesstoken!: string;
81-
82-
@Field(() => String)
83-
@IsString()
84-
idtoken!: string;
85-
86-
@Field(() => String)
87-
@IsString()
88-
refreshtoken!: string;
89-
9080
// Remote Access Settings
9181
@Field(() => DynamicRemoteAccessType)
9282
@IsEnum(DynamicRemoteAccessType)
@@ -207,13 +197,9 @@ export const emptyMyServersConfig = (): MyServersConfig => ({
207197
upnpEnabled: false,
208198
apikey: '',
209199
localApiKey: '',
210-
email: '',
211200
username: '',
212201
avatar: '',
213202
regWizTime: '',
214-
accesstoken: '',
215-
idtoken: '',
216-
refreshtoken: '',
217203
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
218204
});
219205

packages/unraid-api-plugin-connect/src/model/connect.model.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,28 +93,13 @@ export class ConnectSignInInput {
9393
@MinLength(5)
9494
apiKey!: string;
9595

96-
@Field(() => String, { nullable: true, description: 'The ID token for authentication' })
97-
@IsString()
98-
@IsOptional()
99-
idToken?: string;
100-
10196
@Field(() => ConnectUserInfoInput, {
10297
nullable: true,
10398
description: 'User information for the sign-in',
10499
})
105100
@ValidateNested()
106101
@IsOptional()
107102
userInfo?: ConnectUserInfoInput;
108-
109-
@Field(() => String, { nullable: true, description: 'The access token for authentication' })
110-
@IsString()
111-
@IsOptional()
112-
accessToken?: string;
113-
114-
@Field(() => String, { nullable: true, description: 'The refresh token for authentication' })
115-
@IsString()
116-
@IsOptional()
117-
refreshToken?: string;
118103
}
119104

120105
@InputType()

packages/unraid-api-plugin-connect/src/resolver/connect.resolver.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,4 @@ export class ConnectResolver {
4343
public async settings(): Promise<ConnectSettings> {
4444
return {} as ConnectSettings;
4545
}
46-
4746
}

packages/unraid-api-plugin-connect/src/service/config.persistence.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export class ConnectConfigPersister implements OnModuleInit, OnModuleDestroy {
3434
// Persist changes to the config.
3535
this.configService.changes$.pipe(bufferTime(25)).subscribe({
3636
next: async (changes) => {
37-
const connectConfigChanged = changes.some(({ path }) => path.startsWith('connect.config'));
37+
const connectConfigChanged = changes.some(({ path }) =>
38+
path.startsWith('connect.config')
39+
);
3840
if (connectConfigChanged) {
3941
await this.persist();
4042
}
@@ -78,12 +80,14 @@ export class ConnectConfigPersister implements OnModuleInit, OnModuleDestroy {
7880
* @param config - The config object to validate.
7981
* @returns The validated config instance.
8082
*/
81-
private async validate(config: object) {
83+
public async validate(config: object) {
8284
let instance: MyServersConfig;
8385
if (config instanceof MyServersConfig) {
8486
instance = config;
8587
} else {
86-
instance = plainToInstance(MyServersConfig, config, { enableImplicitConversion: true });
88+
instance = plainToInstance(MyServersConfig, config, {
89+
enableImplicitConversion: true,
90+
});
8791
}
8892
await validateOrReject(instance);
8993
return instance;
@@ -101,7 +105,7 @@ export class ConnectConfigPersister implements OnModuleInit, OnModuleDestroy {
101105
this.logger.verbose(`Config loaded from ${this.configPath}`);
102106
return true;
103107
} catch (error) {
104-
this.logger.warn('Error loading config:', error);
108+
this.logger.warn(error, 'Error loading config');
105109
}
106110

107111
try {
@@ -150,7 +154,7 @@ export class ConnectConfigPersister implements OnModuleInit, OnModuleDestroy {
150154
* @throws {Error} - If the legacy config file does not exist.
151155
* @throws {Error} - If the legacy config file is not parse-able.
152156
*/
153-
public async convertLegacyConfig(config:LegacyConfig): Promise<MyServersConfig> {
157+
public async convertLegacyConfig(config: LegacyConfig): Promise<MyServersConfig> {
154158
return this.validate({
155159
...config.api,
156160
...config.local,

0 commit comments

Comments
 (0)