~vpzom/bracketmonster

0acb44875eb6175b660d0394d042287a44329038 — Colin Reeder 5 months ago fc141cd
Adopt oazapfts for API requests
M brackend/openapi/openapi.json => brackend/openapi/openapi.json +168 -1
@@ 7,6 7,7 @@
	"servers": [{"url": "https://api.bracket.monster"}],
	"components": {
		"schemas": {
			"BracketType": {"type": "string", "enum": ["RoundRobin", "SingleElimination", "DoubleElimination", "Custom"]},
			"UserIDOrMe": {
				"oneOf": [{"type": "integer"}, {"type": "string", "enum": ["~me"]}]
			}


@@ 22,6 23,7 @@
	"paths": {
		"/v1/brackets": {
			"post": {
				"operationId": "bracketsCreate",
				"summary": "Create a bracket",
				"requestBody": {
					"content": {


@@ 32,7 34,8 @@
								"properties": {
									"name": {"type": "string", "example": "Tournament of Ultimate Destiny"},
									"players": {"type": "array", "items": {"type": "string"}, "example": ["Liam", "Emma", "Olivia", "Mason"]},
									"type": {"type": "string", "enum": ["RoundRobin", "SingleElimination", "DoubleElimination", "Custom"]},
									"type": {"$ref": "#/components/schemas/BracketType"},
									"use_scores": {"type": "boolean"},
									"matches": {
										"description": "Used for Custom brackets. A list of matches.",
										"type": "array",


@@ 112,7 115,101 @@
			}
		},
		"/v1/brackets/{bracketID}": {
			"get": {
				"operationId": "bracketGet",
				"summary": "Get bracket info",
				"parameters": [
					{
						"name": "bracketID",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": ["id", "name", "description_md", "players", "matches", "use_scores"],
									"properties": {
										"id": {"type": "string"},
										"name": {"type": "string"},
										"description_md": {"type": "string"},
										"players": {
											"type": "array",
											"items": {
												"type": "object",
												"required": ["id", "name"],
												"properties": {
													"id": {"type": "integer"},
													"name": {"type": "string"}
												}
											}
										},
										"matches": {
											"type": "array",
											"items": {
												"type": "object",
												"required": ["id", "players", "winner", "skipped", "column", "section"],
												"properties": {
													"id": {"type": "integer"},
													"players": {
														"type": "array",
														"minItems": 2,
														"maxItems": 2,
														"items": {
															"type": "object",
															"required": ["player", "source", "score"],
															"properties": {
																"player": {
																	"nullable": true,
																	"type": "object",
																	"required": ["id"],
																	"properties": {
																		"id": {"type": "integer"}
																	}
																},
																"source": {
																	"nullable": true,
																	"type": "object",
																	"required": ["match", "outcome"],
																	"properties": {
																		"match": {"type": "integer"},
																		"outcome": {"type": "string", "enum": ["win", "loss"]}
																	}
																},
																"score": {
																	"nullable": true,
																	"type": "integer"
																}
															}
														}
													},
													"winner": {
														"nullable": true,
														"type": "integer"
													},
													"column": {"type": "integer"},
													"section": {"type": "integer"},
													"skipped": {"type": "boolean"}
												}
											}
										},
										"use_scores": {"type": "boolean"}
									}
								}
							}
						}
					}
				}
			},
			"patch": {
				"operationId": "bracketEdit",
				"summary": "Edit bracket info",
				"parameters": [
					{


@@ 148,6 245,7 @@
		},
		"/v1/brackets/{bracketID}/your": {
			"get": {
				"operationId": "bracketYourGet",
				"summary": "Check your permissions on this bracket",
				"parameters": [
					{


@@ 180,6 278,7 @@
		},
		"/v1/brackets/{bracketID}/matches/{matchID}": {
			"patch": {
				"operationId": "bracketMatchEdit",
				"summary": "Update a match",
				"description": "Updates scores and/or winner for a bracket match. Requires admin access for the bracket",
				"parameters": [


@@ 232,6 331,7 @@
		},
		"/v1/users": {
			"post": {
				"operationId": "usersCreate",
				"summary": "Create a user. Can either be created anonymously or with a username and password.",
				"requestBody": {
					"content": {


@@ 274,6 374,7 @@
		},
		"/v1/users/{userID}": {
			"get": {
				"operationId": "userGet",
				"summary": "Get user info.",
				"parameters": [
					{


@@ 305,6 406,72 @@
					}
				},
				"security": [{"bearer": []}]
			},
			"patch": {
				"operationId": "userEdit",
				"summary": "Modify user info",
				"parameters": [
					{
						"name": "userID",
						"in": "path",
						"required": true,
						"schema": {"$ref": "#/components/schemas/UserIDOrMe"}
					}
				],
				"requestBody": {
					"content": {
						"application/json": {
							"schema": {
								"type": "object",
								"properties": {
									"username": {"type": "string"},
									"password": {"type": "string"},
									"email": {"type": "string"}
								}
							}
						}
					}
				},
				"responses": {
					"204": {"description": "Successfully modified."}
				}
			}
		},
		"/v1/users/{userID}/brackets": {
			"get": {
				"operationId": "userBracketsList",
				"summary": "List a user's brackets",
				"description": "List brackets created by a user. Currently only allowed for that user",
				"parameters": [
					{
						"name": "userID",
						"in": "path",
						"required": true,
						"schema": {"$ref": "#/components/schemas/UserIDOrMe"}
					}
				],
				"responses": {
					"200": {
						"description": "",
						"content": {
							"application/json": {
								"schema": {
									"type": "array",
									"items": {
										"type": "object",
										"required": ["id", "name", "type"],
										"properties": {
											"id": {"type": "string"},
											"name": {"type": "string"},
											"type": {"$ref": "#/components/schemas/BracketType"}
										}
									}
								}
							}
						}
					}
				},
				"security": [{"bearer": []}]
			}
		}
	}

M brackend/src/routes/v1/brackets.rs => brackend/src/routes/v1/brackets.rs +2 -4
@@ 78,10 78,8 @@ async fn route_brackets_get_fn(
        id: i32,
        players: (ResponseMatchPlayerInfo, ResponseMatchPlayerInfo),
        winner: Option<i32>,
        #[serde(skip_serializing_if = "Option::is_none")]
        column: Option<i32>,
        #[serde(skip_serializing_if = "Option::is_none")]
        section: Option<i32>,
        column: i32,
        section: i32,
        skipped: bool,
    }
    #[derive(serde_derive::Serialize, Debug)]

M rosebush/package-lock.json => rosebush/package-lock.json +647 -0
@@ 2,6 2,44 @@
  "requires": true,
  "lockfileVersion": 1,
  "dependencies": {
    "@apidevtools/json-schema-ref-parser": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz",
      "integrity": "sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==",
      "dev": true,
      "requires": {
        "@jsdevtools/ono": "^7.1.0",
        "call-me-maybe": "^1.0.1",
        "js-yaml": "^3.13.1"
      }
    },
    "@apidevtools/openapi-schemas": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.0.3.tgz",
      "integrity": "sha512-QoPaxGXfgqgGpK1p21FJ400z56hV681a8DOcZt3J5z0WIHgFeaIZ4+6bX5ATqmOoCpRCsH4ITEwKaOyFMz7wOA==",
      "dev": true
    },
    "@apidevtools/swagger-methods": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.1.tgz",
      "integrity": "sha512-1Vlm18XYW6Yg7uHunroXeunWz5FShPFAdxBbPy8H6niB2Elz9QQsCoYHMbcc11EL1pTxaIr9HXz2An/mHXlX1Q==",
      "dev": true
    },
    "@apidevtools/swagger-parser": {
      "version": "9.0.1",
      "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-9.0.1.tgz",
      "integrity": "sha512-Irqybg4dQrcHhZcxJc/UM4vO7Ksoj1Id5e+K94XUOzllqX1n47HEA50EKiXTCQbykxuJ4cYGIivjx/MRSTC5OA==",
      "dev": true,
      "requires": {
        "@apidevtools/json-schema-ref-parser": "^8.0.0",
        "@apidevtools/openapi-schemas": "^2.0.2",
        "@apidevtools/swagger-methods": "^3.0.0",
        "@jsdevtools/ono": "^7.1.0",
        "call-me-maybe": "^1.0.1",
        "openapi-types": "^1.3.5",
        "z-schema": "^4.2.2"
      }
    },
    "@babel/code-frame": {
      "version": "7.8.3",
      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",


@@ 1103,6 1141,12 @@
        }
      }
    },
    "@jsdevtools/ono": {
      "version": "7.1.2",
      "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.2.tgz",
      "integrity": "sha512-qS/a24RA5FEoiJS9wiv6Pwg2c/kiUo3IVUQcfeM9JvsR6pM8Yx+yl/6xWYLckZCT5jpLNhslgjiA8p/XcGyMRQ==",
      "dev": true
    },
    "@nerd-coder/webpack-node-externals": {
      "version": "1.8.2",
      "resolved": "https://registry.npmjs.org/@nerd-coder/webpack-node-externals/-/webpack-node-externals-1.8.2.tgz",


@@ 1816,6 1860,29 @@
      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
      "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
    },
    "better-ajv-errors": {
      "version": "0.6.7",
      "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-0.6.7.tgz",
      "integrity": "sha512-PYgt/sCzR4aGpyNy5+ViSQ77ognMnWq7745zM+/flYO4/Yisdtp9wDQW2IKCyVYPUxQt3E/b5GBSwfhd1LPdlg==",
      "dev": true,
      "requires": {
        "@babel/code-frame": "^7.0.0",
        "@babel/runtime": "^7.0.0",
        "chalk": "^2.4.1",
        "core-js": "^3.2.1",
        "json-to-ast": "^2.0.3",
        "jsonpointer": "^4.0.1",
        "leven": "^3.1.0"
      },
      "dependencies": {
        "core-js": {
          "version": "3.6.5",
          "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
          "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==",
          "dev": true
        }
      }
    },
    "big.js": {
      "version": "5.2.2",
      "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",


@@ 1997,6 2064,12 @@
        "unset-value": "^1.0.0"
      }
    },
    "call-me-maybe": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
      "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=",
      "dev": true
    },
    "callsites": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",


@@ 2263,6 2336,24 @@
        "shallow-clone": "^3.0.0"
      }
    },
    "co": {
      "version": "4.6.0",
      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
      "dev": true
    },
    "code-error-fragment": {
      "version": "0.0.230",
      "resolved": "https://registry.npmjs.org/code-error-fragment/-/code-error-fragment-0.0.230.tgz",
      "integrity": "sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==",
      "dev": true
    },
    "code-point-at": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
      "dev": true
    },
    "collection-visit": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",


@@ 2781,6 2872,12 @@
        "is-symbol": "^1.0.2"
      }
    },
    "es6-promise": {
      "version": "3.3.1",
      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
      "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=",
      "dev": true
    },
    "escape-html": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",


@@ 3208,6 3305,12 @@
      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
      "dev": true
    },
    "fast-safe-stringify": {
      "version": "2.0.7",
      "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
      "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==",
      "dev": true
    },
    "fbjs": {
      "version": "0.8.17",
      "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",


@@ 3595,6 3698,12 @@
      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
      "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
    },
    "grapheme-splitter": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
      "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
      "dev": true
    },
    "has": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",


@@ 3737,6 3846,12 @@
        "requires-port": "^1.0.0"
      }
    },
    "http2-client": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.3.tgz",
      "integrity": "sha512-nUxLymWQ9pzkzTmir24p2RtsgruLmhje7lH3hLX1IpwvyTg77fW+1brenPPP3USAR+rQ36p5sTA/x7sjCJVkAA==",
      "dev": true
    },
    "https-browserify": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",


