add modern health check banner + expiration tracking (#1730)

---------

Co-authored-by: Weves <chrisweaver101@gmail.com>
This commit is contained in:
pablodanswer 2024-07-24 15:34:22 -07:00 committed by GitHub
parent d58aaf7a59
commit 8bc4123ed7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 220 additions and 189 deletions

View File

@ -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 ###

View File

@ -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",

View File

@ -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"

View File

@ -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

View File

@ -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"""

View File

@ -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:

View File

@ -136,10 +136,6 @@ export default function Page({ params }: { params: { ccPairId: string } }) {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<Main ccPairId={ccPairId} />
</div>
);

View File

@ -248,10 +248,6 @@ const MainSection = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<AxeroIcon size={32} />} title="Axero" />
<MainSection />

View File

@ -249,10 +249,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<BookstackIcon size={32} />} title="Bookstack" />
<Main />

View File

@ -331,10 +331,6 @@ const MainSection = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<ClickupIcon size={32} />} title="Clickup" />
<MainSection />

View File

@ -330,10 +330,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<ConfluenceIcon size={32} />} title="Confluence" />
<Main />

View File

@ -273,10 +273,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<DiscourseIcon size={32} />} title="Discourse" />
<Main />

View File

@ -262,10 +262,6 @@ const MainSection = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle
icon={<Document360Icon size={32} />}
title="Document360"

View File

@ -212,9 +212,7 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
{" "}
<AdminPageTitle icon={<DropboxIcon size={32} />} title="Dropbox" />
<Main />
</div>

View File

@ -287,10 +287,6 @@ const Main = () => {
export default function File() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<FileIcon size={32} />} title="File" />
<Main />

View File

@ -265,10 +265,6 @@ const Main = () => {
export default function Page() {
return (
<div className="container mx-auto">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle
icon={<GithubIcon size={32} />}
title="Github PRs + Issues"

View File

@ -250,10 +250,6 @@ const Main = () => {
export default function Page() {
return (
<div className="container mx-auto">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle
icon={<GitlabIcon size={32} />}
title="Gitlab MRs + Issues"

View File

@ -264,10 +264,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<GmailIcon size={32} />} title="Gmail" />
<Main />

View File

@ -257,10 +257,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<GongIcon size={32} />} title="Gong" />
<Main />

View File

@ -412,10 +412,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle
icon={<GoogleDriveIcon size={32} />}
title="Google Drive"

View File

@ -50,10 +50,6 @@ export default function GoogleSites() {
{popup}
{filesAreUploading && <Spinner />}
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle
icon={<GoogleSitesIcon size={32} />}
title="Google Sites"

View File

@ -244,9 +244,7 @@ const GCSMain = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
{" "}
<AdminPageTitle
icon={<GoogleStorageIcon size={32} />}
title="Google Cloud Storage"

View File

@ -232,10 +232,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<GuruIcon size={32} />} title="Guru" />
<Main />

View File

@ -220,10 +220,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<HubSpotIcon size={32} />} title="HubSpot" />
<Main />

View File

@ -362,10 +362,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<JiraIcon size={32} />} title="Jira" />
<Main />

View File

@ -224,10 +224,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<LinearIcon size={32} />} title="Linear" />
<Main />

View File

@ -250,9 +250,7 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
{" "}
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<LoopioIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Loopio</h1>

View File

@ -207,10 +207,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<MediaWikiIcon size={32} />} title="MediaWiki" />
<Main />

View File

@ -260,10 +260,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<NotionIcon size={32} />} title="Notion" />
<Main />

View File

@ -259,9 +259,7 @@ const OCIMain = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
{" "}
<AdminPageTitle
icon={<OCIStorageIcon size={32} />}
title="Oracle Cloud Infrastructure"

View File

@ -239,10 +239,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle
icon={<ProductboardIcon size={32} />}
title="Productboard"

View File

@ -255,9 +255,7 @@ export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
{" "}
<AdminPageTitle icon={<R2Icon size={32} />} title="R2 Storage" />
<R2Main key={2} />
</div>

View File

@ -241,10 +241,6 @@ const MainSection = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle
icon={<RequestTrackerIcon size={32} />}
title="Request Tracker"

View File

@ -247,11 +247,8 @@ export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
{" "}
<AdminPageTitle icon={<S3Icon size={32} />} title="S3 Storage" />
<S3Main key={1} />
</div>
);

View File

@ -278,10 +278,6 @@ const MainSection = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<SalesforceIcon size={32} />} title="Salesforce" />
<MainSection />

View File

@ -282,10 +282,6 @@ const MainSection = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<SharepointIcon size={32} />} title="Sharepoint" />
<MainSection />

View File

@ -270,10 +270,6 @@ const Main = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<SlabIcon size={32} />} title="Slab" />
<Main />

View File

@ -284,10 +284,6 @@ const MainSection = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<SlackIcon size={32} />} title="Slack" />
<MainSection />

View File

@ -263,10 +263,6 @@ const MainSection = () => {
export default function Page() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<TeamsIcon size={32} />} title="Teams" />
<MainSection />

View File

@ -48,10 +48,6 @@ export default function Web() {
return (
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<AdminPageTitle icon={<GlobeIcon size={32} />} title="Web" />
<Title className="mb-2 mt-6 ml-auto mr-auto">

View File

@ -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 />

View File

@ -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 />

View File

@ -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 />

View File

@ -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");
}

View File

@ -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.

View File

@ -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 &&

View File

@ -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

View File

@ -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 &quot;Log in&quot; 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"

View File

@ -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
)
);
}

View File

@ -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 {