Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a191d81c29 | ||
|
|
d4c8fc9db8 | ||
|
|
95ec090f2e | ||
|
|
e3ab82f6df | ||
|
|
5a0bb6864a | ||
|
|
80f05e481b | ||
|
|
6ff5322353 |
@@ -7,7 +7,7 @@
|
|||||||
},
|
},
|
||||||
"server": {
|
"server": {
|
||||||
"file": "server-v1.openapi.json",
|
"file": "server-v1.openapi.json",
|
||||||
"sha256": "19961572fad04f8545a4f607d4bf7276de64a84b266daecbb21d74e0975f97ef"
|
"sha256": "1edfd78405cfe2a50022afe70a4c4f32caa4e98e3b5b54b40841d532058f2fd6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,6 @@
|
|||||||
"map_extent": {
|
"map_extent": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -152,7 +151,6 @@
|
|||||||
"map_extent": {
|
"map_extent": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -234,7 +232,6 @@
|
|||||||
"map_extent": {
|
"map_extent": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -381,7 +378,6 @@
|
|||||||
"request_data": {
|
"request_data": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -574,7 +570,6 @@
|
|||||||
"link_result_list": {
|
"link_result_list": {
|
||||||
"description": "管道模拟结果列表",
|
"description": "管道模拟结果列表",
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Link Result List",
|
"title": "Link Result List",
|
||||||
@@ -583,7 +578,6 @@
|
|||||||
"node_result_list": {
|
"node_result_list": {
|
||||||
"description": "节点模拟结果列表",
|
"description": "节点模拟结果列表",
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Node Result List",
|
"title": "Node Result List",
|
||||||
@@ -608,7 +602,6 @@
|
|||||||
"iforest_params": {
|
"iforest_params": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -635,7 +628,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
@@ -775,7 +767,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
@@ -802,7 +793,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
@@ -855,7 +845,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
@@ -877,7 +866,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
@@ -1316,7 +1304,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
@@ -1911,7 +1898,6 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"items": {
|
"items": {
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Items",
|
"title": "Items",
|
||||||
@@ -1986,7 +1972,6 @@
|
|||||||
"title": "Duration"
|
"title": "Duration"
|
||||||
},
|
},
|
||||||
"pump_control": {
|
"pump_control": {
|
||||||
"additionalProperties": true,
|
|
||||||
"description": "泵控制策略",
|
"description": "泵控制策略",
|
||||||
"title": "Pump Control",
|
"title": "Pump Control",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -2005,7 +1990,6 @@
|
|||||||
"tank_init_level": {
|
"tank_init_level": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2037,7 +2021,6 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"items": {
|
"items": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Errors",
|
"title": "Errors",
|
||||||
@@ -2324,7 +2307,6 @@
|
|||||||
"map_extent": {
|
"map_extent": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4701,7 +4683,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Patch Admin Projects Project Id Model Imports",
|
"title": "Response Patch Admin Projects Project Id Model Imports",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -4809,7 +4790,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Post Admin Projects Project Id Model Imports",
|
"title": "Response Post Admin Projects Project Id Model Imports",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -6618,7 +6598,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Audit Logs Count",
|
"title": "Response Get Audit Logs Count",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -6890,7 +6869,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Backdrops Properties",
|
"title": "Response Get Backdrops Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -6987,7 +6965,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -7273,7 +7250,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Post Burst Detections",
|
"title": "Response Post Burst Detections",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -7384,7 +7360,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Post Burst Locations",
|
"title": "Response Post Burst Locations",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -7655,7 +7630,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Controls Properties",
|
"title": "Response Get Controls Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -7752,7 +7726,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -8092,7 +8065,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -8324,7 +8296,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Curves Properties",
|
"title": "Response Get Curves Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -8432,7 +8403,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -8554,7 +8524,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Demands Properties",
|
"title": "Response Get Demands Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -8662,7 +8631,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -8765,7 +8733,7 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"description": "总需水量(m³/h)",
|
"description": "总需水量(m³/h)",
|
||||||
"exclusiveMinimum": 0,
|
"exclusiveMinimum": 0.0,
|
||||||
"title": "Demand",
|
"title": "Demand",
|
||||||
"type": "number"
|
"type": "number"
|
||||||
}
|
}
|
||||||
@@ -8886,7 +8854,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -9000,7 +8967,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -9126,7 +9092,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Element Properties",
|
"title": "Response Get Element Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -9248,7 +9213,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Element Properties With Types",
|
"title": "Response Get Element Properties With Types",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -9579,7 +9543,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Emitters Properties",
|
"title": "Response Get Emitters Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -9687,7 +9650,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -9797,7 +9759,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -10168,7 +10129,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Post Geocoding Requests",
|
"title": "Response Post Geocoding Requests",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -11676,7 +11636,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Junctions Properties",
|
"title": "Response Get Junctions Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -11784,7 +11743,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -12350,7 +12308,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -12451,7 +12408,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -12584,7 +12540,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Labels Properties",
|
"title": "Response Get Labels Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -12681,7 +12636,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -12803,7 +12757,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Post Leakage Identifications",
|
"title": "Response Post Leakage Identifications",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -12914,7 +12867,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Link Properties",
|
"title": "Response Get Link Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -13692,7 +13644,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -13793,7 +13744,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -13901,7 +13851,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -14023,7 +13972,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Mixing Configurations Detail",
|
"title": "Response Get Mixing Configurations Detail",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -14167,7 +14115,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Network In Extents",
|
"title": "Response Get Network In Extents",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -14376,7 +14323,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Network Options",
|
"title": "Response Get Network Options",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -14473,7 +14419,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -14584,7 +14529,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Network Options Energy",
|
"title": "Response Get Network Options Energy",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -14695,7 +14639,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Network Options Pump Energy",
|
"title": "Response Get Network Options Pump Energy",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -14803,7 +14746,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -14914,7 +14856,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Network Options Time",
|
"title": "Response Get Network Options Time",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -15015,7 +14956,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Backdrop",
|
"title": "Response Get Network Schemas Backdrop",
|
||||||
@@ -15118,7 +15058,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Control",
|
"title": "Response Get Network Schemas Control",
|
||||||
@@ -15221,7 +15160,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Curve",
|
"title": "Response Get Network Schemas Curve",
|
||||||
@@ -15324,7 +15262,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Demand",
|
"title": "Response Get Network Schemas Demand",
|
||||||
@@ -15427,7 +15364,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Emitter",
|
"title": "Response Get Network Schemas Emitter",
|
||||||
@@ -15530,7 +15466,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Energy",
|
"title": "Response Get Network Schemas Energy",
|
||||||
@@ -15633,7 +15568,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Junction",
|
"title": "Response Get Network Schemas Junction",
|
||||||
@@ -15736,7 +15670,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Label",
|
"title": "Response Get Network Schemas Label",
|
||||||
@@ -15839,7 +15772,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Mixing",
|
"title": "Response Get Network Schemas Mixing",
|
||||||
@@ -15942,7 +15874,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Option",
|
"title": "Response Get Network Schemas Option",
|
||||||
@@ -16045,7 +15976,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Pattern",
|
"title": "Response Get Network Schemas Pattern",
|
||||||
@@ -16148,7 +16078,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Pipe",
|
"title": "Response Get Network Schemas Pipe",
|
||||||
@@ -16251,7 +16180,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Pipe Reaction",
|
"title": "Response Get Network Schemas Pipe Reaction",
|
||||||
@@ -16354,7 +16282,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Pump",
|
"title": "Response Get Network Schemas Pump",
|
||||||
@@ -16457,7 +16384,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Pump Energy",
|
"title": "Response Get Network Schemas Pump Energy",
|
||||||
@@ -16560,7 +16486,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Quality",
|
"title": "Response Get Network Schemas Quality",
|
||||||
@@ -16663,7 +16588,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Reaction",
|
"title": "Response Get Network Schemas Reaction",
|
||||||
@@ -16765,7 +16689,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Region",
|
"title": "Response Get Network Schemas Region",
|
||||||
@@ -16868,7 +16791,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Reservoir",
|
"title": "Response Get Network Schemas Reservoir",
|
||||||
@@ -16970,7 +16892,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Scada Device",
|
"title": "Response Get Network Schemas Scada Device",
|
||||||
@@ -17073,7 +16994,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Source",
|
"title": "Response Get Network Schemas Source",
|
||||||
@@ -17176,7 +17096,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Tag",
|
"title": "Response Get Network Schemas Tag",
|
||||||
@@ -17279,7 +17198,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Tank",
|
"title": "Response Get Network Schemas Tank",
|
||||||
@@ -17382,7 +17300,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Tank Reaction",
|
"title": "Response Get Network Schemas Tank Reaction",
|
||||||
@@ -17485,7 +17402,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Time",
|
"title": "Response Get Network Schemas Time",
|
||||||
@@ -17588,7 +17504,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Valve",
|
"title": "Response Get Network Schemas Valve",
|
||||||
@@ -17691,7 +17606,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Network Schemas Vertex",
|
"title": "Response Get Network Schemas Vertex",
|
||||||
@@ -18056,7 +17970,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Node Properties",
|
"title": "Response Get Node Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -18826,7 +18739,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -19058,7 +18970,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Patterns Properties",
|
"title": "Response Get Patterns Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -19166,7 +19077,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -19276,7 +19186,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -19398,7 +19307,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Pipe Reactions Detail",
|
"title": "Response Get Pipe Reactions Detail",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -21314,7 +21222,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Pipes Properties",
|
"title": "Response Get Pipes Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -21422,7 +21329,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -22113,7 +22019,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Post Project Return Dict Runs",
|
"title": "Response Post Project Return Dict Runs",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -23779,7 +23684,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Pumps Properties",
|
"title": "Response Get Pumps Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -23887,7 +23791,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -24009,7 +23912,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Quality Configurations Properties",
|
"title": "Response Get Quality Configurations Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -24106,7 +24008,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -24216,7 +24117,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -24327,7 +24227,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Reactions Detail",
|
"title": "Response Get Reactions Detail",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -24425,7 +24324,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -24643,7 +24541,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -24750,7 +24647,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -24871,7 +24767,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Regions Detail",
|
"title": "Response Get Regions Detail",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -26303,7 +26198,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Reservoirs Properties",
|
"title": "Response Get Reservoirs Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -26411,7 +26305,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -26998,7 +26891,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Rule Properties",
|
"title": "Response Get Rule Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -27095,7 +26987,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -27207,7 +27098,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Rule Schemas",
|
"title": "Response Get Rule Schemas",
|
||||||
@@ -27546,7 +27436,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Scada Properties",
|
"title": "Response Get Scada Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -28530,7 +28419,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -28638,7 +28526,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -28760,7 +28647,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Sources Detail",
|
"title": "Response Get Sources Detail",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -28871,7 +28757,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Status",
|
"title": "Response Get Status",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -28981,7 +28866,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -29093,7 +28977,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Status Schemas",
|
"title": "Response Get Status Schemas",
|
||||||
@@ -29313,7 +29196,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -29446,7 +29328,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Tags Detail",
|
"title": "Response Get Tags Detail",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -29545,7 +29426,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -29667,7 +29547,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Tank Reactions Detail",
|
"title": "Response Get Tank Reactions Detail",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -32196,7 +32075,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Tanks Properties",
|
"title": "Response Get Tanks Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -32304,7 +32182,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -33105,7 +32982,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -35760,7 +35636,6 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"additionalProperties": true,
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"title": "Response Get Title Schemas",
|
"title": "Response Get Title Schemas",
|
||||||
@@ -35862,7 +35737,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Titles",
|
"title": "Response Get Titles",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -35959,7 +35833,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -37706,7 +37579,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Valves Properties",
|
"title": "Response Get Valves Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -37814,7 +37686,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -38394,7 +38265,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -38495,7 +38365,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -38738,7 +38607,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Get Visual Elements Properties",
|
"title": "Response Get Visual Elements Properties",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -38835,7 +38703,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Payload",
|
"title": "Payload",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
@@ -38956,7 +38823,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"additionalProperties": true,
|
|
||||||
"title": "Response Post Web Searches",
|
"title": "Response Post Web Searches",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ test("已登录管理员可以打开审计日志页面", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("已认证但未授权的账号会看到明确说明", async ({ page }) => {
|
test("已认证但未授权的账号会看到明确说明", async ({ page }) => {
|
||||||
|
await page.addInitScript(() => window.localStorage.clear());
|
||||||
await page.route("**/api/v1/access-context", (route) =>
|
await page.route("**/api/v1/access-context", (route) =>
|
||||||
route.fulfill({
|
route.fulfill({
|
||||||
status: 403,
|
status: 403,
|
||||||
@@ -31,6 +32,44 @@ test("已认证但未授权的账号会看到明确说明", async ({ page }) =>
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByText("身份认证已成功,但当前账号尚未由系统管理员启用"),
|
page.getByText("身份认证已成功,但当前账号尚未由系统管理员启用"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
const accountMenu = page.getByRole("button", {
|
||||||
|
name: "E2E 测试用户 账号菜单",
|
||||||
|
});
|
||||||
|
await expect(accountMenu).toBeVisible();
|
||||||
|
await accountMenu.click();
|
||||||
|
await expect(page.getByRole("menuitem", { name: "登出" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("menuitem", { name: "切换项目" })).toHaveCount(0);
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await expect(page.getByText("账号尚未获得访问权限")).toBeVisible();
|
||||||
|
await expect(accountMenu).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("无权访问项目时可以在窄屏切换项目", async ({ page }) => {
|
||||||
|
await page.route("**/api/v1/access-context", (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 403,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ detail: "No access to project" }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
await expect(page.getByText("无权访问当前项目")).toBeVisible();
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "E2E 测试用户 账号菜单" })
|
||||||
|
.click();
|
||||||
|
await page.getByRole("menuitem", { name: "切换项目" }).click();
|
||||||
|
|
||||||
|
const projectDialog = page.getByRole("dialog");
|
||||||
|
await expect(projectDialog).toBeVisible();
|
||||||
|
const bounds = await projectDialog.boundingBox();
|
||||||
|
expect(bounds).not.toBeNull();
|
||||||
|
expect(bounds!.x).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(390);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("系统管理员可以手动同步 Keycloak 用户目录", async ({ page }) => {
|
test("系统管理员可以手动同步 Keycloak 用户目录", async ({ page }) => {
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ import type { ReactNode } from "react";
|
|||||||
|
|
||||||
import { useAccessStore } from "@/store/accessStore";
|
import { useAccessStore } from "@/store/accessStore";
|
||||||
import { useAuthStore } from "@/store/authStore";
|
import { useAuthStore } from "@/store/authStore";
|
||||||
|
import {
|
||||||
|
clearIntentionalLogout,
|
||||||
|
markIntentionalLogout,
|
||||||
|
} from "@/lib/logoutIntent";
|
||||||
import { App } from "./RefineContext";
|
import { App } from "./RefineContext";
|
||||||
|
|
||||||
let mockSessionState: {
|
let mockSessionState: {
|
||||||
data: {
|
data: {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
user: { id: string; name: string };
|
user: { id: string; name: string };
|
||||||
|
error?: "RefreshAccessTokenError" | "SessionExpired";
|
||||||
} | null;
|
} | null;
|
||||||
status: "authenticated" | "unauthenticated";
|
status: "authenticated" | "unauthenticated";
|
||||||
} = {
|
} = {
|
||||||
@@ -18,6 +23,8 @@ let mockSessionState: {
|
|||||||
},
|
},
|
||||||
status: "authenticated",
|
status: "authenticated",
|
||||||
};
|
};
|
||||||
|
let mockPathname = "/network-simulation";
|
||||||
|
let mockProjectProviderBlocks = false;
|
||||||
|
|
||||||
jest.mock("next-auth/react", () => ({
|
jest.mock("next-auth/react", () => ({
|
||||||
SessionProvider: ({ children }: { children: ReactNode }) => children,
|
SessionProvider: ({ children }: { children: ReactNode }) => children,
|
||||||
@@ -26,7 +33,7 @@ jest.mock("next-auth/react", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("next/navigation", () => ({
|
jest.mock("next/navigation", () => ({
|
||||||
usePathname: () => "/network-simulation",
|
usePathname: () => mockPathname,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@refinedev/core", () => ({
|
jest.mock("@refinedev/core", () => ({
|
||||||
@@ -43,6 +50,11 @@ jest.mock("@refinedev/mui", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@refinedev/nextjs-router", () => ({}));
|
jest.mock("@refinedev/nextjs-router", () => ({}));
|
||||||
|
jest.mock("@/components/header", () => ({
|
||||||
|
Header: ({ accessMode }: { accessMode: string }) => (
|
||||||
|
<div data-testid="access-header">{accessMode}</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
jest.mock("@providers/data-provider", () => ({ dataProvider: {} }));
|
jest.mock("@providers/data-provider", () => ({ dataProvider: {} }));
|
||||||
jest.mock("@/providers/notification-provider/useAppNotificationProvider", () => ({
|
jest.mock("@/providers/notification-provider/useAppNotificationProvider", () => ({
|
||||||
useAppNotificationProvider: {},
|
useAppNotificationProvider: {},
|
||||||
@@ -51,7 +63,8 @@ jest.mock("@contexts/color-mode", () => ({
|
|||||||
ColorModeContextProvider: ({ children }: { children: ReactNode }) => children,
|
ColorModeContextProvider: ({ children }: { children: ReactNode }) => children,
|
||||||
}));
|
}));
|
||||||
jest.mock("@/contexts/ProjectContext", () => ({
|
jest.mock("@/contexts/ProjectContext", () => ({
|
||||||
ProjectProvider: ({ children }: { children: ReactNode }) => children,
|
ProjectProvider: ({ children }: { children: ReactNode }) =>
|
||||||
|
mockProjectProviderBlocks ? <div>请选择项目环境</div> : children,
|
||||||
}));
|
}));
|
||||||
jest.mock("@/lib/authToken", () => ({
|
jest.mock("@/lib/authToken", () => ({
|
||||||
getAccessToken: jest.fn().mockResolvedValue("expired-access-token"),
|
getAccessToken: jest.fn().mockResolvedValue("expired-access-token"),
|
||||||
@@ -62,6 +75,9 @@ describe("RefineContext access authentication", () => {
|
|||||||
const originalRequest = global.Request;
|
const originalRequest = global.Request;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
clearIntentionalLogout();
|
||||||
|
mockPathname = "/network-simulation";
|
||||||
|
mockProjectProviderBlocks = false;
|
||||||
mockSessionState = {
|
mockSessionState = {
|
||||||
data: {
|
data: {
|
||||||
accessToken: "expired-access-token",
|
accessToken: "expired-access-token",
|
||||||
@@ -116,10 +132,12 @@ describe("RefineContext access authentication", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shows an authorization message when access-context rejects the account", async () => {
|
it("shows an authorization message when access-context rejects the account", async () => {
|
||||||
|
mockProjectProviderBlocks = true;
|
||||||
global.fetch = jest.fn().mockResolvedValue({
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
ok: false,
|
ok: false,
|
||||||
status: 403,
|
status: 403,
|
||||||
headers: new Headers(),
|
headers: new Headers(),
|
||||||
|
json: async () => ({ detail: "User authorization pending" }),
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
@@ -131,6 +149,7 @@ describe("RefineContext access authentication", () => {
|
|||||||
expect(
|
expect(
|
||||||
await screen.findByText("账号尚未获得访问权限"),
|
await screen.findByText("账号尚未获得访问权限"),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("access-header")).toHaveTextContent("account");
|
||||||
expect(screen.queryByText("应用内容")).not.toBeInTheDocument();
|
expect(screen.queryByText("应用内容")).not.toBeInTheDocument();
|
||||||
expect(useAuthStore.getState()).toMatchObject({
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
sessionExpired: false,
|
sessionExpired: false,
|
||||||
@@ -138,6 +157,28 @@ describe("RefineContext access authentication", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps project recovery available when the selected project is denied", async () => {
|
||||||
|
global.fetch = jest.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 403,
|
||||||
|
headers: new Headers(),
|
||||||
|
json: async () => ({ detail: "No access to project" }),
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<App>
|
||||||
|
<div>应用内容</div>
|
||||||
|
</App>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await screen.findByText("无权访问当前项目")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("button", { name: "切换项目" }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("access-header")).toHaveTextContent("project");
|
||||||
|
expect(screen.queryByText("应用内容")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("prioritizes an unauthenticated session over route permissions", async () => {
|
it("prioritizes an unauthenticated session over route permissions", async () => {
|
||||||
mockSessionState = { data: null, status: "unauthenticated" };
|
mockSessionState = { data: null, status: "unauthenticated" };
|
||||||
|
|
||||||
@@ -157,4 +198,81 @@ describe("RefineContext access authentication", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not treat an intentional logout landing on login as session expiry", async () => {
|
||||||
|
mockPathname = "/login";
|
||||||
|
mockSessionState = { data: null, status: "unauthenticated" };
|
||||||
|
|
||||||
|
render(
|
||||||
|
<App>
|
||||||
|
<div>登录页面</div>
|
||||||
|
</App>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
|
sessionExpired: false,
|
||||||
|
sessionExpiryReason: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not treat returning to a protected route after logout as session expiry", async () => {
|
||||||
|
markIntentionalLogout();
|
||||||
|
mockSessionState = { data: null, status: "unauthenticated" };
|
||||||
|
useAuthStore.setState({
|
||||||
|
accessToken: null,
|
||||||
|
sessionExpired: true,
|
||||||
|
sessionExpiryReason: "refresh_failed",
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<App>
|
||||||
|
<div>应用内容</div>
|
||||||
|
</App>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
|
sessionExpired: false,
|
||||||
|
sessionExpiryReason: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not hide a real refresh failure after an intentional logout", async () => {
|
||||||
|
markIntentionalLogout();
|
||||||
|
mockSessionState = {
|
||||||
|
data: {
|
||||||
|
accessToken: "expired-access-token",
|
||||||
|
user: { id: "user-1", name: "Test User" },
|
||||||
|
error: "RefreshAccessTokenError",
|
||||||
|
},
|
||||||
|
status: "authenticated",
|
||||||
|
};
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<App>
|
||||||
|
<div>应用内容</div>
|
||||||
|
</App>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
|
sessionExpired: true,
|
||||||
|
sessionExpiryReason: "refresh_failed",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mockSessionState = { data: null, status: "unauthenticated" };
|
||||||
|
rerender(
|
||||||
|
<App>
|
||||||
|
<div>应用内容</div>
|
||||||
|
</App>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(useAuthStore.getState().sessionExpired).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+79
-51
@@ -24,6 +24,10 @@ import { useAccessStore } from "@/store/accessStore";
|
|||||||
import { useProjectStore } from "@/store/projectStore";
|
import { useProjectStore } from "@/store/projectStore";
|
||||||
import { apiFetch } from "@/lib/apiFetch";
|
import { apiFetch } from "@/lib/apiFetch";
|
||||||
import { completeLogout, reportLogoutAudit } from "@/lib/logoutFlow";
|
import { completeLogout, reportLogoutAudit } from "@/lib/logoutFlow";
|
||||||
|
import {
|
||||||
|
clearIntentionalLogout,
|
||||||
|
isIntentionalLogout,
|
||||||
|
} from "@/lib/logoutIntent";
|
||||||
import { clearSessionRecoveryDrafts } from "@/lib/sessionRecoveryDraft";
|
import { clearSessionRecoveryDrafts } from "@/lib/sessionRecoveryDraft";
|
||||||
import { permissionCodes, resourcePermissions } from "@/lib/permissions";
|
import { permissionCodes, resourcePermissions } from "@/lib/permissions";
|
||||||
import { config } from "@config/config";
|
import { config } from "@config/config";
|
||||||
@@ -75,7 +79,9 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
|
|||||||
const setAccessContext = useAccessStore((state) => state.setContext);
|
const setAccessContext = useAccessStore((state) => state.setContext);
|
||||||
const setAccessLoading = useAccessStore((state) => state.setLoading);
|
const setAccessLoading = useAccessStore((state) => state.setLoading);
|
||||||
const resetAccess = useAccessStore((state) => state.reset);
|
const resetAccess = useAccessStore((state) => state.reset);
|
||||||
const [accessDenied, setAccessDenied] = useState(false);
|
const [accessDeniedReason, setAccessDeniedReason] = useState<
|
||||||
|
"account" | "project" | null
|
||||||
|
>(null);
|
||||||
const can = (permission: string) => permissions.includes(permission);
|
const can = (permission: string) => permissions.includes(permission);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -86,31 +92,38 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.error === "SessionExpired") {
|
if (data?.error === "SessionExpired") {
|
||||||
|
clearIntentionalLogout();
|
||||||
markSessionExpired("session_max_age");
|
markSessionExpired("session_max_age");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data?.error === "RefreshAccessTokenError") {
|
if (data?.error === "RefreshAccessTokenError") {
|
||||||
|
clearIntentionalLogout();
|
||||||
markSessionExpired("refresh_failed");
|
markSessionExpired("refresh_failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status === "unauthenticated") {
|
if (status === "unauthenticated") {
|
||||||
|
if (to === "/login" || isIntentionalLogout()) {
|
||||||
|
clearSessionExpired();
|
||||||
|
return;
|
||||||
|
}
|
||||||
markSessionExpired("unauthorized");
|
markSessionExpired("unauthorized");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status === "authenticated") {
|
if (status === "authenticated") {
|
||||||
|
clearIntentionalLogout();
|
||||||
clearSessionExpired();
|
clearSessionExpired();
|
||||||
}
|
}
|
||||||
}, [clearSessionExpired, data?.error, markSessionExpired, status]);
|
}, [clearSessionExpired, data?.error, markSessionExpired, status, to]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status !== "authenticated") {
|
if (status !== "authenticated") {
|
||||||
setAccessDenied(false);
|
setAccessDeniedReason(null);
|
||||||
resetAccess();
|
resetAccess();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setAccessDenied(false);
|
setAccessDeniedReason(null);
|
||||||
setAccessLoading(true);
|
setAccessLoading(true);
|
||||||
apiFetch(`${config.BACKEND_URL}/api/v1/access-context`, {
|
apiFetch(`${config.BACKEND_URL}/api/v1/access-context`, {
|
||||||
projectHeaderMode: currentProjectId ? "include" : "omit",
|
projectHeaderMode: currentProjectId ? "include" : "omit",
|
||||||
@@ -118,7 +131,17 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
|
|||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
setAccessDenied(true);
|
let detail = "";
|
||||||
|
try {
|
||||||
|
const payload = await response.json();
|
||||||
|
detail = typeof payload?.detail === "string" ? payload.detail : "";
|
||||||
|
} catch {
|
||||||
|
// Treat an unparseable 403 as a project access failure so the user
|
||||||
|
// keeps a recovery path instead of being trapped in the account shell.
|
||||||
|
}
|
||||||
|
setAccessDeniedReason(
|
||||||
|
detail === "User authorization pending" ? "account" : "project",
|
||||||
|
);
|
||||||
resetAccess();
|
resetAccess();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -126,7 +149,7 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
|
|||||||
resetAccess();
|
resetAccess();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAccessDenied(false);
|
setAccessDeniedReason(null);
|
||||||
setAccessContext(await response.json());
|
setAccessContext(await response.json());
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -166,6 +189,28 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
|
|||||||
return <span>loading...</span>;
|
return <span>loading...</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLogout = () =>
|
||||||
|
completeLogout({
|
||||||
|
reportAudit: () =>
|
||||||
|
reportLogoutAudit({
|
||||||
|
endpoint: `${config.BACKEND_URL}/api/v1/audit-events`,
|
||||||
|
accessToken:
|
||||||
|
typeof data?.accessToken === "string"
|
||||||
|
? data.accessToken
|
||||||
|
: useAuthStore.getState().accessToken,
|
||||||
|
}),
|
||||||
|
clearLocalState: () => {
|
||||||
|
const authStore = useAuthStore.getState();
|
||||||
|
authStore.setAccessToken(null);
|
||||||
|
authStore.clearSessionExpired();
|
||||||
|
if (data?.user?.id) {
|
||||||
|
sessionStorage.removeItem(`tjwater-login-audit:${data.user.id}`);
|
||||||
|
}
|
||||||
|
clearSessionRecoveryDrafts();
|
||||||
|
},
|
||||||
|
navigate: (path) => window.location.assign(path),
|
||||||
|
});
|
||||||
|
|
||||||
const authProvider: AuthProvider = {
|
const authProvider: AuthProvider = {
|
||||||
login: async () => {
|
login: async () => {
|
||||||
signIn("keycloak", {
|
signIn("keycloak", {
|
||||||
@@ -174,24 +219,7 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
|
|||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
logout: () =>
|
logout: handleLogout,
|
||||||
completeLogout({
|
|
||||||
reportAudit: () =>
|
|
||||||
reportLogoutAudit({
|
|
||||||
endpoint: `${config.BACKEND_URL}/api/v1/audit-events`,
|
|
||||||
accessToken:
|
|
||||||
typeof data?.accessToken === "string"
|
|
||||||
? data.accessToken
|
|
||||||
: useAuthStore.getState().accessToken,
|
|
||||||
}),
|
|
||||||
clearLocalState: () => {
|
|
||||||
if (data?.user?.id) {
|
|
||||||
sessionStorage.removeItem(`tjwater-login-audit:${data.user.id}`);
|
|
||||||
}
|
|
||||||
clearSessionRecoveryDrafts();
|
|
||||||
},
|
|
||||||
navigate: (path) => window.location.assign(path),
|
|
||||||
}),
|
|
||||||
onError: async (error) => {
|
onError: async (error) => {
|
||||||
return { error };
|
return { error };
|
||||||
},
|
},
|
||||||
@@ -384,34 +412,34 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectProvider>
|
<RefineKbarProvider>
|
||||||
<RefineKbarProvider>
|
<ColorModeContextProvider defaultMode={props.defaultMode}>
|
||||||
<ColorModeContextProvider defaultMode={props.defaultMode}>
|
<RefineSnackbarProvider>
|
||||||
<RefineSnackbarProvider>
|
<Refine
|
||||||
<Refine
|
routerProvider={appRouterProvider}
|
||||||
routerProvider={appRouterProvider}
|
dataProvider={dataProvider}
|
||||||
dataProvider={dataProvider}
|
notificationProvider={useAppNotificationProvider}
|
||||||
notificationProvider={useAppNotificationProvider}
|
authProvider={authProvider}
|
||||||
authProvider={authProvider}
|
accessControlProvider={accessControlProvider}
|
||||||
accessControlProvider={accessControlProvider}
|
resources={resources}
|
||||||
resources={resources}
|
options={{
|
||||||
options={{
|
syncWithLocation: true,
|
||||||
syncWithLocation: true,
|
warnWhenUnsavedChanges: true,
|
||||||
warnWhenUnsavedChanges: true,
|
}}
|
||||||
}}
|
>
|
||||||
|
<SessionExpiryDialog expiresAt={data?.sessionExpiresAt} />
|
||||||
|
<RoutePermissionGuard
|
||||||
|
authenticated={status === "authenticated"}
|
||||||
|
accessDeniedReason={accessDeniedReason}
|
||||||
>
|
>
|
||||||
<SessionExpiryDialog expiresAt={data?.sessionExpiresAt} />
|
<ProjectProvider>
|
||||||
<RoutePermissionGuard
|
|
||||||
authenticated={status === "authenticated"}
|
|
||||||
accessDenied={accessDenied}
|
|
||||||
>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</RoutePermissionGuard>
|
</ProjectProvider>
|
||||||
<RefineKbar />
|
</RoutePermissionGuard>
|
||||||
</Refine>
|
<RefineKbar />
|
||||||
</RefineSnackbarProvider>
|
</Refine>
|
||||||
</ColorModeContextProvider>
|
</RefineSnackbarProvider>
|
||||||
</RefineKbarProvider>
|
</ColorModeContextProvider>
|
||||||
</ProjectProvider>
|
</RefineKbarProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
import authOptions from "./options";
|
import authOptions from "./options";
|
||||||
|
|
||||||
|
describe("Keycloak profile mapping", () => {
|
||||||
|
const mapProfile = authOptions.providers[0].profile as (profile: {
|
||||||
|
sub: string;
|
||||||
|
preferred_username: string;
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
picture?: string;
|
||||||
|
}) => { image?: string | null };
|
||||||
|
|
||||||
|
it("uses the Keycloak picture instead of a shared local avatar", () => {
|
||||||
|
expect(
|
||||||
|
mapProfile({
|
||||||
|
sub: "user-1",
|
||||||
|
preferred_username: "zjb",
|
||||||
|
picture: "https://keycloak.example/avatar/zjb.png",
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
image: "https://keycloak.example/avatar/zjb.png",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves the image empty when Keycloak has no picture", () => {
|
||||||
|
const profile = mapProfile({
|
||||||
|
sub: "user-1",
|
||||||
|
preferred_username: "zjb",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(profile.image).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("NextAuth access token refresh", () => {
|
describe("NextAuth access token refresh", () => {
|
||||||
it("forces a Keycloak refresh for an agent credential request", async () => {
|
it("forces a Keycloak refresh for an agent credential request", async () => {
|
||||||
const previousEnv = {
|
const previousEnv = {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { NextAuthOptions } from "next-auth";
|
import { NextAuthOptions } from "next-auth";
|
||||||
import { JWT } from "next-auth/jwt";
|
import { JWT } from "next-auth/jwt";
|
||||||
import KeycloakProvider from "next-auth/providers/keycloak";
|
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||||
import Avatar from "@assets/avatar/avatar-small.jpeg";
|
|
||||||
|
|
||||||
const SESSION_MAX_AGE_SECONDS = 12 * 60 * 60;
|
const SESSION_MAX_AGE_SECONDS = 12 * 60 * 60;
|
||||||
const ACCESS_TOKEN_REFRESH_SKEW_MS = 30_000;
|
const ACCESS_TOKEN_REFRESH_SKEW_MS = 30_000;
|
||||||
@@ -86,12 +85,16 @@ const authOptions: NextAuthOptions = {
|
|||||||
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET ?? "",
|
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET ?? "",
|
||||||
issuer: process.env.KEYCLOAK_ISSUER ?? "",
|
issuer: process.env.KEYCLOAK_ISSUER ?? "",
|
||||||
profile(profile) {
|
profile(profile) {
|
||||||
|
const picture =
|
||||||
|
typeof (profile as { picture?: unknown }).picture === "string"
|
||||||
|
? String((profile as { picture: string }).picture).trim()
|
||||||
|
: "";
|
||||||
return {
|
return {
|
||||||
id: profile.sub,
|
id: profile.sub,
|
||||||
username: profile.preferred_username,
|
username: profile.preferred_username,
|
||||||
name: profile.name ?? profile.preferred_username,
|
name: profile.name ?? profile.preferred_username,
|
||||||
email: profile.email,
|
email: profile.email,
|
||||||
image: Avatar.src,
|
image: picture || null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ jest.mock("next/navigation", () => ({
|
|||||||
usePathname: () => "/network-simulation",
|
usePathname: () => "/network-simulation",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/components/header", () => ({
|
||||||
|
Header: ({ accessMode }: { accessMode: string }) => (
|
||||||
|
<div data-testid="access-header">{accessMode}</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("RoutePermissionGuard", () => {
|
describe("RoutePermissionGuard", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useAccessStore.setState({
|
useAccessStore.setState({
|
||||||
@@ -52,6 +58,34 @@ describe("RoutePermissionGuard", () => {
|
|||||||
expect(screen.getByText(/simulation\.view/)).toBeInTheDocument();
|
expect(screen.getByText(/simulation\.view/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps a minimal identity header and logout action for pending accounts", () => {
|
||||||
|
render(
|
||||||
|
<RoutePermissionGuard
|
||||||
|
authenticated
|
||||||
|
accessDeniedReason="account"
|
||||||
|
>
|
||||||
|
<div>受保护内容</div>
|
||||||
|
</RoutePermissionGuard>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("access-header")).toHaveTextContent("account");
|
||||||
|
expect(screen.queryByText("受保护内容")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps project recovery available when only the selected project is denied", () => {
|
||||||
|
render(
|
||||||
|
<RoutePermissionGuard
|
||||||
|
authenticated
|
||||||
|
accessDeniedReason="project"
|
||||||
|
>
|
||||||
|
<div>受保护内容</div>
|
||||||
|
</RoutePermissionGuard>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("无权访问当前项目")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("access-header")).toHaveTextContent("project");
|
||||||
|
});
|
||||||
|
|
||||||
it("checks authentication before the expired state effect runs", () => {
|
it("checks authentication before the expired state effect runs", () => {
|
||||||
render(
|
render(
|
||||||
<RoutePermissionGuard authenticated={false}>
|
<RoutePermissionGuard authenticated={false}>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
||||||
import { Alert, Box, CircularProgress, Stack, Typography } from "@mui/material";
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { Header } from "@/components/header";
|
||||||
import { permissionForPath } from "@/lib/permissions";
|
import { permissionForPath } from "@/lib/permissions";
|
||||||
import { useAccessStore } from "@/store/accessStore";
|
import { useAccessStore } from "@/store/accessStore";
|
||||||
import { useAuthStore } from "@/store/authStore";
|
import { useAuthStore } from "@/store/authStore";
|
||||||
@@ -12,11 +19,11 @@ import { useAuthStore } from "@/store/authStore";
|
|||||||
export const RoutePermissionGuard = ({
|
export const RoutePermissionGuard = ({
|
||||||
children,
|
children,
|
||||||
authenticated,
|
authenticated,
|
||||||
accessDenied = false,
|
accessDeniedReason = null,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
accessDenied?: boolean;
|
accessDeniedReason?: "account" | "project" | null;
|
||||||
}) => {
|
}) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const permissions = useAccessStore((state) => state.permissions);
|
const permissions = useAccessStore((state) => state.permissions);
|
||||||
@@ -39,17 +46,35 @@ export const RoutePermissionGuard = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessDenied) {
|
if (accessDeniedReason) {
|
||||||
|
const accountPending = accessDeniedReason === "account";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: { xs: 2, md: 4 } }}>
|
<Box sx={{ minHeight: "100vh", bgcolor: "background.default" }}>
|
||||||
<Alert severity="warning" icon={<LockOutlinedIcon />}>
|
<Header accessMode={accessDeniedReason} />
|
||||||
<Stack spacing={0.5}>
|
<Box
|
||||||
<Typography fontWeight={700}>账号尚未获得访问权限</Typography>
|
component="main"
|
||||||
<Typography variant="body2">
|
sx={{
|
||||||
身份认证已成功,但当前账号尚未由系统管理员启用,或无权访问所选项目。请联系系统管理员完成授权后重试。
|
width: "100%",
|
||||||
</Typography>
|
maxWidth: 840,
|
||||||
</Stack>
|
mx: "auto",
|
||||||
</Alert>
|
px: { xs: 2, sm: 3 },
|
||||||
|
py: { xs: 4, md: 8 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Alert severity="warning" icon={<LockOutlinedIcon />}>
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
<Typography fontWeight={700}>
|
||||||
|
{accountPending ? "账号尚未获得访问权限" : "无权访问当前项目"}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{accountPending
|
||||||
|
? "身份认证已成功,但当前账号尚未由系统管理员启用。请联系系统管理员完成授权后重试。"
|
||||||
|
: "当前账号无法访问所选项目。请选择其他已授权项目,或联系系统管理员调整项目权限。"}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { ColorModeContext } from "@contexts/color-mode";
|
import { ColorModeContext } from "@contexts/color-mode";
|
||||||
import DarkModeOutlined from "@mui/icons-material/DarkModeOutlined";
|
import DarkModeOutlined from "@mui/icons-material/DarkModeOutlined";
|
||||||
import LightModeOutlined from "@mui/icons-material/LightModeOutlined";
|
import LightModeOutlined from "@mui/icons-material/LightModeOutlined";
|
||||||
|
import { FiUser } from "react-icons/fi";
|
||||||
import { IoChatbubbleEllipsesOutline } from "react-icons/io5";
|
import { IoChatbubbleEllipsesOutline } from "react-icons/io5";
|
||||||
import Logout from "@mui/icons-material/Logout";
|
import Logout from "@mui/icons-material/Logout";
|
||||||
import SwapHoriz from "@mui/icons-material/SwapHoriz";
|
import SwapHoriz from "@mui/icons-material/SwapHoriz";
|
||||||
@@ -37,8 +38,13 @@ type IUser = {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Header: React.FC<RefineThemedLayoutHeaderProps> = ({
|
type HeaderProps = RefineThemedLayoutHeaderProps & {
|
||||||
|
accessMode?: "default" | "account" | "project";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Header: React.FC<HeaderProps> = ({
|
||||||
sticky = true,
|
sticky = true,
|
||||||
|
accessMode = "default",
|
||||||
}) => {
|
}) => {
|
||||||
const { mode, setMode } = useContext(ColorModeContext);
|
const { mode, setMode } = useContext(ColorModeContext);
|
||||||
const { mutate: logout } = useLogout();
|
const { mutate: logout } = useLogout();
|
||||||
@@ -51,6 +57,7 @@ export const Header: React.FC<RefineThemedLayoutHeaderProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { data: user } = useGetIdentity<IUser>();
|
const { data: user } = useGetIdentity<IUser>();
|
||||||
|
const restricted = accessMode !== "default";
|
||||||
|
|
||||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@@ -92,7 +99,7 @@ export const Header: React.FC<RefineThemedLayoutHeaderProps> = ({
|
|||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<HamburgerMenu />
|
{!restricted && <HamburgerMenu />}
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
width="100%"
|
width="100%"
|
||||||
@@ -110,20 +117,26 @@ export const Header: React.FC<RefineThemedLayoutHeaderProps> = ({
|
|||||||
|
|
||||||
{(user?.avatar || user?.name) && (
|
{(user?.avatar || user?.name) && (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
{!restricted && (
|
||||||
color="inherit"
|
<IconButton
|
||||||
onClick={() => setShowChatbox(true)}
|
color="inherit"
|
||||||
sx={{ mr: 1 }}
|
onClick={() => setShowChatbox(true)}
|
||||||
>
|
sx={{ mr: 1 }}
|
||||||
<IoChatbubbleEllipsesOutline />
|
>
|
||||||
</IconButton>
|
<IoChatbubbleEllipsesOutline />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
|
aria-label={
|
||||||
|
user?.name ? `${user.name} 账号菜单` : "账号菜单"
|
||||||
|
}
|
||||||
onClick={handleMenuOpen}
|
onClick={handleMenuOpen}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: "30px",
|
borderRadius: "30px",
|
||||||
padding: "6px 12px",
|
padding: "6px 12px",
|
||||||
marginLeft: "8px",
|
marginLeft: "8px",
|
||||||
transition: "all 0.3s ease",
|
transition:
|
||||||
|
"background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease",
|
||||||
border: "1px solid transparent",
|
border: "1px solid transparent",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
@@ -178,11 +191,13 @@ export const Header: React.FC<RefineThemedLayoutHeaderProps> = ({
|
|||||||
}`,
|
}`,
|
||||||
transition: "transform 0.3s ease",
|
transition: "transform 0.3s ease",
|
||||||
".MuiButtonBase-root:hover &": {
|
".MuiButtonBase-root:hover &": {
|
||||||
transform: "rotate(5deg) scale(1.05)",
|
transform: "rotate(5deg)",
|
||||||
borderColor: "primary.main",
|
borderColor: "primary.main",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
{!user?.avatar && <FiUser aria-hidden="true" />}
|
||||||
|
</Avatar>
|
||||||
</Stack>
|
</Stack>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
<Menu
|
<Menu
|
||||||
@@ -208,13 +223,15 @@ export const Header: React.FC<RefineThemedLayoutHeaderProps> = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={handleSwitchProjectClick}>
|
{accessMode !== "account" && (
|
||||||
<ListItemIcon>
|
<MenuItem onClick={handleSwitchProjectClick}>
|
||||||
<SwapHoriz fontSize="small" />
|
<ListItemIcon>
|
||||||
</ListItemIcon>
|
<SwapHoriz fontSize="small" />
|
||||||
<ListItemText>切换项目</ListItemText>
|
</ListItemIcon>
|
||||||
</MenuItem>
|
<ListItemText>切换项目</ListItemText>
|
||||||
<Divider />
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{accessMode !== "account" && <Divider />}
|
||||||
<MenuItem onClick={() => logout()}>
|
<MenuItem onClick={() => logout()}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Logout fontSize="small" />
|
<Logout fontSize="small" />
|
||||||
@@ -222,17 +239,21 @@ export const Header: React.FC<RefineThemedLayoutHeaderProps> = ({
|
|||||||
<ListItemText>登出</ListItemText>
|
<ListItemText>登出</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
<ProjectSelector
|
{accessMode !== "account" && (
|
||||||
open={showProjectSelector}
|
<ProjectSelector
|
||||||
onSelect={handleProjectSelect}
|
open={showProjectSelector}
|
||||||
onClose={() => setShowProjectSelector(false)}
|
onSelect={handleProjectSelect}
|
||||||
/>
|
onClose={() => setShowProjectSelector(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<GlobalChatbox
|
{!restricted && (
|
||||||
open={showChatbox}
|
<GlobalChatbox
|
||||||
onClose={() => setShowChatbox(false)}
|
open={showChatbox}
|
||||||
/>
|
onClose={() => setShowChatbox(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|||||||
@@ -132,8 +132,11 @@ export const ProjectSelector: React.FC<ProjectSelectorProps> = ({
|
|||||||
paper: {
|
paper: {
|
||||||
sx: {
|
sx: {
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
padding: 2,
|
boxSizing: "border-box",
|
||||||
minWidth: 400,
|
margin: { xs: 2, sm: 4 },
|
||||||
|
padding: { xs: 1.5, sm: 2 },
|
||||||
|
width: { xs: "calc(100vw - 32px)", sm: 400 },
|
||||||
|
maxWidth: "calc(100vw - 32px)",
|
||||||
background: "rgba(255, 255, 255, 0.95)",
|
background: "rgba(255, 255, 255, 0.95)",
|
||||||
backdropFilter: "blur(10px)",
|
backdropFilter: "blur(10px)",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ describe("ThreeDimensionalControls", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "站房精细版" }));
|
fireEvent.click(screen.getByRole("button", { name: "站区精细版" }));
|
||||||
expect(onCommand).toHaveBeenCalledWith({ name: "set-mode", mode: "detail" });
|
expect(onCommand).toHaveBeenCalledWith({ name: "set-mode", mode: "detail" });
|
||||||
fireEvent.change(screen.getByRole("slider", { name: "建筑背景透明度 60%" }), {
|
fireEvent.change(screen.getByRole("slider", { name: "建筑背景透明度 60%" }), {
|
||||||
target: { value: "0.35" },
|
target: { value: "0.35" },
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const sceneModes: Array<{ mode: SceneMode; label: string }> = [
|
|||||||
{ mode: "network", label: "供水管网" },
|
{ mode: "network", label: "供水管网" },
|
||||||
{ mode: "hydraulic", label: "泵组与管网" },
|
{ mode: "hydraulic", label: "泵组与管网" },
|
||||||
{ mode: "map", label: "站区轻量版" },
|
{ mode: "map", label: "站区轻量版" },
|
||||||
{ mode: "detail", label: "站房精细版" },
|
{ mode: "detail", label: "站区精细版" },
|
||||||
{ mode: "pump", label: "CAD 泵房参考" },
|
{ mode: "pump", label: "CAD 泵房参考" },
|
||||||
{ mode: "meters", label: "设备样件" },
|
{ mode: "meters", label: "设备样件" },
|
||||||
];
|
];
|
||||||
|
|||||||
+134
-402
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,8 @@
|
|||||||
import { completeLogout, reportLogoutAudit } from "./logoutFlow";
|
import { completeLogout, reportLogoutAudit } from "./logoutFlow";
|
||||||
|
import {
|
||||||
|
clearIntentionalLogout,
|
||||||
|
isIntentionalLogout,
|
||||||
|
} from "./logoutIntent";
|
||||||
|
|
||||||
describe("reportLogoutAudit", () => {
|
describe("reportLogoutAudit", () => {
|
||||||
it("starts an authenticated keepalive request", async () => {
|
it("starts an authenticated keepalive request", async () => {
|
||||||
@@ -28,6 +32,10 @@ describe("reportLogoutAudit", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("completeLogout", () => {
|
describe("completeLogout", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
clearIntentionalLogout();
|
||||||
|
});
|
||||||
|
|
||||||
it("navigates immediately without waiting for audit reporting", async () => {
|
it("navigates immediately without waiting for audit reporting", async () => {
|
||||||
const reportAudit = jest.fn(
|
const reportAudit = jest.fn(
|
||||||
() => new Promise<unknown>(() => undefined),
|
() => new Promise<unknown>(() => undefined),
|
||||||
@@ -44,6 +52,7 @@ describe("completeLogout", () => {
|
|||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
|
|
||||||
expect(reportAudit).toHaveBeenCalledTimes(1);
|
expect(reportAudit).toHaveBeenCalledTimes(1);
|
||||||
|
expect(isIntentionalLogout()).toBe(true);
|
||||||
expect(clearLocalState).toHaveBeenCalledTimes(1);
|
expect(clearLocalState).toHaveBeenCalledTimes(1);
|
||||||
expect(navigate).toHaveBeenCalledWith("/api/auth/keycloak-logout");
|
expect(navigate).toHaveBeenCalledWith("/api/auth/keycloak-logout");
|
||||||
await expect(result).resolves.toEqual({ success: true });
|
await expect(result).resolves.toEqual({ success: true });
|
||||||
@@ -79,4 +88,20 @@ describe("completeLogout", () => {
|
|||||||
expect(clearLocalState).toHaveBeenCalledTimes(1);
|
expect(clearLocalState).toHaveBeenCalledTimes(1);
|
||||||
expect(navigate).toHaveBeenCalledWith("/api/auth/keycloak-logout");
|
expect(navigate).toHaveBeenCalledWith("/api/auth/keycloak-logout");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("still navigates when local cleanup throws", async () => {
|
||||||
|
const navigate = jest.fn();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
completeLogout({
|
||||||
|
reportAudit: jest.fn().mockResolvedValue(undefined),
|
||||||
|
clearLocalState: () => {
|
||||||
|
throw new DOMException("Storage unavailable", "SecurityError");
|
||||||
|
},
|
||||||
|
navigate,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({ success: true });
|
||||||
|
|
||||||
|
expect(navigate).toHaveBeenCalledWith("/api/auth/keycloak-logout");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { markIntentionalLogout } from "@/lib/logoutIntent";
|
||||||
|
|
||||||
const KEYCLOAK_LOGOUT_PATH = "/api/auth/keycloak-logout";
|
const KEYCLOAK_LOGOUT_PATH = "/api/auth/keycloak-logout";
|
||||||
|
|
||||||
type CompleteLogoutOptions = {
|
type CompleteLogoutOptions = {
|
||||||
@@ -41,7 +43,12 @@ export const completeLogout = async ({
|
|||||||
// Synchronous reporting errors must not block logout either.
|
// Synchronous reporting errors must not block logout either.
|
||||||
}
|
}
|
||||||
|
|
||||||
clearLocalState();
|
markIntentionalLogout();
|
||||||
|
try {
|
||||||
|
clearLocalState();
|
||||||
|
} catch {
|
||||||
|
// Browser storage cleanup failures must not prevent the IdP logout.
|
||||||
|
}
|
||||||
navigate(KEYCLOAK_LOGOUT_PATH);
|
navigate(KEYCLOAK_LOGOUT_PATH);
|
||||||
return { success: true as const };
|
return { success: true as const };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
clearIntentionalLogout,
|
||||||
|
isIntentionalLogout,
|
||||||
|
markIntentionalLogout,
|
||||||
|
} from "./logoutIntent";
|
||||||
|
|
||||||
|
describe("intentional logout marker", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
clearIntentionalLogout();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores a timestamp only for the current tab", () => {
|
||||||
|
jest.spyOn(Date, "now").mockReturnValue(1_700_000_000_000);
|
||||||
|
|
||||||
|
markIntentionalLogout();
|
||||||
|
|
||||||
|
expect(sessionStorage.getItem("tjwater:intentional-logout")).toBe(
|
||||||
|
"1700000000000",
|
||||||
|
);
|
||||||
|
expect(localStorage.getItem("tjwater:intentional-logout")).toBeNull();
|
||||||
|
expect(isIntentionalLogout()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts an intentional logout event from another open tab", () => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new StorageEvent("storage", {
|
||||||
|
key: "tjwater:intentional-logout-event",
|
||||||
|
newValue: JSON.stringify({
|
||||||
|
type: "mark",
|
||||||
|
markedAt: Date.now(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(isIntentionalLogout()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not broadcast a clear event when the current tab has no marker", () => {
|
||||||
|
clearIntentionalLogout();
|
||||||
|
const setItem = jest.spyOn(Storage.prototype, "setItem");
|
||||||
|
|
||||||
|
expect(isIntentionalLogout()).toBe(false);
|
||||||
|
expect(setItem).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("expires a stale marker", () => {
|
||||||
|
const now = 1_700_000_000_000;
|
||||||
|
const nowSpy = jest.spyOn(Date, "now").mockReturnValue(now);
|
||||||
|
markIntentionalLogout();
|
||||||
|
|
||||||
|
nowSpy.mockReturnValue(now + 30 * 60 * 1000 + 1);
|
||||||
|
|
||||||
|
expect(isIntentionalLogout()).toBe(false);
|
||||||
|
expect(sessionStorage.getItem("tjwater:intentional-logout")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
const INTENTIONAL_LOGOUT_STORAGE_KEY = "tjwater:intentional-logout";
|
||||||
|
const INTENTIONAL_LOGOUT_EVENT_KEY = "tjwater:intentional-logout-event";
|
||||||
|
const INTENTIONAL_LOGOUT_TTL_MS = 30 * 60 * 1000;
|
||||||
|
let memoryMarkedAt: number | null = null;
|
||||||
|
|
||||||
|
const withStorage = (
|
||||||
|
storageName: "localStorage" | "sessionStorage",
|
||||||
|
callback: (storage: Storage) => void,
|
||||||
|
) => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
callback(window[storageName]);
|
||||||
|
} catch {
|
||||||
|
// Logout must still proceed when browser storage is unavailable.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLocalMarker = (markedAt: number | null) => {
|
||||||
|
memoryMarkedAt = markedAt;
|
||||||
|
withStorage("sessionStorage", (storage) => {
|
||||||
|
if (markedAt === null) {
|
||||||
|
storage.removeItem(INTENTIONAL_LOGOUT_STORAGE_KEY);
|
||||||
|
} else {
|
||||||
|
storage.setItem(INTENTIONAL_LOGOUT_STORAGE_KEY, String(markedAt));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const broadcastIntent = (
|
||||||
|
payload: { type: "mark"; markedAt: number } | { type: "clear" },
|
||||||
|
) => {
|
||||||
|
withStorage("localStorage", (storage) => {
|
||||||
|
storage.setItem(INTENTIONAL_LOGOUT_EVENT_KEY, JSON.stringify(payload));
|
||||||
|
storage.removeItem(INTENTIONAL_LOGOUT_EVENT_KEY);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.addEventListener("storage", (event) => {
|
||||||
|
if (event.key !== INTENTIONAL_LOGOUT_EVENT_KEY || !event.newValue) return;
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(event.newValue) as {
|
||||||
|
type?: unknown;
|
||||||
|
markedAt?: unknown;
|
||||||
|
};
|
||||||
|
if (payload.type === "mark" && typeof payload.markedAt === "number") {
|
||||||
|
setLocalMarker(payload.markedAt);
|
||||||
|
} else if (payload.type === "clear") {
|
||||||
|
setLocalMarker(null);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed cross-tab events.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const markIntentionalLogout = () => {
|
||||||
|
const markedAt = Date.now();
|
||||||
|
setLocalMarker(markedAt);
|
||||||
|
broadcastIntent({ type: "mark", markedAt });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearIntentionalLogout = () => {
|
||||||
|
setLocalMarker(null);
|
||||||
|
broadcastIntent({ type: "clear" });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isIntentionalLogout = () => {
|
||||||
|
let markedAt = memoryMarkedAt;
|
||||||
|
withStorage("sessionStorage", (storage) => {
|
||||||
|
const stored = Number(storage.getItem(INTENTIONAL_LOGOUT_STORAGE_KEY));
|
||||||
|
if (Number.isFinite(stored) && stored > 0) markedAt = stored;
|
||||||
|
});
|
||||||
|
if (markedAt === null) return false;
|
||||||
|
const age = Date.now() - markedAt;
|
||||||
|
if (age < 0 || age > INTENTIONAL_LOGOUT_TTL_MS) {
|
||||||
|
setLocalMarker(null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
clearIntentionalLogout,
|
||||||
|
markIntentionalLogout,
|
||||||
|
} from "@/lib/logoutIntent";
|
||||||
|
|
||||||
|
import { useAuthStore } from "./authStore";
|
||||||
|
|
||||||
|
describe("authStore intentional logout boundary", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
clearIntentionalLogout();
|
||||||
|
useAuthStore.setState({
|
||||||
|
accessToken: "access-token",
|
||||||
|
sessionExpired: false,
|
||||||
|
sessionExpiryReason: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearIntentionalLogout();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores late expiry signals after an intentional logout", () => {
|
||||||
|
markIntentionalLogout();
|
||||||
|
|
||||||
|
useAuthStore.getState().markSessionExpired("unauthorized");
|
||||||
|
|
||||||
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
|
accessToken: null,
|
||||||
|
sessionExpired: false,
|
||||||
|
sessionExpiryReason: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps real refresh failures visible after an intentional logout", () => {
|
||||||
|
markIntentionalLogout();
|
||||||
|
|
||||||
|
useAuthStore.getState().markSessionExpired("refresh_failed");
|
||||||
|
|
||||||
|
expect(useAuthStore.getState()).toMatchObject({
|
||||||
|
accessToken: null,
|
||||||
|
sessionExpired: true,
|
||||||
|
sessionExpiryReason: "refresh_failed",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+17
-5
@@ -1,5 +1,7 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
import { isIntentionalLogout } from "@/lib/logoutIntent";
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
sessionExpired: boolean;
|
sessionExpired: boolean;
|
||||||
@@ -16,11 +18,21 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
sessionExpired: false,
|
sessionExpired: false,
|
||||||
sessionExpiryReason: null,
|
sessionExpiryReason: null,
|
||||||
setAccessToken: (token) => set({ accessToken: token }),
|
setAccessToken: (token) => set({ accessToken: token }),
|
||||||
markSessionExpired: (reason) => set({
|
markSessionExpired: (reason) => {
|
||||||
accessToken: null,
|
if (reason === "unauthorized" && isIntentionalLogout()) {
|
||||||
sessionExpired: true,
|
set({
|
||||||
sessionExpiryReason: reason,
|
accessToken: null,
|
||||||
}),
|
sessionExpired: false,
|
||||||
|
sessionExpiryReason: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set({
|
||||||
|
accessToken: null,
|
||||||
|
sessionExpired: true,
|
||||||
|
sessionExpiryReason: reason,
|
||||||
|
});
|
||||||
|
},
|
||||||
clearSessionExpired: () => set({
|
clearSessionExpired: () => set({
|
||||||
sessionExpired: false,
|
sessionExpired: false,
|
||||||
sessionExpiryReason: null,
|
sessionExpiryReason: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user