Skip to content

Quản lý người dùng

Tạo tài khoản hàng loạt cho lớp học, cấp quyền staff/superuser, cấm hoặc tắt bình luận của người vi phạm, mạo danh để gỡ lỗi, chuyển dữ liệu giữa hai tài khoản và xoá tài khoản an toàn.

⏱ ~25 phút · 👤 Quản trị viên trang · 🔑 Superuser (một số việc chỉ cần staff kèm quyền cụ thể, xem Hệ thống phân quyền)

Trước khi bắt đầu

Các loại tài khoản

LCOJ có bốn mức vai trò, từ thấp đến cao. Mỗi mức không tự động bao gồm quyền của mức trên; riêng superuser có mọi quyền.

Vai tròĐược gìCấp ở đâu
Người dùngNộp bài, thi, bình luận (sau khi giải đủ số bài tối thiểu)Tự đăng nhập bằng OAuth, hoặc bạn tạo bằng lệnh (xem bên dưới)
Quản trị tổ chứcQuản lý thành viên, bài tập, kỳ thi của tổ chức mìnhTrang sửa tổ chức, xem Tổ chức
StaffĐăng nhập /admin/; chỉ làm được những gì quyền/nhóm cho phép/admin/auth/user/
SuperuserMọi quyền, không cần cấp từng quyền/admin/auth/user/ hoặc adduser --superuser

Đăng ký chỉ qua OAuth

Cấu hình mặc định của lcoj-docker đặt OAUTH_ONLY = True trong dmoj/config/local_settings.py: form đăng ký bằng mật khẩu bị ẩn, người dùng mới tự tạo tài khoản bằng Google/Facebook/GitHub. Tuy vậy form đăng nhập bằng tên đăng nhập + mật khẩu vẫn hoạt động, nên tài khoản bạn tạo bằng lệnh (có mật khẩu) vẫn đăng nhập bình thường. Đây là cách phát tài khoản cho lớp học hoặc kỳ thi tại chỗ.

Tìm và xem một người dùng

Trong trang quản trị, mục Users có hai trang liên quan:

TrangĐường dẫnDùng để
Tài khoản Django (User)/admin/auth/user/Tên đăng nhập, email, mật khẩu, trạng thái hoạt động, staff/superuser, nhóm, quyền
Hồ sơ LCOJ (Profile, hồ sơ người dùng)/admin/judge/profile/Rank hiển thị, tổ chức, múi giờ, IP cuối, tắt bình luận, ẩn khỏi bảng xếp hạng, lý do cấm, 2FA, ghi chú nội bộ

Các bước:

  1. Mở /admin/judge/profile/, gõ tên đăng nhập, email hoặc địa chỉ IP vào ô tìm kiếm.
  2. Danh sách hiện email, trạng thái TOTP, múi giờ, ngày tham gia, lần truy cập cuối, IP cuối và liên kết Xem trên trang web.
  3. Bấm tên để mở hồ sơ. Trang hồ sơ không có nút xoá (cố ý), và không thể tạo hồ sơ mới trực tiếp ở đây: hồ sơ được tạo kèm khi tạo User.
  4. Từ trang cá nhân của người dùng trên site (/user/<tên>), superuser/staff thấy thêm các tab Mạo danh, Ban người dùng nàyAdmin User (mở thẳng trang /admin/auth/user/<id>/change/) tuỳ quyền.

Hai hành động hàng loạt trên danh sách hồ sơ:

Hành độngTác dụng
Tính lại điểmTính lại điểm bài tập (calculate_points) cho các hồ sơ đã chọn
Tính lại điểm đóng gópTính lại điểm đóng góp (từ vote bình luận, blog…)

Tạo tài khoản

Tạo một tài khoản bằng lệnh adduser

bash
./scripts/manage.py adduser <n> <email> <mật-khẩu> [mã-ngôn-ngữ] [--staff] [--superuser]
Tham sốÝ nghĩa
tênTên đăng nhập
emailEmail, không cần có thật
mật-khẩuMật khẩu ban đầu
mã-ngôn-ngữTuỳ chọn. Mã ngôn ngữ mặc định (key của Language), mặc định DEFAULT_USER_LANGUAGE = CPP20
--staffTạo kèm quyền staff
--superuserTạo kèm quyền superuser

Ví dụ tạo tài khoản quản trị đầu tiên:

bash
./scripts/manage.py adduser admin [email protected] 'MatKhauManh!2026' --superuser --staff

Lệnh chạy xong không in gì. Kiểm tra bằng cách đăng nhập hoặc tìm tài khoản trong /admin/auth/user/.

Mật khẩu nằm trong lịch sử shell

Mật khẩu truyền qua dòng lệnh sẽ nằm trong lịch sử shell của server. Hãy yêu cầu người dùng đổi mật khẩu ngay sau lần đăng nhập đầu tiên.

Bạn cũng có thể tạo từng tài khoản trong /admin/auth/user/add/: nhập tên và mật khẩu, lưu, rồi điền tiếp các thông tin khác. Hồ sơ LCOJ được tạo tự động khi lưu.

Tạo hàng loạt cho lớp học bằng batchadduser

batchadduser đọc một file CSV (tên đăng nhập + họ tên), tạo tài khoản với mật khẩu ngẫu nhiên 8 ký tự, rồi ghi ra một file CSV mới chứa mật khẩu để bạn phát cho học sinh.

  1. Tạo file CSV với dòng tiêu đề đúng là username,fullname:

    csv
    username,fullname
    hs10a_01,Nguyễn Văn An
    hs10a_02,Trần Thị Bình
    hs10a_03,Lê Minh Châu
    • Lưu dạng UTF-8 không BOM. Excel "CSV UTF-8" thêm BOM vào đầu file, khiến tên cột đầu bị dính ký tự BOM và lệnh báo KeyError: 'username'.
    • Tên đăng nhập nên chỉ gồm chữ không dấu, số, _, -. Lệnh không kiểm tra tên hợp lệ hay trùng.
  2. Đặt file vào thư mục dmoj/repo/ trên server. Thư mục này được gắn vào container site tại /site/, cũng là thư mục làm việc của lệnh.

  3. Chạy lệnh (đường dẫn tính từ /site/ trong container):

    bash
    ./scripts/manage.py batchadduser lop10a.csv lop10a_matkhau.csv
  4. Mở dmoj/repo/lop10a_matkhau.csv. File có ba cột username,fullname,password:

    csv
    username,fullname,password
    hs10a_01,Nguyễn Văn An,k7Hq2xTa
    hs10a_02,Trần Thị Bình,3dYzBc9e
  5. Phát mật khẩu cho học sinh, rồi xoá cả hai file khỏi dmoj/repo/.

Tài khoản được tạo có: họ tên lưu ở trường first name, ngôn ngữ mặc định CPP20, không có email, đang hoạt động, không phải staff. Mật khẩu chỉ dùng các ký tự dễ đọc (bỏ i, l, o, 0, 1…).

Không đặt file ở dmoj/media/

dmoj/media/ được nginx phục vụ công khai một phần. Đừng để file chứa mật khẩu ở đó, và đừng commit chúng vào git (dmoj/repo/ là submodule của lcoj-site).

Lỗi giữa chừng

Lệnh tạo từng tài khoản một, không gói trong transaction. Nếu gặp tên đã tồn tại, lệnh dừng với IntegrityError: các tài khoản ở dòng trước đó đã được tạo và có mặt trong file kết quả, các dòng sau thì chưa. Sửa CSV (xoá các dòng đã tạo và dòng lỗi), chạy lại với tên file kết quả khác để không ghi đè mật khẩu đã sinh.

Sau khi tạo, có thể thêm cả lớp vào một tổ chức (xem Tổ chức) hoặc vào danh sách thí sinh của kỳ thi riêng tư (xem Thiết lập kỳ thi).

Cấp quyền staff, superuser và nhóm

  1. Mở /admin/auth/user/, tìm và mở tài khoản.
  2. Trong phần Quyền:
    • Tích tình trạng nhân viên (Staff status) để cho phép vào /admin/.
    • Tích trạng thái superuser (Superuser status) nếu muốn cấp mọi quyền. Chỉ dành cho người vận hành hệ thống.
    • Thêm vào Các nhóm (Groups) hoặc chọn quyền của người sử dụng (User permissions). Danh sách quyền hiển thị dạng judge.<codename> | <mô tả>.
  3. Bấm Lưu.

Quyền nào cho phép làm gì, và các nhóm vai trò gợi ý: xem Hệ thống phân quyền. Cách cấp quyền quản trị cho một tổ chức: xem Tổ chức.

Staff mà không có quyền thì gần như không làm được gì

is_staff chỉ mở cửa vào /admin/. Người đó chỉ thấy những mục mà nhóm/quyền của họ cho phép.

Bắt buộc 2FA với staff

