mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(auth): add username sign-in mode and recovery kits (#3215)
This commit is contained in:
1 parent
bf3d73a5f7
commit
e1eecc3b6c
355 files changed
+52138
-24097
No files matched your search
@@ -11,7 +11,13 @@ import {
|
||||
parsePublicOrigin,
|
||||
parseWebOrigin,
|
||||
} from '@fluxer/config/src/EndpointDerivation';
|
||||
import {CACHE_PURGE_ADAPTER_NAMES, type MasterConfig, STORE_PRODUCT_SLOT_NAMES} from '@fluxer/config/src/MasterConfig';
|
||||
import {
|
||||
ACCOUNT_IDENTITY_MODE_NAMES,
|
||||
CACHE_PURGE_ADAPTER_NAMES,
|
||||
type MasterConfig,
|
||||
STORE_PRODUCT_SLOT_NAMES,
|
||||
TAG_STYLE_NAMES,
|
||||
} from '@fluxer/config/src/MasterConfig';
|
||||
|
||||
let cachedConfig: MasterConfig | null = null;
|
||||
|
||||
@@ -247,6 +253,8 @@ function defaultConfig(): MasterConfig {
|
||||
setup: {
|
||||
configured: false,
|
||||
},
|
||||
account_identity: null,
|
||||
tag_style: null,
|
||||
},
|
||||
dev: {
|
||||
relax_registration_rate_limits: false,
|
||||
@@ -569,6 +577,12 @@ function normalizeConfig(config: MasterConfig): MasterConfig {
|
||||
assertOneOf(config.integrations.email.provider, ['smtp', 'none'], 'FLUXER_EMAIL_PROVIDER');
|
||||
assertOneOf(config.integrations.search.engine, ['elasticsearch', 'meilisearch'], 'FLUXER_SEARCH_ENGINE');
|
||||
assertOneOf(config.integrations.cache_purge.adapter, CACHE_PURGE_ADAPTER_NAMES, 'FLUXER_CACHE_PURGE_ADAPTER');
|
||||
if (config.instance.tag_style !== null) {
|
||||
assertOneOf(config.instance.tag_style, TAG_STYLE_NAMES, 'FLUXER_TAG_STYLE');
|
||||
}
|
||||
if (config.instance.account_identity !== null) {
|
||||
assertOneOf(config.instance.account_identity, ACCOUNT_IDENTITY_MODE_NAMES, 'FLUXER_ACCOUNT_IDENTITY');
|
||||
}
|
||||
validatePostgresConfig(config);
|
||||
validateApiWorkerConfig(config);
|
||||
validateStorageChangeFeedConfig(config);
|
||||
|
||||
@@ -9,6 +9,10 @@ export const CACHE_PURGE_ADAPTER_NAMES = ['none', 'http'] as const;
|
||||
export type CachePurgeAdapterName = (typeof CACHE_PURGE_ADAPTER_NAMES)[number];
|
||||
export const STORE_PRODUCT_SLOT_NAMES = ['monthly', 'yearly', 'gift_1_month', 'gift_1_year'] as const;
|
||||
export type StoreProductSlotName = (typeof STORE_PRODUCT_SLOT_NAMES)[number];
|
||||
export const ACCOUNT_IDENTITY_MODE_NAMES = ['email', 'username'] as const;
|
||||
export type AccountIdentityModeName = (typeof ACCOUNT_IDENTITY_MODE_NAMES)[number];
|
||||
export const TAG_STYLE_NAMES = ['none', 'random'] as const;
|
||||
export type TagStyleName = (typeof TAG_STYLE_NAMES)[number];
|
||||
|
||||
export interface InstanceBrandingConfig {
|
||||
product_name: string;
|
||||
@@ -299,6 +303,8 @@ export interface MasterConfig {
|
||||
setup: {
|
||||
configured: boolean;
|
||||
};
|
||||
account_identity: AccountIdentityModeName | null;
|
||||
tag_style: TagStyleName | null;
|
||||
};
|
||||
dev: {
|
||||
relax_registration_rate_limits: boolean;
|
||||
|
||||
@@ -564,6 +564,48 @@ describe('ConfigLoader', () => {
|
||||
expect(config.instance.setup.configured).toBe(true);
|
||||
});
|
||||
|
||||
test('leaves the account identity unset by default', async () => {
|
||||
stubMinimalEnv();
|
||||
const config = await loadConfig();
|
||||
expect(config.instance.account_identity).toBeNull();
|
||||
});
|
||||
|
||||
test('treats an empty account identity as unset', async () => {
|
||||
stubMinimalEnv({FLUXER_ACCOUNT_IDENTITY: ' '});
|
||||
const config = await loadConfig();
|
||||
expect(config.instance.account_identity).toBeNull();
|
||||
});
|
||||
|
||||
test.each([
|
||||
['email', 'email'],
|
||||
['username', 'username'],
|
||||
[' Username ', 'username'],
|
||||
])('parses FLUXER_ACCOUNT_IDENTITY=%j', async (raw, expected) => {
|
||||
stubMinimalEnv({FLUXER_ACCOUNT_IDENTITY: raw});
|
||||
const config = await loadConfig();
|
||||
expect(config.instance.account_identity).toBe(expected);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['none', 'none'],
|
||||
[' Random ', 'random'],
|
||||
['', null],
|
||||
])('parses FLUXER_TAG_STYLE=%j', async (raw, expected) => {
|
||||
stubMinimalEnv({FLUXER_TAG_STYLE: raw});
|
||||
const config = await loadConfig();
|
||||
expect(config.instance.tag_style).toBe(expected);
|
||||
});
|
||||
|
||||
test.each(['sequential', 'zero_first'])('rejects the tag style %j', async (raw) => {
|
||||
stubMinimalEnv({FLUXER_TAG_STYLE: raw});
|
||||
await expect(loadConfig()).rejects.toThrow('FLUXER_TAG_STYLE must be none or random');
|
||||
});
|
||||
|
||||
test('rejects an unknown account identity', async () => {
|
||||
stubMinimalEnv({FLUXER_ACCOUNT_IDENTITY: 'phone'});
|
||||
await expect(loadConfig()).rejects.toThrow('FLUXER_ACCOUNT_IDENTITY must be email or username');
|
||||
});
|
||||
|
||||
test('leaves both stores off with no apps, packages or products by default', async () => {
|
||||
stubMinimalEnv();
|
||||
const config = await loadConfig();
|
||||
|
||||
@@ -280,6 +280,8 @@ const NAMED_FLUXER_ENV_OVERRIDES: Record<string, NamedEnvOverride> = {
|
||||
FLUXER_APP_STATUS_PAGE_URL: {path: ['instance', 'branding', 'status_page_url']},
|
||||
FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL: {path: ['instance', 'branding', 'status_page_incident_history_url']},
|
||||
FLUXER_INSTANCE_SETUP_CONFIGURED: {path: ['instance', 'setup', 'configured'], parse: parseBoolean},
|
||||
FLUXER_ACCOUNT_IDENTITY: {path: ['instance', 'account_identity'], parse: parseAccountIdentity},
|
||||
FLUXER_TAG_STYLE: {path: ['instance', 'tag_style'], parse: parseTagStyle},
|
||||
FLUXER_DISCOVERY_ENABLED: {path: ['discovery', 'enabled'], parse: parseBoolean},
|
||||
FLUXER_DISCOVERY_MIN_MEMBER_COUNT: {path: ['discovery', 'min_member_count'], parse: parseInteger},
|
||||
FLUXER_DELETION_GRACE_PERIOD_HOURS: {path: ['deletion_grace_period_hours'], parse: parseInteger},
|
||||
@@ -326,6 +328,18 @@ function parseBoolean(raw: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function parseAccountIdentity(raw: string): string {
|
||||
const value = raw.trim().toLowerCase();
|
||||
if (value === 'email' || value === 'username') return value;
|
||||
throw new Error('must be email or username');
|
||||
}
|
||||
|
||||
function parseTagStyle(raw: string): string {
|
||||
const value = raw.trim().toLowerCase();
|
||||
if (value === 'none' || value === 'random') return value;
|
||||
throw new Error('must be none or random');
|
||||
}
|
||||
|
||||
function parseJson(raw: string): unknown {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
|
||||
Reference in new issue
Block a user