From a78c97bb093bd680e57a94b7a9d9f48b3a2f338d Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 11 Jun 2024 16:12:50 +0200 Subject: [PATCH] feat(api): testing oauth --- ee/api/chalicelib/core/signup.py | 69 ++++++++++++++++++++++++++++++++ ee/api/routers/core_dynamic.py | 69 +++++++++++++++++++++++++++++++- 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/ee/api/chalicelib/core/signup.py b/ee/api/chalicelib/core/signup.py index fcc79d190..08affb49a 100644 --- a/ee/api/chalicelib/core/signup.py +++ b/ee/api/chalicelib/core/signup.py @@ -98,3 +98,72 @@ async def create_tenant(data: schemas.UserSignupSchema): "user": r } } + + +async def create_oauth_tenant(fullname: str, email: str): + logger.info(f"==== Signup oauth started at {TimeUTC.to_human_readable(TimeUTC.now())} UTC") + errors = [] + if not config("MULTI_TENANTS", cast=bool, default=False) and await tenants.tenants_exists(): + return {"errors": ["tenants already registered"]} + + logger.debug(f"email: {email}") + + if email is None or len(email) < 5: + errors.append("Invalid email address.") + else: + if users.email_exists(email): + errors.append("Email address already in use.") + if users.get_deleted_user_by_email(email) is not None: + errors.append("Email address previously deleted.") + + if fullname is None or len(fullname) < 1 or not helper.is_alphabet_space_dash(fullname): + errors.append("Invalid full name.") + + if len(errors) > 0: + logger.warning( + f"==> signup error for:\n email:{email}, fullname:{fullname}") + logger.warning(errors) + return {"errors": errors} + + project_name = "my first project" + params = { + "email": email, "fullname": fullname, "projectName": project_name, + "data": json.dumps({"lastAnnouncementView": TimeUTC.now()}), + "permissions": [p.value for p in schemas.Permissions] + } + query = """WITH t AS ( + INSERT INTO public.tenants (name, plan) + VALUES (%(organizationName)s, %(plan)s::jsonb) + RETURNING tenant_id, api_key + ), + r AS ( + INSERT INTO public.roles(tenant_id, name, description, permissions, protected) + VALUES ((SELECT tenant_id FROM t), 'Owner', 'Owner', %(permissions)s::text[], TRUE), + ((SELECT tenant_id FROM t), 'Member', 'Member', %(permissions)s::text[], FALSE) + RETURNING * + ), + u AS ( + INSERT INTO public.users (tenant_id, email, role, name, verified_email, data, role_id) + VALUES ((SELECT tenant_id FROM t), %(email)s, 'owner', %(fullname)s, TRUE,%(data)s, (SELECT role_id FROM r WHERE name ='Owner')) + RETURNING user_id,email,role,name,role_id + ) + INSERT INTO public.projects (tenant_id, name, active) + VALUES ((SELECT t.tenant_id FROM t), %(projectName)s, TRUE) + RETURNING tenant_id,project_id, (SELECT api_key FROM t) AS api_key;""" + + with pg_client.PostgresClient() as cur: + cur.execute(cur.mogrify(query, params)) + t = cur.fetchone() + + telemetry.new_client(tenant_id=t["tenant_id"]) + r = users.authenticate(email, "password") + r["smtp"] = smtp.has_smtp() + + return { + 'jwt': r.pop('jwt'), + 'refreshToken': r.pop('refreshToken'), + 'refreshTokenMaxAge': r.pop('refreshTokenMaxAge'), + 'data': { + "user": r + } + } \ No newline at end of file diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index 28fa11e12..7e7a8e352 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -2,9 +2,11 @@ import logging from typing import Optional, Union from decouple import config -from fastapi import Body, Depends, BackgroundTasks, Request +from fastapi import Body, Depends, BackgroundTasks, Request, RedirectResponse from fastapi import HTTPException, status from starlette.responses import RedirectResponse, FileResponse, JSONResponse, Response +import httpx +import os import schemas from chalicelib.core import scope @@ -55,6 +57,71 @@ if config("MULTI_TENANTS", cast=bool, default=False) or not tenants.tenants_exis return content +# Environment variables (ensure these are set) +GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") +GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") +GOOGLE_REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI") + + +if config("MULTI_TENANTS", cast=bool, default=False) or not tenants.tenants_exists_sync(use_pool=False): + @public_app.get('/signup-oauth', tags=['signup']) + async def signup_oauth_handler(): + google_authorization_url = ( + "https://accounts.google.com/o/oauth2/auth" + f"?client_id={GOOGLE_CLIENT_ID}" + f"&response_type=code" + f"&redirect_uri={GOOGLE_REDIRECT_URI}" + f"&scope=email%20profile" + ) + return RedirectResponse(url=google_authorization_url) + + +if config("MULTI_TENANTS", cast=bool, default=False) or not tenants.tenants_exists_sync(use_pool=False): + @public_app.get('/signup-oauth-callback', tags=['signup']) + async def signup_oauth_callback_handler(code: str): + # Exchange code for token + token_response = httpx.post( + url="https://oauth2.googleapis.com/token", + data={ + "client_id": GOOGLE_CLIENT_ID, + "client_secret": GOOGLE_CLIENT_SECRET, + "code": code, + "grant_type": "authorization_code", + "redirect_uri": GOOGLE_REDIRECT_URI, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + token_response_data = token_response.json() + access_token = token_response_data.get("access_token") + + if not access_token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not retrieve access token") + + # Retrieve user info + user_info_response = httpx.get( + url="https://www.googleapis.com/oauth2/v1/userinfo", + headers={"Authorization": f"Bearer {access_token}"}, + ) + user_info = user_info_response.json() + name = user_info.get("name") + email = user_info.get("email") + + if not email: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Could not retrieve user email") + if not name: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Could not retrieve user name") + + content = await signup.create_oauth_tenant(name, email) + if "errors" in content: + return content + refresh_token = content.pop("refreshToken") + refresh_token_max_age = content.pop("refreshTokenMaxAge") + response = JSONResponse(content=content) + response.set_cookie(key="refreshToken", value=refresh_token, path="/api/refresh", + max_age=refresh_token_max_age, secure=True, httponly=True) + return response + + @public_app.post('/login', tags=["authentication"]) def login_user(response: JSONResponse, spot: Optional[bool] = False, data: schemas.UserLoginSchema = Body(...)): if helper.allow_captcha() and not captcha.is_valid(data.g_recaptcha_response):