~vpzom/bracketmonster

fc141cd02c8baefc6c54cc44a0487368f4fc915c — Colin Reeder 5 months ago db3865a
Move some endpoints from unstable to v1 (slightly changed)
M brackend/openapi/openapi.json => brackend/openapi/openapi.json +95 -43
@@ 6,6 6,11 @@
	},
	"servers": [{"url": "https://api.bracket.monster"}],
	"components": {
		"schemas": {
			"UserIDOrMe": {
				"oneOf": [{"type": "integer"}, {"type": "string", "enum": ["~me"]}]
			}
		},
		"securitySchemes": {
			"bearer": {
				"type": "http",


@@ 15,48 20,6 @@
		}
	},
	"paths": {
		"/v1/users": {
			"post": {
				"summary": "Create a user. Can either be created anonymously or with a username and password.",
				"requestBody": {
					"content": {
						"application/json": {
							"schema": {
								"type": "object",
								"properties": {
									"username": {"type": "string", "example": "paul"},
									"password": {"type": "string", "example": "hunter2"}
								}
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "Successfully created user. Includes new authentication token.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": ["token", "user"],
									"properties": {
										"token": {"type": "string", "example": "e9a86a0f-6f70-4694-bc3c-6b4215f1a8eb"},
										"user": {
											"type": "object",
											"required": ["id", "username"],
											"properties": {
												"id": {"type": "integer", "example": "45"},
												"username": {"nullable": true, "type": "string", "example": "paul"}
											}
										}
									}
								}
							}
						}
					}
				}
			}
		},
		"/v1/brackets": {
			"post": {
				"summary": "Create a bracket",


@@ 146,9 109,21 @@
					}
				},
				"security": [{"bearer": []}]
			},
			}
		},
		"/v1/brackets/{bracketID}": {
			"patch": {
				"summary": "Edit bracket info",
				"parameters": [
					{
						"name": "bracketID",
						"in": "path",
						"required": true,
						"schema": {
							"type": "string"
						}
					}
				],
				"requestBody": {
					"required": true,
					"content": {


@@ 254,6 229,83 @@
				},
				"security": [{"bearer": []}]
			}
		},
		"/v1/users": {
			"post": {
				"summary": "Create a user. Can either be created anonymously or with a username and password.",
				"requestBody": {
					"content": {
						"application/json": {
							"schema": {
								"type": "object",
								"properties": {
									"username": {"type": "string", "example": "paul"},
									"password": {"type": "string", "example": "hunter2"}
								}
							}
						}
					}
				},
				"responses": {
					"200": {
						"description": "Successfully created user. Includes new authentication token.",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": ["token", "user"],
									"properties": {
										"token": {"type": "string", "example": "e9a86a0f-6f70-4694-bc3c-6b4215f1a8eb"},
										"user": {
											"type": "object",
											"required": ["id", "username"],
											"properties": {
												"id": {"type": "integer", "example": "45"},
												"username": {"nullable": true, "type": "string", "example": "paul"}
											}
										}
									}
								}
							}
						}
					}
				}
			}
		},
		"/v1/users/{userID}": {
			"get": {
				"summary": "Get user info.",
				"parameters": [
					{
						"name": "userID",
						"in": "path",
						"required": true,
						"schema": {"$ref": "#/components/schemas/UserIDOrMe"}
					}
				],
				"responses": {
					"401": {"description": "User is not authenticated (sent when trying to use '~me')"},
					"200": {
						"description": "",
						"content": {
							"application/json": {
								"schema": {
									"type": "object",
									"required": ["id", "username"],
									"properties": {
										"id": {"type": "integer"},
										"username": {
											"type": "string",
											"nullable": true
										}
									}
								}
							}
						}
					}
				},
				"security": [{"bearer": []}]
			}
		}
	}
}

M brackend/src/main.rs => brackend/src/main.rs +6 -0
@@ 182,6 182,12 @@ async fn require_login(
        })
}

fn simple_response(code: hyper::StatusCode, text: impl Into<hyper::Body>) -> hyper::Response<hyper::Body> {
    let mut res = hyper::Response::new(text.into());
    *res.status_mut() = code;
    res
}

fn to_200(value: &impl serde::Serialize) -> hyper::Response<hyper::Body> {
    let value = serde_json::to_vec(&value).unwrap();
    let mut res = hyper::Response::new(value.into());

M brackend/src/routes/unstable/mod.rs => brackend/src/routes/unstable/mod.rs +1 -76
@@ 1,30 1,8 @@
use crate::{
    get_login, report_parse_error, require_login, to_200, BracketID, BracketType, DbPool, Error,
    LoginError, UserID,
    report_parse_error, to_200, BracketID, DbPool, Error, UserID,
};

pub fn route_unstable() -> crate::RouteNode<()> {
    let route_users = crate::RouteNode::new()
        .with_child(
            "me",
            crate::RouteNode::new().with_child(
                "brackets",
                crate::RouteNode::new().with_handler_async("GET", |_, ctx, req| async move {
                    route_users_me_brackets_list_fn(req, ctx.db_pool.clone())
                        .await
                        .map(|v| to_200(&v))
                }),
            ),
        )
        .with_child(
            "~me",
            crate::RouteNode::new().with_handler_async("GET", |_, ctx, req| async move {
                route_users_me_get_fn(req, ctx.db_pool.clone())
                    .await
                    .map(|v| to_200(&v))
            }),
        );

    let route_brackets = crate::RouteNode::new().with_child_parse::<BracketID, _>(
        crate::RouteNode::new().with_child(
            "linkQr",


@@ 42,63 20,10 @@ pub fn route_unstable() -> crate::RouteNode<()> {
        });

    crate::RouteNode::new()
        .with_child("users", route_users)
        .with_child("brackets", route_brackets)
        .with_child("loginSessions", route_login_sessions)
}

async fn route_users_me_brackets_list_fn(
    req: hyper::Request<hyper::Body>,
    db_pool: DbPool,
) -> Result<serde_json::Value, Error> {
    let user = require_login(&req, &db_pool).await?;

    let client = db_pool.get().await?;

    let stmt = client.prepare("SELECT id, name, type FROM brackets, bracket_access WHERE bracket_access.bracket=brackets.id AND is_admin=TRUE AND user_id=$1").await?;
    let rows = client.query(&stmt, &[&user]).await?;

    Ok(serde_json::Value::Array(
        rows.into_iter()
            .map(|row| {
                serde_json::json!({
                    "id": BracketID::from_internal(row.get(0)).to_external(),
                    "name": row.get::<_, String>(1),
                    "type": BracketType::from_internal(&row.get::<_, String>(2)),
                })
            })
            .collect(),
    ))
}

async fn route_users_me_get_fn(
    req: hyper::Request<hyper::Body>,
    db_pool: DbPool,
) -> Result<serde_json::Value, Error> {
    let user = get_login(&req, &db_pool)
        .await
        .map_err(LoginError::to_user)?;

    match user {
        None => Ok(serde_json::Value::Null),
        Some(user) => {
            let client = db_pool.get().await?;

            let stmt = client
                .prepare("SELECT username FROM users WHERE id=$1")
                .await?;
            let row = client.query_one(&stmt, &[&user]).await?;

            let username: Option<String> = row.get(0);

            Ok(serde_json::json!({
                "username": username,
                "id": user
            }))
        }
    }
}

async fn route_brackets_linkqr_fn(
    bracket_id: BracketID,
    frontend_host: &Option<String>,

M brackend/src/routes/v1/users.rs => brackend/src/routes/v1/users.rs +101 -2
@@ 1,4 1,4 @@
use crate::{report_parse_error, require_login, to_200, to_204, DbPool, Error, UserID};
use crate::{report_parse_error, require_login, simple_response, to_200, to_204, BracketID, BracketType, DbPool, Error, LoginError, UserID};

#[derive(Debug, Clone, Copy)]
enum UserIDOrMe {


@@ 27,13 27,39 @@ impl std::str::FromStr for UserIDOrMe {
    }
}

async fn require_me(user: UserIDOrMe, me: UserID) -> Result<(), Error> {
    match user {
        UserIDOrMe::Me => Ok(()),
        UserIDOrMe::UserID(user) => {
            if user == me {
                Ok(())
            } else {
                Err(Error::UserError(simple_response(hyper::StatusCode::FORBIDDEN, "You are not authorized to do this")))
            }
        }
    }
}

pub fn route_users() -> crate::RouteNode<()> {
    let route_brackets = crate::RouteNode::new()
        .with_handler_async("GET", |(user,), ctx, req| async move {
            route_users_brackets_list_fn(ctx.db_pool.clone(), req, user)
                .await
                .map(|v| to_200(&v))
        });

    let route_user =
        crate::RouteNode::new().with_handler_async("PATCH", |(user,), ctx, req| async move {
            route_users_edit_fn(ctx.db_pool.clone(), req, user)
                .await
                .map(to_204)
        });
        })
        .with_handler_async("GET", |(user,), ctx, req| async move {
            route_users_get_fn(ctx.db_pool.clone(), req, user)
                .await
                .map(|v| to_200(&v))
        })
        .with_child("brackets", route_brackets);

    crate::RouteNode::new()
        .with_handler_async("POST", |_, ctx, req| async move {


@@ 117,6 143,52 @@ async fn route_users_create_fn(
    }))
}

async fn route_users_get_fn(
    db_pool: DbPool,
    req: hyper::Request<hyper::Body>,
    user: UserIDOrMe,
) -> Result<serde_json::Value, Error> {
    let user: i32 = match user {
        UserIDOrMe::Me => {
            let user = crate::get_login(&req, &db_pool)
                .await
                .map_err(LoginError::to_user)?;

            match user {
                None => {
                    Err(Error::UserError({
                        let mut res = hyper::Response::new("You are not logged in".into());
                        *res.status_mut() = hyper::StatusCode::UNAUTHORIZED;
                        res
                    }))
                },
                Some(user) => Ok(user),
            }?
        },
        UserIDOrMe::UserID(user) => user,
    };

    let client = db_pool.get().await?;

    let row = client.query_opt("SELECT username FROM users WHERE id=$1", &[&user]).await?;

    match row {
        None => Err(Error::UserError({
            let mut res = hyper::Response::new("No such user".into());
            *res.status_mut() = hyper::StatusCode::NOT_FOUND;
            res
        })),
        Some(row) => {
            let username: Option<String> = row.get(0);

            Ok(serde_json::json!({
                "username": username,
                "id": user
            }))
        }
    }
}

async fn route_users_edit_fn(
    db_pool: DbPool,
    req: hyper::Request<hyper::Body>,


@@ 205,3 277,30 @@ async fn route_users_edit_fn(

    Ok(())
}

async fn route_users_brackets_list_fn(
    db_pool: DbPool,
    req: hyper::Request<hyper::Body>,
    user: UserIDOrMe,
) -> Result<serde_json::Value, Error> {
    let login = require_login(&req, &db_pool).await?;
    require_me(user, login).await?;
    let user = login;

    let client = db_pool.get().await?;

    let stmt = client.prepare("SELECT id, name, type FROM brackets, bracket_access WHERE bracket_access.bracket=brackets.id AND is_admin=TRUE AND user_id=$1").await?;
    let rows = client.query(&stmt, &[&user]).await?;

    Ok(serde_json::Value::Array(
        rows.into_iter()
            .map(|row| {
                serde_json::json!({
                    "id": BracketID::from_internal(row.get(0)).to_external(),
                    "name": row.get::<_, String>(1),
                    "type": BracketType::from_internal(&row.get::<_, String>(2)),
                })
            })
            .collect(),
    ))
}

M rosebush/src/client/App.tsx => rosebush/src/client/App.tsx +13 -7
@@ 73,16 73,22 @@ export default class App extends Component<Props, State> {
			console.log("hasLogin: ", hasLogin);

			if(hasLogin) {
				fetch("/api/unstable/users/~me")
					.then(res => res.json())
				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();
					})
					.then(info => {
							if(this.state.userInfo.state === "loading") {
						if(this.state.userInfo.state === "loading") {
							this.setUserInfo(info);
							}
							})
				.catch(err => {
						}
					})
					.catch(err => {
						this.setState({userInfo: {state: "failed", error: err}});
						});
					});
			} else {
				this.setState({userInfo: {state: "done", value: null}});
			}

M rosebush/src/client/pages/myBrackets.tsx => rosebush/src/client/pages/myBrackets.tsx +1 -1
@@ 43,6 43,6 @@ export default class MyBracketsPage extends Component<{}, PageState> {
addData<{}, PageState, MyBracketsPage>(
	MyBracketsPage,
	(_, fetch) => ({
		myBrackets: fetchWithError(fetch, "/api/unstable/users/me/brackets").then(res => res.json()),
		myBrackets: fetchWithError(fetch, "/api/v1/users/~me/brackets").then(res => res.json()),
	}),
);

M rosebush/src/server/index.tsx => rosebush/src/server/index.tsx +8 -2
@@ 340,8 340,14 @@ http.createServer(async (req_, res) => {

			const [userInfo, preloadedData]: [UserInfo | null | undefined, unknown] = await Promise.all([
					isomorphicCookie.load("rosebushToken", req) ?
						apiFetch(req, "unstable/users/~me")
							.then(res => res.json()) :
						apiFetchRaw(req, "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();
							}) :
						null,
					prepareData(req, currentRoute),
			]);