Skip to content

Commit

Permalink
progress
Browse files Browse the repository at this point in the history
  • Loading branch information
kayra1 committed Oct 15, 2024
1 parent 303b558 commit 3a61285
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 62 deletions.
97 changes: 97 additions & 0 deletions app/(nms)/users/modals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { UserEntry } from "@/components/types"
import { useAuth } from "@/utils/auth"
import { changePassword, deleteUser } from "@/utils/queries"
import { passwordIsValid } from "@/utils/utils"
import { Button, Form, Input, Modal, PasswordToggle } from "@canonical/react-components"
import { useMutation } from "@tanstack/react-query"
import { ChangeEvent, useState } from "react"


type accountDeleteActionModalData = {
user: UserEntry
closeFn: () => void
}

export function DeleteModal({ user, closeFn }: accountDeleteActionModalData) {
const auth = useAuth()
const deleteMutation = useMutation({
mutationFn: deleteUser,
onSuccess: () => {
closeFn()
}
})
return (
<Modal
title="Confirm delete"
buttonRow={
<>
<Button onClick={() => deleteMutation.mutate({ authToken: auth.user ? auth.user.authToken : "", id: user.id.toString() })}>Confirm</Button>
<Button onClick={closeFn}>Cancel</Button>
</>
}>
<p>Delete user {user.username}?</p>
<p>This action is irreversible.</p>
</Modal >
)
}

type accountChangePasswordActionModalData = {
user: UserEntry
closeFn: () => void
}

export function ChangePasswordModal({ user, closeFn }: accountChangePasswordActionModalData) {
const auth = useAuth()
const changePWMutation = useMutation({
mutationFn: changePassword,
onSuccess: () => {
closeFn()
}
})
const [password1, setPassword1] = useState<string>("")
const [password2, setPassword2] = useState<string>("")
const passwordsMatch = password1 === password2
const password1Error = password1 && !passwordIsValid(password1) ? "Password is not valid" : ""
const password2Error = password2 && !passwordsMatch ? "Passwords do not match" : ""

const [errorText, setErrorText] = useState<string>("")
const handlePassword1Change = (event: ChangeEvent<HTMLInputElement>) => { setPassword1(event.target.value) }
const handlePassword2Change = (event: ChangeEvent<HTMLInputElement>) => { setPassword2(event.target.value) }
return (
<Modal>
<Form>
<div className="p-form__group row">
<Input
id="InputUsername"
label="Username"
type="text"
value={user.username}
disabled={true}
/>
<PasswordToggle
help="Password must have 8 or more characters, must include at least one capital letter, one lowercase letter, and either a number or a symbol."
id="password1"
label="Password"
onChange={handlePassword1Change}
required={true}
error={password1Error}
/>
<PasswordToggle
id="password2"
label="Password"
onChange={handlePassword2Change}
required={true}
error={password2Error}
/>
<Button
appearance="positive"
disabled={!passwordsMatch || !passwordIsValid(password1)}
onClick={(event) => { changePWMutation.mutate({ authToken: (auth.user ? auth.user.authToken : ""), id: user.id.toString(), password: password1 }) }}
>
Submit
</Button>
</div>
</Form>
</Modal>
)
}
143 changes: 82 additions & 61 deletions app/(nms)/users/page.tsx
Original file line number Diff line number Diff line change
@@ -1,79 +1,100 @@
"use client"

import { useQuery } from "@tanstack/react-query"
import { ListUsers } from "@/utils/queries"
import { listUsers } from "@/utils/queries"
import { UserEntry } from "@/components/types"
import Loader from "@/components/Loader"
import { useAuth } from "@/utils/auth"
import PageHeader from "@/components/PageHeader"
import { Button, MainTable } from "@canonical/react-components"
import { Button, ContextualMenu, MainTable, Modal } from "@canonical/react-components"
import PageContent from "@/components/PageContent"
import { useState } from "react"
import SyncOutlinedIcon from "@mui/icons-material/SyncOutlined";
import { ChangePasswordModal, DeleteModal } from "./modals"

