Skip to content

Commit

Permalink
Lickem22 user management (#455)
Browse files Browse the repository at this point in the history
- Add new user management page for admins
- Fix UX functionality - logged in admin should not be able to un-admin themselves
- Don't show user management page to non-admins
---------

Co-authored-by: amir_emami <[email protected]>
  • Loading branch information
lickem22 and amiraliemami authored Dec 9, 2024
1 parent 4b9e420 commit b58f261
Show file tree
Hide file tree
Showing 20 changed files with 1,178 additions and 300 deletions.
13 changes: 11 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@
"line_number": 62
}
],
".github/workflows/deploy_gcp_core_backend.yaml": [
{
"type": "Secret Keyword",
"filename": ".github/workflows/deploy_gcp_core_backend.yaml",
"hashed_secret": "14a9a30abdb4f24769083489080470ca78f002f6",
"is_verified": false,
"line_number": 64
}
],
".github/workflows/deploy_gcp_litellm_proxy.yaml": [
{
"type": "Secret Keyword",
Expand Down Expand Up @@ -455,7 +464,7 @@
"filename": "core_backend/tests/api/test_user_tools.py",
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
"is_verified": false,
"line_number": 47
"line_number": 70
}
],
"core_backend/tests/rails/test_language_identification.py": [
Expand Down Expand Up @@ -572,5 +581,5 @@
}
]
},
"generated_at": "2024-11-15T14:09:33Z"
"generated_at": "2024-11-13T20:05:41Z"
}
4 changes: 2 additions & 2 deletions admin_app/src/app/integrations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import React, { useState } from "react";
import { format } from "date-fns";
import { Layout } from "@/components/Layout";
import { appColors, sizes } from "@/utils";
import { apiCalls } from "@/utils/api";
import { createNewApiKey } from "./api";
import { useAuth } from "@/utils/auth";

import { KeyRenewConfirmationModal, NewKeyModal } from "./components/APIKeyModals";
import ConnectionsGrid from "./components/ConnectionsGrid";
import { LoadingButton } from "@mui/lab";
import { getUser } from "../user-management/api";

