diff --git a/backend/alembic/versions/91ffac7e65b3_add_expiry_time.py b/backend/alembic/versions/91ffac7e65b3_add_expiry_time.py new file mode 100644 index 000000000..9ec875e5b --- /dev/null +++ b/backend/alembic/versions/91ffac7e65b3_add_expiry_time.py @@ -0,0 +1,27 @@ +"""add expiry time +Revision ID: 91ffac7e65b3 +Revises: bc9771dccadf +Create Date: 2024-06-24 09:39:56.462242 +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "91ffac7e65b3" +down_revision = "795b20b85b4b" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "user", sa.Column("oidc_expiry", sa.DateTime(timezone=True), nullable=True) + ) + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("user", "oidc_expiry") + # ### end Alembic commands ### diff --git a/backend/danswer/auth/users.py b/backend/danswer/auth/users.py index 06c0841d8..d674f560f 100644 --- a/backend/danswer/auth/users.py +++ b/backend/danswer/auth/users.py @@ -1,6 +1,8 @@ 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 Optional @@ -173,7 +175,7 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): verify_email_in_whitelist(account_email) verify_email_domain(account_email) - return await super().oauth_callback( # type: ignore + user = await super().oauth_callback( # type: ignore oauth_name=oauth_name, access_token=access_token, account_id=account_id, @@ -185,6 +187,14 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): is_verified_by_default=is_verified_by_default, ) + # NOTE: google oauth expires after 1hr. We don't want to force the user to + # re-authenticate that frequently, so for now we'll just ignore this for + # google oauth users + if expires_at and AUTH_TYPE != AuthType.GOOGLE_OAUTH: + oidc_expiry = datetime.fromtimestamp(expires_at, tz=timezone.utc) + await self.user_db.update(user, update_dict={"oidc_expiry": oidc_expiry}) + return user + async def on_after_register( self, user: User, request: Optional[Request] = None ) -> None: @@ -227,10 +237,12 @@ cookie_transport = CookieTransport( def get_database_strategy( access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db), ) -> DatabaseStrategy: - return DatabaseStrategy( + strategy = DatabaseStrategy( access_token_db, lifetime_seconds=SESSION_EXPIRE_TIME_SECONDS # type: ignore ) + return strategy + auth_backend = AuthenticationBackend( name="database", diff --git a/backend/danswer/db/models.py b/backend/danswer/db/models.py index b61f81f2d..cd752b4cd 100644 --- a/backend/danswer/db/models.py +++ b/backend/danswer/db/models.py @@ -11,6 +11,7 @@ from uuid import UUID from fastapi_users_db_sqlalchemy import SQLAlchemyBaseOAuthAccountTableUUID from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTableUUID from fastapi_users_db_sqlalchemy.access_token import SQLAlchemyBaseAccessTokenTableUUID +from fastapi_users_db_sqlalchemy.generics import TIMESTAMPAware from sqlalchemy import Boolean from sqlalchemy import DateTime from sqlalchemy import Enum @@ -120,6 +121,10 @@ class User(SQLAlchemyBaseUserTableUUID, Base): postgresql.ARRAY(Integer), nullable=True ) + oidc_expiry: Mapped[datetime.datetime] = mapped_column( + TIMESTAMPAware(timezone=True), nullable=True + ) + # relationships credentials: Mapped[list["Credential"]] = relationship( "Credential", back_populates="user", lazy="joined" diff --git a/backend/danswer/server/manage/models.py b/backend/danswer/server/manage/models.py index 80a2728c6..ee8a6ed6e 100644 --- a/backend/danswer/server/manage/models.py +++ b/backend/danswer/server/manage/models.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Any from typing import TYPE_CHECKING @@ -14,13 +15,15 @@ from danswer.db.models import SlackBotConfig as SlackBotConfigModel from danswer.db.models import SlackBotResponseType from danswer.db.models import StandardAnswer as StandardAnswerModel from danswer.db.models import StandardAnswerCategory as StandardAnswerCategoryModel +from danswer.db.models import User from danswer.indexing.models import EmbeddingModelDetail from danswer.server.features.persona.models import PersonaSnapshot from danswer.server.models import FullUserSnapshot from danswer.server.models import InvitedUserSnapshot + if TYPE_CHECKING: - from danswer.db.models import User as UserModel + pass class VersionResponse(BaseModel): @@ -46,9 +49,17 @@ class UserInfo(BaseModel): is_verified: bool role: UserRole preferences: UserPreferences + oidc_expiry: datetime | None = None + current_token_created_at: datetime | None = None + current_token_expiry_length: int | None = None @classmethod - def from_model(cls, user: "UserModel") -> "UserInfo": + def from_model( + cls, + user: User, + current_token_created_at: datetime | None = None, + expiry_length: int | None = None, + ) -> "UserInfo": return cls( id=str(user.id), email=user.email, @@ -57,6 +68,9 @@ class UserInfo(BaseModel): is_verified=user.is_verified, role=user.role, preferences=(UserPreferences(chosen_assistants=user.chosen_assistants)), + oidc_expiry=user.oidc_expiry, + current_token_created_at=current_token_created_at, + current_token_expiry_length=expiry_length, ) @@ -151,7 +165,9 @@ class SlackBotConfigCreationRequest(BaseModel): # by an optional `PersonaSnapshot` object. Keeping it like this # for now for simplicity / speed of development document_sets: list[int] | None - persona_id: int | None # NOTE: only one of `document_sets` / `persona_id` should be set + persona_id: ( + int | None + ) # NOTE: only one of `document_sets` / `persona_id` should be set channel_names: list[str] respond_tag_only: bool = False respond_to_bots: bool = False diff --git a/backend/danswer/server/manage/users.py b/backend/danswer/server/manage/users.py index c63546991..133e0b944 100644 --- a/backend/danswer/server/manage/users.py +++ b/backend/danswer/server/manage/users.py @@ -1,4 +1,5 @@ import re +from datetime import datetime from fastapi import APIRouter from fastapi import Body @@ -6,6 +7,9 @@ from fastapi import Depends from fastapi import HTTPException from fastapi import status from pydantic import BaseModel +from sqlalchemy import Column +from sqlalchemy import desc +from sqlalchemy import select from sqlalchemy import update from sqlalchemy.orm import Session @@ -19,9 +23,11 @@ from danswer.auth.users import current_admin_user from danswer.auth.users import current_user from danswer.auth.users import optional_user from danswer.configs.app_configs import AUTH_TYPE +from danswer.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS from danswer.configs.app_configs import VALID_EMAIL_DOMAINS from danswer.configs.constants import AuthType from danswer.db.engine import get_session +from danswer.db.models import AccessToken from danswer.db.models import User from danswer.db.users import get_user_by_email from danswer.db.users import list_users @@ -117,9 +123,9 @@ def list_all_users( id=user.id, email=user.email, role=user.role, - status=UserStatus.LIVE - if user.is_active - else UserStatus.DEACTIVATED, + status=( + UserStatus.LIVE if user.is_active else UserStatus.DEACTIVATED + ), ) for user in users ], @@ -246,9 +252,35 @@ async def get_user_role(user: User = Depends(current_user)) -> UserRoleResponse: return UserRoleResponse(role=user.role) +def get_current_token_creation( + user: User | None, db_session: Session +) -> datetime | None: + if user is None: + return None + try: + result = db_session.execute( + select(AccessToken) + .where(AccessToken.user_id == user.id) # type: ignore + .order_by(desc(Column("created_at"))) + .limit(1) + ) + access_token = result.scalar_one_or_none() + + if access_token: + return access_token.created_at + else: + logger.error("No AccessToken found for user") + return None + + except Exception as e: + logger.error(f"Error fetching AccessToken: {e}") + return None + + @router.get("/me") def verify_user_logged_in( user: User | None = Depends(optional_user), + db_session: Session = Depends(get_session), ) -> UserInfo: # NOTE: this does not use `current_user` / `current_admin_user` because we don't want # to enforce user verification here - the frontend always wants to get the info about @@ -264,7 +296,14 @@ def verify_user_logged_in( status_code=status.HTTP_403_FORBIDDEN, detail="User Not Authenticated" ) - return UserInfo.from_model(user) + token_created_at = get_current_token_creation(user, db_session) + user_info = UserInfo.from_model( + user, + current_token_created_at=token_created_at, + expiry_length=SESSION_EXPIRE_TIME_SECONDS, + ) + + return user_info """APIs to adjust user preferences""" diff --git a/deployment/docker_compose/docker-compose.dev.yml b/deployment/docker_compose/docker-compose.dev.yml index 7b373cf38..7b3dd3919 100644 --- a/deployment/docker_compose/docker-compose.dev.yml +++ b/deployment/docker_compose/docker-compose.dev.yml @@ -25,8 +25,8 @@ services: - GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID:-} - GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET:-} - REQUIRE_EMAIL_VERIFICATION=${REQUIRE_EMAIL_VERIFICATION:-} - - SMTP_SERVER=${SMTP_SERVER:-} # For sending verification emails, if unspecified then defaults to 'smtp.gmail.com' - - SMTP_PORT=${SMTP_PORT:-587} # For sending verification emails, if unspecified then defaults to '587' + - SMTP_SERVER=${SMTP_SERVER:-} # For sending verification emails, if unspecified then defaults to 'smtp.gmail.com' + - SMTP_PORT=${SMTP_PORT:-587} # For sending verification emails, if unspecified then defaults to '587' - SMTP_USER=${SMTP_USER:-} - SMTP_PASS=${SMTP_PASS:-} - EMAIL_FROM=${EMAIL_FROM:-} @@ -59,8 +59,8 @@ services: - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-} - AWS_REGION_NAME=${AWS_REGION_NAME:-} # Query Options - - DOC_TIME_DECAY=${DOC_TIME_DECAY:-} # Recency Bias for search results, decay at 1 / (1 + DOC_TIME_DECAY * x years) - - HYBRID_ALPHA=${HYBRID_ALPHA:-} # Hybrid Search Alpha (0 for entirely keyword, 1 for entirely vector) + - DOC_TIME_DECAY=${DOC_TIME_DECAY:-} # Recency Bias for search results, decay at 1 / (1 + DOC_TIME_DECAY * x years) + - HYBRID_ALPHA=${HYBRID_ALPHA:-} # Hybrid Search Alpha (0 for entirely keyword, 1 for entirely vector) - EDIT_KEYWORD_QUERY=${EDIT_KEYWORD_QUERY:-} - MULTILINGUAL_QUERY_EXPANSION=${MULTILINGUAL_QUERY_EXPANSION:-} - LANGUAGE_HINT=${LANGUAGE_HINT:-} @@ -69,7 +69,7 @@ services: # Other services - POSTGRES_HOST=relational_db - VESPA_HOST=index - - WEB_DOMAIN=${WEB_DOMAIN:-} # For frontend redirect auth purpose + - WEB_DOMAIN=${WEB_DOMAIN:-} # For frontend redirect auth purpose # Don't change the NLP model configs unless you know what you're doing - DOCUMENT_ENCODER_MODEL=${DOCUMENT_ENCODER_MODEL:-} - DOC_EMBEDDING_DIM=${DOC_EMBEDDING_DIM:-} @@ -104,7 +104,6 @@ services: max-size: "50m" max-file: "6" - background: image: danswer/danswer-backend:latest build: @@ -139,8 +138,8 @@ services: - LITELLM_EXTRA_HEADERS=${LITELLM_EXTRA_HEADERS:-} - BING_API_KEY=${BING_API_KEY:-} # Query Options - - DOC_TIME_DECAY=${DOC_TIME_DECAY:-} # Recency Bias for search results, decay at 1 / (1 + DOC_TIME_DECAY * x years) - - HYBRID_ALPHA=${HYBRID_ALPHA:-} # Hybrid Search Alpha (0 for entirely keyword, 1 for entirely vector) + - DOC_TIME_DECAY=${DOC_TIME_DECAY:-} # Recency Bias for search results, decay at 1 / (1 + DOC_TIME_DECAY * x years) + - HYBRID_ALPHA=${HYBRID_ALPHA:-} # Hybrid Search Alpha (0 for entirely keyword, 1 for entirely vector) - EDIT_KEYWORD_QUERY=${EDIT_KEYWORD_QUERY:-} - MULTILINGUAL_QUERY_EXPANSION=${MULTILINGUAL_QUERY_EXPANSION:-} - LANGUAGE_HINT=${LANGUAGE_HINT:-} @@ -152,12 +151,12 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-} - POSTGRES_DB=${POSTGRES_DB:-} - VESPA_HOST=index - - WEB_DOMAIN=${WEB_DOMAIN:-} # For frontend redirect auth purpose for OAuth2 connectors + - WEB_DOMAIN=${WEB_DOMAIN:-} # For frontend redirect auth purpose for OAuth2 connectors # Don't change the NLP model configs unless you know what you're doing - DOCUMENT_ENCODER_MODEL=${DOCUMENT_ENCODER_MODEL:-} - DOC_EMBEDDING_DIM=${DOC_EMBEDDING_DIM:-} - NORMALIZE_EMBEDDINGS=${NORMALIZE_EMBEDDINGS:-} - - ASYM_QUERY_PREFIX=${ASYM_QUERY_PREFIX:-} # Needed by DanswerBot + - ASYM_QUERY_PREFIX=${ASYM_QUERY_PREFIX:-} # Needed by DanswerBot - ASYM_PASSAGE_PREFIX=${ASYM_PASSAGE_PREFIX:-} - MODEL_SERVER_HOST=${MODEL_SERVER_HOST:-inference_model_server} - MODEL_SERVER_PORT=${MODEL_SERVER_PORT:-} @@ -183,7 +182,7 @@ services: - DANSWER_BOT_FEEDBACK_VISIBILITY=${DANSWER_BOT_FEEDBACK_VISIBILITY:-} - DANSWER_BOT_DISPLAY_ERROR_MSGS=${DANSWER_BOT_DISPLAY_ERROR_MSGS:-} - DANSWER_BOT_RESPOND_EVERY_CHANNEL=${DANSWER_BOT_RESPOND_EVERY_CHANNEL:-} - - DANSWER_BOT_DISABLE_COT=${DANSWER_BOT_DISABLE_COT:-} # Currently unused + - DANSWER_BOT_DISABLE_COT=${DANSWER_BOT_DISABLE_COT:-} # Currently unused - NOTIFY_SLACKBOT_NO_ANSWER=${NOTIFY_SLACKBOT_NO_ANSWER:-} - DANSWER_BOT_MAX_QPM=${DANSWER_BOT_MAX_QPM:-} - DANSWER_BOT_MAX_WAIT_TIME=${DANSWER_BOT_MAX_WAIT_TIME:-} @@ -207,7 +206,6 @@ services: max-size: "50m" max-file: "6" - web_server: image: danswer/danswer-web-server:latest build: @@ -237,7 +235,6 @@ services: # Enterprise Edition only - ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=${ENABLE_PAID_ENTERPRISE_EDITION_FEATURES:-false} - inference_model_server: image: danswer/danswer-model-server:latest build: @@ -264,7 +261,6 @@ services: max-size: "50m" max-file: "6" - indexing_model_server: image: danswer/danswer-model-server:latest build: @@ -292,7 +288,6 @@ services: max-size: "50m" max-file: "6" - relational_db: image: postgres:15.2-alpine restart: always @@ -304,7 +299,6 @@ services: volumes: - db_volume:/var/lib/postgresql/data - # This container name cannot have an underscore in it due to Vespa expectations of the URL index: image: vespaengine/vespa:8.277.17 @@ -320,7 +314,6 @@ services: max-size: "50m" max-file: "6" - nginx: image: nginx:1.23.4-alpine restart: always @@ -333,7 +326,7 @@ services: - DOMAIN=localhost ports: - "80:80" - - "3000:80" # allow for localhost:3000 usage, since that is the norm + - "3000:80" # allow for localhost:3000 usage, since that is the norm volumes: - ../data/nginx:/etc/nginx/conf.d logging: @@ -350,10 +343,9 @@ services: /bin/sh -c "dos2unix /etc/nginx/conf.d/run-nginx.sh && /etc/nginx/conf.d/run-nginx.sh app.conf.template.dev" - volumes: db_volume: - vespa_volume: - # Created by the container itself + vespa_volume: # Created by the container itself + model_cache_huggingface: indexing_huggingface_model_cache: diff --git a/web/src/app/admin/connector/[ccPairId]/page.tsx b/web/src/app/admin/connector/[ccPairId]/page.tsx index 7e613461b..5ea2e840e 100644 --- a/web/src/app/admin/connector/[ccPairId]/page.tsx +++ b/web/src/app/admin/connector/[ccPairId]/page.tsx @@ -136,10 +136,6 @@ export default function Page({ params }: { params: { ccPairId: string } }) { return (
-
- -
-
); diff --git a/web/src/app/admin/connectors/axero/page.tsx b/web/src/app/admin/connectors/axero/page.tsx index 6d4a5af8b..3b7b7b380 100644 --- a/web/src/app/admin/connectors/axero/page.tsx +++ b/web/src/app/admin/connectors/axero/page.tsx @@ -248,10 +248,6 @@ const MainSection = () => { export default function Page() { return (
-
- -
- } title="Axero" /> diff --git a/web/src/app/admin/connectors/bookstack/page.tsx b/web/src/app/admin/connectors/bookstack/page.tsx index dbf8bd367..d2e70e1b5 100644 --- a/web/src/app/admin/connectors/bookstack/page.tsx +++ b/web/src/app/admin/connectors/bookstack/page.tsx @@ -249,10 +249,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Bookstack" />
diff --git a/web/src/app/admin/connectors/clickup/page.tsx b/web/src/app/admin/connectors/clickup/page.tsx index 6a868bac7..3cf389952 100644 --- a/web/src/app/admin/connectors/clickup/page.tsx +++ b/web/src/app/admin/connectors/clickup/page.tsx @@ -331,10 +331,6 @@ const MainSection = () => { export default function Page() { return (
-
- -
- } title="Clickup" /> diff --git a/web/src/app/admin/connectors/confluence/page.tsx b/web/src/app/admin/connectors/confluence/page.tsx index 981d63e95..1276c6ad4 100644 --- a/web/src/app/admin/connectors/confluence/page.tsx +++ b/web/src/app/admin/connectors/confluence/page.tsx @@ -330,10 +330,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Confluence" />
diff --git a/web/src/app/admin/connectors/discourse/page.tsx b/web/src/app/admin/connectors/discourse/page.tsx index 9ba840d3d..a33fd5b79 100644 --- a/web/src/app/admin/connectors/discourse/page.tsx +++ b/web/src/app/admin/connectors/discourse/page.tsx @@ -273,10 +273,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Discourse" />
diff --git a/web/src/app/admin/connectors/document360/page.tsx b/web/src/app/admin/connectors/document360/page.tsx index 85653c639..c5de91ea7 100644 --- a/web/src/app/admin/connectors/document360/page.tsx +++ b/web/src/app/admin/connectors/document360/page.tsx @@ -262,10 +262,6 @@ const MainSection = () => { export default function Page() { return (
-
- -
- } title="Document360" diff --git a/web/src/app/admin/connectors/dropbox/page.tsx b/web/src/app/admin/connectors/dropbox/page.tsx index a897a3407..a75696d03 100644 --- a/web/src/app/admin/connectors/dropbox/page.tsx +++ b/web/src/app/admin/connectors/dropbox/page.tsx @@ -212,9 +212,7 @@ const Main = () => { export default function Page() { return (
-
- -
+ {" "} } title="Dropbox" />
diff --git a/web/src/app/admin/connectors/file/page.tsx b/web/src/app/admin/connectors/file/page.tsx index b2857f66c..5b94b0596 100644 --- a/web/src/app/admin/connectors/file/page.tsx +++ b/web/src/app/admin/connectors/file/page.tsx @@ -287,10 +287,6 @@ const Main = () => { export default function File() { return (
-
- -
- } title="File" />
diff --git a/web/src/app/admin/connectors/github/page.tsx b/web/src/app/admin/connectors/github/page.tsx index 6d7e915d3..86c12357f 100644 --- a/web/src/app/admin/connectors/github/page.tsx +++ b/web/src/app/admin/connectors/github/page.tsx @@ -265,10 +265,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Github PRs + Issues" diff --git a/web/src/app/admin/connectors/gitlab/page.tsx b/web/src/app/admin/connectors/gitlab/page.tsx index 595cd575f..ff9905cba 100644 --- a/web/src/app/admin/connectors/gitlab/page.tsx +++ b/web/src/app/admin/connectors/gitlab/page.tsx @@ -250,10 +250,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Gitlab MRs + Issues" diff --git a/web/src/app/admin/connectors/gmail/page.tsx b/web/src/app/admin/connectors/gmail/page.tsx index f0800d293..cde90c372 100644 --- a/web/src/app/admin/connectors/gmail/page.tsx +++ b/web/src/app/admin/connectors/gmail/page.tsx @@ -264,10 +264,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Gmail" />
diff --git a/web/src/app/admin/connectors/gong/page.tsx b/web/src/app/admin/connectors/gong/page.tsx index 5fda45d51..f0fbbf55c 100644 --- a/web/src/app/admin/connectors/gong/page.tsx +++ b/web/src/app/admin/connectors/gong/page.tsx @@ -257,10 +257,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Gong" />
diff --git a/web/src/app/admin/connectors/google-drive/page.tsx b/web/src/app/admin/connectors/google-drive/page.tsx index 121d8af9d..1eed294bb 100644 --- a/web/src/app/admin/connectors/google-drive/page.tsx +++ b/web/src/app/admin/connectors/google-drive/page.tsx @@ -412,10 +412,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Google Drive" diff --git a/web/src/app/admin/connectors/google-sites/page.tsx b/web/src/app/admin/connectors/google-sites/page.tsx index 20728633a..371e676d0 100644 --- a/web/src/app/admin/connectors/google-sites/page.tsx +++ b/web/src/app/admin/connectors/google-sites/page.tsx @@ -50,10 +50,6 @@ export default function GoogleSites() { {popup} {filesAreUploading && }
-
- -
- } title="Google Sites" diff --git a/web/src/app/admin/connectors/google-storage/page.tsx b/web/src/app/admin/connectors/google-storage/page.tsx index a836df21f..ec9d5396e 100644 --- a/web/src/app/admin/connectors/google-storage/page.tsx +++ b/web/src/app/admin/connectors/google-storage/page.tsx @@ -244,9 +244,7 @@ const GCSMain = () => { export default function Page() { return (
-
- -
+ {" "} } title="Google Cloud Storage" diff --git a/web/src/app/admin/connectors/guru/page.tsx b/web/src/app/admin/connectors/guru/page.tsx index 094bbe7c7..df7f7514a 100644 --- a/web/src/app/admin/connectors/guru/page.tsx +++ b/web/src/app/admin/connectors/guru/page.tsx @@ -232,10 +232,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Guru" />
diff --git a/web/src/app/admin/connectors/hubspot/page.tsx b/web/src/app/admin/connectors/hubspot/page.tsx index 199c027b6..218e1cf24 100644 --- a/web/src/app/admin/connectors/hubspot/page.tsx +++ b/web/src/app/admin/connectors/hubspot/page.tsx @@ -220,10 +220,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="HubSpot" />
diff --git a/web/src/app/admin/connectors/jira/page.tsx b/web/src/app/admin/connectors/jira/page.tsx index f960348e6..6948364c0 100644 --- a/web/src/app/admin/connectors/jira/page.tsx +++ b/web/src/app/admin/connectors/jira/page.tsx @@ -362,10 +362,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Jira" />
diff --git a/web/src/app/admin/connectors/linear/page.tsx b/web/src/app/admin/connectors/linear/page.tsx index 6af018729..a0c004263 100644 --- a/web/src/app/admin/connectors/linear/page.tsx +++ b/web/src/app/admin/connectors/linear/page.tsx @@ -224,10 +224,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Linear" />
diff --git a/web/src/app/admin/connectors/loopio/page.tsx b/web/src/app/admin/connectors/loopio/page.tsx index 920d15b82..23420ca12 100644 --- a/web/src/app/admin/connectors/loopio/page.tsx +++ b/web/src/app/admin/connectors/loopio/page.tsx @@ -250,9 +250,7 @@ const Main = () => { export default function Page() { return (
-
- -
+ {" "}

Loopio

diff --git a/web/src/app/admin/connectors/mediawiki/page.tsx b/web/src/app/admin/connectors/mediawiki/page.tsx index e0c17a6e7..7da14b394 100644 --- a/web/src/app/admin/connectors/mediawiki/page.tsx +++ b/web/src/app/admin/connectors/mediawiki/page.tsx @@ -207,10 +207,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="MediaWiki" />
diff --git a/web/src/app/admin/connectors/notion/page.tsx b/web/src/app/admin/connectors/notion/page.tsx index aa205dce0..f59cc5634 100644 --- a/web/src/app/admin/connectors/notion/page.tsx +++ b/web/src/app/admin/connectors/notion/page.tsx @@ -260,10 +260,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Notion" />
diff --git a/web/src/app/admin/connectors/oracle-storage/page.tsx b/web/src/app/admin/connectors/oracle-storage/page.tsx index 34847a4b9..9f26ff10a 100644 --- a/web/src/app/admin/connectors/oracle-storage/page.tsx +++ b/web/src/app/admin/connectors/oracle-storage/page.tsx @@ -259,9 +259,7 @@ const OCIMain = () => { export default function Page() { return (
-
- -
+ {" "} } title="Oracle Cloud Infrastructure" diff --git a/web/src/app/admin/connectors/productboard/page.tsx b/web/src/app/admin/connectors/productboard/page.tsx index 1694baa8c..c0445a40b 100644 --- a/web/src/app/admin/connectors/productboard/page.tsx +++ b/web/src/app/admin/connectors/productboard/page.tsx @@ -239,10 +239,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Productboard" diff --git a/web/src/app/admin/connectors/r2/page.tsx b/web/src/app/admin/connectors/r2/page.tsx index 372660acc..4b1362efd 100644 --- a/web/src/app/admin/connectors/r2/page.tsx +++ b/web/src/app/admin/connectors/r2/page.tsx @@ -255,9 +255,7 @@ export default function Page() { return (
-
- -
+ {" "} } title="R2 Storage" />
diff --git a/web/src/app/admin/connectors/request-tracker/page.tsx b/web/src/app/admin/connectors/request-tracker/page.tsx index 147dd1ae2..b83bd87d4 100644 --- a/web/src/app/admin/connectors/request-tracker/page.tsx +++ b/web/src/app/admin/connectors/request-tracker/page.tsx @@ -241,10 +241,6 @@ const MainSection = () => { export default function Page() { return (
-
- -
- } title="Request Tracker" diff --git a/web/src/app/admin/connectors/s3/page.tsx b/web/src/app/admin/connectors/s3/page.tsx index 81064a70b..5044ed820 100644 --- a/web/src/app/admin/connectors/s3/page.tsx +++ b/web/src/app/admin/connectors/s3/page.tsx @@ -247,11 +247,8 @@ export default function Page() { return (
-
- -
+ {" "} } title="S3 Storage" /> -
); diff --git a/web/src/app/admin/connectors/salesforce/page.tsx b/web/src/app/admin/connectors/salesforce/page.tsx index 8771b14f9..5566c914d 100644 --- a/web/src/app/admin/connectors/salesforce/page.tsx +++ b/web/src/app/admin/connectors/salesforce/page.tsx @@ -278,10 +278,6 @@ const MainSection = () => { export default function Page() { return (
-
- -
- } title="Salesforce" /> diff --git a/web/src/app/admin/connectors/sharepoint/page.tsx b/web/src/app/admin/connectors/sharepoint/page.tsx index bf9704154..e1a14f722 100644 --- a/web/src/app/admin/connectors/sharepoint/page.tsx +++ b/web/src/app/admin/connectors/sharepoint/page.tsx @@ -282,10 +282,6 @@ const MainSection = () => { export default function Page() { return (
-
- -
- } title="Sharepoint" /> diff --git a/web/src/app/admin/connectors/slab/page.tsx b/web/src/app/admin/connectors/slab/page.tsx index 11dcd799e..ae02f4240 100644 --- a/web/src/app/admin/connectors/slab/page.tsx +++ b/web/src/app/admin/connectors/slab/page.tsx @@ -270,10 +270,6 @@ const Main = () => { export default function Page() { return (
-
- -
- } title="Slab" />
diff --git a/web/src/app/admin/connectors/slack/page.tsx b/web/src/app/admin/connectors/slack/page.tsx index 9352b0d77..36cea2617 100644 --- a/web/src/app/admin/connectors/slack/page.tsx +++ b/web/src/app/admin/connectors/slack/page.tsx @@ -284,10 +284,6 @@ const MainSection = () => { export default function Page() { return (
-
- -
- } title="Slack" /> diff --git a/web/src/app/admin/connectors/teams/page.tsx b/web/src/app/admin/connectors/teams/page.tsx index 530d430ab..9c02738e1 100644 --- a/web/src/app/admin/connectors/teams/page.tsx +++ b/web/src/app/admin/connectors/teams/page.tsx @@ -263,10 +263,6 @@ const MainSection = () => { export default function Page() { return (
-
- -
- } title="Teams" /> diff --git a/web/src/app/admin/connectors/web/page.tsx b/web/src/app/admin/connectors/web/page.tsx index 06e2a000e..3bce943b8 100644 --- a/web/src/app/admin/connectors/web/page.tsx +++ b/web/src/app/admin/connectors/web/page.tsx @@ -48,10 +48,6 @@ export default function Web() { return (
-
- -
- } title="Web" /> diff --git a/web/src/app/admin/connectors/wikipedia/page.tsx b/web/src/app/admin/connectors/wikipedia/page.tsx index f410b209a..a4890e715 100644 --- a/web/src/app/admin/connectors/wikipedia/page.tsx +++ b/web/src/app/admin/connectors/wikipedia/page.tsx @@ -202,10 +202,6 @@ const Main = () => { export default function Page() { return ( <div className="mx-auto container"> - <div className="mb-4"> - <HealthCheckBanner /> - </div> - <AdminPageTitle icon={<WikipediaIcon size={32} />} title="Wikipedia" /> <Main /> diff --git a/web/src/app/admin/connectors/zendesk/page.tsx b/web/src/app/admin/connectors/zendesk/page.tsx index dac1fe76e..e1bfbebb8 100644 --- a/web/src/app/admin/connectors/zendesk/page.tsx +++ b/web/src/app/admin/connectors/zendesk/page.tsx @@ -242,10 +242,6 @@ const Main = () => { export default function Page() { return ( <div className="mx-auto container"> - <div className="mb-4"> - <HealthCheckBanner /> - </div> - <AdminPageTitle icon={<ZendeskIcon size={32} />} title="Zendesk" /> <Main /> diff --git a/web/src/app/admin/connectors/zulip/page.tsx b/web/src/app/admin/connectors/zulip/page.tsx index 66a35df30..be609d94b 100644 --- a/web/src/app/admin/connectors/zulip/page.tsx +++ b/web/src/app/admin/connectors/zulip/page.tsx @@ -236,10 +236,6 @@ const MainSection = () => { export default function Page() { return ( <div className="mx-auto container"> - <div className="mb-4"> - <HealthCheckBanner /> - </div> - <AdminPageTitle icon={<ZulipIcon size={32} />} title="Zulip" /> <MainSection /> diff --git a/web/src/app/auth/login/page.tsx b/web/src/app/auth/login/page.tsx index ddf389bc5..50f1d42d9 100644 --- a/web/src/app/auth/login/page.tsx +++ b/web/src/app/auth/login/page.tsx @@ -13,6 +13,7 @@ import { Card, Title, Text } from "@tremor/react"; import Link from "next/link"; import { Logo } from "@/components/Logo"; import { LoginText } from "./LoginText"; +import { getSecondsUntilExpiration } from "@/lib/time"; const Page = async ({ searchParams, @@ -41,7 +42,12 @@ const Page = async ({ } // if user is already logged in, take them to the main app page - if (currentUser && currentUser.is_active) { + const secondsTillExpiration = getSecondsUntilExpiration(currentUser); + if ( + currentUser && + currentUser.is_active && + (secondsTillExpiration === null || secondsTillExpiration > 0) + ) { if (authTypeMetadata?.requiresVerification && !currentUser.is_verified) { return redirect("/auth/waiting-on-verification"); } diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 440a2a6ba..260dfb82e 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -73,6 +73,7 @@ import FunctionalHeader from "@/components/chat_search/Header"; import { useSidebarVisibility } from "@/components/chat_search/hooks"; import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants"; import FixedLogo from "./shared_chat_search/FixedLogo"; +import { getSecondsUntilExpiration } from "@/lib/time"; const TEMP_USER_MESSAGE_ID = -1; const TEMP_ASSISTANT_MESSAGE_ID = -2; @@ -1112,10 +1113,11 @@ export function ChatPage({ setDocumentSelection((documentSelection) => !documentSelection); setShowDocSidebar(false); }; + const secondsUntilExpiration = getSecondsUntilExpiration(user); return ( <> - <HealthCheckBanner /> + <HealthCheckBanner secondsUntilExpiration={secondsUntilExpiration} /> <InstantSSRAutoRefresh /> {/* ChatPopup is a custom popup that displays a admin-specified message on initial user visit. diff --git a/web/src/app/search/page.tsx b/web/src/app/search/page.tsx index 4c4ca198d..9d367a423 100644 --- a/web/src/app/search/page.tsx +++ b/web/src/app/search/page.tsx @@ -5,6 +5,7 @@ import { getAuthTypeMetadataSS, getCurrentUserSS, } from "@/lib/userSS"; +import { getSecondsUntilExpiration } from "@/lib/time"; import { redirect } from "next/navigation"; import { HealthCheckBanner } from "@/components/health/healthcheck"; import { ApiKeyModal } from "@/components/llm/ApiKeyModal"; @@ -182,12 +183,12 @@ export default async function Home() { const agenticSearchEnabled = agenticSearchToggle ? agenticSearchToggle.value.toLocaleLowerCase() == "true" || false : false; + const secondsUntilExpiration = getSecondsUntilExpiration(user); return ( <> - <div className="m-3"> - <HealthCheckBanner /> - </div> + <Header user={user} /> + <HealthCheckBanner secondsUntilExpiration={secondsUntilExpiration} /> {shouldShowWelcomeModal && <WelcomeModal user={user} />} {!shouldShowWelcomeModal && diff --git a/web/src/components/admin/Layout.tsx b/web/src/components/admin/Layout.tsx index 7d92be588..c3241764d 100644 --- a/web/src/components/admin/Layout.tsx +++ b/web/src/components/admin/Layout.tsx @@ -57,6 +57,8 @@ import { FiTool, } from "react-icons/fi"; import { UserDropdown } from "../UserDropdown"; +import { HealthCheckBanner } from "../health/healthcheck"; +import { getSecondsUntilExpiration } from "@/lib/time"; export async function Layout({ children }: { children: React.ReactNode }) { const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS()]; @@ -88,8 +90,11 @@ export async function Layout({ children }: { children: React.ReactNode }) { } } + const secondsUntilExpiration = getSecondsUntilExpiration(user); + return ( <div className="h-screen overflow-y-hidden"> + <HealthCheckBanner secondsUntilExpiration={secondsUntilExpiration} /> <div className="flex h-full"> <div className="w-64 z-20 bg-background-100 pt-3 pb-8 h-full border-r border-border miniscroll overflow-auto"> <AdminSidebar diff --git a/web/src/components/health/healthcheck.tsx b/web/src/components/health/healthcheck.tsx index 1c1de9db6..a8110ba8c 100644 --- a/web/src/components/health/healthcheck.tsx +++ b/web/src/components/health/healthcheck.tsx @@ -1,29 +1,41 @@ "use client"; -import { errorHandlingFetcher, FetchError, RedirectError } from "@/lib/fetcher"; +import { errorHandlingFetcher, RedirectError } from "@/lib/fetcher"; import useSWR from "swr"; -import { useRouter } from "next/navigation"; import { Modal } from "../Modal"; +import { useState } from "react"; -export const HealthCheckBanner = () => { - const router = useRouter(); +export const HealthCheckBanner = ({ + secondsUntilExpiration, +}: { + secondsUntilExpiration?: number | null; +}) => { const { error } = useSWR("/api/health", errorHandlingFetcher); + const [expired, setExpired] = useState(false); - if (!error) { + if (secondsUntilExpiration !== null && secondsUntilExpiration !== undefined) { + setTimeout( + () => { + setExpired(true); + }, + secondsUntilExpiration * 1000 - 200 + ); + } + + if (!error && !expired) { return null; } - if (error instanceof RedirectError) { + if (error instanceof RedirectError || expired) { return ( <Modal width="w-1/4" className="overflow-y-hidden flex flex-col" - title="You have been logged out!" + title="You've been logged out" > <div className="flex flex-col gap-y-4"> - <p className="text-lg"> - You can click "Log in" to log back in! Apologies for the - inconvenience. + <p className="text-sm"> + Your session has expired. Please log in again to continue. </p> <a href="/auth/login" diff --git a/web/src/lib/time.ts b/web/src/lib/time.ts index 3a7052fbd..114dbee4c 100644 --- a/web/src/lib/time.ts +++ b/web/src/lib/time.ts @@ -1,3 +1,5 @@ +import { User } from "./types"; + const conditionallyAddPlural = (noun: string, cnt: number) => { if (cnt > 1) { return `${noun}s`; @@ -89,7 +91,53 @@ export function humanReadableFormatWithTime(datetimeString: string): string { hour: "numeric", minute: "numeric", }); - // Format the date and return it return formatter.format(date); } + +export function getSecondsUntilExpiration( + userInfo: User | null +): number | null { + if (!userInfo) { + return null; + } + const { oidc_expiry, current_token_created_at, current_token_expiry_length } = + userInfo; + + const now = new Date(); + + let secondsUntilTokenExpiration: number | null = null; + let secondsUntilOIDCExpiration: number | null = null; + + if (current_token_created_at && current_token_expiry_length !== undefined) { + const createdAt = new Date(current_token_created_at); + const expiresAt = new Date( + createdAt.getTime() + current_token_expiry_length * 1000 + ); + secondsUntilTokenExpiration = Math.floor( + (expiresAt.getTime() - now.getTime()) / 1000 + ); + } + + if (oidc_expiry) { + const expiresAtFromOIDC = new Date(oidc_expiry); + secondsUntilOIDCExpiration = Math.floor( + (expiresAtFromOIDC.getTime() - now.getTime()) / 1000 + ); + } + + if ( + secondsUntilTokenExpiration === null && + secondsUntilOIDCExpiration === null + ) { + return null; + } + + return Math.max( + 0, + Math.min( + secondsUntilTokenExpiration ?? Infinity, + secondsUntilOIDCExpiration ?? Infinity + ) + ); +} diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 60cb664b0..05dd4a788 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -19,6 +19,9 @@ export interface User { role: "basic" | "admin"; preferences: UserPreferences; status: UserStatus; + current_token_created_at?: Date; + current_token_expiry_length?: number; + oidc_expiry?: Date; } export interface MinimalUserSnapshot {