@@ 4213,6 4328,16 @@
      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
      "dev": true
    },
    "json-to-ast": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/json-to-ast/-/json-to-ast-2.1.0.tgz",
      "integrity": "sha512-W9Lq347r8tA1DfMvAGn9QNcgYm4Wm7Yc+k8e6vezpMnRT+NHbtlxgNBXRVjXe9YM6eTn6+p/MKOlV/aABJcSnQ==",
      "dev": true,
      "requires": {
        "code-error-fragment": "0.0.230",
        "grapheme-splitter": "^1.0.4"
      }
    },
    "json5": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",


@@ 4221,6 4346,12 @@
        "minimist": "^1.2.0"
      }
    },
    "jsonpointer": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
      "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
      "dev": true
    },
    "jsx-ast-utils": {
      "version": "2.2.3",
      "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz",


@@ 4315,6 4446,18 @@
      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
      "dev": true
    },
    "lodash.get": {
      "version": "4.4.2",
      "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
      "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
      "dev": true
    },
    "lodash.isequal": {
      "version": "4.5.0",
      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
      "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
      "dev": true
    },
    "lodash.pick": {
      "version": "4.4.0",
      "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",


@@ 4607,6 4750,15 @@
      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
      "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
    },
    "node-fetch-h2": {
      "version": "2.3.0",
      "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz",
      "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==",
      "dev": true,
      "requires": {
        "http2-client": "^1.2.5"
      }
    },
    "node-libs-browser": {
      "version": "2.2.1",
      "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",


@@ 4644,6 4796,15 @@
        }
      }
    },
    "node-readfiles": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz",
      "integrity": "sha1-271K8SE04uY1wkXvk//Pb2BnOl0=",
      "dev": true,
      "requires": {
        "es6-promise": "^3.2.1"
      }
    },
    "node-releases": {
      "version": "1.1.53",
      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz",


@@ 4673,6 4834,256 @@
        "path-key": "^2.0.0"
      }
    },
    "number-is-nan": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
      "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
      "dev": true
    },
    "oas-kit-common": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz",
      "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==",
      "dev": true,
      "requires": {
        "fast-safe-stringify": "^2.0.7"
      }
    },
    "oas-linter": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.1.2.tgz",
      "integrity": "sha512-mv3HBG9aQz8PLGvonewIN9Y2Ra8QL6jvotRvf7NCdZ20n5vg4dO4y61UZh6s+KRDfJaU1PO+9Oxrn3EUN4Xygw==",
      "dev": true,
      "requires": {
        "should": "^13.2.1",
        "yaml": "^1.8.3"
      }
    },
    "oas-resolver": {
      "version": "2.3.2",
      "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.3.2.tgz",
      "integrity": "sha512-toGCUv8wyZZmUAAsw4jn+511xNpUFW2ZLp4sAZ7xpERIeosrbxBxtkVxot9kXvdUHtPjRafi5+bkJ56TwQeYSQ==",
      "dev": true,
      "requires": {
        "node-fetch-h2": "^2.3.0",
        "oas-kit-common": "^1.0.8",
        "reftools": "^1.1.1",
        "yaml": "^1.8.3",
        "yargs": "^15.3.1"
      },
      "dependencies": {
        "ansi-regex": {
          "version": "5.0.0",
          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
          "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
          "dev": true
        },
        "ansi-styles": {
          "version": "4.2.1",
          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
          "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
          "dev": true,
          "requires": {
            "@types/color-name": "^1.1.1",
            "color-convert": "^2.0.1"
          }
        },
        "cliui": {
          "version": "6.0.0",
          "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
          "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
          "dev": true,
          "requires": {
            "string-width": "^4.2.0",
            "strip-ansi": "^6.0.0",
            "wrap-ansi": "^6.2.0"
          }
        },
        "color-convert": {
          "version": "2.0.1",
          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
          "dev": true,
          "requires": {
            "color-name": "~1.1.4"
          }
        },
        "color-name": {
          "version": "1.1.4",
          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
          "dev": true
        },
        "emoji-regex": {
          "version": "8.0.0",
          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
          "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
          "dev": true
        },
        "find-up": {
          "version": "4.1.0",
          "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
          "dev": true,
          "requires": {
            "locate-path": "^5.0.0",
            "path-exists": "^4.0.0"
          }
        },
        "is-fullwidth-code-point": {
          "version": "3.0.0",
          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
          "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
          "dev": true
        },
        "locate-path": {
          "version": "5.0.0",
          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
          "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
          "dev": true,
          "requires": {
            "p-locate": "^4.1.0"
          }
        },
        "p-locate": {
          "version": "4.1.0",
          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
          "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
          "dev": true,
          "requires": {
            "p-limit": "^2.2.0"
          }
        },
        "path-exists": {
          "version": "4.0.0",
          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
          "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
          "dev": true
        },
        "string-width": {
          "version": "4.2.0",
          "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
          "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
          "dev": true,
          "requires": {
            "emoji-regex": "^8.0.0",
            "is-fullwidth-code-point": "^3.0.0",
            "strip-ansi": "^6.0.0"
          }
        },
        "strip-ansi": {
          "version": "6.0.0",
          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
          "dev": true,
          "requires": {
            "ansi-regex": "^5.0.0"
          }
        },
        "wrap-ansi": {
          "version": "6.2.0",
          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
          "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
          "dev": true,
          "requires": {
            "ansi-styles": "^4.0.0",
            "string-width": "^4.1.0",
            "strip-ansi": "^6.0.0"
          }
        },
        "yargs": {
          "version": "15.3.1",
          "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
          "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==",
          "dev": true,
          "requires": {
            "cliui": "^6.0.0",
            "decamelize": "^1.2.0",
            "find-up": "^4.1.0",
            "get-caller-file": "^2.0.1",
            "require-directory": "^2.1.1",
            "require-main-filename": "^2.0.0",
            "set-blocking": "^2.0.0",
            "string-width": "^4.2.0",
            "which-module": "^2.0.0",
            "y18n": "^4.0.0",
            "yargs-parser": "^18.1.1"
          }
        },
        "yargs-parser": {
          "version": "18.1.3",
          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
          "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
          "dev": true,
          "requires": {
            "camelcase": "^5.0.0",
            "decamelize": "^1.2.0"
          }
        }
      }
    },
    "oas-schema-walker": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.4.tgz",
      "integrity": "sha512-foVDDS0RJYMfhQEDh/WdBuCzydTcsCnGo9EeD8SpWq1uW10JXiz+8SfYVDA7LO87kjmlnTRZle/2gr5qxabaEA==",
      "dev": true
    },
    "oas-validator": {
      "version": "3.4.0",
      "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-3.4.0.tgz",
      "integrity": "sha512-l/SxykuACi2U51osSsBXTxdsFc8Fw41xI7AsZkzgVgWJAzoEFaaNptt35WgY9C3757RUclsm6ye5GvSyYoozLQ==",
      "dev": true,
      "requires": {
        "ajv": "^5.5.2",
        "better-ajv-errors": "^0.6.7",
        "call-me-maybe": "^1.0.1",
        "oas-kit-common": "^1.0.7",
        "oas-linter": "^3.1.0",
        "oas-resolver": "^2.3.0",
        "oas-schema-walker": "^1.1.3",
        "reftools": "^1.1.0",
        "should": "^13.2.1",
        "yaml": "^1.8.3"
      },
      "dependencies": {
        "ajv": {
          "version": "5.5.2",
          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
          "dev": true,
          "requires": {
            "co": "^4.6.0",
            "fast-deep-equal": "^1.0.0",
            "fast-json-stable-stringify": "^2.0.0",
            "json-schema-traverse": "^0.3.0"
          }
        },
        "fast-deep-equal": {
          "version": "1.1.0",
          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
          "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
          "dev": true
        },
        "json-schema-traverse": {
          "version": "0.3.1",
          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
          "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
          "dev": true
        }
      }
    },
    "oazapfts": {
      "version": "2.2.4",
      "resolved": "https://registry.npmjs.org/oazapfts/-/oazapfts-2.2.4.tgz",
      "integrity": "sha512-047EYWiAab853VIGWqkWFRmJ40dMs0x+6L331Gr/IBTZmykQH9jhKqyTxwjvwgzMajcjuoXZv8oFxQBckZZ4lA==",
      "dev": true,
      "requires": {
        "@apidevtools/swagger-parser": "^9.0.1",
        "lodash": "^4.17.15",
        "swagger2openapi": "^5.4.0",
        "typescript": "^3.8.3"
      }
    },
    "object-assign": {
      "version": "4.1.1",
      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",


@@ 4805,6 5216,12 @@
        "mimic-fn": "^2.1.0"
      }
    },
    "openapi-types": {
      "version": "1.3.5",
      "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-1.3.5.tgz",
      "integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg==",
      "dev": true
    },
    "optionator": {
      "version": "0.8.3",
      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",


@@ 5395,6 5812,12 @@
        }
      }
    },
    "reftools": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.1.tgz",
      "integrity": "sha512-7ySkzK7YpUeJP16rzJqEXTZ7IrAq/AL/p+wWejD9wdKQOe+mYYVAOB3w5ZTs2eoHfmAidwr/6PcC+q+LzPF/DQ==",
      "dev": true
    },
    "regenerate": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",


