Polish user actions and runtime controls
This commit is contained in:
@@ -36,6 +36,16 @@ class FakeRuntime implements RuntimeController {
|
||||
return this.start();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.status = {
|
||||
status: 'idle',
|
||||
pid: null,
|
||||
startedAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
return this.getSnapshot();
|
||||
}
|
||||
|
||||
async reload() {
|
||||
return this.getSnapshot();
|
||||
}
|
||||
@@ -101,6 +111,41 @@ describe('panel api', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('updates a user through the api', async () => {
|
||||
const app = await createTestApp();
|
||||
const token = await authorize(app);
|
||||
const initial = await request(app).get('/api/state').set('Authorization', `Bearer ${token}`);
|
||||
const userId = initial.body.userRecords[0].id;
|
||||
|
||||
const updated = await request(app).put(`/api/users/${userId}`).set('Authorization', `Bearer ${token}`).send({
|
||||
username: 'night-shift-updated',
|
||||
password: 'fresh-secret',
|
||||
serviceId: 'socks-lab',
|
||||
quotaMb: 512,
|
||||
});
|
||||
|
||||
expect(updated.status).toBe(200);
|
||||
expect(updated.body.userRecords.find((entry: { id: string }) => entry.id === userId)).toMatchObject({
|
||||
username: 'night-shift-updated',
|
||||
password: 'fresh-secret',
|
||||
serviceId: 'socks-lab',
|
||||
quotaBytes: 512 * 1024 * 1024,
|
||||
});
|
||||
});
|
||||
|
||||
it('stops the runtime through the api', async () => {
|
||||
const app = await createTestApp();
|
||||
const token = await authorize(app);
|
||||
|
||||
await request(app).post('/api/runtime/start').set('Authorization', `Bearer ${token}`);
|
||||
const stopped = await request(app).post('/api/runtime/stop').set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(stopped.status).toBe(200);
|
||||
expect(stopped.body.service.status).toBe('idle');
|
||||
expect(stopped.body.service.pidLabel).toBe('pid -');
|
||||
expect(stopped.body.service.lastEvent).toMatch(/stop requested/i);
|
||||
});
|
||||
|
||||
it('rejects system updates when two services reuse the same port', async () => {
|
||||
const app = await createTestApp();
|
||||
const token = await authorize(app);
|
||||
|
||||
@@ -94,18 +94,26 @@ export function createApp({ store, runtime, runtimeRootDir, auth, liveSync }: Ap
|
||||
try {
|
||||
const state = await store.read();
|
||||
const action = request.params.action;
|
||||
const controller = action === 'restart' ? runtime.restart.bind(runtime) : runtime.start.bind(runtime);
|
||||
const controller =
|
||||
action === 'restart'
|
||||
? runtime.restart.bind(runtime)
|
||||
: action === 'stop'
|
||||
? runtime.stop.bind(runtime)
|
||||
: runtime.start.bind(runtime);
|
||||
|
||||
if (!['start', 'restart'].includes(action)) {
|
||||
if (!['start', 'restart', 'stop'].includes(action)) {
|
||||
response.status(404).json({ error: 'Unknown runtime action.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeSnapshot = await controller();
|
||||
state.service.lastEvent = action === 'restart' ? 'Runtime restarted from panel' : 'Runtime start requested from panel';
|
||||
if (runtimeSnapshot.startedAt) {
|
||||
state.service.startedAt = runtimeSnapshot.startedAt;
|
||||
}
|
||||
state.service.lastEvent =
|
||||
action === 'restart'
|
||||
? 'Runtime restarted from panel'
|
||||
: action === 'stop'
|
||||
? 'Runtime stop requested from panel'
|
||||
: 'Runtime start requested from panel';
|
||||
state.service.startedAt = runtimeSnapshot.startedAt ?? null;
|
||||
|
||||
await writeConfigAndState(store, state, runtimePaths);
|
||||
liveSync?.notifyPotentialChange();
|
||||
@@ -130,6 +138,40 @@ export function createApp({ store, runtime, runtimeRootDir, auth, liveSync }: Ap
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/users/:id', async (request, response, next) => {
|
||||
try {
|
||||
const state = await store.read();
|
||||
const user = state.userRecords.find((entry) => entry.id === request.params.id);
|
||||
|
||||
if (!user) {
|
||||
response.status(404).json({ error: 'User not found.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const input = validateCreateUserInput(request.body as Partial<CreateUserInput>, state.system.services);
|
||||
const duplicateUser = state.userRecords.find(
|
||||
(entry) => entry.id !== user.id && entry.username.toLowerCase() === input.username.toLowerCase(),
|
||||
);
|
||||
|
||||
if (duplicateUser) {
|
||||
response.status(400).json({ error: 'Username already exists.' });
|
||||
return;
|
||||
}
|
||||
|
||||
user.username = input.username;
|
||||
user.password = input.password;
|
||||
user.serviceId = input.serviceId;
|
||||
user.quotaBytes = input.quotaMb === null ? null : input.quotaMb * 1024 * 1024;
|
||||
state.service.lastEvent = `User ${user.username} updated from panel`;
|
||||
|
||||
await persistRuntimeMutation(store, runtime, state, runtimePaths);
|
||||
liveSync?.notifyPotentialChange();
|
||||
response.json(await getDashboardSnapshot(store, runtime, runtimePaths));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/system', async (request, response, next) => {
|
||||
try {
|
||||
const state = await store.read();
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { RuntimePaths, RuntimeSnapshot } from './config';
|
||||
export interface RuntimeController {
|
||||
getSnapshot(): RuntimeSnapshot;
|
||||
start(): Promise<RuntimeSnapshot>;
|
||||
stop(): Promise<RuntimeSnapshot>;
|
||||
restart(): Promise<RuntimeSnapshot>;
|
||||
reload(): Promise<RuntimeSnapshot>;
|
||||
}
|
||||
@@ -83,19 +84,17 @@ export class ThreeProxyManager implements RuntimeController {
|
||||
return this.start();
|
||||
}
|
||||
|
||||
async reload(): Promise<RuntimeSnapshot> {
|
||||
if (!this.child || this.child.exitCode !== null || !this.child.pid) {
|
||||
return this.start();
|
||||
}
|
||||
|
||||
process.kill(this.child.pid, 'SIGUSR1');
|
||||
return this.getSnapshot();
|
||||
}
|
||||
|
||||
private async stop(): Promise<void> {
|
||||
async stop(): Promise<RuntimeSnapshot> {
|
||||
if (!this.child || this.child.exitCode !== null || !this.child.pid) {
|
||||
this.child = null;
|
||||
return;
|
||||
this.snapshot = {
|
||||
...this.snapshot,
|
||||
status: 'idle',
|
||||
pid: null,
|
||||
startedAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
return this.getSnapshot();
|
||||
}
|
||||
|
||||
const current = this.child;
|
||||
@@ -105,6 +104,16 @@ export class ThreeProxyManager implements RuntimeController {
|
||||
|
||||
current.kill('SIGTERM');
|
||||
await waitForExit;
|
||||
return this.getSnapshot();
|
||||
}
|
||||
|
||||
async reload(): Promise<RuntimeSnapshot> {
|
||||
if (!this.child || this.child.exitCode !== null || !this.child.pid) {
|
||||
return this.start();
|
||||
}
|
||||
|
||||
process.kill(this.child.pid, 'SIGUSR1');
|
||||
return this.getSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user