hydrooj-addons-manager 0.0.1
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/index.ts +153 -0
- package/locale/zh_TW.yaml +11 -0
- package/package.json +7 -0
- package/templates/manage_addons.html +143 -0
- package/templates/manage_base.html +24 -0
package/index.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { HydroResponse } from '@hydrooj/framework';
|
|
2
|
+
import {
|
|
3
|
+
Context, Handler, PRIV, Schema,
|
|
4
|
+
Service, superagent, SystemModel,
|
|
5
|
+
TokenModel,
|
|
6
|
+
UserFacingError, ForbiddenError, Types,
|
|
7
|
+
Model,
|
|
8
|
+
requireSudo
|
|
9
|
+
} from 'hydrooj';
|
|
10
|
+
import { exec } from 'child_process';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
|
|
13
|
+
let addonsManagerModel : {
|
|
14
|
+
add: (name: string, version?: string) => Promise<commandResult>,
|
|
15
|
+
update: (name: string, version?: string) => Promise<commandResult>,
|
|
16
|
+
remove: (name: string) => Promise<commandResult>,
|
|
17
|
+
getActivedPackages: () => Promise<String[]>,
|
|
18
|
+
getLockedPackages: () => Promise<String[]>
|
|
19
|
+
};
|
|
20
|
+
type commandResult = {success: boolean, message?: string};
|
|
21
|
+
function checkNpmPackageValidity(name : string): Boolean {
|
|
22
|
+
return /^(?:(?:@(?:[a-z0-9-*~][a-z0-9-*. _~]*)?\/[a-z0-9-._~])|[a-z0-9-~])[a-z0-9-._~]*$/.test(name);
|
|
23
|
+
}
|
|
24
|
+
function checkNpmVersionValidity(name : string): Boolean {
|
|
25
|
+
return name === '' || /^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$/.test(name);
|
|
26
|
+
}
|
|
27
|
+
class AddonsManagerHandler extends Handler {
|
|
28
|
+
@requireSudo
|
|
29
|
+
async get()
|
|
30
|
+
{
|
|
31
|
+
this.response.template = 'manage_addons.html';
|
|
32
|
+
const packages = await addonsManagerModel.getActivedPackages();
|
|
33
|
+
const lockedPackages = await addonsManagerModel.getLockedPackages();
|
|
34
|
+
this.response.body = {
|
|
35
|
+
packages: packages,
|
|
36
|
+
lockedPackages: lockedPackages,
|
|
37
|
+
result: null
|
|
38
|
+
};
|
|
39
|
+
this.renderHTML(this.response.template, {title: 'manage_addons'});
|
|
40
|
+
}
|
|
41
|
+
async post()
|
|
42
|
+
{
|
|
43
|
+
const body = this.request.body;
|
|
44
|
+
let result: commandResult = {success: false, message: 'Unknown error'};
|
|
45
|
+
if(body.state === 'activate'){
|
|
46
|
+
const name = body.new_package_name;
|
|
47
|
+
const version = body.new_package_version;
|
|
48
|
+
result = await addonsManagerModel.add(name, version);
|
|
49
|
+
}else if(body['delete_package']){
|
|
50
|
+
const name = body['delete_package'];
|
|
51
|
+
result = await addonsManagerModel.remove(name);
|
|
52
|
+
}else if(body['update_package']){
|
|
53
|
+
const name = body['update_package'];
|
|
54
|
+
result = await addonsManagerModel.update(name);
|
|
55
|
+
} else{
|
|
56
|
+
throw new UserFacingError('Invalid request');
|
|
57
|
+
}
|
|
58
|
+
const packages = await addonsManagerModel.getActivedPackages();
|
|
59
|
+
const lockedPackages = await addonsManagerModel.getLockedPackages();
|
|
60
|
+
console.log(result);
|
|
61
|
+
this.back({
|
|
62
|
+
result: result,
|
|
63
|
+
packages: packages,
|
|
64
|
+
lockedPackages: lockedPackages
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function sendCommand(cmd: string, cwd: string) : Promise<commandResult> {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
try {
|
|
72
|
+
exec(cmd, { cwd: cwd }, (err, stdout, stderr) => {
|
|
73
|
+
if (err) {
|
|
74
|
+
console.error(`exec error: ${err}`);
|
|
75
|
+
resolve({ success: false, message: err.message });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (stderr) {
|
|
79
|
+
console.error(`stderr: ${stderr}`);
|
|
80
|
+
resolve({ success: false, message: stderr });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log(`stdout: ${stdout}`);
|
|
84
|
+
resolve({ success: true });
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
resolve({ success: false, message: (error as Error).message });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default class AddonsManagerService extends Service {
|
|
93
|
+
static Config = Schema.object({
|
|
94
|
+
pathToHydro: Schema.string().description('Path to Hydro').required(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
constructor(ctx: Context, config: ReturnType<typeof AddonsManagerService.Config>) {
|
|
98
|
+
super(ctx, 'hydrooj-addons-manager');
|
|
99
|
+
ctx.Route('manage_addons', '/manage/addons', AddonsManagerHandler, PRIV.PRIV_ALL);
|
|
100
|
+
global.Hydro.ui.inject('ControlPanel', 'manage_addons');
|
|
101
|
+
init();
|
|
102
|
+
async function init() {
|
|
103
|
+
|
|
104
|
+
async function remove(name: string): Promise<commandResult> {
|
|
105
|
+
if(!checkNpmPackageValidity(name))throw new UserFacingError('Invalid package name');
|
|
106
|
+
await sendCommand("yarn global remove "+name, config.pathToHydro);
|
|
107
|
+
const result = await sendCommand("hydrooj addon remove "+name, config.pathToHydro);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
async function update(name: string, version: string = ''): Promise<commandResult> {
|
|
111
|
+
if(!checkNpmPackageValidity(name))throw new UserFacingError('Invalid package name');
|
|
112
|
+
if(!checkNpmVersionValidity(version))throw new UserFacingError('Invalid version');
|
|
113
|
+
const result = await sendCommand("yarn global add "+name+(version ? '@' + version : ''), config.pathToHydro);
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
async function add(name: string, version: string = ''): Promise<commandResult> {
|
|
117
|
+
if(!checkNpmPackageValidity(name))throw new UserFacingError('Invalid package name');
|
|
118
|
+
if(!checkNpmVersionValidity(version))throw new UserFacingError('Invalid version');
|
|
119
|
+
const updateResult = await update(name, version);
|
|
120
|
+
const result = await sendCommand("hydrooj addon add "+name+(version ? '@' + version : ''), config.pathToHydro);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async function getActivedPackages(): Promise<String[]> {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
let packages: String[] = [];
|
|
128
|
+
fs.readFile(config.pathToHydro+'addon.json', (err, data) => {
|
|
129
|
+
if(err) reject(new UserFacingError(err.message));
|
|
130
|
+
else {
|
|
131
|
+
packages = JSON.parse(data.toString());
|
|
132
|
+
resolve(packages);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function getLockedPackages(): Promise<String[]> {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
let packages: String[] = [];
|
|
141
|
+
fs.readFile(config.pathToHydro+'addon-locked.json', (err, data) => {
|
|
142
|
+
if(err) reject(new UserFacingError(err.message));
|
|
143
|
+
else {
|
|
144
|
+
packages = JSON.parse(data.toString());
|
|
145
|
+
resolve(packages);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
addonsManagerModel = { add, update, remove, getActivedPackages, getLockedPackages };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
__langname: 正體中文
|
|
2
|
+
addons manager: 插件管理員
|
|
3
|
+
activated package name: 已啟用套件名稱
|
|
4
|
+
activate: 啟用
|
|
5
|
+
add new package: 新增套件
|
|
6
|
+
delete: 刪除
|
|
7
|
+
package name: 套件名稱
|
|
8
|
+
update: 更新
|
|
9
|
+
version name *unnecessary: 版本名稱 *非必要
|
|
10
|
+
need to restart hydrooj to make changes work: 需要重新啟動 HydroOJ 以使更改生效
|
|
11
|
+
manage_addons: 管理插件
|
package/package.json
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{% extends "manage_base.html" %}
|
|
2
|
+
{% block manage_content %}
|
|
3
|
+
<div class="main">
|
|
4
|
+
<style>
|
|
5
|
+
.manage-addons .section__header { display: flex; flex-direction: column; gap: 12px; }
|
|
6
|
+
.manage-addons .section__title { margin: 0; font-size: 22px; }
|
|
7
|
+
.manage-addons .section__subtitle { margin: 8px 0 4px; font-size: 16px; color: #374151; }
|
|
8
|
+
|
|
9
|
+
.manage-addons #activate-package-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 10px; }
|
|
10
|
+
.manage-addons .activate-package-form { margin: 0; }
|
|
11
|
+
.manage-addons .activate-package-item {
|
|
12
|
+
display: flex; align-items: center; gap: 12px; justify-content: space-between;
|
|
13
|
+
padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff;
|
|
14
|
+
}
|
|
15
|
+
.manage-addons .activate-package-item a { font-weight: 600; color: #111827; }
|
|
16
|
+
.manage-addons .activate-package-item:hover { box-shadow: 0 1px 3px rgba(0,0,0,.06); }
|
|
17
|
+
|
|
18
|
+
.manage-addons .activate-package-item .btn { min-width: 96px; }
|
|
19
|
+
.manage-addons .activate-package-item .btn + .btn { margin-left: 8px; }
|
|
20
|
+
|
|
21
|
+
.manage-addons .activate-package-item input[type="text"] {
|
|
22
|
+
flex: 1; min-width: 160px; padding: 8px 10px; border: 1px solid #d1d5db; border-radius: 6px;
|
|
23
|
+
}
|
|
24
|
+
.manage-addons .alert { margin: 6px 0 2px; }
|
|
25
|
+
|
|
26
|
+
/* Styled buttons */
|
|
27
|
+
.manage-addons .btn {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
gap: 6px;
|
|
32
|
+
padding: 8px 12px;
|
|
33
|
+
border-radius: 8px;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
font-size: 14px;
|
|
36
|
+
line-height: 1.2;
|
|
37
|
+
border: 1px solid transparent;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
transition: background-color .15s ease, color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .05s ease;
|
|
40
|
+
user-select: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.manage-addons .btn:disabled {
|
|
44
|
+
opacity: .6;
|
|
45
|
+
cursor: not-allowed;
|
|
46
|
+
transform: none !important;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Primary */
|
|
50
|
+
.manage-addons .btn--primary {
|
|
51
|
+
background: #2563eb;
|
|
52
|
+
color: #fff;
|
|
53
|
+
border-color: #1d4ed8;
|
|
54
|
+
}
|
|
55
|
+
.manage-addons .btn--primary:hover { background: #1d4ed8; }
|
|
56
|
+
.manage-addons .btn--primary:active { transform: translateY(1px); }
|
|
57
|
+
.manage-addons .btn--primary:focus-visible {
|
|
58
|
+
outline: none;
|
|
59
|
+
box-shadow: 0 0 0 3px rgba(37,99,235,.25);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Secondary */
|
|
63
|
+
.manage-addons .btn--secondary {
|
|
64
|
+
background: #ffffff;
|
|
65
|
+
color: #111827;
|
|
66
|
+
border-color: #d1d5db;
|
|
67
|
+
}
|
|
68
|
+
.manage-addons .btn--secondary:hover { background: #f3f4f6; }
|
|
69
|
+
.manage-addons .btn--secondary:active { transform: translateY(1px); }
|
|
70
|
+
.manage-addons .btn--secondary:focus-visible {
|
|
71
|
+
outline: none;
|
|
72
|
+
box-shadow: 0 0 0 3px rgba(17,24,39,.15);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Danger */
|
|
76
|
+
.manage-addons .btn--danger {
|
|
77
|
+
background: #dc2626;
|
|
78
|
+
color: #fff;
|
|
79
|
+
border-color: #b91c1c;
|
|
80
|
+
}
|
|
81
|
+
.manage-addons .btn--danger:hover { background: #b91c1c; }
|
|
82
|
+
.manage-addons .btn--danger:active { transform: translateY(1px); }
|
|
83
|
+
.manage-addons .btn--danger:focus-visible {
|
|
84
|
+
outline: none;
|
|
85
|
+
box-shadow: 0 0 0 3px rgba(220,38,38,.25);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Dark mode button tweaks */
|
|
89
|
+
@media (prefers-color-scheme: dark) {
|
|
90
|
+
.manage-addons .btn--secondary {
|
|
91
|
+
background: transparent;
|
|
92
|
+
color: #e5e7eb;
|
|
93
|
+
border-color: #374151;
|
|
94
|
+
}
|
|
95
|
+
.manage-addons .btn--secondary:hover { background: #111827; }
|
|
96
|
+
.manage-addons .btn--primary { border-color: #1e40af; }
|
|
97
|
+
.manage-addons .btn--danger { border-color: #991b1b; }
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
100
|
+
|
|
101
|
+
<div class="section manage-addons">
|
|
102
|
+
<div class="section__header">
|
|
103
|
+
<h1 class="section__title" data-heading>{{ _('addons manager') }}</h1>
|
|
104
|
+
<h2 class="section__subtitle" data-heading>{{ _('activated package name') }}</h2>
|
|
105
|
+
<h2 class="section__subtitle" data-heading style="color: #ef4444; font-weight: 600;">{{ _('need to restart hydrooj to make changes work') }}</h2>
|
|
106
|
+
<ul id="activate-package-list">
|
|
107
|
+
{% for package in packages %}
|
|
108
|
+
<li>
|
|
109
|
+
<form method="post" class="activate-package-form">
|
|
110
|
+
<div class="activate-package-item">
|
|
111
|
+
<a>{{ package }}</a>
|
|
112
|
+
{% if package not in lockedPackages %}
|
|
113
|
+
<button type="submit" name="delete_package" value="{{ package }}" class="btn btn--danger">{{ _('delete') }}</button>
|
|
114
|
+
{% endif %}
|
|
115
|
+
<button type="submit" name="update_package" value="{{ package }}" class="btn btn--secondary">{{ _('update') }}</button>
|
|
116
|
+
</div>
|
|
117
|
+
</form>
|
|
118
|
+
</li>
|
|
119
|
+
{% endfor %}
|
|
120
|
+
</ul>
|
|
121
|
+
<h2 class="section__subtitle" data-heading>{{ _('add new package') }}</h2>
|
|
122
|
+
<form method="post" class="activate-package-form">
|
|
123
|
+
<div class="activate-package-item">
|
|
124
|
+
<input type="text" name="new_package_name" placeholder="{{ _('package name') }}" required>
|
|
125
|
+
<input type="text" name="new_package_version" placeholder="{{ _('version name *unnecessary') }}">
|
|
126
|
+
<button type="submit" name="state" value="activate" class="btn btn--primary">{{ _('activate') }}</button>
|
|
127
|
+
</div>
|
|
128
|
+
</form>
|
|
129
|
+
{% if result %}
|
|
130
|
+
{% if result.success %}
|
|
131
|
+
<div class="alert alert--success">
|
|
132
|
+
{{ result.success }}
|
|
133
|
+
</div>
|
|
134
|
+
{% else %}
|
|
135
|
+
<div class="alert alert--danger">
|
|
136
|
+
{{ result.error }}
|
|
137
|
+
</div>
|
|
138
|
+
{% endif %}
|
|
139
|
+
{% endif %}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
{% endblock %}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{% extends "layout/basic.html" %}
|
|
2
|
+
{% block content %}
|
|
3
|
+
<div class="row" data-sticky-parent>
|
|
4
|
+
<div class="medium-9 columns">
|
|
5
|
+
{% block manage_content %}{% endblock %}
|
|
6
|
+
</div>
|
|
7
|
+
<div class="medium-3 columns"><div data-sticky="large">
|
|
8
|
+
<div class="section side">
|
|
9
|
+
<ol class="menu">
|
|
10
|
+
<li class="menu__item">
|
|
11
|
+
<div class="menu__link expandable">
|
|
12
|
+
<span class="icon icon-info"></span> {{ _('Properties') }}
|
|
13
|
+
</div>
|
|
14
|
+
<ol class="menu collapsed">
|
|
15
|
+
{%- for item in ui.getNodes('ControlPanel') -%}
|
|
16
|
+
{{ sidemenu.render_item(item.icon, item.name) }}
|
|
17
|
+
{%- endfor -%}
|
|
18
|
+
</ol>
|
|
19
|
+
</li>
|
|
20
|
+
</ol>
|
|
21
|
+
</div>
|
|
22
|
+
</div></div>
|
|
23
|
+
</div>
|
|
24
|
+
{% endblock %}
|