Skip to content

Commit

Permalink
Forgot password feature (#3437)
Browse files Browse the repository at this point in the history
* forgot password feature

* improved config

* nit

* nit
  • Loading branch information
pablonyx authored Dec 20, 2024
1 parent 9011b8a commit aa4bfa2
Show file tree
Hide file tree
Showing 21 changed files with 415 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ jobs:
NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.POSTHOG_HOST }}
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
NEXT_PUBLIC_GTM_ENABLED=true
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=true
# needed due to weird interactions with the builds for different platforms
no-cache: true
labels: ${{ steps.meta.outputs.labels }}
Expand Down
80 changes: 80 additions & 0 deletions backend/onyx/auth/email_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from textwrap import dedent

from onyx.configs.app_configs import EMAIL_CONFIGURED
from onyx.configs.app_configs import EMAIL_FROM
from onyx.configs.app_configs import SMTP_PASS
from onyx.configs.app_configs import SMTP_PORT
from onyx.configs.app_configs import SMTP_SERVER
from onyx.configs.app_configs import SMTP_USER
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.db.models import User


def send_email(
user_email: str,
subject: str,
body: str,
mail_from: str = EMAIL_FROM,
) -> None:
if not EMAIL_CONFIGURED:
raise ValueError("Email is not configured.")

msg = MIMEMultipart()
msg["Subject"] = subject
msg["To"] = user_email
if mail_from:
msg["From"] = mail_from

msg.attach(MIMEText(body))

try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as s:
s.starttls()
s.login(SMTP_USER, SMTP_PASS)
s.send_message(msg)
except Exception as e:
raise e


def send_user_email_invite(user_email: str, current_user: User) -> None:
subject = "Invitation to Join Onyx Workspace"
body = dedent(
f"""\
Hello,
You have been invited to join a workspace on Onyx.
To join the workspace, please visit the following link:
{WEB_DOMAIN}/auth/login
Best regards,
The Onyx Team
"""
)
send_email(user_email, subject, body, current_user.email)


def send_forgot_password_email(
user_email: str,
token: str,
mail_from: str = EMAIL_FROM,
) -> None:
subject = "Onyx Forgot Password"
link = f"{WEB_DOMAIN}/auth/reset-password?token={token}"
body = f"Click the following link to reset your password: {link}"
send_email(user_email, subject, body, mail_from)


def send_user_verification_email(
user_email: str,
token: str,
mail_from: str = EMAIL_FROM,
) -> None:
subject = "Onyx Email Verification"
link = f"{WEB_DOMAIN}/auth/verify-email?token={token}"
body = f"Click the following link to verify your email address: {link}"
send_email(user_email, subject, body, mail_from)
49 changes: 13 additions & 36 deletions backend/onyx/auth/users.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import smtplib
import uuid
from collections.abc import AsyncGenerator
from datetime import datetime
from datetime import timezone
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import cast
from typing import Dict
from typing import List
Expand Down Expand Up @@ -53,19 +50,17 @@
from sqlalchemy.ext.asyncio import AsyncSession

from onyx.auth.api_key import get_hashed_api_key_from_request
from onyx.auth.email_utils import send_forgot_password_email
from onyx.auth.email_utils import send_user_verification_email
from onyx.auth.invited_users import get_invited_users
from onyx.auth.schemas import UserCreate
from onyx.auth.schemas import UserRole
from onyx.auth.schemas import UserUpdate
from onyx.configs.app_configs import AUTH_TYPE
from onyx.configs.app_configs import DISABLE_AUTH
from onyx.configs.app_configs import EMAIL_FROM
from onyx.configs.app_configs import EMAIL_CONFIGURED
from onyx.configs.app_configs import REQUIRE_EMAIL_VERIFICATION
from onyx.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS
from onyx.configs.app_configs import SMTP_PASS
from onyx.configs.app_configs import SMTP_PORT
from onyx.configs.app_configs import SMTP_SERVER
from onyx.configs.app_configs import SMTP_USER
from onyx.configs.app_configs import TRACK_EXTERNAL_IDP_EXPIRY
from onyx.configs.app_configs import USER_AUTH_SECRET
from onyx.configs.app_configs import VALID_EMAIL_DOMAINS
Expand Down Expand Up @@ -193,30 +188,6 @@ def verify_email_domain(email: str) -> None:
)


def send_user_verification_email(
user_email: str,
token: str,
mail_from: str = EMAIL_FROM,
) -> None:
msg = MIMEMultipart()
msg["Subject"] = "Onyx Email Verification"
msg["To"] = user_email
if mail_from:
msg["From"] = mail_from

link = f"{WEB_DOMAIN}/auth/verify-email?token={token}"

body = MIMEText(f"Click the following link to verify your email address: {link}")
msg.attach(body)

with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as s:
s.starttls()
# If credentials fails with gmail, check (You need an app password, not just the basic email password)
# https://support.google.com/accounts/answer/185833?sjid=8512343437447396151-NA
s.login(SMTP_USER, SMTP_PASS)
s.send_message(msg)


class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
reset_password_token_secret = USER_AUTH_SECRET
verification_token_secret = USER_AUTH_SECRET
Expand Down Expand Up @@ -506,7 +477,15 @@ async def on_after_register(
async def on_after_forgot_password(
self, user: User, token: str, request: Optional[Request] = None
) -> None:
logger.notice(f"User {user.id} has forgot their password. Reset token: {token}")
if not EMAIL_CONFIGURED:
logger.error(
"Email is not configured. Please configure email in the admin panel"
)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
"Your admin has not enbaled this feature.",
)
send_forgot_password_email(user.email, token)

