diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index f986423c..3d3e7cb3 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -130,8 +130,16 @@ async function handleBootstrapCommand( controllerConfig: lib.ControllerConfig ): Promise { let subCommand = args._[1]; - let userManager = new UserManager(controllerConfig); - await userManager.load(path.join(controllerConfig.get("controller.database_directory"), "users.json")); + + const databaseDirectory = controllerConfig.get("controller.database_directory"); + const roles = new lib.SubscribableDatastore(...await new lib.JsonIdDatastoreProvider( + path.join(databaseDirectory, "roles.json"), + lib.Role.fromJSON.bind(lib.Role), + ).bootstrap()); + + const userManager = new UserManager(controllerConfig, roles); + await userManager.load(path.join(databaseDirectory, "users.json")); + if (subCommand === "create-admin") { if (!args.name) { logger.error("name cannot be blank"); @@ -144,7 +152,7 @@ async function handleBootstrapCommand( admin = userManager.createUser(args.name); } - let adminRole = lib.ensureDefaultAdminRole(userManager.roles); + let adminRole = lib.ensureDefaultAdminRole(roles); admin.roleIds.add(adminRole.id); admin.isAdmin = true; await userManager.save(path.join(controllerConfig.get("controller.database_directory"), "users.json")); diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index 4ce638a8..e26855c5 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -671,22 +671,21 @@ export default class ControlConnection extends BaseConnection { } async handleRoleListRequest() { - return [...this._controller.userManager.roles.values()]; + return [...this._controller.roles.values()]; } async handleRoleCreateRequest(request: lib.RoleCreateRequest) { - let lastId = Math.max.apply(null, [...this._controller.userManager.roles.keys()]); + let lastId = Math.max.apply(null, [...this._controller.roles.keys()]); // Start at 5 to leave space for future default roles let id = Math.max(5, lastId+1); - this._controller.userManager.roles.set(id, lib.Role.fromJSON({ id, ...request })); - this._controller.userManager.dirty = true; + this._controller.roles.set(lib.Role.fromJSON({ id, ...request })); return id; } async handleRoleUpdateRequest(request: lib.RoleUpdateRequest) { let { id, name, description, permissions } = request; - let role = this._controller.userManager.roles.get(id); + let role = this._controller.roles.getMutable(id); if (!role) { throw new lib.RequestError(`Role with ID ${id} does not exist`); } @@ -694,27 +693,29 @@ export default class ControlConnection extends BaseConnection { role.name = name; role.description = description; role.permissions = new Set(permissions); - this._controller.rolePermissionsUpdated(role); + this._controller.roles.set(role); } async handleRoleGrantDefaultPermissionsRequest(request: lib.RoleGrantDefaultPermissionsRequest) { - let role = this._controller.userManager.roles.get(request.id); + let role = this._controller.roles.get(request.id); if (!role) { throw new lib.RequestError(`Role with ID ${request.id} does not exist`); } role.grantDefaultPermissions(); - this._controller.rolePermissionsUpdated(role); + this._controller.roles.set(role); } async handleRoleDeleteRequest(request: lib.RoleDeleteRequest) { let id = request.id; - if (!this._controller.userManager.roles.delete(id)) { + const role = this._controller.roles.get(id); + if (!role) { throw new lib.RequestError(`Role with ID ${id} does not exist`); } - this._controller.userManager.dirty = true; + this._controller.roles.delete(role); + this._controller.userManager.dirty = true; for (let user of this._controller.userManager.users.values()) { if (user.roleIds.delete(id)) { this._controller.userPermissionsUpdated(user); @@ -766,7 +767,7 @@ export default class ControlConnection extends BaseConnection { } for (let roleId of request.roles) { - if (!this._controller.userManager.roles.has(roleId)) { + if (!this._controller.roles.has(roleId)) { throw new lib.RequestError(`Role with ID ${roleId} does not exist`); } } diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index 64478751..024b0c51 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -138,17 +138,28 @@ export default class Controller { lib.ModPack.fromJSON.bind(lib.ModPack) ).bootstrap()); - if (modPacks.size === 0) { - modPacks.setMany(lib.ModPack.defaultModPacks); - } + const roles = new lib.SubscribableDatastore(...await new lib.JsonIdDatastoreProvider( + path.join(databaseDirectory, "roles.json"), + lib.Role.fromJSON.bind(lib.Role), + this.migrateRoles, + ).bootstrap()); - const userManager = new UserManager(config); + const userManager = new UserManager(config, roles); await userManager.load(path.join(databaseDirectory, "users.json")); let modsDirectory = config.get("controller.mods_directory"); await fs.ensureDir(modsDirectory); const modStore = await lib.ModStore.fromDirectory(modsDirectory); + // Add default mod packs + if (modPacks.size === 0) { + modPacks.setMany(lib.ModPack.defaultModPacks); + } + + // Add default roles + lib.ensureDefaultAdminRole(roles); + lib.ensureDefaultPlayerRole(roles); + return [ systems, hosts, @@ -156,6 +167,7 @@ export default class Controller { saves, modPacks, modStore, + roles, userManager, ] as const; } @@ -186,8 +198,10 @@ export default class Controller { public modPacks = new lib.SubscribableDatastore(), /** Mods stored on the controller */ public modStore = new lib.ModStore(config.get("controller.mods_directory"), new Map()), + /** Mapping of mod pack id to mod pack */ + public roles = new lib.SubscribableDatastore(), /** User and roles manager for the cluster */ - public userManager = new UserManager(config), + public userManager = new UserManager(config, roles), ) { this.clusterLogger = clusterLogger; this.pluginInfos = pluginInfos; @@ -214,6 +228,7 @@ export default class Controller { this.subscriptions.handle(lib.ModPackUpdatesEvent, this.handleModPackSubscription.bind(this)); this.subscriptions.handle(lib.ModUpdatesEvent, this.handleModSubscription.bind(this)); this.subscriptions.handle(lib.UserUpdatesEvent, this.handleUserSubscription.bind(this)); + this.subscriptions.handle(lib.RoleUpdatesEvent, this.handleRoleSubscription.bind(this)); // Handle updates for datastores this.systems.on("update", this.systemsUpdated.bind(this)); @@ -221,6 +236,7 @@ export default class Controller { this.instances.on("update", this.instanceDetailsUpdated.bind(this)); this.saves.on("update", this.savesUpdated.bind(this)); this.modPacks.on("update", this.modPacksUpdated.bind(this)); + this.roles.on("update", this.rolesUpdated.bind(this)); } async start(args: ControllerArgs) { @@ -597,6 +613,7 @@ export default class Controller { this.instances.save(), this.saves.save(), this.modPacks.save(), + this.roles.save(), ]); if (this.userManager.dirty) { @@ -657,6 +674,20 @@ export default class Controller { return instance; } + static migrateRoles(rawJson: unknown[]): Static[] { + const serialized = rawJson as Static[]; + return serialized.map(json => { + json.permissions = json.permissions.map(permission => { + // migrate: core.instance.save.list.subscribe was renamed in alpha 17 + if (permission === "core.instance.save.list.subscribe") { + return "core.instance.save.subscribe"; + } + return permission; + }); + return json; + }); + } + static async loadJsonObject(filePath: string, throwOnMissing: boolean = false): Promise { let manifest = {}; try { @@ -1060,27 +1091,34 @@ export default class Controller { } } - /** - * Notify connected control clients with the given role that the - * permissions may have changed. - * @param role - Role permisions updated for. - */ - rolePermissionsUpdated(role: lib.Role) { - for (let controlConnection of this.wsServer.controlConnections.values()) { - if (controlConnection.user.roles.has(role)) { - controlConnection.send( - new lib.AccountUpdateEvent( - [...controlConnection.user.roles].map(r => ({ - name: r.name, - id: r.id, - permissions: [...r.permissions], - })) - ) - ); + rolesUpdated(roles: lib.Role[]) { + this.subscriptions.broadcast(new lib.RoleUpdatesEvent(roles)); + // lib.invokeHook(this.plugins, "onRolesUpdated", roles); // This doesn't exist at the moment + // Notify connected control clients with the given role that the permissions may have changed. + for (const role of roles) { + for (let controlConnection of this.wsServer.controlConnections.values()) { + if (controlConnection.user.roles.has(role)) { + controlConnection.send( + new lib.AccountUpdateEvent( + [...controlConnection.user.roles].map(r => ({ + name: r.name, + id: r.id, + permissions: [...r.permissions], + })) + ) + ); + } } } } + async handleRoleSubscription(request: lib.SubscriptionRequest) { + const roles = [...this.roles.values()].filter( + role => role.updatedAtMs > request.lastRequestTimeMs, + ); + return roles.length ? new lib.RoleUpdatesEvent(roles) : null; + } + async loadPlugins() { for (let pluginInfo of this.pluginInfos) { try { diff --git a/packages/controller/src/ControllerUser.ts b/packages/controller/src/ControllerUser.ts index b8e83d9f..3673ae95 100644 --- a/packages/controller/src/ControllerUser.ts +++ b/packages/controller/src/ControllerUser.ts @@ -1,10 +1,13 @@ import { Static, Type } from "@sinclair/typebox"; -import { IControllerUser, PermissionError, PlayerStats, Role, User, permissions } from "@clusterio/lib"; -import type UserManager from "./UserManager"; + +import { + IControllerUser, PermissionError, PlayerStats, + Role, SubscribableDatastore, User, permissions, +} from "@clusterio/lib"; export default class ControllerUser extends User implements IControllerUser { constructor( - public userManager: UserManager, + private _controllerRoles: SubscribableDatastore, /** Unix time in seconds the user token must be issued after to be valid. */ public tokenValidAfter = 0, ...args: ConstructorParameters @@ -17,7 +20,7 @@ export default class ControllerUser extends User implements IControllerUser { token_valid_after: Type.Optional(Type.Number()), }); - static fromJSON(json: Static, userManager: UserManager) { + static fromJSON(json: Static, controllerRoles: SubscribableDatastore) { const roleIds = new Set(json.roles ?? []); const instanceStats = new Map( (json.instance_stats ?? []).map( @@ -26,7 +29,7 @@ export default class ControllerUser extends User implements IControllerUser { ); const playerStats = User._calculatePlayerStats(instanceStats); return new this( - userManager, + controllerRoles, json.token_valid_after, json.name, roleIds, @@ -57,7 +60,7 @@ export default class ControllerUser extends User implements IControllerUser { get roles(): ReadonlySet> { return new Set([...this.roleIds].map( - id => this.userManager.roles.get(id) + id => this._controllerRoles.get(id) ).filter( (r): r is Role => Boolean(r) )); @@ -79,7 +82,7 @@ export default class ControllerUser extends User implements IControllerUser { } for (let roleId of this.roleIds) { - let role = this.userManager.roles.get(roleId); + let role = this._controllerRoles.get(roleId); if (!role) { continue; } diff --git a/packages/controller/src/UserManager.ts b/packages/controller/src/UserManager.ts index 9d05b538..6c88a1d6 100644 --- a/packages/controller/src/UserManager.ts +++ b/packages/controller/src/UserManager.ts @@ -10,7 +10,6 @@ import ControllerUser from "./ControllerUser"; * @alias module:controller/src/UserManager */ export default class UserManager { - roles: Map = new Map(); users: Map = new Map(); dirty = false; @@ -20,7 +19,8 @@ export default class UserManager { onlineUsers = new Set(); constructor( - private _config: lib.ControllerConfig + private _config: lib.ControllerConfig, + private _roles: lib.SubscribableDatastore, ) { } @@ -31,14 +31,15 @@ export default class UserManager { async load(filePath: string): Promise { try { let content = JSON.parse(await fs.readFile(filePath, { encoding: "utf8" })); - for (let serializedRole of content.roles) { - let role = lib.Role.fromJSON(serializedRole); - this.roles.set(role.id, role); + if ("roles" in content) { // Split file format after 2.0.0-alpha.22 + this._roles.setMany(content.roles.map((serializedRole: any) => lib.Role.fromJSON(serializedRole))); + await this._roles.save(); // Force a save to prevent data loss + content = content.users; } let duplicates = 0; - for (let serializedUser of content.users) { - let user = ControllerUser.fromJSON(serializedUser, this); + for (const serializedUser of content) { + const user = ControllerUser.fromJSON(serializedUser, this._roles); const existingUser = this.users.get(user.id); if (existingUser) { // Required migration to all lowercase ids in alpha 19 @@ -65,30 +66,14 @@ export default class UserManager { if (err.code !== "ENOENT") { throw err; } - - // Create default roles if loading failed - lib.ensureDefaultAdminRole(this.roles); - lib.ensureDefaultPlayerRole(this.roles); } } async save(filePath: string): Promise { - let serializedRoles = []; - for (let role of this.roles.values()) { - serializedRoles.push(role.toJSON()); - } - - let serializedUsers = []; - for (let user of this.users.values()) { - serializedUsers.push(user.toJSON(true)); - } - - let serialized = { - users: serializedUsers, - roles: serializedRoles, - }; this.dirty = false; - await lib.safeOutputFile(filePath, JSON.stringify(serialized, null, "\t")); + await lib.safeOutputFile(filePath, + JSON.stringify([...this.users.values()].map(user => user.toJSON(true)), null, "\t") + ); } /** @@ -103,11 +88,11 @@ export default class UserManager { let roles = new Set(); let defaultRoleId = this._config.get("controller.default_role_id"); - if (defaultRoleId !== null && this.roles.has(defaultRoleId)) { + if (defaultRoleId !== null && this._roles.has(defaultRoleId)) { roles.add(defaultRoleId); } - let user = new ControllerUser(this, 0, name, roles); + let user = new ControllerUser(this._roles, 0, name, roles); this.users.set(user.id, user); this.dirty = true; return user; diff --git a/packages/lib/src/data/Role.ts b/packages/lib/src/data/Role.ts index 56f104bc..3ce7c704 100644 --- a/packages/lib/src/data/Role.ts +++ b/packages/lib/src/data/Role.ts @@ -10,6 +10,8 @@ export default class Role { public name: string, public description: string, public permissions: Set, + public updatedAtMs = 0, + public isDeleted = false, ) { } static jsonSchema = Type.Object({ @@ -17,22 +19,18 @@ export default class Role { name: Type.String(), description: Type.String(), permissions: Type.Array(Type.String()), + updated_at_ms: Type.Optional(Type.Number()), + is_deleted: Type.Optional(Type.Boolean()), }); - static remapPermission(permission: string) { - // migrate: core.instance.save.list.subscribe was renamed in alpha 17 - if (permission === "core.instance.save.list.subscribe") { - return "core.instance.save.subscribe"; - } - return permission; - } - static fromJSON(json: Static) { return new this( json.id, json.name, json.description, - new Set(json.permissions.map(this.remapPermission)), + new Set(json.permissions), + json.updated_at_ms, + json.is_deleted, ); } @@ -42,6 +40,8 @@ export default class Role { name: this.name, description: this.description, permissions: [...this.permissions], + updated_at_ms: this.updatedAtMs, + is_deleted: this.isDeleted, }; } diff --git a/packages/lib/src/data/index.ts b/packages/lib/src/data/index.ts index d3f781cf..cacc1211 100644 --- a/packages/lib/src/data/index.ts +++ b/packages/lib/src/data/index.ts @@ -21,5 +21,6 @@ export * from "./messages_host"; export * from "./messages_instance"; export * from "./messages_mod"; export * from "./messages_user"; +export * from "./messages_role"; export * from "./messages_plugin"; export * from "./version"; diff --git a/packages/lib/src/data/messages_role.ts b/packages/lib/src/data/messages_role.ts new file mode 100644 index 00000000..423c1d65 --- /dev/null +++ b/packages/lib/src/data/messages_role.ts @@ -0,0 +1,134 @@ +import { Type, Static } from "@sinclair/typebox"; +import { JsonNumber, jsonArray } from "./composites"; +import Permission from "./Permission"; +import Role from "./Role"; + +export class PermissionListRequest { + declare ["constructor"]: typeof PermissionListRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.permission.list" as const; + static Response = jsonArray(Permission); +} + +export class RoleListRequest { + declare ["constructor"]: typeof RoleListRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.list" as const; + static Response = jsonArray(Role); +} + +export class RoleUpdatesEvent { + declare ["constructor"]: typeof RoleUpdatesEvent; + static type = "event" as const; + static src = "controller" as const; + static dst = "control" as const; + static permission = "core.role.subscribe" as const; + + constructor( + public updates: Role[], + ) { } + + static jsonSchema = Type.Object({ + "updates": Type.Array(Role.jsonSchema), + }); + + static fromJSON(json: Static) { + return new this(json.updates.map(update => Role.fromJSON(update))); + } +} + +export class RoleCreateRequest { + declare ["constructor"]: typeof RoleCreateRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.create" as const; + + constructor( + public name: string, + public description: string, + public permissions: string[], + ) { } + + static jsonSchema = Type.Object({ + name: Type.String(), + description: Type.String(), + permissions: Type.Array(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.name, json.description, json.permissions); + } + + static Response = JsonNumber; +} + +export class RoleUpdateRequest { + declare ["constructor"]: typeof RoleUpdateRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.update" as const; + + constructor( + public id: number, + public name: string, + public description: string, + public permissions: string[], + ) { } + + static jsonSchema = Type.Object({ + id: Type.Integer(), + name: Type.String(), + description: Type.String(), + permissions: Type.Array(Type.String()), + }); + + static fromJSON(json: Static) { + return new this(json.id, json.name, json.description, json.permissions); + } +} + +export class RoleGrantDefaultPermissionsRequest { + declare ["constructor"]: typeof RoleGrantDefaultPermissionsRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.update" as const; + + constructor( + public id: number, + ) { } + + static jsonSchema = Type.Object({ + id: Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.id); + } +} + +export class RoleDeleteRequest { + declare ["constructor"]: typeof RoleDeleteRequest; + static type = "request" as const; + static src = "control" as const; + static dst = "controller" as const; + static permission = "core.role.delete" as const; + + constructor( + public id: number, + ) { } + + static jsonSchema = Type.Object({ + id: Type.Integer(), + }); + + static fromJSON(json: Static) { + return new this(json.id); + } +} diff --git a/packages/lib/src/data/messages_user.ts b/packages/lib/src/data/messages_user.ts index c827b69b..cb35eb6c 100644 --- a/packages/lib/src/data/messages_user.ts +++ b/packages/lib/src/data/messages_user.ts @@ -1,120 +1,8 @@ import { Type, Static } from "@sinclair/typebox"; -import Permission from "./Permission"; -import Role from "./Role"; import User, { IControllerUser } from "./User"; -import { JsonNumber, StringEnum, jsonArray, plainJson } from "./composites"; +import { StringEnum, jsonArray, plainJson } from "./composites"; import { MessageRequest } from "./messages_core"; -export class PermissionListRequest { - declare ["constructor"]: typeof PermissionListRequest; - static type = "request" as const; - static src = "control" as const; - static dst = "controller" as const; - static permission = "core.permission.list" as const; - static Response = jsonArray(Permission); -} - -export class RoleListRequest { - declare ["constructor"]: typeof RoleListRequest; - static type = "request" as const; - static src = "control" as const; - static dst = "controller" as const; - static permission = "core.role.list" as const; - static Response = jsonArray(Role); -} - -export class RoleCreateRequest { - declare ["constructor"]: typeof RoleCreateRequest; - static type = "request" as const; - static src = "control" as const; - static dst = "controller" as const; - static permission = "core.role.create" as const; - - constructor( - public name: string, - public description: string, - public permissions: string[], - ) { } - - static jsonSchema = Type.Object({ - name: Type.String(), - description: Type.String(), - permissions: Type.Array(Type.String()), - }); - - static fromJSON(json: Static) { - return new this(json.name, json.description, json.permissions); - } - - static Response = JsonNumber; -} - -export class RoleUpdateRequest { - declare ["constructor"]: typeof RoleUpdateRequest; - static type = "request" as const; - static src = "control" as const; - static dst = "controller" as const; - static permission = "core.role.update" as const; - - constructor( - public id: number, - public name: string, - public description: string, - public permissions: string[], - ) { } - - static jsonSchema = Type.Object({ - id: Type.Integer(), - name: Type.String(), - description: Type.String(), - permissions: Type.Array(Type.String()), - }); - - static fromJSON(json: Static) { - return new this(json.id, json.name, json.description, json.permissions); - } -} - -export class RoleGrantDefaultPermissionsRequest { - declare ["constructor"]: typeof RoleGrantDefaultPermissionsRequest; - static type = "request" as const; - static src = "control" as const; - static dst = "controller" as const; - static permission = "core.role.update" as const; - - constructor( - public id: number, - ) { } - - static jsonSchema = Type.Object({ - id: Type.Integer(), - }); - - static fromJSON(json: Static) { - return new this(json.id); - } -} - -export class RoleDeleteRequest { - declare ["constructor"]: typeof RoleDeleteRequest; - static type = "request" as const; - static src = "control" as const; - static dst = "controller" as const; - static permission = "core.role.delete" as const; - - constructor( - public id: number, - ) { } - - static jsonSchema = Type.Object({ - id: Type.Integer(), - }); - - static fromJSON(json: Static) { - return new this(json.id); - } -} - export class UserGetRequest { declare ["constructor"]: typeof UserGetRequest; static type = "request" as const; diff --git a/packages/lib/src/datastore.ts b/packages/lib/src/datastore.ts index 1b84e7f8..a305e960 100644 --- a/packages/lib/src/datastore.ts +++ b/packages/lib/src/datastore.ts @@ -30,7 +30,7 @@ export class MemoryDatastoreProvider< V extends DatastoreValue, > extends DatastoreProvider { constructor( - private value: Map = new Map() + private value = new Map() ) { super(); } @@ -39,34 +39,33 @@ export class MemoryDatastoreProvider< this.value = new Map(data); } - async load() { + async load(): Promise> { return new Map(this.value); } } // Store all data within a json object file and validate the result against a schema export class JsonDatastoreProvider< - K extends DatastoreKey, V extends DatastoreValue, J, // Intermediate type returned from migrations -> extends DatastoreProvider { +> extends DatastoreProvider { constructor( private filePath: string, - private fromJson: (json: J) => V = v => v as any, - private migrations: (rawJson: Record) => Record = v => v as any, + private fromJson: (json: J) => V = v => v as any as V, + private migrations: (rawJson: Record) => Record = v => v as Record, private finalise: (obj: V) => V = v => v, ) { super(); } // Save the data to the json file - async save(data: Map) { + async save(data: Map) { logger.verbose(`Saving ${this.filePath}`); await safeOutputFile(this.filePath, JSON.stringify(Object.fromEntries(data), null, "\t")); } // Load the data from the json file - async load() { + async load(): Promise> { // Read the raw json from the file let rawJson; try { @@ -89,27 +88,26 @@ export class JsonDatastoreProvider< // Store all data within a json array file where each element has a unique id export class JsonIdDatastoreProvider< - K extends DatastoreKey, - V extends DatastoreValue & { id: K }, + V extends DatastoreValue & { id: DatastoreKey }, J, // Intermediate type returned from migrations -> extends DatastoreProvider { +> extends DatastoreProvider { constructor( private filePath: string, - private fromJson: (json: J) => V = v => v as any, - private migrations: (rawJson: Array) => Array = v => v as any, + private fromJson: (json: J) => V = v => v as any as V, + private migrations: (rawJson: Array) => Array = v => v as Array, private finalise: (obj: V) => V = v => v, ) { super(); } // Save the data to the json file - async save(data: Map) { + async save(data: Map) { logger.verbose(`Saving ${this.filePath}`); await safeOutputFile(this.filePath, JSON.stringify([...data.values()], null, "\t")); } // Load the data from the json file - async load() { + async load(): Promise> { // Read the raw json from the file let rawJson; try { @@ -211,13 +209,17 @@ export abstract class Datastore< valuesMutable() { return this.data.values(); } + + keys() { + return this.data.keys(); + } } // General key value mapping which can be saved and loaded with the file system export class KeyValueDatastore< K extends DatastoreKey, V extends DatastoreValue, -> extends Datastore { +> extends Datastore { // Set the value in the datastore, be careful of race conditions if you await any functions before calling set set(key: K, value: V) { this.data.set(key, value); @@ -235,8 +237,10 @@ export class KeyValueDatastore< // Delete a value in the datastore delete(key: K) { const value = this.data.get(key); - this.data.delete(key); - this.touch([[key, value!, true]]); + if (value !== undefined) { + this.data.delete(key); + this.touch([[key, value, true]]); + } } // Delete many values at once from an array of keys diff --git a/packages/lib/src/link/messages.ts b/packages/lib/src/link/messages.ts index 5d4977c1..3fd30de6 100644 --- a/packages/lib/src/link/messages.ts +++ b/packages/lib/src/link/messages.ts @@ -4,6 +4,7 @@ import * as host from "../data/messages_host"; import * as instance from "../data/messages_instance"; import * as mod from "../data/messages_mod"; import * as user from "../data/messages_user"; +import * as role from "../data/messages_role"; import * as plugin from "../data/messages_plugin"; import * as subscriptions from "../subscriptions"; import type { RequestClass, EventClass } from "./link"; @@ -107,12 +108,14 @@ export const dataClasses: (RequestClass | EventClass) mod.ModUpdatesEvent, mod.ModPortalDownloadRequest, - user.PermissionListRequest, - user.RoleListRequest, - user.RoleCreateRequest, - user.RoleUpdateRequest, - user.RoleGrantDefaultPermissionsRequest, - user.RoleDeleteRequest, + role.PermissionListRequest, + role.RoleListRequest, + role.RoleUpdatesEvent, + role.RoleCreateRequest, + role.RoleUpdateRequest, + role.RoleGrantDefaultPermissionsRequest, + role.RoleDeleteRequest, + user.UserGetRequest, user.UserListRequest, user.UserCreateRequest, diff --git a/packages/lib/src/permissions.ts b/packages/lib/src/permissions.ts index ba50389b..23ac4b31 100644 --- a/packages/lib/src/permissions.ts +++ b/packages/lib/src/permissions.ts @@ -7,6 +7,7 @@ * @module lib/users */ import { Permission, Role } from "./data"; +import { SubscribableDatastore } from "./datastore"; export const permissions = new Map(); @@ -66,7 +67,7 @@ export function definePermission({ * @param roles - role storage to modify. * @returns the default admin role */ -export function ensureDefaultAdminRole(roles: Map) { +export function ensureDefaultAdminRole(roles: SubscribableDatastore) { let admin = roles.get(0); if (!admin) { admin = Role.fromJSON({ @@ -75,9 +76,9 @@ export function ensureDefaultAdminRole(roles: Map) { description: "Cluster wide administrator.", permissions: [], }); - roles.set(0, admin); } admin.permissions.add("core.admin"); + roles.set(admin); return admin; } @@ -90,7 +91,7 @@ export function ensureDefaultAdminRole(roles: Map) { * * @param roles - role storage to modify. */ -export function ensureDefaultPlayerRole(roles: Map) { +export function ensureDefaultPlayerRole(roles: SubscribableDatastore) { let player = roles.get(1); if (!player) { player = Role.fromJSON({ @@ -99,9 +100,9 @@ export function ensureDefaultPlayerRole(roles: Map) { description: "Default player role.", permissions: [], }); - roles.set(1, player); } player.grantDefaultPermissions(); + roles.set(player); } @@ -431,6 +432,12 @@ definePermission({ description: "Get the full list of roles and their permissions in the cluster.", grantByDefault: true, }); +definePermission({ + name: "core.role.subscribe", + title: "Subscribe to role updates", + description: "Subscribe to be notified on updates on the details of roles.", + grantByDefault: true, +}); definePermission({ name: "core.role.create", title: "Create role", diff --git a/packages/lib/tsconfig.browser.json b/packages/lib/tsconfig.browser.json index 0825dd20..059b1ce6 100644 --- a/packages/lib/tsconfig.browser.json +++ b/packages/lib/tsconfig.browser.json @@ -6,6 +6,7 @@ "./src/api.ts", "./src/config/*.ts", "./src/data/*.ts", + "./src/datastore.ts", "./src/errors.ts", "./src/factorio/exchange_string.ts", "./src/helpers.ts", diff --git a/packages/web_ui/src/components/InputRole.tsx b/packages/web_ui/src/components/InputRole.tsx index 49a9a78f..e8039cda 100644 --- a/packages/web_ui/src/components/InputRole.tsx +++ b/packages/web_ui/src/components/InputRole.tsx @@ -1,21 +1,10 @@ -import React, { useContext, useEffect, useState } from "react"; import { Select } from "antd"; -import * as lib from "@clusterio/lib"; - -import ControlContext from "./ControlContext"; -import { notifyErrorHandler } from "../util/notify"; import { InputComponentProps } from "../BaseWebPlugin"; +import { useRoles } from "../model/roles"; export default function InputRole(props: InputComponentProps) { - let control = useContext(ControlContext); - let [roles, setRoles] = useState([]); - - useEffect(() => { - control.send(new lib.RoleListRequest()).then(newRoles => { - setRoles(newRoles); - }).catch(notifyErrorHandler("Error fetching role list")); - }, []); + const [roles] = useRoles(); return