diff --git a/dashboard/src/pages/Admin/Users/UsersListPanel.tsx b/dashboard/src/pages/Admin/Users/UsersListPanel.tsx
index f88c9d60..bc226e68 100644
--- a/dashboard/src/pages/Admin/Users/UsersListPanel.tsx
+++ b/dashboard/src/pages/Admin/Users/UsersListPanel.tsx
@@ -115,6 +115,7 @@ interface CreateValues {
}
interface EditValues {
+ username: string;
display_name?: string;
email?: string;
role: "admin" | "user";
@@ -997,6 +998,7 @@ export default function UsersListPanel() {
const openEdit = (row: UserRow) => {
setEditTarget(row);
editForm.setFieldsValue({
+ username: row.username,
display_name: row.display_name ?? "",
email: row.email ?? "",
role: row.role,
@@ -1038,6 +1040,7 @@ export default function UsersListPanel() {
await request(`/users/${editTarget.id}`, {
method: "PATCH",
body: JSON.stringify({
+ username: values.username.trim(),
display_name: values.display_name?.trim() || null,
email: values.email?.trim() || null,
role: values.role,
@@ -1664,6 +1667,19 @@ export default function UsersListPanel() {
{t("adminUsers.createSectionAccount")}
+
+ } />
+
None:
(display_name, user_id),
)
+ def set_username(self, user_id: int, username: str) -> None:
+ with self._db.transaction() as conn:
+ conn.execute(
+ "UPDATE users SET username = ? WHERE id = ?",
+ (username, user_id),
+ )
+
def set_email(self, user_id: int, email: str | None) -> None:
with self._db.transaction() as conn:
conn.execute("UPDATE users SET email = ? WHERE id = ?", (email, user_id))
diff --git a/src/octop/infra/users/manager.py b/src/octop/infra/users/manager.py
index d28a90f2..2f7b5cdb 100644
--- a/src/octop/infra/users/manager.py
+++ b/src/octop/infra/users/manager.py
@@ -441,6 +441,25 @@ async def set_display_name(self, username: str, display_name: str | None) -> Non
if current is not None:
current.display_name = display_name
+ async def set_username(self, username: str, new_username: str) -> None:
+ row = self._services.user_repo.get_by_username(username)
+ if row is None:
+ raise OctopError(ErrorCode.NOT_FOUND, "user not found")
+ if new_username == row.username:
+ return
+ self._services.user_repo.set_username(row.id, new_username)
+ async with self._lock:
+ current = self._users.pop(username, None)
+ if current is not None:
+ current.username = new_username
+ self._users[new_username] = current
+ self._services.audit_repo.write(
+ actor=ACTOR_ADMIN,
+ action="user.set_username",
+ target=username,
+ payload=new_username,
+ )
+
async def set_email(self, username: str, email: str | None) -> None:
row = self._services.user_repo.get_by_username(username)
if row is None: