feat: API key management (#1407)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added a full-featured API key management UI, including creation,
listing, and deletion of API keys with customizable roles and
permissions.
  - Introduced a new page for API key management.
- Accordion UI components are now available for enhanced interface
interactions.
  - API now provides queries for possible API key roles and permissions.

- **Improvements**
- API key-related mutations are now grouped under a single field,
improving organization and usability.
  - Permissions can be assigned directly to API keys, not just roles.

- **Bug Fixes**
- Validation updated to require at least one role or permission when
creating an API key.

- **Documentation**
- Updated and added rules and configuration documentation for code
generation and testing.

- **Tests**
- Added and updated tests for new API key mutation logic; removed
obsolete tests for deprecated mutations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Eli Bosley
2025-05-23 10:12:26 -07:00
committed by GitHub
parent 83076bb940
commit d37dc3bce2
30 changed files with 965 additions and 184 deletions

View File

@@ -8,5 +8,6 @@ alwaysApply: false
* always run scripts from api/package.json unless requested * always run scripts from api/package.json unless requested
* prefer adding new files to the nest repo located at api/src/unraid-api/ instead of the legacy code * prefer adding new files to the nest repo located at api/src/unraid-api/ instead of the legacy code
* Test suite is VITEST, do not use jest * Test suite is VITEST, do not use jest
pnpm --filter ./api test
* Prefer to not mock simple dependencies * Prefer to not mock simple dependencies

View File

@@ -0,0 +1,9 @@
---
description:
globs: web/**/*
alwaysApply: false
---
* Always run `pnpm codegen` for GraphQL code generation in the web directory
* GraphQL queries must be placed in `.query.ts` files
* GraphQL mutations must be placed in `.mutation.ts` files
* All GraphQL under `web/` and follow this naming convention

View File

@@ -1,5 +1,5 @@
[api] [api]
version="4.7.0" version="4.8.0"
extraOrigins="https://google.com,https://test.com" extraOrigins="https://google.com,https://test.com"
[local] [local]
sandbox="yes" sandbox="yes"

View File

@@ -883,6 +883,52 @@ type VmMutations {
reset(id: PrefixedID!): Boolean! reset(id: PrefixedID!): Boolean!
} }
"""API Key related mutations"""
type ApiKeyMutations {
"""Create an API key"""
create(input: CreateApiKeyInput!): ApiKeyWithSecret!
"""Add a role to an API key"""
addRole(input: AddRoleForApiKeyInput!): Boolean!
"""Remove a role from an API key"""
removeRole(input: RemoveRoleFromApiKeyInput!): Boolean!
"""Delete one or more API keys"""
delete(input: DeleteApiKeyInput!): Boolean!
}
input CreateApiKeyInput {
name: String!
description: String
roles: [Role!]
permissions: [AddPermissionInput!]
"""
This will replace the existing key if one already exists with the same name, otherwise returns the existing key
"""
overwrite: Boolean
}
input AddPermissionInput {
resource: Resource!
actions: [String!]!
}
input AddRoleForApiKeyInput {
apiKeyId: PrefixedID!
role: Role!
}
input RemoveRoleFromApiKeyInput {
apiKeyId: PrefixedID!
role: Role!
}
input DeleteApiKeyInput {
ids: [PrefixedID!]!
}
""" """
Parity check related mutations, WIP, response types and functionaliy will change Parity check related mutations, WIP, response types and functionaliy will change
""" """
@@ -1455,8 +1501,6 @@ type UserAccount implements Node {
scalar PrefixedID scalar PrefixedID
type Query { type Query {
apiKeys: [ApiKey!]!
apiKey(id: PrefixedID!): ApiKey
cloud: Cloud! cloud: Cloud!
config: Config! config: Config!
display: Display! display: Display!
@@ -1482,6 +1526,14 @@ type Query {
vms: Vms! vms: Vms!
parityHistory: [ParityCheck!]! parityHistory: [ParityCheck!]!
array: UnraidArray! array: UnraidArray!
apiKeys: [ApiKey!]!
apiKey(id: PrefixedID!): ApiKey
"""All possible roles for API keys"""
apiKeyPossibleRoles: [Role!]!
"""All possible permissions for API keys"""
apiKeyPossiblePermissions: [Permission!]!
connect: Connect! connect: Connect!
remoteAccess: RemoteAccess! remoteAccess: RemoteAccess!
extraAllowedOrigins: [String!]! extraAllowedOrigins: [String!]!
@@ -1496,10 +1548,6 @@ type Query {
} }
type Mutation { type Mutation {
createApiKey(input: CreateApiKeyInput!): ApiKeyWithSecret!
addRoleForApiKey(input: AddRoleForApiKeyInput!): Boolean!
removeRoleFromApiKey(input: RemoveRoleFromApiKeyInput!): Boolean!
"""Creates a new notification record""" """Creates a new notification record"""
createNotification(input: NotificationData!): Notification! createNotification(input: NotificationData!): Notification!
deleteNotification(id: PrefixedID!, type: NotificationType!): NotificationOverview! deleteNotification(id: PrefixedID!, type: NotificationType!): NotificationOverview!
@@ -1523,6 +1571,7 @@ type Mutation {
docker: DockerMutations! docker: DockerMutations!
vm: VmMutations! vm: VmMutations!
parityCheck: ParityCheckMutations! parityCheck: ParityCheckMutations!
apiKey: ApiKeyMutations!
updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues! updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues!
connectSignIn(input: ConnectSignInInput!): Boolean! connectSignIn(input: ConnectSignInInput!): Boolean!
connectSignOut: Boolean! connectSignOut: Boolean!
@@ -1532,33 +1581,6 @@ type Mutation {
setDemo: String! setDemo: String!
} }
input CreateApiKeyInput {
name: String!
description: String
roles: [Role!]
permissions: [AddPermissionInput!]
"""
This will replace the existing key if one already exists with the same name, otherwise returns the existing key
"""
overwrite: Boolean
}
input AddPermissionInput {
resource: Resource!
actions: [String!]!
}
input AddRoleForApiKeyInput {
apiKeyId: PrefixedID!
role: Role!
}
input RemoveRoleFromApiKeyInput {
apiKeyId: PrefixedID!
role: Role!
}
input NotificationData { input NotificationData {
title: String! title: String!
subject: String! subject: String!

View File

@@ -185,7 +185,7 @@ describe('ApiKeyService', () => {
await expect( await expect(
apiKeyService.create({ name: 'name', description: 'desc', roles: [] }) apiKeyService.create({ name: 'name', description: 'desc', roles: [] })
).rejects.toThrow('At least one role must be specified'); ).rejects.toThrow('At least one role or permission must be specified');
await expect( await expect(
apiKeyService.create({ apiKeyService.create({

View File

@@ -130,11 +130,11 @@ export class ApiKeyService implements OnModuleInit {
throw new GraphQLError('API key name is required'); throw new GraphQLError('API key name is required');
} }
if (!roles?.length) { if (!roles?.length && !permissions?.length) {
throw new GraphQLError('At least one role must be specified'); throw new GraphQLError('At least one role or permission must be specified');
} }
if (roles.some((role) => !ApiKeyService.validRoles.has(role))) { if (roles?.some((role) => !ApiKeyService.validRoles.has(role))) {
throw new GraphQLError('Invalid role specified'); throw new GraphQLError('Invalid role specified');
} }

View File

@@ -132,3 +132,11 @@ export class RemoveRoleFromApiKeyInput {
@IsEnum(Role) @IsEnum(Role)
role!: Role; role!: Role;
} }
@InputType()
export class DeleteApiKeyInput {
@Field(() => [PrefixedID])
@IsArray()
@IsString({ each: true })
ids!: string[];
}

View File

@@ -1,11 +1,14 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
import { AuthService } from '@app/unraid-api/auth/auth.service.js'; import { AuthService } from '@app/unraid-api/auth/auth.service.js';
import { ApiKeyMutationsResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.mutation.js';
import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js'; import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js';
@Module({ @Module({
providers: [ApiKeyResolver, ApiKeyService, AuthService], imports: [AuthModule],
exports: [ApiKeyResolver], providers: [ApiKeyResolver, ApiKeyService, AuthService, ApiKeyMutationsResolver],
exports: [ApiKeyResolver, ApiKeyService],
}) })
export class ApiKeyModule {} export class ApiKeyModule {}

View File

@@ -0,0 +1,165 @@
import { newEnforcer } from 'casbin';
import { AuthZService } from 'nest-authz';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { AuthService } from '@app/unraid-api/auth/auth.service.js';
import { CookieService } from '@app/unraid-api/auth/cookie.service.js';
import {
ApiKey,
ApiKeyWithSecret,
CreateApiKeyInput,
DeleteApiKeyInput,
} from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js';
import { ApiKeyMutationsResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.mutation.js';
import { Role } from '@app/unraid-api/graph/resolvers/base.model.js';
describe('ApiKeyMutationsResolver', () => {
let resolver: ApiKeyMutationsResolver;
let authService: AuthService;
let apiKeyService: ApiKeyService;
let authzService: AuthZService;
let cookieService: CookieService;
const mockApiKey: ApiKey = {
id: 'test-api-id',
name: 'Test API Key',
description: 'Test API Key Description',
roles: [Role.GUEST],
createdAt: new Date().toISOString(),
permissions: [],
};
const mockApiKeyWithSecret: ApiKeyWithSecret = {
id: 'test-api-id',
key: 'test-api-key',
name: 'Test API Key',
description: 'Test API Key Description',
roles: [Role.GUEST],
createdAt: new Date().toISOString(),
permissions: [],
};
beforeEach(async () => {
vi.resetAllMocks();
const enforcer = await newEnforcer();
apiKeyService = new ApiKeyService();
authzService = new AuthZService(enforcer);
cookieService = new CookieService();
authService = new AuthService(cookieService, apiKeyService, authzService);
resolver = new ApiKeyMutationsResolver(authService, apiKeyService);
});
describe('create', () => {
it('should create new API key and sync roles', async () => {
const input: CreateApiKeyInput = {
name: 'New API Key',
description: 'New API Key Description',
roles: [Role.GUEST],
permissions: [],
};
vi.spyOn(apiKeyService, 'create').mockResolvedValue(mockApiKeyWithSecret);
vi.spyOn(authService, 'syncApiKeyRoles').mockResolvedValue();
const result = await resolver.create(input);
expect(result).toEqual(mockApiKeyWithSecret);
expect(apiKeyService.create).toHaveBeenCalledWith({
name: input.name,
description: input.description,
overwrite: false,
roles: input.roles,
permissions: [],
});
expect(authService.syncApiKeyRoles).toHaveBeenCalledWith(mockApiKey.id, mockApiKey.roles);
});
it('should throw if API key creation fails', async () => {
const input: CreateApiKeyInput = {
name: 'Failing API Key',
description: 'Should fail',
roles: [Role.GUEST],
permissions: [],
};
vi.spyOn(apiKeyService, 'create').mockRejectedValue(new Error('Create failed'));
await expect(resolver.create(input)).rejects.toThrow('Create failed');
});
it('should throw if role synchronization fails', async () => {
const input: CreateApiKeyInput = {
name: 'Sync Fail API Key',
description: 'Should fail sync',
roles: [Role.GUEST],
permissions: [],
};
vi.spyOn(apiKeyService, 'create').mockResolvedValue(mockApiKeyWithSecret);
vi.spyOn(authService, 'syncApiKeyRoles').mockRejectedValue(new Error('Sync failed'));
await expect(resolver.create(input)).rejects.toThrow('Sync failed');
});
it('should throw if input validation fails (empty name)', async () => {
const input: CreateApiKeyInput = {
name: '',
description: 'No name',
roles: [Role.GUEST],
permissions: [],
};
await expect(resolver.create(input)).rejects.toThrow();
});
});
describe('delete', () => {
it('should delete API keys', async () => {
const input: DeleteApiKeyInput = { ids: [mockApiKey.id] };
vi.spyOn(apiKeyService, 'deleteApiKeys').mockResolvedValue();
const result = await resolver.delete(input);
expect(result).toBe(true);
expect(apiKeyService.deleteApiKeys).toHaveBeenCalledWith(input.ids);
});
});
describe('addRole', () => {
it('should add a role to an API key', async () => {
const input = { apiKeyId: mockApiKey.id, role: Role.ADMIN };
vi.spyOn(authService, 'addRoleToApiKey').mockResolvedValue(true);
const result = await resolver.addRole(input);
expect(result).toBe(true);
expect(authService.addRoleToApiKey).toHaveBeenCalledWith(input.apiKeyId, input.role);
});
it('should throw if addRoleToApiKey throws', async () => {
const input = { apiKeyId: 'bad-id', role: Role.ADMIN };
vi.spyOn(authService, 'addRoleToApiKey').mockRejectedValue(new Error('API key not found'));
await expect(resolver.addRole(input)).rejects.toThrow('API key not found');
});
});
describe('removeRole', () => {
it('should remove a role from an API key', async () => {
const input = { apiKeyId: mockApiKey.id, role: Role.GUEST };
vi.spyOn(authService, 'removeRoleFromApiKey').mockResolvedValue(true);
const result = await resolver.removeRole(input);
expect(result).toBe(true);
expect(authService.removeRoleFromApiKey).toHaveBeenCalledWith(input.apiKeyId, input.role);
});
it('should throw if removeRoleFromApiKey throws', async () => {
const input = { apiKeyId: 'bad-id', role: Role.GUEST };
vi.spyOn(authService, 'removeRoleFromApiKey').mockRejectedValue(
new Error('API key not found')
);
await expect(resolver.removeRole(input)).rejects.toThrow('API key not found');
});
});
});

View File

@@ -0,0 +1,80 @@
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { AuthService } from '@app/unraid-api/auth/auth.service.js';
import {
AuthActionVerb,
AuthPossession,
UsePermissions,
} from '@app/unraid-api/graph/directives/use-permissions.directive.js';
import {
AddRoleForApiKeyInput,
ApiKeyWithSecret,
CreateApiKeyInput,
DeleteApiKeyInput,
RemoveRoleFromApiKeyInput,
} from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js';
import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js';
import { ApiKeyMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';
import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js';
@Resolver(() => ApiKeyMutations)
export class ApiKeyMutationsResolver {
constructor(
private authService: AuthService,
private apiKeyService: ApiKeyService
) {}
@UsePermissions({
action: AuthActionVerb.CREATE,
resource: Resource.API_KEY,
possession: AuthPossession.ANY,
})
@ResolveField(() => ApiKeyWithSecret, { description: 'Create an API key' })
async create(@Args('input') unvalidatedInput: CreateApiKeyInput): Promise<ApiKeyWithSecret> {
const input = await validateObject(CreateApiKeyInput, unvalidatedInput);
const apiKey = await this.apiKeyService.create({
name: input.name,
description: input.description ?? undefined,
roles: input.roles ?? [],
permissions: input.permissions ?? [],
overwrite: input.overwrite ?? false,
});
await this.authService.syncApiKeyRoles(apiKey.id, apiKey.roles);
return apiKey;
}
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.API_KEY,
possession: AuthPossession.ANY,
})
@ResolveField(() => Boolean, { description: 'Add a role to an API key' })
async addRole(@Args('input') input: AddRoleForApiKeyInput): Promise<boolean> {
const validatedInput = await validateObject(AddRoleForApiKeyInput, input);
return this.authService.addRoleToApiKey(validatedInput.apiKeyId, Role[validatedInput.role]);
}
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.API_KEY,
possession: AuthPossession.ANY,
})
@ResolveField(() => Boolean, { description: 'Remove a role from an API key' })
async removeRole(@Args('input') input: RemoveRoleFromApiKeyInput): Promise<boolean> {
const validatedInput = await validateObject(RemoveRoleFromApiKeyInput, input);
return this.authService.removeRoleFromApiKey(validatedInput.apiKeyId, Role[validatedInput.role]);
}
@UsePermissions({
action: AuthActionVerb.DELETE,
resource: Resource.API_KEY,
possession: AuthPossession.ANY,
})
@ResolveField(() => Boolean, { description: 'Delete one or more API keys' })
async delete(@Args('input') input: DeleteApiKeyInput): Promise<boolean> {
const validatedInput = await validateObject(DeleteApiKeyInput, input);
await this.apiKeyService.deleteApiKeys(validatedInput.ids);
return true;
}
}

View File

@@ -78,65 +78,4 @@ describe('ApiKeyResolver', () => {
expect(apiKeyService.findById).toHaveBeenCalled(); expect(apiKeyService.findById).toHaveBeenCalled();
}); });
}); });
describe('createApiKey', () => {
it('should create new API key and sync roles', async () => {
const input = {
name: 'New API Key',
description: 'New API Key Description',
roles: [Role.GUEST],
permissions: [],
};
vi.spyOn(apiKeyService, 'create').mockResolvedValue(mockApiKeyWithSecret);
vi.spyOn(authService, 'syncApiKeyRoles').mockResolvedValue();
const result = await resolver.createApiKey(input);
expect(result).toEqual(mockApiKeyWithSecret);
expect(apiKeyService.create).toHaveBeenCalledWith({
name: input.name,
description: input.description,
overwrite: false,
roles: input.roles,
permissions: [],
});
expect(authService.syncApiKeyRoles).toHaveBeenCalledWith(mockApiKey.id, mockApiKey.roles);
});
});
describe('addRoleForApiKey', () => {
it('should add role to API key', async () => {
const input = {
apiKeyId: mockApiKey.id,
role: Role.ADMIN,
};
vi.spyOn(authService, 'addRoleToApiKey').mockResolvedValue(true);
const result = await resolver.addRoleForApiKey(input);
expect(result).toBe(true);
expect(authService.addRoleToApiKey).toHaveBeenCalledWith(input.apiKeyId, Role[input.role]);
});
});
describe('removeRoleFromApiKey', () => {
it('should remove role from API key', async () => {
const input = {
apiKeyId: mockApiKey.id,
role: Role.ADMIN,
};
vi.spyOn(authService, 'removeRoleFromApiKey').mockResolvedValue(true);
const result = await resolver.removeRoleFromApiKey(input);
expect(result).toBe(true);
expect(authService.removeRoleFromApiKey).toHaveBeenCalledWith(
input.apiKeyId,
Role[input.role]
);
});
});
}); });

View File

@@ -1,4 +1,4 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Args, Query, Resolver } from '@nestjs/graphql';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js'; import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { AuthService } from '@app/unraid-api/auth/auth.service.js'; import { AuthService } from '@app/unraid-api/auth/auth.service.js';
@@ -7,15 +7,8 @@ import {
AuthPossession, AuthPossession,
UsePermissions, UsePermissions,
} from '@app/unraid-api/graph/directives/use-permissions.directive.js'; } from '@app/unraid-api/graph/directives/use-permissions.directive.js';
import { import { ApiKey, Permission } from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js';
AddRoleForApiKeyInput,
ApiKey,
ApiKeyWithSecret,
CreateApiKeyInput,
RemoveRoleFromApiKeyInput,
} from '@app/unraid-api/graph/resolvers/api-key/api-key.model.js';
import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js'; import { Resource, Role } from '@app/unraid-api/graph/resolvers/base.model.js';
import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js';
import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js'; import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js';
@Resolver(() => ApiKey) @Resolver(() => ApiKey)
@@ -48,60 +41,29 @@ export class ApiKeyResolver {
return this.apiKeyService.findById(id); return this.apiKeyService.findById(id);
} }
@Mutation(() => ApiKeyWithSecret) @Query(() => [Role], { description: 'All possible roles for API keys' })
@UsePermissions({ @UsePermissions({
action: AuthActionVerb.CREATE, action: AuthActionVerb.READ,
resource: Resource.API_KEY, resource: Resource.PERMISSION,
possession: AuthPossession.ANY, possession: AuthPossession.ANY,
}) })
async createApiKey( async apiKeyPossibleRoles(): Promise<Role[]> {
@Args('input') return Object.values(Role);
unvalidatedInput: CreateApiKeyInput
): Promise<ApiKeyWithSecret> {
// Validate the input using class-validator
const input = await validateObject(CreateApiKeyInput, unvalidatedInput);
const apiKey = await this.apiKeyService.create({
name: input.name,
description: input.description ?? undefined,
roles: input.roles ?? [],
permissions: input.permissions ?? [],
overwrite: input.overwrite ?? false,
});
await this.authService.syncApiKeyRoles(apiKey.id, apiKey.roles);
return apiKey;
} }
@Mutation(() => Boolean) @Query(() => [Permission], { description: 'All possible permissions for API keys' })
@UsePermissions({ @UsePermissions({
action: AuthActionVerb.UPDATE, action: AuthActionVerb.READ,
resource: Resource.API_KEY, resource: Resource.PERMISSION,
possession: AuthPossession.ANY, possession: AuthPossession.ANY,
}) })
async addRoleForApiKey( async apiKeyPossiblePermissions(): Promise<Permission[]> {
@Args('input') // Build all combinations of Resource and AuthActionVerb
input: AddRoleForApiKeyInput const resources = Object.values(Resource);
): Promise<boolean> { const actions = Object.values(AuthActionVerb);
// Validate the input using class-validator return resources.map((resource) => ({
const validatedInput = await validateObject(AddRoleForApiKeyInput, input); resource,
actions,
return this.authService.addRoleToApiKey(validatedInput.apiKeyId, Role[validatedInput.role]); }));
}
@Mutation(() => Boolean)
@UsePermissions({
action: AuthActionVerb.UPDATE,
resource: Resource.API_KEY,
possession: AuthPossession.ANY,
})
async removeRoleFromApiKey(
@Args('input')
input: RemoveRoleFromApiKeyInput
): Promise<boolean> {
// Validate the input using class-validator
const validatedInput = await validateObject(RemoveRoleFromApiKeyInput, input);
return this.authService.removeRoleFromApiKey(validatedInput.apiKeyId, Role[validatedInput.role]);
} }
} }