async def on_after_request_verify(
self, user: User, token: str, request: Optional[Request] = None
Expand Down Expand Up @@ -624,9 +603,7 @@ def get_database_strategy(


auth_backend = AuthenticationBackend(
name="jwt" if MULTI_TENANT else "database",
transport=cookie_transport,
get_strategy=get_jwt_strategy if MULTI_TENANT else get_database_strategy, # type: ignore
name="jwt", transport=cookie_transport, get_strategy=get_jwt_strategy
) # type: ignore


Expand Down
1 change: 1 addition & 0 deletions backend/onyx/configs/app_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
SMTP_PORT = int(os.environ.get("SMTP_PORT") or "587")
SMTP_USER = os.environ.get("SMTP_USER", "[email protected]")
SMTP_PASS = os.environ.get("SMTP_PASS", "your-gmail-password")
EMAIL_CONFIGURED = all([SMTP_SERVER, SMTP_USER, SMTP_PASS])
EMAIL_FROM = os.environ.get("EMAIL_FROM") or SMTP_USER

# If set, Onyx will listen to the `expires_at` returned by the identity
Expand Down
2 changes: 1 addition & 1 deletion backend/onyx/server/manage/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from sqlalchemy.orm import Session

from ee.onyx.configs.app_configs import SUPER_USERS
from onyx.auth.email_utils import send_user_email_invite
from onyx.auth.invited_users import get_invited_users
from onyx.auth.invited_users import write_invited_users
from onyx.auth.noauth_user import fetch_no_auth_user
Expand Down Expand Up @@ -61,7 +62,6 @@
from onyx.server.models import InvitedUserSnapshot
from onyx.server.models import MinimalUserSnapshot
from onyx.server.utils import BasicAuthenticationError
from onyx.server.utils import send_user_email_invite
from onyx.utils.logger import setup_logger
from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
from shared_configs.configs import MULTI_TENANT
Expand Down
39 changes: 0 additions & 39 deletions backend/onyx/server/utils.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import json
import smtplib
from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from textwrap import dedent
from typing import Any

from fastapi import HTTPException
from fastapi import status

from onyx.configs.app_configs import SMTP_PASS
from onyx.configs.app_configs import SMTP_PORT
from onyx.configs.app_configs import SMTP_SERVER
from onyx.configs.app_configs import SMTP_USER
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.db.models import User


class BasicAuthenticationError(HTTPException):
def __init__(self, detail: str):
Expand Down Expand Up @@ -62,31 +51,3 @@ def mask_credential_dict(credential_dict: dict[str, Any]) -> dict[str, str]:

masked_creds[key] = mask_string(val)
return masked_creds


def send_user_email_invite(user_email: str, current_user: User) -> None:
msg = MIMEMultipart()
msg["Subject"] = "Invitation to Join Onyx Workspace"
msg["From"] = current_user.email
msg["To"] = user_email

email_body = dedent(
f"""\
Hello,
You have been invited to join a workspace on Onyx.
To join the workspace, please visit the following link:
{WEB_DOMAIN}/auth/login
Best regards,
The Onyx Team
"""
)

msg.attach(MIMEText(email_body, "plain"))
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp_server:
smtp_server.starttls()
smtp_server.login(SMTP_USER, SMTP_PASS)
smtp_server.send_message(msg)
2 changes: 1 addition & 1 deletion deployment/docker_compose/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ services:
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
- NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN=${NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN:-}

- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
# Enterprise Edition only
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
# DO NOT TURN ON unless you have EXPLICIT PERMISSION from Onyx.
Expand Down
1 change: 1 addition & 0 deletions deployment/docker_compose/docker-compose.prod-cloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ services:
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
depends_on:
- api_server
restart: always
Expand Down
1 change: 1 addition & 0 deletions deployment/docker_compose/docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ services:
- NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-}
- NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-}
- NEXT_PUBLIC_THEME=${NEXT_PUBLIC_THEME:-}
- NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED:-}
depends_on:
- api_server
restart: always
Expand Down
6 changes: 6 additions & 0 deletions web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
ARG NEXT_PUBLIC_GTM_ENABLED
ENV NEXT_PUBLIC_GTM_ENABLED=${NEXT_PUBLIC_GTM_ENABLED}

ARG NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED
ENV NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED}

RUN npx next build

# Step 2. Production image, copy all the files and run next
Expand Down Expand Up @@ -150,6 +153,9 @@ ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
ARG NEXT_PUBLIC_GTM_ENABLED
ENV NEXT_PUBLIC_GTM_ENABLED=${NEXT_PUBLIC_GTM_ENABLED}

ARG NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED
ENV NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED}

# Note: Don't expose ports here, Compose will handle that for us if necessary.
# If you want to run this without compose, specify the ports to
# expose via cli
Expand Down
6 changes: 3 additions & 3 deletions web/src/app/admin/configuration/llm/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ export const getProviderIcon = (providerName: string, modelName?: string) => {
}
if (modelName?.toLowerCase().includes("phi")) {
return MicrosoftIconSVG;
}
}
if (modelName?.toLowerCase().includes("mistral")) {
return MistralIcon;
}
}
if (modelName?.toLowerCase().includes("llama")) {
return MetaIcon;
}
}
if (modelName?.toLowerCase().includes("gemini")) {
return GeminiIcon;
}
Expand Down
Loading

0 comments on commit aa4bfa2

Please sign in to comment.