@@ 5806,6 6229,60 @@
      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
    },
    "should": {
      "version": "13.2.3",
      "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz",
      "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==",
      "dev": true,
      "requires": {
        "should-equal": "^2.0.0",
        "should-format": "^3.0.3",
        "should-type": "^1.4.0",
        "should-type-adaptors": "^1.0.1",
        "should-util": "^1.0.0"
      }
    },
    "should-equal": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz",
      "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==",
      "dev": true,
      "requires": {
        "should-type": "^1.4.0"
      }
    },
    "should-format": {
      "version": "3.0.3",
      "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz",
      "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=",
      "dev": true,
      "requires": {
        "should-type": "^1.3.0",
        "should-type-adaptors": "^1.0.1"
      }
    },
    "should-type": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
      "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=",
      "dev": true
    },
    "should-type-adaptors": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz",
      "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==",
      "dev": true,
      "requires": {
        "should-type": "^1.3.0",
        "should-util": "^1.0.0"
      }
    },
    "should-util": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz",
      "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==",
      "dev": true
    },
    "side-channel": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz",


@@ 6245,6 6722,152 @@
        "has-flag": "^3.0.0"
      }
    },
    "swagger2openapi": {
      "version": "5.4.0",
      "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-5.4.0.tgz",
      "integrity": "sha512-f5QqfXawiVijhjMtYqWZ55ESHPZFqrPC8L9idhIiuSX8O2qsa1i4MVGtCM3TQF+Smzr/6WfT/7zBuzG3aTgPAA==",
      "dev": true,
      "requires": {
        "better-ajv-errors": "^0.6.1",
        "call-me-maybe": "^1.0.1",
        "node-fetch-h2": "^2.3.0",
        "node-readfiles": "^0.2.0",
        "oas-kit-common": "^1.0.7",
        "oas-resolver": "^2.3.0",
        "oas-schema-walker": "^1.1.3",
        "oas-validator": "^3.4.0",
        "reftools": "^1.1.0",
        "yaml": "^1.8.3",
        "yargs": "^12.0.5"
      },
      "dependencies": {
        "ansi-regex": {
          "version": "3.0.0",
          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
          "dev": true
        },
        "cliui": {
          "version": "4.1.0",
          "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
          "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
          "dev": true,
          "requires": {
            "string-width": "^2.1.1",
            "strip-ansi": "^4.0.0",
            "wrap-ansi": "^2.0.0"
          }
        },
        "get-caller-file": {
          "version": "1.0.3",
          "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
          "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
          "dev": true
        },
        "require-main-filename": {
          "version": "1.0.1",
          "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
          "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
          "dev": true
        },
        "string-width": {
          "version": "2.1.1",
          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
          "dev": true,
          "requires": {
            "is-fullwidth-code-point": "^2.0.0",
            "strip-ansi": "^4.0.0"
          }
        },
        "strip-ansi": {
          "version": "4.0.0",
          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
          "dev": true,
          "requires": {
            "ansi-regex": "^3.0.0"
          }
        },
        "wrap-ansi": {
          "version": "2.1.0",
          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
          "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
          "dev": true,
          "requires": {
            "string-width": "^1.0.1",
            "strip-ansi": "^3.0.1"
          },
          "dependencies": {
            "ansi-regex": {
              "version": "2.1.1",
              "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
              "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
              "dev": true
            },
            "is-fullwidth-code-point": {
              "version": "1.0.0",
              "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
              "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
              "dev": true,
              "requires": {
                "number-is-nan": "^1.0.0"
              }
            },
            "string-width": {
              "version": "1.0.2",
              "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
              "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
              "dev": true,
              "requires": {
                "code-point-at": "^1.0.0",
                "is-fullwidth-code-point": "^1.0.0",
                "strip-ansi": "^3.0.0"
              }
            },
            "strip-ansi": {
              "version": "3.0.1",
              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
              "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
              "dev": true,
              "requires": {
                "ansi-regex": "^2.0.0"
              }
            }
          }
        },
        "yargs": {
          "version": "12.0.5",
          "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
          "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
          "dev": true,
          "requires": {
            "cliui": "^4.0.0",
            "decamelize": "^1.2.0",
            "find-up": "^3.0.0",
            "get-caller-file": "^1.0.1",
            "os-locale": "^3.0.0",
            "require-directory": "^2.1.1",
            "require-main-filename": "^1.0.1",
            "set-blocking": "^2.0.0",
            "string-width": "^2.0.0",
            "which-module": "^2.0.0",
            "y18n": "^3.2.1 || ^4.0.0",
            "yargs-parser": "^11.1.1"
          }
        },
        "yargs-parser": {
          "version": "11.1.1",
          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
          "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
          "dev": true,
          "requires": {
            "camelcase": "^5.0.0",
            "decamelize": "^1.2.0"
          }
        }
      }
    },
    "table": {
      "version": "5.4.6",
      "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",


@@ 6622,6 7245,12 @@
      "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
      "dev": true
    },
    "validator": {
      "version": "12.2.0",
      "resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz",
      "integrity": "sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ==",
      "dev": true
    },
    "vm-browserify": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",


