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, |     configs, | ||||||
|     utils, |     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() | app = FastAPI() | ||||||
| 
 | 
 | ||||||
|  | @ -20,6 +28,9 @@ origins = ["*"] | ||||||
| app.state.ENABLE_SIGNUP = ENABLE_SIGNUP | app.state.ENABLE_SIGNUP = ENABLE_SIGNUP | ||||||
| app.state.DEFAULT_MODELS = DEFAULT_MODELS | app.state.DEFAULT_MODELS = DEFAULT_MODELS | ||||||
| app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS | 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( | app.add_middleware( | ||||||
|     CORSMiddleware, |     CORSMiddleware, | ||||||
|  |  | ||||||
|  | @ -19,7 +19,12 @@ from apps.web.models.auths import ( | ||||||
| ) | ) | ||||||
| from apps.web.models.users import Users | 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 utils.misc import get_gravatar_url, validate_email_format | ||||||
| from constants import ERROR_MESSAGES | from constants import ERROR_MESSAGES | ||||||
| 
 | 
 | ||||||
|  | @ -116,16 +121,24 @@ async def signin(form_data: SigninForm): | ||||||
| @router.post("/signup", response_model=SigninResponse) | @router.post("/signup", response_model=SigninResponse) | ||||||
| async def signup(request: Request, form_data: SignupForm): | async def signup(request: Request, form_data: SignupForm): | ||||||
|     if not request.app.state.ENABLE_SIGNUP: |     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()): |     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()): |     if Users.get_user_by_email(form_data.email.lower()): | ||||||
|         raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) |         raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) | ||||||
| 
 | 
 | ||||||
|     try: |     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) |         hashed = get_password_hash(form_data.password) | ||||||
|         user = Auths.insert_new_auth( |         user = Auths.insert_new_auth( | ||||||
|             form_data.email.lower(), hashed, form_data.name, role |             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)): | async def toggle_sign_up(request: Request, user=Depends(get_admin_user)): | ||||||
|     request.app.state.ENABLE_SIGNUP = not request.app.state.ENABLE_SIGNUP |     request.app.state.ENABLE_SIGNUP = not request.app.state.ENABLE_SIGNUP | ||||||
|     return 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) | @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) |     result = Chats.delete_chat_by_id_and_user_id(id, user.id) | ||||||
|     return result |     return result | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| from fastapi import Response | from fastapi import Response, Request | ||||||
| from fastapi import Depends, FastAPI, HTTPException, status | from fastapi import Depends, FastAPI, HTTPException, status | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| from typing import List, Union, Optional | 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) |     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 | # UpdateUserRole | ||||||
| ############################ | ############################ | ||||||
|  |  | ||||||
|  | @ -93,12 +93,15 @@ DEFAULT_PROMPT_SUGGESTIONS = os.environ.get( | ||||||
|         }, |         }, | ||||||
|     ], |     ], | ||||||
| ) | ) | ||||||
|  | DEFAULT_USER_ROLE = "pending" | ||||||
|  | USER_PERMISSIONS = {"chat": {"deletion": True}} | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| #################################### | #################################### | ||||||
| # WEBUI_VERSION | # 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) | # WEBUI_AUTH (Required for security) | ||||||
|  |  | ||||||
|  | @ -178,6 +178,63 @@ export const getSignUpEnabledStatus = async (token: string) => { | ||||||
| 	return res; | 	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) => { | export const toggleSignUpEnabledStatus = async (token: string) => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -272,7 +272,7 @@ export const deleteChatById = async (token: string, id: string) => { | ||||||
| 			return json; | 			return json; | ||||||
| 		}) | 		}) | ||||||
| 		.catch((err) => { | 		.catch((err) => { | ||||||
| 			error = err; | 			error = err.detail; | ||||||
| 
 | 
 | ||||||
| 			console.log(err); | 			console.log(err); | ||||||
| 			return null; | 			return null; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,62 @@ | ||||||
| import { WEBUI_API_BASE_URL } from '$lib/constants'; | 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) => { | export const updateUserRole = async (token: string, id: string, role: string) => { | ||||||
| 	let error = null; | 	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, | 		getChatListByTagName, | ||||||
| 		updateChatById | 		updateChatById | ||||||
| 	} from '$lib/apis/chats'; | 	} from '$lib/apis/chats'; | ||||||
|  | 	import toast from 'svelte-french-toast'; | ||||||
| 
 | 
 | ||||||
| 	let show = false; | 	let show = false; | ||||||
| 	let navElement; | 	let navElement; | ||||||
|  | @ -64,10 +65,17 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const deleteChat = async (id) => { | 	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; | ||||||
| 		await chats.set(await getChatList(localStorage.token)); | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (res) { | ||||||
|  | 			goto('/'); | ||||||
|  | 			await chats.set(await getChatList(localStorage.token)); | ||||||
|  | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const saveSettings = async (updated) => { | 	const saveSettings = async (updated) => { | ||||||
|  |  | ||||||
|  | @ -9,13 +9,14 @@ | ||||||
| 	import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users'; | 	import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users'; | ||||||
| 	import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; | 	import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; | ||||||
| 	import EditUserModal from '$lib/components/admin/EditUserModal.svelte'; | 	import EditUserModal from '$lib/components/admin/EditUserModal.svelte'; | ||||||
|  | 	import SettingsModal from '$lib/components/admin/SettingsModal.svelte'; | ||||||
| 
 | 
 | ||||||
| 	let loaded = false; | 	let loaded = false; | ||||||
| 	let users = []; | 	let users = []; | ||||||
| 
 | 
 | ||||||
| 	let selectedUser = null; | 	let selectedUser = null; | ||||||
| 
 | 
 | ||||||
| 	let signUpEnabled = true; | 	let showSettingsModal = false; | ||||||
| 	let showEditUserModal = false; | 	let showEditUserModal = false; | ||||||
| 
 | 
 | ||||||
| 	const updateRoleHandler = async (id, role) => { | 	const updateRoleHandler = async (id, role) => { | ||||||
|  | @ -50,17 +51,11 @@ | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const toggleSignUpEnabled = async () => { |  | ||||||
| 		signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token); |  | ||||||
| 	}; |  | ||||||
| 
 |  | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
| 		if ($user?.role !== 'admin') { | 		if ($user?.role !== 'admin') { | ||||||
| 			await goto('/'); | 			await goto('/'); | ||||||
| 		} else { | 		} else { | ||||||
| 			users = await getUsers(localStorage.token); | 			users = await getUsers(localStorage.token); | ||||||
| 
 |  | ||||||
| 			signUpEnabled = await getSignUpEnabledStatus(localStorage.token); |  | ||||||
| 		} | 		} | ||||||
| 		loaded = true; | 		loaded = true; | ||||||
| 	}); | 	}); | ||||||
|  | @ -77,6 +72,8 @@ | ||||||
| 	/> | 	/> | ||||||
| {/key} | {/key} | ||||||
| 
 | 
 | ||||||
|  | <SettingsModal bind:show={showSettingsModal} /> | ||||||
|  | 
 | ||||||
| <div | <div | ||||||
| 	class=" bg-white dark:bg-gray-900 dark:text-gray-100 min-h-screen w-full flex justify-center font-mona" | 	class=" bg-white dark:bg-gray-900 dark:text-gray-100 min-h-screen w-full flex justify-center font-mona" | ||||||
| > | > | ||||||
|  | @ -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" | 								class="flex items-center space-x-1 border border-gray-200 dark:border-gray-600 px-3 py-1 rounded-lg" | ||||||
| 								type="button" | 								type="button" | ||||||
| 								on:click={() => { | 								on:click={() => { | ||||||
| 									toggleSignUpEnabled(); | 									showSettingsModal = !showSettingsModal; | ||||||
| 								}} | 								}} | ||||||
| 							> | 							> | ||||||
| 								{#if signUpEnabled} | 								<svg | ||||||
| 									<svg | 									xmlns="http://www.w3.org/2000/svg" | ||||||
| 										xmlns="http://www.w3.org/2000/svg" | 									viewBox="0 0 16 16" | ||||||
| 										viewBox="0 0 16 16" | 									fill="currentColor" | ||||||
| 										fill="currentColor" | 									class="w-4 h-4" | ||||||
| 										class="w-4 h-4" | 								> | ||||||
| 									> | 									<path | ||||||
| 										<path | 										fill-rule="evenodd" | ||||||
| 											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" | 										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> | 									/> | ||||||
|  | 								</svg> | ||||||
| 
 | 
 | ||||||
| 									<div class=" text-xs"> | 								<div class=" text-xs">Admin Settings</div> | ||||||
| 										New Sign Up <span class=" font-semibold">Enabled</span> |  | ||||||
| 									</div> |  | ||||||
| 								{: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> |  | ||||||
| 
 |  | ||||||
| 									<div class=" text-xs"> |  | ||||||
| 										New Sign Up <span class=" font-semibold">Disabled</span> |  | ||||||
| 									</div> |  | ||||||
| 								{/if} |  | ||||||
| 							</button> | 							</button> | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek