Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions Internal/adapters/repository/userRepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,23 @@ func (ur *UserRepository) FindById(id uint) (*domain.User, error) {
}
return &user, nil
}

func (ur *UserRepository) AllUser() ([]domain.User, error) {
var users []domain.User
err := ur.db.Gorm.Find(&users).Error
if err != nil {
return nil, err
}
return users, nil
}

func (ur *UserRepository) RoleUpdate(id uint, role string) (*domain.User, error) {
var user domain.User
if err := ur.db.Gorm.First(&user, id).Error; err != nil {
return nil, err
}
if err := ur.db.Gorm.Model(&user).Update("role", role).Error; err != nil {
return nil, err
}
return &user, nil
}
2 changes: 2 additions & 0 deletions Internal/api/Routes/routev1.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func SetupRouterV1(r *gin.Engine, deps Deps) {
admin := protected.Group("/admin")
{
admin.POST("/invite", deps.Auth.CreateInviteHandler)
admin.GET("/alluser", deps.User.AllUserHandler)
admin.PUT("/updaterole", deps.User.UpdateRoleHandler)
}
}
}
Expand Down
53 changes: 53 additions & 0 deletions Internal/api/handlers/userHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,56 @@ func (uh *UserHandler) GetUserHandler(c *gin.Context) {
"data": user,
})
}

func (uh *UserHandler) AllUserHandler(c *gin.Context) {
userRole, _ := c.Get("userRole")
if userRole != "admin" {
c.JSON(400, gin.H{
"message": "Not allowed to get all users",
"data": false,
})
return
}
users, err := uh.Service.GetAllUser()
if err != nil {
c.JSON(400, gin.H{
"message": "Unable to get all users",
"data": false,
})
}
c.JSON(200, gin.H{
"message": "Successfully get all users",
"data": users,
})
}

func (uh *UserHandler) UpdateRoleHandler(c *gin.Context) {
var input services.RoleInput
//userRole, _ := c.Get("userRole")
//if userRole != "admin" {
// c.JSON(400, gin.H{
// "message": "Not allowed to update role",
// "data": false,
// })
// return
//}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(400, gin.H{
"message": "input not valid",
"data": err.Error(),
})
return
}
user, err := uh.Service.UpdateRole(&input)
if err != nil {
c.JSON(400, gin.H{
"message": "Unable to update role",
"data": err,
})
return
}
c.JSON(200, gin.H{
"message": "Successfully update role",
"data": user,
})
}
21 changes: 21 additions & 0 deletions Internal/core/services/userService.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,31 @@ func NewUserService(repo *repository.UserRepository) *UserService {
return &UserService{Repo: repo}
}

type RoleInput struct {
UserID uint `json:"id"`
Role string `json:"role"`
}

func (us *UserService) GetUser(id uint) (*domain.User, error) {
user, err := us.Repo.FindById(id)
if err != nil {
return nil, errors.New("user not found")
}
return user, nil
}

func (us *UserService) GetAllUser() ([]domain.User, error) {
users, err := us.Repo.AllUser()
if err != nil {
return nil, errors.New("user not found")
}
return users, nil
}

func (us *UserService) UpdateRole(input *RoleInput) (*domain.User, error) {
user, err := us.Repo.RoleUpdate(input.UserID, input.Role)
if err != nil {
return nil, errors.New("user not found")
}
return user, nil
}
179 changes: 167 additions & 12 deletions Lb-web/src/features/admin-setting/pages/UserManagement.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button";
import { Plus, Loader2, Copy, Check } from "lucide-react";
import { Plus, Loader2, Copy, Check, Pencil } from "lucide-react";
import {
Dialog,
DialogContent,
Expand All @@ -10,19 +10,47 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { useState } from "react";
import { userManagementService } from "../services/userManagementService";
import { useState, useEffect } from "react";
import { userManagementService, type User } from "../services/userManagementService";
import { Label } from "@/components/ui/label";

export default function UserManagement() {
const [email, setEmail] = useState("");
const [role, setRole] = useState("user");
const [open, setOpen] = useState(false);

// Add User / Invite loading state
const [loading, setLoading] = useState(false);
const [inviteLink, setInviteLink] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const [error, setError] = useState<string | null>(null);

// User list state
const [users, setUsers] = useState<User[]>([]);
const [fetchingUsers, setFetchingUsers] = useState(true);

// Edit User state
const [editOpen, setEditOpen] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [editRole, setEditRole] = useState("user");
const [updatingRole, setUpdatingRole] = useState(false);

useEffect(() => {
fetchUsers();
}, []);

const fetchUsers = async () => {
setFetchingUsers(true);
try {
const data = await userManagementService.getAllUsers();
setUsers(data);
} catch (err) {
console.error("Failed to fetch users:", err);
} finally {
setFetchingUsers(false);
}
};

const handleInvite = async () => {
if (!email) return;

Expand Down Expand Up @@ -55,13 +83,36 @@ export default function UserManagement() {
setLoading(false);
};

const openEditModal = (user: User) => {
setEditingUser(user);
setEditRole(user.role);
setEditOpen(true);
};

const handleUpdateRole = async () => {
if (!editingUser) return;
setUpdatingRole(true);
try {
await userManagementService.updateUserRole(editingUser.id, editRole);
setEditOpen(false);
setEditingUser(null);
fetchUsers(); // Refresh the list
} catch (err) {
console.error("Failed to update role:", err);
// Optionally show error toast or message
} finally {
setUpdatingRole(false);
}
};

return (
<div className="p-8 space-y-8">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-sky-900 tracking-tight">User Management</h2>
<p className="text-sky-600">Manage system users and their roles.</p>
</div>
{/* Add User Dialog */}
<Dialog open={open} onOpenChange={(val) => {
setOpen(val);
if (!val) resetForm();
Expand Down Expand Up @@ -161,18 +212,122 @@ export default function UserManagement() {
</DialogFooter>
</DialogContent>
</Dialog>

{/* Edit Role Dialog */}
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="text-sky-900">Edit User Role</DialogTitle>
<DialogDescription>
Change the role for {editingUser?.username} ({editingUser?.email}).
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="edit-role">Role</Label>
<select
id="edit-role"
className="w-full rounded-md border border-sky-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={editRole}
onChange={(e) => setEditRole(e.target.value)}
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="secondary"
onClick={() => setEditOpen(false)}
disabled={updatingRole}
>
Cancel
</Button>
<Button
type="submit"
className="bg-sky-600 hover:bg-sky-700 text-white"
onClick={handleUpdateRole}
disabled={updatingRole}
>
{updatingRole ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Updating...
</>
) : (
"Save Changes"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>

<div className="bg-white rounded-xl border border-sky-100 shadow-sm p-6 flex flex-col items-center justify-center min-h-[400px] text-center space-y-4">
<div className="p-4 bg-sky-50 rounded-full">
<Plus className="h-8 w-8 text-sky-400" />
{fetchingUsers ? (
<div className="flex justify-center p-12">
<Loader2 className="h-8 w-8 animate-spin text-sky-600" />
</div>
<h3 className="text-lg font-medium text-sky-900">No users found</h3>
<p className="text-sky-500 max-w-sm">
You haven't added any users yet. Click the button above to create a new user account.
</p>
{/* This is a placeholder for the actual table */}
</div>
) : users.length === 0 ? (
<div className="bg-white rounded-xl border border-sky-100 shadow-sm p-6 flex flex-col items-center justify-center min-h-[400px] text-center space-y-4">
<div className="p-4 bg-sky-50 rounded-full">
<Plus className="h-8 w-8 text-sky-400" />
</div>
<h3 className="text-lg font-medium text-sky-900">No users found</h3>
<p className="text-sky-500 max-w-sm">
You haven't added any users yet. Click the button above to create a new user account.
</p>
</div>
) : (
<div className="bg-white rounded-xl border border-sky-100 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="bg-sky-50 text-sky-900 font-medium border-b border-sky-100">
<tr>
<th className="px-6 py-4">ID</th>
<th className="px-6 py-4">Username</th>
<th className="px-6 py-4">Email</th>
<th className="px-6 py-4">Role</th>
<th className="px-6 py-4">Created At</th>
<th className="px-6 py-4 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-sky-100">
{users.map((user) => (
<tr key={user.id} className="hover:bg-sky-50/50 transition-colors">
<td className="px-6 py-4 text-sky-700">{user.id}</td>
<td className="px-6 py-4 font-medium text-sky-900">{user.username}</td>
<td className="px-6 py-4 text-sky-600">{user.email}</td>
<td className="px-6 py-4">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${user.role === 'admin'
? 'bg-purple-100 text-purple-800'
: 'bg-emerald-100 text-emerald-800'
}`}>
{user.role}
</span>
</td>
<td className="px-6 py-4 text-sky-600">
{new Date(user.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-4 text-right">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-sky-600 hover:text-sky-900 hover:bg-sky-100"
onClick={() => openEditModal(user)}
>
<Pencil className="h-4 w-4" />
<span className="sr-only">Edit</span>
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,32 @@ export interface InviteUserRequest {
role: string;
}

export interface User {
id: number;
username: string;
email: string;
role: string;
created_at: string;
}

export interface GetAllUsersResponse {
message: string;
data: User[];
}

export const userManagementService = {
inviteUser: async (data: InviteUserRequest): Promise<InviteUserResponse> => {
const response = await api.post<InviteUserResponse>('/protected/admin/invite', data);
return response.data;
},

getAllUsers: async (): Promise<User[]> => {
const response = await api.get<GetAllUsersResponse>('/protected/admin/alluser');
return response.data.data;
},

updateUserRole: async (id: number, role: string): Promise<User> => {
const response = await api.put<{ message: string, data: User }>('/protected/admin/updaterole', { id, role });
return response.data.data;
}
};
Loading