mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 07:04:53 +09:00
Add imperfect dependency validation
This commit is contained in:
4 files changed
+139
-16
No files matched your search
@@ -95,6 +95,11 @@ export interface FieldDefinition<
|
||||
* Should throw an error if the value in invalid / inconsistent
|
||||
*/
|
||||
validator?: (value: Type, config: Config<Fields>) => void;
|
||||
/**
|
||||
* Names the config fields that the validator depends on.
|
||||
* This will ensure it is checked when these fields are changed.
|
||||
*/
|
||||
dependsOn?: string[];
|
||||
/**
|
||||
* Value to use for the autocomplete attribute in the Web UI's input element.
|
||||
* Useful for making browsers behave properly with credential inputs.
|
||||
@@ -265,6 +270,46 @@ export class Config<
|
||||
}
|
||||
}
|
||||
|
||||
// TODO add stage, stageProp, revertStaging, and commitStaging
|
||||
/** Staging is invalided when _set is called, so a new one is made with the new state */
|
||||
private _staging?: this;
|
||||
get staging(): typeof this {
|
||||
if (this._staging) { return this._staging; }
|
||||
this._staging = new this.constructor(this.location, this.fields) as this;
|
||||
return this._staging;
|
||||
}
|
||||
|
||||
private static _fieldDependents: Map<string, {
|
||||
name: string,
|
||||
def: FieldDefinition & { validator: NonNullable<FieldDefinition["validator"]> }
|
||||
}[]>;
|
||||
|
||||
/** Map of all fields whose validators depend on the given field */
|
||||
static get fieldDependents(): typeof this._fieldDependents {
|
||||
if (this._fieldDependents) {
|
||||
return this._fieldDependents;
|
||||
}
|
||||
|
||||
const dependents = new Map<string, { name: string, def: FieldDefinition }[]>();
|
||||
|
||||
for (const [name, def] of Object.entries(this.fieldDefinitions)) {
|
||||
if (!def.validator || !def.dependsOn) { continue; }
|
||||
|
||||
for (const otherName of def.dependsOn) {
|
||||
const fieldDependents = dependents.get(otherName);
|
||||
if (fieldDependents) {
|
||||
fieldDependents.push({name, def});
|
||||
} else {
|
||||
dependents.set(otherName, [{name, def}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The type is correct because of "!def.validator", not clear why this doesn't infer
|
||||
this._fieldDependents = dependents as any;
|
||||
return this._fieldDependents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access for a field
|
||||
*
|
||||
@@ -309,6 +354,7 @@ export class Config<
|
||||
* @param notify - False to skip setting dirty flags and emitting events
|
||||
*/
|
||||
_set(name: keyof Fields & string, value: FieldValue, notify: boolean = true) {
|
||||
this._staging = undefined;
|
||||
const prev = this.fields[name];
|
||||
this.fields[name] = value as any;
|
||||
if (notify && !isDeepStrictEqual(value, prev)) {
|
||||
@@ -589,6 +635,18 @@ export class Config<
|
||||
}
|
||||
}
|
||||
|
||||
const dependents = this.constructor.fieldDependents.get(name);
|
||||
if (dependents) {
|
||||
// this.staging.stage(name, value, remote); // TODO need stage to avoid self reference
|
||||
for (const dependent of dependents) {
|
||||
try {
|
||||
dependent.def.validator(this.get(dependent.name as keyof Fields & string, remote), this.staging);
|
||||
} catch (err: any) {
|
||||
throw new InvalidValue(`Failed validation of dependent ${dependent.name}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._set(name, value);
|
||||
}
|
||||
|
||||
@@ -615,6 +673,7 @@ export class Config<
|
||||
if (!def) {
|
||||
throw new InvalidField(`No field named '${name}'`);
|
||||
}
|
||||
|
||||
this._checkAccess(name, def, remote, ConfigAccess.write, true);
|
||||
if (remote !== this.location) {
|
||||
this._checkAccess(name, def, this.location, ConfigAccess.write, true);
|
||||
@@ -633,6 +692,26 @@ export class Config<
|
||||
delete updated[prop];
|
||||
}
|
||||
|
||||
if (def.validator) {
|
||||
try {
|
||||
def.validator(updated, this);
|
||||
} catch (err: any) {
|
||||
throw new InvalidValue(`Failed validation of ${name}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const dependents = this.constructor.fieldDependents.get(name);
|
||||
if (dependents) {
|
||||
// this.staging.setProp(name, prop, value, remote); // TODO need stageProp to avoid self reference
|
||||
for (const dependent of dependents) {
|
||||
try {
|
||||
dependent.def.validator(this.get(dependent.name as keyof Fields & string, remote), this.staging);
|
||||
} catch (err: any) {
|
||||
throw new InvalidValue(`Failed validation of dependent ${dependent.name}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._set(name, updated);
|
||||
}
|
||||
}
|
||||
@@ -572,6 +572,7 @@ export class InstanceConfig extends classes.Config<InstanceConfigFields> {
|
||||
"autosave pool with this many slots. Requires autosaves to be enabled to work. Set to 0 to disable.",
|
||||
type: "number",
|
||||
initialValue: 5,
|
||||
dependsOn: ["factorio.settings", "factorio.settings.autosave_slots"],
|
||||
validator: function(value, config) {
|
||||
const factorioSettings = config.get("factorio.settings");
|
||||
const autosaveSlots = factorioSettings.autosave_slots;
|
||||
|
||||
@@ -161,7 +161,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
let value = form.getFieldValue(field);
|
||||
let request;
|
||||
if (propsMap.has(field)) {
|
||||
let [fieldName, prop] = propsMap.get(field);
|
||||
let [fieldName, prop] = propsMap.get(field)!;
|
||||
request = props.setProp(fieldName, prop, value);
|
||||
|
||||
} else {
|
||||
@@ -254,7 +254,7 @@ function computeTreeData(
|
||||
};
|
||||
|
||||
let treeData = [];
|
||||
let propsMap = new Map();
|
||||
const propsMap = new Map<string, [fieldName: string, prop: string]>();
|
||||
const groups = new Map<string, { [name: string]: lib.FieldDefinition }>();
|
||||
for (let [fieldName, def] of Object.entries(ConfigClass.fieldDefinitions)) {
|
||||
const [groupName, field] = lib.splitOn(".", fieldName);
|
||||
@@ -312,17 +312,40 @@ function computeTreeData(
|
||||
{`"${prop}"`}
|
||||
{restart && restartTip}
|
||||
</>}
|
||||
rules={[{ validator: (_, fieldValue) => {
|
||||
dependencies={def.dependsOn}
|
||||
rules={[({getFieldValue}) => ({ validator: async (_, fieldValue) => {
|
||||
if (!fieldValue.length) {
|
||||
return Promise.reject(new Error("Will be removed"));
|
||||
throw new Error("Will be removed");
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(fieldValue);
|
||||
fieldValue = JSON.parse(fieldValue);
|
||||
} catch (err) {
|
||||
return Promise.reject(new Error("Must be valid json"));
|
||||
throw new Error("Must be valid json");
|
||||
}
|
||||
return Promise.resolve();
|
||||
}}]}
|
||||
|
||||
if (!def.validator) { return; }
|
||||
|
||||
// TODO this needs to use stage and stageProp to avoid deadlocks
|
||||
const staging = config.staging;
|
||||
staging.setProp(fieldName, prop, fieldValue);
|
||||
|
||||
for (const dependsOn of def.dependsOn ?? []) {
|
||||
const dependsOnDef = ConfigClass.fieldDefinitions[dependsOn as any] as typeof def;
|
||||
try {
|
||||
if (propsMap.has(dependsOn)) {
|
||||
const [name, otherProp] = propsMap.get(dependsOn)!;
|
||||
staging.setProp(name, otherProp, JSON.parse(getFieldValue(dependsOn)));
|
||||
} else if (dependsOnDef.type !== "object") {
|
||||
staging.set(dependsOn, getFieldValue(dependsOn));
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to fetch field: ${dependsOn}`);
|
||||
}
|
||||
}
|
||||
|
||||
def.validator(fieldValue, staging);
|
||||
}})]}
|
||||
>
|
||||
<Input className="json-input" disabled={!canWrite} />
|
||||
</Form.Item>,
|
||||
@@ -390,14 +413,33 @@ function computeTreeData(
|
||||
validateStatus={errorFields.has(fieldName) ? "error" : undefined}
|
||||
help={errorFields.get(fieldName)}
|
||||
tooltip={def.description}
|
||||
dependencies={def.dependsOn}
|
||||
valuePropName={def.type === "boolean" ? "checked" : "value"}
|
||||
rules={[{
|
||||
rules={[({getFieldValue}) => ({
|
||||
validator: async (_, fieldValue) => {
|
||||
if (def.extendedValidation) {
|
||||
def.extendedValidation(fieldValue, config);
|
||||
if (!def.validator) { return; }
|
||||
|
||||
// TODO this needs to use stage and stageProp to avoid deadlocks
|
||||
const staging = config.staging;
|
||||
staging.set(fieldName, fieldValue);
|
||||
|
||||
for (const dependsOn of def.dependsOn ?? []) {
|
||||
const dependsOnDef = ConfigClass.fieldDefinitions[dependsOn as any] as typeof def;
|
||||
try {
|
||||
if (propsMap.has(dependsOn)) {
|
||||
const [name, prop] = propsMap.get(dependsOn)!;
|
||||
staging.setProp(name, prop, JSON.parse(getFieldValue(dependsOn)));
|
||||
} else if (dependsOnDef.type !== "object") {
|
||||
staging.set(dependsOn, getFieldValue(dependsOn));
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to fetch field: ${dependsOn}`);
|
||||
}
|
||||
}
|
||||
|
||||
def.validator(fieldValue, staging);
|
||||
},
|
||||
}]}
|
||||
})]}
|
||||
>
|
||||
{renderInput(control.inputComponents, def, !canWrite)}
|
||||
</Form.Item>;
|
||||
|
||||
@@ -90,7 +90,6 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
for (let entry of this.players.values()) {
|
||||
if (entry.playerCode === playerCode && entry.expiresMs > Date.now()) {
|
||||
let verifyCode = await generateCode(this.controller.config.get("player_auth.code_length"));
|
||||
let secret = Buffer.from(this.controller.config.get("controller.auth_secret"), "base64");
|
||||
let verifyToken = jwt.sign(
|
||||
{
|
||||
aud: "player_auth.verify_code",
|
||||
@@ -98,7 +97,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
verify_code: verifyCode,
|
||||
player_code: playerCode,
|
||||
},
|
||||
secret
|
||||
this.controller.authSecret
|
||||
);
|
||||
|
||||
res.send({ verify_code: verifyCode, verify_token: verifyToken });
|
||||
@@ -133,9 +132,11 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
let secret = Buffer.from(this.controller.config.get("controller.auth_secret"), "base64");
|
||||
try {
|
||||
let payload = jwt.verify(verifyToken, secret, { audience: "player_auth.verify_code" }) as jwt.JwtPayload;
|
||||
let payload = jwt.verify(
|
||||
verifyToken, this.controller.authSecret, { audience: "player_auth.verify_code" }
|
||||
) as jwt.JwtPayload;
|
||||
|
||||
if (payload.verify_code !== verifyCode) {
|
||||
throw new Error("invalid verify_code");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user