forked from open-webui/open-webui
		
	Merge pull request #726 from ollama-webui/disable-chat-delete
feat: admin settings
This commit is contained in:
		
						commit
						9704a5754e
					
				
					 13 changed files with 527 additions and 52 deletions
				
			
		|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| ############################ | ||||
|  |  | |||
|  | @ -93,12 +93,15 @@ DEFAULT_PROMPT_SUGGESTIONS = os.environ.get( | |||
|         }, | ||||
|     ], | ||||
| ) | ||||
| DEFAULT_USER_ROLE = "pending" | ||||
| USER_PERMISSIONS = {"chat": {"deletion": True}} | ||||
| 
 | ||||
| 
 | ||||
| #################################### | ||||
| # WEBUI_VERSION | ||||
| #################################### | ||||
| 
 | ||||
| WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.92") | ||||
| WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100") | ||||
| 
 | ||||
| #################################### | ||||
| # WEBUI_AUTH (Required for security) | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										108
									
								
								src/lib/components/admin/Settings/General.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/lib/components/admin/Settings/General.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,108 @@ | |||
| <script lang="ts"> | ||||
| 	import { | ||||
| 		getDefaultUserRole, | ||||
| 		getSignUpEnabledStatus, | ||||
| 		toggleSignUpEnabledStatus, | ||||
| 		updateDefaultUserRole | ||||
| 	} from '$lib/apis/auths'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	export let saveHandler: Function; | ||||
| 	let signUpEnabled = true; | ||||
| 	let defaultUserRole = 'pending'; | ||||
| 
 | ||||
| 	const toggleSignUpEnabled = async () => { | ||||
| 		signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token); | ||||
| 	}; | ||||
| 
 | ||||
