@ddj-v2/user-management 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,77 +1,80 @@
1
- # HydroOJ插件 用户管理面板
1
+ # HydroOJ插件 使用者管理面板
2
2
 
3
- 一个为 HydroOJ 提供可视化用户管理功能的插件,允许管理员在控制面板中方便地管理用户信息、权限和状态。
3
+ 修改自 https://github.com/SummerofOrange/hydrooj-user-management
4
4
 
5
- > 代码很简单,佛系不定期更新~
5
+ 這是一個為 HydroOJ 提供可視化使用者管理功能的插件,讓管理員能在控制面板中方便地管理使用者資訊、權限與狀態。
6
+
7
+ > 程式碼很簡單,佛系不定期更新~
6
8
  >
7
- > 如果认为好用请给我点一个star,不胜感激。
9
+ > 如果覺得好用請幫我點個 star,感激不盡。
8
10
 
9
- ## 安装方法
11
+ ## 安裝方法
10
12
 
11
13
  ```bash
12
14
  sudo su
13
- cd /root/.hydro/
14
- git clone https://github.com/SummerofOrange/hydrooj-user-management
15
- hydrooj addon add /root/.hydro/hydrooj-user-management
15
+ yarn global add @ddj-v2/user-management
16
+ hydrooj addon add @ddj-v2/user-management
16
17
  pm2 restart hydrooj
17
18
  ```
18
19
 
19
20
  ## 使用方法
20
21
 
21
- 1. **访问用户管理**: 登录 HydroOJ 后,在控制面板侧边栏找到"用户管理"菜单项
22
- 2. **搜索用户**: 在用户列表页面使用搜索框查找特定用户
23
- 3. **编辑用户**: 点击用户列表中的"编辑"按钮进入用户详情页面
24
- 4. **管理权限**: 在用户详情页面的"权限管理"部分设置用户权限
25
- 5. **重置密码**: 在"密码管理"部分为用户重置新密码
26
- 6. **封禁用户**: 在"用户状态"部分封禁或解封用户
22
+ 1. **進入使用者管理**:登入 HydroOJ 後,在控制面板側邊欄找到「使用者管理」選單項
23
+ 2. **搜尋使用者**:在使用者列表頁面使用搜尋框查找特定使用者
24
+ 3. **編輯使用者**:點擊使用者列表中的「編輯」按鈕進入使用者詳細頁面
25
+ 4. **管理權限**:在使用者詳細頁面的「權限管理」部分設定使用者權限
26
+ 5. **重設密碼**:在「密碼管理」部分為使用者重設新密碼
27
+ 6. **封鎖使用者**:在「使用者狀態」部分封鎖或解封使用者
28
+
27
29
 
28
- ## 权限说明
30
+ 1. **域用戶管理**: 可看見 default role
31
+ ## 權限說明
29
32
 
30
- 插件使用以下权限级别:
33
+ 插件使用以下權限等級:
31
34
 
32
- - **-1**: root(超级管理员)
33
- - **0**: 已封禁用户
34
- - **4**: 系统保留
35
- - **8**: 访客用户
36
- - **16842756**: 默认用户权限
37
- - **其他值**: 自定义权限
35
+ - **-1**:root(超級管理員)
36
+ - **0**:已封鎖使用者
37
+ - **4**:系統保留
38
+ - **8**:訪客使用者
39
+ - **16842756**:預設使用者權限
40
+ - **其他值**:自訂權限
38
41
 
39
- ## 界面展示
42
+ ## 介面展示
40
43
 
41
- ### 用户列表页面
44
+ ### 使用者列表頁面
42
45
 
43
- ![用户列表](asset/fig1.png)
46
+ ![使用者列表](asset/fig1.png)
44
47
 
45
- ### 用户详情页面
48
+ ### 使用者詳細頁面
46
49
 
47
- ![用户详情](asset/fig2.png)
50
+ ![使用者詳細](asset/fig2.png)
48
51
 
49
52
  ## 安全特性
50
53
 
51
- - ✅ 权限验证:只有具有系统管理权限的用户才能访问
52
- - ✅ 操作确认:重要操作(如重置密码、封禁用户)需要确认
53
- - ✅ 权限保护:防止非超级管理员修改超级管理员账户
54
- - ✅ 数据验证:自动验证用户名和邮箱的唯一性
55
- - ✅ 输入验证:前端和后端双重验证用户输入
54
+ - ✅ 權限驗證:只有具有系統管理權限的使用者才能訪問
55
+ - ✅ 操作確認:重要操作(如重設密碼、封鎖使用者)需確認
56
+ - ✅ 權限保護:防止非超級管理員修改超級管理員帳戶
57
+ - ✅ 資料驗證:自動驗證使用者名稱與電子郵件唯一性
58
+ - ✅ 輸入驗證:前端與後端雙重驗證使用者輸入
56
59
 
57
- ## 开发说明
60
+ ## 開發說明
58
61
 
59
- ### 贡献代码
62
+ ### 貢獻程式碼
60
63
 
61
- 欢迎提交 Issue Pull Request 来改进这个插件。
64
+ 歡迎提交 Issue Pull Request 改進此插件。
62
65
 
63
- ## 许可证
66
+ ## 授權
64
67
 
65
68
  MIT License
66
69
 
67
- ## 支持
70
+ ## 支援
68
71
 
69
- 如果您在使用过程中遇到问题,请:
72
+ 若您在使用過程中遇到問題,請:
70
73
 
71
- 1. 查看 [Issues](https://github.com/SummerofOrange/hydrooj-user-management/issues) 页面
72
- 2. 提交新的 Issue 描述您的问题
73
- 3. 联系作者获取技术支持
74
+ 1. 查看 [Issues](https://github.com/SummerofOrange/hydrooj-user-management/issues) 頁面
75
+ 2. 提交新的 Issue 描述您的問題
76
+ 3. 聯絡作者取得技術支援
74
77
 
75
78
  ---
76
79
 
77
- **注意**: 此插件需要 HydroOJ v5.0.0-beta.6 或更高版本。使用前请确保您有足够的系统管理权限。
80
+ **注意**:此插件需 HydroOJ v5.0.0-beta.6 或更高版本。使用前請確保您有足夠的系統管理權限。
package/index.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  Context, Handler, param, PRIV, Types, UserModel, DomainModel,
3
- ValidationError, UserNotFoundError, PermissionError, Time, SystemModel, moment
3
+ ValidationError, UserNotFoundError, PermissionError, Time, SystemModel, moment,
4
+ PERM
4
5
  } from 'hydrooj';
6
+ import domain from 'hydrooj/src/model/domain';
5
7
 
6
8
  declare module 'hydrooj' {
7
9
  interface Collections {
@@ -210,6 +212,77 @@ export async function apply(ctx: Context) {
210
212
  // 在控制面板侧边栏添加用户管理菜单项
211
213
  ctx.injectUI('ControlPanel', 'user_manage_main', { icon: 'user' });
212
214
 
215
+ ctx.withHandlerClass('DomainUser', (DomainUserHandler: { prototype: any }) => {
216
+ const originalGet = DomainUserHandler.prototype.get;
217
+
218
+ // 包裝原方法
219
+ DomainUserHandler.prototype.get = async function() {
220
+ const { domainId } = this.args;
221
+ const format = this.args.format || 'default';
222
+ console.log('DomainUserHandler get called with domainId:', domainId, 'format:', format);
223
+ const [dudocs, roles] = await Promise.all([
224
+ domain.collUser.aggregate([
225
+ {
226
+ $match: {
227
+ // TODO: add a page to display users who joined but with default role
228
+ role: {
229
+ $nin: ['guest'],
230
+ $ne: null,
231
+ },
232
+ domainId,
233
+ },
234
+ },
235
+ {
236
+ $lookup: {
237
+ from: 'user',
238
+ let: { uid: '$uid' },
239
+ pipeline: [
240
+ {
241
+ $match: {
242
+ $expr: { $eq: ['$_id', '$$uid'] },
243
+ priv: { $bitsAllSet: PRIV.PRIV_USER_PROFILE },
244
+ },
245
+ },
246
+ {
247
+ $project: {
248
+ _id: 1,
249
+ uname: 1,
250
+ avatar: 1,
251
+ },
252
+ },
253
+ ],
254
+ as: 'user',
255
+ },
256
+ },
257
+ { $unwind: '$user' },
258
+ {
259
+ $project: {
260
+ user: 1,
261
+ role: 1,
262
+ join: 1,
263
+ ...(this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO) ? { displayName: 1 } : {}),
264
+ },
265
+ },
266
+ ]).toArray(),
267
+ domain.getRoles(domainId),
268
+ ]);
269
+ const users = dudocs.map((dudoc) => {
270
+ const u = {
271
+ ...dudoc,
272
+ ...dudoc.user,
273
+ };
274
+ delete u.user;
275
+ return u;
276
+ });
277
+ const rudocs: Record<string, any[]> = {};
278
+ for (const role of roles) rudocs[role._id] = users.filter((udoc) => udoc.role === role._id);
279
+ this.response.template = format === 'raw' ? 'domain_user_raw.html' : 'domain_user.html';
280
+ this.response.body = {
281
+ roles, rudocs, domain: this.domain,
282
+ };
283
+ };
284
+ return DomainUserHandler;
285
+ });
213
286
  // 添加国际化支持
214
287
  ctx.i18n.load('zh', {
215
288
  'user_manage_main': '用户管理',
@@ -275,6 +348,71 @@ export async function apply(ctx: Context) {
275
348
  'Copy User ID': '复制用户ID'
276
349
  });
277
350
 
351
+ // 添加国际化支持
352
+ ctx.i18n.load('zh_TW', {
353
+ 'user_manage_main': '用戶管理',
354
+ 'user_manage_detail': '用戶詳情',
355
+
356
+ 'User Management': '用戶管理',
357
+ 'User List': '用戶列表',
358
+ 'Search Users': '搜尋用戶',
359
+ 'Search by': '搜尋方式',
360
+ 'Username': '用戶名',
361
+ 'Email': '電子郵件',
362
+ 'User ID': '用戶ID',
363
+ 'Keyword': '關鍵字',
364
+ 'Sort by': '排序方式',
365
+ 'Registration Time': '註冊時間',
366
+ 'Last Login': '最後登入',
367
+ 'Privilege': '權限',
368
+ 'Order': '順序',
369
+ 'Ascending': '升序',
370
+ 'Descending': '降序',
371
+ 'Search': '搜尋',
372
+ 'Clear': '清除',
373
+ 'Refresh': '刷新',
374
+
375
+ 'Normal User': '普通用戶',
376
+ 'Admin': '管理員',
377
+ 'Banned': '已封禁',
378
+ 'Super Admin': '超級管理員',
379
+ 'Active': '活躍',
380
+ 'Inactive': '不活躍',
381
+ 'Actions': '操作',
382
+ 'View': '查看',
383
+ 'Edit': '編輯',
384
+ 'Ban': '封禁',
385
+ 'Unban': '解封',
386
+ 'Set Privilege': '設置權限',
387
+ 'Status': '狀態',
388
+ 'School': '學校',
389
+ 'Bio': '個人簡介',
390
+ 'Never': '從未',
391
+ 'Not set': '未設置',
392
+ 'Previous': '上一頁',
393
+ 'Next': '下一頁',
394
+ 'Page': '頁',
395
+ 'of': '共',
396
+ 'users': '用戶',
397
+ 'Total': '總計',
398
+ 'Showing': '顯示',
399
+ 'to': '到',
400
+ 'User Details': '用戶詳情',
401
+ 'Basic Information': '基本資訊',
402
+ 'User Statistics': '用戶統計',
403
+ 'Privilege Management': '權限管理',
404
+ 'Password Management': '密碼管理',
405
+ 'User Status': '用戶狀態',
406
+ 'Back to List': '返回列表',
407
+ 'Save Changes': '保存更改',
408
+ 'Cancel': '取消',
409
+ 'Reset Password': '重設密碼',
410
+ 'Current Privilege': '當前權限',
411
+ 'Ban User': '封禁用戶',
412
+ 'Unban User': '解封用戶',
413
+ 'Copy User ID': '複製用戶ID'
414
+ });
415
+
278
416
  ctx.i18n.load('en', {
279
417
  'user_manage_main': 'User Management',
280
418
  'user_manage_detail': 'User Detail',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddj-v2/user-management",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Advanced user management plugin for Hydro",
5
5
  "main": "index.ts",
6
6
  "author": "Grass_cat",
@@ -0,0 +1,109 @@
1
+ {% extends "domain_base.html" %}
2
+ {% block domain_content %}
3
+ {{ set(UiContext, 'roles', roles.map(eval('(i) => i._id'))) }}
4
+ {{ set(UiContext, 'canForceJoin', model.system.get('server.allowInvite') or handler.user.hasPriv(PRIV.PRIV_MANAGE_ALL_DOMAIN)) }}
5
+ {% set _rolesSelect = roles.filter(eval("(i) => i._id !== 'guest'")).map(eval('(role) => [role._id, role._id]')) %}
6
+ <div class="section">
7
+ <div class="section__header">
8
+ <h1 class="section__title">{{ _('{0}: Users').format(domain.name) }}</h1>
9
+ <div class="section__tools">
10
+ <button class="primary rounded button" name="add_user">{{ _('Add User') }}</button>
11
+ </div>
12
+ <!-- Search bar for Role -->
13
+ <div class="section__search" style="margin-top: 10px;">
14
+ <input type="text" id="roleSearch" placeholder="{{ _('Search by Role') }}" class="input" style="width: 200px;">
15
+ </div>
16
+ </div>
17
+ {{ noscript_note.render() }}
18
+ <div class="section__body no-padding domain-users">
19
+ <table class="data-table" id="userTable">
20
+ <colgroup>
21
+ <col class="col--checkbox">
22
+ <col class="col--uid">
23
+ <col class="col--user">
24
+ <col class="col--role">
25
+ </colgroup>
26
+ <thead>
27
+ <tr>
28
+ <th class="col--checkbox">
29
+ <label class="compact checkbox">
30
+ <input type="checkbox" name="select_all" data-checkbox-toggle="user">
31
+ </label>
32
+ </th>
33
+ <th class="col--uid">{{ _('User ID') }}</th>
34
+ <th class="col--user">{{ _('Username') }}</th>
35
+ <th class="col--role">{{ _('Role') }}</th>
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ {%- for role, udocs in rudocs -%}
40
+ {%- if udocs|length > 50 -%}
41
+ <tr data-role="{{ role }}">
42
+ <td colspan="3" style="text-wrap: wrap;"><div style="max-height: 300px; overflow-y: auto">
43
+ {%- for udoc in udocs -%}
44
+ {% set is_disabled=(rudoc._id == handler.user._id) %}
45
+ <input type="checkbox" data-uid="{{udoc._id}}" data-checkbox-group="user" {% if is_disabled %}disabled{% else %}data-checkbox-range{% endif %}>
46
+ [{{udoc._id}}]{{ user.render_inline(udoc, avatar=false, badge=false) }}{% if not udoc.join %}
47
+ <span class="not-joined">({{ _('Not joined yet') }})</span>
48
+ {% endif %}&nbsp;&nbsp;&nbsp;
49
+ {%- endfor -%}
50
+ </div></td>
51
+ <td>{{ role }}</td>
52
+ </tr>
53
+ {%- else -%}
54
+ {%- for rudoc in udocs -%}
55
+ {% set is_disabled=(rudoc._id == handler.user._id) %}
56
+ <tr data-role="{{ rudoc.role }}" data-uid="{{ rudoc._id }}">
57
+ <td class="col--checkbox">
58
+ <label class="compact checkbox">
59
+ <input type="checkbox" data-checkbox-group="user" {% if is_disabled %}disabled{% else %}data-checkbox-range{% endif %}>
60
+ </label>
61
+ </td>
62
+ <td class="col--uid">
63
+ {{ rudoc._id }}
64
+ </td>
65
+ <td class="col--user">
66
+ {{ user.render_inline(rudoc, badge=false) }}
67
+ {% if not rudoc.join %}<span class="text-orange">({{ _('Not joined yet') }})</span>{% endif %}
68
+ </td>
69
+ <td class="col--role">
70
+ {{ form.select({
71
+ options:_rolesSelect,
72
+ name:'role',
73
+ value:rudoc.role,
74
+ extra_class:'compact',
75
+ disabled:is_disabled
76
+ }) }}
77
+ </td>
78
+ </tr>
79
+ {%- endfor -%}
80
+ {%- endif -%}
81
+ {%- endfor -%}
82
+ </tbody>
83
+ </table>
84
+ </div>
85
+ <div class="section__body">
86
+ <button class="rounded button" name="remove_selected">{{ _('Remove Selected User') }}</button>
87
+ <button class="rounded button" name="set_roles">{{ _('Set Roles for Selected User') }}</button>
88
+ </div>
89
+ </div>
90
+ <script>
91
+ document.addEventListener('DOMContentLoaded', function() {
92
+ var searchInput = document.getElementById('roleSearch');
93
+ searchInput.addEventListener('input', function() {
94
+ var filter = searchInput.value.toLowerCase();
95
+ var rows = document.querySelectorAll('#userTable tbody tr');
96
+ rows.forEach(function(row) {
97
+ var role = row.getAttribute('data-role');
98
+ if (role && role.toLowerCase().indexOf(filter) !== -1) {
99
+ row.style.display = '';
100
+ } else if (filter === '') {
101
+ row.style.display = '';
102
+ } else {
103
+ row.style.display = 'none';
104
+ }
105
+ });
106
+ });
107
+ });
108
+ </script>
109
+ {% endblock %}
@@ -134,14 +134,43 @@
134
134
  </div>
135
135
 
136
136
  <style>
137
- .col--uid { width: 80px; }
138
- .col--user { width: 150px; }
139
- .col--email { width: 200px; }
140
- .col--regat { width: 120px; }
141
- .col--loginat { width: 120px; }
142
- .col--priv { width: 100px; }
143
- .col--status { width: 80px; }
144
- .col--actions { width: 100px; }
137
+ .section__body.no-padding {
138
+ max-width: 100%;
139
+ overflow-x: hidden;
140
+ }
141
+
142
+ .data-table {
143
+ width: 100%;
144
+ table-layout: fixed;
145
+ }
146
+
147
+ .data-table th,
148
+ .data-table td {
149
+ word-break: break-word;
150
+ overflow-wrap: anywhere;
151
+ white-space: normal;
152
+ }
153
+
154
+ .data-table td {
155
+ max-width: 0;
156
+ }
157
+
158
+ .data-table td a,
159
+ .data-table td span,
160
+ .data-table td small {
161
+ word-break: break-word;
162
+ overflow-wrap: anywhere;
163
+ white-space: normal;
164
+ }
165
+
166
+ .col--uid { width: 8%; }
167
+ .col--user { width: 17%; }
168
+ .col--email { width: 19%; }
169
+ .col--regat { width: 13%; }
170
+ .col--loginat { width: 13%; }
171
+ .col--priv { width: 10%; }
172
+ .col--status { width: 8%; }
173
+ .col--actions { width: 12%; }
145
174
 
146
175
  .badge {
147
176
  display: inline-block;
@@ -171,6 +200,27 @@
171
200
  padding: 4px 8px;
172
201
  font-size: 12px;
173
202
  }
203
+
204
+ .col--actions .button--small {
205
+ display: block;
206
+ width: 100%;
207
+ max-width: 100%;
208
+ min-width: 0;
209
+ box-sizing: border-box;
210
+ padding: 4px 4px;
211
+ line-height: 1.2;
212
+ white-space: normal !important;
213
+ overflow-wrap: anywhere;
214
+ word-break: break-word;
215
+ text-align: center;
216
+ }
217
+
218
+ @media (max-width: 768px) {
219
+ .button--small {
220
+ padding: 3px 6px;
221
+ font-size: 11px;
222
+ }
223
+ }
174
224
  </style>
175
225
 
176
226
  <script>