mirror of
https://github.com/unraid/api.git
synced 2026-01-06 00:30:22 -06:00
feat: api sign in / out (#642)
* feat: initial commit * fix: minor issues with sign in endpoint * feat: Permission check bypassing error * test: fix snapshot
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="3.1.1+bf51f1f0"
|
||||
version="3.1.1+251f9020"
|
||||
[local]
|
||||
[notifier]
|
||||
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="3.1.1+bf51f1f0"
|
||||
version="3.1.1+251f9020"
|
||||
[local]
|
||||
[notifier]
|
||||
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"stop:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs stop --debug'",
|
||||
"start:report": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development LOG_CONTEXT=true tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs report --debug'",
|
||||
"start:docker": "docker compose run --rm builder-interactive",
|
||||
"docker:dev": "docker-compose run --rm --service-ports dev"
|
||||
"docker:dev": "docker-compose run --rm --service-ports dev",
|
||||
"docker:test": "docker-compose run --rm builder npm run test"
|
||||
},
|
||||
"files": [
|
||||
".env.staging",
|
||||
|
||||
@@ -329,6 +329,16 @@ exports[`Returns default permissions 1`] = `
|
||||
"attributes": "*",
|
||||
"resource": "vars",
|
||||
},
|
||||
{
|
||||
"action": "read:own",
|
||||
"attributes": "*",
|
||||
"resource": "connect",
|
||||
},
|
||||
{
|
||||
"action": "update:own",
|
||||
"attributes": "*",
|
||||
"resource": "connect",
|
||||
},
|
||||
],
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -79,6 +79,8 @@ export const upc: Role = {
|
||||
{ resource: 'registration', action: 'read:any', attributes: '*' },
|
||||
{ resource: 'servers', action: 'read:any', attributes: '*' },
|
||||
{ resource: 'vars', action: 'read:any', attributes: '*' },
|
||||
{ resource: 'connect', action: 'read:own', attributes: '*' },
|
||||
{ resource: 'connect', action: 'update:own', attributes: '*' }
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,30 +1,49 @@
|
||||
import { PermissionError } from '@app/core/errors/permission-error';
|
||||
import { type User } from '@app/core/types/states/user';
|
||||
import { checkPermission, type AccessControlOptions } from '@app/core/utils/permissions/check-permission';
|
||||
import {
|
||||
checkPermission,
|
||||
type AccessControlOptions,
|
||||
} from '@app/core/utils/permissions/check-permission';
|
||||
import { logger } from '@app/core/log';
|
||||
import { BYPASS_PERMISSION_CHECKS, NODE_ENV } from '@app/environment';
|
||||
|
||||
/**
|
||||
* Ensure the user has the correct permissions.
|
||||
* @param user The user to check permissions on.
|
||||
* @param options A permissions object.
|
||||
*/
|
||||
export const ensurePermission = (user: User | undefined, options: AccessControlOptions) => {
|
||||
const { resource, action, possession = 'own' } = options;
|
||||
export const ensurePermission = (
|
||||
user: User | undefined,
|
||||
options: AccessControlOptions
|
||||
) => {
|
||||
const { resource, action, possession = 'own' } = options;
|
||||
|
||||
// Bail if no user was passed
|
||||
if (!user) throw new PermissionError(`No user provided for authentication check when trying to access "${resource}".`);
|
||||
// Bail if no user was passed
|
||||
if (!user)
|
||||
throw new PermissionError(
|
||||
`No user provided for authentication check when trying to access "${resource}".`
|
||||
);
|
||||
|
||||
const permissionGranted = checkPermission(user, {
|
||||
resource,
|
||||
action,
|
||||
possession,
|
||||
});
|
||||
const permissionGranted = checkPermission(user, {
|
||||
resource,
|
||||
action,
|
||||
possession,
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development' && process.env.BYPASS_PERMISSION_CHECKS && !permissionGranted) {
|
||||
logger.warn(`BYPASSING_PERMISSION_CHECK: ${user.name} doesn't have permission to access "${resource}".`);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
NODE_ENV === 'development' &&
|
||||
BYPASS_PERMISSION_CHECKS === true &&
|
||||
!permissionGranted
|
||||
) {
|
||||
logger.warn(
|
||||
`BYPASSING_PERMISSION_CHECK: ${user.name} doesn't have permission to access "${resource}".`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail if user doesn't have permission
|
||||
if (!permissionGranted) throw new PermissionError(`${user.name} doesn't have permission to access "${resource}".`);
|
||||
// Bail if user doesn't have permission
|
||||
if (!permissionGranted)
|
||||
throw new PermissionError(
|
||||
`${user.name} doesn't have permission to access "${resource}".`
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,4 +12,5 @@ export const GRAPHQL_INTROSPECTION = Boolean(
|
||||
INTROSPECTION ?? DEBUG ?? ENVIRONMENT !== 'production'
|
||||
);
|
||||
export const PORT = process.env.PORT ?? '/var/run/unraid-api.sock';
|
||||
export const DRY_RUN = process.env.DRY_RUN === 'true';
|
||||
export const DRY_RUN = process.env.DRY_RUN === 'true';
|
||||
export const BYPASS_PERMISSION_CHECKS = process.env.BYPASS_PERMISSION_CHECKS === 'true';
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as Types from '@app/graphql/generated/api/types';
|
||||
|
||||
import { z } from 'zod'
|
||||
import { ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Device, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, Permissions, ProfileModel, Registration, RegistrationState, RelayResponse, Scope, Server, ServerStatus, Service, Share, System, Temperature, Theme, TwoFactorLocal, TwoFactorRemote, TwoFactorWithToken, TwoFactorWithoutToken, UnassignedDevice, Uptime, Usb, User, Vars, Versions, VmDomain, VmNetwork, VmState, Vms, Welcome, addApiKeyInput, addScopeInput, addScopeToApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, testMutationInput, testQueryInput, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
|
||||
import { AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Device, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, Permissions, ProfileModel, Registration, RegistrationState, RelayResponse, Scope, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, TwoFactorLocal, TwoFactorRemote, TwoFactorWithToken, TwoFactorWithoutToken, UnassignedDevice, Uptime, Usb, User, Vars, Versions, VmDomain, VmNetwork, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addScopeInput, addScopeToApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
|
||||
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
|
||||
type Properties<T> = Required<{
|
||||
@@ -15,6 +15,12 @@ export const isDefinedNonNullAny = (v: any): v is definedNonNullAny => v !== und
|
||||
|
||||
export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny(v));
|
||||
|
||||
export function AllowedOriginInputSchema(): z.ZodObject<Properties<AllowedOriginInput>> {
|
||||
return z.object<Properties<AllowedOriginInput>>({
|
||||
origins: z.array(z.string())
|
||||
})
|
||||
}
|
||||
|
||||
export function ApiKeySchema(): z.ZodObject<Properties<ApiKey>> {
|
||||
return z.object<Properties<ApiKey>>({
|
||||
__typename: z.literal('ApiKey').optional(),
|
||||
@@ -155,6 +161,24 @@ export function ConfigSchema(): z.ZodObject<Properties<Config>> {
|
||||
|
||||
export const ConfigErrorStateSchema = z.nativeEnum(ConfigErrorState);
|
||||
|
||||
export function ConnectSignInInputSchema(): z.ZodObject<Properties<ConnectSignInInput>> {
|
||||
return z.object<Properties<ConnectSignInInput>>({
|
||||
accessToken: z.string().nullish(),
|
||||
apiKey: z.string(),
|
||||
idToken: z.string().nullish(),
|
||||
refreshToken: z.string().nullish(),
|
||||
userInfo: z.lazy(() => ConnectUserInfoInputSchema().nullish())
|
||||
})
|
||||
}
|
||||
|
||||
export function ConnectUserInfoInputSchema(): z.ZodObject<Properties<ConnectUserInfoInput>> {
|
||||
return z.object<Properties<ConnectUserInfoInput>>({
|
||||
avatar: z.string().nullish(),
|
||||
email: z.string(),
|
||||
preferred_username: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function ContainerHostConfigSchema(): z.ZodObject<Properties<ContainerHostConfig>> {
|
||||
return z.object<Properties<ContainerHostConfig>>({
|
||||
__typename: z.literal('ContainerHostConfig').optional(),
|
||||
@@ -715,6 +739,14 @@ export function ServiceSchema(): z.ZodObject<Properties<Service>> {
|
||||
})
|
||||
}
|
||||
|
||||
export function SetupRemoteAccessInputSchema(): z.ZodObject<Properties<SetupRemoteAccessInput>> {
|
||||
return z.object<Properties<SetupRemoteAccessInput>>({
|
||||
accessType: WAN_ACCESS_TYPESchema,
|
||||
forwardType: WAN_FORWARD_TYPESchema.nullish(),
|
||||
port: z.number().nullish()
|
||||
})
|
||||
}
|
||||
|
||||
export function ShareSchema(): z.ZodObject<Properties<Share>> {
|
||||
return z.object<Properties<Share>>({
|
||||
__typename: z.literal('Share').optional(),
|
||||
@@ -1072,6 +1104,10 @@ export function VmsSchema(): z.ZodObject<Properties<Vms>> {
|
||||
})
|
||||
}
|
||||
|
||||
export const WAN_ACCESS_TYPESchema = z.nativeEnum(WAN_ACCESS_TYPE);
|
||||
|
||||
export const WAN_FORWARD_TYPESchema = z.nativeEnum(WAN_FORWARD_TYPE);
|
||||
|
||||
export function WelcomeSchema(): z.ZodObject<Properties<Welcome>> {
|
||||
return z.object<Properties<Welcome>>({
|
||||
__typename: z.literal('Welcome').optional(),
|
||||
@@ -1132,19 +1168,6 @@ export const mdStateSchema = z.nativeEnum(mdState);
|
||||
|
||||
export const registrationTypeSchema = z.nativeEnum(registrationType);
|
||||
|
||||
export function testMutationInputSchema(): z.ZodObject<Properties<testMutationInput>> {
|
||||
return z.object<Properties<testMutationInput>>({
|
||||
state: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function testQueryInputSchema(): z.ZodObject<Properties<testQueryInput>> {
|
||||
return z.object<Properties<testQueryInput>>({
|
||||
optional: z.boolean().nullish(),
|
||||
state: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function updateApikeyInputSchema(): z.ZodObject<Properties<updateApikeyInput>> {
|
||||
return z.object<Properties<updateApikeyInput>>({
|
||||
description: z.string().nullish(),
|
||||
|
||||
@@ -17,9 +17,14 @@ export type Scalars = {
|
||||
DateTime: string;
|
||||
JSON: { [key: string]: any };
|
||||
Long: number;
|
||||
Port: number;
|
||||
UUID: string;
|
||||
};
|
||||
|
||||
export type AllowedOriginInput = {
|
||||
origins: Array<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ApiKey = {
|
||||
__typename?: 'ApiKey';
|
||||
description?: Maybe<Scalars['String']>;
|
||||
@@ -238,6 +243,20 @@ export enum ConfigErrorState {
|
||||
WITHDRAWN = 'WITHDRAWN'
|
||||
}
|
||||
|
||||
export type ConnectSignInInput = {
|
||||
accessToken?: InputMaybe<Scalars['String']>;
|
||||
apiKey: Scalars['String'];
|
||||
idToken?: InputMaybe<Scalars['String']>;
|
||||
refreshToken?: InputMaybe<Scalars['String']>;
|
||||
userInfo?: InputMaybe<ConnectUserInfoInput>;
|
||||
};
|
||||
|
||||
export type ConnectUserInfoInput = {
|
||||
avatar?: InputMaybe<Scalars['String']>;
|
||||
email: Scalars['String'];
|
||||
preferred_username: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ContainerHostConfig = {
|
||||
__typename?: 'ContainerHostConfig';
|
||||
networkMode: Scalars['String'];
|
||||
@@ -564,6 +583,8 @@ export type Mutation = {
|
||||
/** Cancel parity check */
|
||||
cancelParityCheck?: Maybe<Scalars['JSON']>;
|
||||
clearArrayDiskStatistics?: Maybe<Scalars['JSON']>;
|
||||
connectSignIn: Scalars['Boolean'];
|
||||
connectSignOut: Scalars['Boolean'];
|
||||
/** Delete a user */
|
||||
deleteUser?: Maybe<User>;
|
||||
/** Get an existing API key */
|
||||
@@ -578,6 +599,8 @@ export type Mutation = {
|
||||
/** Resume parity check */
|
||||
resumeParityCheck?: Maybe<Scalars['JSON']>;
|
||||
sendNotification?: Maybe<Notification>;
|
||||
setAdditionalAllowedOrigins: Array<Scalars['String']>;
|
||||
setupRemoteAccess: Scalars['Boolean'];
|
||||
shutdown?: Maybe<Scalars['String']>;
|
||||
/** Start array */
|
||||
startArray?: Maybe<ArrayType>;
|
||||
@@ -585,7 +608,6 @@ export type Mutation = {
|
||||
startParityCheck?: Maybe<Scalars['JSON']>;
|
||||
/** Stop array */
|
||||
stopArray?: Maybe<ArrayType>;
|
||||
testMutation?: Maybe<Scalars['JSON']>;
|
||||
unmountArrayDisk?: Maybe<Disk>;
|
||||
/** Update an existing API key */
|
||||
updateApikey?: Maybe<ApiKey>;
|
||||
@@ -623,6 +645,11 @@ export type MutationclearArrayDiskStatisticsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationconnectSignInArgs = {
|
||||
input: ConnectSignInInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationdeleteUserArgs = {
|
||||
input: deleteUserInput;
|
||||
};
|
||||
@@ -655,14 +682,18 @@ export type MutationsendNotificationArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationstartParityCheckArgs = {
|
||||
correct?: InputMaybe<Scalars['Boolean']>;
|
||||
export type MutationsetAdditionalAllowedOriginsArgs = {
|
||||
input: AllowedOriginInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationtestMutationArgs = {
|
||||
id: Scalars['String'];
|
||||
input?: InputMaybe<testMutationInput>;
|
||||
export type MutationsetupRemoteAccessArgs = {
|
||||
input: SetupRemoteAccessInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationstartParityCheckArgs = {
|
||||
correct?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -886,7 +917,6 @@ export type Query = {
|
||||
servers: Array<Server>;
|
||||
/** Network Shares */
|
||||
shares?: Maybe<Array<Maybe<Share>>>;
|
||||
testQuery?: Maybe<Scalars['JSON']>;
|
||||
twoFactor?: Maybe<TwoFactorWithToken>;
|
||||
unassignedDevices?: Maybe<Array<Maybe<UnassignedDevice>>>;
|
||||
/** User account */
|
||||
@@ -937,12 +967,6 @@ export type QueryserverArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QuerytestQueryArgs = {
|
||||
id: Scalars['String'];
|
||||
input?: InputMaybe<testQueryInput>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryuserArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
@@ -1051,6 +1075,12 @@ export type Service = {
|
||||
version?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type SetupRemoteAccessInput = {
|
||||
accessType: WAN_ACCESS_TYPE;
|
||||
forwardType?: InputMaybe<WAN_FORWARD_TYPE>;
|
||||
port?: InputMaybe<Scalars['Port']>;
|
||||
};
|
||||
|
||||
/** Network Share */
|
||||
export type Share = {
|
||||
__typename?: 'Share';
|
||||
@@ -1105,7 +1135,6 @@ export type Subscription = {
|
||||
service?: Maybe<Array<Service>>;
|
||||
share: Share;
|
||||
shares?: Maybe<Array<Share>>;
|
||||
testSubscription: Scalars['String'];
|
||||
twoFactor?: Maybe<TwoFactorWithoutToken>;
|
||||
unassignedDevices?: Maybe<Array<UnassignedDevice>>;
|
||||
user: User;
|
||||
@@ -1503,6 +1532,17 @@ export type Vms = {
|
||||
domain?: Maybe<Array<VmDomain>>;
|
||||
};
|
||||
|
||||
export enum WAN_ACCESS_TYPE {
|
||||
ALWAYS = 'ALWAYS',
|
||||
DISABLED = 'DISABLED',
|
||||
DYNAMIC = 'DYNAMIC'
|
||||
}
|
||||
|
||||
export enum WAN_FORWARD_TYPE {
|
||||
STATIC = 'STATIC',
|
||||
UPNP = 'UPNP'
|
||||
}
|
||||
|
||||
export type Welcome = {
|
||||
__typename?: 'Welcome';
|
||||
message: Scalars['String'];
|
||||
@@ -1566,15 +1606,6 @@ export enum registrationType {
|
||||
TRIAL = 'TRIAL'
|
||||
}
|
||||
|
||||
export type testMutationInput = {
|
||||
state: Scalars['String'];
|
||||
};
|
||||
|
||||
export type testQueryInput = {
|
||||
optional?: InputMaybe<Scalars['Boolean']>;
|
||||
state: Scalars['String'];
|
||||
};
|
||||
|
||||
export type updateApikeyInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
expiresAt: Scalars['Long'];
|
||||
@@ -1656,6 +1687,7 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
|
||||
|
||||
/** Mapping between all available schema types and the resolvers types */
|
||||
export type ResolversTypes = ResolversObject<{
|
||||
AllowedOriginInput: AllowedOriginInput;
|
||||
ApiKey: ResolverTypeWrapper<ApiKey>;
|
||||
ApiKeyResponse: ResolverTypeWrapper<ApiKeyResponse>;
|
||||
Array: ResolverTypeWrapper<ArrayType>;
|
||||
@@ -1674,6 +1706,8 @@ export type ResolversTypes = ResolversObject<{
|
||||
CloudResponse: ResolverTypeWrapper<CloudResponse>;
|
||||
Config: ResolverTypeWrapper<Config>;
|
||||
ConfigErrorState: ConfigErrorState;
|
||||
ConnectSignInInput: ConnectSignInInput;
|
||||
ConnectUserInfoInput: ConnectUserInfoInput;
|
||||
ContainerHostConfig: ResolverTypeWrapper<ContainerHostConfig>;
|
||||
ContainerMount: ResolverTypeWrapper<ContainerMount>;
|
||||
ContainerPort: ResolverTypeWrapper<ContainerPort>;
|
||||
@@ -1722,6 +1756,7 @@ export type ResolversTypes = ResolversObject<{
|
||||
Partition: ResolverTypeWrapper<Partition>;
|
||||
Pci: ResolverTypeWrapper<Pci>;
|
||||
Permissions: ResolverTypeWrapper<Permissions>;
|
||||
Port: ResolverTypeWrapper<Scalars['Port']>;
|
||||
ProfileModel: ResolverTypeWrapper<ProfileModel>;
|
||||
Query: ResolverTypeWrapper<{}>;
|
||||
Registration: ResolverTypeWrapper<Registration>;
|
||||
@@ -1731,6 +1766,7 @@ export type ResolversTypes = ResolversObject<{
|
||||
Server: ResolverTypeWrapper<Server>;
|
||||
ServerStatus: ServerStatus;
|
||||
Service: ResolverTypeWrapper<Service>;
|
||||
SetupRemoteAccessInput: SetupRemoteAccessInput;
|
||||
Share: ResolverTypeWrapper<Share>;
|
||||
String: ResolverTypeWrapper<Scalars['String']>;
|
||||
Subscription: ResolverTypeWrapper<{}>;
|
||||
@@ -1753,6 +1789,8 @@ export type ResolversTypes = ResolversObject<{
|
||||
VmNetwork: ResolverTypeWrapper<VmNetwork>;
|
||||
VmState: VmState;
|
||||
Vms: ResolverTypeWrapper<Vms>;
|
||||
WAN_ACCESS_TYPE: WAN_ACCESS_TYPE;
|
||||
WAN_FORWARD_TYPE: WAN_FORWARD_TYPE;
|
||||
Welcome: ResolverTypeWrapper<Welcome>;
|
||||
addApiKeyInput: addApiKeyInput;
|
||||
addScopeInput: addScopeInput;
|
||||
@@ -1763,14 +1801,13 @@ export type ResolversTypes = ResolversObject<{
|
||||
deleteUserInput: deleteUserInput;
|
||||
mdState: mdState;
|
||||
registrationType: registrationType;
|
||||
testMutationInput: testMutationInput;
|
||||
testQueryInput: testQueryInput;
|
||||
updateApikeyInput: updateApikeyInput;
|
||||
usersInput: usersInput;
|
||||
}>;
|
||||
|
||||
/** Mapping between all available schema types and the resolvers parents */
|
||||
export type ResolversParentTypes = ResolversObject<{
|
||||
AllowedOriginInput: AllowedOriginInput;
|
||||
ApiKey: ApiKey;
|
||||
ApiKeyResponse: ApiKeyResponse;
|
||||
Array: ArrayType;
|
||||
@@ -1783,6 +1820,8 @@ export type ResolversParentTypes = ResolversObject<{
|
||||
Cloud: Cloud;
|
||||
CloudResponse: CloudResponse;
|
||||
Config: Config;
|
||||
ConnectSignInInput: ConnectSignInInput;
|
||||
ConnectUserInfoInput: ConnectUserInfoInput;
|
||||
ContainerHostConfig: ContainerHostConfig;
|
||||
ContainerMount: ContainerMount;
|
||||
ContainerPort: ContainerPort;
|
||||
@@ -1821,6 +1860,7 @@ export type ResolversParentTypes = ResolversObject<{
|
||||
Partition: Partition;
|
||||
Pci: Pci;
|
||||
Permissions: Permissions;
|
||||
Port: Scalars['Port'];
|
||||
ProfileModel: ProfileModel;
|
||||
Query: {};
|
||||
Registration: Registration;
|
||||
@@ -1828,6 +1868,7 @@ export type ResolversParentTypes = ResolversObject<{
|
||||
Scope: Scope;
|
||||
Server: Server;
|
||||
Service: Service;
|
||||
SetupRemoteAccessInput: SetupRemoteAccessInput;
|
||||
Share: Share;
|
||||
String: Scalars['String'];
|
||||
Subscription: {};
|
||||
@@ -1855,8 +1896,6 @@ export type ResolversParentTypes = ResolversObject<{
|
||||
arrayDiskInput: arrayDiskInput;
|
||||
authenticateInput: authenticateInput;
|
||||
deleteUserInput: deleteUserInput;
|
||||
testMutationInput: testMutationInput;
|
||||
testQueryInput: testQueryInput;
|
||||
updateApikeyInput: updateApikeyInput;
|
||||
usersInput: usersInput;
|
||||
}>;
|
||||
@@ -2245,6 +2284,8 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
|
||||
addUser?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<MutationaddUserArgs, 'input'>>;
|
||||
cancelParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
clearArrayDiskStatistics?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, RequireFields<MutationclearArrayDiskStatisticsArgs, 'id'>>;
|
||||
connectSignIn?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationconnectSignInArgs, 'input'>>;
|
||||
connectSignOut?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
deleteUser?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<MutationdeleteUserArgs, 'input'>>;
|
||||
getApiKey?: Resolver<Maybe<ResolversTypes['ApiKey']>, ParentType, ContextType, RequireFields<MutationgetApiKeyArgs, 'name'>>;
|
||||
login?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MutationloginArgs, 'password' | 'username'>>;
|
||||
@@ -2254,11 +2295,12 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
|
||||
removeDiskFromArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<MutationremoveDiskFromArrayArgs>>;
|
||||
resumeParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
sendNotification?: Resolver<Maybe<ResolversTypes['Notification']>, ParentType, ContextType, RequireFields<MutationsendNotificationArgs, 'notification'>>;
|
||||
setAdditionalAllowedOrigins?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MutationsetAdditionalAllowedOriginsArgs, 'input'>>;
|
||||
setupRemoteAccess?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationsetupRemoteAccessArgs, 'input'>>;
|
||||
shutdown?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
startArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType>;
|
||||
startParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, Partial<MutationstartParityCheckArgs>>;
|
||||
stopArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType>;
|
||||
testMutation?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, RequireFields<MutationtestMutationArgs, 'id'>>;
|
||||
unmountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<MutationunmountArrayDiskArgs, 'id'>>;
|
||||
updateApikey?: Resolver<Maybe<ResolversTypes['ApiKey']>, ParentType, ContextType, RequireFields<MutationupdateApikeyArgs, 'name'>>;
|
||||
}>;
|
||||
@@ -2404,6 +2446,10 @@ export type PermissionsResolvers<ContextType = Context, ParentType extends Resol
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export interface PortScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['Port'], any> {
|
||||
name: 'Port';
|
||||
}
|
||||
|
||||
export type ProfileModelResolvers<ContextType = Context, ParentType extends ResolversParentTypes['ProfileModel'] = ResolversParentTypes['ProfileModel']> = ResolversObject<{
|
||||
avatar?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
url?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
@@ -2438,7 +2484,6 @@ export type QueryResolvers<ContextType = Context, ParentType extends ResolversPa
|
||||
server?: Resolver<Maybe<ResolversTypes['Server']>, ParentType, ContextType, RequireFields<QueryserverArgs, 'name'>>;
|
||||
servers?: Resolver<Array<ResolversTypes['Server']>, ParentType, ContextType>;
|
||||
shares?: Resolver<Maybe<Array<Maybe<ResolversTypes['Share']>>>, ParentType, ContextType>;
|
||||
testQuery?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, RequireFields<QuerytestQueryArgs, 'id'>>;
|
||||
twoFactor?: Resolver<Maybe<ResolversTypes['TwoFactorWithToken']>, ParentType, ContextType>;
|
||||
unassignedDevices?: Resolver<Maybe<Array<Maybe<ResolversTypes['UnassignedDevice']>>>, ParentType, ContextType>;
|
||||
user?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<QueryuserArgs, 'id'>>;
|
||||
@@ -2537,7 +2582,6 @@ export type SubscriptionResolvers<ContextType = Context, ParentType extends Reso
|
||||
service?: SubscriptionResolver<Maybe<Array<ResolversTypes['Service']>>, "service", ParentType, ContextType, RequireFields<SubscriptionserviceArgs, 'name'>>;
|
||||
share?: SubscriptionResolver<ResolversTypes['Share'], "share", ParentType, ContextType, RequireFields<SubscriptionshareArgs, 'id'>>;
|
||||
shares?: SubscriptionResolver<Maybe<Array<ResolversTypes['Share']>>, "shares", ParentType, ContextType>;
|
||||
testSubscription?: SubscriptionResolver<ResolversTypes['String'], "testSubscription", ParentType, ContextType>;
|
||||
twoFactor?: SubscriptionResolver<Maybe<ResolversTypes['TwoFactorWithoutToken']>, "twoFactor", ParentType, ContextType>;
|
||||
unassignedDevices?: SubscriptionResolver<Maybe<Array<ResolversTypes['UnassignedDevice']>>, "unassignedDevices", ParentType, ContextType>;
|
||||
user?: SubscriptionResolver<ResolversTypes['User'], "user", ParentType, ContextType, RequireFields<SubscriptionuserArgs, 'id'>>;
|
||||
@@ -2911,6 +2955,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
|
||||
Partition?: PartitionResolvers<ContextType>;
|
||||
Pci?: PciResolvers<ContextType>;
|
||||
Permissions?: PermissionsResolvers<ContextType>;
|
||||
Port?: GraphQLScalarType;
|
||||
ProfileModel?: ProfileModelResolvers<ContextType>;
|
||||
Query?: QueryResolvers<ContextType>;
|
||||
Registration?: RegistrationResolvers<ContextType>;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ensurePermission } from '@app/core/utils/index';
|
||||
import { type MutationResolvers } from '@app/graphql/generated/api/types';
|
||||
import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types';
|
||||
import { validateApiKeyWithKeyServer } from '@app/mothership/api-key/validate-api-key-with-keyserver';
|
||||
import { getters, store } from '@app/store/index';
|
||||
import { setApiKeyState } from '@app/store/modules/apikey';
|
||||
import { loginUser, signIn } from '@app/store/modules/config';
|
||||
import { FileLoadStatus } from '@app/store/types';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { decodeJwt } from 'jose';
|
||||
|
||||
export const connectSignIn: MutationResolvers['connectSignIn'] = async (
|
||||
_,
|
||||
args,
|
||||
context
|
||||
) => {
|
||||
ensurePermission(context.user, {
|
||||
resource: 'connect',
|
||||
possession: 'own',
|
||||
action: 'update',
|
||||
});
|
||||
|
||||
if (getters.emhttp().status === FileLoadStatus.LOADED) {
|
||||
const result = await validateApiKeyWithKeyServer({
|
||||
apiKey: args.input.apiKey,
|
||||
flashGuid: getters.emhttp().var.flashGuid,
|
||||
});
|
||||
if (result !== API_KEY_STATUS.API_KEY_VALID) {
|
||||
throw new GraphQLError(
|
||||
`Validating API Key Failed with Error: ${result}`
|
||||
);
|
||||
}
|
||||
|
||||
const userInfo = args.input.idToken
|
||||
? decodeJwt(args.input.idToken)
|
||||
: args.input.userInfo ?? null;
|
||||
if (
|
||||
!userInfo ||
|
||||
!userInfo.preferred_username ||
|
||||
!userInfo.email ||
|
||||
typeof userInfo.preferred_username !== 'string' ||
|
||||
typeof userInfo.email !== 'string'
|
||||
) {
|
||||
throw new GraphQLError('Missing User Attributes');
|
||||
}
|
||||
store.dispatch(setApiKeyState(API_KEY_STATUS.API_KEY_VALID));
|
||||
store.dispatch(
|
||||
signIn({
|
||||
apikey: args.input.apiKey,
|
||||
username: userInfo.preferred_username,
|
||||
email: userInfo.email,
|
||||
avatar:
|
||||
typeof userInfo.avatar === 'string' ? userInfo.avatar : '',
|
||||
})
|
||||
);
|
||||
// @TODO once we deprecate old sign in method, switch this to do all validation requests
|
||||
await store.dispatch(
|
||||
loginUser({
|
||||
avatar: '',
|
||||
username: userInfo.preferred_username,
|
||||
email: userInfo.email,
|
||||
})
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { type MutationResolvers } from '@app/graphql/generated/api/types';
|
||||
import { store } from '@app/store/index';
|
||||
import { logoutUser, signOut } from '@app/store/modules/config';
|
||||
|
||||
export const connectSignOut: MutationResolvers['connectSignOut'] = async (
|
||||
_,
|
||||
__,
|
||||
context
|
||||
) => {
|
||||
ensurePermission(context.user, {
|
||||
resource: 'connect',
|
||||
possession: 'own',
|
||||
action: 'update',
|
||||
});
|
||||
|
||||
store.dispatch(signOut());
|
||||
await store.dispatch(logoutUser({ reason: 'Manual Sign Out With API' }));
|
||||
return true;
|
||||
};
|
||||
@@ -1,6 +1,10 @@
|
||||
import { type Resolvers } from '@app/graphql/generated/api/types';
|
||||
import { sendNotification } from './notifications';
|
||||
import { connectSignIn } from '@app/graphql/resolvers/mutation/connect/connect-sign-in';
|
||||
import { connectSignOut } from '@app/graphql/resolvers/mutation/connect/connect-sign-out';
|
||||
|
||||
export const Mutation: Resolvers['Mutation'] = {
|
||||
sendNotification,
|
||||
connectSignIn,
|
||||
connectSignOut
|
||||
};
|
||||
|
||||
28
api/src/graphql/resolvers/resolvers.ts
Normal file
28
api/src/graphql/resolvers/resolvers.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { DateTimeResolver, JSONResolver, PortResolver, UUIDResolver } from 'graphql-scalars';
|
||||
|
||||
import { Query } from '@app/graphql/resolvers/query';
|
||||
import { Mutation } from '@app/graphql/resolvers/mutation';
|
||||
import { Subscription } from '@app/graphql/resolvers/subscription';
|
||||
import { UserAccount } from '@app/graphql/resolvers/user-account';
|
||||
import { type Resolvers } from '../generated/api/types';
|
||||
import { infoSubResolvers } from './query/info';
|
||||
import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long';
|
||||
import { domainResolver } from '@app/core/modules/index';
|
||||
|
||||
export const resolvers: Resolvers = {
|
||||
JSON: JSONResolver,
|
||||
Long: GraphQLLong,
|
||||
UUID: UUIDResolver,
|
||||
DateTime: DateTimeResolver,
|
||||
Port: PortResolver,
|
||||
Query,
|
||||
Mutation,
|
||||
Subscription,
|
||||
UserAccount,
|
||||
Info: {
|
||||
...infoSubResolvers,
|
||||
},
|
||||
Vms: {
|
||||
domain: domainResolver,
|
||||
},
|
||||
};
|
||||
41
api/src/graphql/schema/types/connect/connect.graphql
Normal file
41
api/src/graphql/schema/types/connect/connect.graphql
Normal file
@@ -0,0 +1,41 @@
|
||||
input ConnectUserInfoInput {
|
||||
preferred_username: String!
|
||||
email: String!
|
||||
avatar: String
|
||||
}
|
||||
|
||||
input ConnectSignInInput {
|
||||
apiKey: String!
|
||||
idToken: String
|
||||
userInfo: ConnectUserInfoInput
|
||||
accessToken: String
|
||||
refreshToken: String
|
||||
}
|
||||
|
||||
input AllowedOriginInput {
|
||||
origins: [String!]!
|
||||
}
|
||||
|
||||
enum WAN_ACCESS_TYPE {
|
||||
DYNAMIC
|
||||
ALWAYS
|
||||
DISABLED
|
||||
}
|
||||
|
||||
enum WAN_FORWARD_TYPE {
|
||||
UPNP
|
||||
STATIC
|
||||
}
|
||||
|
||||
input SetupRemoteAccessInput {
|
||||
accessType: WAN_ACCESS_TYPE!
|
||||
forwardType: WAN_FORWARD_TYPE
|
||||
port: Port
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
connectSignIn(input: ConnectSignInInput!): Boolean!
|
||||
connectSignOut: Boolean!
|
||||
setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]!
|
||||
setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean!
|
||||
}
|
||||
46
api/src/graphql/types.ts
Normal file
46
api/src/graphql/types.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { mergeTypeDefs } from '@graphql-tools/merge';
|
||||
import { gql } from 'graphql-tag';
|
||||
import { typeDefs } from '@app/graphql/schema/index';
|
||||
|
||||
export const baseTypes = [
|
||||
gql`
|
||||
scalar JSON
|
||||
scalar Long
|
||||
scalar UUID
|
||||
scalar DateTime
|
||||
scalar Port
|
||||
|
||||
directive @subscription(channel: String!) on FIELD_DEFINITION
|
||||
|
||||
type Welcome {
|
||||
message: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
# This should always be available even for guest users
|
||||
welcome: Welcome @func(module: "getWelcome")
|
||||
online: Boolean
|
||||
info: Info
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
login(username: String!, password: String!): String
|
||||
sendNotification(notification: NotificationInput!): Notification
|
||||
shutdown: String
|
||||
reboot: String
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
ping: String!
|
||||
info: Info!
|
||||
online: Boolean!
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
export const types = mergeTypeDefs([
|
||||
...baseTypes,
|
||||
typeDefs,
|
||||
]);
|
||||
|
||||
export default types;
|
||||
@@ -38,7 +38,7 @@ export const handleRemoteAccessEvent = createAsyncThunk<void, RemoteAccessEventF
|
||||
break;
|
||||
case RemoteAccessEventActionType.PING:
|
||||
// Ping - would continue remote access if necessary;
|
||||
RemoteAccessController.instance.extendRemoteAccess({ getState, dispatch });
|
||||
RemoteAccessController.instance.extendRemoteAccess({ dispatch });
|
||||
break;
|
||||
case RemoteAccessEventActionType.END:
|
||||
// End
|
||||
|
||||
355
api/src/store/modules/config.ts
Normal file
355
api/src/store/modules/config.ts
Normal file
@@ -0,0 +1,355 @@
|
||||
import { parseConfig } from '@app/core/utils/misc/parse-config';
|
||||
import {
|
||||
type MyServersConfig,
|
||||
type MyServersConfigMemory,
|
||||
} from '@app/types/my-servers-config';
|
||||
import {
|
||||
createAsyncThunk,
|
||||
createSlice,
|
||||
type PayloadAction,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { access } from 'fs/promises';
|
||||
import merge from 'lodash/merge';
|
||||
import { FileLoadStatus } from '@app/store/types';
|
||||
import { F_OK } from 'constants';
|
||||
import { type RecursivePartial } from '@app/types';
|
||||
import { MinigraphStatus, type Owner } from '@app/graphql/generated/api/types';
|
||||
import { type RootState } from '@app/store';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { logger } from '@app/core/log';
|
||||
import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status';
|
||||
import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
|
||||
import { pubsub } from '@app/core/pubsub';
|
||||
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export type SliceState = {
|
||||
status: FileLoadStatus;
|
||||
nodeEnv: string;
|
||||
} & MyServersConfigMemory;
|
||||
|
||||
export const initialState: SliceState = {
|
||||
status: FileLoadStatus.UNLOADED,
|
||||
nodeEnv: process.env.NODE_ENV ?? 'production',
|
||||
remote: {
|
||||
'2Fa': '',
|
||||
wanaccess: '',
|
||||
wanport: '',
|
||||
upnpEnabled: '',
|
||||
apikey: '',
|
||||
email: '',
|
||||
username: '',
|
||||
avatar: '',
|
||||
regWizTime: '',
|
||||
accesstoken: '',
|
||||
idtoken: '',
|
||||
refreshtoken: '',
|
||||
allowedOrigins: '',
|
||||
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
|
||||
},
|
||||
local: {
|
||||
showT2Fa: '',
|
||||
'2Fa': '',
|
||||
},
|
||||
api: {
|
||||
extraOrigins: '',
|
||||
version: '',
|
||||
},
|
||||
upc: {
|
||||
apikey: '',
|
||||
},
|
||||
notifier: {
|
||||
apikey: '',
|
||||
},
|
||||
connectionStatus: {
|
||||
minigraph: MinigraphStatus.PRE_INIT,
|
||||
upnpStatus: '',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const loginUser = createAsyncThunk<
|
||||
Pick<MyServersConfig['remote'], 'email' | 'avatar' | 'username'>,
|
||||
Pick<MyServersConfig['remote'], 'email' | 'avatar' | 'username'>,
|
||||
{ state: RootState }
|
||||
>('config/login-user', async (userInfo) => {
|
||||
logger.info('Logging in user: %s', userInfo.username);
|
||||
const owner: Owner = {
|
||||
username: userInfo.username,
|
||||
avatar: userInfo.avatar,
|
||||
};
|
||||
await pubsub.publish('owner', { owner });
|
||||
return userInfo;
|
||||
});
|
||||
|
||||
export const logoutUser = createAsyncThunk<
|
||||
void,
|
||||
{ reason?: string },
|
||||
{ state: RootState }
|
||||
>('config/logout-user', async ({ reason }) => {
|
||||
logger.info('Logging out user: %s', reason ?? 'No reason provided');
|
||||
const { pubsub } = await import('@app/core/pubsub');
|
||||
|
||||
// Publish to servers endpoint
|
||||
await pubsub.publish('servers', {
|
||||
servers: [],
|
||||
});
|
||||
|
||||
const owner: Owner = {
|
||||
username: 'root',
|
||||
url: '',
|
||||
avatar: '',
|
||||
};
|
||||
// Publish to owner endpoint
|
||||
await pubsub.publish('owner', { owner });
|
||||
});
|
||||
|
||||
/**
|
||||
* Load the myservers.cfg into the store. Returns null if the state after loading doesn't change
|
||||
*
|
||||
* Note: If the file doesn't exist this will fallback to default values.
|
||||
*/
|
||||
enum CONFIG_LOAD_ERROR {
|
||||
CONFIG_EQUAL = 'CONFIG_EQUAL',
|
||||
CONFIG_CORRUPTED = 'CONFIG_CORRUPTED',
|
||||
}
|
||||
|
||||
type LoadFailureWithConfig = {
|
||||
type: CONFIG_LOAD_ERROR.CONFIG_CORRUPTED;
|
||||
error: Error | null;
|
||||
config: MyServersConfig;
|
||||
};
|
||||
type LoadFailureConfigEqual = {
|
||||
type: CONFIG_LOAD_ERROR.CONFIG_EQUAL;
|
||||
};
|
||||
type ConfigRejectedValues = LoadFailureConfigEqual | LoadFailureWithConfig;
|
||||
|
||||
const generateApiKeysIfNotExistent = (
|
||||
file: RecursivePartial<MyServersConfig>
|
||||
): MyServersConfig => {
|
||||
const newConfigFile = merge(file, {
|
||||
upc: {
|
||||
apikey:
|
||||
file.upc?.apikey?.trim()?.length === 64
|
||||
? file.upc?.apikey
|
||||
: `unupc_${randomBytes(58).toString('hex')}`.substring(
|
||||
0,
|
||||
64
|
||||
),
|
||||
},
|
||||
notifier: {
|
||||
apikey:
|
||||
file.notifier?.apikey?.trim().length === 64
|
||||
? file.notifier?.apikey
|
||||
: `unnotify_${randomBytes(58).toString('hex')}`.substring(
|
||||
0,
|
||||
64
|
||||
),
|
||||
},
|
||||
}) as MyServersConfig;
|
||||
return newConfigFile;
|
||||
};
|
||||
|
||||
export const loadConfigFile = createAsyncThunk<
|
||||
MyServersConfig,
|
||||
string | undefined,
|
||||
{
|
||||
state: RootState;
|
||||
rejectValue: ConfigRejectedValues;
|
||||
}
|
||||
>(
|
||||
'config/load-config-file',
|
||||
async (filePath, { getState, rejectWithValue }) => {
|
||||
try {
|
||||
const { paths, config } = getState();
|
||||
|
||||
const path = filePath ?? paths['myservers-config'];
|
||||
|
||||
const fileExists = await access(path, F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (!fileExists) {
|
||||
throw new Error('Config File Missing');
|
||||
}
|
||||
|
||||
const file = fileExists
|
||||
? parseConfig<RecursivePartial<MyServersConfig>>({
|
||||
filePath: path,
|
||||
type: 'ini',
|
||||
})
|
||||
: {};
|
||||
|
||||
const newConfigFile = generateApiKeysIfNotExistent(file);
|
||||
|
||||
const isNewlyLoadedConfigEqual = isEqual(
|
||||
getWriteableConfig(newConfigFile as SliceState, 'flash'),
|
||||
getWriteableConfig(config, 'flash')
|
||||
);
|
||||
if (isNewlyLoadedConfigEqual) {
|
||||
logger.warn(
|
||||
'Not loading config because it is the same as before'
|
||||
);
|
||||
return rejectWithValue({
|
||||
type: CONFIG_LOAD_ERROR.CONFIG_EQUAL,
|
||||
});
|
||||
}
|
||||
return newConfigFile;
|
||||
} catch (error: unknown) {
|
||||
logger.warn('Config file is corrupted, recreating config', error);
|
||||
const config = getWriteableConfig(initialState, 'flash');
|
||||
const newConfig = generateApiKeysIfNotExistent(config);
|
||||
newConfig.remote.wanaccess = 'no';
|
||||
const serializedConfig = safelySerializeObjectToIni(newConfig);
|
||||
writeFileSync(
|
||||
getState().paths['myservers-config'],
|
||||
serializedConfig
|
||||
);
|
||||
return rejectWithValue({
|
||||
type: CONFIG_LOAD_ERROR.CONFIG_CORRUPTED,
|
||||
error:
|
||||
error instanceof Error ? error : new Error('Unknown Error'),
|
||||
config: newConfig,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const config = createSlice({
|
||||
name: 'config',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateUserConfig(
|
||||
state,
|
||||
action: PayloadAction<RecursivePartial<MyServersConfig>>
|
||||
) {
|
||||
return merge(state, action.payload);
|
||||
},
|
||||
updateAccessTokens(
|
||||
state,
|
||||
action: PayloadAction<
|
||||
Partial<
|
||||
Pick<
|
||||
Pick<MyServersConfig, 'remote'>['remote'],
|
||||
'accesstoken' | 'refreshtoken' | 'idtoken'
|
||||
>
|
||||
>
|
||||
>
|
||||
) {
|
||||
return merge(state, { remote: action.payload });
|
||||
},
|
||||
updateAllowedOrigins(state, action: PayloadAction<string[]>) {
|
||||
state.remote.allowedOrigins = action.payload.join(', ');
|
||||
},
|
||||
setUpnpState(
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
enabled?: 'no' | 'yes' | 'auto';
|
||||
status?: string | null;
|
||||
}>
|
||||
) {
|
||||
if (action.payload.enabled) {
|
||||
state.remote.upnpEnabled = action.payload.enabled;
|
||||
}
|
||||
|
||||
if (action.payload.status) {
|
||||
state.connectionStatus.upnpStatus = action.payload.status;
|
||||
}
|
||||
},
|
||||
setWanPortToValue(state, action: PayloadAction<number>) {
|
||||
logger.debug('Wan port set to %s', action.payload);
|
||||
state.remote.wanport = String(action.payload);
|
||||
},
|
||||
setWanAccess(state, action: PayloadAction<'yes' | 'no'>) {
|
||||
state.remote.wanaccess = action.payload;
|
||||
},
|
||||
signIn: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
Pick<
|
||||
MyServersConfig['remote'],
|
||||
'apikey'
|
||||
> & Partial<Pick<MyServersConfig['remote'],
|
||||
'idtoken' | 'accesstoken' | 'refreshtoken' | 'username' | 'avatar' | 'email'>>
|
||||
>
|
||||
) => {
|
||||
state.remote.apikey = action.payload.apikey;
|
||||
state.remote.idtoken = action.payload.idtoken ?? '';
|
||||
state.remote.accesstoken = action.payload.accesstoken ?? ''
|
||||
state.remote.refreshtoken = action.payload.refreshtoken ?? ''
|
||||
state.remote.email = action.payload.email ?? '',
|
||||
state.remote.username = action.payload.username ?? '',
|
||||
state.remote.avatar = action.payload.avatar ?? ''
|
||||
},
|
||||
signOut: (state) => {
|
||||
state.remote.apikey = '';
|
||||
state.remote.idtoken = '';
|
||||
state.remote.accesstoken = '';
|
||||
state.remote.refreshtoken = '';
|
||||
state.remote.email = '';
|
||||
state.remote.username = '';
|
||||
state.remote.avatar = '';
|
||||
}
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(loadConfigFile.pending, (state) => {
|
||||
state.status = FileLoadStatus.LOADING;
|
||||
});
|
||||
|
||||
builder.addCase(loadConfigFile.fulfilled, (state, action) => {
|
||||
if (action.payload) {
|
||||
merge(state, action.payload, { status: FileLoadStatus.LOADED });
|
||||
} else {
|
||||
state.status = FileLoadStatus.LOADED;
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(loadConfigFile.rejected, (state, action) => {
|
||||
switch (action.payload?.type) {
|
||||
case CONFIG_LOAD_ERROR.CONFIG_EQUAL:
|
||||
logger.debug('Configs equivalent');
|
||||
state.status = FileLoadStatus.LOADED;
|
||||
break;
|
||||
case CONFIG_LOAD_ERROR.CONFIG_CORRUPTED:
|
||||
logger.debug(
|
||||
'Config File Load Failed - %o',
|
||||
action.payload.error
|
||||
);
|
||||
merge(state, action.payload.config);
|
||||
state.status = FileLoadStatus.LOADED;
|
||||
break;
|
||||
default:
|
||||
logger.error('Config File Load Failed', action.error);
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(logoutUser.pending, (state) => {
|
||||
merge(state, {
|
||||
remote: {
|
||||
apikey: '',
|
||||
avatar: '',
|
||||
email: '',
|
||||
username: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
builder.addCase(setGraphqlConnectionStatus, (state, action) => {
|
||||
state.connectionStatus.minigraph = action.payload.status;
|
||||
});
|
||||
},
|
||||
});
|
||||
const { actions, reducer } = config;
|
||||
|
||||
export const {
|
||||
updateUserConfig,
|
||||
updateAccessTokens,
|
||||
updateAllowedOrigins,
|
||||
setUpnpState,
|
||||
setWanPortToValue,
|
||||
setWanAccess,
|
||||
signIn,
|
||||
signOut
|
||||
} = actions;
|
||||
|
||||
export const configReducer = reducer;
|
||||
Reference in New Issue
Block a user