# 取得 Admin Token
由於在 user_views.py 中,使用 IsAdminUser 來限制只有 Admin 才能取得 User 資料,所以需要先登入 Admin 帳號,取得 Admin 使用者 Token,才能透過 Postman 取得 User 資料。
@api_view(['GET']) # 取所有 User
@permission_classes([IsAdminUser]) # 只有Admin才能 access
def getUsers(request):
users = User.objects.all()
serializer = UserSerializer(users,many=True)
return Response(serializer.data)
若套用一般使用者的 Token 會出現以下錯誤:

因此先登入 Admin 帳號,取得 Admin 使用者 Token,才能透過 Postman 取得所有 User 的資料。

# 用戶列表前端實作
# Constants 定義
到 frontend/src/constants/userConstants.js 定義以下常數:
....
export const USER_LIST_REQUEST = "USER_LIST_REQUEST";
export const USER_LIST_SUCCESS = "USER_LIST_SUCCESS";
export const USER_LIST_FAIL = "USER_LIST_FAIL";
export const USER_LIST_RESET = "USER_LIST_RESET";
# Reducers 實作
這邊 state 會需要多一個 users 陣列來存放所有 User 資料,然後在 success 終究不在需要 success: true了,因為這邊只是取得 User 資料,不需要判斷是否成功更新。然後 userInfo 改成 user,一樣都是儲存 action 傳遞過來的 payload。
export const userListReducer = (state = { users: [] }, action) => {
switch (action.type) {
case USER_LIST_REQUEST:
return { loading: true };
case USER_LIST_SUCCESS:
return { loading: false, user: action.payload }; // success 是為了在 ProfilePage.js output 判斷是否成功更新
case USER_LIST_FAIL:
return { loading: false, error: action.payload };
case USER_LIST_RESET: // 這邊是為了在 ProfilePage.js 中的 useEffect 中,當 user 更新成功後,清空 state
return { users: [] };
default:
return state;
}
};
一樣到 store.js 中加入 userListReducer:
import { ...,userListReducer } from "./reducers/userReducers";
const store = configureStore({
reducer: {
// 儲存多個state的地方,到時會再各個component用useSelector指定要使用哪一個
// 用戶相關
...
userList: userListReducer,
},
preloadedState: initialState,
middleware: middleware,
});
此時去網頁開啟 Redux DevTools,應能看到多了一個 userList 的 state:

# Actions 實作
這邊直接複製 updateUserProfile 的 actio 來修改,因為都是取得 User 資料,只是 endpoint 不同而已。
- 將 put 改成 get
- 將
/api/users/profile/改成/api/users/ - 拿掉 localStorage 的更新,因為這邊只是取得 User 資料,不需要更新 localStorage
// userActions.js
// 更新使用者資料
export const listUsers = (user) => async (dispatch, getState) => {
// getState 大多都是在需要訪問 store 的裡的數據以做一些操作時使用
try {
dispatch({
type: USER_LIST_REQUEST,
});
const {
userLogin: { userInfo },
} = getState(); // 確保 User 是登入的狀態
const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`, // 需要有 token 才能取得資料,因為後端有設 permission_classes
},
};
const { data } = await axios.get(
`/api/users/`,
user, // 這邊的 user 是從 ProfilePage.js 傳過來的
config
);
dispatch({
type: USER_LIST_SUCCESS,
payload: data, // payload 是後端回傳的資料
});
} catch (error) {
dispatch({
type: USER_LIST_FAIL,
payload: error.response && error.response.data.message ? error.response.data.message : error.message,
});
}
};
為了登出後清空 User 資料,所以在同檔案中的 logout 函式加上 USER_LIST_RESET:
// userActions.js
export const logout = () => (dispatch) => {
localStorage.removeItem("userInfo");
dispatch({ type: USER_LOGOUT });
dispatch({ type: USER_DETAILS_RESET }); // 登出後要清空 user Object,否則會造成登出後還是會顯示上一個使用者的資料
dispatch({ type: ORDER_LIST_MY_RESET }); // 登出後清空訂單資料
dispatch({ type: USER_LIST_RESET }); // 登出後清空訂單資料
};
# 前端頁面實作
建立一個新的名為 UserListPage.js 的 component,這邊會使用到 useEffect 來取得 User 資料,並且使用 useDispatch 來 dispatch action 來取得 User 資料。
useEffect(() => {
dispatch(listUsers());
}, [dispatch]); // 當 dispatch有變動就觸發 useEffect
...
return(
...
)
回到 App.js 中註冊 UserListPage 的 Route,測試看頁面是否有成功顯示:
// App.js
import UserListPage from "./Pages/UserListPage";
...
<Routes>
<Route path="/" element={<HomePage/>} />
....
<Route path="/admin/userlist" element={<UserListPage/>} />
</Routes>4
# 非 Admin 使用者重新導向至登入頁面
判斷若非 Admin 使用者就直接不顯示這個下拉選項以及不能訪問該頁面 參考這篇 -> 管理員專屬後台頁面-路由結構最佳化寫法 :
# 製作顯示所有使用者資料的表格

# 在 Header 新增相應的下拉選項
這邊不直接使用 NavDropdown.Item 的 href,而是使用 LinkContainer 來導向到 /admin/userlist,是因為要保有 React SPA 的優勢,不要重新整理頁面。
// Header.js
userInfo && userInfo.isAdmin && (
<NavDropdown title='後台管理專用' id='admin-menu'>
<LinkContainer to='/admin/userlist'>
<NavDropdown.Item>用戶管理</NavDropdown.Item>
</LinkContainer>
<NavDropdown.Item>Another action</NavDropdown.Item>
<NavDropdown.Item>Something</NavDropdown.Item>
<NavDropdown.Divider />
<NavDropdown.Item>Separated link</NavDropdown.Item>
</NavDropdown>
);
# 刪除使用者資料實作
在前端簡單測試按鈕是否有成功觸發事件,並取得 User 的 id 後,就可以開始撰寫後端 API 來處理編輯、刪除使用者資料。

const deleteUserHandler = (id) => {
console.log("delete", id);
};
...
<Button variant='danger' className='btn-sm' onClick={() => deleteUserHandler(user.id)}>
<i className='fas fa-trash'></i>
</Button>
# 後端 API 實作
一樣先從
user_views.py中新增一個deleteUser的 APIurls.py中新增一個path('user/<str:pk>/', views.deleteUser, name='user-delete')的路由
# views(刪除使用者視圖)
@api_view(['DELETE']) # 刪除 User
@permission_classes([IsAdminUser]) # 只有Admin才能 access
def deleteUser(request,pk):
userForDeletion = User.objects.get(id=pk)
userForDeletion.delete()
return Response('用戶已成功刪除')
# urls(更新路由)
path('delete/<str:pk>/',views.deleteUser , name='user_delete'),
# 前端實作
# Constants 定義
export const USER_DELETE_REQUEST = "USER_DELETE_REQUEST";
export const USER_DELETE_SUCCESS = "USER_DELETE_SUCCESS";
export const USER_DELETE_FAIL = "USER_DELETE_FAIL";
# Reducers 實作
直接複製 userListReducer 函式來修改,這邊重點修改處:
state初始值改為空物件,因為已不需要users陣列來存放 User 資料USER_LIST_XXX改為USER_DELETE_XXX- 在 case
USER_DELETE_SUCCESS中,不再需要users: action.payload - 去除
USER_LIST_RESET,因為這邊不需要清空 state
export const userDeleteReducer = (state = {}, action) => {
switch (action.type) {
case USER_DELETE_REQUEST:
return { loading: true };
case USER_DELETE_SUCCESS:
return { loading: false, success: true };
case USER_DELETE_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
至 store.js 的 configureStore 中加入 userDeleteReducer,
const store = configureStore({
reducer: {
// 用戶相關
...
userDelete: userDeleteReducer,
},
preloadedState: initialState,
middleware: middleware,
});
註冊完畢後,在前端開啟 Redux DevTools,應能看到多了一個 userDelete 的 state,此時裡面應該空物件的狀態。

# Actions 實作
一樣複製 listUsers 函式來修改,重點修改處:
- 需要在
deleteUser函式中傳入id來刪除指定 user - 將
get改為delete - HTTP 請求路由改為
/api/users/delete/${id}/,這邊${id}是從UserListPage.js傳過來的
// 刪除使用者資料
export const deleteUser = (id) => async (dispatch, getState) => {
// getState 大多都是在需要訪問 store 的裡的數據以做一些操作時使用
try {
dispatch({
type: USER_DELETE_REQUEST,
});
const {
userLogin: { userInfo },
} = getState(); // 確保 User 是登入的狀態
const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`, // 需要有 token 才能取得資料,因為後端有設 permission_classes
},
};
const { data } = await axios.delete(`/api/users/delete/${id}/`, config);
dispatch({
type: USER_DELETE_SUCCESS,
payload: data, // payload 是後端回傳的資料
});
} catch (error) {
dispatch({
type: USER_DELETE_FAIL,
payload: error.response && error.response.data.message ? error.response.data.message : error.message,
});
}
};
# 前端頁面中的按鈕實現刪除使用者資料功能
引入先前寫好的 deleteUser action,並從 Redux store 中讀取 userDelete 狀態,然後解構 success 為 successDelete(提取刪除操作是否成功的標誌,並重命名為 successDelete),最後再透過 useEffect 來監聽刪除結果,若成功刪除使用者後,自動重新載入頁面。
- listUsers: 用來取得所有使用者的列表,並更新到 Redux store 中。
- deleteUser: 用來發送刪除使用者的請求,並更新刪除的結果到 Redux store。
import { listUsers,deleteUser } from "../actions/userActions";
...
const userDelete = useSelector((state) => state.userDelete);
const { success: successDelete } = userDelete;
useEffect(() => {
dispatch(listUsers());
}, [dispatch,successDelete]); // 這邊放入 successDelete,因為要在刪除使用者後自動重新載入頁面
const deleteUserHandler = (id) => {
// console.log("delete",id);
if(window.confirm("確定要刪除此使用者嗎?")){
dispatch(deleteUser(id));
}
}
這一段 useEffect 的白話文解析: 每次 successDelete 發生變化時(即刪除操作成功後),重新觸發 listUsers,從後端更新用戶列表。
# 觀念補充
至於為何不直接如以下這樣調用 userDelete.success,還需要解構 userDelete,再取出 success,再重新命名為 successDelete?
const userDelete = useSelector((state) => state.userDelete);
// const { success: successDelete } = userDelete;
useEffect(() => {
dispatch(listUsers());
}, [dispatch, userDelete.success]);
是為了防止不必要的多次觸發,也是培養一個良好的寫程式習慣,一般在 useEffect 中,盡量不直接與 Redux store 中的整個 userDelete 物件進行比較,而是只比較 success 屬性,這樣可以避免不必要的重新渲染。
例如以上情況就是,只有當 userDelete.success 的值改變(例如從 false 變為 true)時,successDelete 才會更新,進而觸發 useEffect。
# 編輯使用者資料實作
先前只有做取得用戶個人資訊的功能,現在需要取得「所有用戶」個別的資訊,所以需先取得各個不同使用者的資料才能去進行編輯、更新,所以會需創建兩個後端的 Route,
# 後端 API 實作
# views
與前面不同的是這邊的 user 都從 單單 request.user 改為 User.objects.get(id=pk),這樣才能取得特定 User 的資料,因為不再是取得登入用戶自己的資料,而是要取得指定 User 的資料。
@api_view(['GET']) # 後台用戶管理,取得單個用戶
@permission_classes([IsAdminUser]) # 只有Admin才能 access
def adminGetUserById(request,pk):
user = User.objects.get(id=pk) # 根據提供的 id 手動查詢特定用戶資料。
serializer = UserSerializer(user,many=False)
return Response(serializer.data)
- 這邊要用
user.is_staff來判斷是否為 Admin,而不是user.isAdmin,is_staff才是實際在資料庫中的使用的欄位名稱。
# serializers.py
class UserSerializer(serializers.ModelSerializer):
first_name = serializers.SerializerMethodField(
read_only=True) # 這個欄位不會存到資料庫,只是用來顯示
isAdmin = serializers.SerializerMethodField(read_only=True)
class Meta:
model = User
fields = ['id', 'isAdmin', 'first_name', 'last_name', 'username', 'email']
def get_isAdmin(self, obj): # obj 就是user
return obj.is_staff # django 預設欄位
...
@api_view(['PUT']) # 更新用戶的 profile
@permission_classes([IsAuthenticated]) # 只有登入的人才能 access
def adminUpdateUser(request,pk):
user = User.objects.get(id=pk)
serializer = UserSerializer(user,many=False)
data = request.data
user.first_name = data['first_name']
user.username = data['email']
user.email = data['email']
# user.isAdmin = data['isAdmin']
user.is_staff = data['isAdmin']
user.save()
return Response(serializer.data)
# 更新 urls
# Admin
path('<str:pk>/', views.adminGetUserById, name='user_get_by_id'),
path('update/<str:pk>/', views.adminUpdateUser, name='user_update'),
寫完後去 Postman 測試看看是否有成功取得特定 User 的資料,一樣應要代入 Admin Token 才能取得 User 資料。

# 前端實作
# 編輯使用者頁面
建立一個名為 UserEditPage.js 的頁面,功能部分類似於註冊頁面
- 透過
useParams取得 URL 中用戶的id,記得在App.js中若是定義:userID,那用useParams取時也一樣要用
userID,而不是id
// App.js
...
<Route path='/admin/user/:userID/edit' element={<AdminRoute><UserEditPage /></AdminRoute>} />
- 透過
useDispatch來 dispatchgetUserDetailsaction 來取得特定 User 的資料 - 透過
useSelector來取得userDetails的狀態,並解構出loading、error、user三個屬性
// UserEditPage.js
const UserEditPage = () => {
const { userID } = useParams(); // React v6以後要用這種方式來取得id
};
...
const userDetails = useSelector((state) => state.userDetails);
const { loading, error, user } = userDetails;
const submitHandler = (e) => {
e.preventDefault();
};
如果沒用 Number 將userID轉換成數字,user.id !== userID 就會始終為 true,dispatch(getUserDetails(userID)) 就會不停被觸發,Redux 狀態一直被重置為 loading: true,從而導致頁面不斷進入加載狀態。
這邊目的要能判斷當前用戶是否存在,並且用戶 id 是否等於當前用戶 id,如果是,就自動載入用戶資料,否則就發送取得用戶資料的請求。
useEffect(() => {
if (!user.first_name || user.id !== Number(userID)) {
dispatch(getUserDetails(userID)); // 如果用戶不存在或者用戶id不等於當前用戶id,就發送請求
} else {
// 如果用戶存在,就自動載入用戶資料
setFirst_name(user.first_name);
setEmail(user.email);
setIsAdmin(user.isAdmin);
}
}, [dispatch, user, userID]);
return (
<>
<Link to='/admin/userlist' className='btn btn-light my-3'>
回到用戶列表
</Link>
<h1>編輯用戶資料</h1>
{loading ? (
<Loader />
) : error ? (
<Message variant='danger'>{error}</Message>
) : (
<FormContainer>
<Form onSubmit={submitHandler}>
<Form.Group controlId='first_name' className='mb-3'>
<Form.Label>用戶姓名</Form.Label>
<Form.Control required type='name' placeholder='請輸入您的ID' value={first_name} onChange={(e) => setFirst_name(e.target.value)}></Form.Control>
</Form.Group>
<Form.Group controlId='email' className='mb-3'>
<Form.Label>電子郵件</Form.Label>
<Form.Control required type='email' placeholder='請輸入電子郵件' value={email} onChange={(e) => setEmail(e.target.value)} autoComplete='username'></Form.Control>
</Form.Group>
<Form.Group controlId='isAdmin'>
<Form.Check type='checkbox' label='是否為管理員' checked={isAdmin} onChange={(e) => setIsAdmin(e.target.checked)}></Form.Check>
</Form.Group>
<p>{isAdmin ? "User is an Admin" : "User is not an Admin"}</p>
<Button type='submit' variant='primary' className='mt-3'>
更新
</Button>
</Form>
</FormContainer>
)}
</>
);
# 解決從編輯用戶資料跳至個人資料頁面,資料顯示不正確的問題
目前若從用戶編輯頁面

跳至用戶個人資料頁面,會發現資料載入不正確,必須要重新整理頁面才能正確載入。

那這是由於在 useEffect 中,當 user.id !== Number(userID) 時,才會發送 getUserDetails(userID) 請求,但是當從編輯頁面跳至個人資料頁面時,user.id 並不會改變,所以就不會發送請求,這就是為什麼會出現資料不正確的問題。
解決方是就是回到個人資料頁面 ProfilePage.js 時,多檢查 userInfo.id 若不等於 user.id 就再發送一次getUserDetails(userInfo.id) 請求,重新獲取正確的用戶個人資料。
// ProfilePage.js
useEffect(() => {
...
if (!user || !user.first_name || success || userInfo.id !== user.id) {
...
}
...
}, [dispatch, userInfo, user, success]);
# Constants 定義
// userConstants.js
export const USER_UPDATE_REQUEST = "USER_UPDATE_REQUEST";
export const USER_UPDATE_SUCCESS = "USER_UPDATE_SUCCESS";
export const USER_UPDATE_FAIL = "USER_UPDATE_FAIL";
# Reducers 實作
// userReducers.js
export const userUpdateReducer = (state = { user: [] }, action) => {
switch (action.type) {
case USER_UPDATE_REQUEST:
return { loading: true };
case USER_UPDATE_SUCCESS:
return { loading: false, success: true };
case USER_UPDATE_FAIL:
return { loading: false, error: action.payload };
case USER_UPDATE_RESET:
return { user: [] }; // 重置 user 狀態,清空舊資料,避免影響下一次操作或導致錯誤的資料顯示
default:
return state;
}
};
然後一樣至 store.js 的 configureStore 中加入 userUpdateReducer,並回瀏覽器的 Redux DevTools 中檢查是否有 userUpdate 的 state。
# Actions 實作
「更新操作的重點在於需要修改的資料,而不是指定哪個用戶」
updateUser函式改傳入user而不是id,因為更新需要多個欄位(例如名字、電子郵件、角色、狀態等),所以傳入整個user物件axios使用的是put方法,並帶入user物件,路由所帶入的id是user.id- 不會在 UPDATE_SUCCESS 中帶入
payload:data,而是在更新成功用戶資料後,再次發送USER_DETAILS_SUCCESS來重新獲取用戶資料
export const updateUser = (user) => async (dispatch, getState) => {
// getState 大多都是在需要訪問 store 的裡的數據以做一些操作時使用
try {
dispatch({
type: USER_UPDATE_REQUEST,
});
const {
userLogin: { userInfo },
} = getState(); // 確保 User 是登入的狀態
const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`, // 需要有 token 才能取得資料,因為後端有設 permission_classes
},
};
const { data } = await axios.put(`/api/users/update/${user.id}/`, user, config);
dispatch({
type: USER_UPDATE_SUCCESS,
});
dispatch({
// 更新成功後自動重新獲取用戶列表
type: USER_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: USER_UPDATE_FAIL,
payload: error.response && error.response.data.message ? error.response.data.message : error.message,
});
}
};
# 前端頁面功能實作
回到用戶編輯頁面
- import 在 userActions.js 中沒用到的
USER_UPDATE_RESET常數,負責在更新成功後清空user狀態,避免影響下一次操作或導致錯誤的資料顯示 - 更新完成後,自動導向到用戶列表頁面
- 顯示更新操作的 loading 狀態,以及錯誤信息
- 透過
useEffect依賴陣列監聽successUpdate狀態,若更新成功,則清空user狀態,並自動導向到用戶列表頁面
// UserEditPage.js
import { ..., updateUser } from "../actions/userActions"; // 自動載入用戶資料
import { USER_UPDATE_RESET } from "../constants/userConstants";
const userUpdate = useSelector((state) => state.userUpdate);
// console.log("Redux state userDetails:", userDetails);
const { error: errorUpdate, loading: loadingUpdate, suecces: successUpdate } = userUpdate;
const submitHandler = (e) => {
e.preventDefault();
dispatch(updateUser({ id: userID, first_name, email, isAdmin }));
};
useEffect(() => {
if (successUpdate) {
dispatch({ type: USER_UPDATE_RESET });
navigate("/admin/userlist");
} else {
if (!user.first_name || user.id !== Number(userID)) {
dispatch(getUserDetails(userID)); // 如果用戶不存在或者用戶id不等於當前用戶id,就發送請求
} else {
// 如果用戶存在,就自動載入用戶資料
setFirst_name(user.first_name);
setEmail(user.email);
setIsAdmin(user.isAdmin);
}
}
}, [dispatch, user, userID, successUpdate, navigate]);
return(
...
<h1>編輯用戶資料</h1>
{loadingUpdate && <Loader />}
{errorUpdate && <Message variant='danger'>{errorUpdate}</Message>}
)
此時回到網頁,進行用戶資料的編輯,應能正常更新用戶資料,並自動導向到用戶列表頁面:

