Compare commits

...
Sign in to create a new pull request.

16 commits

Author SHA1 Message Date
Alexander
6d87d9cb90 feat(oauth): added httpx lib 2024-08-30 14:56:38 +02:00
Alexander
f206006e65 feat(api): removed wrong return in helper 2024-08-29 13:00:46 +02:00
Alexander
a9c2356f79 feat(api): removed unsupported symbols from Google account's name 2024-08-29 13:00:46 +02:00
Alexander
e081fdc446 feat(api): update internal id for existing account 2024-08-29 13:00:46 +02:00
Alexander
09daaa2666 feat(api): changed the error check order 2024-08-29 13:00:46 +02:00
Alexander
4f20d8dde0 feat(api): fixed jwt extraction 2024-08-29 13:00:46 +02:00
Alexander
07ee332b4c feat(api): added login redirect 2024-08-29 13:00:46 +02:00
Alexander
2b76b00e9a feat(api): fixed an issue in plan generator 2024-08-29 13:00:46 +02:00
Alexander
a6c811847a feat(api): added free plan def 2024-08-29 13:00:46 +02:00
Alexander
eab18dd9b4 feat(api): added free plan for oauth signup 2024-08-29 13:00:46 +02:00
Alexander
caf5d76050 feat(api): code review changes 2024-08-29 13:00:46 +02:00
Alexander
25baa6825e feat(api): use different config parser and disable multi_tenant 2024-08-29 13:00:46 +02:00
Alexander
a6c4847ae2 feat(api): fixed signup imports 2024-08-29 13:00:45 +02:00
Alexander
a5fe0e246f feat(api): removed wrong import 2024-08-29 13:00:45 +02:00
Alexander
771dd8bc49 feat(api): generate jwt without password 2024-08-29 13:00:45 +02:00
Alexander
a78c97bb09 feat(api): testing oauth 2024-08-29 13:00:45 +02:00
5 changed files with 176 additions and 1 deletions

View file

@ -176,6 +176,11 @@ def is_alphabet_space_dash(word):
return r.match(word) is not None return r.match(word) is not None
def remove_non_alphabet_space_dash(word):
r = re.compile("[^a-zA-Z -]")
return r.sub('', word)
def merge_lists_by_key(l1, l2, key): def merge_lists_by_key(l1, l2, key):
merged = {} merged = {}
for item in l1 + l2: for item in l1 + l2:

View file

@ -4,7 +4,7 @@ import logging
from decouple import config from decouple import config
import schemas import schemas
from chalicelib.core import users, telemetry, tenants from chalicelib.core import users, telemetry, tenants, authorizers
from chalicelib.utils import captcha, smtp from chalicelib.utils import captcha, smtp
from chalicelib.utils import helper from chalicelib.utils import helper
from chalicelib.utils import pg_client from chalicelib.utils import pg_client
@ -98,3 +98,88 @@ async def create_tenant(data: schemas.UserSignupSchema):
"user": r "user": r
} }
} }
def __plan_generator(type_name, trial_period, users_limit, data_retention, projects_limit, sessions_limits):
return {
"type": type_name,
"trial": trial_period,
"users": users_limit,
"dataRetention": data_retention,
"projects": projects_limit,
"sessions": sessions_limits,
# The following params can be overwritten in each plan change
"stripeCustomerId": None,
"stripeSubscriptionId": None,
"endAt": None,
"billingStartsAt": None
}
FREE_PLAN = __plan_generator(type_name="free",
trial_period=0,
users_limit=2,
data_retention=7,
projects_limit=1,
sessions_limits=1000)
async def create_oauth_tenant(fullname: str, email: str):
logger.info(f"==== Signup oauth started at {TimeUTC.to_human_readable(TimeUTC.now())} UTC")
errors = []
logger.debug(f"email: {email}")
if email is None or len(email) < 5:
errors.append("Invalid email address.")
else:
if users.email_exists(email):
users.update_user_internal_id(email, email)
return users.authenticate_sso(email, email)
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):
edited_fullname = helper.remove_non_alphabet_space_dash(fullname)
if len(edited_fullname) < 1:
errors.append("Invalid full name.")
fullname = edited_fullname
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],
"plan": json.dumps(FREE_PLAN)
}
query = """WITH t AS (
INSERT INTO public.tenants (name, plan)
VALUES ('my organisation', %(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, origin, internal_id)
VALUES ((SELECT tenant_id FROM t), %(email)s, 'owner', %(fullname)s, TRUE,%(data)s, (SELECT role_id FROM r WHERE name ='Owner'), 'google', %(email)s)
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))
r = cur.fetchone()
return users.authenticate_sso(email, email)

View file

@ -1020,3 +1020,15 @@ def update_user_settings(user_id, settings):
{"user_id": user_id, "settings": json.dumps(settings)}) {"user_id": user_id, "settings": json.dumps(settings)})
) )
return helper.dict_to_camel_case(cur.fetchone()) return helper.dict_to_camel_case(cur.fetchone())
def update_user_internal_id(email, internal_id):
with pg_client.PostgresClient() as cur:
cur.execute(
cur.mogrify(
f"""UPDATE public.users
SET internal_id = %(internal_id)s
WHERE email = %(email)s
AND deleted_at IS NULL;""",
{"email": email, "internal_id": internal_id})
)

View file

@ -26,3 +26,4 @@ python3-saml==1.16.0 --no-binary=lxml
redis==5.1.0b6 redis==5.1.0b6
#confluent-kafka==2.1.0 #confluent-kafka==2.1.0
azure-storage-blob==12.22.0 azure-storage-blob==12.22.0
httpx==0.23.0

View file

@ -5,6 +5,8 @@ from decouple import config
from fastapi import Body, Depends, BackgroundTasks, Request from fastapi import Body, Depends, BackgroundTasks, Request
from fastapi import HTTPException, status from fastapi import HTTPException, status
from starlette.responses import RedirectResponse, FileResponse, JSONResponse, Response from starlette.responses import RedirectResponse, FileResponse, JSONResponse, Response
import httpx
import os
import schemas import schemas
from chalicelib.core import scope from chalicelib.core import scope
@ -55,6 +57,76 @@ if config("MULTI_TENANTS", cast=bool, default=False) or not tenants.tenants_exis
return content return content
# Environment variables (ensure these are set)
GOOGLE_CLIENT_ID = config("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = config("GOOGLE_CLIENT_SECRET")
GOOGLE_REDIRECT_URI = config("GOOGLE_REDIRECT_URI")
if True or 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 True or 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 content is None:
return {"errors": ["null JWT"]}
if "errors" in content:
return content
refresh_token = content.pop("refreshToken")
refresh_token_max_age = content.pop("refreshTokenMaxAge")
response = Response(
status_code=status.HTTP_302_FOUND,
headers={'Location': config("SITE_URL") + "/login?jwt=" + content.pop("jwt")})
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"]) @public_app.post('/login', tags=["authentication"])
def login_user(response: JSONResponse, spot: Optional[bool] = False, data: schemas.UserLoginSchema = Body(...)): 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): if helper.allow_captcha() and not captcha.is_valid(data.g_recaptcha_response):