View File

@@ -9,6 +9,11 @@ export class DockerMutations {}
@ObjectType() @ObjectType()
export class VmMutations {} export class VmMutations {}
@ObjectType({
description: 'API Key related mutations',
})
export class ApiKeyMutations {}
@ObjectType({ @ObjectType({
description: 'Parity check related mutations, WIP, response types and functionaliy will change', description: 'Parity check related mutations, WIP, response types and functionaliy will change',
}) })
@@ -25,6 +30,9 @@ export class RootMutations {
@Field(() => VmMutations, { description: 'VM related mutations' }) @Field(() => VmMutations, { description: 'VM related mutations' })
vm: VmMutations = new VmMutations(); vm: VmMutations = new VmMutations();
@Field(() => ApiKeyMutations, { description: 'API Key related mutations' })
apiKey: ApiKeyMutations = new ApiKeyMutations();
@Field(() => ParityCheckMutations, { description: 'Parity check related mutations' }) @Field(() => ParityCheckMutations, { description: 'Parity check related mutations' })
parityCheck: ParityCheckMutations = new ParityCheckMutations(); parityCheck: ParityCheckMutations = new ParityCheckMutations();
} }

View File

@@ -1,6 +1,7 @@
import { Mutation, Resolver } from '@nestjs/graphql'; import { Mutation, Resolver } from '@nestjs/graphql';
import { import {
ApiKeyMutations,
ArrayMutations, ArrayMutations,
DockerMutations, DockerMutations,
ParityCheckMutations, ParityCheckMutations,
@@ -12,21 +13,26 @@ import {
export class RootMutationsResolver { export class RootMutationsResolver {
@Mutation(() => ArrayMutations, { name: 'array' }) @Mutation(() => ArrayMutations, { name: 'array' })
array(): ArrayMutations { array(): ArrayMutations {
return new ArrayMutations(); // You can pass context/state here if needed return new ArrayMutations();
} }
@Mutation(() => DockerMutations, { name: 'docker' }) @Mutation(() => DockerMutations, { name: 'docker' })
docker(): DockerMutations { docker(): DockerMutations {
return new DockerMutations(); // You can pass context/state here if needed return new DockerMutations();
} }
@Mutation(() => VmMutations, { name: 'vm' }) @Mutation(() => VmMutations, { name: 'vm' })
vm(): VmMutations { vm(): VmMutations {
return new VmMutations(); // You can pass context/state here if needed return new VmMutations();
} }
@Mutation(() => ParityCheckMutations, { name: 'parityCheck' }) @Mutation(() => ParityCheckMutations, { name: 'parityCheck' })
parityCheck(): ParityCheckMutations { parityCheck(): ParityCheckMutations {
return new ParityCheckMutations(); // You can pass context/state here if needed return new ParityCheckMutations();
}
@Mutation(() => ApiKeyMutations, { name: 'apiKey' })
apiKey(): ApiKeyMutations {
return new ApiKeyMutations();
} }
} }

View File

@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AuthModule } from '@app/unraid-api/auth/auth.module.js'; import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
import { ApiKeyModule } from '@app/unraid-api/graph/resolvers/api-key/api-key.module.js';
import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js'; import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js';
import { ArrayModule } from '@app/unraid-api/graph/resolvers/array/array.module.js'; import { ArrayModule } from '@app/unraid-api/graph/resolvers/array/array.module.js';
import { ArrayMutationsResolver } from '@app/unraid-api/graph/resolvers/array/array.mutations.resolver.js'; import { ArrayMutationsResolver } from '@app/unraid-api/graph/resolvers/array/array.mutations.resolver.js';
@@ -34,9 +35,8 @@ import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js'
import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js'; import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js';
@Module({ @Module({
imports: [ArrayModule, AuthModule, ConnectModule, CustomizationModule, DockerModule, DisksModule], imports: [ArrayModule, ApiKeyModule, ConnectModule, CustomizationModule, DockerModule, DisksModule],
providers: [ providers: [
ApiKeyResolver,
CloudResolver, CloudResolver,
ConfigResolver, ConfigResolver,
DisplayResolver, DisplayResolver,
@@ -60,6 +60,6 @@ import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js';
VmsResolver, VmsResolver,
VmsService, VmsService,
], ],
exports: [AuthModule, ApiKeyResolver], exports: [ApiKeyModule],
}) })
export class ResolversModule {} export class ResolversModule {}

2
pnpm-lock.yaml generated
View File

@@ -694,7 +694,7 @@ importers:
specifier: ^0.511.0 specifier: ^0.511.0
version: 0.511.0(vue@3.5.13(typescript@5.8.3)) version: 0.511.0(vue@3.5.13(typescript@5.8.3))
reka-ui: reka-ui:
specifier: ^2.1.0 specifier: ^2.1.1
version: 2.1.1(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)) version: 2.1.1(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
shadcn-vue: shadcn-vue:
specifier: ^2.0.0 specifier: ^2.0.0

View File

@@ -53,7 +53,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"kebab-case": "^2.0.1", "kebab-case": "^2.0.1",
"lucide-vue-next": "^0.511.0", "lucide-vue-next": "^0.511.0",
"reka-ui": "^2.1.0", "reka-ui": "^2.1.1",
"shadcn-vue": "^2.0.0", "shadcn-vue": "^2.0.0",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"vue-sonner": "^1.3.0" "vue-sonner": "^1.3.0"

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
AccordionRoot,
useForwardPropsEmits,
type AccordionRootEmits,
type AccordionRootProps,
} from 'reka-ui';
const props = defineProps<AccordionRootProps>();
const emits = defineEmits<AccordionRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<AccordionRoot v-bind="forwarded">
<slot />
</AccordionRoot>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
import { AccordionContent, type AccordionContentProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<AccordionContent
v-bind="delegatedProps"
class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
>
<div :class="cn('pb-4 pt-0', props.class)">
<slot />
</div>
</AccordionContent>
</template>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
import { AccordionItem, useForwardProps, type AccordionItemProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');
const forwardedProps = useForwardProps(delegatedProps);
</script>
<template>
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
<slot />
</AccordionItem>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { cn } from '@/lib/utils';
import { reactiveOmit } from '@vueuse/core';
import { ChevronDown } from 'lucide-vue-next';
import { AccordionHeader, AccordionTrigger, type AccordionTriggerProps } from 'reka-ui';
import type { HTMLAttributes } from 'vue';
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>();
const delegatedProps = reactiveOmit(props, 'class');
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
v-bind="delegatedProps"
:class="
cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
props.class
)
"
>
<slot />
<slot name="icon">
<ChevronDown class="h-4 w-4 shrink-0 transition-transform duration-200" />
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>

View File

@@ -0,0 +1,4 @@
export { default as Accordion } from './Accordion.vue';
export { default as AccordionContent } from './AccordionContent.vue';
export { default as AccordionItem } from './AccordionItem.vue';
export { default as AccordionTrigger } from './AccordionTrigger.vue';

View File

@@ -9,6 +9,12 @@ import {
BrandLogoConnect, BrandLogoConnect,
type BrandButtonProps, type BrandButtonProps,
} from '@/components/brand'; } from '@/components/brand';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/common/accordion';
// Components // Components
import { Badge, type BadgeProps } from '@/components/common/badge'; import { Badge, type BadgeProps } from '@/components/common/badge';
import { Button, buttonVariants, type ButtonProps } from '@/components/common/button'; import { Button, buttonVariants, type ButtonProps } from '@/components/common/button';
@@ -80,6 +86,10 @@ import tailwindConfig from '../tailwind.config';
// Export // Export
export { export {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Bar, Bar,
Badge, Badge,
BrandButton, BrandButton,

View File

@@ -1,6 +1,7 @@
import tailwindRemToRem from '@unraid/tailwind-rem-to-rem'; import tailwindRemToRem from '@unraid/tailwind-rem-to-rem';
import type { Config } from 'tailwindcss'; import type { Config } from 'tailwindcss';
import tailwindcssAnimate from 'tailwindcss-animate'; import tailwindcssAnimate from 'tailwindcss-animate';
/* eslint-disable no-relative-import-paths/no-relative-import-paths */
import { unraidPreset } from './src/theme/preset'; import { unraidPreset } from './src/theme/preset';
export default { export default {
@@ -88,6 +89,28 @@ export default {
'5': 'hsl(var(--chart-5))', '5': 'hsl(var(--chart-5))',
}, },
}, },
keyframes: {
'accordion-down': {
from: {
height: '0',
},
to: {
height: 'var(--reka-accordion-content-height)',
},
},
'accordion-up': {
from: {
height: 'var(--reka-accordion-content-height)',
},
to: {
height: '0',
},
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
}, },
}, },
} satisfies Partial<Config>; } satisfies Partial<Config>;

