feat(auth): add username sign-in mode and recovery kits (#3215)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-05 14:10:07 +02:00
1 parent bf3d73a5f7
commit e1eecc3b6c
355 files changed
+52138 -24097

No files matched your search

@@ -196,6 +196,7 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS
const instanceConfigRepository = getInstanceConfigRepository();
const limitConfigService = getLimitConfigService();
await instanceConfigRepository.initialize();
await instanceConfigRepository.ensureAccountIdentityMode();
await limitConfigService.initialize();
limitConfigService.setAsGlobalInstance();
const userCacheService = createUserCacheService();
@@ -71,34 +71,51 @@ describe('harvestMessages', () => {
});
});
function userDataParams(overrides: Partial<Parameters<typeof buildUserDataJson>[0]> = {}) {
return {
user: new User({...EMPTY_USER_ROW, user_id: AUTHOR, username: 'exporter', discriminator: 1}),
userId: AUTHOR,
productName: 'Fluxer',
authSessions: [],
relationships: [],
userNotes: new Map(),
userSettings: null,
guildMemberships: [],
guildSettings: [],
savedMessages: [],
privateChannels: [],
favoriteMemes: [],
pushSubscriptions: [],
webAuthnCredentials: [],
mfaBackupCodes: [],
recoveryKitCreatedAt: null,
createdGiftCodes: [],
payments: [],
storePurchases: [],
oauthClients: [],
connections: [],
pinnedDms: [],
authorizedIps: [],
activityData: {last_active_at: null, last_active_ip: null},
...overrides,
};
}
describe('buildUserDataJson', () => {
it('exports the store purchases in their public shape', () => {
const purchase = buildStorePurchaseRow({user_id: AUTHOR});
const data = buildUserDataJson({
user: new User({...EMPTY_USER_ROW, user_id: AUTHOR, username: 'exporter', discriminator: 1}),
userId: AUTHOR,
productName: 'Fluxer',
authSessions: [],
relationships: [],
userNotes: new Map(),
userSettings: null,
guildMemberships: [],
guildSettings: [],
savedMessages: [],
privateChannels: [],
favoriteMemes: [],
pushSubscriptions: [],
webAuthnCredentials: [],
mfaBackupCodes: [],
createdGiftCodes: [],
payments: [],
storePurchases: [purchase],
oauthClients: [],
connections: [],
pinnedDms: [],
authorizedIps: [],
activityData: {last_active_at: null, last_active_ip: null},
});
const data = buildUserDataJson(userDataParams({storePurchases: [purchase]}));
expect(data.store_purchases).toEqual([mapStorePurchaseToResponse(purchase)]);
});
it('exports when the recovery kit was created and nothing else about it', () => {
const createdAt = new Date('2026-10-01T12:00:00.000Z');
const data = buildUserDataJson(userDataParams({recoveryKitCreatedAt: createdAt}));
expect(data.recovery_kit).toEqual({created_at: '2026-10-01T12:00:00.000Z'});
});
it('leaves the recovery kit out when the account has none', () => {
const data = buildUserDataJson(userDataParams());
expect('recovery_kit' in data).toBe(false);
});
});
@@ -12,6 +12,7 @@ import {
throwIfArchiveTerminallyFailed,
} from '@app/api/archive/ArchiveTask';
import {makeDataPackageAttachmentCdnUrl} from '@app/api/attachment/AttachmentUrls';
import {findRecoveryKitCreatedAt} from '@app/api/auth/AuthRecoveryKit';
import {
type ChannelID,
createAttachmentID,
@@ -132,6 +133,7 @@ interface UserDataJsonParams {
pushSubscriptions: Array<PushSubscription>;
webAuthnCredentials: Array<WebAuthnCredential>;
mfaBackupCodes: Array<MfaBackupCode>;
recoveryKitCreatedAt: Date | null;
createdGiftCodes: Array<GiftCode>;
payments: Array<Payment>;
storePurchases: Array<StorePurchaseRow>;
@@ -427,6 +429,7 @@ export function buildUserDataJson(params: UserDataJsonParams) {
pushSubscriptions,
webAuthnCredentials,
mfaBackupCodes,
recoveryKitCreatedAt,
createdGiftCodes,
payments,
storePurchases,
@@ -622,6 +625,7 @@ export function buildUserDataJson(params: UserDataJsonParams) {
consumed_count: mfaBackupCodes.filter((code) => code.consumed).length,
remaining_count: mfaBackupCodes.filter((code) => !code.consumed).length,
},
...(recoveryKitCreatedAt ? {recovery_kit: {created_at: recoveryKitCreatedAt.toISOString()}} : {}),
gift_codes_created: createdGiftCodes.map((gift) => ({
code: gift.code,
duration_months: gift.durationMonths,
@@ -849,6 +853,7 @@ const harvestUserData: ArchiveTaskHandler = async (payload, helpers, attempt) =>
pushSubscriptions,
webAuthnCredentials,
mfaBackupCodes,
recoveryKitCreatedAt,
createdGiftCodes,
payments,
storePurchases,
@@ -869,6 +874,7 @@ const harvestUserData: ArchiveTaskHandler = async (payload, helpers, attempt) =>
userRepository.listPushSubscriptions(userId),
userRepository.listWebAuthnCredentials(userId),
userRepository.listMfaBackupCodes(userId),
findRecoveryKitCreatedAt(userId),
userRepository.findGiftCodesByCreator(userId),
paymentRepository.findPaymentsByUserId(userId),
storeEntitlementService.listStorePurchases(userId),
@@ -905,6 +911,7 @@ const harvestUserData: ArchiveTaskHandler = async (payload, helpers, attempt) =>
pushSubscriptions,
webAuthnCredentials,
mfaBackupCodes,
recoveryKitCreatedAt,
createdGiftCodes,
payments,
storePurchases,