core&ui: implement domain leave and kick

This commit is contained in:
undefined committed 2025-03-22 05:55:56 +08:00
1 parent f8ed636b30
commit a0f38d9ad1
13 files changed
+64 -28

No files matched your search

+3
View File
@@ -133,6 +133,8 @@ Hydro 用户群:1085853538
- import-qduoj: 修复 spj=null - import-qduoj: 修复 spj=null
- core: 修复文件复制 - core: 修复文件复制
- core: 支持存储并显示提交记录重测历史 - core: 支持存储并显示提交记录重测历史
- core: 新的加域逻辑
- ui: UserSelect.Multi: 支持批量粘贴用户名
### Breaking API Changes ### Breaking API Changes
@@ -144,6 +146,7 @@ Hydro 用户群:1085853538
- core: 移除 global.Hydro.ui.template - core: 移除 global.Hydro.ui.template
- core: 移除 global.Hydro.lib - core: 移除 global.Hydro.lib
- core: 移除 String.prototype.translate (使用 app.i18n.translate) - core: 移除 String.prototype.translate (使用 app.i18n.translate)
- core: 升级至 Mongo Driver 6
## Hydro 4.19.0 / UI 4.57.0 ## Hydro 4.19.0 / UI 4.57.0
+1 -1
View File
@@ -688,7 +688,6 @@ The role to assign when user joining the domain.: 사용자가 그룹에 참여
The uploaded file is too long.: 업로드한 파일이 너무 깁니다. The uploaded file is too long.: 업로드한 파일이 너무 깁니다.
The user is already a member of the domain.: 이미 그룹에 속한 사용자입니다. The user is already a member of the domain.: 이미 그룹에 속한 사용자입니다.
The user is too lazy to leave something here...: 설명이 없습니다... (´・ω・`) The user is too lazy to leave something here...: 설명이 없습니다... (´・ω・`)
Their account will not be deleted and they will be with the default role.: 계정은 삭제되지 않을 것이고, default 역할을 가질 것입니다.
There are no contests...: 대회 없음 There are no contests...: 대회 없음
There is no homework so far ╰(*°▽°*)╯: 과제 없음 ╰(*°▽°*)╯ There is no homework so far ╰(*°▽°*)╯: 과제 없음 ╰(*°▽°*)╯
This contest is not live.: 대회가 실시간이 아닙니다. This contest is not live.: 대회가 실시간이 아닙니다.
@@ -810,6 +809,7 @@ You cannot visit this domain.: 이 그룹을 방문할 수 없습니다.
You don't have the required permission ({0}) in this domain.: 그룹 권한이 없습니다. ({0})。 You don't have the required permission ({0}) in this domain.: 그룹 권한이 없습니다. ({0})。
You don't have the required privilege.: 권한이 없습니다. You don't have the required privilege.: 권한이 없습니다.
You don't own any domain and don't have privilege to create domain.: 그룹을 만들 권한이 없습니다. You don't own any domain and don't have privilege to create domain.: 그룹을 만들 권한이 없습니다.
You have been kicked from domain {0} by {1}.: 당신은 {1}에 의해 {0}에서 추방되었습니다.
You have not submitted any solutions for this problem: 아직 답안을 제출하지 않았습니다. You have not submitted any solutions for this problem: 아직 답안을 제출하지 않았습니다.
You have permission to create problems for this domain.: 이 그룹에서 문제를 만들기 위한 권한이 있습니다. You have permission to create problems for this domain.: 이 그룹에서 문제를 만들기 위한 권한이 있습니다.
You have uncompleted domain creation operation. You can click "Continue" to finish the creation.: 그룹 생성이 끝나지 않았습니다. "계속"을 눌러 생성을 완료하십시오. You have uncompleted domain creation operation. You can click "Continue" to finish the creation.: 그룹 생성이 끝나지 않았습니다. "계속"을 눌러 생성을 완료하십시오.
+1 -1
View File
@@ -792,7 +792,6 @@ The role to assign when user joining the domain.: 用户加入此域时分配的
The uploaded file is too long.: 上传的文件太长。 The uploaded file is too long.: 上传的文件太长。
The user is already a member of the domain.: 该用户已是域的成员。 The user is already a member of the domain.: 该用户已是域的成员。
The user is too lazy to leave something here...: 该用户太懒,这里啥也没写 (´・ω・`) The user is too lazy to leave something here...: 该用户太懒,这里啥也没写 (´・ω・`)
Their account will not be deleted and they will be with the default role.: 他们的账号不会被删除,并且之后将以 default 角色访问。
theme: 主题 theme: 主题
There are no contests...: 没有比赛… There are no contests...: 没有比赛…
There is no homework so far ╰(*°▽°*)╯: 目前还没有作业 ╰(*°▽°*)╯ There is no homework so far ╰(*°▽°*)╯: 目前还没有作业 ╰(*°▽°*)╯
@@ -934,6 +933,7 @@ You cannot visit this domain.: 您不能访问此域。
You don't have the required permission ({0}) in this domain.: 您在该域中无相应权限 ({0})。 You don't have the required permission ({0}) in this domain.: 您在该域中无相应权限 ({0})。
You don't have the required privilege.: 您没有相应的特权。 You don't have the required privilege.: 您没有相应的特权。
You don't own any domain and don't have privilege to create domain.: 您不拥有任何域也没有创建域的特权。 You don't own any domain and don't have privilege to create domain.: 您不拥有任何域也没有创建域的特权。
You have been kicked from domain {0} by {1}.: 您已被 {1} 从域 {0} 中移除。
You have not submitted any solutions for this problem: 您在该题下没有递交过任何解答 You have not submitted any solutions for this problem: 您在该题下没有递交过任何解答
You have permission to create problems for this domain.: 您有权限为此域创建题目。 You have permission to create problems for this domain.: 您有权限为此域创建题目。
You have uncompleted domain creation operation. You can click "Continue" to finish the creation.: 您有未完成的创建域操作。您可以点击“继续”来完成创建。 You have uncompleted domain creation operation. You can click "Continue" to finish the creation.: 您有未完成的创建域操作。您可以点击“继续”来完成创建。
+1 -1
View File
@@ -463,7 +463,6 @@ Technical Information: 技術資訊
Temporary session: 臨時會話 Temporary session: 臨時會話
Text: 文字 Text: 文字
The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 功課已經截止,但處於延期時間内。您此時遞交的題目無法獲得全部分數。 The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 功課已經截止,但處於延期時間内。您此時遞交的題目無法獲得全部分數。
Their account will not be deleted and they will be with the default role.: 他們的賬號不會被刪除,並且之後將以 default 角色訪問。
There are no contests...: 沒有比賽… There are no contests...: 沒有比賽…
There is no homework so far ╰(*°▽°*)╯: 目前沒有功課 ╰(*°▽°*)╯ There is no homework so far ╰(*°▽°*)╯: 目前沒有功課 ╰(*°▽°*)╯
This contest is not live.: 比賽沒有開始。 This contest is not live.: 比賽沒有開始。
@@ -551,6 +550,7 @@ You cannot submit for this problem because the homework's deadline is due.: 功
You don't have the required permission ({0}) in this domain.: 您在該域中無相應許可權 ({0})。 You don't have the required permission ({0}) in this domain.: 您在該域中無相應許可權 ({0})。
You don't have the required privilege.: 您沒有相應的特權。 You don't have the required privilege.: 您沒有相應的特權。
You don't own any domain and don't have privilege to create domain.: 您不擁有任何域也沒有創建域的特權。 You don't own any domain and don't have privilege to create domain.: 您不擁有任何域也沒有創建域的特權。
You have been kicked from domain {0} by {1}.: 您已被 {1} 從域 {0} 中移除。
You have not submitted any solutions for this problem: 您在該題下沒有遞交過任何解答 You have not submitted any solutions for this problem: 您在該題下沒有遞交過任何解答
You have permission to create problems for this domain.: 您有許可權為此域創建題目。 You have permission to create problems for this domain.: 您有許可權為此域創建題目。
You haven't attended this contest yet.: 您還沒有參加本次比賽。 You haven't attended this contest yet.: 您還沒有參加本次比賽。
+21 -10
View File
@@ -11,6 +11,7 @@ import avatar from '../lib/avatar';
import { PERM, PERMS_BY_FAMILY, PRIV } from '../model/builtin'; import { PERM, PERMS_BY_FAMILY, PRIV } from '../model/builtin';
import * as discussion from '../model/discussion'; import * as discussion from '../model/discussion';
import domain from '../model/domain'; import domain from '../model/domain';
import MessageModel from '../model/message';
import * as oplog from '../model/oplog'; import * as oplog from '../model/oplog';
import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting'; import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting';
import * as system from '../model/system'; import * as system from '../model/system';
@@ -163,11 +164,15 @@ class DomainUserHandler extends ManageHandler {
}; };
} }
@param('uids', Types.NumericArray)
async post({ }, uids: number[]) {
if (uids.includes(this.domain.owner)) throw new ForbiddenError();
}
@requireSudo @requireSudo
@post('uid', Types.Int) @param('uids', Types.NumericArray)
@post('role', Types.Role) @param('role', Types.Role)
async postSetUser(domainId: string, uid: number, role: string) { async postSetUsers(domainId: string, uid: number[], role: string) {
if (uid === this.domain.owner) throw new ForbiddenError();
await Promise.all([ await Promise.all([
domain.setUserRole(domainId, uid, role), domain.setUserRole(domainId, uid, role),
oplog.log(this, 'domain.setRole', { uid, role }), oplog.log(this, 'domain.setRole', { uid, role }),
@@ -176,14 +181,20 @@ class DomainUserHandler extends ManageHandler {
} }
@requireSudo @requireSudo
@param('uid', Types.NumericArray) @param('uids', Types.NumericArray)
@param('role', Types.Role) async postKick({ domainId }, uids: number[]) {
async postSetUsers(domainId: string, uid: number[], role: string) { const original = await domain.getMultiUserInDomain(domainId, { uid: { $in: uids } }).toArray();
if (uid.includes(this.domain.owner)) throw new ForbiddenError(); const needUpdate = uids.filter((uid) => original.find((i) => i.uid === uid)?.join);
if (!needUpdate.length) return;
await Promise.all([ await Promise.all([
domain.setUserRole(domainId, uid, role), domain.setJoin(domainId, needUpdate.length > 1 ? needUpdate : needUpdate[0], false),
oplog.log(this, 'domain.setRole', { uid, role }), oplog.log(this, 'domain.kick', { uids: needUpdate }),
]); ]);
const msg = JSON.stringify({
message: 'You have been kicked from domain {0} by {1}.',
params: [this.domain.name, this.user.uname],
});
await Promise.all(needUpdate.map((i) => MessageModel.send(1, i, msg, MessageModel.FLAG_RICHTEXT | MessageModel.FLAG_UNREAD)));
this.back(); this.back();
} }
} }
+7 -1
View File
@@ -488,7 +488,7 @@ class HomeDomainHandler extends Handler {
} else { } else {
for (const ddoc of ddocs) { for (const ddoc of ddocs) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const udoc = (await user.getById(ddoc._id, this.user._id))!; const udoc = await user.getById(ddoc._id, this.user._id);
canManage[ddoc._id] = udoc.hasPerm(PERM.PERM_EDIT_DOMAIN); canManage[ddoc._id] = udoc.hasPerm(PERM.PERM_EDIT_DOMAIN);
role[ddoc._id] = udoc.role; role[ddoc._id] = udoc.role;
} }
@@ -504,6 +504,12 @@ class HomeDomainHandler extends Handler {
else user.setById(this.user._id, { pinnedDomains: this.user.pinnedDomains.filter((i) => i !== id) }); else user.setById(this.user._id, { pinnedDomains: this.user.pinnedDomains.filter((i) => i !== id) });
this.back({ star }); this.back({ star });
} }
@param('id', Types.String)
async postLeave({ }, id: string) {
await domain.setJoin(id, this.user._id, false);
this.back();
}
} }
class HomeDomainCreateHandler extends Handler { class HomeDomainCreateHandler extends Handler {
+10 -1
View File
@@ -135,7 +135,7 @@ class DomainModel {
@ArgMethod @ArgMethod
static async setUserRole(domainId: string, uid: MaybeArray<number>, role: string, autojoin = false) { static async setUserRole(domainId: string, uid: MaybeArray<number>, role: string, autojoin = false) {
const update = { $set: { role, ...(autojoin ? { join: true } : {}) } }; const update = { $set: { role, ...(autojoin ? { join: true } : {}) } };
if (!(uid instanceof Array)) { if (!(Array.isArray(uid))) {
const res = await collUser.findOneAndUpdate( const res = await collUser.findOneAndUpdate(
{ domainId, uid }, { domainId, uid },
update, update,
@@ -152,6 +152,15 @@ class DomainModel {
return await collUser.updateMany({ domainId, uid: { $in: uid } }, update, { upsert: true }); return await collUser.updateMany({ domainId, uid: { $in: uid } }, update, { upsert: true });
} }
static async setJoin(domainId: string, uid: MaybeArray<number>, join: boolean) {
if (!(Array.isArray(uid))) {
await DomainModel.updateUserInDomain(domainId, uid, { $set: { join } });
return;
}
await collUser.updateMany({ domainId, uid: { $in: uid } }, { $set: { join } });
deleteUserCache(domainId);
}
static async getRoles(domainId: string, count?: boolean): Promise<any[]>; static async getRoles(domainId: string, count?: boolean): Promise<any[]>;
static async getRoles(domain: DomainDoc, count?: boolean): Promise<any[]>; static async getRoles(domain: DomainDoc, count?: boolean): Promise<any[]>;
@ArgMethod @ArgMethod
+2 -1
View File
@@ -680,6 +680,7 @@ Terms of Service: 이용 약관
Test data comes from: Testdata 출처 Test data comes from: Testdata 출처
Testdata Config: Testdata 설정 Testdata Config: Testdata 설정
Text: 본문 Text: 본문
Leave: 나가기
The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 추가 접수 중입니다. 제출할 수 있지만 감점될 수 있습니다. The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 추가 접수 중입니다. 제출할 수 있지만 감점될 수 있습니다.
The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.: 초대 코드는 64글자 미만입니다. The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.: 초대 코드는 64글자 미만입니다.
The invitation code you provided is invalid.: 잘못된 초대 코드 The invitation code you provided is invalid.: 잘못된 초대 코드
@@ -688,7 +689,7 @@ The role to assign when user joining the domain.: 사용자가 그룹에 참여
The uploaded file is too long.: 업로드한 파일이 너무 깁니다. The uploaded file is too long.: 업로드한 파일이 너무 깁니다.
The user is already a member of the domain.: 이미 그룹에 속한 사용자입니다. The user is already a member of the domain.: 이미 그룹에 속한 사용자입니다.
The user is too lazy to leave something here...: 설명이 없습니다... (´・ω・`) The user is too lazy to leave something here...: 설명이 없습니다... (´・ω・`)
Their account will not be deleted and they will be with the default role.: 계정은 삭제되지 않을 것이고, default 역할을 가질 것입니다. Their account will not be deleted and they will be with the guest role until they re-join the domain.: 계정은 삭제되지 않을 것이고, guest 역할을 가질 것입니다. 그룹에 다시 참여할 때까지 말이죠.
There are no contests...: 대회 없음 There are no contests...: 대회 없음
There is no homework so far ╰(*°▽°*)╯: 과제 없음 ╰(*°▽°*)╯ There is no homework so far ╰(*°▽°*)╯: 과제 없음 ╰(*°▽°*)╯
This contest is not live.: 대회가 실시간이 아닙니다. This contest is not live.: 대회가 실시간이 아닙니다.
+2 -1
View File
@@ -512,6 +512,7 @@ LaTeX Expressions: LaTeX 表达式
Learn More: 了解更多 Learn More: 了解更多
Leave blank to use numeric pid.: 留空来使用数字题号。 Leave blank to use numeric pid.: 留空来使用数字题号。
Leave empty for default: 留空以使用默认 Leave empty for default: 留空以使用默认
Leave: 离开
Ledo: 乐多 Ledo: 乐多
Legacy mode: 兼容模式 Legacy mode: 兼容模式
Light: 亮色 Light: 亮色
@@ -916,7 +917,7 @@ The scoreboard was frozen with {0} minutes remaining at {1} - submissions in the
The uploaded file is too long.: 上传的文件太长。 The uploaded file is too long.: 上传的文件太长。
The user is already a member of the domain.: 该用户已是域的成员。 The user is already a member of the domain.: 该用户已是域的成员。
The user is too lazy to leave something here...: 该用户太懒,这里啥也没写 (´・ω・`) The user is too lazy to leave something here...: 该用户太懒,这里啥也没写 (´・ω・`)
Their account will not be deleted and they will be with the default role.: 他们的账号不会被删除,并且之后将以 default 角色访问。 Their account will not be deleted and they will be with the guest role until they re-join the domain.: 他们的账号不会被删除,并且之后将以 guest 角色访问,直到他们重新加入域。
Theme: 主题 Theme: 主题
There are no contests...: 没有比赛… There are no contests...: 没有比赛…
There is no homework so far ╰(*°▽°*)╯: 目前还没有作业 ╰(*°▽°*)╯ There is no homework so far ╰(*°▽°*)╯: 目前还没有作业 ╰(*°▽°*)╯
+2 -1
View File
@@ -238,6 +238,7 @@ Last Submit At: 最後遞交於
Last Update At: 最後活動時間 Last Update At: 最後活動時間
LaTeX Expressions: LaTeX 表示式 LaTeX Expressions: LaTeX 表示式
Learn More: 瞭解更多 Learn More: 瞭解更多
Leave: 離開
Links: 連結 Links: 連結
List View: 列表視圖 List View: 列表視圖
Live: 正在進行 Live: 正在進行
@@ -462,7 +463,7 @@ Technical Information: 技術資訊
Temporary session: 臨時會話 Temporary session: 臨時會話
Text: 文字 Text: 文字
The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 功課已經截止,但處於延期時間内。您此時遞交的題目無法獲得全部分數。 The homework's deadline is due but in extension. You can still submit for this problem but your score will be penalized.: 功課已經截止,但處於延期時間内。您此時遞交的題目無法獲得全部分數。
Their account will not be deleted and they will be with the default role.: 他們的賬號不會被刪除,並且之後將以 default 角色訪問。 Their account will not be deleted and they will be with the guest role until they re-join the domain.: 他們的賬號不會被刪除,並且之後將以 guest 角色訪問,直到他們重新加入域。
There are no contests...: 沒有比賽… There are no contests...: 沒有比賽…
There is no homework so far ╰(*°▽°*)╯: 目前沒有功課 ╰(*°▽°*)╯ There is no homework so far ╰(*°▽°*)╯: 目前沒有功課 ╰(*°▽°*)╯
This contest is not live.: 比賽沒有開始。 This contest is not live.: 比賽沒有開始。
@@ -60,8 +60,8 @@ const page = new NamedPage('domain_user', () => {
const role = addUserDialog.$dom.find('[name="role"]').val(); const role = addUserDialog.$dom.find('[name="role"]').val();
try { try {
await request.post('', { await request.post('', {
operation: 'set_user', operation: 'set_users',
uid: user._id, uids: [user._id],
role, role,
}); });
window.location.reload(); window.location.reload();
@@ -91,15 +91,14 @@ const page = new NamedPage('domain_user', () => {
$body: tpl` $body: tpl`
<div class="typo"> <div class="typo">
<p>${i18n('Confirm removing the selected users?')}</p> <p>${i18n('Confirm removing the selected users?')}</p>
<p>${i18n('Their account will not be deleted and they will be with the default role.')}</p> <p>${i18n('Their account will not be deleted and they will be with the guest role until they re-join the domain.')}</p>
</div>`, </div>`,
}).open(); }).open();
if (action !== 'yes') return; if (action !== 'yes') return;
try { try {
await request.post('', { await request.post('', {
operation: 'set_users', operation: 'kick',
uid: selectedUsers, uids: selectedUsers,
role: 'default',
}); });
Notification.success(i18n('Selected users have been removed from the domain.')); Notification.success(i18n('Selected users have been removed from the domain.'));
await delay(2000); await delay(2000);
@@ -122,7 +121,7 @@ const page = new NamedPage('domain_user', () => {
try { try {
await request.post('', { await request.post('', {
operation: 'set_users', operation: 'set_users',
uid: selectedUsers, uids: selectedUsers,
role, role,
}); });
Notification.success(i18n('Role has been updated to {0} for selected users.', role)); Notification.success(i18n('Role has been updated to {0} for selected users.', role));
@@ -138,8 +137,8 @@ const page = new NamedPage('domain_user', () => {
const role = $(ev.currentTarget).val(); const role = $(ev.currentTarget).val();
try { try {
await request.post('', { await request.post('', {
operation: 'set_user', operation: 'set_users',
uid: row.attr('data-uid'), uids: [row.attr('data-uid')],
role, role,
}); });
Notification.success(i18n('Role has been updated to {0}.', role)); Notification.success(i18n('Role has been updated to {0}.', role));
@@ -11,7 +11,7 @@
border-left: 1px solid $table-border-color border-left: 1px solid $table-border-color
.col--action .col--action
width: rem(135px) width: rem(200px)
+mobile() +mobile()
.col--icon, .col--role .col--icon, .col--role
@@ -44,6 +44,11 @@
{% if canManage[ddoc['_id']] %} {% if canManage[ddoc['_id']] %}
· <a class="typo-a" href="{{ url('domain_dashboard', domainId=ddoc['_id']) }}">{{ _('Manage') }}</a> · <a class="typo-a" href="{{ url('domain_dashboard', domainId=ddoc['_id']) }}">{{ _('Manage') }}</a>
{% endif %} {% endif %}
· <form class="form--inline" method="post">
<input type="hidden" name="operation" value="leave">
<input type="hidden" name="id" value="{{ ddoc['_id'] }}">
<button class="typo-a" type="submit">{{ _('Leave') }}</button>
</form>
</td> </td>
</tr> </tr>
{%- endfor -%} {%- endfor -%}