Add expiring panel auth sessions

This commit is contained in:
2026-04-02 00:45:27 +03:00
parent e342693211
commit 69c97ea387
11 changed files with 514 additions and 93 deletions

View File

@@ -5,6 +5,7 @@ import request from 'supertest';
import { afterEach, describe, expect, it } from 'vitest';
import type { UpdateSystemInput } from '../src/shared/contracts';
import { createApp } from './app';
import { AuthService } from './lib/auth';
import type { RuntimeSnapshot } from './lib/config';
import type { RuntimeController } from './lib/runtime';
import { StateStore } from './lib/store';
@@ -47,9 +48,31 @@ afterEach(async () => {
});
describe('panel api', () => {
it('rejects protected api access without a bearer token', async () => {
const app = await createTestApp();
const response = await request(app).get('/api/state');
expect(response.status).toBe(401);
expect(response.body.error).toMatch(/authorization required/i);
});
it('logs in and returns an expiring panel token', async () => {
const app = await createTestApp();
const response = await request(app).post('/api/auth/login').send({
login: 'admin',
password: 'proxy-ui-demo',
});
expect(response.status).toBe(200);
expect(response.body.token).toMatch(/\./);
expect(response.body.ttlMs).toBe(24 * 60 * 60 * 1000);
expect(typeof response.body.expiresAt).toBe('string');
});
it('rejects user creation against a non-assignable service', async () => {
const app = await createTestApp();
const response = await request(app).post('/api/users').send({
const token = await authorize(app);
const response = await request(app).post('/api/users').set('Authorization', `Bearer ${token}`).send({
username: 'bad-admin-user',
password: 'secret123',
serviceId: 'admin',
@@ -62,15 +85,16 @@ describe('panel api', () => {
it('pauses and deletes a user through the api', async () => {
const app = await createTestApp();
const initial = await request(app).get('/api/state');
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 username = initial.body.userRecords[0].username;
const paused = await request(app).post(`/api/users/${userId}/pause`);
const paused = await request(app).post(`/api/users/${userId}/pause`).set('Authorization', `Bearer ${token}`);
expect(paused.status).toBe(200);
expect(paused.body.userRecords.find((entry: { id: string }) => entry.id === userId).paused).toBe(true);
const removed = await request(app).delete(`/api/users/${userId}`);
const removed = await request(app).delete(`/api/users/${userId}`).set('Authorization', `Bearer ${token}`);
expect(removed.status).toBe(200);
expect(removed.body.userRecords.some((entry: { username: string }) => entry.username === username)).toBe(
false,
@@ -79,12 +103,13 @@ describe('panel api', () => {
it('rejects system updates when two services reuse the same port', async () => {
const app = await createTestApp();
const initial = await request(app).get('/api/state');
const token = await authorize(app);
const initial = await request(app).get('/api/state').set('Authorization', `Bearer ${token}`);
const system = createSystemPayload(initial.body);
system.services[1].port = system.services[0].port;
const response = await request(app).put('/api/system').send(system);
const response = await request(app).put('/api/system').set('Authorization', `Bearer ${token}`).send(system);
expect(response.status).toBe(400);
expect(response.body.error).toMatch(/cannot share port/i);
@@ -92,14 +117,15 @@ describe('panel api', () => {
it('rejects system updates that strand existing users on a disabled service', async () => {
const app = await createTestApp();
const initial = await request(app).get('/api/state');
const token = await authorize(app);
const initial = await request(app).get('/api/state').set('Authorization', `Bearer ${token}`);
const system = createSystemPayload(initial.body);
system.services = system.services.map((service) =>
service.id === 'socks-main' ? { ...service, enabled: false } : service,
);
const response = await request(app).put('/api/system').send(system);
const response = await request(app).put('/api/system').set('Authorization', `Bearer ${token}`).send(system);
expect(response.status).toBe(400);
expect(response.body.error).toMatch(/enabled assignable service/i);
@@ -108,7 +134,8 @@ describe('panel api', () => {
it('updates system settings and regenerates the rendered config', async () => {
const app = await createTestApp();
const initial = await request(app).get('/api/state');
const token = await authorize(app);
const initial = await request(app).get('/api/state').set('Authorization', `Bearer ${token}`);
const system = createSystemPayload(initial.body);
system.publicHost = 'ops-gateway.example.net';
@@ -116,7 +143,7 @@ describe('panel api', () => {
service.id === 'socks-main' ? { ...service, port: 1180 } : service,
);
const response = await request(app).put('/api/system').send(system);
const response = await request(app).put('/api/system').set('Authorization', `Bearer ${token}`).send(system);
expect(response.status).toBe(200);
expect(response.body.system.publicHost).toBe('ops-gateway.example.net');
@@ -134,11 +161,17 @@ async function createTestApp() {
const runtime = new FakeRuntime();
const store = new StateStore(path.join(dir, 'state', 'panel-state.json'));
const auth = new AuthService({
login: 'admin',
password: 'proxy-ui-demo',
ttlMs: 24 * 60 * 60 * 1000,
});
return createApp({
store,
runtime,
runtimeRootDir: dir,
auth,
});
}
@@ -146,3 +179,12 @@ function createSystemPayload(body: { system: Record<string, unknown> }): UpdateS
const { previewConfig: _previewConfig, ...system } = body.system;
return structuredClone(system) as unknown as UpdateSystemInput;
}
async function authorize(app: Awaited<ReturnType<typeof createTestApp>>) {
const response = await request(app).post('/api/auth/login').send({
login: 'admin',
password: 'proxy-ui-demo',
});
return response.body.token as string;
}