View File

@@ -0,0 +1,183 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useMutation } from '@vue/apollo-composable';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Button,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@unraid/ui';
import { CREATE_API_KEY } from './apikey.query';
const props = defineProps<{
possibleRoles: string[];
possiblePermissions: { resource: string; actions: string[] }[];
}>();
const emit = defineEmits(['created', 'cancel']);
const newKeyName = ref('');
const newKeyDescription = ref('');
const newKeyRoles = ref<string[]>([]);
const newKeyPermissions = ref<{ resource: string; actions: string[] }[]>([]);
const { mutate: createApiKey, loading, error } = useMutation<
{ apiKey: { create: { key: string } } },
{ input: { name: string; description?: string; roles?: string[]; permissions?: { resource: string; actions: string[] }[] } }
>(CREATE_API_KEY);
const postCreateLoading = ref(false);
function togglePermission(resource: string, action: string, checked: boolean) {
const perm = newKeyPermissions.value.find(p => p.resource === resource);
if (checked) {
if (perm) {
if (!perm.actions.includes(action)) perm.actions.push(action);
} else {
newKeyPermissions.value.push({ resource, actions: [action] });
}
} else {
if (perm) {
perm.actions = perm.actions.filter(a => a !== action);
if (perm.actions.length === 0) {
newKeyPermissions.value = newKeyPermissions.value.filter(p => p.resource !== resource);
}
}
}
}
function areAllPermissionsSelected() {
return props.possiblePermissions.every(perm => {
const selected = newKeyPermissions.value.find(p => p.resource === perm.resource)?.actions || [];
return perm.actions.every(a => selected.includes(a));
});
}
function selectAllPermissions() {
newKeyPermissions.value = props.possiblePermissions.map(perm => ({
resource: perm.resource,
actions: [...perm.actions],
}));
}
function clearAllPermissions() {
newKeyPermissions.value = [];
}
function areAllActionsSelected(resource: string) {
const perm = props.possiblePermissions.find(p => p.resource === resource);
if (!perm) return false;
const selected = newKeyPermissions.value.find(p => p.resource === resource)?.actions || [];
return perm.actions.every(a => selected.includes(a));
}
function selectAllActions(resource: string) {
const perm = props.possiblePermissions.find(p => p.resource === resource);
if (!perm) return;
const idx = newKeyPermissions.value.findIndex(p => p.resource === resource);
if (idx !== -1) {
newKeyPermissions.value[idx].actions = [...perm.actions];
} else {
newKeyPermissions.value.push({ resource, actions: [...perm.actions] });
}
}
function clearAllActions(resource: string) {
newKeyPermissions.value = newKeyPermissions.value.filter(p => p.resource !== resource);
}
async function createKey() {
const res = await createApiKey({
input: {
name: newKeyName.value,
description: newKeyDescription.value,
roles: newKeyRoles.value,
permissions: newKeyPermissions.value.length ? newKeyPermissions.value : undefined,
},
});
postCreateLoading.value = true;
setTimeout(() => {
emit('created', res?.data?.apiKey?.create ?? null);
postCreateLoading.value = false;
newKeyName.value = '';
newKeyDescription.value = '';
newKeyRoles.value = [];
newKeyPermissions.value = [];
}, 1000);
}
</script>
<template>
<div class="mb-4 p-4 border rounded bg-muted">
<div class="mb-2">
<Label for="api-key-name">Name</Label>
<Input id="api-key-name" v-model="newKeyName" placeholder="Name" class="mt-1" />
</div>
<div class="mb-2">
<Label for="api-key-desc">Description</Label>
<Input id="api-key-desc" v-model="newKeyDescription" placeholder="Description" class="mt-1" />
</div>
<div class="mb-2">
<Label for="api-key-roles">Roles</Label>
<Select v-model="newKeyRoles" multiple class="mt-1 w-full">
<SelectTrigger>
<SelectValue placeholder="Select Roles" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="role in props.possibleRoles" :key="role" :value="role">{{ role }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="mb-2">
<Accordion type="single" collapsible class="w-full mt-2">
<AccordionItem value="permissions">
<AccordionTrigger>Permissions</AccordionTrigger>
<AccordionContent>
<div class="flex flex-row justify-end mb-2">
<span class="mr-auto text-sm text-muted-foreground">
Selected: {{ newKeyPermissions.reduce((sum, perm) => sum + perm.actions.length, 0) }}
</span>
<Button size="sm" variant="secondary" @click="areAllPermissionsSelected() ? clearAllPermissions() : selectAllPermissions()">
{{ areAllPermissionsSelected() ? 'Select None' : 'Select All' }}
</Button>
</div>
<div class="flex flex-col gap-2 mt-1">
<div v-for="perm in props.possiblePermissions" :key="perm.resource" class="border rounded p-2">
<div class="flex items-center justify-between mb-1">
<span class="font-semibold">{{ perm.resource }}</span>
<Button size="sm" variant="secondary" @click="areAllActionsSelected(perm.resource) ? clearAllActions(perm.resource) : selectAllActions(perm.resource)">
{{ areAllActionsSelected(perm.resource) ? 'Select None' : 'Select All' }}
</Button>
</div>
<div class="flex gap-4 flex-wrap">
<label v-for="action in perm.actions" :key="action" class="flex items-center gap-1">
<input
type="checkbox"
:checked="!!newKeyPermissions.find(p => p.resource === perm.resource && p.actions.includes(action))"
@change="(e: Event) => togglePermission(perm.resource, action, (e.target as HTMLInputElement)?.checked)"
/>
<span>{{ action }}</span>
</label>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<div class="flex gap-2 mt-2">
<Button variant="primary" :disabled="loading || postCreateLoading" @click="createKey">
<span v-if="loading || postCreateLoading">Creating...</span>
<span v-else>Create</span>
</Button>
<Button variant="secondary" @click="$emit('cancel')">Cancel</Button>
</div>
<div v-if="error" class="text-red-500 mt-2 text-sm">
{{ error.message }}
</div>
</div>
</template>

View File

@@ -0,0 +1,125 @@
<script setup lang="ts">
import { ref, watchEffect } from 'vue';
import { useMutation, useQuery } from '@vue/apollo-composable';
import {
Button,
CardWrapper,
PageContainer,
} from '@unraid/ui';
import { DELETE_API_KEY, GET_API_KEY_META, GET_API_KEYS } from './apikey.query';
import ApiKeyCreate from './ApiKeyCreate.vue';
import { EyeIcon, EyeSlashIcon } from '@heroicons/vue/24/solid';
interface ApiKey {
id: string;
name: string;
description?: string;
createdAt: string;
roles: string[];
permissions: { resource: string; actions: string[] }[];
}
const { result, refetch } = useQuery<{ apiKeys: ApiKey[] }>(GET_API_KEYS);
const apiKeys = ref<ApiKey[]>([]);
watchEffect(() => {
apiKeys.value = result.value?.apiKeys || [];
});
const metaQuery = useQuery(GET_API_KEY_META);
const possibleRoles = ref<string[]>([]);
const possiblePermissions = ref<{ resource: string; actions: string[] }[]>([]);
watchEffect(() => {
possibleRoles.value = metaQuery.result.value?.apiKeyPossibleRoles || [];
possiblePermissions.value = metaQuery.result.value?.apiKeyPossiblePermissions || [];
});
const showCreate = ref(false);
const createdKey = ref<{ id: string; key: string } | null>(null);
const showKey = ref(false);
const { mutate: deleteKey } = useMutation(DELETE_API_KEY);
const deleteError = ref<string | null>(null);
function toggleShowKey() {
showKey.value = !showKey.value;
}
function onCreated(key: { id: string; key: string } | null) {
createdKey.value = key;
showCreate.value = false;
refetch();
}
async function _deleteKey(_id: string) {
if (!window.confirm('Are you sure you want to delete this API key? This action cannot be undone.')) return;
deleteError.value = null;
try {
await deleteKey({ input: { ids: [_id] } });
await refetch();
} catch (err: unknown) {
if (typeof err === 'object' && err !== null && 'message' in err && typeof (err as { message?: unknown }).message === 'string') {
deleteError.value = (err as { message: string }).message;
} else {
deleteError.value = 'Failed to delete API key.';
}
}
}
</script>
<template>
<PageContainer>
<CardWrapper>
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold">API Keys</h2>
<Button variant="primary" @click="showCreate = true">Create API Key</Button>
</div>
<div v-if="deleteError" class="mb-2 p-2 bg-red-100 text-red-700 border border-red-300 rounded">
{{ deleteError }}
</div>
<ul v-if="apiKeys.length" class="space-y-2 mb-4">
<li
v-for="key in apiKeys"
:key="key.id"
class="flex items-center justify-between p-2 border rounded"
>
<div>
<span class="font-medium">{{ key.name }}</span>
<div v-if="key.roles.length" class="mt-1">
<span class="font-semibold">Roles:</span>
<span>{{ key.roles.join(', ') }}</span>
</div>
<div v-if="key.permissions.length" class="mt-1">
<span class="font-semibold">Permissions:</span>
<ul class="ml-2">
<li v-for="perm in key.permissions" :key="perm.resource">
<span class="font-medium">{{ perm.resource }}</span>
<span v-if="perm.actions && perm.actions.length"> ({{ perm.actions.join(', ') }})</span>
</li>
</ul>
</div>
<div v-if="createdKey && createdKey.key && createdKey.id === key.id" class="mt-2 flex items-center gap-2">
<span>API Key created:</span>
<b>{{ showKey ? createdKey.key : '••••••••••••••••••••••••••••••••' }}</b>
<button type="button" class="focus:outline-none" @click="toggleShowKey">
<component :is="showKey ? EyeSlashIcon : EyeIcon" class="w-5 h-5 text-gray-500" />
</button>
</div>
</div>
<Button variant="destructive" size="sm" @click="_deleteKey(key.id)">Delete</Button>
</li>
</ul>
<div v-if="showCreate" class="mb-4 p-4 border rounded bg-muted">
<ApiKeyCreate
v-if="showCreate"
:possible-roles="possibleRoles"
:possible-permissions="possiblePermissions"
@created="onCreated"
@cancel="showCreate = false"
/>
</div>
</CardWrapper>
</PageContainer>
</template>

View File

@@ -0,0 +1,55 @@
import { graphql } from '~/composables/gql/gql';
export const GET_API_KEYS = graphql(/* GraphQL */ `
query ApiKeys {
apiKeys {
id
name
description
createdAt
roles
permissions {
resource
actions
}
}
}
`);
export const CREATE_API_KEY = graphql(/* GraphQL */ `
mutation CreateApiKey($input: CreateApiKeyInput!) {
apiKey {
create(input: $input) {
id
key
name
description
createdAt
roles
permissions {
resource
actions
}
}
}
}
`);
export const DELETE_API_KEY = graphql(/* GraphQL */ `
mutation DeleteApiKey($input: DeleteApiKeyInput!) {
apiKey {
delete(input: $input)
}
}
`);
export const GET_API_KEY_META = graphql(/* GraphQL */ `
query ApiKeyMeta {
apiKeyPossibleRoles
apiKeyPossiblePermissions {
resource
actions
}
}
`);

View File

@@ -16,6 +16,10 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
type Documents = { type Documents = {
"\n query PartnerInfo {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n": typeof types.PartnerInfoDocument, "\n query PartnerInfo {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n": typeof types.PartnerInfoDocument,
"\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n": typeof types.ActivationCodeDocument, "\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n": typeof types.ActivationCodeDocument,
"\n query ApiKeys {\n apiKeys {\n id\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n": typeof types.ApiKeysDocument,
"\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n }\n": typeof types.CreateApiKeyDocument,
"\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n": typeof types.DeleteApiKeyDocument,
"\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n": typeof types.ApiKeyMetaDocument,
"\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, "\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,
"\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, "\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,
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": typeof types.LogFilesDocument, "\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": typeof types.LogFilesDocument,
@@ -45,6 +49,10 @@ type Documents = {
const documents: Documents = { const documents: Documents = {
"\n query PartnerInfo {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n": types.PartnerInfoDocument, "\n query PartnerInfo {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n": types.PartnerInfoDocument,
"\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n": types.ActivationCodeDocument, "\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n": types.ActivationCodeDocument,
"\n query ApiKeys {\n apiKeys {\n id\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n": types.ApiKeysDocument,
"\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n }\n": types.CreateApiKeyDocument,
"\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n": types.DeleteApiKeyDocument,
"\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n": types.ApiKeyMetaDocument,
"\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, "\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,
"\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, "\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,
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": types.LogFilesDocument, "\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": types.LogFilesDocument,
@@ -94,6 +102,22 @@ export function graphql(source: "\n query PartnerInfo {\n publicPartnerInfo
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql(source: "\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n"): (typeof documents)["\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n"]; export function graphql(source: "\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n"): (typeof documents)["\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ApiKeys {\n apiKeys {\n id\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n"): (typeof documents)["\n query ApiKeys {\n apiKeys {\n id\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n"): (typeof documents)["\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n"): (typeof documents)["\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

View File

@@ -131,6 +131,43 @@ export type ApiKey = Node & {
roles: Array<Role>; roles: Array<Role>;
}; };
/** API Key related mutations */
export type ApiKeyMutations = {
__typename?: 'ApiKeyMutations';
/** Add a role to an API key */
addRole: Scalars['Boolean']['output'];
/** Create an API key */
create: ApiKeyWithSecret;
/** Delete one or more API keys */
delete: Scalars['Boolean']['output'];
/** Remove a role from an API key */
removeRole: Scalars['Boolean']['output'];
};
/** API Key related mutations */
export type ApiKeyMutationsAddRoleArgs = {
input: AddRoleForApiKeyInput;
};
/** API Key related mutations */
export type ApiKeyMutationsCreateArgs = {
input: CreateApiKeyInput;
};
/** API Key related mutations */
export type ApiKeyMutationsDeleteArgs = {
input: DeleteApiKeyInput;
};
/** API Key related mutations */
export type ApiKeyMutationsRemoveRoleArgs = {
input: RemoveRoleFromApiKeyInput;
};
export type ApiKeyResponse = { export type ApiKeyResponse = {
__typename?: 'ApiKeyResponse'; __typename?: 'ApiKeyResponse';
error?: Maybe<Scalars['String']['output']>; error?: Maybe<Scalars['String']['output']>;
@@ -497,6 +534,10 @@ export type Customization = {
theme: Theme; theme: Theme;
}; };
export type DeleteApiKeyInput = {
ids: Array<Scalars['PrefixedID']['input']>;
};
export type Devices = Node & { export type Devices = Node & {
__typename?: 'Devices'; __typename?: 'Devices';
gpu: Array<Gpu>; gpu: Array<Gpu>;
@@ -850,7 +891,7 @@ export type MinigraphqlResponse = {
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
addRoleForApiKey: Scalars['Boolean']['output']; apiKey: ApiKeyMutations;
archiveAll: NotificationOverview; archiveAll: NotificationOverview;
/** Marks a notification as archived. */ /** Marks a notification as archived. */
archiveNotification: Notification; archiveNotification: Notification;
@@ -858,7 +899,6 @@ export type Mutation = {
array: ArrayMutations; array: ArrayMutations;
connectSignIn: Scalars['Boolean']['output']; connectSignIn: Scalars['Boolean']['output'];
connectSignOut: Scalars['Boolean']['output']; connectSignOut: Scalars['Boolean']['output'];
createApiKey: ApiKeyWithSecret;
/** Creates a new notification record */ /** Creates a new notification record */
createNotification: Notification; createNotification: Notification;
/** Deletes all archived notifications on server. */ /** Deletes all archived notifications on server. */
@@ -869,7 +909,6 @@ export type Mutation = {
parityCheck: ParityCheckMutations; parityCheck: ParityCheckMutations;
/** Reads each notification to recompute & update the overview. */ /** Reads each notification to recompute & update the overview. */
recalculateOverview: NotificationOverview; recalculateOverview: NotificationOverview;
removeRoleFromApiKey: Scalars['Boolean']['output'];
setAdditionalAllowedOrigins: Array<Scalars['String']['output']>; setAdditionalAllowedOrigins: Array<Scalars['String']['output']>;
setDemo: Scalars['String']['output']; setDemo: Scalars['String']['output'];
setupRemoteAccess: Scalars['Boolean']['output']; setupRemoteAccess: Scalars['Boolean']['output'];
@@ -882,11 +921,6 @@ export type Mutation = {
}; };
export type MutationAddRoleForApiKeyArgs = {
input: AddRoleForApiKeyInput;
};
export type MutationArchiveAllArgs = { export type MutationArchiveAllArgs = {
importance?: InputMaybe<NotificationImportance>; importance?: InputMaybe<NotificationImportance>;
}; };
@@ -907,11 +941,6 @@ export type MutationConnectSignInArgs = {
}; };
export type MutationCreateApiKeyArgs = {
input: CreateApiKeyInput;
};
export type MutationCreateNotificationArgs = { export type MutationCreateNotificationArgs = {
input: NotificationData; input: NotificationData;
}; };
@@ -928,11 +957,6 @@ export type MutationEnableDynamicRemoteAccessArgs = {
}; };
export type MutationRemoveRoleFromApiKeyArgs = {
input: RemoveRoleFromApiKeyInput;
};
export type MutationSetAdditionalAllowedOriginsArgs = { export type MutationSetAdditionalAllowedOriginsArgs = {
input: AllowedOriginInput; input: AllowedOriginInput;
}; };
@@ -1145,6 +1169,10 @@ export type PublicPartnerInfo = {
export type Query = { export type Query = {
__typename?: 'Query'; __typename?: 'Query';
apiKey?: Maybe<ApiKey>; apiKey?: Maybe<ApiKey>;
/** All possible permissions for API keys */
apiKeyPossiblePermissions: Array<Permission>;
/** All possible roles for API keys */
apiKeyPossibleRoles: Array<Role>;
apiKeys: Array<ApiKey>; apiKeys: Array<ApiKey>;
array: UnraidArray; array: UnraidArray;
cloud: Cloud; cloud: Cloud;
@@ -1799,6 +1827,30 @@ export type ActivationCodeQueryVariables = Exact<{ [key: string]: never; }>;
export type ActivationCodeQuery = { __typename?: 'Query', vars: { __typename?: 'Vars', regState?: RegistrationState | null }, customization?: { __typename?: 'Customization', activationCode?: { __typename?: 'ActivationCode', code?: string | null, partnerName?: string | null, serverName?: string | null, sysModel?: string | null, comment?: string | null, header?: string | null, headermetacolor?: string | null, background?: string | null, showBannerGradient?: boolean | null, theme?: string | null } | null, partnerInfo?: { __typename?: 'PublicPartnerInfo', hasPartnerLogo: boolean, partnerName?: string | null, partnerUrl?: string | null, partnerLogoUrl?: string | null } | null } | null }; export type ActivationCodeQuery = { __typename?: 'Query', vars: { __typename?: 'Vars', regState?: RegistrationState | null }, customization?: { __typename?: 'Customization', activationCode?: { __typename?: 'ActivationCode', code?: string | null, partnerName?: string | null, serverName?: string | null, sysModel?: string | null, comment?: string | null, header?: string | null, headermetacolor?: string | null, background?: string | null, showBannerGradient?: boolean | null, theme?: string | null } | null, partnerInfo?: { __typename?: 'PublicPartnerInfo', hasPartnerLogo: boolean, partnerName?: string | null, partnerUrl?: string | null, partnerLogoUrl?: string | null } | null } | null };
export type ApiKeysQueryVariables = Exact<{ [key: string]: never; }>;
export type ApiKeysQuery = { __typename?: 'Query', apiKeys: Array<{ __typename?: 'ApiKey', id: string, name: string, description?: string | null, createdAt: string, roles: Array<Role>, permissions: Array<{ __typename?: 'Permission', resource: Resource, actions: Array<string> }> }> };
export type CreateApiKeyMutationVariables = Exact<{
input: CreateApiKeyInput;
}>;
export type CreateApiKeyMutation = { __typename?: 'Mutation', apiKey: { __typename?: 'ApiKeyMutations', create: { __typename?: 'ApiKeyWithSecret', id: string, key: string, name: string, description?: string | null, createdAt: string, roles: Array<Role>, permissions: Array<{ __typename?: 'Permission', resource: Resource, actions: Array<string> }> } } };
export type DeleteApiKeyMutationVariables = Exact<{
input: DeleteApiKeyInput;
}>;
export type DeleteApiKeyMutation = { __typename?: 'Mutation', apiKey: { __typename?: 'ApiKeyMutations', delete: boolean } };
export type ApiKeyMetaQueryVariables = Exact<{ [key: string]: never; }>;
export type ApiKeyMetaQuery = { __typename?: 'Query', apiKeyPossibleRoles: Array<Role>, apiKeyPossiblePermissions: Array<{ __typename?: 'Permission', resource: Resource, actions: Array<string> }> };
export type GetConnectSettingsFormQueryVariables = Exact<{ [key: string]: never; }>; export type GetConnectSettingsFormQueryVariables = Exact<{ [key: string]: never; }>;
@@ -1965,6 +2017,10 @@ export const NotificationCountFragmentFragmentDoc = {"kind":"Document","definiti
export const PartialCloudFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<PartialCloudFragment, unknown>; export const PartialCloudFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<PartialCloudFragment, unknown>;
export const PartnerInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PartnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publicPartnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasPartnerLogo"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"partnerUrl"}},{"kind":"Field","name":{"kind":"Name","value":"partnerLogoUrl"}}]}}]}}]} as unknown as DocumentNode<PartnerInfoQuery, PartnerInfoQueryVariables>; export const PartnerInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PartnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publicPartnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasPartnerLogo"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"partnerUrl"}},{"kind":"Field","name":{"kind":"Name","value":"partnerLogoUrl"}}]}}]}}]} as unknown as DocumentNode<PartnerInfoQuery, PartnerInfoQueryVariables>;
export const ActivationCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActivationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regState"}}]}},{"kind":"Field","name":{"kind":"Name","value":"customization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"serverName"}},{"kind":"Field","name":{"kind":"Name","value":"sysModel"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"header"}},{"kind":"Field","name":{"kind":"Name","value":"headermetacolor"}},{"kind":"Field","name":{"kind":"Name","value":"background"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerGradient"}},{"kind":"Field","name":{"kind":"Name","value":"theme"}}]}},{"kind":"Field","name":{"kind":"Name","value":"partnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasPartnerLogo"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"partnerUrl"}},{"kind":"Field","name":{"kind":"Name","value":"partnerLogoUrl"}}]}}]}}]}}]} as unknown as DocumentNode<ActivationCodeQuery, ActivationCodeQueryVariables>; export const ActivationCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActivationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regState"}}]}},{"kind":"Field","name":{"kind":"Name","value":"customization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"serverName"}},{"kind":"Field","name":{"kind":"Name","value":"sysModel"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"header"}},{"kind":"Field","name":{"kind":"Name","value":"headermetacolor"}},{"kind":"Field","name":{"kind":"Name","value":"background"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerGradient"}},{"kind":"Field","name":{"kind":"Name","value":"theme"}}]}},{"kind":"Field","name":{"kind":"Name","value":"partnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasPartnerLogo"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"partnerUrl"}},{"kind":"Field","name":{"kind":"Name","value":"partnerLogoUrl"}}]}}]}}]}}]} as unknown as DocumentNode<ActivationCodeQuery, ActivationCodeQueryVariables>;
export const ApiKeysDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ApiKeys"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKeys"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"roles"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]}}]} as unknown as DocumentNode<ApiKeysQuery, ApiKeysQueryVariables>;
export const CreateApiKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateApiKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateApiKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"roles"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CreateApiKeyMutation, CreateApiKeyMutationVariables>;
export const DeleteApiKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApiKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteApiKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"delete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<DeleteApiKeyMutation, DeleteApiKeyMutationVariables>;
export const ApiKeyMetaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ApiKeyMeta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKeyPossibleRoles"}},{"kind":"Field","name":{"kind":"Name","value":"apiKeyPossiblePermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]} as unknown as DocumentNode<ApiKeyMetaQuery, ApiKeyMetaQueryVariables>;
export const GetConnectSettingsFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectSettingsForm"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connect"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}},{"kind":"Field","name":{"kind":"Name","value":"values"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sandbox"}},{"kind":"Field","name":{"kind":"Name","value":"extraOrigins"}},{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"ssoUserIds"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetConnectSettingsFormQuery, GetConnectSettingsFormQueryVariables>; export const GetConnectSettingsFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectSettingsForm"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connect"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}},{"kind":"Field","name":{"kind":"Name","value":"values"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sandbox"}},{"kind":"Field","name":{"kind":"Name","value":"extraOrigins"}},{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"ssoUserIds"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetConnectSettingsFormQuery, GetConnectSettingsFormQueryVariables>;
export const UpdateConnectSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateConnectSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ApiSettingsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateApiSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sandbox"}},{"kind":"Field","name":{"kind":"Name","value":"extraOrigins"}},{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"ssoUserIds"}}]}}]}}]} as unknown as DocumentNode<UpdateConnectSettingsMutation, UpdateConnectSettingsMutationVariables>; export const UpdateConnectSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateConnectSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ApiSettingsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateApiSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sandbox"}},{"kind":"Field","name":{"kind":"Name","value":"extraOrigins"}},{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"ssoUserIds"}}]}}]}}]} as unknown as DocumentNode<UpdateConnectSettingsMutation, UpdateConnectSettingsMutationVariables>;
export const LogFilesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"modifiedAt"}}]}}]}}]} as unknown as DocumentNode<LogFilesQuery, LogFilesQueryVariables>; export const LogFilesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"modifiedAt"}}]}}]}}]} as unknown as DocumentNode<LogFilesQuery, LogFilesQueryVariables>;

10
web/pages/apikey.vue Normal file
View File

@@ -0,0 +1,10 @@
<script setup lang="ts">
import ApiKeyManager from '~/components/ApiKey/ApiKeyManager.vue';
</script>
<template>
<div>
<h1>API Key</h1>
<ApiKeyManager />
</div>
</template>