Skip to content

Commit 9959aff

Browse files
committed
Adding support for roles API in frontend
1 parent dbde8f0 commit 9959aff

File tree

8 files changed

+848
-7
lines changed

8 files changed

+848
-7
lines changed

backend/open_webui/models/roles.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class RoleModel(BaseModel):
5555
# Forms
5656
####################
5757

58-
class RoleAddForm(BaseModel):
58+
class RoleForm(BaseModel):
5959
role: str
6060

6161

@@ -94,6 +94,14 @@ def get_role_by_name(self, name: str) -> Optional[RoleModel]:
9494
except Exception:
9595
return None
9696

97+
def update_name_by_id(self, role_id: str, name: str) -> Optional[RoleModel]:
98+
with get_db() as db:
99+
db.query(Role).filter_by(id=role_id).update(
100+
{"name": name, "updated_at": int(time.time())}
101+
)
102+
db.commit()
103+
return self.get_role_by_id(role_id)
104+
97105
def get_roles(self, skip: Optional[int] = None, limit: Optional[int] = None) -> list[RoleModel]:
98106
with get_db() as db:
99107

backend/open_webui/routers/roles.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from open_webui.models.roles import (
99
RoleModel,
1010
Roles,
11-
RoleAddForm
11+
RoleForm
1212
)
1313
from open_webui.models.permissions import (
1414
Permissions,
@@ -40,7 +40,7 @@ async def get_roles(skip: Optional[int] = None, limit: Optional[int] = None, use
4040

4141

4242
@router.post("/", response_model=Optional[RoleModel])
43-
async def add_role(form_data: RoleAddForm, user=Depends(get_admin_user)):
43+
async def add_role(form_data: RoleForm, user=Depends(get_admin_user)):
4444
# Check if the role already exists
4545
existing_role = Roles.get_role_by_name(name=form_data.role)
4646
if existing_role:
@@ -52,6 +52,22 @@ async def add_role(form_data: RoleAddForm, user=Depends(get_admin_user)):
5252
return Roles.insert_new_role(name=form_data.role)
5353

5454

55+
############################
56+
# UpdateRoleById
57+
############################
58+
59+
@router.post("/{role_id}", response_model=Optional[RoleModel])
60+
async def update_role_name(role_id: int, form_data: RoleForm, user=Depends(get_admin_user)):
61+
# Check if the role already exists
62+
existing_role = Roles.get_role_by_id(role_id)
63+
if not existing_role:
64+
raise HTTPException(
65+
status_code=status.HTTP_409_CONFLICT,
66+
detail=f"Role with name '{form_data.role}' do not exists"
67+
)
68+
69+
return Roles.update_name_by_id(role_id, form_data.role)
70+
5571
############################
5672
# DeleteRoleById
5773
############################

src/lib/apis/roles/index.ts

Lines changed: 127 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const getRoles = async (token: string) => {
2828
return res;
2929
};
3030

31-
export const addRole = async (token: string, role: string) => {
31+
export const addRole = async (token: string, roleName: string) => {
3232
let error = null;
3333

3434
const res = await fetch(`${WEBUI_API_BASE_URL}/roles/`, {
@@ -38,7 +38,7 @@ export const addRole = async (token: string, role: string) => {
3838
Authorization: `Bearer ${token}`
3939
},
4040
body: JSON.stringify({
41-
role: role
41+
role: roleName
4242
})
4343
})
4444
.then(async (res) => {
@@ -58,10 +58,40 @@ export const addRole = async (token: string, role: string) => {
5858
return res;
5959
};
6060

61-
export const deleteRole = async (token: string, roleId: string) => {
61+
export const updateRole = async (token: string, roleId: number, roleName: string) => {
6262
let error = null;
6363

6464
const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleId}`, {
65+
method: 'POST',
66+
headers: {
67+
'Content-Type': 'application/json',
68+
Authorization: `Bearer ${token}`
69+
},
70+
body: JSON.stringify({
71+
role: roleName
72+
})
73+
})
74+
.then(async (res) => {
75+
if (!res.ok) throw await res.json();
76+
return res.json();
77+
})
78+
.catch((err) => {
79+
console.log(err);
80+
error = err.detail;
81+
return null;
82+
});
83+
84+
if (error) {
85+
throw error;
86+
}
87+
88+
return res;
89+
};
90+
91+
export const deleteRole = async (token: string, roleName: string) => {
92+
let error = null;
93+
94+
const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}`, {
6595
method: 'DELETE',
6696
headers: {
6797
'Content-Type': 'application/json',
@@ -83,4 +113,97 @@ export const deleteRole = async (token: string, roleId: string) => {
83113
}
84114

85115
return res;
86-
};
116+
};
117+
118+
export const getRolePermissions = async (token: string, roleName: string ) => {
119+
let error = null;
120+
121+
const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permissions`, {
122+
method: 'GET',
123+
headers: {
124+
'Content-Type': 'application/json',
125+
Authorization: `Bearer ${token}`
126+
}
127+
})
128+
.then(async (res) => {
129+
if (!res.ok) throw await res.json();
130+
return res.json();
131+
})
132+
.catch((err) => {
133+
console.log(err);
134+
error = err.detail;
135+
return null;
136+
});
137+
138+
if (error) {
139+
throw error;
140+
}
141+
142+
return res;
143+
};
144+
145+
// GET /api/v1/roles/{role_name}/permission Add New Default Permission With Role
146+
// {
147+
// "name": "string",
148+
// "category": "workspace",
149+
// "description": "string",
150+
// "value": false
151+
// }
152+
153+
export const linkRoleToPermissions = async (token: string, roleName: string, categoryName: string, permissionName: string, value: boolean ) => {
154+
let error = null;
155+
const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permission/link`, {
156+
method: 'POST',
157+
headers: {
158+
'Content-Type': 'application/json',
159+
Authorization: `Bearer ${token}`
160+
},
161+
body: JSON.stringify({
162+
permission_name: permissionName,
163+
category: categoryName,
164+
value: value
165+
})
166+
})
167+
.then(async (res) => {
168+
if (!res.ok) throw await res.json();
169+
return res.json();
170+
})
171+
.catch((err) => {
172+
console.log(err);
173+
error = err.detail;
174+
return null;
175+
});
176+
177+
if (error) {
178+
throw error;
179+
}
180+
181+
return res;
182+
};
183+
184+
export const unlinkRoleFromPermissions = async (token: string, roleName: string, categoryName: string, permissionName: string) => {
185+
let error = null;
186+
187+
const res = await fetch(`${WEBUI_API_BASE_URL}/roles/${roleName}/permission/${categoryName}/${permissionName}`, {
188+
method: 'DELETE',
189+
headers: {
190+
'Content-Type': 'application/json',
191+
Authorization: `Bearer ${token}`
192+
},
193+
})
194+
.then(async (res) => {
195+
if (!res.ok) throw await res.json();
196+
return res.json();
197+
})
198+
.catch((err) => {
199+
console.log(err);
200+
error = err.detail;
201+
return null;
202+
});
203+
204+
if (error) {
205+
throw error;
206+
}
207+
208+
return res;
209+
};

src/lib/components/admin/Users.svelte

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { getUsers } from '$lib/apis/users';
99
1010
import UserList from './Users/UserList.svelte';
11+
import Roles from './Users/RoleList.svelte';
1112
import Groups from './Users/Groups.svelte';
1213
1314
const i18n = getContext('i18n');
@@ -98,13 +99,39 @@
9899
</div>
99100
<div class=" self-center">{$i18n.t('Groups')}</div>
100101
</button>
102+
103+
<button
104+
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition {selectedTab ===
105+
'roles'
106+
? ''
107+
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
108+
on:click={() => {
109+
selectedTab = 'roles';
110+
}}
111+
>
112+
<div class=" self-center mr-2">
113+
<svg
114+
xmlns="http://www.w3.org/2000/svg"
115+
viewBox="0 0 640 512"
116+
fill="currentColor"
117+
class="size-4"
118+
>
119+
<path
120+
d="M144 160A80 80 0 1 0 144 0a80 80 0 1 0 0 160zm368 0A80 80 0 1 0 512 0a80 80 0 1 0 0 160zM0 298.7C0 310.4 9.6 320 21.3 320l213.3 0c.2 0 .4 0 .7 0c-26.6-23.5-43.3-57.8-43.3-96c0-7.6 .7-15 1.9-22.3c-13.6-6.3-28.7-9.7-44.6-9.7l-42.7 0C47.8 192 0 239.8 0 298.7zM320 320c24 0 45.9-8.8 62.7-23.3c2.5-3.7 5.2-7.3 8-10.7c2.7-3.3 5.7-6.1 9-8.3C410 262.3 416 243.9 416 224c0-53-43-96-96-96s-96 43-96 96s43 96 96 96zm65.4 60.2c-10.3-5.9-18.1-16.2-20.8-28.2l-103.2 0C187.7 352 128 411.7 128 485.3c0 14.7 11.9 26.7 26.7 26.7l300.6 0c-2.1-5.2-3.2-10.9-3.2-16.4l0-3c-1.3-.7-2.7-1.5-4-2.3l-2.6 1.5c-16.8 9.7-40.5 8-54.7-9.7c-4.5-5.6-8.6-11.5-12.4-17.6l-.1-.2-.1-.2-2.4-4.1-.1-.2-.1-.2c-3.4-6.2-6.4-12.6-9-19.3c-8.2-21.2 2.2-42.6 19-52.3l2.7-1.5c0-.8 0-1.5 0-2.3s0-1.5 0-2.3l-2.7-1.5zM533.3 192l-42.7 0c-15.9 0-31 3.5-44.6 9.7c1.3 7.2 1.9 14.7 1.9 22.3c0 17.4-3.5 33.9-9.7 49c2.5 .9 4.9 2 7.1 3.3l2.6 1.5c1.3-.8 2.6-1.6 4-2.3l0-3c0-19.4 13.3-39.1 35.8-42.6c7.9-1.2 16-1.9 24.2-1.9s16.3 .6 24.2 1.9c22.5 3.5 35.8 23.2 35.8 42.6l0 3c1.3 .7 2.7 1.5 4 2.3l2.6-1.5c16.8-9.7 40.5-8 54.7 9.7c2.3 2.8 4.5 5.8 6.6 8.7c-2.1-57.1-49-102.7-106.6-102.7zm91.3 163.9c6.3-3.6 9.5-11.1 6.8-18c-2.1-5.5-4.6-10.8-7.4-15.9l-2.3-4c-3.1-5.1-6.5-9.9-10.2-14.5c-4.6-5.7-12.7-6.7-19-3l-2.9 1.7c-9.2 5.3-20.4 4-29.6-1.3s-16.1-14.5-16.1-25.1l0-3.4c0-7.3-4.9-13.8-12.1-14.9c-6.5-1-13.1-1.5-19.9-1.5s-13.4 .5-19.9 1.5c-7.2 1.1-12.1 7.6-12.1 14.9l0 3.4c0 10.6-6.9 19.8-16.1 25.1s-20.4 6.6-29.6 1.3l-2.9-1.7c-6.3-3.6-14.4-2.6-19 3c-3.7 4.6-7.1 9.5-10.2 14.6l-2.3 3.9c-2.8 5.1-5.3 10.4-7.4 15.9c-2.6 6.8 .5 14.3 6.8 17.9l2.9 1.7c9.2 5.3 13.7 15.8 13.7 26.4s-4.5 21.1-13.7 26.4l-3 1.7c-6.3 3.6-9.5 11.1-6.8 17.9c2.1 5.5 4.6 10.7 7.4 15.8l2.4 4.1c3 5.1 6.4 9.9 10.1 14.5c4.6 5.7 12.7 6.7 19 3l2.9-1.7c9.2-5.3 20.4-4 29.6 1.3s16.1 14.5 16.1 25.1l0 3.4c0 7.3 4.9 13.8 12.1 14.9c6.5 1 13.1 1.5 19.9 1.5s13.4-.5 19.9-1.5c7.2-1.1 12.1-7.6 12.1-14.9l0-3.4c0-10.6 6.9-19.8 16.1-25.1s20.4-6.6 29.6-1.3l2.9 1.7c6.3 3.6 14.4 2.6 19-3c3.7-4.6 7.1-9.4 10.1-14.5l2.4-4.2c2.8-5.1 5.3-10.3 7.4-15.8c2.6-6.8-.5-14.3-6.8-17.9l-3-1.7c-9.2-5.3-13.7-15.8-13.7-26.4s4.5-21.1 13.7-26.4l3-1.7zM472 384a40 40 0 1 1 80 0 40 40 0 1 1 -80 0z"
121+
/>
122+
</svg>
123+
</div>
124+
<div class=" self-center">{$i18n.t('Roles')}</div>
125+
</button>
101126
</div>
102127
103128
<div class="flex-1 mt-1 lg:mt-0 overflow-y-scroll">
104129
{#if selectedTab === 'overview'}
105130
<UserList {users} />
106131
{:else if selectedTab === 'groups'}
107132
<Groups {users} />
133+
{:else if selectedTab === 'roles'}
134+
<Roles />
108135
{/if}
109136
</div>
110137
</div>

0 commit comments

Comments
 (0)