DMOJ_REQUIRE_STAFF_2FA (mặc định True) không ép staff phải bật 2FA. Nó chỉ ngăn staff tắt phương thức 2FA cuối cùng: nút Tắt trên trang sửa hồ sơ bị vô hiệu, và xoá khoá WebAuthn cuối cùng bị từ chối với thông báo Staff may not disable 2FA.

Vì vậy quy trình nên là: yêu cầu người đó bật 2FA (TOTP hoặc WebAuthn) trong trang sửa hồ sơ trước, rồi mới tích staff.

Khi người dùng mất thiết bị 2FA: người có quyền judge.totp (Edit TOTP settings) hoặc superuser mở /admin/judge/profile/<id>/change/, bỏ tích TOTP 2FA enabled, và/hoặc xoá thiết bị trong bảng WebAuthn phía dưới, rồi lưu. Người không có quyền judge.totp chỉ thấy ô này ở chế độ chỉ đọc.

Cấm và tắt bình luận

LCOJ có nhiều mức xử lý, từ nhẹ đến nặng:

MứcCách làmTác dụng
Tắt bình luậnHồ sơ → tích tắt bình luận (mute)Không bình luận, không vote bình luận được; thông báo "Im lặng đi, bạn không có quyền nói ở đây." kèm lý do nếu có ban_reason
Ẩn khỏi xếp hạngHồ sơ → tích thành viên không được liệt kê (is_unlisted)Không xuất hiện trên bảng xếp hạng người dùng
Cấm ở một bài/kỳ thiMục Công lý (Justice) của bài tập hoặc kỳ thi → các người dùng bị cấmKhông nộp bài / không vào được kỳ thi đó, xem Thiết lập kỳ thi
Cấm toàn trangTab Ban người dùng này trên trang cá nhânKhoá tài khoản (xem bên dưới)

Cấm một tài khoản

Cần quyền judge.ban_user (Ban users). Không ai cấm được chính mình hoặc một superuser.

  1. Mở trang cá nhân https://luyencode.net/user/<tên>.
  2. Bấm tab Ban người dùng này.
  3. Nhập lý do vào ô Ban reason rồi bấm Submit.

Cấm có hiệu lực ngay

Khi cấm, LCOJ đồng thời: lưu ban_reason, đổi rank hiển thị thành banned, đặt ẩn khỏi xếp hạng, và tắt tài khoản (is_active = False). Phiên đăng nhập hiện tại bị vô hiệu ở request tiếp theo. Thao tác được ghi vào lịch sử phiên bản (reversion) với chú thích "Banned by <người cấm>".

Người bị cấm thấy gì:

  • Đăng nhập bằng mật khẩu: form báo "Tài khoản này đã bị cấm vì lý do: <lý do>". Nếu có Discord (cấu hình ở trang cấu hình) thì hiện thêm link Discord để khiếu nại.
  • Đăng nhập bằng Google/Facebook/GitHub: tài khoản đã bị tắt nên không đăng nhập được, nhưng lý do chỉ hiện trên form mật khẩu.

Bỏ cấm

Trên trang cá nhân, bấm tab Unban this user (nhãn này chưa được dịch) rồi bấm Submit. LCOJ xoá lý do, trả rank hiển thị về mặc định, bỏ ẩn khỏi xếp hạng và bật lại tài khoản.

Đừng cấm bằng cách sửa tay trong admin

Chỉ điền Ban reason trong /admin/judge/profile/ thì tài khoản chưa bị cấm: LCOJ coi là bị cấm khi đồng thời is_active = Falseban_reason khác rỗng. Luôn dùng tab trên trang cá nhân để các trường được cập nhật đồng bộ.

Tự động cấm khi gian lận trong kỳ thi

Có sẵn cơ chế tự cấm sau nhiều lần bị loại khỏi kỳ thi, nhưng tắt theo mặc định (VNOJ_SHOULD_BAN_FOR_CHEATING_IN_CONTESTS = False). Khi bật:

SettingMặc địnhÝ nghĩa
VNOJ_SHOULD_BAN_FOR_CHEATING_IN_CONTESTSFalseBật/tắt cơ chế
VNOJ_MAX_DISQUALIFICATIONS_BEFORE_BANNING3Số lần bị loại (disqualify) để bị cấm
VNOJ_BAN_COUNT_FROM_DATE2026-01-01 (UTC)Chỉ đếm các kỳ thi bắt đầu từ ngày này
VNOJ_CONTEST_CHEATING_BAN_MESSAGEBanned for multiple cheating offenses during contestsLý do cấm được ghi

Chỉ đếm kỳ thi không riêng tư theo tổ chức. Khi bỏ loại một lượt thi khiến số lần xuống dưới ngưỡng, tài khoản được tự bỏ cấm (nếu lý do cấm đúng là thông điệp trên). Người bị cấm theo cơ chế này thấy thêm danh sách các kỳ thi bị loại trên form đăng nhập. Cách loại thí sinh: xem Thiết lập kỳ thi.

Mạo danh người dùng

Mạo danh (impersonate, dùng django-impersonate) cho phép bạn xem site đúng như một người dùng khác thấy để gỡ lỗi (ví dụ "em không thấy bài X").

  1. Mở trang cá nhân của người đó và bấm tab Mạo danh, hoặc truy cập thẳng /impersonate/<id-của-User>/.
  2. Thanh điều hướng chuyển sang màu tím để nhắc bạn đang mạo danh.
  3. Khi xong, mở menu người dùng và bấm Ngừng mạo danh, hoặc vào /impersonate/stop/.

Hành vi trong LCOJ:

  • Không thể mạo danh superuser, và không thể bắt đầu mạo danh khi đang mạo danh.
  • Các trang dưới /admin/ luôn chạy bằng tài khoản thật của bạn.
  • Khi mạo danh, "user script" của người đó không chạy và thời điểm truy cập cuối của họ không bị cập nhật. Log uWSGI ghi tên dạng <bạn> as <người dùng>.

Mọi thao tác đều nhân danh người dùng đó

Nộp bài, bình luận, vote, tham gia kỳ thi, đổi cài đặt… khi đang mạo danh đều được ghi cho người bị mạo danh, kể cả khi họ đang trong một kỳ thi. Chỉ xem, đừng thao tác. Luôn bấm Ngừng mạo danh khi xong.

Ai được mạo danh và xem nhật ký ở đâu

Với cấu hình mặc định:

  • Tab Mạo danh chỉ hiện cho superuser, nhưng mọi staff đều có thể mạo danh (người dùng không phải superuser) bằng cách vào thẳng /impersonate/<id>/.
  • Mọi lần mạo danh đều được ghi nhật ký: xem tại /admin/impersonate/impersonationlog/ (ai mạo danh ai, bắt đầu/kết thúc lúc nào).

Để chỉ superuser được mạo danh, thêm vào dmoj/config/local_settings.py:

python
IMPERSONATE = {
    'REQUIRE_SUPERUSER': True,
}

rồi chép sang dmoj/repo/dmoj/local_settings.pydocker compose restart site. Xem Biến môi trường.

Đăng nhập theo IP (không bật mặc định)

LCOJ có sẵn cơ chế tự đăng nhập theo địa chỉ IP, dành cho phòng thi mà mỗi máy gán cố định cho một thí sinh:

  • Trường IP-based authentication (ip_auth) trong hồ sơ: mỗi IP chỉ gán cho một người.
  • Backend judge.ip_auth.IPBasedAuthBackend đã có trong AUTHENTICATION_BACKENDS.
  • Middleware judge.middleware.IPBasedAuthMiddleware không bật sẵn trong MIDDLEWARE; khi chưa bật, điền ip_auth không có tác dụng gì.

Khi middleware được bật, với mỗi request nó đọc IP từ request.META[IP_BASED_AUTHENTICATION_HEADER] (mặc định REMOTE_ADDR); nếu IP khớp ip_auth của một hồ sơ đang hoạt động thì đăng nhập thay bằng tài khoản đó, kể cả khi trình duyệt đang đăng nhập tài khoản khác.

Cần thiết kế cẩn thận trước khi bật

Site chạy sau reverse proxy (nginx trong lcoj-docker), nên REMOTE_ADDR là IP của proxy chứ không phải IP máy thí sinh. Muốn dùng, bạn cần đổi IP_BASED_AUTHENTICATION_HEADER sang header chứa IP thật và thêm middleware sau AuthenticationMiddleware. Chỉ nên bật trên một instance riêng cho phòng thi.

Chuyển dữ liệu giữa hai tài khoản

Khi một người có hai tài khoản (ví dụ tài khoản cũ tạo bằng lệnh và tài khoản mới đăng nhập Google), dùng move_user_content để chuyển dữ liệu từ tài khoản nguồn sang tài khoản đích:

bash
./scripts/manage.py move_user_content <nguồn> <đích>
Được chuyểnKhông được chuyển
Tất cả bài nộpLượt tham gia kỳ thi, rating
Tất cả bình luậnBài blog, ticket, tổ chức, huy hiệu
Tất cả vote bình luậnCài đặt hồ sơ, 2FA, API token
  1. Kiểm tra tên hai tài khoản thật kỹ (nguồn trước, đích sau).
  2. Chạy lệnh. Lệnh từ chối nếu tài khoản nguồn có bất kỳ lượt tham gia kỳ thi nào (Cannot move user … because it has contest participations.).
  3. Mở /admin/judge/profile/, chọn cả hai hồ sơ, chạy Tính lại điểmTính lại điểm đóng góp. Lệnh không tự tính lại.
  4. Cấm hoặc xoá tài khoản nguồn nếu không còn dùng.

Không hoàn tác được

Cả ba bước chuyển nằm trong một transaction (lỗi thì không thay đổi gì), nhưng một khi thành công thì không có lệnh chuyển ngược tự động. Nếu cả hai tài khoản từng vote cùng một bình luận, lệnh sẽ lỗi IntegrityError và không chuyển gì: xoá vote trùng của tài khoản nguồn rồi chạy lại.

Xoá tài khoản

Xoá là xoá dây chuyền

Xoá một User sẽ xoá luôn hồ sơ và mọi dữ liệu gắn với hồ sơ: bài nộp, bình luận, vote, lượt tham gia kỳ thi (bảng xếp hạng kỳ thi thay đổi), đề xuất tag… Không có thùng rác. Trong hầu hết trường hợp, cấm hoặc bỏ tích Kích hoạt (Active) là đủ.

Nếu vẫn cần xoá (ví dụ tài khoản rác, hoặc chủ tài khoản yêu cầu xoá dữ liệu):

  1. Nếu cần giữ bài nộp/bình luận, chuyển sang tài khoản khác trước bằng move_user_content.
  2. Mở /admin/auth/user/<id>/change/, bấm Xoá ở cuối trang.
  3. Đọc kỹ trang xác nhận: Django liệt kê mọi đối tượng sẽ bị xoá kèm. Chỉ xác nhận khi đã chắc.

Danh sách hồ sơ (/admin/judge/profile/) cố ý không có hành động xoá hàng loạt và trang hồ sơ không có nút xoá; hãy xoá từ trang User.

Yêu cầu tải dữ liệu cá nhân

Người dùng tự yêu cầu bản sao dữ liệu (mã nguồn bài nộp, bình luận) tại /data/prepare/ và tải về ở /data/download/; quản trị viên không cần duyệt. Nếu người dùng báo lỗi, xem Tải dữ liệu người dùng.

Sự cố thường gặp

Triệu chứngNguyên nhânCách xử lý
batchadduser báo KeyError: 'username'CSV có BOM hoặc sai tiêu đềLưu UTF-8 không BOM, dòng đầu đúng username,fullname
batchadduser báo FileNotFoundErrorFile không nằm trong dmoj/repo/Đặt file vào dmoj/repo/, dùng đường dẫn tương đối
IntegrityError … Duplicate entry khi tạo tài khoảnTên đăng nhập đã tồn tạiĐổi tên hoặc bỏ dòng đó; với batchadduser xem cảnh báo "Lỗi giữa chừng"
adduser báo Language matching query does not existSai mã ngôn ngữDùng key trong /admin/judge/language/, ví dụ CPP20
Học sinh quên mật khẩu tài khoản tạo hàng loạtTài khoản không có email nên không tự đặt lại được/admin/auth/user/<id>/change/ → liên kết đổi mật khẩu ngay dưới trường mật khẩu
Staff không tắt được 2FADMOJ_REQUIRE_STAFF_2FA chặn tắt phương thức cuốiThêm phương thức khác trước, hoặc nhờ người có quyền judge.totp tắt trong admin
Không thấy tab Ban người dùng nàyThiếu quyền judge.ban_user, hoặc đang xem chính mình / một superuserCấp quyền, xem Hệ thống phân quyền
Điền Ban reason trong admin nhưng người đó vẫn đăng nhập đượcTài khoản vẫn is_activeDùng tab Ban người dùng này
move_user_content báo có lượt tham gia kỳ thiNguồn đã từng thiLệnh không hỗ trợ trường hợp này; giữ cả hai tài khoản hoặc xử lý thủ công
Mạo danh xong vẫn "là" người khácChưa dừng mạo danhVào /impersonate/stop/

Tiếp theo