mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
feat(api): exempt APP_STORE_REVIEWER accounts from captcha (#2090)
This commit is contained in:
1 parent
9b8659a40b
commit
d297dc5805
2 files changed
+99
No files matched your search
@@ -0,0 +1,79 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||||
|
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||||
|
import {Config} from '../../Config';
|
||||||
|
import type {ApiTestHarness} from '../../test/ApiTestHarness';
|
||||||
|
import {HTTP_STATUS} from '../../test/TestConstants';
|
||||||
|
import {createBuilderWithoutAuth} from '../../test/TestRequestBuilder';
|
||||||
|
import {createAuthHarness, createUniqueEmail, createUniqueUsername, registerUser} from './AuthTestUtils';
|
||||||
|
|
||||||
|
async function withCaptchaEnabled<T>(run: () => Promise<T>): Promise<T> {
|
||||||
|
const previousEnabled = Config.captcha.enabled;
|
||||||
|
const previousTestModeEnabled = Config.dev.testModeEnabled;
|
||||||
|
Config.captcha.enabled = true;
|
||||||
|
Config.dev.testModeEnabled = true;
|
||||||
|
try {
|
||||||
|
return await run();
|
||||||
|
} finally {
|
||||||
|
Config.captcha.enabled = previousEnabled;
|
||||||
|
Config.dev.testModeEnabled = previousTestModeEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerAndFlag(
|
||||||
|
harness: ApiTestHarness,
|
||||||
|
flags: Array<string>,
|
||||||
|
): Promise<{email: string; password: string; userId: string}> {
|
||||||
|
const email = createUniqueEmail('captcha-flags');
|
||||||
|
const password = 'a-strong-password';
|
||||||
|
const reg = await registerUser(harness, {
|
||||||
|
email,
|
||||||
|
username: createUniqueUsername('captchaflags'),
|
||||||
|
global_name: 'Captcha Flags User',
|
||||||
|
password,
|
||||||
|
date_of_birth: '2000-01-01',
|
||||||
|
consent: true,
|
||||||
|
});
|
||||||
|
if (flags.length > 0) {
|
||||||
|
await createBuilderWithoutAuth(harness)
|
||||||
|
.post(`/test/users/${reg.user_id}/security-flags`)
|
||||||
|
.body({set_flags: flags})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
return {email, password, userId: reg.user_id};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Auth Captcha Bypass Flags', () => {
|
||||||
|
let harness: ApiTestHarness;
|
||||||
|
beforeAll(async () => {
|
||||||
|
harness = await createAuthHarness();
|
||||||
|
});
|
||||||
|
beforeEach(async () => {
|
||||||
|
await harness.reset();
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
await harness?.shutdown();
|
||||||
|
});
|
||||||
|
it('lets APP_STORE_REVIEWER accounts log in without solving a captcha', async () => {
|
||||||
|
const account = await registerAndFlag(harness, ['APP_STORE_REVIEWER']);
|
||||||
|
await withCaptchaEnabled(async () => {
|
||||||
|
const resp = await createBuilderWithoutAuth<{token?: string; user_id?: string}>(harness)
|
||||||
|
.post('/auth/login')
|
||||||
|
.body({email: account.email, password: account.password})
|
||||||
|
.execute();
|
||||||
|
expect(resp.token).toBeTruthy();
|
||||||
|
expect(resp.user_id).toBe(account.userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('still requires a captcha for accounts without the APP_STORE_REVIEWER flag', async () => {
|
||||||
|
const account = await registerAndFlag(harness, []);
|
||||||
|
await withCaptchaEnabled(async () => {
|
||||||
|
await createBuilderWithoutAuth(harness)
|
||||||
|
.post('/auth/login')
|
||||||
|
.body({email: account.email, password: account.password})
|
||||||
|
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.CAPTCHA_REQUIRED)
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {UserFlags} from '@fluxer/constants/src/UserConstants';
|
||||||
import {CaptchaRequiredError, InvalidCaptchaError} from '@fluxer/errors/src/CaptchaErrors';
|
import {CaptchaRequiredError, InvalidCaptchaError} from '@fluxer/errors/src/CaptchaErrors';
|
||||||
import {extractClientIp} from '@fluxer/ip_utils/src/ClientIp';
|
import {extractClientIp} from '@fluxer/ip_utils/src/ClientIp';
|
||||||
import {createCaptchaProvider} from '@pkgs/captcha/src/CaptchaProviderFactory';
|
import {createCaptchaProvider} from '@pkgs/captcha/src/CaptchaProviderFactory';
|
||||||
@@ -53,7 +54,9 @@ export async function verifyCaptchaToken(ctx: Context<HonoEnv>): Promise<void> {
|
|||||||
if (!captchaConfig.enabled && !(Config.dev.testModeEnabled && Config.captcha.enabled)) return;
|
if (!captchaConfig.enabled && !(Config.dev.testModeEnabled && Config.captcha.enabled)) return;
|
||||||
const user = ctx.get('user') as User | undefined;
|
const user = ctx.get('user') as User | undefined;
|
||||||
if (accountPolicyContactHasCapability(user?.email, 'captcha_exempt')) return;
|
if (accountPolicyContactHasCapability(user?.email, 'captcha_exempt')) return;
|
||||||
|
if (userHasCaptchaExemptFlag(user)) return;
|
||||||
if (await requestContactHasCaptchaExemption(ctx.req.raw)) return;
|
if (await requestContactHasCaptchaExemption(ctx.req.raw)) return;
|
||||||
|
if (await requestUserHasCaptchaExemptFlag(ctx)) return;
|
||||||
const token = ctx.req.header('x-captcha-token');
|
const token = ctx.req.header('x-captcha-token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new CaptchaRequiredError();
|
throw new CaptchaRequiredError();
|
||||||
@@ -83,6 +86,23 @@ async function requestContactHasCaptchaExemption(request: Request): Promise<bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function userHasCaptchaExemptFlag(user: User | null | undefined): boolean {
|
||||||
|
return user != null && (user.flags & UserFlags.APP_STORE_REVIEWER) !== 0n;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestUserHasCaptchaExemptFlag(ctx: Context<HonoEnv>): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const body = (await ctx.req.raw.clone().json()) as unknown;
|
||||||
|
if (!body || typeof body !== 'object' || Array.isArray(body)) return false;
|
||||||
|
const email = (body as Record<string, unknown>).email;
|
||||||
|
if (typeof email !== 'string') return false;
|
||||||
|
const user = await ctx.get('userRepository').findByEmail(email);
|
||||||
|
return userHasCaptchaExemptFlag(user);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const CaptchaMiddleware = createMiddleware<HonoEnv>(async (ctx, next) => {
|
export const CaptchaMiddleware = createMiddleware<HonoEnv>(async (ctx, next) => {
|
||||||
await verifyCaptchaToken(ctx);
|
await verifyCaptchaToken(ctx);
|
||||||
await next();
|
await next();
|
||||||
|
|||||||
Reference in new issue
Block a user