mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
fix(self-hosting): use instance contacts and live product name (#3333)
This commit is contained in:
1 parent
454fefdb92
commit
a5c1362f4a
308 files changed
+16533
-13512
No files matched your search
@@ -479,6 +479,7 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
? omitUndefinedFields({
|
||||
terms_url: readOptionalField(data.app_public.legal, 'terms_url'),
|
||||
privacy_url: readOptionalField(data.app_public.legal, 'privacy_url'),
|
||||
guidelines_url: readOptionalField(data.app_public.legal, 'guidelines_url'),
|
||||
})
|
||||
: undefined,
|
||||
registration: data.app_public.registration
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {ChannelService} from '@app/api/channel/services/ChannelService';
|
||||
import {SYSTEM_USER_ID} from '@app/api/constants/Core';
|
||||
import {SYSTEM_THREAD_VIEWER} from '@app/api/experiment/ChannelThreadsGate';
|
||||
import type {UserCacheService} from '@app/api/infrastructure/UserCacheService';
|
||||
import {resolveContactEmails} from '@app/api/instance/ContactEmails';
|
||||
import {getInstanceProductName} from '@app/api/instance/ProductName';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
import {createRequestCache} from '@app/api/middleware/RequestCacheMiddleware';
|
||||
import type {User} from '@app/api/models/User';
|
||||
@@ -120,12 +122,18 @@ export class ReporterResolutionNotifier {
|
||||
if (!systemDm) {
|
||||
return false;
|
||||
}
|
||||
const template = getEmailTemplate('report_resolved', reporter.locale, {
|
||||
username: reporter.username,
|
||||
reportId,
|
||||
publicComment: comment,
|
||||
hasComment: comment ? 'yes' : 'no',
|
||||
});
|
||||
const template = getEmailTemplate(
|
||||
'report_resolved',
|
||||
reporter.locale,
|
||||
{
|
||||
username: reporter.username,
|
||||
reportId,
|
||||
publicComment: comment,
|
||||
hasComment: comment ? 'yes' : 'no',
|
||||
safety_email: resolveContactEmails().safetyEmail,
|
||||
},
|
||||
getInstanceProductName(),
|
||||
);
|
||||
if (!template.ok) {
|
||||
Logger.warn(
|
||||
{reportId, reporterId: reporter.id.toString(), locale: reporter.locale, error: template.error},
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {TestAccount} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import {createTestAccount, setUserACLs} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import {getConfig} from '@app/api/Config';
|
||||
import {setCachedConfiguredLegalUrls} from '@app/api/instance/LegalUrls';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@app/api/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
||||
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import type {InstanceConfigResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
|
||||
import type {WellKnownFluxerResponse} from '@fluxer/schema/src/domains/instance/InstanceSchemas';
|
||||
import type {ReportFlowResponse} from '@fluxer/schema/src/domains/report/ReportFlowSchemas';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
const RULES_URL = 'https://rules.example.org/community';
|
||||
|
||||
function useInstanceHarness(selfHosted: boolean) {
|
||||
const context: {harness: ApiTestHarness} = {harness: undefined as unknown as ApiTestHarness};
|
||||
let originalSelfHosted: boolean;
|
||||
beforeAll(async () => {
|
||||
originalSelfHosted = getConfig().instance.selfHosted;
|
||||
getConfig().instance.selfHosted = selfHosted;
|
||||
context.harness = await createApiTestHarness();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await context.harness.reset();
|
||||
setCachedConfiguredLegalUrls({terms_url: null, guidelines_url: null});
|
||||
});
|
||||
afterAll(async () => {
|
||||
await context.harness.shutdown();
|
||||
getConfig().instance.selfHosted = originalSelfHosted;
|
||||
setCachedConfiguredLegalUrls({terms_url: null, guidelines_url: null});
|
||||
});
|
||||
return context;
|
||||
}
|
||||
|
||||
async function createAdmin(harness: ApiTestHarness): Promise<TestAccount> {
|
||||
return await setUserACLs(harness, await createTestAccount(harness), [
|
||||
AdminACLs.AUTHENTICATE,
|
||||
AdminACLs.INSTANCE_CONFIG_VIEW,
|
||||
AdminACLs.INSTANCE_CONFIG_UPDATE,
|
||||
]);
|
||||
}
|
||||
|
||||
function patchLegal(harness: ApiTestHarness, admin: TestAccount, legal: Record<string, unknown>) {
|
||||
return createBuilder<InstanceConfigResponse>(harness, admin.token)
|
||||
.patch('/admin/instance/config')
|
||||
.body({app_public: {legal}});
|
||||
}
|
||||
|
||||
function getUserFlow(harness: ApiTestHarness): Promise<ReportFlowResponse> {
|
||||
return createBuilderWithoutAuth<ReportFlowResponse>(harness)
|
||||
.get('/reports/flows/user?surface=in_app&locale=en-US')
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
}
|
||||
|
||||
describe('community guidelines URL on a self-hosted instance', () => {
|
||||
const context = useInstanceHarness(true);
|
||||
|
||||
it('is unset by default, so no guidelines link is offered', async () => {
|
||||
const admin = await createAdmin(context.harness);
|
||||
const config = await createBuilder<InstanceConfigResponse>(context.harness, admin.token)
|
||||
.get('/admin/instance/config')
|
||||
.execute();
|
||||
expect(config.app_public.legal.guidelines_url).toBeNull();
|
||||
const flow = await getUserFlow(context.harness);
|
||||
expect(flow.guidelines_url).toBeNull();
|
||||
expect(flow.screens[0].options).toEqual([]);
|
||||
});
|
||||
|
||||
it('round-trips through the admin API and reaches discovery and the report flow', async () => {
|
||||
const admin = await createAdmin(context.harness);
|
||||
const before = await getUserFlow(context.harness);
|
||||
const updated = await patchLegal(context.harness, admin, {guidelines_url: ` ${RULES_URL} `}).execute();
|
||||
expect(updated.app_public.legal).toEqual({terms_url: null, privacy_url: null, guidelines_url: RULES_URL});
|
||||
const reread = await createBuilder<InstanceConfigResponse>(context.harness, admin.token)
|
||||
.get('/admin/instance/config')
|
||||
.execute();
|
||||
expect(reread.app_public.legal.guidelines_url).toBe(RULES_URL);
|
||||
const discovery = await createBuilderWithoutAuth<WellKnownFluxerResponse>(context.harness)
|
||||
.get('/.well-known/fluxer')
|
||||
.execute();
|
||||
expect(discovery.app_public.legal.guidelines_url).toBe(RULES_URL);
|
||||
const flow = await getUserFlow(context.harness);
|
||||
expect(flow.guidelines_url).toBe(RULES_URL);
|
||||
expect(flow.screens[0].options.map((option) => option.outcome.url)).toEqual([RULES_URL]);
|
||||
expect(flow.revision_hash).not.toBe(before.revision_hash);
|
||||
expect(flow.screens.find((screen) => screen.id === 'root_user')?.options.map((option) => option.id)).not.toContain(
|
||||
'dsa',
|
||||
);
|
||||
await patchLegal(context.harness, admin, {terms_url: 'https://rules.example.org/tos'}).execute();
|
||||
expect((await getUserFlow(context.harness)).guidelines_url).toBe(RULES_URL);
|
||||
const cleared = await patchLegal(context.harness, admin, {guidelines_url: null}).execute();
|
||||
expect(cleared.app_public.legal.guidelines_url).toBeNull();
|
||||
expect(cleared.app_public.legal.terms_url).toBe('https://rules.example.org/tos');
|
||||
const after = await getUserFlow(context.harness);
|
||||
expect(after.guidelines_url).toBeNull();
|
||||
expect(after.revision_hash).toBe(before.revision_hash);
|
||||
});
|
||||
|
||||
it('clears every legal URL at once, and an empty string clears like null', async () => {
|
||||
const admin = await createAdmin(context.harness);
|
||||
await patchLegal(context.harness, admin, {
|
||||
terms_url: 'https://rules.example.org/tos',
|
||||
privacy_url: 'https://rules.example.org/privacy',
|
||||
guidelines_url: RULES_URL,
|
||||
}).execute();
|
||||
const cleared = await patchLegal(context.harness, admin, {
|
||||
terms_url: null,
|
||||
privacy_url: '',
|
||||
guidelines_url: '',
|
||||
}).execute();
|
||||
expect(cleared.app_public.legal).toEqual({terms_url: null, privacy_url: null, guidelines_url: null});
|
||||
expect((await getUserFlow(context.harness)).guidelines_url).toBeNull();
|
||||
});
|
||||
|
||||
it.each(['rules.example.org', 'javascript:alert(1)', 'ftp://rules.example.org', '/guidelines'])(
|
||||
'rejects %j as a guidelines URL',
|
||||
async (value) => {
|
||||
const admin = await createAdmin(context.harness);
|
||||
await patchLegal(context.harness, admin, {guidelines_url: value})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
|
||||
.execute();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('community guidelines URL on a hosted instance', () => {
|
||||
const context = useInstanceHarness(false);
|
||||
|
||||
it('defaults to the marketing page and can be overridden', async () => {
|
||||
const admin = await createAdmin(context.harness);
|
||||
const hosted = await getUserFlow(context.harness);
|
||||
expect(hosted.guidelines_url).toBe(`${getConfig().endpoints.marketing}/guidelines`);
|
||||
await patchLegal(context.harness, admin, {guidelines_url: RULES_URL}).execute();
|
||||
const flow = await getUserFlow(context.harness);
|
||||
expect(flow.guidelines_url).toBe(RULES_URL);
|
||||
expect(flow.revision_hash).toBe(hosted.revision_hash);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user