@@ 7609,6 8238,12 @@
      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
    },
    "yaml": {
      "version": "1.10.0",
      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz",
      "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==",
      "dev": true
    },
    "yargs": {
      "version": "13.2.4",
      "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz",


@@ 7635,6 8270,18 @@
        "camelcase": "^5.0.0",
        "decamelize": "^1.2.0"
      }
    },
    "z-schema": {
      "version": "4.2.3",
      "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.3.tgz",
      "integrity": "sha512-zkvK/9TC6p38IwcrbnT3ul9in1UX4cm1y/VZSs4GHKIiDCrlafc+YQBgQBUdDXLAoZHf2qvQ7gJJOo6yT1LH6A==",
      "dev": true,
      "requires": {
        "commander": "^2.7.1",
        "lodash.get": "^4.4.2",
        "lodash.isequal": "^4.5.0",
        "validator": "^12.0.0"
      }
    }
  }
}

M rosebush/package.json => rosebush/package.json +4 -2
@@ 48,10 48,12 @@
    "@typescript-eslint/parser": "^2.30.0",
    "chokidar-cli": "^2.1.0",
    "eslint": "^6.8.0",
    "eslint-plugin-react": "^7.19.0"
    "eslint-plugin-react": "^7.19.0",
    "oazapfts": "^2.2.4"
  },
  "scripts": {
    "build": "node_modules/.bin/webpack",
    "start": "node dist/server/main.js"
    "start": "node dist/server/main.js",
    "gen-api": "node_modules/.bin/oazapfts ../brackend/openapi/openapi.json src/client/api.ts"
  }
}

