diff --git a/backend/apps/web/main.py b/backend/apps/web/main.py index bd14f4bda..dd5c0c704 100644 --- a/backend/apps/web/main.py +++ b/backend/apps/web/main.py @@ -19,6 +19,7 @@ from config import ( DEFAULT_USER_ROLE, ENABLE_SIGNUP, USER_PERMISSIONS, + WEBHOOK_URL, ) app = FastAPI() @@ -32,6 +33,7 @@ app.state.DEFAULT_MODELS = DEFAULT_MODELS app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.USER_PERMISSIONS = USER_PERMISSIONS +app.state.WEBHOOK_URL = WEBHOOK_URL app.add_middleware( diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index 3db2d0ad2..d881ec746 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -27,7 +27,8 @@ from utils.utils import ( create_token, ) from utils.misc import parse_duration, validate_email_format -from constants import ERROR_MESSAGES +from utils.webhook import post_webhook +from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES router = APIRouter() @@ -155,6 +156,17 @@ async def signup(request: Request, form_data: SignupForm): ) # response.set_cookie(key='token', value=token, httponly=True) + if request.app.state.WEBHOOK_URL: + post_webhook( + request.app.state.WEBHOOK_URL, + WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + { + "action": "signup", + "message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + "user": user.model_dump_json(exclude_none=True), + }, + ) + return { "token": token, "token_type": "Bearer", diff --git a/backend/config.py b/backend/config.py index b5e7a5394..e99e248c5 100644 --- a/backend/config.py +++ b/backend/config.py @@ -302,6 +302,7 @@ MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", False) MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "") MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")] +WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "") #################################### # WEBUI_VERSION diff --git a/backend/constants.py b/backend/constants.py index 994b8caf7..42c5c85eb 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -5,6 +5,13 @@ class MESSAGES(str, Enum): DEFAULT = lambda msg="": f"{msg if msg else ''}" +class WEBHOOK_MESSAGES(str, Enum): + DEFAULT = lambda msg="": f"{msg if msg else ''}" + USER_SIGNUP = lambda username="": ( + f"New user signed up: {username}" if username else "New user signed up" + ) + + class ERROR_MESSAGES(str, Enum): def __str__(self) -> str: return super().__str__() diff --git a/backend/main.py b/backend/main.py index 253227182..35f405e69 100644 --- a/backend/main.py +++ b/backend/main.py @@ -38,6 +38,7 @@ from config import ( FRONTEND_BUILD_DIR, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST, + WEBHOOK_URL, ) from constants import ERROR_MESSAGES @@ -58,6 +59,9 @@ app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST +app.state.WEBHOOK_URL = WEBHOOK_URL + + origins = ["*"] @@ -178,7 +182,7 @@ class ModelFilterConfigForm(BaseModel): @app.post("/api/config/model/filter") -async def get_model_filter_config( +async def update_model_filter_config( form_data: ModelFilterConfigForm, user=Depends(get_admin_user) ): @@ -197,6 +201,28 @@ async def get_model_filter_config( } +@app.get("/api/webhook") +async def get_webhook_url(user=Depends(get_admin_user)): + return { + "url": app.state.WEBHOOK_URL, + } + + +class UrlForm(BaseModel): + url: str + + +@app.post("/api/webhook") +async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): + app.state.WEBHOOK_URL = form_data.url + + webui_app.state.WEBHOOK_URL = app.state.WEBHOOK_URL + + return { + "url": app.state.WEBHOOK_URL, + } + + @app.get("/api/version") async def get_app_config(): diff --git a/backend/utils/webhook.py b/backend/utils/webhook.py new file mode 100644 index 000000000..1bc5a6048 --- /dev/null +++ b/backend/utils/webhook.py @@ -0,0 +1,20 @@ +import requests + + +def post_webhook(url: str, message: str, event_data: dict) -> bool: + try: + payload = {} + + if "https://hooks.slack.com" in url: + payload["text"] = message + elif "https://discord.com/api/webhooks" in url: + payload["content"] = message + else: + payload = {**event_data} + + r = requests.post(url, json=payload) + r.raise_for_status() + return True + except Exception as e: + print(e) + return False diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index b33fb571b..a610f7210 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -139,3 +139,60 @@ export const updateModelFilterConfig = async ( return res; }; + +export const getWebhookUrl = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.url; +}; + +export const updateWebhookUrl = async (token: string, url: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.url; +}; diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte index 351f6744b..be319e26f 100644 --- a/src/lib/components/admin/Settings/General.svelte +++ b/src/lib/components/admin/Settings/General.svelte @@ -1,4 +1,5 @@
{ - // console.log('submit'); updateJWTExpiresDurationHandler(JWTExpiresIn); + updateWebhookUrlHandler(); saveHandler(); }} > @@ -108,6 +116,23 @@
+
+
+
{$i18n.t('Webhook URL')}
+
+ +
+ +
+
+ +
+
{$i18n.t('JWT Expiration')}