| 	const updateDefaultUserRoleHandler = async (role) => { | ||||
| 		defaultUserRole = await updateDefaultUserRole(localStorage.token, role); | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		signUpEnabled = await getSignUpEnabledStatus(localStorage.token); | ||||
| 		defaultUserRole = await getDefaultUserRole(localStorage.token); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <form | ||||
| 	class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||
| 	on:submit|preventDefault={() => { | ||||
| 		// console.log('submit'); | ||||
| 		saveHandler(); | ||||
| 	}} | ||||
| > | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 		<div> | ||||
| 			<div class=" mb-2 text-sm font-medium">General Settings</div> | ||||
| 
 | ||||
| 			<div class="  flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">Enable New Sign Ups</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
| 					on:click={() => { | ||||
| 						toggleSignUpEnabled(); | ||||
| 					}} | ||||
| 					type="button" | ||||
| 				> | ||||
| 					{#if signUpEnabled} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 16 16" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 						<span class="ml-2 self-center">Enabled</span> | ||||
| 					{:else} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 16 16" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								fill-rule="evenodd" | ||||
| 								d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z" | ||||
| 								clip-rule="evenodd" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 
 | ||||
| 						<span class="ml-2 self-center">Disabled</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class=" flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">Default User Role</div> | ||||
| 				<div class="flex items-center relative"> | ||||
| 					<select | ||||
| 						class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" | ||||
| 						bind:value={defaultUserRole} | ||||
| 						placeholder="Select a theme" | ||||
| 						on:change={(e) => { | ||||
| 							updateDefaultUserRoleHandler(e.target.value); | ||||
| 						}} | ||||
| 					> | ||||
| 						<option value="pending">Pending</option> | ||||
| 						<option value="user">User</option> | ||||
| 						<option value="admin">Admin</option> | ||||
| 					</select> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="flex justify-end pt-3 text-sm font-medium"> | ||||
| 		<button | ||||
| 			class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||
| 			type="submit" | ||||
| 		> | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </form> | ||||
							
								
								
									
										82
									
								
								src/lib/components/admin/Settings/Users.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/lib/components/admin/Settings/Users.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,82 @@ | |||
| <script lang="ts"> | ||||
| 	import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; | ||||
| 	import { getUserPermissions, updateUserPermissions } from '$lib/apis/users'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	export let saveHandler: Function; | ||||
| 
 | ||||
| 	let permissions = { | ||||
| 		chat: { | ||||
| 			deletion: true | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		permissions = await getUserPermissions(localStorage.token); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <form | ||||
| 	class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||
| 	on:submit|preventDefault={async () => { | ||||
| 		// console.log('submit'); | ||||
| 		await updateUserPermissions(localStorage.token, permissions); | ||||
| 		saveHandler(); | ||||
| 	}} | ||||
| > | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 		<div> | ||||
| 			<div class=" mb-2 text-sm font-medium">User Permissions</div> | ||||
| 
 | ||||
| 			<div class="  flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">Allow Chat Deletion</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
| 					on:click={() => { | ||||
| 						permissions.chat.deletion = !permissions.chat.deletion; | ||||
| 					}} | ||||
| 					type="button" | ||||
| 				> | ||||
| 					{#if permissions.chat.deletion} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 16 16" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 						<span class="ml-2 self-center">Allow</span> | ||||
| 					{:else} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 16 16" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								fill-rule="evenodd" | ||||
| 								d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z" | ||||
| 								clip-rule="evenodd" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 
 | ||||
| 						<span class="ml-2 self-center">Don't Allow</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="flex justify-end pt-3 text-sm font-medium"> | ||||
| 		<button | ||||
| 			class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||
| 			type="submit" | ||||
| 		> | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </form> | ||||
							
								
								
									
										107
									
								
								src/lib/components/admin/SettingsModal.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/lib/components/admin/SettingsModal.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,107 @@ | |||
| <script> | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
| 
 | ||||
| 	import General from './Settings/General.svelte'; | ||||
| 	import Users from './Settings/Users.svelte'; | ||||
| 
 | ||||
| 	export let show = false; | ||||
| 
 | ||||
| 	let selectedTab = 'general'; | ||||
| </script> | ||||
| 
 | ||||
| <Modal bind:show> | ||||
| 	<div> | ||||
| 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||
| 			<div class=" text-lg font-medium self-center">Admin Settings</div> | ||||
| 			<button | ||||
| 				class="self-center" | ||||
| 				on:click={() => { | ||||
| 					show = false; | ||||
| 				}} | ||||
| 			> | ||||
| 				<svg | ||||
| 					xmlns="http://www.w3.org/2000/svg" | ||||
| 					viewBox="0 0 20 20" | ||||
| 					fill="currentColor" | ||||
| 					class="w-5 h-5" | ||||
| 				> | ||||
| 					<path | ||||
| 						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" | ||||
| 					/> | ||||
| 				</svg> | ||||
| 			</button> | ||||
| 		</div> | ||||
| 		<hr class=" dark:border-gray-800" /> | ||||
| 
 | ||||
| 		<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4"> | ||||
| 			<div | ||||
| 				class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0" | ||||
| 			> | ||||
| 				<button | ||||
| 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | ||||
| 					'general' | ||||
| 						? 'bg-gray-200 dark:bg-gray-700' | ||||
| 						: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" | ||||
| 					on:click={() => { | ||||
| 						selectedTab = 'general'; | ||||
| 					}} | ||||
| 				> | ||||
| 					<div class=" self-center mr-2"> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 16 16" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								fill-rule="evenodd" | ||||
| 								d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z" | ||||
| 								clip-rule="evenodd" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">General</div> | ||||
| 				</button> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | ||||
| 					'users' | ||||
| 						? 'bg-gray-200 dark:bg-gray-700' | ||||
| 						: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" | ||||
| 					on:click={() => { | ||||
| 						selectedTab = 'users'; | ||||
| 					}} | ||||
| 				> | ||||
| 					<div class=" self-center mr-2"> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 16 16" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								d="M8 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM3.156 11.763c.16-.629.44-1.21.813-1.72a2.5 2.5 0 0 0-2.725 1.377c-.136.287.102.58.418.58h1.449c.01-.077.025-.156.045-.237ZM12.847 11.763c.02.08.036.16.046.237h1.446c.316 0 .554-.293.417-.579a2.5 2.5 0 0 0-2.722-1.378c.374.51.653 1.09.813 1.72ZM14 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM3.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 13c-.552 0-1.013-.455-.876-.99a4.002 4.002 0 0 1 7.753 0c.136.535-.324.99-.877.99H5Z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">Users</div> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 			<div class="flex-1 md:min-h-[380px]"> | ||||
| 				{#if selectedTab === 'general'} | ||||
| 					<General | ||||
| 						saveHandler={() => { | ||||
| 							show = false; | ||||
| 						}} | ||||
| 					/> | ||||
| 				{:else if selectedTab === 'users'} | ||||
| 					<Users | ||||
| 						saveHandler={() => { | ||||
| 							show = false; | ||||
| 						}} | ||||
| 					/> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </Modal> | ||||
|  | @ -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); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 		if (res) { | ||||
| 			goto('/'); | ||||
| 			await chats.set(await getChatList(localStorage.token)); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const saveSettings = async (updated) => { | ||||
|  |  | |||
|  | @ -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} | ||||
| 
 | ||||
| <SettingsModal bind:show={showSettingsModal} /> | ||||
| 
 | ||||
| <div | ||||
| 	class=" bg-white dark:bg-gray-900 dark:text-gray-100 min-h-screen w-full flex justify-center font-mona" | ||||
| > | ||||
|  | @ -91,25 +88,9 @@ | |||
| 								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} | ||||
| 									<svg | ||||
| 										xmlns="http://www.w3.org/2000/svg" | ||||
| 										viewBox="0 0 16 16" | ||||
| 										fill="currentColor" | ||||
| 										class="w-4 h-4" | ||||
| 									> | ||||
| 										<path | ||||
| 											d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z" | ||||
| 										/> | ||||
| 									</svg> | ||||
| 
 | ||||
| 									<div class=" text-xs"> | ||||
| 										New Sign Up <span class=" font-semibold">Enabled</span> | ||||
| 									</div> | ||||
| 								{:else} | ||||
| 								<svg | ||||
| 									xmlns="http://www.w3.org/2000/svg" | ||||
| 									viewBox="0 0 16 16" | ||||
|  | @ -118,15 +99,12 @@ | |||
| 								> | ||||
| 									<path | ||||
| 										fill-rule="evenodd" | ||||
| 											d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z" | ||||
| 										d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z" | ||||
| 										clip-rule="evenodd" | ||||
| 									/> | ||||
| 								</svg> | ||||
| 
 | ||||
| 									<div class=" text-xs"> | ||||
| 										New Sign Up <span class=" font-semibold">Disabled</span> | ||||
| 									</div> | ||||
| 								{/if} | ||||
| 								<div class=" text-xs">Admin Settings</div> | ||||
| 							</button> | ||||
| 						</div> | ||||
| 					</div> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek