feat: admin panel added

This commit is contained in:
Timothy J. Baek 2023-11-19 00:13:59 -08:00
parent 8547b7807d
commit 07d2c9871f
9 changed files with 326 additions and 1087 deletions

View file

@ -1,7 +1,7 @@
from fastapi import FastAPI, Request, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from apps.web.routers import auths
from apps.web.routers import auths, users
from config import OLLAMA_WEBUI_VERSION, OLLAMA_WEBUI_AUTH
app = FastAPI()
@ -18,6 +18,7 @@ app.add_middleware(
app.include_router(auths.router, prefix="/auths", tags=["auths"])
app.include_router(users.router, prefix="/users", tags=["users"])
@app.get("/")

View file

@ -27,6 +27,11 @@ class UserModel(BaseModel):
####################
class UserRoleUpdateForm(BaseModel):
id: str
role: str
class UsersTable:
def __init__(self, db):
self.db = db
@ -71,10 +76,19 @@ class UsersTable:
def get_users(self, skip: int = 0, limit: int = 50) -> Optional[UserModel]:
return [
UserModel(**user)
for user in list(self.table.find({}, {"_id": False}))
.skip(skip)
.limit(limit)
for user in list(
self.table.find({}, {"_id": False}).skip(skip).limit(limit)
)
]
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
user = self.table.find_one_and_update(
{"id": id}, {"$set": updated}, return_document=ReturnDocument.AFTER
)
return UserModel(**user)
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
return self.update_user_by_id(id, {"role": role})
Users = UsersTable(DB)

View file

@ -8,15 +8,6 @@ from pydantic import BaseModel
import time
import uuid
from constants import ERROR_MESSAGES
from utils.utils import (
get_password_hash,
bearer_scheme,
create_token,
)
from utils.misc import get_gravatar_url
from apps.web.models.auths import (
SigninForm,
SignupForm,
@ -25,13 +16,19 @@ from apps.web.models.auths import (
Auths,
)
from apps.web.models.users import Users
import config
from utils.utils import (
get_password_hash,
bearer_scheme,
create_token,
)
from utils.misc import get_gravatar_url
from constants import ERROR_MESSAGES
router = APIRouter()
DB = config.DB
############################
# GetSessionUser
############################

View file

@ -0,0 +1,75 @@
from fastapi import Response
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import time
import uuid
from apps.web.models.users import UserModel, UserRoleUpdateForm, Users
from utils.utils import (
get_password_hash,
bearer_scheme,
create_token,
)
from constants import ERROR_MESSAGES
router = APIRouter()
############################
# GetUsers
############################
@router.get("/", response_model=List[UserModel])
async def get_users(skip: int = 0, limit: int = 50, cred=Depends(bearer_scheme)):
token = cred.credentials
user = Users.get_user_by_token(token)
if user:
if user.role == "admin":
return Users.get_users(skip, limit)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
############################
# UpdateUserRole
############################
@router.post("/update/role", response_model=Optional[UserModel])
async def update_user_role(form_data: UserRoleUpdateForm, cred=Depends(bearer_scheme)):
token = cred.credentials
user = Users.get_user_by_token(token)
if user:
if user.role == "admin":
if user.id != form_data.id:
return Users.update_user_role_by_id(form_data.id, form_data.role)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)

View file

@ -13,6 +13,8 @@ class ERROR_MESSAGES(str, Enum):
INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
UNAUTHORIZED = "401 Unauthorized"
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
NOT_FOUND = "We could not find what you're looking for :/"
ACTION_PROHIBITED = (
"The requested action has been restricted as a security measure."
)
USER_NOT_FOUND = "We could not find what you're looking for :/"
MALICIOUS = "Unusual activities detected, please try again in a few minutes."

View file

@ -389,31 +389,33 @@
<div class=" self-center">Add-ons</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 ===
'auth'
? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
on:click={() => {
selectedTab = 'auth';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M12.516 2.17a.75.75 0 00-1.032 0 11.209 11.209 0 01-7.877 3.08.75.75 0 00-.722.515A12.74 12.74 0 002.25 9.75c0 5.942 4.064 10.933 9.563 12.348a.749.749 0 00.374 0c5.499-1.415 9.563-6.406 9.563-12.348 0-1.39-.223-2.73-.635-3.985a.75.75 0 00-.722-.516l-.143.001c-2.996 0-5.717-1.17-7.734-3.08zm3.094 8.016a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">Authentication</div>
</button>
{#if !$config || ($config && !$config.auth)}
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'auth'
? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
on:click={() => {
selectedTab = 'auth';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M12.516 2.17a.75.75 0 00-1.032 0 11.209 11.209 0 01-7.877 3.08.75.75 0 00-.722.515A12.74 12.74 0 002.25 9.75c0 5.942 4.064 10.933 9.563 12.348a.749.749 0 00.374 0c5.499-1.415 9.563-6.406 9.563-12.348 0-1.39-.223-2.73-.635-3.985a.75.75 0 00-.722-.516l-.143.001c-2.996 0-5.717-1.17-7.734-3.08zm3.094 8.016a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">Authentication</div>
</button>
{/if}
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===

View file

@ -398,7 +398,11 @@
}}
>
<div class=" self-center mr-3">
<img src={$user.profile_image_url} class=" max-w-[30px] object-cover rounded-full" />
<img
src={$user.profile_image_url}
class=" max-w-[30px] object-cover rounded-full"
alt="User profile"
/>
</div>
<div class=" self-center font-semibold">{$user.name}</div>
</button>
@ -409,6 +413,33 @@
class="absolute z-10 bottom-[70px] 4.5rem rounded-lg shadow w-[240px] bg-gray-900"
>
<div class="py-2 w-full">
{#if $user.role === 'admin'}
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
on:click={() => {
goto('/admin');
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div class=" self-center font-medium">Admin Panel</div>
</button>
{/if}
<button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-800 transition"
on:click={() => {

View file

@ -0,0 +1,154 @@
<script>
import { WEBUI_API_BASE_URL } from '$lib/constants';
import { config, user } from '$lib/stores';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import toast from 'svelte-french-toast';
let loaded = false;
let users = [];
const updateUserRole = async (id, role) => {
const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.token}`
},
body: JSON.stringify({
id: id,
role: role
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((error) => {
console.log(error);
toast.error(error.detail);
return null;
});
if (res) {
await getUsers();
}
};
const getUsers = async () => {
const res = await fetch(`${WEBUI_API_BASE_URL}/users/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((error) => {
console.log(error);
toast.error(error.detail);
return null;
});
users = res ? res : [];
};
onMount(async () => {
if ($config === null || !$config.auth || ($config.auth && $user && $user.role !== 'admin')) {
await goto('/');
} else {
await getUsers();
}
loaded = true;
});
</script>
<div
class=" bg-white dark:bg-gray-800 dark:text-gray-100 min-h-screen w-full flex justify-center font-mona"
>
{#if loaded}
<div class="w-full max-w-3xl px-10 md:px-16 min-h-screen flex flex-col">
<div class="py-10 w-full">
<div class=" flex flex-col justify-center">
<div class=" text-2xl font-semibold">Users ({users.length})</div>
<div class=" text-gray-500 text-xs font-medium mt-1">
Click on the user role cell in the table to change a user's role.
</div>
<hr class=" my-3 dark:border-gray-600" />
<div class="scrollbar-hidden relative overflow-x-auto whitespace-nowrap">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"
>
<tr>
<th scope="col" class="px-6 py-3"> Name </th>
<th scope="col" class="px-6 py-3"> Email </th>
<th scope="col" class="px-6 py-3"> Role </th>
<!-- <th scope="col" class="px-6 py-3"> Action </th> -->
</tr>
</thead>
<tbody>
{#each users as user}
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
<th
scope="row"
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
>
<div class="flex flex-row">
<img
class=" rounded-full max-w-[30px] max-h-[30px] object-cover mr-4"
src={user.profile_image_url}
/>
<div class=" font-semibold md:self-center">{user.name}</div>
</div>
</th>
<td class="px-6 py-4"> {user.email} </td>
<td class="px-6 py-4">
<button
class=" text-white underline"
on:click={() => {
if (user.role === 'user') {
updateUserRole(user.id, 'admin');
} else if (user.role === 'pending') {
updateUserRole(user.id, 'user');
} else {
updateUserRole(user.id, 'pending');
}
}}>{user.role}</button
>
</td>
<!-- <td class="px-6 py-4 text-center">
<button class=" text-white underline"> Edit </button>
</td> -->
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
</div>
{/if}
</div>
<style>
.font-mona {
font-family: 'Mona Sans';
}
.scrollbar-hidden::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.scrollbar-hidden {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>

File diff suppressed because it is too large Load diff