Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Netmanager]: Profile Settings Page #2401

Merged
merged 35 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7314367
Profile Settings
danielmarv Jan 23, 2025
efeceff
User Details
danielmarv Jan 23, 2025
99c47c8
Remove commented-out code from PasswordEdit component
danielmarv Jan 28, 2025
5bfa9b7
commit
danielmarv Jan 28, 2025
dabe3f0
commit
danielmarv Jan 28, 2025
5992733
commit
danielmarv Jan 28, 2025
36741ae
Refactor ApiTokens component to fetch user clients and display them i…
danielmarv Jan 28, 2025
4dac54e
Implement client token creation and enhance client fetching in ApiTok…
danielmarv Jan 28, 2025
3c4ea58
commit
danielmarv Jan 29, 2025
cc432de
Add clients slice and hooks for managing client data; integrate Radix…
danielmarv Jan 29, 2025
91afbf1
Merge remote-tracking branch 'origin/staging' into Daniel-Net
danielmarv Jan 30, 2025
6d5b0d6
Add Clients tab to profile page and adjust layout
danielmarv Jan 30, 2025
d97fc00
Add permission-based visibility for Clients tab in profile
danielmarv Jan 30, 2025
5a82718
Fix client email display to handle undefined user cases
danielmarv Jan 30, 2025
5ff6a08
Refactor client activation API call to use POST method and update dat…
danielmarv Feb 1, 2025
6375ae9
Remove "Copy ID" button from client management table
danielmarv Feb 1, 2025
cf77a7f
Remove unused handleCopyClientId function from ClientManagement compo…
danielmarv Feb 1, 2025
4ce1cc3
Enhance MyProfile component with improved user data fetching and erro…
danielmarv Feb 1, 2025
6cb5db2
Enhance MyProfile component with loading indicator and error display;…
danielmarv Feb 1, 2025
5307a67
Add Cloudinary image upload API and update user details API; refactor…
danielmarv Feb 1, 2025
c0f547d
Implement password change functionality with validation and strength …
danielmarv Feb 2, 2025
71ff5a8
Refactor updateUserPasswordApi to use query parameters for user ID; a…
danielmarv Feb 2, 2025
845c0fa
Merge remote-tracking branch 'origin/staging' into Daniel-Net
danielmarv Feb 2, 2025
48d88c9
commit
danielmarv Feb 4, 2025
83a4cfb
Refactor token generation error handling and remove unnecessary toast…
danielmarv Feb 4, 2025
d635349
Refactor API imports in settings components to use a centralized sett…
danielmarv Feb 5, 2025
041bafd
Rename "Password Edit" tab to "Reset Password" for improved clarity
danielmarv Feb 5, 2025
84907a3
Refactor token generation and retrieval logic for improved error hand…
danielmarv Feb 5, 2025
bb70faf
fix pagination
Codebmk Feb 5, 2025
9887f72
fix update button
Codebmk Feb 5, 2025
6680208
Enhance API token management by improving access token checks and add…
danielmarv Feb 5, 2025
882aa7b
Merge branch 'Daniel-Net' of https://github.com/airqo-platform/AirQo-…
danielmarv Feb 5, 2025
0318786
Add ClientsDetails interface and improve access token display in User…
danielmarv Feb 5, 2025
4aff39c
Remove unused ClientsDetails interface from clients.ts
danielmarv Feb 5, 2025
faef2c6
Refactor UserClientsTable to improve access token handling and add us…
danielmarv Feb 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 316 additions & 0 deletions netmanager-app/app/(authenticated)/profile/components/ApiTokens.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
import type React from "react"
import { useEffect, useState } from "react"
import moment from "moment"
import { useAppDispatch, useAppSelector } from "@/core/redux/hooks"
import { useToast } from "@/components/ui/use-toast"
import { Button } from "@/components/ui/button"
import { performRefresh } from "@/core/redux/slices/clientsSlice"
import EditClientForm from "./EditClientForm"
import CreateClientForm from "./CreateClientForm"
import DialogWrapper from "./DialogWrapper"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Skeleton } from "@/components/ui/skeleton"
import { Edit, Copy, Info, Plus } from "lucide-react"
import { users } from "@/core/apis/users"
import { getUserClientsApi } from "@/core/apis/settings"
import type { Client } from "@/app/types/clients"
import { generateTokenApi, activationRequestApi } from "@/core/apis/settings"

const UserClientsTable: React.FC = () => {
const dispatch = useAppDispatch()
const { toast } = useToast()
const [showInfoModal, setShowInfoModal] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isLoadingToken, setIsLoadingToken] = useState(false)
const [isLoadingActivationRequest, setIsLoadingActivationRequest] = useState(false)
const [openEditForm, setOpenEditForm] = useState(false)
const [openCreateForm, setOpenCreateForm] = useState(false)
const [selectedClient, setSelectedClient] = useState({} as Client)
const userInfo = useAppSelector((state) => state.user.userDetails)
const [clients, setClients] = useState<Client[]>([])
const [clientsDetails, setClientsDetails] = useState<Client[]>([])
const [currentPage, setCurrentPage] = useState(1)
const itemsPerPage = 4

const fetchClients = async () => {
setIsLoading(true)
try {
const res = await users.getUserDetails(userInfo?._id || "")
if (res) {
dispatch({ type: "ADD_CLIENTS", payload: res.users[0].clients })
setCurrentPage(1)
}
setClients(res.users[0].clients)
} catch (error) {
console.error(error)
toast({
title: "Error",
description: "Failed to fetch user details",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}

const fetchClientDetails = async () => {
setIsLoading(true)
try {
const response = await getUserClientsApi(userInfo?._id || "")
if (response) {
dispatch({ type: "ADD_CLIENTS_DETAILS", payload: response })
}
setClientsDetails(response)
} catch (error) {
console.error(error)
toast({
title: "Error",
description: "Failed to fetch client details",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}

useEffect(() => {
fetchClients()
fetchClientDetails()
}, [userInfo?._id, dispatch])

const hasAccessToken = (clientId: string) => {
const client =
Array.isArray(clientsDetails) && clientsDetails
? clientsDetails?.find((client: Client) => client._id === clientId)
: undefined
return client && client.access_token
}

const getClientToken = (clientID: string) => {
const client =
Array.isArray(clientsDetails) && clientsDetails
? clientsDetails?.find((client: Client) => client._id === clientID)
: undefined
return client && client.access_token && client.access_token.token
}

const getClientTokenExpiryDate = (clientID: string) => {
const client =
Array.isArray(clientsDetails) && clientsDetails
? clientsDetails?.find((client: Client) => client._id === clientID)
: undefined
return client && client.access_token && client.access_token.expires
}

const handleGenerateToken = async (res: Client) => {
setIsLoadingToken(true)
if (!res?.isActive) {
setShowInfoModal(true)
setIsLoadingToken(false)
} else {
try {
const response = await generateTokenApi(res)
if (response) {
toast({
title: "Success",
description: "Token generated successfully",
variant: "success",
})
}
dispatch(performRefresh())
} catch (error: any) {
toast({
title: "Error",
description: error.message || "Failed to generate token",
variant: "destructive",
})
} finally {
setIsLoadingToken(false)
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve token generation error handling and add rate limiting.

The token generation could benefit from additional security measures.

   const handleGenerateToken = async (res: Client) => {
     setIsLoadingToken(true)
+    const rateLimitKey = `token_gen_${res._id}`
+    const lastAttempt = localStorage.getItem(rateLimitKey)
+    
+    if (lastAttempt && Date.now() - Number(lastAttempt) < 60000) {
+      toast({
+        title: "Error",
+        description: "Please wait 1 minute before generating another token",
+        variant: "destructive",
+      })
+      setIsLoadingToken(false)
+      return
+    }
+    
     if (!res?.isActive) {
       setShowInfoModal(true)
       setIsLoadingToken(false)
     } else {
       try {
         const response = await generateTokenApi(res)
         if (response) {
+          localStorage.setItem(rateLimitKey, Date.now().toString())
           toast({
             title: "Success",
             description: "Token generated successfully",
             variant: "success",
           })
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleGenerateToken = async (res: Client) => {
setIsLoadingToken(true)
if (!res?.isActive) {
setShowInfoModal(true)
setIsLoadingToken(false)
} else {
try {
const response = await generateTokenApi(res)
if (response) {
toast({
title: "Success",
description: "Token generated successfully",
variant: "success",
})
}
dispatch(performRefresh())
} catch (error: any) {
toast({
title: "Error",
description: error.message || "Failed to generate token",
variant: "destructive",
})
} finally {
setIsLoadingToken(false)
}
}
}
const handleGenerateToken = async (res: Client) => {
setIsLoadingToken(true)
const rateLimitKey = `token_gen_${res._id}`
const lastAttempt = localStorage.getItem(rateLimitKey)
if (lastAttempt && Date.now() - Number(lastAttempt) < 60000) {
toast({
title: "Error",
description: "Please wait 1 minute before generating another token",
variant: "destructive",
})
setIsLoadingToken(false)
return
}
if (!res?.isActive) {
setShowInfoModal(true)
setIsLoadingToken(false)
} else {
try {
const response = await generateTokenApi(res)
if (response) {
localStorage.setItem(rateLimitKey, Date.now().toString())
toast({
title: "Success",
description: "Token generated successfully",
variant: "success",
})
}
dispatch(performRefresh())
} catch (error: any) {
toast({
title: "Error",
description: error.message || "Failed to generate token",
variant: "destructive",
})
} finally {
setIsLoadingToken(false)
}
}
}


const handleActivationRequest = async () => {
setIsLoadingActivationRequest(true)
try {
const clientID = selectedClient?._id
const response = await activationRequestApi(clientID)
if (response) {
setShowInfoModal(false)
setTimeout(() => {
toast({
title: "Success",
description: "Activation request sent successfully",
variant: "success",
})
}, 3000)
}
} catch (error: any) {
setShowInfoModal(false)
setTimeout(() => {
toast({
title: "Error",
description: error.message || "Failed to send activation request",
variant: "destructive",
})
}, 3000)
} finally {
setIsLoadingActivationRequest(false)
}
}

const displayIPAddresses = (client: Client) => {
return Array.isArray(client.ip_addresses) ? client.ip_addresses.join(", ") : client.ip_addresses
}

const handleClientCreated = () => {
fetchClients()
fetchClientDetails()
}

return (
<div className="overflow-x-auto">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold">Clients</h2>
<Button onClick={() => setOpenCreateForm(true)}>
<Plus className="mr-2 h-4 w-4" /> Create Client
</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]">Client name</TableHead>
<TableHead className="w-[138px]">IP Address</TableHead>
<TableHead className="w-[142px]">Client Status</TableHead>
<TableHead className="w-[138px]">Created</TableHead>
<TableHead className="w-[138px]">Token</TableHead>
<TableHead className="w-[138px]">Expires</TableHead>
<TableHead className="w-24"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={7}>
<Skeleton className="w-full h-12" />
</TableCell>
</TableRow>
) : clients?.length > 0 ? (
clients
.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
.map((client: Client, index: number) => (
<TableRow key={index}>
<TableCell className="font-medium">{client?.name}</TableCell>
<TableCell>{displayIPAddresses(client)}</TableCell>
<TableCell>
<span
className={`px-2 py-1 rounded-full text-xs ${
client?.isActive ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"
}`}
>
{client?.isActive ? "Activated" : "Not Activated"}
</span>
</TableCell>
<TableCell>
{client?.access_token?.createdAt
? moment(client.access_token.createdAt).format("MMM DD, YYYY")
: "N/A"}
</TableCell>
<TableCell>
{getClientToken(client._id) ? (
<div className="flex items-center">
<span className="mr-2 font-mono">
{getClientToken(client._id).slice(0, 4)}
<span className="mx-1">•••••••</span>
{getClientToken(client._id).slice(-4)}
</span>
<Button
title="Copy full token"
variant="ghost"
size="sm"
onClick={() => {
navigator.clipboard.writeText(getClientToken(client._id))
toast({
title: "Success",
description: "API token copied to clipboard",
variant: "success",
})
}}
>
<Copy className="h-4 w-4" />
</Button>
</div>
) : (
<Button
variant={!hasAccessToken(client._id) ? "default" : "secondary"}
size="sm"
disabled={isLoadingToken}
onClick={() => {
setSelectedClient(client)
handleGenerateToken(client)
}}
>
Generate
</Button>
)}
</TableCell>
<TableCell>
{getClientTokenExpiryDate(client._id) &&
moment(getClientTokenExpiryDate(client._id)).format("MMM DD, YYYY")}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => {
setOpenEditForm(true)
setSelectedClient(client)
}}
>
<Edit className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={7} className="text-center">
No data found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* Add your Pagination component here */}
Copy link

@coderabbitai coderabbitai bot Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement pagination component.

The comment indicates a missing pagination component. This is essential for proper table navigation.

-      {/* Add your Pagination component here */}
+      {clients?.length > itemsPerPage && (
+        <div className="flex justify-center mt-4">
+          <div className="flex space-x-2">
+            {Array.from({ length: Math.ceil(clients.length / itemsPerPage) }).map((_, index) => (
+              <Button
+                key={index}
+                variant={currentPage === index + 1 ? "default" : "outline"}
+                size="sm"
+                onClick={() => onPageChange(index + 1)}
+              >
+                {index + 1}
+              </Button>
+            ))}
+          </div>
+        </div>
+      )}

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @danielmarv , I hope you have been able to review this suggestion?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<EditClientForm
open={openEditForm}
onClose={() => setOpenEditForm(false)}
data={selectedClient}
onClientUpdated={handleClientCreated}
/>
<CreateClientForm
open={openCreateForm}
onClose={() => setOpenCreateForm(false)}
onClientCreated={handleClientCreated}
/>
<DialogWrapper
open={showInfoModal}
onClose={() => setShowInfoModal(false)}
handleClick={handleActivationRequest}
primaryButtonText={"Send activation request"}
loading={isLoadingActivationRequest}
>
<div className="flex items-center">
<Info className="h-5 w-5 mr-2 text-blue-500" />
<p className="text-sm text-gray-600">
You cannot generate a token for an inactive client. Reach out to support for assistance at [email protected]
or send an activation request.
</p>
</div>
</DialogWrapper>
</div>
)
}

export default UserClientsTable

Loading