M rosebush/src/client/App.tsx => rosebush/src/client/App.tsx +5 -7
@@ 4,6 4,7 @@ import { createContext, h, Component, JSX, VNode } from "preact";
import { Text } from "preact-jsx-i18n";
import Router, { Route } from "preact-router";

import { HttpError, userGet } from "./api";
import Helmet from "./Helmet";
import Data, { LoadState } from "./Data";



@@ 73,13 74,10 @@ export default class App extends Component<Props, State> {
			console.log("hasLogin: ", hasLogin);

			if(hasLogin) {
				fetch("/api/v1/users/~me")
					.then(res => {
						if(res.status === 401) return null;
						else if(res.status < 200 || res.status >= 300) {
							return res.text().then(Promise.reject.bind(Promise));
						}
						return res.json();
				return userGet("~me")
					.catch(err => {
						if(err instanceof HttpError && err.status === 401) return null;
						return Promise.reject(err);
					})
					.then(info => {
						if(this.state.userInfo.state === "loading") {

A rosebush/src/client/api.ts => rosebush/src/client/api.ts +353 -0
@@ 0,0 1,353 @@
/**
 * BracketMonster API
 * 1.0.0
 * DO NOT MODIFY - This file has been generated using oazapfts.
 * See https://www.npmjs.com/package/oazapfts
 */
export const defaults: RequestOpts = {
    baseUrl: "https://api.bracket.monster"
};
export const servers = {
    server1: "https://api.bracket.monster"
};
type Encoders = Array<(s: string) => string>;
export type RequestOpts = {
    baseUrl?: string;
    fetch?: typeof fetch;
    headers?: Record<string, string | undefined>;
} & Omit<RequestInit, "body" | "headers">;
type FetchRequestOpts = RequestOpts & {
    body?: string | FormData;
};
type JsonRequestOpts = RequestOpts & {
    body: object;
};
type MultipartRequestOpts = RequestOpts & {
    body: Record<string, string | Blob | undefined | any>;
};
export const _ = {
    async fetch(url: string, req?: FetchRequestOpts) {
        const { baseUrl, headers, fetch: customFetch, ...init } = {
            ...defaults,
            ...req
        };
        const href = _.joinUrl(baseUrl, url);
        const res = await (customFetch || fetch)(href, {
            ...init,
            headers: _.stripUndefined({ ...defaults.headers, ...headers })
        });
        if (!res.ok) {
            throw new HttpError(res.status, res.statusText, href);
        }
        return res.text();
    },
    async fetchJson(url: string, req: FetchRequestOpts = {}) {
        const res = await _.fetch(url, {
            ...req,
            headers: {
                ...req.headers,
                Accept: "application/json"
            }
        });
        return res && JSON.parse(res);
    },
    json({ body, headers, ...req }: JsonRequestOpts) {
        return {
            ...req,
            body: JSON.stringify(body),
            headers: {
                ...headers,
                "Content-Type": "application/json"
            }
        };
    },
    form({ body, headers, ...req }: JsonRequestOpts) {
        return {
            ...req,
            body: QS.form(body),
            headers: {
                ...headers,
                "Content-Type": "application/x-www-form-urlencoded"
            }
        };
    },
    multipart({ body, ...req }: MultipartRequestOpts) {
        const data = new FormData();
        Object.entries(body).forEach(([name, value]) => {
            data.append(name, value);
        });
        return {
            ...req,
            body: data
        };
    },
    /**
     * Deeply remove all properties with undefined values.
     */
    stripUndefined<T>(obj: T) {
        return obj && JSON.parse(JSON.stringify(obj));
    },
    // Encode param names and values as URIComponent
    encodeReserved: [encodeURIComponent, encodeURIComponent],
    allowReserved: [encodeURIComponent, encodeURI],
    /**
     * Creates a tag-function to encode template strings with the given encoders.
     */
    encode(encoders: Encoders, delimiter = ",") {
        const q = (v: any, i: number) => {
            const encoder = encoders[i % encoders.length];
            if (typeof v === "object") {
                if (Array.isArray(v)) {
                    return v.map(encoder).join(delimiter);
                }
                const flat = Object.entries(v).reduce((flat, entry) => [...flat, ...entry], [] as any);
                return flat.map(encoder).join(delimiter);
            }
            return encoder(String(v));
        };
        return (strings: TemplateStringsArray, ...values: any[]) => {
            return strings.reduce((prev, s, i) => {
                return `${prev}${s}${q(values[i] || "", i)}`;
            }, "");
        };
    },
    /**
     * Separate array values by the given delimiter.
     */
    delimited(delimiter = ",") {
        return (params: Record<string, any>, encoders = _.encodeReserved) => Object.entries(params)
            .filter(([, value]) => value !== undefined)
            .map(([name, value]) => _.encode(encoders, delimiter) `${name}=${value}`)
            .join("&");
    },
    joinUrl(...parts: Array<string | undefined>) {
        return parts
            .filter(Boolean)
            .join("/")
            .replace(/([^:]\/)\/+/, "$1");
    }
};
/**
 * Functions to serialize query parameters in different styles.
 */
export const QS = {
    /**
     * Join params using an ampersand and prepends a questionmark if not empty.
     */
    query(...params: string[]) {
        const s = params.join("&");
        return s && `?${s}`;
    },
    /**
     * Serializes nested objects according to the `deepObject` style specified in
     * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#style-values
     */
    deep(params: Record<string, any>, [k, v] = _.encodeReserved): string {
        const qk = _.encode([s => s, k]);
        const qv = _.encode([s => s, v]);
        // don't add index to arrays
        // https://github.com/expressjs/body-parser/issues/289
        const visit = (obj: any, prefix = ""): string => Object.entries(obj)
            .filter(([, v]) => v !== undefined)
            .map(([prop, v]) => {
            const index = Array.isArray(obj) ? "" : prop;
            const key = prefix ? qk `${prefix}[${index}]` : prop;
            if (typeof v === "object") {
                return visit(v, key);
            }
            return qv `${key}=${v}`;
        })
            .join("&");
        return visit(params);
    },
    /**
     * Property values of type array or object generate separate parameters
     * for each value of the array, or key-value-pair of the map.
     * For other types of properties this property has no effect.
     * See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#encoding-object
     */
    explode(params: Record<string, any>, encoders = _.encodeReserved): string {
        const q = _.encode(encoders);
        return Object.entries(params)
            .filter(([, value]) => value !== undefined)
            .map(([name, value]) => {
            if (Array.isArray(value)) {
                return value.map(v => q `${name}=${v}`).join("&");
            }
            if (typeof value === "object") {
                return QS.explode(value, encoders);
            }
            return q `${name}=${value}`;
        })
            .join("&");
    },
    form: _.delimited(),
    pipe: _.delimited("|"),
    space: _.delimited("%20")
};
export class HttpError extends Error {
    status: number;
    constructor(status: number, message: string, url: string) {
        super(`${url} - ${message} (${status})`);
        this.status = status;
    }
}
export type ApiResult<Fn> = Fn extends (...args: any) => Promise<infer T> ? T : never;
export type BracketType = "RoundRobin" | "SingleElimination" | "DoubleElimination" | "Custom";
export type UserIDOrMe = number | ("~me");
/**
 * Create a bracket
 */
export async function bracketsCreate(body: {
    name: string;
    players: string[];
    "type": BracketType;
    use_scores?: boolean;
    matches?: {
        players: {
            "type": "player" | "winner" | "loser";
            index?: number;
            "from"?: number;
        }[];
        section?: number;
        skip_if?: {
            "type": "winner";
            "from": number;
            "is": number;
        };
    }[];
}, opts?: RequestOpts) {
    return await _.fetchJson("/v1/brackets", _.json({
        ...opts,
        method: "POST",
        body
    })) as {
        id: string;
    };
}
/**
 * Get bracket info
 */
export async function bracketGet(bracketId: string, opts?: RequestOpts) {
    return await _.fetchJson(`/v1/brackets/${bracketId}`, {
        ...opts
    }) as {
        id: string;
        name: string;
        description_md: string;
        players: {
            id: number;
            name: string;
        }[];
        matches: {
            id: number;
            players: {
                player: {
                    id: number;
                } | null;
                source: {
                    match: number;
                    outcome: "win" | "loss";
                } | null;
                score: number | null;
            }[];
            winner: number | null;
            column: number;
            section: number;
            skipped: boolean;
        }[];
        use_scores: boolean;
    };
}
/**
 * Edit bracket info
 */
export async function bracketEdit(bracketId: string, body: {
    name?: string;
    description_md?: string;
}, opts?: RequestOpts) {
    return await _.fetch(`/v1/brackets/${bracketId}`, _.json({
        ...opts,
        method: "PATCH",
        body
    }));
}
/**
 * Check your permissions on this bracket
 */
export async function bracketYourGet(bracketId: string, opts?: RequestOpts) {
    return await _.fetchJson(`/v1/brackets/${bracketId}/your`, {
        ...opts
    }) as {
        isAdmin: boolean;
    };
}
/**
 * Update a match
 */
export async function bracketMatchEdit(bracketId: string, matchId: number, body: {
    scores?: number[];
    winner?: number | null;
}, opts?: RequestOpts) {
    return await _.fetch(`/v1/brackets/${bracketId}/matches/${matchId}`, _.json({
        ...opts,
        method: "PATCH",
        body
    }));
}
/**
 * Create a user. Can either be created anonymously or with a username and password.
 */
export async function usersCreate(body: {
    username?: string;
    password?: string;
}, opts?: RequestOpts) {
    return await _.fetchJson("/v1/users", _.json({
        ...opts,
        method: "POST",
        body
    })) as {
        token: string;
        user: {
            id: number;
            username: string | null;
        };
    };
}
/**
 * Get user info.
 */
export async function userGet(userId: UserIDOrMe, opts?: RequestOpts) {
    return await _.fetchJson(`/v1/users/${userId}`, {
        ...opts
    }) as {
        id: number;
        username: string | null;
    };
}
/**
 * Modify user info
 */
export async function userEdit(userId: UserIDOrMe, body: {
    username?: string;
    password?: string;
    email?: string;
}, opts?: RequestOpts) {
    return await _.fetch(`/v1/users/${userId}`, _.json({
        ...opts,
        method: "PATCH",
        body
    }));
}
/**
 * List a user's brackets
 */
export async function userBracketsList(userId: UserIDOrMe, opts?: RequestOpts) {
    return await _.fetchJson(`/v1/users/${userId}/brackets`, {
        ...opts
    }) as {
        id: string;
        name: string;
        "type": BracketType;
    }[];
}

D rosebush/src/client/fetchWithError.ts => rosebush/src/client/fetchWithError.ts +0 -10
@@ 1,10 0,0 @@
export default function fetchWithError(fetch: typeof window.fetch, input: RequestInfo, init?: RequestInit): Promise<Response> {
	return fetch(input, init)
		.then(res => {
			if(res.status < 200 || res.status >= 300) {
				return res.text()
					.then(text => Promise.reject(new Error(text)));
			}
			return res;
		});
}

M rosebush/src/client/index.tsx => rosebush/src/client/index.tsx +3 -0
@@ 6,6 6,7 @@ if(process.env.NODE_ENV !== "production") {
	require("preact/debug");
}

import { defaults as apiDefaults } from "./api";
import App, { UserInfo } from "./App";
import LanguageHandler from "./LanguageHandler";
import Loading from "./Loading";


@@ 14,6 15,8 @@ import routes from "./routes";

import "./main.scss";

apiDefaults.baseUrl = "/api";

const root = document.getElementById("root")!;

console.log(root);

M rosebush/src/client/pages/bracket.tsx => rosebush/src/client/pages/bracket.tsx +14 -19
@@ 6,11 6,11 @@ import { Text } from "preact-jsx-i18n";
import { Radio, RadioGroup } from "preact-radio-group";
import { SteppedLineTo } from "react-lineto";

import { bracketEdit, bracketGet, bracketMatchEdit, bracketYourGet } from "../api";
import Data, { LoadState } from "../Data";
import Helmet from "../Helmet";
import Loading from "../Loading";
import { CancelButton, Modal, ModalContainer } from "../modals";
import fetchWithError from "../fetchWithError";
import { addData } from "../preload";
import { BracketID } from "../types";



@@ 21,10 21,7 @@ interface PlayerInfo {
	name: string;
}

enum PlayerOutcome {
	Win = "win",
	Loss = "loss",
}
type PlayerOutcome = "win" | "loss";

export interface MatchInfo {
	id: number;


@@ 160,21 157,21 @@ export default class BracketPage extends Component<PageProps, PageState> {
addData<PageProps, PageState, BracketPage>(
	BracketPage,
	(props, fetch) => ({
		yourInfo: fetchWithError(fetch, "/api/v1/brackets/" + props.id + "/your")
			.then(res => res.json()),
		yourInfo: bracketYourGet(props.id, {fetch}),

		// preload bracket only on server, since we fetch it from the socket anyway
		bracket: (
			typeof window !== "undefined" ?
				undefined :
				Promise.all([
					fetchWithError(fetch, "/api/v1/brackets/" + props.id)
						.then(res => res.json()),
					bracketGet(props.id, {fetch}),
					import("../markdown"),
				])
					.then(([info, mdMod]) => {
						info.description_html = mdMod.default.render(info.description_md);
						return info;
						return {
							...info,
							description_html: mdMod.default.render(info.description_md),
						};
					})
		),
	}),


@@ 268,7 265,7 @@ class BracketView extends Component<BracketViewProps, BracketViewState> {
					})}
					{typeof window !== "undefined" && bracket.matches.map(match => {
						return match.players.map((matchPlayer, idx) => {
							if(matchPlayer.source && matchPlayer.source.outcome === PlayerOutcome.Win) {
							if(matchPlayer.source && matchPlayer.source.outcome === "win") {
								return <SteppedLineTo from={"matchMagic_" + matchPlayer.source.match} to={"matchMagic_" + match.id + "_player_" + idx} orientation="h" delay fromAnchor="right" toAnchor="left" borderColor="gray" within={"bracketMagic_" + bracket.id} />;
							}
						});


@@ 415,12 412,10 @@ export class MatchEditDialog extends Component<MatchEditProps, MatchEditState> {
		evt.preventDefault();

		this.setState({submitting: true}, () => {
			fetch(
				"/api/v1/brackets/" + this.props.bracket.id + "/matches/" + this.props.match.id,
				{
					method: "PATCH",
					body: JSON.stringify({scores: this.props.bracket.use_scores ? this.state.scores : undefined, winner: this.state.winner}),
				},
			bracketMatchEdit(
				this.props.bracket.id,
				this.props.match.id,
				{scores: this.props.bracket.use_scores ? this.state.scores : undefined, winner: this.state.winner},
			)
				.then(() => {
					this.modal.current.dismiss();


@@ 481,7 476,7 @@ export class BracketInfoEditDialog extends Component<BracketInfoEditProps, Brack
		const body = {name: this.state.name, description_md: this.state.description};

		this.setState({submitting: true}, () => {
			fetch("/api/v1/brackets/" + this.props.bracket.id, {method: "PATCH", body: JSON.stringify(body)})
			bracketEdit(this.props.bracket.id, body)
				.then(() => this.modal.current.dismiss())
				.catch(console.error)
				.then(() => this.setState({submitting: false}));

M rosebush/src/client/pages/dialog/bracketInfoEdit.tsx => rosebush/src/client/pages/dialog/bracketInfoEdit.tsx +2 -3
@@ 1,8 1,8 @@
import { bind } from "decko";
import { Component, h } from "preact";

import { bracketGet } from "../../api";
import Data, { LoadState } from "../../Data";
import fetchWithError from "../../fetchWithError";
import { FakeModalContainer } from "../../modals";
import { addData } from "../../preload";



@@ 37,7 37,6 @@ export default class BracketInfoEditPage extends Component<PageProps, PageState>
addData<PageProps, PageState, BracketInfoEditPage>(
	BracketInfoEditPage,
	(props, fetch) => ({
		bracket: fetchWithError(fetch, "/api/v1/brackets/" + props.bracketID)
			.then(res => res.json()),
		bracket: bracketGet(props.bracketID, {fetch}),
	}),
);

M rosebush/src/client/pages/dialog/matchEdit.tsx => rosebush/src/client/pages/dialog/matchEdit.tsx +2 -3
@@ 2,8 2,8 @@ import { bind } from "decko";
import { Component, h } from "preact";
import { Text } from "preact-jsx-i18n";

import { bracketGet } from "../../api";
import Data, { LoadState } from "../../Data";
import fetchWithError from "../../fetchWithError";
import { FakeModalContainer } from "../../modals";
import { addData } from "../../preload";



@@ 55,7 55,6 @@ export default class BracketInfoEditPage extends Component<PageProps, PageState>
addData<PageProps, PageState, BracketInfoEditPage>(
	BracketInfoEditPage,
	(props, fetch) => ({
		bracket: fetchWithError(fetch, "/api/v1/brackets/" + props.bracketID)
			.then(res => res.json()),
		bracket: bracketGet(props.bracketID, {fetch}),
	}),
);

M rosebush/src/client/pages/myBrackets.tsx => rosebush/src/client/pages/myBrackets.tsx +2 -2
@@ 1,9 1,9 @@
import { Component, JSX, h } from "preact";
import { Text } from "preact-jsx-i18n";

import { userBracketsList } from "../api";
import Data, { LoadState } from "../Data";
import Helmet from "../Helmet";
import fetchWithError from "../fetchWithError";
import { addData } from "../preload";
import { BracketID } from "../types";



@@ 43,6 43,6 @@ export default class MyBracketsPage extends Component<{}, PageState> {
addData<{}, PageState, MyBracketsPage>(
	MyBracketsPage,
	(_, fetch) => ({
		myBrackets: fetchWithError(fetch, "/api/v1/users/~me/brackets").then(res => res.json()),
		myBrackets: userBracketsList("~me", {fetch}),
	}),
);

M rosebush/src/client/pages/newBracket.tsx => rosebush/src/client/pages/newBracket.tsx +4 -5
@@ 5,6 5,7 @@ import { Text } from "preact-jsx-i18n";
import { route } from "preact-router";
import { shuffle } from "@pacote/shuffle";

import { bracketsCreate, usersCreate } from "../api";
import { AppContext, AppContextContent } from "../App";
import { COOKIE_AGE } from "../constants";



@@ 138,8 139,7 @@ class PageContent extends Component<Props, State> {
			(
				(app.userInfo.state === "done" && app.userInfo.value !== null) ?
					Promise.resolve() :
					fetch("/api/v1/users", {method: "POST"})
						.then(res => res.json())
					usersCreate({})
						.then(({user, token}) => {
							document.cookie = "rosebushToken=" + token + ";path=/;max-age=" + COOKIE_AGE;
							app.setUserInfo(user);


@@ 152,14 152,13 @@ class PageContent extends Component<Props, State> {
					players = shuffle(players);
				}

				return fetch("/api/v1/brackets", {method: "POST", body: JSON.stringify({
				return bracketsCreate({
					name: form.bracketName.value,
					players,
					type: form.bracketType.value,
					use_scores: this.state.useScores,
				})});
				});
			})
			.then(res => res.json())
			.then(({id}) => {
				route("/bracket/" + id);
			})

M rosebush/src/client/pages/register.tsx => rosebush/src/client/pages/register.tsx +3 -3
@@ 3,6 3,7 @@ import { useContext } from "preact/hooks";
import { Text } from "preact-jsx-i18n";
import { route } from "preact-router";

import { usersCreate, userEdit } from "../api";
import { AppContext } from "../App";
import { COOKIE_AGE } from "../constants";
import Data from "../Data";


@@ 73,8 74,7 @@ export default class RegisterPage extends Component<Props, State> {

			(
				app.userInfo.value === null ?
					fetch("/api/v1/users", {method: "POST", body: JSON.stringify(data)})
						.then(res => res.json())
					usersCreate(data)
						.then(({token, user}) => {
							document.cookie = "rosebushToken=" + token + ";path=/;max-age=" + COOKIE_AGE;



@@ 84,7 84,7 @@ export default class RegisterPage extends Component<Props, State> {
								route(this.props.continue);
							}
						}) :
					fetch("/api/v1/users/~me", {method: "PATCH", body: JSON.stringify(data)})
					userEdit("~me", data)
						.then(() => undefined)
			)
				.then(() => this.setState({success: true}))

M rosebush/src/server/index.tsx => rosebush/src/server/index.tsx +3 -0
@@ 15,6 15,7 @@ import * as serveStatic from "serve-static";
import * as querystring from "querystring";
import { shuffle } from "@pacote/shuffle";

import { defaults as apiDefaults } from "../client/api";
import App, { UserInfo } from "../client/App";
import { COOKIE_AGE } from "../client/constants";
import Data, { LoadState } from "../client/Data";


@@ 22,6 23,8 @@ import Helmet, { rewindClear } from "../client/Helmet";
import { PreloadedDataContext } from "../client/preload";
import routes from "../client/routes";

apiDefaults.baseUrl = "/api";

const API_HOST = process.env.API_HOST || "http://localhost:5000";
const SOCKET_HOST = process.env.SOCKET_HOST || "ws://localhost:5555";