mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
chore!: remove deprecated POST events search endpoint (#10030)
https://linear.app/unleash/issue/2-3368/remove-post-apiadmineventssearch-deprecated-in-610 Removes POST `/api/admin/events/search` which was deprecated in v6.1. Also cleans up related code.
This commit is contained in:
parent
45434109a9
commit
290ef6ca40
@ -2,17 +2,21 @@ import dbInit, {
|
|||||||
type ITestDb,
|
type ITestDb,
|
||||||
} from '../../../test/e2e/helpers/database-init.js';
|
} from '../../../test/e2e/helpers/database-init.js';
|
||||||
import {
|
import {
|
||||||
|
createUserWithRootRole,
|
||||||
type IUnleashTest,
|
type IUnleashTest,
|
||||||
setupAppWithCustomConfig,
|
setupAppWithAuth,
|
||||||
} from '../../../test/e2e/helpers/test-helper.js';
|
} from '../../../test/e2e/helpers/test-helper.js';
|
||||||
import getLogger from '../../../test/fixtures/no-logger.js';
|
import getLogger from '../../../test/fixtures/no-logger.js';
|
||||||
|
import { RoleName } from '../../types/index.js';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
|
|
||||||
|
const adminEmail = 'admin-user@getunleash.io';
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('tag_types_api_serial', getLogger);
|
db = await dbInit('tag_types_api_serial', getLogger);
|
||||||
app = await setupAppWithCustomConfig(
|
app = await setupAppWithAuth(
|
||||||
db.stores,
|
db.stores,
|
||||||
{
|
{
|
||||||
experimental: {
|
experimental: {
|
||||||
@ -23,6 +27,17 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
db.rawDatabase,
|
db.rawDatabase,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await createUserWithRootRole({
|
||||||
|
app,
|
||||||
|
stores: db.stores,
|
||||||
|
email: adminEmail,
|
||||||
|
roleName: RoleName.ADMIN,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await app.login({ email: adminEmail });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -18,8 +18,6 @@ import {
|
|||||||
type FeatureEventsSchema,
|
type FeatureEventsSchema,
|
||||||
} from '../../../lib/openapi/spec/feature-events-schema.js';
|
} from '../../../lib/openapi/spec/feature-events-schema.js';
|
||||||
import { getStandardResponses } from '../../../lib/openapi/util/standard-responses.js';
|
import { getStandardResponses } from '../../../lib/openapi/util/standard-responses.js';
|
||||||
import { createRequestSchema } from '../../openapi/util/create-request-schema.js';
|
|
||||||
import type { DeprecatedSearchEventsSchema } from '../../openapi/spec/deprecated-search-events-schema.js';
|
|
||||||
import type { IFlagResolver } from '../../types/experimental.js';
|
import type { IFlagResolver } from '../../types/experimental.js';
|
||||||
import type { IAuthRequest } from '../unleash-types.js';
|
import type { IAuthRequest } from '../unleash-types.js';
|
||||||
import {
|
import {
|
||||||
@ -100,27 +98,6 @@ export default class EventController extends Controller {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route({
|
|
||||||
method: 'post',
|
|
||||||
path: '/events/search',
|
|
||||||
handler: this.deprecatedSearchEvents,
|
|
||||||
permission: NONE,
|
|
||||||
middleware: [
|
|
||||||
openApiService.validPath({
|
|
||||||
operationId: 'deprecatedSearchEvents',
|
|
||||||
tags: ['Events'],
|
|
||||||
deprecated: true,
|
|
||||||
summary: 'Search for events (deprecated)',
|
|
||||||
description:
|
|
||||||
'Allows searching for events matching the search criteria in the request body',
|
|
||||||
requestBody: createRequestSchema(
|
|
||||||
'deprecatedSearchEventsSchema',
|
|
||||||
),
|
|
||||||
responses: { 200: createResponseSchema('eventsSchema') },
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '/event-creators',
|
path: '/event-creators',
|
||||||
@ -201,28 +178,6 @@ export default class EventController extends Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deprecatedSearchEvents(
|
|
||||||
req: Request<unknown, unknown, DeprecatedSearchEventsSchema>,
|
|
||||||
res: Response<EventsSchema>,
|
|
||||||
): Promise<void> {
|
|
||||||
const eventList = await this.eventService.deprecatedSearchEvents(
|
|
||||||
req.body,
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
version,
|
|
||||||
events: serializeDates(this.maybeAnonymiseEvents(eventList.events)),
|
|
||||||
totalEvents: eventList.totalEvents,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
|
||||||
200,
|
|
||||||
res,
|
|
||||||
featureEventsSchema.$id,
|
|
||||||
response,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getEventCreators(
|
async getEventCreators(
|
||||||
req: IAuthRequest,
|
req: IAuthRequest,
|
||||||
res: Response<ProjectFlagCreatorsSchema>,
|
res: Response<ProjectFlagCreatorsSchema>,
|
||||||
|
@ -6,66 +6,26 @@ import {
|
|||||||
type IUnleashStores,
|
type IUnleashStores,
|
||||||
RoleName,
|
RoleName,
|
||||||
} from '../../../../lib/types/index.js';
|
} from '../../../../lib/types/index.js';
|
||||||
import type {
|
import type { EventService } from '../../../../lib/services/index.js';
|
||||||
AccessService,
|
|
||||||
EventService,
|
|
||||||
} from '../../../../lib/services/index.js';
|
|
||||||
import getLogger from '../../../fixtures/no-logger.js';
|
import getLogger from '../../../fixtures/no-logger.js';
|
||||||
import {
|
import {
|
||||||
|
createUserWithRootRole,
|
||||||
type IUnleashTest,
|
type IUnleashTest,
|
||||||
setupAppWithAuth,
|
setupAppWithAuth,
|
||||||
} from '../../helpers/test-helper.js';
|
} from '../../helpers/test-helper.js';
|
||||||
import { createEventsService } from '../../../../lib/features/index.js';
|
import { createEventsService } from '../../../../lib/features/index.js';
|
||||||
import { createTestConfig } from '../../../config/test-config.js';
|
import { createTestConfig } from '../../../config/test-config.js';
|
||||||
import type { IRole } from '../../../../lib/types/stores/access-store.js';
|
|
||||||
import { FEATURE_CREATED, USER_CREATED } from '../../../../lib/events/index.js';
|
import { FEATURE_CREATED, USER_CREATED } from '../../../../lib/events/index.js';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
let eventService: EventService;
|
let eventService: EventService;
|
||||||
const TEST_USER_ID = -9999;
|
const TEST_USER_ID = -9999;
|
||||||
const regularUserName = 'import-user';
|
const regularEmail = 'import-user@getunleash.io';
|
||||||
const adminUserName = 'admin-user';
|
const adminEmail = 'admin-user@getunleash.io';
|
||||||
|
|
||||||
const config: IUnleashConfig = createTestConfig();
|
const config: IUnleashConfig = createTestConfig();
|
||||||
let adminRole: IRole;
|
|
||||||
let stores: IUnleashStores;
|
let stores: IUnleashStores;
|
||||||
let accessService: AccessService;
|
|
||||||
|
|
||||||
const loginRegularUser = () =>
|
|
||||||
app.request
|
|
||||||
.post(`/auth/demo/login`)
|
|
||||||
.send({
|
|
||||||
email: `${regularUserName}@getunleash.io`,
|
|
||||||
})
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const loginAdminUser = () =>
|
|
||||||
app.request
|
|
||||||
.post(`/auth/demo/login`)
|
|
||||||
.send({
|
|
||||||
email: `${adminUserName}@getunleash.io`,
|
|
||||||
})
|
|
||||||
.expect(200);
|
|
||||||
|
|
||||||
const createUserEditorAccess = async (name, email) => {
|
|
||||||
const { userStore } = stores;
|
|
||||||
const user = await userStore.insert({
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
return user;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createUserAdminAccess = async (name, email) => {
|
|
||||||
const { userStore } = stores;
|
|
||||||
const user = await userStore.insert({
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
});
|
|
||||||
await accessService.addUserToRole(user.id, adminRole.id, 'default');
|
|
||||||
return user;
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('event_search', getLogger);
|
db = await dbInit('event_search', getLogger);
|
||||||
@ -84,19 +44,18 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
eventService = createEventsService(db.rawDatabase, config);
|
eventService = createEventsService(db.rawDatabase, config);
|
||||||
|
|
||||||
accessService = app.services.accessService;
|
await createUserWithRootRole({
|
||||||
|
app,
|
||||||
|
stores,
|
||||||
|
email: regularEmail,
|
||||||
|
});
|
||||||
|
|
||||||
const roles = await accessService.getRootRoles();
|
await createUserWithRootRole({
|
||||||
adminRole = roles.find((role) => role.name === RoleName.ADMIN)!;
|
app,
|
||||||
|
stores,
|
||||||
await createUserEditorAccess(
|
email: adminEmail,
|
||||||
regularUserName,
|
roleName: RoleName.ADMIN,
|
||||||
`${regularUserName}@getunleash.io`,
|
});
|
||||||
);
|
|
||||||
await createUserAdminAccess(
|
|
||||||
adminUserName,
|
|
||||||
`${adminUserName}@getunleash.io`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -105,7 +64,7 @@ afterAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await loginAdminUser();
|
await app.login({ email: adminEmail });
|
||||||
await db.stores.featureToggleStore.deleteAll();
|
await db.stores.featureToggleStore.deleteAll();
|
||||||
await db.stores.segmentStore.deleteAll();
|
await db.stores.segmentStore.deleteAll();
|
||||||
await db.stores.eventStore.deleteAll();
|
await db.stores.eventStore.deleteAll();
|
||||||
@ -522,7 +481,7 @@ test('should filter events by project using IS_ANY_OF', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not show user creation events for non-admins', async () => {
|
test('should not show user creation events for non-admins', async () => {
|
||||||
await loginRegularUser();
|
await app.login({ email: regularEmail });
|
||||||
await eventService.storeEvent({
|
await eventService.storeEvent({
|
||||||
type: USER_CREATED,
|
type: USER_CREATED,
|
||||||
createdBy: 'test-user',
|
createdBy: 'test-user',
|
||||||
|
@ -90,75 +90,6 @@ test('Can filter by project', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can search for events', async () => {
|
|
||||||
const events: IBaseEvent[] = [
|
|
||||||
{
|
|
||||||
type: FEATURE_CREATED,
|
|
||||||
project: randomId(),
|
|
||||||
data: { id: randomId() },
|
|
||||||
tags: [],
|
|
||||||
createdBy: randomId(),
|
|
||||||
createdByUserId: TEST_USER_ID,
|
|
||||||
ip: '127.0.0.1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: FEATURE_CREATED,
|
|
||||||
project: randomId(),
|
|
||||||
data: { id: randomId() },
|
|
||||||
preData: { id: randomId() },
|
|
||||||
tags: [{ type: 'simple', value: randomId() }],
|
|
||||||
createdBy: randomId(),
|
|
||||||
createdByUserId: TEST_USER_ID,
|
|
||||||
ip: '127.0.0.1',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
events.map((event) => {
|
|
||||||
return eventService.storeEvent(event);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await app.request
|
|
||||||
.post('/api/admin/events/search')
|
|
||||||
.send({})
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
expect(res.body.events).toHaveLength(2);
|
|
||||||
});
|
|
||||||
await app.request
|
|
||||||
.post('/api/admin/events/search')
|
|
||||||
.send({ limit: 1, offset: 1 })
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
expect(res.body.events).toHaveLength(1);
|
|
||||||
});
|
|
||||||
await app.request
|
|
||||||
.post('/api/admin/events/search')
|
|
||||||
.send({ query: events[1].data.id })
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
expect(res.body.events).toHaveLength(1);
|
|
||||||
expect(res.body.events[0].data.id).toEqual(events[1].data.id);
|
|
||||||
});
|
|
||||||
await app.request
|
|
||||||
.post('/api/admin/events/search')
|
|
||||||
.send({ query: events[1].preData.id })
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
expect(res.body.events).toHaveLength(1);
|
|
||||||
expect(res.body.events[0].preData.id).toEqual(events[1].preData.id);
|
|
||||||
});
|
|
||||||
await app.request
|
|
||||||
.post('/api/admin/events/search')
|
|
||||||
.send({ query: events[1].tags![0].value })
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
expect(res.body.events).toHaveLength(1);
|
|
||||||
expect(res.body.events[0].data.id).toEqual(events[1].data.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('event creators - if system user, return system name, else should return name from database if user exists, else from events table', async () => {
|
test('event creators - if system user, return system name, else should return name from database if user exists, else from events table', async () => {
|
||||||
const user = await db.stores.userStore.insert({ name: 'database-user' });
|
const user = await db.stores.userStore.insert({ name: 'database-user' });
|
||||||
const events: IBaseEvent[] = [
|
const events: IBaseEvent[] = [
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
import dbInit, { type ITestDb } from '../../helpers/database-init.js';
|
import dbInit, { type ITestDb } from '../../helpers/database-init.js';
|
||||||
import getLogger from '../../../fixtures/no-logger.js';
|
import getLogger from '../../../fixtures/no-logger.js';
|
||||||
import {
|
import {
|
||||||
|
createUserWithRootRole,
|
||||||
type IUnleashTest,
|
type IUnleashTest,
|
||||||
setupAppWithCustomConfig,
|
setupAppWithAuth,
|
||||||
} from '../../helpers/test-helper.js';
|
} from '../../helpers/test-helper.js';
|
||||||
import { validateSchema } from '../../../../lib/openapi/validate.js';
|
import { validateSchema } from '../../../../lib/openapi/validate.js';
|
||||||
import { featureTypesSchema } from '../../../../lib/openapi/spec/feature-types-schema.js';
|
import { featureTypesSchema } from '../../../../lib/openapi/spec/feature-types-schema.js';
|
||||||
|
import { RoleName } from '../../../../lib/types/index.js';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
|
|
||||||
|
const adminEmail = 'admin-user@getunleash.io';
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('feature_type_api_serial', getLogger);
|
db = await dbInit('feature_type_api_serial', getLogger);
|
||||||
app = await setupAppWithCustomConfig(
|
app = await setupAppWithAuth(
|
||||||
db.stores,
|
db.stores,
|
||||||
{
|
{
|
||||||
experimental: {
|
experimental: {
|
||||||
@ -23,6 +27,17 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
db.rawDatabase,
|
db.rawDatabase,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await createUserWithRootRole({
|
||||||
|
app,
|
||||||
|
stores: db.stores,
|
||||||
|
email: adminEmail,
|
||||||
|
roleName: RoleName.ADMIN,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await app.login({ email: adminEmail });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -24,9 +24,23 @@ import type { Knex } from 'knex';
|
|||||||
import type TestAgent from 'supertest/lib/agent.d.ts';
|
import type TestAgent from 'supertest/lib/agent.d.ts';
|
||||||
import type Test from 'supertest/lib/test.d.ts';
|
import type Test from 'supertest/lib/test.d.ts';
|
||||||
import type { Server } from 'node:http';
|
import type { Server } from 'node:http';
|
||||||
import { initialServiceSetup } from '../../../lib/server-impl.js';
|
import {
|
||||||
|
initialServiceSetup,
|
||||||
|
type IUser,
|
||||||
|
type RoleName,
|
||||||
|
} from '../../../lib/server-impl.js';
|
||||||
|
import type { EventSearchQueryParameters } from '../../../lib/openapi/spec/event-search-query-parameters.js';
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
|
type DemoLoginArgs = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SimpleLoginArgs = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IUnleashTest extends IUnleashHttpAPI {
|
export interface IUnleashTest extends IUnleashHttpAPI {
|
||||||
request: TestAgent<Test>;
|
request: TestAgent<Test>;
|
||||||
destroy: () => Promise<void>;
|
destroy: () => Promise<void>;
|
||||||
@ -115,7 +129,10 @@ export interface IUnleashHttpAPI {
|
|||||||
expectedResponseCode?: number,
|
expectedResponseCode?: number,
|
||||||
): supertest.Test;
|
): supertest.Test;
|
||||||
|
|
||||||
getRecordedEvents(): supertest.Test;
|
getRecordedEvents(
|
||||||
|
queryParams?: EventSearchQueryParameters,
|
||||||
|
expectedResponseCode?: number,
|
||||||
|
): supertest.Test;
|
||||||
|
|
||||||
createSegment(postData: object, expectStatusCode?: number): supertest.Test;
|
createSegment(postData: object, expectStatusCode?: number): supertest.Test;
|
||||||
deleteSegment(
|
deleteSegment(
|
||||||
@ -127,6 +144,8 @@ export interface IUnleashHttpAPI {
|
|||||||
postData: object,
|
postData: object,
|
||||||
expectStatusCode?: number,
|
expectStatusCode?: number,
|
||||||
): supertest.Test;
|
): supertest.Test;
|
||||||
|
|
||||||
|
login(args: DemoLoginArgs | SimpleLoginArgs): supertest.Test;
|
||||||
}
|
}
|
||||||
|
|
||||||
function httpApis(
|
function httpApis(
|
||||||
@ -318,15 +337,32 @@ function httpApis(
|
|||||||
.expect(expectStatusCode);
|
.expect(expectStatusCode);
|
||||||
},
|
},
|
||||||
getRecordedEvents(
|
getRecordedEvents(
|
||||||
project: string | null = null,
|
queryParams: EventSearchQueryParameters = {},
|
||||||
expectedResponseCode: number = 200,
|
expectedResponseCode: number = 200,
|
||||||
): supertest.Test {
|
): supertest.Test {
|
||||||
|
const query = new URLSearchParams(queryParams as any).toString();
|
||||||
return request
|
return request
|
||||||
.post('/api/admin/events/search')
|
.get(`/api/admin/search/events${query ? `?${query}` : ''}`)
|
||||||
.send({ project, query: '', limit: 50, offset: 0 })
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(expectedResponseCode);
|
.expect(expectedResponseCode);
|
||||||
},
|
},
|
||||||
|
login(args: DemoLoginArgs | SimpleLoginArgs): supertest.Test {
|
||||||
|
if ('email' in args) {
|
||||||
|
const { email } = args;
|
||||||
|
return request
|
||||||
|
.post(`${base}/auth/demo/login`)
|
||||||
|
.send({ email })
|
||||||
|
.expect(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { username, password } = args;
|
||||||
|
return request
|
||||||
|
.post(`${base}/auth/simple/login`)
|
||||||
|
.send({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
} as SimpleLoginArgs)
|
||||||
|
.expect(200);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,3 +545,39 @@ export const insertFeatureEnvironmentsLastSeen = async (
|
|||||||
|
|
||||||
return date;
|
return date;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createUserWithRootRole = async ({
|
||||||
|
app,
|
||||||
|
stores,
|
||||||
|
email,
|
||||||
|
name = email,
|
||||||
|
roleName,
|
||||||
|
}: {
|
||||||
|
app: IUnleashTest;
|
||||||
|
stores: IUnleashStores;
|
||||||
|
name?: string;
|
||||||
|
email: string;
|
||||||
|
roleName?: RoleName;
|
||||||
|
}): Promise<IUser> => {
|
||||||
|
const createdUser = await stores.userStore.insert({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (roleName) {
|
||||||
|
const roles = await app.services.accessService.getRootRoles();
|
||||||
|
const role = roles.find((role) => role.name === roleName);
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new Error(`Role ${roleName} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await app.services.accessService.addUserToRole(
|
||||||
|
createdUser.id,
|
||||||
|
role.id,
|
||||||
|
'default',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdUser;
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user