mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #152 from learnhouse/feat/emails
Implement Users inviting via Email
This commit is contained in:
commit
ae2367bdea
20 changed files with 361 additions and 41 deletions
|
|
@ -14,7 +14,8 @@
|
|||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-python.isort",
|
||||
"redhat.vscode-yaml"
|
||||
"redhat.vscode-yaml",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
],
|
||||
"settings": {
|
||||
"[python]": {
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
pnpm run format && cd ./apps/web && pnpm run lint:fix
|
||||
|
|
@ -51,12 +51,19 @@ class HostingConfig(BaseModel):
|
|||
content_delivery: ContentDeliveryConfig
|
||||
|
||||
|
||||
class MailingConfig(BaseModel):
|
||||
resend_api_key: str
|
||||
system_email_address: str
|
||||
|
||||
|
||||
class DatabaseConfig(BaseModel):
|
||||
sql_connection_string: Optional[str]
|
||||
|
||||
|
||||
class RedisConfig(BaseModel):
|
||||
redis_connection_string: Optional[str]
|
||||
|
||||
|
||||
class LearnHouseConfig(BaseModel):
|
||||
site_name: str
|
||||
site_description: str
|
||||
|
|
@ -67,6 +74,7 @@ class LearnHouseConfig(BaseModel):
|
|||
redis_config: RedisConfig
|
||||
security_config: SecurityConfig
|
||||
ai_config: AIConfig
|
||||
mailing_config: MailingConfig
|
||||
|
||||
|
||||
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_allowed_origins = os.environ.get("LEARNHOUSE_ALLOWED_ORIGINS")
|
||||
env_cookie_domain = os.environ.get("LEARNHOUSE_COOKIE_DOMAIN")
|
||||
|
||||
# Allowed origins should be a comma separated string
|
||||
if env_allowed_origins:
|
||||
env_allowed_origins = env_allowed_origins.split(",")
|
||||
|
|
@ -182,14 +191,6 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
|||
).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
|
||||
env_openai_api_key = os.environ.get("LEARNHOUSE_OPENAI_API_KEY")
|
||||
env_is_ai_enabled = os.environ.get("LEARNHOUSE_IS_AI_ENABLED")
|
||||
|
|
@ -200,6 +201,22 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
|||
"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
|
||||
# check if the sentry config is provided in the YAML file
|
||||
sentry_config_verif = (
|
||||
|
|
@ -262,6 +279,9 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
|||
security_config=SecurityConfig(auth_jwt_secret_key=auth_jwt_secret_key),
|
||||
ai_config=ai_config,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ hosting_config:
|
|||
bucket_name: ""
|
||||
endpoint_url: ""
|
||||
|
||||
mailing_config:
|
||||
resend_api_key: ""
|
||||
system_email_adress: ""
|
||||
|
||||
database_config:
|
||||
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]
|
||||
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]]
|
||||
name = "rsa"
|
||||
version = "4.9"
|
||||
|
|
@ -3495,4 +3509,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "909bc8706c915f93e93d0d27bcc69db648213249ecd53e293283a1a8369e9692"
|
||||
content-hash = "7443226d64e2ee6b0844e1f85d7c01b20c0bdb78e32f84875aa71ce956e141d3"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ python-dotenv = "^1.0.0"
|
|||
redis = "^5.0.1"
|
||||
langchain-community = "^0.0.11"
|
||||
langchain-openai = "^0.0.2.post1"
|
||||
resend = "^0.7.2"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from sqlmodel import SQLModel, Session, create_engine
|
|||
|
||||
learnhouse_config = get_learnhouse_config()
|
||||
engine = create_engine(
|
||||
learnhouse_config.database_config.sql_connection_string, echo=False # type: ignore
|
||||
learnhouse_config.database_config.sql_connection_string, echo=False, pool_pre_ping=True # type: ignore
|
||||
)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
|
@ -16,8 +16,6 @@ async def connect_to_db(app: FastAPI):
|
|||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
|
||||
|
||||
|
||||
def get_db_session():
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ async def api_get_invite_codes(
|
|||
"""
|
||||
return await get_invite_codes(request, org_id, current_user, db_session)
|
||||
|
||||
|
||||
@router.get("/{org_id}/invites/code/{invite_code}")
|
||||
async def api_get_invite_code(
|
||||
request: Request,
|
||||
|
|
@ -209,14 +210,17 @@ async def api_delete_invite_code(
|
|||
async def api_invite_batch_users(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
users: str,
|
||||
emails: str,
|
||||
invite_code_uuid: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
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")
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
@router.delete("/{org_id}/invites/users/{email}")
|
||||
async def api_delete_org_users_invites(
|
||||
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 @@
|
|||
from pydantic import EmailStr
|
||||
import resend
|
||||
from config.config import get_learnhouse_config
|
||||
|
||||
|
||||
def send_email(to: EmailStr, subject: str, body: str):
|
||||
lh_config = get_learnhouse_config()
|
||||
params = {
|
||||
"from": "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
|
||||
from datetime import datetime, timedelta
|
||||
from sqlmodel import Session, select
|
||||
from src.services.email.utils import send_email
|
||||
from config.config import get_learnhouse_config
|
||||
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 (
|
||||
Organization,
|
||||
OrganizationRead,
|
||||
)
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
|
|
@ -142,6 +144,7 @@ async def get_invite_codes(
|
|||
|
||||
return invite_codes_list
|
||||
|
||||
|
||||
async def get_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
|
|
@ -182,7 +185,6 @@ async def get_invite_code(
|
|||
detail="Could not connect to Redis",
|
||||
)
|
||||
|
||||
|
||||
# Get invite code
|
||||
invite_code = r.keys(f"org_invite_code_*:org:{org.org_uuid}:code:{invite_code}") # type: ignore
|
||||
|
||||
|
|
@ -198,7 +200,6 @@ async def get_invite_code(
|
|||
return invite_code
|
||||
|
||||
|
||||
|
||||
async def delete_invite_code(
|
||||
request: Request,
|
||||
org_id: int,
|
||||
|
|
@ -251,3 +252,57 @@ async def delete_invite_code(
|
|||
)
|
||||
|
||||
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
|
||||
from fastapi import HTTPException, Request
|
||||
from sqlmodel import Session, select
|
||||
from src.services.orgs.invites import send_invite_email
|
||||
from config.config import get_learnhouse_config
|
||||
from src.services.orgs.orgs import rbac_check
|
||||
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.organizations import (
|
||||
Organization,
|
||||
OrganizationRead,
|
||||
OrganizationUser,
|
||||
)
|
||||
|
||||
|
|
@ -234,6 +236,7 @@ async def invite_batch_users(
|
|||
request: Request,
|
||||
org_id: int,
|
||||
emails: str,
|
||||
invite_code_uuid: str,
|
||||
db_session: Session,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
):
|
||||
|
|
@ -258,6 +261,10 @@ async def invite_batch_users(
|
|||
detail="Organization not found",
|
||||
)
|
||||
|
||||
# get User sender
|
||||
statement = select(User).where(User.id == current_user.id)
|
||||
user = db_session.exec(statement).first()
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, org.org_uuid, current_user, "create", db_session)
|
||||
|
||||
|
|
@ -272,8 +279,8 @@ async def invite_batch_users(
|
|||
|
||||
invite_list = emails.split(",")
|
||||
|
||||
# invitations expire after 30 days
|
||||
ttl = int(timedelta(days=365).total_seconds())
|
||||
# invitations expire after 60 days
|
||||
ttl = int(timedelta(days=60).total_seconds())
|
||||
|
||||
for email in invite_list:
|
||||
email = email.strip()
|
||||
|
|
@ -286,11 +293,22 @@ async def invite_batch_users(
|
|||
# skip this user
|
||||
continue
|
||||
|
||||
org = OrganizationRead.from_orm(org)
|
||||
user = UserRead.from_orm(user)
|
||||
|
||||
isEmailSent = send_invite_email(
|
||||
org,
|
||||
invite_code_uuid,
|
||||
user,
|
||||
email,
|
||||
)
|
||||
|
||||
invited_user_object = {
|
||||
"email": email,
|
||||
"org_id": org.id,
|
||||
"invite_code_uuid": invite_code_uuid,
|
||||
"pending": True,
|
||||
"email_sent": False,
|
||||
"email_sent": isEmailSent,
|
||||
"expires": ttl,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"created_by": current_user.user_uuid,
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ async def create_user(
|
|||
|
||||
return user
|
||||
|
||||
|
||||
async def create_user_with_invite(
|
||||
request: Request,
|
||||
db_session: Session,
|
||||
|
|
@ -114,7 +115,9 @@ async def create_user_with_invite(
|
|||
):
|
||||
|
||||
# Check if invite code exists
|
||||
isInviteCodeCorrect = await get_invite_code(request, org_id, invite_code, current_user, db_session)
|
||||
isInviteCodeCorrect = await get_invite_code(
|
||||
request, org_id, invite_code, current_user, db_session
|
||||
)
|
||||
|
||||
if not isInviteCodeCorrect:
|
||||
raise HTTPException(
|
||||
|
|
@ -126,6 +129,7 @@ async def create_user_with_invite(
|
|||
|
||||
return user
|
||||
|
||||
|
||||
async def create_user_without_org(
|
||||
request: Request,
|
||||
db_session: Session,
|
||||
|
|
@ -201,6 +205,32 @@ async def update_user(
|
|||
# RBAC check
|
||||
await rbac_check(request, current_user, "update", user.user_uuid, db_session)
|
||||
|
||||
# Verifications
|
||||
|
||||
# Username
|
||||
statement = select(User).where(User.username == user_object.username)
|
||||
username_user = db_session.exec(statement).first()
|
||||
|
||||
if username_user:
|
||||
isSameUser = username_user.id == current_user.id
|
||||
if not isSameUser:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Username already exists",
|
||||
)
|
||||
|
||||
# Email
|
||||
statement = select(User).where(User.email == user_object.email)
|
||||
email_user = db_session.exec(statement).first()
|
||||
|
||||
if email_user:
|
||||
isSameUser = email_user.id == current_user.id
|
||||
if not isSameUser:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Email already exists",
|
||||
)
|
||||
|
||||
# Update user
|
||||
user_data = user_object.dict(exclude_unset=True)
|
||||
for key, value in user_data.items():
|
||||
|
|
@ -239,7 +269,9 @@ async def update_user_avatar(
|
|||
|
||||
# Upload thumbnail
|
||||
if avatar_file and avatar_file.filename:
|
||||
name_in_disk = f"{user.user_uuid}_avatar_{uuid4()}.{avatar_file.filename.split('.')[-1]}"
|
||||
name_in_disk = (
|
||||
f"{user.user_uuid}_avatar_{uuid4()}.{avatar_file.filename.split('.')[-1]}"
|
||||
)
|
||||
await upload_avatar(avatar_file, name_in_disk, user.user_uuid)
|
||||
|
||||
# Update course
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useSession } from '@components/Contexts/SessionContext'
|
|||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers'
|
||||
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess'
|
||||
import OrgUsersAdd from '@components/Dashboard/Users/OrgUsersAdd/OrgUsersAdd'
|
||||
|
||||
export type SettingsParams = {
|
||||
subpage: string
|
||||
|
|
@ -120,6 +121,7 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
|||
>
|
||||
{params.subpage == 'users' ? <OrgUsers /> : ''}
|
||||
{params.subpage == 'signups' ? <OrgAccess /> : ''}
|
||||
{params.subpage == 'add' ? <OrgUsersAdd /> : ''}
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ function UserEditPassword() {
|
|||
const session = useSession() as any
|
||||
|
||||
const updatePasswordUI = async (values: any) => {
|
||||
let user_id = session.user.user_id
|
||||
let user_id = session.user.id
|
||||
await updatePassword(user_id, values)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -216,13 +216,14 @@ function OrgAccess() {
|
|||
</tbody>
|
||||
</>
|
||||
</table>
|
||||
<button
|
||||
<div className='flex flex-row-reverse mt-3 mr-2'><button
|
||||
onClick={() => createInvite()}
|
||||
className="mt-3 mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||
className=" flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
<span> Create invite code</span>
|
||||
</button>
|
||||
<span> Generate invite code</span>
|
||||
</button></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
140
apps/web/components/Dashboard/Users/OrgUsersAdd/OrgUsersAdd.tsx
Normal file
140
apps/web/components/Dashboard/Users/OrgUsersAdd/OrgUsersAdd.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||
import Toast from '@components/StyledElements/Toast/Toast'
|
||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
import { inviteBatchUsers } from '@services/organizations/invites'
|
||||
import { swrFetcher } from '@services/utils/ts/requests'
|
||||
import { Info, Shield, UserPlus } from 'lucide-react'
|
||||
import React, { useEffect } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import useSWR, { mutate } from 'swr'
|
||||
|
||||
function OrgUsersAdd() {
|
||||
const org = useOrg() as any
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [invitedUsers, setInvitedUsers] = React.useState('');
|
||||
const [selectedInviteCode, setSelectedInviteCode] = React.useState('');
|
||||
|
||||
async function sendInvites() {
|
||||
setIsLoading(true)
|
||||
let res = await inviteBatchUsers(org.id, invitedUsers, selectedInviteCode)
|
||||
if (res.status == 200) {
|
||||
mutate(`${getAPIUrl()}orgs/${org?.id}/invites/users`)
|
||||
setIsLoading(false)
|
||||
} else {
|
||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const { data: invites } = useSWR(
|
||||
org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null,
|
||||
swrFetcher
|
||||
)
|
||||
const { data: invited_users } = useSWR(
|
||||
org ? `${getAPIUrl()}orgs/${org?.id}/invites/users` : null,
|
||||
swrFetcher
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (invites) {
|
||||
setSelectedInviteCode(invites?.[0]?.invite_code_uuid)
|
||||
}
|
||||
console.log('dev,',selectedInviteCode)
|
||||
}
|
||||
, [invites, invited_users])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toast></Toast>
|
||||
{!isLoading ? (
|
||||
<>
|
||||
<div className="h-6"></div>
|
||||
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 anit ">
|
||||
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 ">
|
||||
<h1 className="font-bold text-xl text-gray-800">Invite users to your Organization</h1>
|
||||
<h2 className="text-gray-500 text-md">
|
||||
{' '}
|
||||
Send invite via email, separated by comma{' '}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex space-x-2 mx-auto">
|
||||
<textarea
|
||||
onChange={(e) => setInvitedUsers(e.target.value)}
|
||||
className='w-full h-[200px] rounded-md border px-3 py-2 bg-gray-100/40 placeholder:italic placeholder:text-slate-300' placeholder='Example : spike.spiegel@bepop.space, michael.scott@dundermifflin.com' name="" id="" ></textarea>
|
||||
</div>
|
||||
<div className="flex space-x-2 mx-auto my-5 ml-2 items-center space-x-4 justify-between">
|
||||
|
||||
<div className='flex space-x-2 items-center'>
|
||||
<p className='flex items-center'>Invite Code </p>
|
||||
<select
|
||||
onChange={(e) => setSelectedInviteCode(e.target.value)}
|
||||
defaultValue={selectedInviteCode}
|
||||
className='text-gray-400 border rounded-md px-3 py-1' name="" id="">
|
||||
{invites?.map((invite: any) => (
|
||||
<option key={invite.invite_code_uuid} value={invite.invite_code_uuid}>{invite.invite_code}</option>
|
||||
))}
|
||||
</select>
|
||||
<ToolTip content={'Use one of the invite codes that you generated from the signup access page'} sideOffset={8} side="right"><Info className='text-gray-400' size={14} /></ToolTip>
|
||||
</div>
|
||||
<div className='flex flex-row-reverse '>
|
||||
<button
|
||||
onClick={sendInvites}
|
||||
className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||
>
|
||||
<UserPlus className="w-4 h-4" />
|
||||
<span>Send invites via email</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mt-3 mb-3 ">
|
||||
<h1 className="font-bold text-xl text-gray-800">
|
||||
Invited Users
|
||||
</h1>
|
||||
<h2 className="text-gray-500 text-md">
|
||||
{' '}
|
||||
Users who have been invited to join your organization{' '}
|
||||
</h2>
|
||||
</div>
|
||||
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
|
||||
<tr className="font-bolder text-sm">
|
||||
<th className="py-3 px-4">Email</th>
|
||||
<th className="py-3 px-4">Signup Status</th>
|
||||
<th className="py-3 px-4">Email sent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<>
|
||||
<tbody className="mt-5 bg-white rounded-md">
|
||||
{invited_users?.map((invited_user: any) => (
|
||||
<tr
|
||||
key={invited_user.email}
|
||||
className="border-b border-gray-100 text-sm"
|
||||
>
|
||||
<td className="py-3 px-4">{invited_user.email}</td>
|
||||
<td className="py-3 px-4">{invited_user.pending ? <div className='bg-orange-400 text-orange-100 w-fit px-2 py1 rounded-md'>Pending</div> : <div className='bg-green-400 text-green-100 w-fit px-2 py1 rounded-md'>Signed</div>}</td>
|
||||
<td className="py-3 px-4">{invited_user.email_sent ? <div className='bg-green-600 text-green-100 w-fit px-2 py1 rounded-md'>Sent</div> : <div className='bg-red-400 text-red-100 w-fit px-2 py1 rounded-md'>No</div>}</td>
|
||||
|
||||
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
</table>
|
||||
|
||||
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<PageLoading />
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgUsersAdd
|
||||
|
|
@ -42,3 +42,16 @@ export async function validateInviteCode(org_id: any, invite_code: string) {
|
|||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
|
||||
export async function inviteBatchUsers(
|
||||
org_id: any,
|
||||
emails: string,
|
||||
invite_code_uuid: string
|
||||
) {
|
||||
const result = await fetch(
|
||||
`${getAPIUrl()}orgs/${org_id}/invites/users/batch?emails=${emails}&invite_code_uuid=${invite_code_uuid}`,
|
||||
RequestBody('POST', null, null)
|
||||
)
|
||||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { RequestBody, errorHandling } from '@services/utils/ts/requests'
|
|||
|
||||
export async function updatePassword(user_id: string, data: any) {
|
||||
const result: any = await fetch(
|
||||
`${getAPIUrl()}users/password/user_id/` + user_id,
|
||||
`${getAPIUrl()}users/change_password/` + user_id,
|
||||
RequestBody('PUT', data, null)
|
||||
)
|
||||
const res = await errorHandling(result)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@
|
|||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||
"prepare": "husky"
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.51.0",
|
||||
"husky": "^9.0.10",
|
||||
"prettier": "^3.0.3",
|
||||
"turbo": "^1.10.15"
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue