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 7 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
116 changes: 116 additions & 0 deletions netmanager-app/app/(authenticated)/profile/components/ApiTokens.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"use client"

import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Skeleton } from "@/components/ui/skeleton"
import type { Client } from "@/app/types/clients"
import { getUserClientsApi } from "@/core/apis/settings"
import { useAppSelector } from "@/core/redux/hooks"

export default function ApiTokens() {
const [clients, setClients] = useState<Client[]>([])
const [newTokenName, setNewTokenName] = useState("")
const [isLoading, setIsLoading] = useState(true)
const currentuser = useAppSelector((state) => state.user.userDetails)

useEffect(() => {
const fetchClients = async () => {
try {
setIsLoading(true)
const userID = currentuser?._id || ""
const response = await getUserClientsApi(userID)
setClients(response)
} catch (error) {
console.error("Failed to fetch clients:", error)
} finally {
setIsLoading(false)
}
}

fetchClients()
}, [currentuser?._id])

const handleCreateToken = (e: React.FormEvent) => {
e.preventDefault()
// Handle token creation logic here if needed
console.log("Create token feature is not implemented yet.")
}

return (
<div className="space-y-6">
<h2 className="text-2xl font-bold">API Access Tokens</h2>
<p className="text-sm text-gray-500">
Clients are used to generate API tokens that can be used to authenticate with the API. Your secret API tokens
are listed below. Remember to keep them secure and never share them.
</p>
<form onSubmit={handleCreateToken} className="space-y-4">
<div className="flex space-x-2">
<div className="flex-grow">
<Label htmlFor="new-token-name" className="sr-only">
New Token Name
</Label>
<Input
id="new-token-name"
placeholder="Enter new token name"
value={newTokenName}
onChange={(e) => setNewTokenName(e.target.value)}
required
/>
</div>
<Button type="submit">Create New Token</Button>
</div>
</form>
<Table>
<TableHeader>
<TableRow>
<TableHead>Client Name</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>Client Status</TableHead>
<TableHead>Created</TableHead>
<TableHead>Token</TableHead>
<TableHead>Expires</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
// Loading skeleton
Array.from({ length: 3 }).map((_, index) => (
<TableRow key={index}>
{Array.from({ length: 6 }).map((_, cellIndex) => (
<TableCell key={cellIndex}>
<Skeleton className="h-6 w-full" />
</TableCell>
))}
</TableRow>
))
) : clients.length > 0 ? (
clients.map((client) => (
<TableRow key={client._id}>
<TableCell>{client.name}</TableCell>
<TableCell>{client.ip_addresses.length > 0 ? client.ip_addresses[0] : "N/A"}</TableCell>
<TableCell>{client.isActive ? "Active" : "Inactive"}</TableCell>
<TableCell>{new Date(client.access_token.createdAt).toLocaleDateString()}</TableCell>
<TableCell>{client.access_token.token}</TableCell>
<TableCell>
{client.access_token.expires
? new Date(client.access_token.expires).toLocaleDateString()
: "No Expiration"}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={6} className="text-center">
No clients found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}

203 changes: 203 additions & 0 deletions netmanager-app/app/(authenticated)/profile/components/MyProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
"use client";

import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { getCountries } from "@/utils/countries";
import { getTimezones } from "@/utils/timezones";
import { users } from "@/core/apis/users";
import { useAppSelector } from "@/core/redux/hooks";

export default function MyProfile() {
const currentuser = useAppSelector((state) => state.user.userDetails);
const [profile, setProfile] = useState<any>(null);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add proper TypeScript interface for profile state.

Using any type reduces type safety. Define a proper interface for the profile state.

+interface Profile {
+  firstName: string;
+  lastName: string;
+  email: string;
+  jobTitle?: string;
+  country?: string;
+  timezone?: string;
+  bio?: string;
+  profilePicture?: string;
+}

-const [profile, setProfile] = useState<any>(null);
+const [profile, setProfile] = useState<Profile | null>(null);
📝 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 [profile, setProfile] = useState<any>(null);
interface Profile {
firstName: string;
lastName: string;
email: string;
jobTitle?: string;
country?: string;
timezone?: string;
bio?: string;
profilePicture?: string;
}
const [profile, setProfile] = useState<Profile | null>(null);

const [isEditing, setIsEditing] = useState(false);
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Issue Detected: Immediate Toggle of isEditing Without Awaiting API Response

  • The isEditing state is initialized as shown, but in the handleSubmit function it’s set to false immediately after preventing the default form submission—without any asynchronous operation for API integration.
  • This means the form inputs meant to be disabled during submission aren’t truly protected against premature user interaction if an update fails.
  • Consider adopting a dedicated submission state (e.g., isSubmitting) and waiting for the API response before updating the UI, ensuring a more robust user experience.
🔗 Analysis chain

Review isEditing state usage.

The isEditing state is used to disable form inputs, but it's set to false immediately in handleSubmit without waiting for the API response. This could lead to a poor user experience if the update fails.

Consider implementing proper form submission with API integration:

-const [isEditing, setIsEditing] = useState(false);
+const [isSubmitting, setIsSubmitting] = useState(false);
🏁 Scripts executed

The following scripts were executed for the analysis:

Error: No shell scripts found


Script:

#!/bin/bash
echo "=== Searching for handleSubmit usage and context ==="
rg --context 5 "handleSubmit" "netmanager-app/app/(authenticated)/profile/components/MyProfile.tsx"
echo ""
echo "=== Searching for setIsEditing usage and context ==="
rg --context 5 "setIsEditing" "netmanager-app/app/(authenticated)/profile/components/MyProfile.tsx"

Length of output: 1927

const [countries, setCountries] = useState<{ value: string; label: string }[]>([]);
const [timezones, setTimezones] = useState<{ value: string; label: string }[]>([]);

useEffect(() => {
const fetchUserData = async () => {
if (currentuser) {
const response = await users.getUserDetails(currentuser._id);
const userData = response.users[0];
setProfile({
firstName: userData.firstName,
lastName: userData.lastName,
email: userData.email,
jobTitle: userData.jobTitle,
country: userData.country,
timezone: userData.timezone,
bio: userData.description,
profilePicture: userData.profilePicture,
});
}
};
fetchUserData();
}, [currentuser]);

useEffect(() => {
setCountries(getCountries());
setTimezones(getTimezones());
}, []);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setProfile({ ...profile, [e.target.name]: e.target.value });
};

const handleSelectChange = (name: string) => (value: string) => {
setProfile({ ...profile, [name]: value });
};

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setIsEditing(false);
};

const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setProfile({ ...profile, profilePicture: reader.result as string });
};
reader.readAsDataURL(file);
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance image upload with validation and backend integration.

Add proper file validation and backend integration for image uploads.

+  const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+  const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];

   const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0];
     if (file) {
+      if (!ALLOWED_TYPES.includes(file.type)) {
+        alert('Please upload an image file (JPEG, PNG, or GIF)');
+        return;
+      }
+      if (file.size > MAX_FILE_SIZE) {
+        alert('File size should not exceed 5MB');
+        return;
+      }
+
       const reader = new FileReader();
       reader.onloadend = () => {
-        setProfile({ ...profile, profilePicture: reader.result as string });
+        // Show preview
+        setProfile({ ...profile, profilePicture: reader.result as string });
+        
+        // Upload to backend
+        const formData = new FormData();
+        formData.append('image', file);
+        fetch('/api/upload-profile-image', {
+          method: 'POST',
+          body: formData,
+        })
+        .then(response => response.json())
+        .then(data => {
+          setProfile({ ...profile, profilePicture: data.imageUrl });
+        })
+        .catch(error => {
+          console.error('Error uploading image:', error);
+          // Show error message to user
+        });
       };
       reader.readAsDataURL(file);
     }
   };
📝 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 handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setProfile({ ...profile, profilePicture: reader.result as string });
};
reader.readAsDataURL(file);
}
};
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (!ALLOWED_TYPES.includes(file.type)) {
alert('Please upload an image file (JPEG, PNG, or GIF)');
return;
}
if (file.size > MAX_FILE_SIZE) {
alert('File size should not exceed 5MB');
return;
}
const reader = new FileReader();
reader.onloadend = () => {
// Show preview
setProfile({ ...profile, profilePicture: reader.result as string });
// Upload to backend
const formData = new FormData();
formData.append('image', file);
fetch('/api/upload-profile-image', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(data => {
setProfile({ ...profile, profilePicture: data.imageUrl });
})
.catch(error => {
console.error('Error uploading image:', error);
// Show error message to user
});
};
reader.readAsDataURL(file);
}
};


if (!profile) {
return <div>Loading...</div>;
}

return (
<div className="space-y-6">
<h2 className="text-2xl font-bold">Personal Information</h2>
<p className="text-sm text-gray-500">Update your photo and personal details.</p>

<form onSubmit={handleSubmit} className="space-y-8">
<div className="flex items-center space-x-4">
<Avatar className="w-24 h-24">
{profile?.profilePicture ? (
<AvatarImage
src={profile.profilePicture}
alt={`${profile.firstName} ${profile.lastName}`}
/>
) : (
<div className="bg-gray-200 w-full h-full flex items-center justify-center">
<span className="text-gray-500">No Image</span>
</div>
)}
</Avatar>
<div>
<Button type="button" variant="outline" className="mb-2">
<Label htmlFor="avatar-upload" className="cursor-pointer">
Update
</Label>
</Button>
<Input
id="avatar-upload"
type="file"
accept="image/*"
className="hidden"
onChange={handleImageUpload}
disabled={isEditing}
/>
<Button type="button" variant="outline" className="text-red-500">
Delete
</Button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Implement profile picture deletion.

The delete button is present but lacks an onClick handler for removing the profile picture.

-<Button type="button" variant="outline" className="text-red-500">
+<Button 
+  type="button" 
+  variant="outline" 
+  className="text-red-500"
+  onClick={async () => {
+    try {
+      await fetch('/api/delete-profile-image', {
+        method: 'DELETE',
+      });
+      setProfile(prev => prev ? ({
+        ...prev,
+        profilePicture: '',
+      }) : null);
+      toast({
+        title: "Success",
+        description: "Profile image deleted successfully",
+      });
+    } catch (error) {
+      toast({
+        title: "Error",
+        description: "Failed to delete profile image",
+        variant: "destructive",
+      });
+    }
+  }}
+>
   Delete
 </Button>
📝 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
<Button type="button" variant="outline" className="text-red-500">
Delete
</Button>
<Button
type="button"
variant="outline"
className="text-red-500"
onClick={async () => {
try {
await fetch('/api/delete-profile-image', {
method: 'DELETE',
});
setProfile(prev => prev ? ({
...prev,
profilePicture: '',
}) : null);
toast({
title: "Success",
description: "Profile image deleted successfully",
});
} catch (error) {
toast({
title: "Error",
description: "Failed to delete profile image",
variant: "destructive",
});
}
}}
>
Delete
</Button>

</div>
</div>

<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="firstName">First name</Label>
<Input
id="firstName"
name="firstName"
value={profile.firstName || ""}
onChange={handleInputChange}
disabled={isEditing}
/>
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Last name</Label>
<Input
id="lastName"
name="lastName"
value={profile.lastName || ""}
onChange={handleInputChange}
disabled={isEditing}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
value={profile.email || ""}
onChange={handleInputChange}
disabled={isEditing}
/>
</div>
<div className="space-y-2">
<Label htmlFor="jobTitle">Job title</Label>
<Input
id="jobTitle"
name="jobTitle"
value={profile.jobTitle || ""}
onChange={handleInputChange}
disabled={isEditing}
/>
</div>
<div className="space-y-2">
<Label htmlFor="country">Country</Label>
<Select value={profile.country} onValueChange={handleSelectChange("country")} disabled={isEditing}>
<SelectTrigger>
<SelectValue placeholder="Select a country" />
</SelectTrigger>
<SelectContent>
{countries.map((country) => (
<SelectItem key={country.value} value={country.value}>
{country.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="timezone">Timezone</Label>
<Select value={profile.timezone} onValueChange={handleSelectChange("timezone")} disabled={isEditing}>
<SelectTrigger>
<SelectValue placeholder="Select a timezone" />
</SelectTrigger>
<SelectContent>
{timezones.map((timezone) => (
<SelectItem key={timezone.value} value={timezone.value}>
{timezone.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>

<div className="space-y-2">
<Label htmlFor="description">Bio</Label>
<Textarea
id="description"
name="description"
value={profile.bio || ""}
onChange={handleInputChange}
disabled={isEditing}
rows={4}
/>
</div>
<Button type="submit">Save Changes</Button>
</form>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

export default function PasswordEdit() {
const [passwords, setPasswords] = useState({
current: "",
new: "",
confirm: "",
})

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPasswords({ ...passwords, [e.target.name]: e.target.value })
}

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setPasswords({ current: "", new: "", confirm: "" })
}

return (
<div className="space-y-6">
<h2 className="text-2xl font-bold">Change Password</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="current-password">Current Password</Label>
<Input
id="current-password"
name="current"
type="password"
value={passwords.current}
onChange={handleInputChange}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="new-password">New Password</Label>
<Input
id="new-password"
name="new"
type="password"
value={passwords.new}
onChange={handleInputChange}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password">Confirm New Password</Label>
<Input
id="confirm-password"
name="confirm"
type="password"
value={passwords.confirm}
onChange={handleInputChange}
required
/>
</div>
<Button type="submit">Change Password</Button>
</form>
</div>
)
}

Loading