Skip to content

Commit 08ce55c

Browse files
longshuicylmarini
andauthored
842 UI component for assign admin (#845)
* add admin to the right model so it's reflect in the return object * populate manage users page * put admin to correct place; update faker script * add faker output file to gitignore * add pagination * set and revoke logic works now * add error modal * update revoke admin logic * disable button that is the current user * wire in the search user * add padding * add gravatar * black format * linting * update use switch * Drop metadata definition in mongo-delete.js. * move to sidebar --------- Co-authored-by: Luigi Marini <[email protected]>
1 parent 66abc2e commit 08ce55c

File tree

19 files changed

+632
-277
lines changed

19 files changed

+632
-277
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,7 @@ backend/app/tests/*
7878
!clowder-theme.tgz
7979
*clowder2-software-dev.yaml
8080
secrets.yaml
81+
82+
# faker
83+
official.csv
84+
fact.png

backend/app/models/users.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ class UserLogin(BaseModel):
2424

2525

2626
class UserDoc(Document, UserBase):
27+
admin: bool
28+
2729
class Settings:
2830
name = "users"
2931

3032

3133
class UserDB(UserDoc):
3234
hashed_password: str = Field()
3335
keycloak_id: Optional[str] = None
34-
admin: bool
3536

3637
def verify_password(self, password):
3738
return pwd_context.verify(password, self.hashed_password)

backend/app/routers/authentication.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22

3+
from beanie import PydanticObjectId
34
from fastapi import APIRouter, HTTPException, Depends
45
from keycloak.exceptions import (
56
KeycloakAuthenticationError,
@@ -8,8 +9,6 @@
89
)
910
from passlib.hash import bcrypt
1011

11-
from beanie import PydanticObjectId
12-
1312
from app.keycloak_auth import create_user, get_current_user
1413
from app.keycloak_auth import keycloak_openid
1514
from app.models.datasets import DatasetDB
@@ -113,7 +112,7 @@ async def get_admin(dataset_id: str = None, current_username=Depends(get_current
113112
async def set_admin(
114113
useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin)
115114
):
116-
if admin:
115+
if admin and current_username.admin:
117116
if (user := await UserDB.find_one(UserDB.email == useremail)) is not None:
118117
user.admin = True
119118
await user.replace()
@@ -125,3 +124,29 @@ async def set_admin(
125124
status_code=403,
126125
detail=f"User {current_username.email} is not an admin. Only admin can make others admin.",
127126
)
127+
128+
129+
@router.post("/users/revoke_admin/{useremail}", response_model=UserOut)
130+
async def revoke_admin(
131+
useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin)
132+
):
133+
if admin:
134+
if current_username.email == useremail:
135+
raise HTTPException(
136+
status_code=403,
137+
detail=f"You are currently an admin. Admin cannot revoke their own admin access.",
138+
)
139+
else:
140+
if (user := await UserDB.find_one(UserDB.email == useremail)) is not None:
141+
user.admin = False
142+
await user.replace()
143+
return user.dict()
144+
else:
145+
raise HTTPException(
146+
status_code=404, detail=f"User {useremail} not found"
147+
)
148+
else:
149+
raise HTTPException(
150+
status_code=403,
151+
detail=f"User {current_username.email} is not an admin. Only admin can revoke admin access.",
152+
)

frontend/src/actions/user.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,39 @@ export function fetchUserProfile() {
231231
});
232232
};
233233
}
234+
235+
export const SET_ADMIN = "SET_ADMIN";
236+
237+
export function setAdmin(email) {
238+
return (dispatch) => {
239+
return V2.LoginService.setAdminApiV2UsersSetAdminUseremailPost(email)
240+
.then((json) => {
241+
dispatch({
242+
type: SET_ADMIN,
243+
profile: json,
244+
receivedAt: Date.now(),
245+
});
246+
})
247+
.catch((reason) => {
248+
dispatch(handleErrors(reason, setAdmin(email)));
249+
});
250+
};
251+
}
252+
253+
export const REVOKE_ADMIN = "REVOKE_ADMIN";
254+
255+
export function revokeAdmin(email) {
256+
return (dispatch) => {
257+
return V2.LoginService.revokeAdminApiV2UsersRevokeAdminUseremailPost(email)
258+
.then((json) => {
259+
dispatch({
260+
type: REVOKE_ADMIN,
261+
profile: json,
262+
receivedAt: Date.now(),
263+
});
264+
})
265+
.catch((reason) => {
266+
dispatch(handleErrors(reason, revokeAdmin(email)));
267+
});
268+
};
269+
}

frontend/src/components/Layout.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import ListItemIcon from "@mui/material/ListItemIcon";
1818
import ListItemText from "@mui/material/ListItemText";
1919
import { Link, Menu, MenuItem, MenuList, Typography } from "@mui/material";
2020
import { Link as RouterLink, useLocation } from "react-router-dom";
21-
import { useSelector } from "react-redux";
21+
import { useDispatch, useSelector } from "react-redux";
2222
import { RootState } from "../types/data";
2323
import { AddBox, Explore } from "@material-ui/icons";
2424
import HistoryIcon from "@mui/icons-material/History";
@@ -30,6 +30,8 @@ import { getCurrEmail } from "../utils/common";
3030
import VpnKeyIcon from "@mui/icons-material/VpnKey";
3131
import LogoutIcon from "@mui/icons-material/Logout";
3232
import { EmbeddedSearch } from "./search/EmbeddedSearch";
33+
import { fetchUserProfile } from "../actions/user";
34+
import ManageAccountsIcon from "@mui/icons-material/ManageAccounts";
3335

3436
const drawerWidth = 240;
3537

@@ -103,6 +105,9 @@ export default function PersistentDrawerLeft(props) {
103105
const [embeddedSearchHidden, setEmbeddedSearchHidden] = React.useState(false);
104106
const [anchorEl, setAnchorEl] = React.useState(null);
105107
const isMenuOpen = Boolean(anchorEl);
108+
const profile = useSelector((state: RootState) => state.user.profile);
109+
const dispatch = useDispatch();
110+
const fetchProfile = () => dispatch(fetchUserProfile());
106111

107112
const handleDrawerOpen = () => {
108113
setOpen(true);
@@ -128,6 +133,7 @@ export default function PersistentDrawerLeft(props) {
128133
} else {
129134
setEmbeddedSearchHidden(false);
130135
}
136+
fetchProfile();
131137
}, [location]);
132138

133139
const loggedOut = useSelector((state: RootState) => state.error.loggedOut);
@@ -278,6 +284,21 @@ export default function PersistentDrawerLeft(props) {
278284
</ListItem>
279285
</List>
280286
<Divider />
287+
{profile.admin ? (
288+
<>
289+
<List>
290+
<ListItem key={"manage-user"} disablePadding>
291+
<ListItemButton component={RouterLink} to="/manage-users">
292+
<ListItemIcon>
293+
<ManageAccountsIcon />
294+
</ListItemIcon>
295+
<ListItemText primary={"Manage Users"} />
296+
</ListItemButton>
297+
</ListItem>
298+
</List>
299+
<Divider />
300+
</>
301+
) : null}
281302
<List>
282303
<ListItem key={"groups"} disablePadding>
283304
<ListItemButton component={RouterLink} to="/groups">
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)