mirror of
https://github.com/unraid/api.git
synced 2025-12-31 21:49:57 -06:00
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
9
.cursor/rules/web-graphql.mdc
Normal file
9
.cursor/rules/web-graphql.mdc
Normal 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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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!
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
2
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
19
unraid-ui/src/components/common/accordion/Accordion.vue
Normal file
19
unraid-ui/src/components/common/accordion/Accordion.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
18
unraid-ui/src/components/common/accordion/AccordionItem.vue
Normal file
18
unraid-ui/src/components/common/accordion/AccordionItem.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
4
unraid-ui/src/components/common/accordion/index.ts
Normal file
4
unraid-ui/src/components/common/accordion/index.ts
Normal 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';
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
183
web/components/ApiKey/ApiKeyCreate.vue
Normal file
183
web/components/ApiKey/ApiKeyCreate.vue
Normal 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>
|
||||||
125
web/components/ApiKey/ApiKeyManager.vue
Normal file
125
web/components/ApiKey/ApiKeyManager.vue
Normal 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>
|
||||||
55
web/components/ApiKey/apikey.query.ts
Normal file
55
web/components/ApiKey/apikey.query.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
10
web/pages/apikey.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user