mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19:46 +00:00
Move Pleroma permission_groups controller to its own routes file, add tests
This commit is contained in:
parent
d29bc8c020
commit
b7bf2fc76f
8 changed files with 109 additions and 48 deletions
|
|
@ -79,7 +79,6 @@ import {
|
||||||
configController,
|
configController,
|
||||||
frontendConfigController,
|
frontendConfigController,
|
||||||
pleromaAdminDeleteStatusController,
|
pleromaAdminDeleteStatusController,
|
||||||
pleromaAdminPromoteController,
|
|
||||||
pleromaAdminSuggestController,
|
pleromaAdminSuggestController,
|
||||||
pleromaAdminTagController,
|
pleromaAdminTagController,
|
||||||
pleromaAdminUnsuggestController,
|
pleromaAdminUnsuggestController,
|
||||||
|
|
@ -149,6 +148,7 @@ import { uploaderMiddleware } from '@/middleware/uploaderMiddleware.ts';
|
||||||
import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts';
|
import { translatorMiddleware } from '@/middleware/translatorMiddleware.ts';
|
||||||
import { logiMiddleware } from '@/middleware/logiMiddleware.ts';
|
import { logiMiddleware } from '@/middleware/logiMiddleware.ts';
|
||||||
import dittoNamesRoute from '@/routes/dittoNamesRoute.ts';
|
import dittoNamesRoute from '@/routes/dittoNamesRoute.ts';
|
||||||
|
import pleromaAdminPermissionGroupsRoute from '@/routes/pleromaAdminPermissionGroupsRoute.ts';
|
||||||
import { DittoRelayStore } from '@/storages/DittoRelayStore.ts';
|
import { DittoRelayStore } from '@/storages/DittoRelayStore.ts';
|
||||||
|
|
||||||
export interface AppEnv extends DittoEnv {
|
export interface AppEnv extends DittoEnv {
|
||||||
|
|
@ -440,11 +440,7 @@ app.delete('/api/v1/pleroma/statuses/:id{[0-9a-f]{64}}/reactions/:emoji', userMi
|
||||||
app.get('/api/v1/pleroma/admin/config', userMiddleware({ role: 'admin' }), configController);
|
app.get('/api/v1/pleroma/admin/config', userMiddleware({ role: 'admin' }), configController);
|
||||||
app.post('/api/v1/pleroma/admin/config', userMiddleware({ role: 'admin' }), updateConfigController);
|
app.post('/api/v1/pleroma/admin/config', userMiddleware({ role: 'admin' }), updateConfigController);
|
||||||
app.delete('/api/v1/pleroma/admin/statuses/:id', userMiddleware({ role: 'admin' }), pleromaAdminDeleteStatusController);
|
app.delete('/api/v1/pleroma/admin/statuses/:id', userMiddleware({ role: 'admin' }), pleromaAdminDeleteStatusController);
|
||||||
app.post(
|
app.route('/api/v1/pleroma/admin/users/permission_group', pleromaAdminPermissionGroupsRoute);
|
||||||
'/api/v1/pleroma/admin/users/permission_group/:group',
|
|
||||||
userMiddleware({ role: 'admin' }),
|
|
||||||
pleromaAdminPromoteController,
|
|
||||||
);
|
|
||||||
|
|
||||||
app.get('/api/v1/admin/ditto/relays', userMiddleware({ role: 'admin' }), adminRelaysController);
|
app.get('/api/v1/admin/ditto/relays', userMiddleware({ role: 'admin' }), adminRelaysController);
|
||||||
app.put('/api/v1/admin/ditto/relays', userMiddleware({ role: 'admin' }), adminSetRelaysController);
|
app.put('/api/v1/admin/ditto/relays', userMiddleware({ role: 'admin' }), adminSetRelaysController);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { createAdminEvent, parseBody, updateAdminEvent, updateUser } from '@/utils/api.ts';
|
import { createAdminEvent, updateAdminEvent, updateUser } from '@/utils/api.ts';
|
||||||
import { lookupPubkey } from '@/utils/lookup.ts';
|
import { lookupPubkey } from '@/utils/lookup.ts';
|
||||||
import { getPleromaConfigs } from '@/utils/pleroma.ts';
|
import { getPleromaConfigs } from '@/utils/pleroma.ts';
|
||||||
import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts';
|
import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts';
|
||||||
|
|
@ -62,36 +62,6 @@ const pleromaAdminTagSchema = z.object({
|
||||||
tags: z.string().array(),
|
tags: z.string().array(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const pleromaPromoteAdminSchema = z.object({
|
|
||||||
nicknames: z.string().array(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const pleromaAdminPromoteController: AppController = async (c) => {
|
|
||||||
const body = await parseBody(c.req.raw);
|
|
||||||
const result = pleromaPromoteAdminSchema.safeParse(body);
|
|
||||||
const group = c.req.param('group');
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return c.json({ error: 'Bad request', schema: result.error }, 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!['admin', 'moderator'].includes(group)) {
|
|
||||||
return c.json({ error: 'Bad request', schema: 'Invalid group' }, 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = result;
|
|
||||||
const { nicknames } = data;
|
|
||||||
|
|
||||||
for (const nickname of nicknames) {
|
|
||||||
const pubkey = await lookupPubkey(nickname, c.var);
|
|
||||||
if (pubkey) {
|
|
||||||
await updateUser(pubkey, { [group]: true }, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ is_admin: true }, 200);
|
|
||||||
};
|
|
||||||
|
|
||||||
const pleromaAdminTagController: AppController = async (c) => {
|
const pleromaAdminTagController: AppController = async (c) => {
|
||||||
const { conf } = c.var;
|
const { conf } = c.var;
|
||||||
const params = pleromaAdminTagSchema.parse(await c.req.json());
|
const params = pleromaAdminTagSchema.parse(await c.req.json());
|
||||||
|
|
@ -180,7 +150,6 @@ export {
|
||||||
configController,
|
configController,
|
||||||
frontendConfigController,
|
frontendConfigController,
|
||||||
pleromaAdminDeleteStatusController,
|
pleromaAdminDeleteStatusController,
|
||||||
pleromaAdminPromoteController,
|
|
||||||
pleromaAdminSuggestController,
|
pleromaAdminSuggestController,
|
||||||
pleromaAdminTagController,
|
pleromaAdminTagController,
|
||||||
pleromaAdminUnsuggestController,
|
pleromaAdminUnsuggestController,
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,10 @@ import { assertEquals } from '@std/assert';
|
||||||
import route from './dittoNamesRoute.ts';
|
import route from './dittoNamesRoute.ts';
|
||||||
|
|
||||||
Deno.test('POST / creates a name request event', async () => {
|
Deno.test('POST / creates a name request event', async () => {
|
||||||
await using app = new TestApp();
|
await using app = new TestApp(route);
|
||||||
const { conf, relay } = app.var;
|
const { conf, relay } = app.var;
|
||||||
|
|
||||||
const user = app.user();
|
const user = app.user();
|
||||||
app.route('/', route);
|
|
||||||
|
|
||||||
const response = await app.api.post('/', { name: 'Alex@Ditto.pub', reason: 'for testing' });
|
const response = await app.api.post('/', { name: 'Alex@Ditto.pub', reason: 'for testing' });
|
||||||
|
|
||||||
|
|
@ -28,10 +27,9 @@ Deno.test('POST / creates a name request event', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('POST / can be called multiple times with the same name', async () => {
|
Deno.test('POST / can be called multiple times with the same name', async () => {
|
||||||
await using app = new TestApp();
|
await using app = new TestApp(route);
|
||||||
|
|
||||||
app.user();
|
app.user();
|
||||||
app.route('/', route);
|
|
||||||
|
|
||||||
const response1 = await app.api.post('/', { name: 'alex@ditto.pub' });
|
const response1 = await app.api.post('/', { name: 'alex@ditto.pub' });
|
||||||
const response2 = await app.api.post('/', { name: 'alex@ditto.pub' });
|
const response2 = await app.api.post('/', { name: 'alex@ditto.pub' });
|
||||||
|
|
@ -41,11 +39,10 @@ Deno.test('POST / can be called multiple times with the same name', async () =>
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('POST / returns 400 if the name has already been granted', async () => {
|
Deno.test('POST / returns 400 if the name has already been granted', async () => {
|
||||||
await using app = new TestApp();
|
await using app = new TestApp(route);
|
||||||
const { conf, relay } = app.var;
|
const { conf, relay } = app.var;
|
||||||
|
|
||||||
app.user();
|
app.user();
|
||||||
app.route('/', route);
|
|
||||||
|
|
||||||
const grant = await conf.signer.signEvent({
|
const grant = await conf.signer.signEvent({
|
||||||
kind: 30360,
|
kind: 30360,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { TestApp } from '@ditto/mastoapi/test';
|
||||||
|
import { assertEquals } from '@std/assert';
|
||||||
|
import { nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
|
import route from './pleromaAdminPermissionGroupsRoute.ts';
|
||||||
|
|
||||||
|
Deno.test('POST /admin returns 403 if user is not an admin', async () => {
|
||||||
|
await using app = new TestApp(route);
|
||||||
|
|
||||||
|
app.user();
|
||||||
|
|
||||||
|
const response = await app.api.post('/admin', { nicknames: ['alex@ditto.pub'] });
|
||||||
|
|
||||||
|
assertEquals(response.status, 403);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('POST /admin promotes to admin', async () => {
|
||||||
|
await using app = new TestApp(route);
|
||||||
|
const { conf, relay } = app.var;
|
||||||
|
|
||||||
|
await app.admin();
|
||||||
|
|
||||||
|
const pawn = app.createUser();
|
||||||
|
const pubkey = await pawn.signer.getPublicKey();
|
||||||
|
|
||||||
|
const response = await app.api.post('/admin', { nicknames: [nip19.npubEncode(pubkey)] });
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
assertEquals(response.status, 200);
|
||||||
|
assertEquals(json, { is_admin: true });
|
||||||
|
|
||||||
|
const [event] = await relay.query([{ kinds: [30382], authors: [await conf.signer.getPublicKey()], '#d': [pubkey] }]);
|
||||||
|
|
||||||
|
assertEquals(event.tags, [['d', pubkey], ['n', 'admin']]);
|
||||||
|
});
|
||||||
40
packages/ditto/routes/pleromaAdminPermissionGroupsRoute.ts
Normal file
40
packages/ditto/routes/pleromaAdminPermissionGroupsRoute.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { userMiddleware } from '@ditto/mastoapi/middleware';
|
||||||
|
import { DittoRoute } from '@ditto/mastoapi/router';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { parseBody, updateUser } from '@/utils/api.ts';
|
||||||
|
import { lookupPubkey } from '@/utils/lookup.ts';
|
||||||
|
|
||||||
|
const route = new DittoRoute();
|
||||||
|
|
||||||
|
const pleromaPromoteAdminSchema = z.object({
|
||||||
|
nicknames: z.string().array(),
|
||||||
|
});
|
||||||
|
|
||||||
|
route.post('/:group', userMiddleware({ role: 'admin' }), async (c) => {
|
||||||
|
const body = await parseBody(c.req.raw);
|
||||||
|
const result = pleromaPromoteAdminSchema.safeParse(body);
|
||||||
|
const group = c.req.param('group');
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return c.json({ error: 'Bad request', schema: result.error }, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['admin', 'moderator'].includes(group)) {
|
||||||
|
return c.json({ error: 'Bad request', schema: 'Invalid group' }, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = result;
|
||||||
|
const { nicknames } = data;
|
||||||
|
|
||||||
|
for (const nickname of nicknames) {
|
||||||
|
const pubkey = await lookupPubkey(nickname, c.var);
|
||||||
|
if (pubkey) {
|
||||||
|
await updateUser(pubkey, { [group]: true }, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ is_admin: true }, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default route;
|
||||||
|
|
@ -118,7 +118,7 @@ async function updateAdminEvent<E extends EventStub>(
|
||||||
return createAdminEvent(fn(prev), c);
|
return createAdminEvent(fn(prev), c);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUser(pubkey: string, n: Record<string, boolean>, c: AppContext): Promise<NostrEvent> {
|
function updateUser(pubkey: string, n: Record<string, boolean>, c: Context): Promise<NostrEvent> {
|
||||||
return updateNames(30382, pubkey, n, c);
|
return updateNames(30382, pubkey, n, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export class DittoRoute extends Hono<DittoEnv> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private throwMissingVar(name: string): never {
|
private throwMissingVar(name: string): never {
|
||||||
throw new HTTPException(500, { message: `Missing required variable: ${name}` });
|
throw new Error(`Missing required variable: ${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _errorHandler: ErrorHandler = (error, c) => {
|
private _errorHandler: ErrorHandler = (error, c) => {
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,14 @@ import { DittoConf } from '@ditto/conf';
|
||||||
import { type DittoDB, DummyDB } from '@ditto/db';
|
import { type DittoDB, DummyDB } from '@ditto/db';
|
||||||
import { HTTPException } from '@hono/hono/http-exception';
|
import { HTTPException } from '@hono/hono/http-exception';
|
||||||
import { type NRelay, NSecSigner } from '@nostrify/nostrify';
|
import { type NRelay, NSecSigner } from '@nostrify/nostrify';
|
||||||
|
import { MockRelay } from '@nostrify/nostrify/test';
|
||||||
import { generateSecretKey, nip19 } from 'nostr-tools';
|
import { generateSecretKey, nip19 } from 'nostr-tools';
|
||||||
|
|
||||||
import { DittoApp, type DittoAppOpts } from '../router/DittoApp.ts';
|
import { DittoApp, type DittoAppOpts } from '../router/DittoApp.ts';
|
||||||
|
|
||||||
import type { Context } from '@hono/hono';
|
import type { Context } from '@hono/hono';
|
||||||
import type { User } from '../middleware/User.ts';
|
import type { User } from '../middleware/User.ts';
|
||||||
import { MockRelay } from '@nostrify/nostrify/test';
|
import type { DittoRoute } from '../router/DittoRoute.ts';
|
||||||
|
|
||||||
interface DittoVars {
|
interface DittoVars {
|
||||||
db: DittoDB;
|
db: DittoDB;
|
||||||
|
|
@ -19,7 +20,7 @@ interface DittoVars {
|
||||||
export class TestApp extends DittoApp implements AsyncDisposable {
|
export class TestApp extends DittoApp implements AsyncDisposable {
|
||||||
private _user?: User;
|
private _user?: User;
|
||||||
|
|
||||||
constructor(opts?: Partial<DittoAppOpts>) {
|
constructor(route?: DittoRoute, opts?: Partial<DittoAppOpts>) {
|
||||||
const nsec = nip19.nsecEncode(generateSecretKey());
|
const nsec = nip19.nsecEncode(generateSecretKey());
|
||||||
|
|
||||||
const conf = opts?.conf ?? new DittoConf(
|
const conf = opts?.conf ?? new DittoConf(
|
||||||
|
|
@ -44,6 +45,10 @@ export class TestApp extends DittoApp implements AsyncDisposable {
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (route) {
|
||||||
|
this.route('/', route);
|
||||||
|
}
|
||||||
|
|
||||||
this.onError((err, c) => {
|
this.onError((err, c) => {
|
||||||
if (err instanceof HTTPException) {
|
if (err instanceof HTTPException) {
|
||||||
if (err.res) {
|
if (err.res) {
|
||||||
|
|
@ -65,6 +70,25 @@ export class TestApp extends DittoApp implements AsyncDisposable {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async admin(user?: User): Promise<User> {
|
||||||
|
const { conf, relay } = this.opts;
|
||||||
|
user ??= this.createUser();
|
||||||
|
|
||||||
|
const event = await conf.signer.signEvent({
|
||||||
|
kind: 30382,
|
||||||
|
content: '',
|
||||||
|
tags: [
|
||||||
|
['d', await user.signer.getPublicKey()],
|
||||||
|
['n', 'admin'],
|
||||||
|
],
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
await relay.event(event);
|
||||||
|
|
||||||
|
return this.user(user);
|
||||||
|
}
|
||||||
|
|
||||||
user(user?: User): User {
|
user(user?: User): User {
|
||||||
user ??= this.createUser();
|
user ??= this.createUser();
|
||||||
this._user = user;
|
this._user = user;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue