async def change_password_page_endpoint(request: Request): """Render the change password form""" if not request.user.is_authenticated: flash(request, "error", "You are not logged in.") return RedirectResponse("/", 303) return render_template(request, "change_password.html.j2")
async def complete_github_integration(request: Request): if not github_integration_enabled(): return PlainTextResponse("GitHub integration is currently disabled.") if not request.user.is_authenticated: flash(request, "error", "You are not logged in!") return RedirectResponse("/") code = request.query_params.get("code") if not code: flash(request, "error", "Invalid OAuth code") return RedirectResponse("/") query = users.select().where(users.c.username == request.user.username) user_id = await database.fetch_val(query) async with httpx.AsyncClient() as http: res = await http.post( "https://github.com/login/oauth/access_token", params={ "client_id": GITHUB_CLIENT_ID, "client_secret": str(GITHUB_CLIENT_SECRET), "code": code, }, headers={"Accept": "application/json"}, ) if res.is_error or "error" in res.json(): # Why is this a 2xx, GitHub? print("Could not complete GitHub OAuth:") print(res.json()) flash( request, "error", "An error occurred while trying to authenticate with GitHub", ) return RedirectResponse("/") async with database.transaction(): query = user_integrations.insert().values( user_id=user_id, integration_type=GITHUB_INTEGRATION_TYPE, integration_domain="github.com", integration_data=res.json(), # { "access_token": ..., "token_type": ... } ) await database.execute(query) query = ( # Why does encode.io's databases module make it so hard to get back the created record? user_integrations.select() .where(user_integrations.c.user_id == user_id) .where(user_integrations.c.integration_type == GITHUB_INTEGRATION_TYPE) .where(user_integrations.c.integration_domain == "github.com") ) user_integration_row = await database.fetch_one(query) flash(request, "success", "Added GitHub integration for 'github.com'") async with get_integration_from_db(user_integration_row) as integration: return RedirectResponse( "/", background=BackgroundTask(integration.full_sync) )
async def initiate_github_integration(request: Request): if not github_integration_enabled(): return PlainTextResponse("GitHub integration is currently disabled.") if not request.user.is_authenticated: flash(request, "error", "You are not logged in!") return RedirectResponse("/", 303) params = { "client_id": GITHUB_CLIENT_ID, "redirect_uri": GITHUB_REDIRECT_URL, "scope": "admin:public_key", } return RedirectResponse( "https://github.com/login/oauth/authorize?" + urlencode(params) )
async def login_endpoint(request: Request): """Process a login form submission (via POST request only)""" form_data = await request.form() username = form_data.get("username") password = form_data.get("password") if (username and password) and await login_valid(username, password): request.session["username"] = username flash(request, "success", "Logged in.") else: flash( request, "error", "The username and password specified not match a user. Please try again.", ) return RedirectResponse("/", 303)
async def delete_key_endpoint(request: Request): """Delete a key for the logged in user via its comment""" if not request.user.is_authenticated: flash(request, "error", "You are not logged in!") return RedirectResponse("/", 303) form_data = await request.form() key_comment = form_data.get("key_comment") if not key_comment: flash(request, "error", "key_comment is a required field.") return RedirectResponse("/", 303) query = users.select().where(users.c.username == request.user.username) user_id = await database.fetch_val(query) background_task = None async with database.transaction(): flash(request, "success", "Deleted key.") query = (keys.select().where(keys.c.user_id == user_id).where( keys.c.key_comment == key_comment)) key = await database.fetch_one(query) ssh_algo, ssh_contents, ssh_comment = key[2:] query = (keys.delete().where(keys.c.user_id == user_id).where( keys.c.key_comment == key_comment)) await database.execute(query) background_task = BackgroundTask(run_key_delete_integrations, user_id, ssh_algo, ssh_contents, ssh_comment) return RedirectResponse("/", 303, background=background_task)
async def force_sync_github_integration(request): """Force sync any GitHub integrations for this user (via POST request only)""" if not request.user.is_authenticated: flash(request, "error", "You are not logged in!") return RedirectResponse("/", 303) username = request.user.username query = users.select().where(users.c.username == request.user.username) user_id = await database.fetch_val(query) query = ( user_integrations.select() .where(user_integrations.c.user_id == user_id) .where(user_integrations.c.integration_type == GITHUB_INTEGRATION_TYPE) ) integrations = await database.fetch_all(query) for integration_row in integrations: async with get_integration_from_db(integration_row) as integration: await integration.full_sync() return PlainTextResponse("Success.")
async def deploy_key_endpoint(request: Request): if not request.user.is_authenticated: flash(request, "error", "Cannot deploy key: You are not logged in.") return RedirectResponse("/", 303) form_data = await request.form() plaintext_key = form_data.get("key") if not plaintext_key: flash(request, "error", "'key' is a required field.") return RedirectResponse("/", 303) plaintext_key = plaintext_key.strip() background_task = None try: ssh_algo, ssh_contents, ssh_comment = parse_ssh_key(plaintext_key) async with database.transaction(): query = users.select().where( users.c.username == request.user.username) user_id = await database.fetch_val(query) query = keys.insert().values( user_id=user_id, key_algorithm=ssh_algo, key_contents=ssh_contents, key_comment=ssh_comment, ) await database.execute(query) flash(request, "success", "The provided SSH key has been deployed.") background_task = BackgroundTask( run_key_deploy_integrations, user_id, ssh_algo, ssh_contents, ssh_comment, ) except: flash(request, "error", "Invalid key data.") return RedirectResponse("/", 303, background=background_task)
async def change_password_endpoint(request: Request): """Process a change password form submission (via POST request only)""" if not request.user.is_authenticated: flash(request, "error", "You are not logged in.") return RedirectResponse("/", 303) query = users.select().where(users.c.username == request.user.username) user = await database.fetch_one(query=query) user_id, user_username, user_password_hash = user form_data = await request.form() current_password = form_data.get("current_password") password = form_data.get("password") password_confirm = form_data.get("password_confirm") has_errors = False if not current_password: has_errors = True flash(request, "error", "'current_password' is a required field.") if not password: has_errors = True flash(request, "error", "'password' is a required field.") if not password_confirm: has_errors = True flash(request, "error", "'password_confirm' is a required field.") if not bcrypt.checkpw(current_password.encode("utf-8"), user_password_hash.encode("ascii")): has_errors = True flash(request, "error", "The provided password confirmation is incorrect.") if len(password) < 32: has_errors = True flash( request, "error", "The provided password is too short. It must be at least 32 characters in length.", ) if password != password_confirm: has_errors = True flash( request, "error", "The provided password does not match the password confirmation.", ) if has_errors: return RedirectResponse("/change_password/", 303) password_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()) password_hash = password_hash.decode("ascii") async with database.transaction(): update_query = (users.update().where( users.c.username == request.user.username).values( password_hash=password_hash)) await database.execute(update_query) flash(request, "success", "Password changed.") return RedirectResponse("/", 303)
async def logout_endpoint(request: Request): """Process a logout request (via POST request only)""" del request.session["username"] flash(request, "success", "Logged out.") return RedirectResponse("/", 303)