Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/generated-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2697,6 +2697,9 @@ type Server implements Node {
lanip: String!
localurl: String!
remoteurl: String!

"""Preferred live URL from nginx.ini defaultUrl"""
defaultUrl: String
}

enum ServerStatus {
Expand Down
20 changes: 20 additions & 0 deletions api/src/unraid-api/avahi/avahi.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable, Logger } from '@nestjs/common';

import { execa } from 'execa';

@Injectable()
export class AvahiService {
private readonly logger = new Logger(AvahiService.name);

async restart(): Promise<void> {
try {
await execa('/etc/rc.d/rc.avahidaemon', ['restart'], {
timeout: 10_000,
});
this.logger.log('Avahi daemon restarted');
} catch (error) {
this.logger.error('Failed to restart Avahi daemon', error as Error);
throw error;
}
}
}
2 changes: 2 additions & 0 deletions api/src/unraid-api/cli/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2685,6 +2685,8 @@ export type Server = Node & {
apikey: Scalars['String']['output'];
/** Server description/comment */
comment?: Maybe<Scalars['String']['output']>;
/** Preferred live URL from nginx.ini defaultUrl */
defaultUrl?: Maybe<Scalars['String']['output']>;
guid: Scalars['String']['output'];
id: Scalars['PrefixedID']['output'];
lanip: Scalars['String']['output'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2005,5 +2005,26 @@ describe('OnboardingService - updateCfgFile', () => {
},
});
});

