mirror of
https://github.com/unraid/api.git
synced 2026-02-11 02:18:58 -06:00
286 lines
6.8 KiB
TypeScript
286 lines
6.8 KiB
TypeScript
/*!
|
|
* Copyright 2019-2020 Lime Technology Inc. All rights reserved.
|
|
* Written by: Alexis Tyler
|
|
*/
|
|
|
|
import { pluginManager, pubsub, utils, bus, errors, states, modules, apiManager, log } from '@unraid/core';
|
|
import dee from '@gridplus/docker-events';
|
|
import { setIntervalAsync } from 'set-interval-async/dynamic';
|
|
import GraphQLJSON from 'graphql-type-json';
|
|
import GraphQLLong from 'graphql-type-long';
|
|
import GraphQLUUID from 'graphql-type-uuid';
|
|
import { run, publish } from '../../run';
|
|
import { userCache, CachedServer, CachedServers } from '../../cache';
|
|
import { hasSubscribedToChannel } from '../../ws';
|
|
import { getServers as getUserServers } from '../../utils'
|
|
|
|
const { ensurePermission } = utils;
|
|
const { usersState, varState, networkState } = states;
|
|
const { AppError, PluginError } = errors;
|
|
|
|
// Update array values when slots change
|
|
bus.on('slots', async () => {
|
|
// @todo: Create a system user for this
|
|
const user = usersState.findOne({ name: 'root' });
|
|
|
|
await run('array', 'UPDATED', {
|
|
moduleToRun: modules.getArray,
|
|
context: {
|
|
user
|
|
}
|
|
});
|
|
});
|
|
|
|
// Update info/hostname when hostname changes
|
|
bus.on('varstate', async (data) => {
|
|
const hostname = data.varstate.node.name;
|
|
// @todo: Create a system user for this
|
|
const user = usersState.findOne({ name: 'root' });
|
|
|
|
if (user) {
|
|
publish('info', 'UPDATED', {
|
|
os: {
|
|
hostname
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// On Docker event update info with { apps: { installed, started } }
|
|
dee.on('*', async (data: { Type: string }) => {
|
|
// Only listen to container events
|
|
if (data.Type !== 'container') {
|
|
return;
|
|
}
|
|
|
|
// @todo: Create a system user for this
|
|
const user = usersState.findOne({ name: 'root' });
|
|
|
|
if (user) {
|
|
const { json } = await modules.getAppCount({
|
|
user
|
|
});
|
|
publish('info', 'UPDATED', {
|
|
apps: json
|
|
});
|
|
}
|
|
});
|
|
|
|
dee.listen();
|
|
|
|
// This needs to be fixed to run from events
|
|
setIntervalAsync(async () => {
|
|
// @todo: Create a system user for this
|
|
const user = usersState.findOne({ name: 'root' });
|
|
|
|
await run('services', 'UPDATED', {
|
|
moduleToRun: modules.getServices,
|
|
context: {
|
|
user
|
|
}
|
|
});
|
|
}, 1000);
|
|
|
|
interface Context {
|
|
user: any;
|
|
websocketId: string;
|
|
};
|
|
|
|
/**
|
|
* Create a pubsub subscription.
|
|
* @param channel The pubsub channel to subscribe to.
|
|
* @param resource The access-control permission resource to check against.
|
|
*/
|
|
const createSubscription = (channel: string, resource?: string) => ({
|
|
subscribe(_: unknown, __: unknown, context: Context) {
|
|
if (!context.user) {
|
|
throw new AppError('<ws> No user found in context.', 500);
|
|
}
|
|
|
|
// Check the user has permissison to subscribe to this endpoint
|
|
ensurePermission(context.user, {
|
|
resource: resource || channel,
|
|
action: 'read',
|
|
possession: 'any'
|
|
});
|
|
|
|
hasSubscribedToChannel(context.websocketId, channel);
|
|
return pubsub.asyncIterator(channel);
|
|
}
|
|
});
|
|
|
|
// Add null to types
|
|
type makeNullUndefinedAndOptional<T> = {
|
|
[K in keyof T]?: T[K] | null | undefined;
|
|
};
|
|
|
|
type Server = makeNullUndefinedAndOptional<CachedServer>;
|
|
|
|
const getServers = async (): Promise<Server[]> => {
|
|
const cachedServers = userCache.get<CachedServers>('mine')?.servers;
|
|
if (cachedServers) {
|
|
return cachedServers;
|
|
}
|
|
|
|
// For now use the my_servers key
|
|
// Later we should return the correct one for the current user with the correct scope, etc.
|
|
const apiKey = apiManager.getValidKeys().find(key => key.name === 'my_servers')?.key.toString()!;
|
|
|
|
// No cached servers found
|
|
if (!cachedServers) {
|
|
// Fetch servers from mothership
|
|
const servers = await getUserServers(apiKey);
|
|
|
|
log.debug('Using upstream for /servers endpoint');
|
|
|
|
// No servers found
|
|
if (!servers || servers.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
// Cache servers
|
|
userCache.set<CachedServers>('mine', {
|
|
servers
|
|
});
|
|
|
|
// Return servers from mothership
|
|
return servers;
|
|
}
|
|
|
|
log.debug('Falling back to local state for /servers endpoint');
|
|
const guid = varState?.data?.regGuid;
|
|
const name = varState?.data?.name;
|
|
const wanip = null;
|
|
const lanip = networkState.data[0].ipaddr[0];
|
|
const localurl = `http://${lanip}:${varState?.data?.port}`;
|
|
const remoteurl = null;
|
|
|
|
return [{
|
|
owner: {
|
|
username: 'root',
|
|
url: '',
|
|
avatar: ''
|
|
},
|
|
guid,
|
|
apikey: apiKey,
|
|
name,
|
|
status: 'online',
|
|
wanip,
|
|
lanip,
|
|
localurl,
|
|
remoteurl
|
|
}];
|
|
};
|
|
|
|
export const resolvers = {
|
|
Query: {
|
|
online: () => true,
|
|
info: () => ({}),
|
|
vms: () => ({}),
|
|
async server(_: unknown, { name }, context: Context) {
|
|
ensurePermission(context.user, {
|
|
resource: 'servers',
|
|
action: 'read',
|
|
possession: 'any'
|
|
});
|
|
|
|
// Single server
|
|
return getServers().then(server => server.find(server => server.name === name));
|
|
},
|
|
servers(_: unknown, __: unknown, context: Context) {
|
|
ensurePermission(context.user, {
|
|
resource: 'servers',
|
|
action: 'read',
|
|
possession: 'any'
|
|
});
|
|
|
|
// All servers
|
|
return getServers();
|
|
}
|
|
},
|
|
Subscription: {
|
|
apikeys: {
|
|
// Not sure how we're going to secure this
|
|
// ...createSubscription('apikeys')
|
|
},
|
|
array: {
|
|
...createSubscription('array')
|
|
},
|
|
// devices: {
|
|
// ...createSubscription('device')
|
|
// },
|
|
dockerContainers: {
|
|
...createSubscription('docker/container')
|
|
},
|
|
dockerNetworks: {
|
|
...createSubscription('docker/network')
|
|
},
|
|
info: {
|
|
...createSubscription('info')
|
|
},
|
|
ping: {
|
|
// subscribe: (_, __, context) => {
|
|
// // startPing();
|
|
// hasSubscribedToChannel(context.websocketId, 'ping');
|
|
// return pubsub.asyncIterator('ping');
|
|
// }
|
|
},
|
|
services: {
|
|
...createSubscription('services')
|
|
},
|
|
servers: {
|
|
...createSubscription('servers')
|
|
},
|
|
shares: {
|
|
...createSubscription('shares')
|
|
},
|
|
unassignedDevices: {
|
|
...createSubscription('devices/unassigned')
|
|
},
|
|
users: {
|
|
...createSubscription('users')
|
|
},
|
|
vars: {
|
|
...createSubscription('vars')
|
|
},
|
|
vms: {
|
|
...createSubscription('vms/domains')
|
|
},
|
|
pluginModule: {
|
|
subscribe: async (_: unknown, directiveArgs, context: Context) => {
|
|
const {plugin: pluginName, module: pluginModuleName} = directiveArgs;
|
|
const channel = `${pluginName}/${pluginModuleName}`;
|
|
|
|
// Verify plugin is installed and active
|
|
if (!pluginManager.isInstalled(pluginName, pluginModuleName)) {
|
|
throw new PluginError('Plugin not installed.', 500);
|
|
}
|
|
|
|
if (!pluginManager.isActive(pluginName, pluginModuleName)) {
|
|
throw new PluginError('Plugin disabled.', 500);
|
|
}
|
|
|
|
// It's up to the plugin to publish new data as needed
|
|
// so we'll just return the Iterator
|
|
hasSubscribedToChannel(context.websocketId, channel);
|
|
return pubsub.asyncIterator(channel);
|
|
}
|
|
},
|
|
online: {
|
|
...createSubscription('online')
|
|
}
|
|
},
|
|
JSON: GraphQLJSON,
|
|
Long: GraphQLLong,
|
|
UUID: GraphQLUUID,
|
|
UserAccount: {
|
|
__resolveType(obj) {
|
|
// Only a user has a password field, the current user aka "me" doesn't.
|
|
if (obj.password) {
|
|
return 'User';
|
|
}
|
|
|
|
return 'Me';
|
|
}
|
|
}
|
|
}; |