async def get_nodes(request: Request) -> ORJSONResponse: try: limit = int(request.query_params.get("limit", 50)) except ValueError as e: raise HTTPException("Invalid limit", 422) from e if limit > 100: raise HTTPException("Limit too large", 422) if "all" in request.query_params: cursor = await connection().fetch( "select * from nodes where user_id = $1 order by id limit $2;", request.user.id, limit, ) return ORJSONResponse(list(cursor)) # if parent is not in params, it is none, so it gets the nodes at the root id_ = request.query_params.get("parent") cursor = await connection().fetch( "select * from nodes where user_id = $1 and parent = $2 order by id limit $3;", request.user.id, id_, limit, ) return ORJSONResponse(list(cursor))
async def check_signup_availability(_request: Request) -> ORJSONResponse: return ORJSONResponse( { "enabled": settings.SIGNUP_ENABLED, "email_confirm_required": settings.SIGNUP_EMAIL_CONFIRM_REQUIRED, } )
async def patch_account_name(request: Request) -> ORJSONResponse: name = await get_json(request) validate_types_raising(name, str) if not name: raise HTTPException("invalid name", 400) await connection().execute("update users set name = $1 where id = $2", name, request.user.id) return ORJSONResponse()
async def handle_http_exception(_request: Request, exception: HTTPException) -> ORJSONResponse: """handles our custom HTTPException with headers""" return ORJSONResponse( {"error": exception.detail}, headers=exception.headers, status_code=exception.status_code, )
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # noinspection PyBroadException try: return await self.app(scope, receive, send) except Exception: response = ORJSONResponse({"error": "internal server error"}, status_code=500) await response(scope, receive, send)
async def cancel_two_factor_setup(request: Request) -> ORJSONResponse: if not request.user.two_factor_secret: raise HTTPException("Not enabled", 422) if request.user.two_factor_recovery: raise HTTPException("Already confirmed", 422) await connection().execute( "update users set two_factor_secret = null where id = $1", request.user.id) return ORJSONResponse()
async def delete_account(request: Request) -> ORJSONResponse: conn = connection() async with conn.transaction(): await conn.execute( "delete from api_keys where user_id = $1;" "delete from users where id = $1;", request.user.id, ) return ORJSONResponse(None, 205)
async def patch_account_email(request: Request) -> ORJSONResponse: json = await get_json(request) validate_types_raising(json, str) try: email = email_validator.validate_email(json).email except email_validator.EmailNotValidError as e: raise HTTPException("invalid email address", 400) from e await connection().execute("update users set email = $1 where id = $2", email, request.user.id) return ORJSONResponse()
async def reset_two_factor_recovery_code(request: Request) -> ORJSONResponse: recovery = pyotp.random_base32(32) await connection().execute( """ update users set two_factor_recovery = $1 where id = $2 """, recovery, request.user.id, ) return ORJSONResponse(recovery)
async def signup_no_confirm(request: Request) -> ORJSONResponse: json = await get_json(request) validate_types_raising(json, {"name": str, "password": str, "email": str}) try: email_validator.validate_email(json["email"]) except email_validator.EmailNotValidError as e: raise HTTPException("invalid email address", 422) from e user_id = await _create_account(**json) return ORJSONResponse(user_id, 201)
async def get_two_factor_status(request: Request) -> ORJSONResponse: if request.user.two_factor_recovery: two_factor_status = "setup_complete" elif request.user.two_factor_secret: two_factor_status = "setup_in_progress" else: two_factor_status = "disabled" return ORJSONResponse({ "status": two_factor_status, "recovery": request.user.two_factor_recovery })
async def delete_api_key(request: Request) -> ORJSONResponse: query = dict(request.query_params) validate_types_raising(query, {"id": str}) result = await connection().execute( "delete from api_keys where id = $1 and user_id = $2", query["id"], request.user.id, ) if result.split(" ")[1] == "0": raise HTTPException("not found", 404) return ORJSONResponse()
async def disable_two_factor(request: Request) -> ORJSONResponse: await connection().execute( """ update users set two_factor_secret = null, two_factor_recovery = null where id = $1 """, request.user.id, ) return ORJSONResponse()
async def delete_node(request: Request) -> ORJSONResponse: try: id_ = uuid.UUID(request.query_params.get("id")) except (ValueError, TypeError) as e: raise HTTPException("invalid ID", 422) from e result = await connection().execute("""delete from nodes where id = $1""", id_) if result != "DELETE 1": raise HTTPException("not found", 404) return ORJSONResponse()
async def signup_confirm(request: Request) -> ORJSONResponse: json = await get_json(request) validate_types_raising(json, {"name": str, "password": str, "token": str}) email = decode_jwt(json["token"])["sub"] json["email"] = email try: user_id = await _create_account(**json) except HTTPException as e: raise HTTPException("account has already been created", 409) from e return ORJSONResponse(user_id, 201)
async def generate_api_key(request: Request) -> ORJSONResponse: json = await get_json(request) validate_types_raising(json, { "scope": List[str], "duration": Optional[int] }) if json["duration"] is not None: expiry = time.time() + json["duration"] else: expiry = None scope = json["scope"] key = await create_api_key(expiry, request.user.id, scope) return ORJSONResponse(key, 201)
async def signup(request: Request) -> ORJSONResponse: json = await get_json(request) validate_types_raising(json, str) try: email = email_validator.validate_email(json) except email_validator.EmailNotValidError as e: raise HTTPException("invalid email address", 422) from e if await connection().fetchval("""select id from users where email = $1""", email): raise HTTPException("email address in use", 422) await send_message(email, "Confirm Tome account", "signup_confirm") return ORJSONResponse(None, 202)
async def confirm_two_factor_setup(request: Request) -> ORJSONResponse: json = await get_json(request) validate_types_raising(json, str) if request.user.two_factor_recovery: raise HTTPException("Already confirmed", 422) if not request.user.two_factor_secret: raise HTTPException("Not enabled", 422) if not pyotp.TOTP(request.user.two_factor_secret).verify(json): raise HTTPException("Incorrect code", 422) recovery = pyotp.random_base32(32) await connection().execute( "update users set two_factor_recovery = $1 where id = $2", recovery, request.user.id, ) return ORJSONResponse(recovery)
async def begin_two_factor_setup(request: Request) -> ORJSONResponse: if request.user.two_factor_recovery: raise HTTPException("Already enabled", 422) elif request.user.two_factor_secret: raise HTTPException("Setup already started", 422) secret = pyotp.random_base32(32) await connection().execute( "update users set two_factor_secret = $1 where id = $2", secret, request.user.id) return ORJSONResponse({ "secret": secret, "qr_code_url": make_totp_qr_code(secret, request.user.email) })
async def change_password(request: Request) -> ORJSONResponse: json = await get_json(request) validate_types_raising(json, {"new": str, "current": str}) # check new password validity if json["new"] == json["current"]: # same as current (even if current is incorrect, we needn't bother checking) raise HTTPException("Password not changed", 422) check_password_strength(json["new"]) # check current password is correct if not verify_password(request.user.password, json["current"]): raise HTTPException("Incorrect password", 401) # update password hashed_new = hash_password(json["new"]) await connection().execute("update users set password = $1 where id = $2", hashed_new, request.user.id) return ORJSONResponse()
async def get_api_key(request: Request) -> ORJSONResponse: if "id" in request.query_params: result = await connection().fetchrow( "select id, scope, expiry from api_keys where id = $1 and user_id = $2", request.query_params["id"], request.user.id, ) if not result: raise HTTPException("not found", 404) else: result = dict(result) else: result = list( map( dict, await connection().fetch( "select id, scope, expiry from api_keys where user_id = $1", request.user.id, ), )) return ORJSONResponse(result)
async def modify_node(request: Request) -> ORJSONResponse: try: id_ = uuid.UUID(request.query_params.get("id")) except (ValueError, TypeError) as e: raise HTTPException("invalid ID", 422) from e json = await get_json(request) validate_types_raising(json, { "content": Optional[str], "parent": Optional[str] }) query = _modify_node_query(json, id_) try: result = await connection().fetchrow(*query) except (TypeError, ValueError) as e: raise HTTPException("Invalid ID", 422) from e except asyncpg.ForeignKeyViolationError as e: raise HTTPException("Parent does not exist", 404) from e return ORJSONResponse(result)
async def create_node(request: Request) -> ORJSONResponse: json = await get_json(request) validate_types_raising(json, { "parent": Optional[str], "content": Optional[str] }) parent = None if "parent" in json: try: parent = uuid.UUID(json["parent"]) except ValueError as e: raise HTTPException("invalid ID", 422) from e try: result = await connection().fetchval( "insert into nodes (user_id, parent) values ($1, $2) returning id", request.user.id, parent, ) except asyncpg.ForeignKeyViolationError as e: raise HTTPException("Parent node does not exist", 404) from e return ORJSONResponse(result)
async def index(_request: Request) -> ORJSONResponse: return ORJSONResponse("hello, world!")
async def get_account(request: Request) -> ORJSONResponse: return ORJSONResponse({ "id": request.user.id, "email": request.user.email, "name": request.user.name, })