it('skips saving the onboarding draft when the expected server name does not match the live identity', async () => {
vi.spyOn(getters, 'emhttp').mockReturnValue({
...getters.emhttp(),
nginx: {
...getters.emhttp().nginx,
lanName: 'Tower',
},
});

await expect(
service.saveOnboardingDraft({
navigation: {
currentStepId: OnboardingWizardStepId.NEXT_STEPS,
},
expectedServerName: 'Cheese',
})
).resolves.toBe(false);

expect(onboardingTrackerMock.saveDraft).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,19 @@ export class OnboardingService implements OnModuleInit {
this.onboardingTracker.setBypassActive(false);
}

public async saveOnboardingDraft(input: SaveOnboardingDraftInput): Promise<void> {
public async saveOnboardingDraft(input: SaveOnboardingDraftInput): Promise<boolean> {
if (input.expectedServerName) {
// Rename flows use this guard to avoid persisting a resume step until
// the live server identity has actually switched to the expected host.
const liveServerName = getters.emhttp().nginx?.lanName?.trim() ?? '';
if (liveServerName !== input.expectedServerName.trim()) {
this.logger.warn(
`Skipping onboarding draft save because live server name '${liveServerName}' did not match expected '${input.expectedServerName}'.`
);
return false;
}
}

await this.onboardingTracker.saveDraft({
draft: input.draft,
navigation: input.navigation
Expand All @@ -547,6 +559,7 @@ export class OnboardingService implements OnModuleInit {
}
: undefined,
});
return true;
}

public isFreshInstall(): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,11 @@ export class SaveOnboardingDraftInput {
@IsOptional()
draft?: OnboardingDraft;

@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
expectedServerName?: string;

@Field(() => OnboardingWizardNavigationInput, { nullable: true })
@IsOptional()
@ValidateNested()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe('OnboardingMutationsResolver', () => {
onboardingService.closeOnboarding.mockResolvedValue(undefined);
onboardingService.bypassOnboarding.mockResolvedValue(undefined);
onboardingService.resumeOnboarding.mockResolvedValue(undefined);
onboardingService.saveOnboardingDraft.mockResolvedValue(undefined);
onboardingService.saveOnboardingDraft.mockResolvedValue(true);
onboardingService.getOnboardingResponse.mockResolvedValue(defaultOnboardingResponse);

resolver = createResolver();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,7 @@ export class OnboardingMutationsResolver {
resource: Resource.WELCOME,
})
async saveOnboardingDraft(@Args('input') input: SaveOnboardingDraftInput): Promise<boolean> {
await this.onboardingService.saveOnboardingDraft(input);
return true;
return this.onboardingService.saveOnboardingDraft(input);
}

@ResolveField(() => OnboardingInternalBootResult, {
Expand Down
2 changes: 2 additions & 0 deletions api/src/unraid-api/graph/resolvers/resolvers.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';

import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
import { AvahiService } from '@app/unraid-api/avahi/avahi.service.js';
import { ApiConfigModule } from '@app/unraid-api/config/api-config.module.js';
import { OnboardingOverrideModule } from '@app/unraid-api/config/onboarding-override.module.js';
import { OnboardingStateModule } from '@app/unraid-api/config/onboarding-state.module.js';
Expand Down Expand Up @@ -83,6 +84,7 @@ import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js';
RootMutationsResolver,
ServerResolver,
ServerService,
AvahiService,
ServicesResolver,
SharesResolver,
VarsResolver,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { type SliceState } from '@app/store/modules/emhttp.js';
import {
ProfileModel,
Server,
ServerStatus,
} from '@app/unraid-api/graph/resolvers/servers/server.model.js';

type BuildServerResponseOptions = {
apikey?: string;
comment?: string;
name?: string;
owner?: Partial<ProfileModel>;
};

const DEFAULT_OWNER: ProfileModel = {
id: 'local',
username: 'root',
url: '',
avatar: '',
};

export const buildServerResponse = (
emhttpState: SliceState,
{ apikey = '', comment, name, owner }: BuildServerResponseOptions = {}
): Server => {
const lanip = emhttpState.networks?.[0]?.ipaddr?.[0] ?? '';
const port = emhttpState.var?.port ?? '';
const defaultUrl = emhttpState.nginx?.defaultUrl?.trim() || undefined;

return {
id: 'local',
owner: {
...DEFAULT_OWNER,
...owner,
},
guid: emhttpState.var?.regGuid ?? '',
apikey,
name: name ?? emhttpState.var?.name ?? 'Local Server',
comment: comment ?? emhttpState.var?.comment,
status: ServerStatus.ONLINE,
wanip: '',
lanip,
localurl: lanip ? `http://${lanip}${port ? `:${port}` : ''}` : '',
remoteurl: '',
defaultUrl,
};
};
3 changes: 3 additions & 0 deletions api/src/unraid-api/graph/resolvers/servers/server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,7 @@ export class Server extends Node {

@Field()
remoteurl!: string;

@Field({ nullable: true, description: 'Preferred live URL from nginx.ini defaultUrl' })
defaultUrl?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,44 @@ import { Test } from '@nestjs/testing';

import { beforeEach, describe, expect, it, vi } from 'vitest';

import { getters } from '@app/store/index.js';
import { ServerResolver } from '@app/unraid-api/graph/resolvers/servers/server.resolver.js';
import { ServerService } from '@app/unraid-api/graph/resolvers/servers/server.service.js';

vi.mock('@app/store/index.js', () => ({
getters: {
emhttp: vi.fn(),
},
}));

describe('ServersResolver', () => {
let resolver: ServerResolver;
let mockConfigService: { get: ReturnType<typeof vi.fn> };

beforeEach(async () => {
const mockConfigService = {
mockConfigService = {
get: vi.fn(),
};

vi.mocked(getters.emhttp).mockReturnValue({
var: {
regGuid: 'GUID-123',
name: 'Tower',
comment: 'Primary host',
port: 80,
},
networks: [{ ipaddr: ['192.168.1.10'] }],
nginx: {
defaultUrl: 'https://Tower.local:4443',
},
} as ReturnType<typeof getters.emhttp>);
mockConfigService.get.mockReturnValue({
config: {
username: 'ajit',
apikey: 'api-key-123',
},
});

const module: TestingModule = await Test.createTestingModule({
providers: [
ServerResolver,
Expand All @@ -35,4 +62,26 @@ describe('ServersResolver', () => {
it('should be defined', () => {
expect(resolver).toBeDefined();
});

it('returns the shared server shape with defaultUrl for the server query', async () => {
await expect(resolver.server()).resolves.toEqual({
id: 'local',
owner: {
id: 'local',
username: 'ajit',
url: '',
avatar: '',
},
guid: 'GUID-123',
apikey: 'api-key-123',
name: 'Tower',
comment: 'Primary host',
status: 'ONLINE',
wanip: '',
lanip: '192.168.1.10',
localurl: 'http://192.168.1.10:80',
remoteurl: '',
defaultUrl: 'https://Tower.local:4443',
});
});
});
41 changes: 7 additions & 34 deletions api/src/unraid-api/graph/resolvers/servers/server.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@ import { UsePermissions } from '@unraid/shared/use-permissions.directive.js';
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
import { getters } from '@app/store/index.js';
import { MinigraphStatus } from '@app/unraid-api/graph/resolvers/cloud/cloud.model.js';
import {
ProfileModel,
Server as ServerModel,
ServerStatus,
} from '@app/unraid-api/graph/resolvers/servers/server.model.js';
import { buildServerResponse } from '@app/unraid-api/graph/resolvers/servers/build-server-response.js';
import { Server as ServerModel } from '@app/unraid-api/graph/resolvers/servers/server.model.js';
import { ServerService } from '@app/unraid-api/graph/resolvers/servers/server.service.js';

@Injectable()
Expand Down Expand Up @@ -65,35 +62,11 @@ export class ServerResolver {
private getLocalServer(): ServerModel {
const emhttp = getters.emhttp();
const connectConfig = this.configService.get('connect');

const guid = emhttp.var.regGuid;
const name = emhttp.var.name;
const comment = emhttp.var.comment;
const wanip = '';
const lanip: string = emhttp.networks[0]?.ipaddr[0] || '';
const port = emhttp.var?.port;
const localurl = `http://${lanip}:${port}`;
const remoteurl = '';

const owner: ProfileModel = {
id: 'local',
username: connectConfig?.config?.username ?? 'root',
url: '',
avatar: '',
};

return {
id: 'local',
owner,
guid: guid || '',
return buildServerResponse(emhttp, {
apikey: connectConfig?.config?.apikey ?? '',
name: name ?? 'Local Server',
comment,
status: ServerStatus.ONLINE,
wanip,
lanip,
localurl,
remoteurl,
};
owner: {
username: connectConfig?.config?.username ?? 'root',
},
});
}
}
Loading
Loading