Skip to content

Commit 5ba3fa6

Browse files
pujitmelibosley
andauthored
feat: add gui settings field for sso users (#1310)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Bulk SSO account management is now supported, allowing users to update multiple SSO account IDs at once. - Connect settings now include SSO account identifiers for streamlined configuration. - Expanded array management functionality introduces advanced operations for managing disk arrays. - New fields for SSO user IDs have been added to various settings and queries. - **Style & Documentation** - Improved formatting and support for rich HTML in form descriptions enhance clarity and presentation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Eli Bosley <[email protected]>
1 parent 1f10b63 commit 5ba3fa6

File tree

10 files changed

+202
-83
lines changed

10 files changed

+202
-83
lines changed

api/src/graphql/generated/api/operations.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ export function ApiSettingsInputSchema(): z.ZodObject<Properties<ApiSettingsInpu
160160
extraOrigins: z.array(z.string()).nullish(),
161161
forwardType: WAN_FORWARD_TYPESchema.nullish(),
162162
port: z.number().nullish(),
163-
sandbox: z.boolean().nullish()
163+
sandbox: z.boolean().nullish(),
164+
ssoUserIds: z.array(z.string()).nullish()
164165
})
165166
}
166167

@@ -362,7 +363,8 @@ export function ConnectSettingsValuesSchema(): z.ZodObject<Properties<ConnectSet
362363
extraOrigins: z.array(z.string()),
363364
forwardType: WAN_FORWARD_TYPESchema.nullish(),
364365
port: z.number().nullish(),
365-
sandbox: z.boolean()
366+
sandbox: z.boolean(),
367+
ssoUserIds: z.array(z.string())
366368
})
367369
}
368370

api/src/graphql/generated/api/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export type ApiSettingsInput = {
107107
* If false, the GraphQL sandbox will be disabled and only the production API will be available.
108108
*/
109109
sandbox?: InputMaybe<Scalars['Boolean']['input']>;
110+
/** A list of Unique Unraid Account ID's. */
111+
ssoUserIds?: InputMaybe<Array<Scalars['String']['input']>>;
110112
};
111113

112114
export type ArrayType = Node & {
@@ -408,6 +410,8 @@ export type ConnectSettingsValues = {
408410
* If false, the GraphQL sandbox is disabled and only the production API will be available.
409411
*/
410412
sandbox: Scalars['Boolean']['output'];
413+
/** A list of Unique Unraid Account ID's. */
414+
ssoUserIds: Array<Scalars['String']['output']>;
411415
};
412416

413417
export type ConnectSignInInput = {
@@ -2373,6 +2377,7 @@ export type ConnectSettingsValuesResolvers<ContextType = Context, ParentType ext
23732377
forwardType?: Resolver<Maybe<ResolversTypes['WAN_FORWARD_TYPE']>, ParentType, ContextType>;
23742378
port?: Resolver<Maybe<ResolversTypes['Port']>, ParentType, ContextType>;
23752379
sandbox?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
2380+
ssoUserIds?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
23762381
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
23772382
}>;
23782383

api/src/graphql/schema/types/connect/connect.graphql

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ input SetupRemoteAccessInput {
3939
port: Port
4040
}
4141

42-
43-
4442
input EnableDynamicRemoteAccessInput {
4543
url: AccessUrlInput!
4644
enabled: Boolean!
@@ -59,59 +57,67 @@ type DynamicRemoteAccessStatus {
5957
}
6058

6159
"""
62-
Intersection type of ApiSettings and RemoteAccess
60+
Intersection type of ApiSettings and RemoteAccess
6361
"""
6462
type ConnectSettingsValues {
6563
"""
66-
If true, the GraphQL sandbox is enabled and available at /graphql.
67-
If false, the GraphQL sandbox is disabled and only the production API will be available.
64+
If true, the GraphQL sandbox is enabled and available at /graphql.
65+
If false, the GraphQL sandbox is disabled and only the production API will be available.
6866
"""
6967
sandbox: Boolean!
7068
"""
71-
A list of origins allowed to interact with the API.
69+
A list of origins allowed to interact with the API.
7270
"""
7371
extraOrigins: [String!]!
7472
"""
75-
The type of WAN access used for Remote Access.
73+
The type of WAN access used for Remote Access.
7674
"""
7775
accessType: WAN_ACCESS_TYPE!
7876
"""
79-
The type of port forwarding used for Remote Access.
77+
The type of port forwarding used for Remote Access.
8078
"""
8179
forwardType: WAN_FORWARD_TYPE
8280
"""
83-
The port used for Remote Access.
81+
The port used for Remote Access.
8482
"""
8583
port: Port
84+
"""
85+
A list of Unique Unraid Account ID's.
86+
"""
87+
ssoUserIds: [String!]!
8688
}
8789

8890
"""
89-
Input should be a subset of ApiSettings that can be updated.
90-
Some field combinations may be required or disallowed. Please refer to each field for more information.
91+
Input should be a subset of ApiSettings that can be updated.
92+
Some field combinations may be required or disallowed. Please refer to each field for more information.
9193
"""
9294
input ApiSettingsInput {
9395
"""
94-
If true, the GraphQL sandbox will be enabled and available at /graphql.
95-
If false, the GraphQL sandbox will be disabled and only the production API will be available.
96+
If true, the GraphQL sandbox will be enabled and available at /graphql.
97+
If false, the GraphQL sandbox will be disabled and only the production API will be available.
9698
"""
9799
sandbox: Boolean
98100
"""
99-
A list of origins allowed to interact with the API.
101+
A list of origins allowed to interact with the API.
100102
"""
101103
extraOrigins: [String!]
102104
"""
103-
The type of WAN access to use for Remote Access.
105+
The type of WAN access to use for Remote Access.
104106
"""
105107
accessType: WAN_ACCESS_TYPE
106108
"""
107-
The type of port forwarding to use for Remote Access.
109+
The type of port forwarding to use for Remote Access.
108110
"""
109111
forwardType: WAN_FORWARD_TYPE
110112
"""
111-
The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType.
112-
Ignored if accessType is DISABLED or forwardType is UPNP.
113+
The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType.
114+
Ignored if accessType is DISABLED or forwardType is UPNP.
113115
"""
114116
port: Port
117+
"""
118+
A list of Unique Unraid Account ID's.
119+
"""
120+
ssoUserIds: [String!]
115121
}
116122

117123
type ConnectSettings implements Node {
@@ -140,8 +146,8 @@ type Mutation {
140146
setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]!
141147
setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean!
142148
"""
143-
Update the API settings.
144-
Some setting combinations may be required or disallowed. Please refer to each setting for more information.
149+
Update the API settings.
150+
Some setting combinations may be required or disallowed. Please refer to each setting for more information.
145151
"""
146152
updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues!
147-
}
153+
}

api/src/store/modules/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ export const config = createSlice({
219219
stateAsArray.push(action.payload);
220220
state.remote.ssoSubIds = stateAsArray.join(',');
221221
},
222+
setSsoUsers(state, action: PayloadAction<string[]>) {
223+
state.remote.ssoSubIds = action.payload.filter((id) => id).join(',');
224+
},
222225
removeSsoUser(state, action: PayloadAction<string | null>) {
223226
if (action.payload === null) {
224227
state.remote.ssoSubIds = '';
@@ -309,6 +312,7 @@ const { actions, reducer } = config;
309312

310313
export const {
311314
addSsoUser,
315+
setSsoUsers,
312316
updateUserConfig,
313317
updateAccessTokens,
314318
updateAllowedOrigins,
@@ -324,6 +328,7 @@ export const {
324328
*/
325329
export const configUpdateActionsFlash = isAnyOf(
326330
addSsoUser,
331+
setSsoUsers,
327332
updateUserConfig,
328333
updateAccessTokens,
329334
updateAllowedOrigins,

api/src/unraid-api/graph/connect/connect-settings.service.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
WAN_FORWARD_TYPE,
1919
} from '@app/graphql/generated/api/types.js';
2020
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js';
21-
import { updateAllowedOrigins, updateUserConfig } from '@app/store/modules/config.js';
21+
import { setSsoUsers, updateAllowedOrigins, updateUserConfig } from '@app/store/modules/config.js';
2222
import { mergeSettingSlices } from '@app/unraid-api/types/json-forms.js';
2323
import { csvStringToArray } from '@app/utils.js';
2424

@@ -50,11 +50,12 @@ export class ConnectSettingsService {
5050

5151
async getCurrentSettings(): Promise<ConnectSettingsValues> {
5252
const { getters } = await import('@app/store/index.js');
53-
const { local, api } = getters.config();
53+
const { local, api, remote } = getters.config();
5454
return {
5555
...(await this.dynamicRemoteAccessSettings()),
5656
sandbox: local.sandbox === 'yes',
5757
extraOrigins: csvStringToArray(api.extraOrigins),
58+
ssoUserIds: csvStringToArray(remote.ssoSubIds),
5859
};
5960
}
6061

@@ -63,7 +64,7 @@ export class ConnectSettingsService {
6364
* @param settings - The settings to sync
6465
* @returns true if a restart is required, false otherwise
6566
*/
66-
async syncSettings(settings: Partial<ApiSettingsInput>) {
67+
async syncSettings(settings: Partial<ApiSettingsInput>): Promise<boolean> {
6768
let restartRequired = false;
6869
const { getters } = await import('@app/store/index.js');
6970
const { nginx } = getters.emhttp();
@@ -86,13 +87,15 @@ export class ConnectSettingsService {
8687
port: settings.port,
8788
});
8889
}
89-
9090
if (settings.extraOrigins) {
9191
await this.updateAllowedOrigins(settings.extraOrigins);
9292
}
9393
if (typeof settings.sandbox === 'boolean') {
9494
restartRequired ||= await this.setSandboxMode(settings.sandbox);
9595
}
96+
if (settings.ssoUserIds) {
97+
restartRequired ||= await this.updateSSOUsers(settings.ssoUserIds);
98+
}
9699
const { writeConfigSync } = await import('@app/store/sync/config-disk-sync.js');
97100
writeConfigSync('flash');
98101
return restartRequired;
@@ -117,6 +120,32 @@ export class ConnectSettingsService {
117120
return true;
118121
}
119122

123+
/**
124+
* Updates the SSO users and returns true if a restart is required
125+
* @param userIds - The list of SSO user IDs
126+
* @returns true if a restart is required, false otherwise
127+
*/
128+
private async updateSSOUsers(userIds: string[]): Promise<boolean> {
129+
const { ssoUserIds } = await this.getCurrentSettings();
130+
const currentUserSet = new Set(ssoUserIds);
131+
const newUserSet = new Set(userIds);
132+
if (newUserSet.symmetricDifference(currentUserSet).size === 0) {
133+
// there's no change, so no need to update
134+
return false;
135+
}
136+
// make sure we aren't adding invalid user ids
137+
const uuidRegex =
138+
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
139+
const invalidUserIds = userIds.filter((id) => !uuidRegex.test(id));
140+
if (invalidUserIds.length > 0) {
141+
throw new GraphQLError(`Invalid SSO user ID's: ${invalidUserIds.join(', ')}`);
142+
}
143+
const { store } = await import('@app/store/index.js');
144+
store.dispatch(setSsoUsers(userIds));
145+
// request a restart if we're there were no sso users before
146+
return currentUserSet.size === 0;
147+
}
148+
120149
private async updateRemoteAccess(input: SetupRemoteAccessInput): Promise<boolean> {
121150
const { store } = await import('@app/store/index.js');
122151
await store.dispatch(setupRemoteAccessThunk(input)).unwrap();
@@ -151,6 +180,7 @@ export class ConnectSettingsService {
151180
await this.remoteAccessSlice(),
152181
await this.sandboxSlice(),
153182
this.flashBackupSlice(),
183+
this.ssoUsersSlice(),
154184
// Because CORS is effectively disabled, this setting is no longer necessary
155185
// keeping it here for in case it needs to be re-enabled
156186
//
@@ -344,4 +374,32 @@ export class ConnectSettingsService {
344374
],
345375
};
346376
}
377+
378+
/**
379+
* Extra origins settings slice
380+
*/
381+
ssoUsersSlice(): SettingSlice {
382+
return {
383+
properties: {
384+
ssoUserIds: {
385+
type: 'array',
386+
items: {
387+
type: 'string',
388+
},
389+
title: 'Unraid API SSO Users',
390+
description: `Provide a list of Unique Unraid Account ID's. Find yours at <a href="https://account.unraid.net/settings" target="_blank">account.unraid.net/settings</a>`,
391+
},
392+
},
393+
elements: [
394+
{
395+
type: 'Control',
396+
scope: '#/properties/ssoUserIds',
397+
options: {
398+
inputType: 'text',
399+
placeholder: 'UUID',
400+
},
401+
},
402+
],
403+
};
404+
}
347405
}

api/src/unraid-api/graph/connect/connect.resolver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ export class ConnectResolver implements ConnectResolvers {
6969
const restartRequired = await this.connectSettingsService.syncSettings(settings);
7070
const currentSettings = await this.connectSettingsService.getCurrentSettings();
7171
if (restartRequired) {
72-
const restartDelayMs = 3_000;
7372
setTimeout(async () => {
73+
// Send restart out of band to avoid blocking the return of this resolver
7474
this.logger.log('Restarting API');
7575
await this.connectService.restartApi();
76-
}, restartDelayMs);
76+
}, 300);
7777
}
7878
return currentSettings;
7979
}

unraid-ui/src/forms/StringArrayField.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const placeholder = computed(() => control.value.uischema?.options?.placeholder
4444
<template>
4545
<ControlLayout v-if="control.visible" :label="control.label" :errors="control.errors">
4646
<div class="space-y-4">
47-
<p v-if="control.description">{{ control.description }}</p>
47+
<p v-if="control.description" v-html="control.description" />
4848
<div v-for="(item, index) in items" :key="index" class="flex gap-2">
4949
<Input
5050
:type="inputType"

web/components/ConnectSettings/graphql/settings.query.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const getConnectSettingsForm = graphql(/* GraphQL */ `
1414
accessType
1515
forwardType
1616
port
17+
ssoUserIds
1718
}
1819
}
1920
}
@@ -28,6 +29,7 @@ export const updateConnectSettings = graphql(/* GraphQL */ `
2829
accessType
2930
forwardType
3031
port
32+
ssoUserIds
3133
}
3234
}
3335
`);

web/composables/gql/gql.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
1414
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
1515
*/
1616
type Documents = {
17-
"\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n }\n }\n }\n }\n": typeof types.GetConnectSettingsFormDocument,
18-
"\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n }\n }\n": typeof types.UpdateConnectSettingsDocument,
17+
"\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n }\n }\n": typeof types.GetConnectSettingsFormDocument,
18+
"\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n": typeof types.UpdateConnectSettingsDocument,
1919
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": typeof types.LogFilesDocument,
2020
"\n query LogFileContent($path: String!, $lines: Int, $startLine: Int) {\n logFile(path: $path, lines: $lines, startLine: $startLine) {\n path\n content\n totalLines\n startLine\n }\n }\n": typeof types.LogFileContentDocument,
2121
"\n subscription LogFileSubscription($path: String!) {\n logFile(path: $path) {\n path\n content\n totalLines\n }\n }\n": typeof types.LogFileSubscriptionDocument,
@@ -40,8 +40,8 @@ type Documents = {
4040
"\n mutation setupRemoteAccess($input: SetupRemoteAccessInput!) {\n setupRemoteAccess(input: $input)\n }\n": typeof types.setupRemoteAccessDocument,
4141
};
4242
const documents: Documents = {
43-
"\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n }\n }\n }\n }\n": types.GetConnectSettingsFormDocument,
44-
"\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n }\n }\n": types.UpdateConnectSettingsDocument,
43+
"\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n }\n }\n": types.GetConnectSettingsFormDocument,
44+
"\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n": types.UpdateConnectSettingsDocument,
4545
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": types.LogFilesDocument,
4646
"\n query LogFileContent($path: String!, $lines: Int, $startLine: Int) {\n logFile(path: $path, lines: $lines, startLine: $startLine) {\n path\n content\n totalLines\n startLine\n }\n }\n": types.LogFileContentDocument,
4747
"\n subscription LogFileSubscription($path: String!) {\n logFile(path: $path) {\n path\n content\n totalLines\n }\n }\n": types.LogFileSubscriptionDocument,
@@ -83,11 +83,11 @@ export function graphql(source: string): unknown;
8383
/**
8484
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
8585
*/
86-
export function graphql(source: "\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n }\n }\n }\n }\n"): (typeof documents)["\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n }\n }\n }\n }\n"];
86+
export function graphql(source: "\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n }\n }\n"): (typeof documents)["\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n }\n }\n"];
8787
/**
8888
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
8989
*/
90-
export function graphql(source: "\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n }\n }\n"): (typeof documents)["\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n }\n }\n"];
90+
export function graphql(source: "\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n"): (typeof documents)["\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n"];
9191
/**
9292
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
9393
*/

0 commit comments

Comments
 (0)