mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: email sending and invites backend code
This commit is contained in:
parent
287fa8f41e
commit
79ddfb1ce1
9 changed files with 155 additions and 21 deletions
|
|
@ -51,12 +51,19 @@ class HostingConfig(BaseModel):
|
||||||
content_delivery: ContentDeliveryConfig
|
content_delivery: ContentDeliveryConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MailingConfig(BaseModel):
|
||||||
|
resend_api_key: str
|
||||||
|
system_email_address: str
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConfig(BaseModel):
|
class DatabaseConfig(BaseModel):
|
||||||
sql_connection_string: Optional[str]
|
sql_connection_string: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class RedisConfig(BaseModel):
|
class RedisConfig(BaseModel):
|
||||||
redis_connection_string: Optional[str]
|
redis_connection_string: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class LearnHouseConfig(BaseModel):
|
class LearnHouseConfig(BaseModel):
|
||||||
site_name: str
|
site_name: str
|
||||||
site_description: str
|
site_description: str
|
||||||
|
|
@ -67,6 +74,7 @@ class LearnHouseConfig(BaseModel):
|
||||||
redis_config: RedisConfig
|
redis_config: RedisConfig
|
||||||
security_config: SecurityConfig
|
security_config: SecurityConfig
|
||||||
ai_config: AIConfig
|
ai_config: AIConfig
|
||||||
|
mailing_config: MailingConfig
|
||||||
|
|
||||||
|
|
||||||
def get_learnhouse_config() -> LearnHouseConfig:
|
def get_learnhouse_config() -> LearnHouseConfig:
|
||||||
|
|
@ -113,6 +121,7 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
||||||
env_use_default_org = os.environ.get("LEARNHOUSE_USE_DEFAULT_ORG")
|
env_use_default_org = os.environ.get("LEARNHOUSE_USE_DEFAULT_ORG")
|
||||||
env_allowed_origins = os.environ.get("LEARNHOUSE_ALLOWED_ORIGINS")
|
env_allowed_origins = os.environ.get("LEARNHOUSE_ALLOWED_ORIGINS")
|
||||||
env_cookie_domain = os.environ.get("LEARNHOUSE_COOKIE_DOMAIN")
|
env_cookie_domain = os.environ.get("LEARNHOUSE_COOKIE_DOMAIN")
|
||||||
|
|
||||||
# Allowed origins should be a comma separated string
|
# Allowed origins should be a comma separated string
|
||||||
if env_allowed_origins:
|
if env_allowed_origins:
|
||||||
env_allowed_origins = env_allowed_origins.split(",")
|
env_allowed_origins = env_allowed_origins.split(",")
|
||||||
|
|
@ -182,14 +191,6 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
||||||
).get("sql_connection_string")
|
).get("sql_connection_string")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Redis config
|
|
||||||
env_redis_connection_string = os.environ.get("LEARNHOUSE_REDIS_CONNECTION_STRING")
|
|
||||||
redis_connection_string = env_redis_connection_string or yaml_config.get(
|
|
||||||
"redis_config", {}
|
|
||||||
).get("redis_connection_string")
|
|
||||||
|
|
||||||
|
|
||||||
# AI Config
|
# AI Config
|
||||||
env_openai_api_key = os.environ.get("LEARNHOUSE_OPENAI_API_KEY")
|
env_openai_api_key = os.environ.get("LEARNHOUSE_OPENAI_API_KEY")
|
||||||
env_is_ai_enabled = os.environ.get("LEARNHOUSE_IS_AI_ENABLED")
|
env_is_ai_enabled = os.environ.get("LEARNHOUSE_IS_AI_ENABLED")
|
||||||
|
|
@ -200,6 +201,22 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
||||||
"is_ai_enabled"
|
"is_ai_enabled"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Redis config
|
||||||
|
env_redis_connection_string = os.environ.get("LEARNHOUSE_REDIS_CONNECTION_STRING")
|
||||||
|
redis_connection_string = env_redis_connection_string or yaml_config.get(
|
||||||
|
"redis_config", {}
|
||||||
|
).get("redis_connection_string")
|
||||||
|
|
||||||
|
# Mailing config
|
||||||
|
env_resend_api_key = os.environ.get("LEARNHOUSE_RESEND_API_KEY")
|
||||||
|
env_system_email_address = os.environ.get("LEARNHOUSE_SYSTEM_EMAIL_ADDRESS")
|
||||||
|
resend_api_key = env_resend_api_key or yaml_config.get("mailing_config", {}).get(
|
||||||
|
"resend_api_key"
|
||||||
|
)
|
||||||
|
system_email_address = env_system_email_address or yaml_config.get(
|
||||||
|
"mailing_config", {}
|
||||||
|
).get("system_email_adress")
|
||||||
|
|
||||||
# Sentry config
|
# Sentry config
|
||||||
# check if the sentry config is provided in the YAML file
|
# check if the sentry config is provided in the YAML file
|
||||||
sentry_config_verif = (
|
sentry_config_verif = (
|
||||||
|
|
@ -262,6 +279,9 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
||||||
security_config=SecurityConfig(auth_jwt_secret_key=auth_jwt_secret_key),
|
security_config=SecurityConfig(auth_jwt_secret_key=auth_jwt_secret_key),
|
||||||
ai_config=ai_config,
|
ai_config=ai_config,
|
||||||
redis_config=RedisConfig(redis_connection_string=redis_connection_string),
|
redis_config=RedisConfig(redis_connection_string=redis_connection_string),
|
||||||
|
mailing_config=MailingConfig(
|
||||||
|
resend_api_key=resend_api_key, system_email_address=system_email_address
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ hosting_config:
|
||||||
bucket_name: ""
|
bucket_name: ""
|
||||||
endpoint_url: ""
|
endpoint_url: ""
|
||||||
|
|
||||||
|
mailing_config:
|
||||||
|
resend_api_key: ""
|
||||||
|
system_email_adress: ""
|
||||||
|
|
||||||
database_config:
|
database_config:
|
||||||
sql_connection_string: postgresql://learnhouse:learnhouse@db:5432/learnhouse
|
sql_connection_string: postgresql://learnhouse:learnhouse@db:5432/learnhouse
|
||||||
|
|
||||||
|
|
|
||||||
16
apps/api/poetry.lock
generated
16
apps/api/poetry.lock
generated
|
|
@ -2510,6 +2510,20 @@ requests = ">=2.0.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
|
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "resend"
|
||||||
|
version = "0.7.2"
|
||||||
|
description = "Resend Python SDK"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "resend-0.7.2-py2.py3-none-any.whl", hash = "sha256:4f16711e11b007da7f8826283af6cdc34c99bd77c1dfad92afe9466a90d06c61"},
|
||||||
|
{file = "resend-0.7.2.tar.gz", hash = "sha256:bb10522a5ef1235b6cc2d74902df39c4863ac12b89dc48b46dd5c6f980574622"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
requests = "2.31.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "4.9"
|
version = "4.9"
|
||||||
|
|
@ -3495,4 +3509,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "909bc8706c915f93e93d0d27bcc69db648213249ecd53e293283a1a8369e9692"
|
content-hash = "7443226d64e2ee6b0844e1f85d7c01b20c0bdb78e32f84875aa71ce956e141d3"
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ python-dotenv = "^1.0.0"
|
||||||
redis = "^5.0.1"
|
redis = "^5.0.1"
|
||||||
langchain-community = "^0.0.11"
|
langchain-community = "^0.0.11"
|
||||||
langchain-openai = "^0.0.2.post1"
|
langchain-openai = "^0.0.2.post1"
|
||||||
|
resend = "^0.7.2"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ async def api_get_invite_codes(
|
||||||
"""
|
"""
|
||||||
return await get_invite_codes(request, org_id, current_user, db_session)
|
return await get_invite_codes(request, org_id, current_user, db_session)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{org_id}/invites/code/{invite_code}")
|
@router.get("/{org_id}/invites/code/{invite_code}")
|
||||||
async def api_get_invite_code(
|
async def api_get_invite_code(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -186,7 +187,7 @@ async def api_get_invite_code(
|
||||||
Get invite code
|
Get invite code
|
||||||
"""
|
"""
|
||||||
print(f"org_id: {org_id}, invite_code: {invite_code}")
|
print(f"org_id: {org_id}, invite_code: {invite_code}")
|
||||||
return await get_invite_code(request, org_id,invite_code, current_user, db_session)
|
return await get_invite_code(request, org_id, invite_code, current_user, db_session)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{org_id}/invites/{org_invite_code_uuid}")
|
@router.delete("/{org_id}/invites/{org_invite_code_uuid}")
|
||||||
|
|
@ -209,14 +210,17 @@ async def api_delete_invite_code(
|
||||||
async def api_invite_batch_users(
|
async def api_invite_batch_users(
|
||||||
request: Request,
|
request: Request,
|
||||||
org_id: int,
|
org_id: int,
|
||||||
users: str,
|
emails: str,
|
||||||
|
invite_code_uuid: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
db_session: Session = Depends(get_db_session),
|
db_session: Session = Depends(get_db_session),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Invite batch users
|
Invite batch users by emails
|
||||||
"""
|
"""
|
||||||
return await invite_batch_users(request, org_id, users, db_session, current_user)
|
return await invite_batch_users(
|
||||||
|
request, org_id, emails, invite_code_uuid, db_session, current_user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{org_id}/invites/users")
|
@router.get("/{org_id}/invites/users")
|
||||||
|
|
@ -231,6 +235,7 @@ async def api_get_org_users_invites(
|
||||||
"""
|
"""
|
||||||
return await get_list_of_invited_users(request, org_id, db_session, current_user)
|
return await get_list_of_invited_users(request, org_id, db_session, current_user)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{org_id}/invites/users/{email}")
|
@router.delete("/{org_id}/invites/users/{email}")
|
||||||
async def api_delete_org_users_invites(
|
async def api_delete_org_users_invites(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
|
||||||
0
apps/api/src/services/email/__init__.py
Normal file
0
apps/api/src/services/email/__init__.py
Normal file
19
apps/api/src/services/email/utils.py
Normal file
19
apps/api/src/services/email/utils.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import resend
|
||||||
|
from config.config import get_learnhouse_config
|
||||||
|
|
||||||
|
def send_email(to: str, subject: str, body: str):
|
||||||
|
lh_config = get_learnhouse_config()
|
||||||
|
params = {
|
||||||
|
"from": f"LearnHouse <"
|
||||||
|
+ lh_config.mailing_config.system_email_address
|
||||||
|
+ ">",
|
||||||
|
"to": [to],
|
||||||
|
"subject": subject,
|
||||||
|
"html": body,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
resend.api_key = lh_config.mailing_config.resend_api_key
|
||||||
|
email = resend.Emails.send(params)
|
||||||
|
|
||||||
|
return email
|
||||||
|
|
@ -5,11 +5,13 @@ import uuid
|
||||||
import redis
|
import redis
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
from src.services.email.utils import send_email
|
||||||
from config.config import get_learnhouse_config
|
from config.config import get_learnhouse_config
|
||||||
from src.services.orgs.orgs import rbac_check
|
from src.services.orgs.orgs import rbac_check
|
||||||
from src.db.users import AnonymousUser, PublicUser
|
from src.db.users import AnonymousUser, PublicUser, UserRead
|
||||||
from src.db.organizations import (
|
from src.db.organizations import (
|
||||||
Organization,
|
Organization,
|
||||||
|
OrganizationRead,
|
||||||
)
|
)
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
|
|
||||||
|
|
@ -142,6 +144,7 @@ async def get_invite_codes(
|
||||||
|
|
||||||
return invite_codes_list
|
return invite_codes_list
|
||||||
|
|
||||||
|
|
||||||
async def get_invite_code(
|
async def get_invite_code(
|
||||||
request: Request,
|
request: Request,
|
||||||
org_id: int,
|
org_id: int,
|
||||||
|
|
@ -181,23 +184,21 @@ async def get_invite_code(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail="Could not connect to Redis",
|
detail="Could not connect to Redis",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Get invite code
|
# Get invite code
|
||||||
invite_code = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:{invite_code}") # type: ignore
|
invite_code = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:{invite_code}") # type: ignore
|
||||||
|
|
||||||
if not invite_code:
|
if not invite_code:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=404,
|
status_code=404,
|
||||||
detail="Invite code not found",
|
detail="Invite code not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
invite_code = r.get(invite_code[0]) # type: ignore
|
invite_code = r.get(invite_code[0]) # type: ignore
|
||||||
invite_code = json.loads(invite_code)
|
invite_code = json.loads(invite_code)
|
||||||
|
|
||||||
return invite_code
|
return invite_code
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_invite_code(
|
async def delete_invite_code(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -251,3 +252,57 @@ async def delete_invite_code(
|
||||||
)
|
)
|
||||||
|
|
||||||
return keys
|
return keys
|
||||||
|
|
||||||
|
|
||||||
|
def send_invite_email(
|
||||||
|
org: OrganizationRead,
|
||||||
|
invite_code_uuid: str,
|
||||||
|
user: UserRead,
|
||||||
|
email: str,
|
||||||
|
):
|
||||||
|
LH_CONFIG = get_learnhouse_config()
|
||||||
|
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
|
||||||
|
|
||||||
|
if not redis_conn_string:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Redis connection string not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Connect to Redis
|
||||||
|
r = redis.Redis.from_url(redis_conn_string)
|
||||||
|
|
||||||
|
if not r:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Could not connect to Redis",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get invite code
|
||||||
|
invite = r.keys(f"{invite_code_uuid}:org:{org.org_uuid}:code:*") # type: ignore
|
||||||
|
|
||||||
|
# Send email
|
||||||
|
if invite:
|
||||||
|
invite = r.get(invite[0])
|
||||||
|
invite = json.loads(invite)
|
||||||
|
|
||||||
|
# send email
|
||||||
|
send_email(
|
||||||
|
to=email,
|
||||||
|
subject=f"You have been invited to {org.name}",
|
||||||
|
body=f"""
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<p>Hello {email}</p>
|
||||||
|
<p>You have been invited to {org.name} by @{user.username}. Your invite code is {invite['invite_code']}.</p>
|
||||||
|
<p>Click <a href="{org.slug}.learnhouse.io/signup?inviteCode={invite['invite_code']}">here</a> to sign up.</p>
|
||||||
|
<p>Thank you</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import logging
|
||||||
import redis
|
import redis
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
from src.services.orgs.invites import send_invite_email
|
||||||
from config.config import get_learnhouse_config
|
from config.config import get_learnhouse_config
|
||||||
from src.services.orgs.orgs import rbac_check
|
from src.services.orgs.orgs import rbac_check
|
||||||
from src.db.roles import Role, RoleRead
|
from src.db.roles import Role, RoleRead
|
||||||
|
|
@ -12,6 +13,7 @@ from src.db.users import AnonymousUser, PublicUser, User, UserRead
|
||||||
from src.db.user_organizations import UserOrganization
|
from src.db.user_organizations import UserOrganization
|
||||||
from src.db.organizations import (
|
from src.db.organizations import (
|
||||||
Organization,
|
Organization,
|
||||||
|
OrganizationRead,
|
||||||
OrganizationUser,
|
OrganizationUser,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -259,6 +261,10 @@ async def invite_batch_users(
|
||||||
detail="Organization not found",
|
detail="Organization not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# get User sender
|
||||||
|
statement = select(User).where(User.id == current_user.id)
|
||||||
|
user = db_session.exec(statement).first()
|
||||||
|
|
||||||
# RBAC check
|
# RBAC check
|
||||||
await rbac_check(request, org.org_uuid, current_user, "create", db_session)
|
await rbac_check(request, org.org_uuid, current_user, "create", db_session)
|
||||||
|
|
||||||
|
|
@ -287,12 +293,22 @@ async def invite_batch_users(
|
||||||
# skip this user
|
# skip this user
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
org = OrganizationRead.from_orm(org)
|
||||||
|
user = UserRead.from_orm(user)
|
||||||
|
|
||||||
|
isEmailSent = send_invite_email(
|
||||||
|
org,
|
||||||
|
invite_code_uuid,
|
||||||
|
user,
|
||||||
|
email,
|
||||||
|
)
|
||||||
|
|
||||||
invited_user_object = {
|
invited_user_object = {
|
||||||
"email": email,
|
"email": email,
|
||||||
"org_id": org.id,
|
"org_id": org.id,
|
||||||
"invite_code_uuid": invite_code_uuid,
|
"invite_code_uuid": invite_code_uuid,
|
||||||
"pending": True,
|
"pending": True,
|
||||||
"email_sent": False,
|
"email_sent": isEmailSent,
|
||||||
"expires": ttl,
|
"expires": ttl,
|
||||||
"created_at": datetime.now().isoformat(),
|
"created_at": datetime.now().isoformat(),
|
||||||
"created_by": current_user.user_uuid,
|
"created_by": current_user.user_uuid,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue