Implement permission manage web UI

This commit is contained in:
Yukino Song
2024-09-16 23:10:19 +08:00
parent 2e7bde8958
commit db5790b374
14 changed files with 714 additions and 125 deletions

View File

@@ -57,6 +57,71 @@
<div class="alert alert-warning">
<b>{{ $t('_common.warning') }}</b> {{ $t('pin.warning_msg') }}
</div>
<!-- Unpair Clients -->
<div class="card my-4 align-self-stretch">
<div class="card-body">
<div class="p-2">
<div class="d-flex justify-content-end align-items-center">
<h2 id="unpair" class="me-auto">{{ $t('pin.device_management') }}</h2>
<button class="btn btn-danger" :disabled="unpairAllPressed" @click="unpairAll">
{{ $t('pin.unpair_all') }}
</button>
</div>
<br />
<p class="mb-0">{{ $t('pin.device_management_desc') }}</p>
<div id="apply-alert" class="alert alert-success d-flex align-items-center mt-3" :style="{ 'display': (showApplyMessage ? 'flex !important': 'none !important') }">
<div class="me-2"><b>{{ $t('_common.success') }}</b> {{ $t('pin.unpair_single_success') }}</div>
<button class="btn btn-success ms-auto apply" @click="clickedApplyBanner">{{ $t('_common.dismiss') }}</button>
</div>
<div class="alert alert-success mt-3" v-if="unpairAllStatus === true">
{{ $t('pin.unpair_all_success') }}
</div>
<div class="alert alert-danger mt-3" v-if="unpairAllStatus === false">
{{ $t('pin.unpair_all_error') }}
</div>
</div>
</div>
<ul id="client-list" class="list-group list-group-flush list-group-item-light" v-if="clients && clients.length > 0">
<template v-for="client in clients" class="list-group-item d-flex align-items-center">
<div v-if="client.editing" class="list-group-item d-flex align-items-stretch flex-column">
<div class="d-flex align-items-center">
<div class="p-2 flex-grow-1 d-flex align-items-center">
<span class="badge" :class="client.editPerm >= 0x04000000 ? 'bg-danger' : 'bg-primary'">
[ {{permToStr(client.editPerm)}} ]
</span>
&nbsp;
<input v-model="client.editName" @keyup.enter="saveClient(client)" class="form-control flex-grow-1" type="text" :placeholder="$t('pin.device_name')">
</div>
<div class="me-2 btn btn-success" @click="saveClient(client)"><i class="fas fa-check"></i></div>
<div class="me-2 btn btn-secondary" @click="cancelEdit(client)"><i class="fas fa-times"></i></div>
</div>
<div class="align-items-top d-flex flex-row justify-content-center">
<div v-for="group in permissionGroups" class="d-flex flex-column mx-2">
<div class="mx-2">{{ group.name }}:</div>
<button v-for="perm in group.permissions" class="my-1 btn btn-sm" :disabled="isSupressed(client.editPerm, perm.name, perm.supressed_by)" :class="(isSupressed(client.editPerm, perm.name, perm.supressed_by) || checkPermission(client.editPerm, perm.name)) ? 'btn-success' : 'btn-outline-secondary'" @click="togglePermission(client, perm.name)">
{{ $t(`permissions.${perm.name}`) }}
</button>
</div>
</div>
</div>
<div v-else class="list-group-item d-flex align-items-center">
<div class="p-2 flex-grow-1 d-flex align-items-center">
<span class="badge" :class="client.perm >= 0x04000000 ? 'bg-danger' : 'bg-primary'">
[ {{permToStr(client.perm)}} ]
</span>
&nbsp;
<span class="me-2">{{client.name != "" ? client.name : $t('pin.unpair_single_unknown')}}</span>
</div>
<div v-if="client.connected" class="me-2 btn btn-warning" @click="disconnectClient(client.uuid)"><i class="fas fa-link-slash"></i></div>
<div class="me-2 btn btn-primary" @click="editClient(client)"><i class="fas fa-edit"></i></div>
<div class="me-2 btn btn-danger" @click="unpairSingle(client.uuid)"><i class="fas fa-trash"></i></div>
</div>
</template>
</ul>
<ul v-else class="list-group list-group-flush list-group-item-light">
<div class="list-group-item p-3 text-center"><em>{{ $t('pin.unpair_single_no_devices') }}</em></div>
</ul>
</div>
</div>
</body>
@@ -91,6 +156,106 @@
}
}
/**
* Permissions:
enum class PERM: uint32_t {
_reserved = 1,
_input = _reserved << 8, // Input permission group
input_controller = _input << 0, // Allow controller input
input_touch = _input << 1, // Allow touch input
input_pen = _input << 2, // Allow pen input
input_kbdm = _input << 3, // Allow kbd/mouse input
_all_inputs = input_controller | input_touch | input_pen | input_kbdm,
_operation = _input << 8, // Operation permission group
clipboard_set = _operation << 0, // Allow set clipboard from client
clipboard_read = _operation << 1, // Allow read clipboard from host
file_upload = _operation << 2, // Allow upload files to host
file_dwnload = _operation << 3, // Allow download files from host
server_cmd = _operation << 4, // Allow execute server cmd
_all_opeiations = clipboard_set | clipboard_read | file_upload | file_dwnload | server_cmd,
_action = _operation << 8, // Action permission group
list = _action << 0, // Allow list apps
view = _action << 1, // Allow view streams
launch = _action << 2, // Allow launch apps
_allow_view = view | launch, // Launch contains view permission
_all_actions = list | view | launch,
_default = view | list, // Default permissions for new clients
_no = 0, // No permissions are granted
_all = _all_inputs | _all_opeiations | _all_actions, // All current permissions
};
*/
const permissionMapping = {
// Input permission group
input_controller: 0x00000100,
input_touch: 0x00000200,
input_pen: 0x00000400,
input_kbdm: 0x00000800,
_all_inputs: 0x00000F00,
// Operation permission group
clipboard_set: 0x00010000,
clipboard_read: 0x00020000,
file_upload: 0x00040000,
file_dwnload: 0x00080000,
server_cmd: 0x00100000,
_all_operations: 0x001F0000,
// Action permission group
list: 0x01000000,
view: 0x02000000,
launch: 0x04000000,
_allow_view: 0x06000000,
_all_actions: 0x07000000,
// Special permissions
_default: 0x03000000,
_no: 0x00000000,
_all: 0x071F0F00
};
const permissionGroups = [
{ name: 'Action', permissions: [
{
name: 'list',
supressed_by: ['view', 'launch']
}, {
name: 'view',
supressed_by: ['launch']
}, {
name: 'launch',
supressed_by: []
}
] },
{ name: 'Operation', permissions: [
{
name: 'server_cmd',
supressed_by: []
}
] },
{ name: 'Input', permissions: [
{
name: 'input_controller',
supressed_by: []
}, {
name: 'input_touch',
supressed_by: []
}, {
name: 'input_pen',
supressed_by: []
}, {
name: 'input_kbdm',
supressed_by: []
}
] },
];
let currentEditingClient = null;
const data = () => {
return {
editingHost: false,
@@ -102,7 +267,12 @@
deviceName: '',
hostAddr: '',
hostPort: '',
hostName: ''
hostName: '',
permissionGroups,
clients: [],
showApplyMessage: false,
unpairAllPressed: false,
unpairAllStatus: null
}
}
@@ -120,10 +290,14 @@
return !!(this.hostAddr && this.hostPort);
}
},
created() {
this.refreshClients();
},
methods: {
switchTab(currentTab) {
location.hash = currentTab;
Object.assign(this, data());
const clients = this.clients;
Object.assign(this, data(), { clients });
hostInfoCache = null;
clearTimeout(resetOTPTimeout);
},
@@ -155,6 +329,8 @@
).innerHTML = `<div class="alert alert-success" role="alert">${this.i18n.t('pin.pair_success')}</div>`;
document.querySelector("#pin-input").value = "";
document.querySelector("#name-input").value = "";
setTimeout(() => this.refreshClients(), 1000);
} else {
document.querySelector(
"#status"
@@ -217,7 +393,128 @@
}
}
})
}
},
clickedApplyBanner() {
this.showApplyMessage = false;
},
editClient(client) {
if (currentEditingClient) {
this.cancelEdit(currentEditingClient);
}
currentEditingClient = client;
client.editing = true;
client.editPerm = client.perm;
client.editName = client.name;
},
cancelEdit(client) {
client.editing = false;
client.editPerm = client.perm;
client.editName = client.name;
currentEditingClient = null;
},
saveClient(client) {
client.editing = false;
currentEditingClient = null;
const editedClient = {
uuid: client.uuid,
name: client.editName,
perm: client.editPerm & permissionMapping._all
}
fetch("/api/clients/update", {
credentials: 'include',
method: "POST",
body: JSON.stringify(editedClient)
}).finally(() => {
setTimeout(() => {
this.refreshClients();
}, 1000);
});
},
permToStr(perm) {
const permSegments = [];
permSegments.push((perm >> 24) & 0xFF);
permSegments.push((perm >> 16) & 0xFF);
permSegments.push((perm >> 8) & 0xFF);
return permSegments.map(seg => seg.toString(16).toUpperCase().padStart(2, '0')).join(' ');
},
checkPermission(perm, permission) {
return (perm & permissionMapping[permission]) !== 0;
},
isSupressed(perm, permission, supressed_by) {
for (const supressed of supressed_by) {
if (this.checkPermission(perm, supressed)) {
return true;
}
}
return false;
},
togglePermission(client, permission) {
client.editPerm ^= permissionMapping[permission];
},
disconnectClient(uuid) {
fetch("/api/clients/disconnect", {
credentials: 'include',
method: "POST",
body: JSON.stringify({ uuid })
}).finally(() => {
setTimeout(() => {
this.refreshClients();
}, 1000);
});
},
unpairAll() {
this.unpairAllPressed = true;
fetch("/api/clients/unpair-all", {
credentials: 'include',
method: "POST"
})
.then((r) => r.json())
.then((r) => {
this.unpairAllPressed = false;
this.unpairAllStatus = r.status.toString() === "true";
setTimeout(() => {
this.unpairAllStatus = null;
}, 5000);
this.refreshClients();
});
},
unpairSingle(uuid) {
fetch("/api/clients/unpair", {
credentials: 'include',
method: "POST",
body: JSON.stringify({ uuid })
}).then(() => {
this.showApplyMessage = true;
this.refreshClients();
});
},
refreshClients() {
if (currentEditingClient) {
this.cancelEdit(currentEditingClient);
}
fetch("/api/clients/list", { credentials: 'include' })
.then((response) => response.json())
.then((response) => {
const clientList = document.querySelector("#client-list");
if (response.status === 'true' && response.named_certs && response.named_certs.length) {
this.clients = response.named_certs.map(({name, uuid, perm, connected}) => {
const permInt = parseInt(perm, 10);
return {
name,
uuid,
perm: permInt,
connected: connected === 'true',
editing: false,
editPerm: permInt,
editName: name
}
})
currentEditingClient = null;
} else {
this.clients = [];
}
});
},
}
});

View File

@@ -388,6 +388,20 @@
"password_change": "Password Change",
"success_msg": "Password has been changed successfully! This page will reload soon, your browser will ask you for the new credentials."
},
"permissions": {
"input_controller": "Controller Input",
"input_touch": "Touch Input",
"input_pen": "Pen Input",
"input_kbdm": "Keyboard & Mouse Input",
"clipboard_set": "Clipboard Set",
"clipboard_read": "Clipboard Read",
"file_upload": "File Upload",
"file_dwnload": "File Download",
"server_cmd": "Server Command",
"list": "List Apps",
"view": "View Streams",
"launch": "Launch Apps"
},
"pin": {
"device_name": "Optional: Device Name",
"pair_failure": "Pairing Failed: Check if the PIN is typed correctly",
@@ -402,7 +416,15 @@
"otp_expired_msg": "OTP expired. Please request a new one.",
"otp_success": "PIN request success, the PIN is available within 3 minutes.",
"otp_msg": "OTP pairing is only available for the latest Artemis clients. Please use legacy pairing method for other clients.",
"otp_pair_now": "PIN generated successfully, do you want to pair now?"
"otp_pair_now": "PIN generated successfully, do you want to pair now?",
"device_management": "Device Management",
"device_management_desc": "Manage your paired devices.",
"unpair_all": "Unpair All",
"unpair_all_success": "All devices unpaired.",
"unpair_all_error": "Error while unpairing",
"unpair_single_no_devices": "There are no paired devices.",
"unpair_single_success": "However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.",
"unpair_single_unknown": "Unknown Client"
},
"resource_card": {
"github_discussions": "GitHub Discussions",
@@ -430,15 +452,7 @@
"quit_apollo_success": "Apollo has exited.",
"quit_apollo_success_ongoing": "Apollo is quitting...",
"quit_apollo_confirm": "Do you really want to quit Apollo? You'll not be able to start Apollo again if you have no other methods to operate your computer.",
"troubleshooting": "Troubleshooting",
"unpair_all": "Unpair All",
"unpair_all_error": "Error while unpairing",
"unpair_all_success": "All devices unpaired.",
"unpair_desc": "Remove your paired devices. Individually unpaired devices with an active session will remain connected, but cannot start or resume a session.",
"unpair_single_no_devices": "There are no paired devices.",
"unpair_single_success": "However, the device(s) may still be in an active session. Use the 'Force Close' button above to end any open sessions.",
"unpair_single_unknown": "Unknown Client",
"unpair_title": "Unpair Devices"
"troubleshooting": "Troubleshooting"
},
"welcome": {
"confirm_password": "Confirm password",

View File

@@ -388,6 +388,20 @@
"password_change": "更改密码",
"success_msg": "密码已成功更改!此页面即将重新加载,您的浏览器将要求您输入新的账户信息。"
},
"permissions": {
"input_controller": "手柄输入",
"input_touch": "触摸输入",
"input_pen": "笔输入",
"input_kbdm": "键鼠输入",
"clipboard_set": "上传剪贴板",
"clipboard_read": "获取剪贴板",
"file_upload": "上传文件",
"file_dwnload": "下载文件",
"server_cmd": "服务端命令",
"list": "列出APP",
"view": "查看串流",
"launch": "启动APP"
},
"pin": {
"device_name": "设备名称",
"pair_failure": "配对失败:请检查 PIN 码是否正确输入",
@@ -402,7 +416,15 @@
"otp_expired_msg": "口令已过期,请重新请求。",
"otp_success": "一次性 PIN 请求成功3分钟内有效。",
"otp_msg": "一次性口令目前仅支持最新的 Artemis 客户端使用。其他客户端请使用传统配对方式。",
"otp_pair_now": "PIN 请求成功,是否一键配对?"
"otp_pair_now": "PIN 请求成功,是否一键配对?",
"device_management": "设备管理",
"device_management_desc": "管理已配对的设备。",
"unpair_all": "全部取消配对",
"unpair_all_success": "全部取消配对成功!",
"unpair_all_error": "取消配对时出错",
"unpair_single_no_devices": "没有配对的设备。",
"unpair_single_success": "然而,设备可能仍然处于活动会话中,使用上面的“强制关闭”按钮结束任何打开的会话。",
"unpair_single_unknown": "未知客户端"
},
"resource_card": {
"github_discussions": "Github 讨论区",
@@ -430,15 +452,7 @@
"quit_apollo_success": "Apollo 已成功退出。",
"quit_apollo_success_ongoing": "Apollo 正在退出...",
"quit_apollo_confirm": "确定要退出 Apollo 吗?如果没有其他操作方式,你将无法再次启动 Apollo。",
"troubleshooting": "故障排除",
"unpair_all": "全部取消配对",
"unpair_all_error": "取消配对时出错",
"unpair_all_success": "取消配对成功!",
"unpair_desc": "删除您已配对的设备。未配对的单独设备与活动会话将保持连接,但不能启动或继续会话。",
"unpair_single_no_devices": "没有配对的设备。",
"unpair_single_success": "然而,设备可能仍然处于活动会话中,使用上面的“强制关闭”按钮结束任何打开的会话。",
"unpair_single_unknown": "未知客户端",
"unpair_title": "取消配对设备"
"troubleshooting": "故障排除"
},
"welcome": {
"confirm_password": "确认密码",

View File

@@ -94,40 +94,6 @@
</div>
</div>
</div>
<!-- Unpair Clients -->
<div class="card my-4">
<div class="card-body">
<div class="p-2">
<div class="d-flex justify-content-end align-items-center">
<h2 id="unpair" class="text-center me-auto">{{ $t('troubleshooting.unpair_title') }}</h2>
<button class="btn btn-danger" :disabled="unpairAllPressed" @click="unpairAll">
{{ $t('troubleshooting.unpair_all') }}
</button>
</div>
<br />
<p class="mb-0">{{ $t('troubleshooting.unpair_desc') }}</p>
<div id="apply-alert" class="alert alert-success d-flex align-items-center mt-3" :style="{ 'display': (showApplyMessage ? 'flex !important': 'none !important') }">
<div class="me-2"><b>{{ $t('_common.success') }}</b> {{ $t('troubleshooting.unpair_single_success') }}</div>
<button class="btn btn-success ms-auto apply" @click="clickedApplyBanner">{{ $t('_common.dismiss') }}</button>
</div>
<div class="alert alert-success mt-3" v-if="unpairAllStatus === true">
{{ $t('troubleshooting.unpair_all_success') }}
</div>
<div class="alert alert-danger mt-3" v-if="unpairAllStatus === false">
{{ $t('troubleshooting.unpair_all_error') }}
</div>
</div>
</div>
<ul id="client-list" class="list-group list-group-flush list-group-item-light" v-if="clients && clients.length > 0">
<div v-for="client in clients" class="list-group-item d-flex">
<div class="p-2 flex-grow-1">{{client.name != "" ? client.name : $t('troubleshooting.unpair_single_unknown')}}</div><div class="me-2 ms-auto btn btn-danger" @click="unpairSingle(client.uuid)"><i class="fas fa-trash"></i></div>
</div>
</ul>
<ul v-else class="list-group list-group-flush list-group-item-light">
<div class="list-group-item p-3 text-center"><em>{{ $t('troubleshooting.unpair_single_no_devices') }}</em></div>
</ul>
</div>
<!-- Logs -->
<div class="card p-2 my-4">
<div class="card-body">
@@ -166,10 +132,7 @@
logInterval: null,
serverRestarting: false,
serverQuitting: false,
serverQuit: false,
showApplyMessage: false,
unpairAllPressed: false,
unpairAllStatus: null,
serverQuit: false
};
},
computed: {
@@ -213,48 +176,6 @@
}, 5000);
});
},
unpairAll() {
this.unpairAllPressed = true;
fetch("/api/clients/unpair-all", {
credentials: 'include',
method: "POST"
})
.then((r) => r.json())
.then((r) => {
this.unpairAllPressed = false;
this.unpairAllStatus = r.status.toString() === "true";
setTimeout(() => {
this.unpairAllStatus = null;
}, 5000);
this.refreshClients();
});
},
unpairSingle(uuid) {
fetch("/api/clients/unpair", { credentials: 'include',
method: "POST",
body: JSON.stringify({ uuid })
}).then(() => {
this.showApplyMessage = true;
this.refreshClients();
});
},
refreshClients() {
fetch("/api/clients/list", { credentials: 'include' })
.then((response) => response.json())
.then((response) => {
const clientList = document.querySelector("#client-list");
if (response.status === 'true' && response.named_certs && response.named_certs.length) {
this.clients = response.named_certs.sort((a, b) => {
return (a.name.toLowerCase() > b.name.toLowerCase() || a.name == "" ? 1 : -1)
});
} else {
this.clients = [];
}
});
},
clickedApplyBanner() {
this.showApplyMessage = false;
},
copyLogs() {
navigator.clipboard.writeText(this.actualLogs);
},