From 511e939b5dbdf1d9fb1df02f2430b094b99ba0e8 Mon Sep 17 00:00:00 2001 From: "Timothy J. Baek" Date: Wed, 14 Feb 2024 01:17:43 -0800 Subject: [PATCH] feat: admin settings --- backend/apps/web/main.py | 13 ++- backend/apps/web/routers/auths.py | 44 ++++++- backend/apps/web/routers/chats.py | 12 +- backend/apps/web/routers/users.py | 20 +++- backend/config.py | 3 + src/lib/apis/auths/index.ts | 57 +++++++++ src/lib/apis/chats/index.ts | 2 +- src/lib/apis/users/index.ts | 57 +++++++++ .../components/admin/Settings/General.svelte | 108 ++++++++++++++++++ .../components/admin/Settings/Users.svelte | 82 +++++++++++++ src/lib/components/admin/SettingsModal.svelte | 107 +++++++++++++++++ src/lib/components/layout/Sidebar.svelte | 14 ++- src/routes/(app)/admin/+page.svelte | 58 +++------- 13 files changed, 526 insertions(+), 51 deletions(-) create mode 100644 src/lib/components/admin/Settings/General.svelte create mode 100644 src/lib/components/admin/Settings/Users.svelte create mode 100644 src/lib/components/admin/SettingsModal.svelte diff --git a/backend/apps/web/main.py b/backend/apps/web/main.py index 8877df46..400ddac0 100644 --- a/backend/apps/web/main.py +++ b/backend/apps/web/main.py @@ -11,7 +11,15 @@ from apps.web.routers import ( configs, utils, ) -from config import WEBUI_VERSION, WEBUI_AUTH, DEFAULT_MODELS, DEFAULT_PROMPT_SUGGESTIONS, ENABLE_SIGNUP +from config import ( + WEBUI_VERSION, + WEBUI_AUTH, + DEFAULT_MODELS, + DEFAULT_PROMPT_SUGGESTIONS, + DEFAULT_USER_ROLE, + ENABLE_SIGNUP, + USER_PERMISSIONS, +) app = FastAPI() @@ -20,6 +28,9 @@ origins = ["*"] app.state.ENABLE_SIGNUP = ENABLE_SIGNUP 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.add_middleware( CORSMiddleware, diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index 58da3512..7ccef630 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -19,7 +19,12 @@ from apps.web.models.auths import ( ) from apps.web.models.users import Users -from utils.utils import get_password_hash, get_current_user, get_admin_user, create_token +from utils.utils import ( + get_password_hash, + get_current_user, + get_admin_user, + create_token, +) from utils.misc import get_gravatar_url, validate_email_format from constants import ERROR_MESSAGES @@ -116,16 +121,24 @@ async def signin(form_data: SigninForm): @router.post("/signup", response_model=SigninResponse) async def signup(request: Request, form_data: SignupForm): if not request.app.state.ENABLE_SIGNUP: - raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + raise HTTPException( + status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED + ) if not validate_email_format(form_data.email.lower()): - raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) + raise HTTPException( + status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT + ) if Users.get_user_by_email(form_data.email.lower()): raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) try: - role = "admin" if Users.get_num_users() == 0 else "pending" + role = ( + "admin" + if Users.get_num_users() == 0 + else request.app.state.DEFAULT_USER_ROLE + ) hashed = get_password_hash(form_data.password) user = Auths.insert_new_auth( form_data.email.lower(), hashed, form_data.name, role @@ -164,3 +177,26 @@ async def get_sign_up_status(request: Request, user=Depends(get_admin_user)): async def toggle_sign_up(request: Request, user=Depends(get_admin_user)): request.app.state.ENABLE_SIGNUP = not request.app.state.ENABLE_SIGNUP return request.app.state.ENABLE_SIGNUP + + +############################ +# Default User Role +############################ + + +@router.get("/signup/user/role") +async def get_default_user_role(request: Request, user=Depends(get_admin_user)): + return request.app.state.DEFAULT_USER_ROLE + + +class UpdateRoleForm(BaseModel): + role: str + + +@router.post("/signup/user/role") +async def update_default_user_role( + request: Request, form_data: UpdateRoleForm, user=Depends(get_admin_user) +): + if form_data.role in ["pending", "user", "admin"]: + request.app.state.DEFAULT_USER_ROLE = form_data.role + return request.app.state.DEFAULT_USER_ROLE diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index 1150234a..00dcfb6e 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -165,7 +165,17 @@ async def update_chat_by_id( @router.delete("/{id}", response_model=bool) -async def delete_chat_by_id(id: str, user=Depends(get_current_user)): +async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_user)): + + if ( + user.role == "user" + and not request.app.state.USER_PERMISSIONS["chat"]["deletion"] + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + result = Chats.delete_chat_by_id_and_user_id(id, user.id) return result diff --git a/backend/apps/web/routers/users.py b/backend/apps/web/routers/users.py index 18c6c620..b8e2732c 100644 --- a/backend/apps/web/routers/users.py +++ b/backend/apps/web/routers/users.py @@ -1,4 +1,4 @@ -from fastapi import Response +from fastapi import Response, Request from fastapi import Depends, FastAPI, HTTPException, status from datetime import datetime, timedelta from typing import List, Union, Optional @@ -26,6 +26,24 @@ async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user) return Users.get_users(skip, limit) +############################ +# User Permissions +############################ + + +@router.get("/permissions/user") +async def get_user_permissions(request: Request, user=Depends(get_admin_user)): + return request.app.state.USER_PERMISSIONS + + +@router.post("/permissions/user") +async def update_user_permissions( + request: Request, form_data: dict, user=Depends(get_admin_user) +): + request.app.state.USER_PERMISSIONS = form_data + return request.app.state.USER_PERMISSIONS + + ############################ # UpdateUserRole ############################ diff --git a/backend/config.py b/backend/config.py index 81b90084..90c5149d 100644 --- a/backend/config.py +++ b/backend/config.py @@ -93,6 +93,9 @@ DEFAULT_PROMPT_SUGGESTIONS = os.environ.get( }, ], ) +DEFAULT_USER_ROLE = "pending" +USER_PERMISSIONS = {"chat": {"deletion": True}} + #################################### # WEBUI_VERSION diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts index 5f16f83f..07858998 100644 --- a/src/lib/apis/auths/index.ts +++ b/src/lib/apis/auths/index.ts @@ -178,6 +178,63 @@ export const getSignUpEnabledStatus = async (token: string) => { return res; }; +export const getDefaultUserRole = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/user/role`, { + 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.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateDefaultUserRole = async (token: string, role: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/user/role`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + role: role + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const toggleSignUpEnabledStatus = async (token: string) => { let error = null; diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts index 58c14bb9..aadf3769 100644 --- a/src/lib/apis/chats/index.ts +++ b/src/lib/apis/chats/index.ts @@ -272,7 +272,7 @@ export const deleteChatById = async (token: string, id: string) => { return json; }) .catch((err) => { - error = err; + error = err.detail; console.log(err); return null; diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index 3faeb8c4..788e8820 100644 --- a/src/lib/apis/users/index.ts +++ b/src/lib/apis/users/index.ts @@ -1,5 +1,62 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; +export const getUserPermissions = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions/user`, { + 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.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserPermissions = async (token: string, permissions: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions/user`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...permissions + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const updateUserRole = async (token: string, id: string, role: string) => { let error = null; diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte new file mode 100644 index 00000000..48ed41e7 --- /dev/null +++ b/src/lib/components/admin/Settings/General.svelte @@ -0,0 +1,108 @@ + + +
{ + // console.log('submit'); + saveHandler(); + }} +> +
+
+
General Settings
+ +
+
Enable New Sign Ups
+ + +
+ +
+
Default User Role
+
+ +
+
+
+
+ +
+ +
+
diff --git a/src/lib/components/admin/Settings/Users.svelte b/src/lib/components/admin/Settings/Users.svelte new file mode 100644 index 00000000..60863936 --- /dev/null +++ b/src/lib/components/admin/Settings/Users.svelte @@ -0,0 +1,82 @@ + + +
{ + // console.log('submit'); + await updateUserPermissions(localStorage.token, permissions); + saveHandler(); + }} +> +
+
+
User Permission
+ +
+
Allow Chat Deletion
+ + +
+
+
+ +
+ +
+
diff --git a/src/lib/components/admin/SettingsModal.svelte b/src/lib/components/admin/SettingsModal.svelte new file mode 100644 index 00000000..67f6be88 --- /dev/null +++ b/src/lib/components/admin/SettingsModal.svelte @@ -0,0 +1,107 @@ + + + +
+
+
Admin Settings
+ +
+
+ +
+
+ + + +
+
+ {#if selectedTab === 'general'} + { + show = false; + }} + /> + {:else if selectedTab === 'users'} + { + show = false; + }} + /> + {/if} +
+
+
+
diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 003af0f1..4deaa8dd 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -15,6 +15,7 @@ getChatListByTagName, updateChatById } from '$lib/apis/chats'; + import toast from 'svelte-french-toast'; let show = false; let navElement; @@ -64,10 +65,17 @@ }; const deleteChat = async (id) => { - goto('/'); + const res = await deleteChatById(localStorage.token, id).catch((error) => { + toast.error(error); + chatDeleteId = null; - await deleteChatById(localStorage.token, id); - await chats.set(await getChatList(localStorage.token)); + return null; + }); + + if (res) { + goto('/'); + await chats.set(await getChatList(localStorage.token)); + } }; const saveSettings = async (updated) => { diff --git a/src/routes/(app)/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte index 7da246bd..48d5b15c 100644 --- a/src/routes/(app)/admin/+page.svelte +++ b/src/routes/(app)/admin/+page.svelte @@ -9,13 +9,14 @@ import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users'; import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; import EditUserModal from '$lib/components/admin/EditUserModal.svelte'; + import SettingsModal from '$lib/components/admin/SettingsModal.svelte'; let loaded = false; let users = []; let selectedUser = null; - let signUpEnabled = true; + let showSettingsModal = false; let showEditUserModal = false; const updateRoleHandler = async (id, role) => { @@ -50,17 +51,11 @@ } }; - const toggleSignUpEnabled = async () => { - signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token); - }; - onMount(async () => { if ($user?.role !== 'admin') { await goto('/'); } else { users = await getUsers(localStorage.token); - - signUpEnabled = await getSignUpEnabledStatus(localStorage.token); } loaded = true; }); @@ -77,6 +72,8 @@ /> {/key} + +
@@ -91,42 +88,23 @@ class="flex items-center space-x-1 border border-gray-200 dark:border-gray-600 px-3 py-1 rounded-lg" type="button" on:click={() => { - toggleSignUpEnabled(); + showSettingsModal = !showSettingsModal; }} > - {#if signUpEnabled} - - - + + + -
- New Sign Up Enabled -
- {:else} - - - - -
- New Sign Up Disabled -
- {/if} +
Admin Settings