const IntegrationsPage = () => {
const [currAccessLevel, setCurrAccessLevel] = React.useState("readonly");
Expand Down Expand Up @@ -59,7 +59,7 @@ const KeyManagement = ({
const setApiKeyInfo = async () => {
setKeyInfoFetchIsLoading(true);
try {
const data = await apiCalls.getUser(token!);
const data = await getUser(token!);
setCurrentKey(data.api_key_first_characters);
const formatted_api_update_date = format(
data.api_key_updated_datetime_utc,
Expand Down
235 changes: 11 additions & 224 deletions admin_app/src/app/login/components/RegisterModal.tsx
Original file line number Diff line number Diff line change
@@ -1,162 +1,21 @@
import CheckIcon from "@mui/icons-material/Check";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import {
Alert,
Avatar,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
TextField,
Typography,
} from "@mui/material";
import React, { useState } from "react";

interface User {
username: string;
recovery_codes: string[];
}
interface RegisterModalProps {
open: boolean;
onClose: (event: {}, reason: string) => void;
onContinue: (recoveryCodes: string[]) => void;
registerUser: (username: string, password: string) => Promise<User>;
}

function RegisterModal({
open,
onClose,
onContinue,
registerUser,
}: RegisterModalProps) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [errors, setErrors] = React.useState({
username: false,
password: false,
confirmPassword: false,
confirmPasswordMatch: false,
});
const validateForm = () => {
const newErrors = {
username: username === "",
password: password === "",
confirmPassword: confirmPassword === "",
confirmPasswordMatch: password !== confirmPassword,
};
setErrors(newErrors);
return Object.values(newErrors).every((value) => value === false);
};
const handleRegister = async (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
if (validateForm()) {
console.log("Registering user");

const data = await registerUser(username, password);
if (data && data.username) {
onContinue(data.recovery_codes);
} else {
setErrorMessage("Unexpected response from the server.");
}
}
};

return (
<Dialog open={open} onClose={onClose} aria-labelledby="form-dialog-title">
<DialogContent>
<Box
component="form"
noValidate
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: "24px",
}}
>
<Avatar sx={{ bgcolor: "secondary.main", marginBottom: 4 }}>
<LockOutlinedIcon />
</Avatar>
<Typography variant="h5" align="center" sx={{ marginBottom: 4 }}>
Register Admin User
</Typography>
<Box>
{errorMessage && errorMessage != "" && (
<Alert severity="error" sx={{ marginBottom: 2 }}>
{errorMessage}
</Alert>
)}
</Box>
<TextField
margin="normal"
error={errors.username}
helperText={errors.username ? "Please enter a username" : " "}
required
fullWidth
id="register-username"
label="Username"
name="username"
autoComplete="username"
autoFocus
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<TextField
margin="normal"
error={errors.password}
helperText={errors.password ? "Please enter a password" : " "}
required
fullWidth
name="password"
label="Password"
type="password"
id="register-password"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<TextField
required
fullWidth
name="confirm-password"
label="Confirm Password"
type="password"
id="confirm-password"
error={errors.confirmPasswordMatch}
value={confirmPassword}
helperText={errors.confirmPasswordMatch ? "Passwords do not match" : " "}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<Box
mt={1}
width="100%"
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
>
<Button
onClick={handleRegister}
type="submit"
fullWidth
variant="contained"
sx={{ maxWidth: "120px" }}
>
Register
</Button>
</Box>
</Box>
</DialogContent>
</Dialog>
);
}

import React from "react";
import { UserModal } from "@/app/user-management/components/UserCreateModal";
import type { UserModalProps } from "@/app/user-management/components/UserCreateModal";
const RegisterModal = (props: Omit<UserModalProps, "fields">) => (
<UserModal
{...props}
fields={["username", "password", "confirmPassword"]}
showCancel={false}
/>
);
const AdminAlertModal = ({
open,
onClose,
Expand All @@ -183,76 +42,4 @@ const AdminAlertModal = ({
);
};

const ConfirmationModal = ({
open,
onClose,
recoveryCodes,
}: {
open: boolean;
onClose: () => void;
recoveryCodes: string[];
}) => {
const [isClicked, setIsClicked] = useState(false);

const handleClose = () => {
setIsClicked(false);
onClose();
};

const handleCopyToClipboard = async () => {
try {
await navigator.clipboard.writeText(recoveryCodes.join("\n"));
} catch (err) {
console.error("Failed to copy recovery codes: ", err);
}
};

return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Admin User Created</DialogTitle>
<DialogContent>
<DialogContentText>
The admin user has been successfully registered. Please save the recovery
codes below. You will not be able to see them again.
</DialogContentText>
<TextField
fullWidth
multiline
margin="normal"
value={recoveryCodes ? recoveryCodes.join("\n") : ""}
InputProps={{
readOnly: true,
sx: {
textAlign: "center",
},
}}
inputProps={{
style: { textAlign: "center" },
}}
/>

<Box display="flex" justifyContent="center" mt={2}>
<Button
variant="contained"
onClick={() => {
handleCopyToClipboard();
setIsClicked(true);
}}
startIcon={isClicked ? <CheckIcon /> : <ContentCopyIcon />}
style={{ paddingLeft: "20px", paddingRight: "20px" }}
>
{isClicked ? "Copied" : "Copy"}
</Button>
</Box>
</DialogContent>

<DialogActions sx={{ marginBottom: 1, marginRight: 1 }}>
<Button onClick={handleClose} color="primary" variant="contained" autoFocus>
Back to Login
</Button>
</DialogActions>
</Dialog>
);
};

export { AdminAlertModal, RegisterModal, ConfirmationModal };
export { AdminAlertModal, RegisterModal };
24 changes: 15 additions & 9 deletions admin_app/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ import Typography from "@mui/material/Typography";
import * as React from "react";
import { useEffect } from "react";
import { appColors, sizes } from "@/utils";
import { apiCalls } from "@/utils/api";
import {
AdminAlertModal,
ConfirmationModal,
RegisterModal,
} from "./components/RegisterModal";
getRegisterOption,
registerUser,
UserBody,
UserBodyPassword,
} from "@/app/user-management/api";
import { AdminAlertModal, RegisterModal } from "./components/RegisterModal";
import { ConfirmationModal } from "@/app/user-management/components/ConfirmationModal";
import { LoadingButton } from "@mui/lab";

const NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID: string =
Expand Down Expand Up @@ -62,7 +64,7 @@ const Login = () => {

useEffect(() => {
const fetchRegisterPrompt = async () => {
const data = await apiCalls.getRegisterOption();
const data = await getRegisterOption();
setShowAdminAlertModal(data.require_register);
setIsLoading(false);
};
Expand Down Expand Up @@ -100,12 +102,12 @@ const Login = () => {
}
}, [recoveryCodes]);

const handleAdminModalClose = (event: {}, reason: string) => {
const handleAdminModalClose = (_?: {}, reason?: string) => {
if (reason !== "backdropClick" && reason !== "escapeKeyDown") {
setShowAdminAlertModal(false);
}
};
const handleRegisterModalClose = (event: {}, reason: string) => {
const handleRegisterModalClose = (_?: {}, reason?: string) => {
if (reason !== "backdropClick" && reason !== "escapeKeyDown") {
setShowRegisterModal(false);
}
Expand Down Expand Up @@ -433,12 +435,16 @@ const Login = () => {
open={showRegisterModal}
onClose={handleRegisterModalClose}
onContinue={handleRegisterModalContinue}
registerUser={apiCalls.registerUser}
registerUser={(user: UserBodyPassword | UserBody) => {
const newUser = user as UserBodyPassword;
return registerUser(newUser.username, newUser.password);
}}
/>
<ConfirmationModal
open={showConfirmationModal}
onClose={handleCloseConfirmationModal}
recoveryCodes={recoveryCodes}
closeButtonText="Back to Login"
/>
</Grid>
</Grid>
Expand Down
Loading

0 comments on commit b58f261

Please sign in to comment.