export default function Users() {
const [isCreateModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [isEditModalVisible, setEditModalVisible] = useState<boolean>(false);
type modalData = {
user: UserEntry
type: "delete" | "change password"
}

const toggleCreateModal = () => setCreateModalVisible((prev) => !prev);
const toggleEditModal = () => setEditModalVisible((prev) => !prev);
export default function Users() {
const [modalData, setModalData] = useState<modalData | null>(null);

const auth = useAuth()
const auth = useAuth()
const query = useQuery<UserEntry[], Error>({
queryKey: ['users'],
queryFn: () => listUsers({ authToken: auth.user ? auth.user.authToken : "" }),
retry: (failureCount, error): boolean => {
if (error.message.includes("401")) {
return false
}
return true
},
})
if (query.status == "pending") { return <Loader text="loading..." /> }
if (query.status == "error") {
if (query.error.message.includes("401")) {
auth.logout()
}
return <p>{query.error.message}</p>
}

const query = useQuery<UserEntry[], Error>({
queryKey: ['users'],
queryFn: () => ListUsers({ authToken: auth.user ? auth.user.authToken : "" }),
retry: (failureCount, error): boolean => {
if (error.message.includes("401")) {
return false
}
return true
const users = Array.from(query.data ? query.data : [])
const tableContent = users.map((user) => {
return {
key: user.id,
columns: [
{ content: user.username },
{
content: (
<ContextualMenu
links={[
{
children: "Delete account",
disabled: user.id == 1,
onClick: () => setModalData({ user: user, type: "delete" })
}, {
children: "Change password",
onClick: () => setModalData({ user: user, type: "change password" })
}
]}
hasToggleIcon
position={"right"}
/>
),
},
})

const handleRefresh = async () => {
await queryClient.invalidateQueries({ queryKey: [queryKeys.subscribers] });
await queryClient.invalidateQueries({ queryKey: [queryKeys.deviceGroups] });
await queryClient.invalidateQueries({ queryKey: [queryKeys.networkSlices] });
],
};
});

if (query.status == "pending") { return <Loader text="loading..." /> }
if (query.status == "error") {
if (query.error.message.includes("401")) {
auth.logout()
}
return <p>{query.error.message}</p>
}
const users = Array.from(query.data ? query.data : [])
return (
<>
<PageHeader title={`NMS Accounts (${users.length})`}>
<Button
hasIcon
appearance="base"
onClick={handleRefresh}
title="refresh subscriber list"
>
<SyncOutlinedIcon style={{ color: "#666" }} />
</Button>
<Button appearance="positive" onClick={toggleCreateModal}>
Create
</Button>
</PageHeader>
<PageContent>
<MainTable
defaultSort='"abcd"'
defaultSortDirection="ascending"
headers={[
{ content: "IMSI" },
{ content: "Actions", className: "u-align--right" },
]}
rows={tableContent}
/>
</PageContent>
{isCreateModalVisible && <SubscriberModal toggleModal={toggleCreateModal} slices={slices} deviceGroups={deviceGroups} />}
{isEditModalVisible &&
<SubscriberModal toggleModal={toggleEditModal} subscriber={subscriber} slices={slices} deviceGroups={deviceGroups} />}
</>
)
return (
<>
<PageHeader title={`NMS Accounts (${users.length})`}>
<Button
hasIcon
appearance="base"
onClick={() => { query.refetch() }}
title="refresh accounts list"
>
<SyncOutlinedIcon style={{ color: "#666" }} />
</Button>
<Button appearance="positive" >
Create
</Button>
</PageHeader>
<PageContent>
<MainTable
defaultSort='"abcd"'
defaultSortDirection="ascending"
headers={[
{ content: "IMSI" },
{ content: "Actions", className: "u-align--right" },
]}
rows={tableContent}
/>
</PageContent>
{modalData?.type == "delete" && <DeleteModal user={modalData.user} closeFn={() => setModalData(null)} />}
{modalData?.type == "change password" && <ChangePasswordModal user={modalData.user} closeFn={() => setModalData(null)} />}
</>
)
}
31 changes: 30 additions & 1 deletion utils/queries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { UserEntry } from "@/components/types";

export async function ListUsers(params: { authToken: string }): Promise<UserEntry[]> {
export async function listUsers(params: { authToken: string }): Promise<UserEntry[]> {
const response = await fetch("/config/v1/account", {
headers: { "Authorization": "Bearer " + params.authToken }
})
Expand All @@ -11,6 +11,35 @@ export async function ListUsers(params: { authToken: string }): Promise<UserEntr
return respData.result
}

export async function deleteUser(params: { authToken: string, id: string }) {
const response = await fetch("/config/v1/account/" + params.id, {
method: 'delete',
headers: {
'Authorization': "Bearer " + params.authToken
}
})
const respData = await response.json();
if (!response.ok) {
throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`)
}
return respData.result
}

export async function changePassword(changePasswordForm: { authToken: string, id: string, password: string }) {
const response = await fetch("/api/v1/accounts/" + changePasswordForm.id + "/change_password", {
method: "POST",
headers: {
'Authorization': 'Bearer ' + changePasswordForm.authToken
},
body: JSON.stringify({ "password": changePasswordForm.password })
})
const respData = await response.json();
if (!response.ok) {
throw new Error(`${response.status}: ${HTTPStatus(response.status)}. ${respData.error}`)
}
return respData.result
}

export const HTTPStatus = (code: number): string => {
const map: { [key: number]: string } = {
400: "Bad Request",
Expand Down
28 changes: 28 additions & 0 deletions utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const passwordIsValid = (pw: string) => {
if (pw.length < 8) return false

const result = {
hasCapital: false,
hasLowercase: false,
hasSymbol: false,
hasNumber: false
};

if (/[A-Z]/.test(pw)) {
result.hasCapital = true;
}
if (/[a-z]/.test(pw)) {
result.hasLowercase = true;
}
if (/[0-9]/.test(pw)) {
result.hasNumber = true;
}
if (/[^A-Za-z0-9]/.test(pw)) {
result.hasSymbol = true;
}

if (result.hasCapital && result.hasLowercase && (result.hasSymbol || result.hasNumber)) {
return true
}
return false
}

0 comments on commit 3a61285

Please sign in to comment.