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 (
-
-
-
-
} title="Wikipedia" />
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 (
-
-
-
-
} title="Zendesk" />
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 (
-
-
-
-
} title="Zulip" />
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 (
<>
-
+
{/* 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 (
<>
-
-
-
+
+
{shouldShowWelcomeModal &&
}
{!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 (
+
{
- 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 (
-
- You can click "Log in" to log back in! Apologies for the
- inconvenience.
+
+ Your session has expired. Please log in again to continue.
{
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 {