initial commit

This commit is contained in:
Hampus Kraft
2026-01-01 20:42:59 +00:00
commit 2f557eda8c
9029 changed files with 1490197 additions and 0 deletions

View File

@@ -0,0 +1,447 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {Plural, Trans, useLingui} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import * as ModalActionCreators from '~/actions/ModalActionCreators';
import {modal} from '~/actions/ModalActionCreators';
import * as UserActionCreators from '~/actions/UserActionCreators';
import {BackupCodesViewModal} from '~/components/modals/BackupCodesViewModal';
import {ClaimAccountModal} from '~/components/modals/ClaimAccountModal';
import {ConfirmModal} from '~/components/modals/ConfirmModal';
import {MfaTotpDisableModal} from '~/components/modals/MfaTotpDisableModal';
import {MfaTotpEnableModal} from '~/components/modals/MfaTotpEnableModal';
import {PasskeyNameModal} from '~/components/modals/PasskeyNameModal';
import {PhoneAddModal} from '~/components/modals/PhoneAddModal';
import {SettingsTabSection} from '~/components/modals/shared/SettingsTabLayout';
import {Button} from '~/components/uikit/Button/Button';
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
import type {UserRecord} from '~/records/UserRecord';
import * as DateUtils from '~/utils/DateUtils';
import * as WebAuthnUtils from '~/utils/WebAuthnUtils';
import styles from './SecurityTab.module.css';
interface SecurityTabProps {
user: UserRecord;
isClaimed: boolean;
hasSmsMfa: boolean;
hasTotpMfa: boolean;
isSmsMfaDisabledForUser: boolean;
passkeys: Array<UserActionCreators.WebAuthnCredential>;
loadingPasskeys: boolean;
enablingSmsMfa: boolean;
disablingSmsMfa: boolean;
loadPasskeys: () => Promise<void>;
setEnablingSmsMfa: React.Dispatch<React.SetStateAction<boolean>>;
setDisablingSmsMfa: React.Dispatch<React.SetStateAction<boolean>>;
}
export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
({
user,
isClaimed,
hasSmsMfa,
hasTotpMfa,
isSmsMfaDisabledForUser,
passkeys,
loadingPasskeys,
enablingSmsMfa,
disablingSmsMfa,
loadPasskeys,
setEnablingSmsMfa,
setDisablingSmsMfa,
}) => {
const {t, i18n} = useLingui();
const handleAddPasskey = async () => {
try {
const options = await UserActionCreators.getWebAuthnRegistrationOptions();
const credential = await WebAuthnUtils.performRegistration(options);
ModalActionCreators.push(
modal(() => (
<PasskeyNameModal
onSubmit={async (name: string) => {
await UserActionCreators.registerWebAuthnCredential(credential, options.challenge, name);
await loadPasskeys();
}}
/>
)),
);
} catch (error) {
console.error('Failed to add passkey', error);
}
};
const handleRenamePasskey = async (credentialId: string) => {
ModalActionCreators.push(
modal(() => (
<PasskeyNameModal
onSubmit={async (name: string) => {
try {
await UserActionCreators.renameWebAuthnCredential(credentialId, name);
await loadPasskeys();
} catch (error) {
console.error('Failed to rename passkey', error);
}
}}
/>
)),
);
};
const handleDeletePasskey = (credentialId: string) => {
const passkey = passkeys.find((p) => p.id === credentialId);
ModalActionCreators.push(
modal(() => (
<ConfirmModal
title={t`Delete Passkey`}
description={
passkey ? (
<div>
<Trans>
Are you sure you want to delete the passkey <strong>{passkey.name}</strong>?
</Trans>
</div>
) : (
<Trans>Are you sure you want to delete this passkey?</Trans>
)
}
primaryText={t`Delete Passkey`}
primaryVariant="danger-primary"
onPrimary={async () => {
try {
await UserActionCreators.deleteWebAuthnCredential(credentialId);
await loadPasskeys();
} catch (error) {
console.error('Failed to delete passkey', error);
}
}}
/>
)),
);
};
const handleEnableSmsMfa = () => {
ModalActionCreators.push(
modal(() => (
<ConfirmModal
title={t`Enable SMS Two-Factor Authentication`}
description={
<Trans>
SMS two-factor authentication adds an additional layer of security to your account by requiring a
verification code sent to your phone number when signing in.
</Trans>
}
primaryText={t`Enable SMS 2FA`}
primaryVariant="primary"
onPrimary={async () => {
setEnablingSmsMfa(true);
try {
await UserActionCreators.enableSmsMfa();
} catch (error) {
console.error('Failed to enable SMS MFA', error);
} finally {
setEnablingSmsMfa(false);
}
}}
/>
)),
);
};
const handleDisableSmsMfa = () => {
ModalActionCreators.push(
modal(() => (
<ConfirmModal
title={t`Disable SMS Two-Factor Authentication`}
description={
<Trans>
Are you sure you want to disable SMS two-factor authentication? This will make your account less secure.
</Trans>
}
primaryText={t`Disable SMS 2FA`}
primaryVariant="danger-primary"
onPrimary={async () => {
setDisablingSmsMfa(true);
try {
await UserActionCreators.disableSmsMfa();
} catch (error) {
console.error('Failed to disable SMS MFA', error);
} finally {
setDisablingSmsMfa(false);
}
}}
/>
)),
);
};
if (!isClaimed) {
return (
<SettingsTabSection
title={<Trans>Security Features</Trans>}
description={
<Trans>Claim your account to access security features like two-factor authentication and passkeys.</Trans>
}
>
<Button onClick={() => ModalActionCreators.push(modal(() => <ClaimAccountModal />))}>
<Trans>Claim Account</Trans>
</Button>
</SettingsTabSection>
);
}
return (
<>
<SettingsTabSection
title={<Trans>Two-Factor Authentication</Trans>}
description={<Trans>Add an extra layer of security to your account</Trans>}
>
<div className={styles.row}>
<div className={styles.rowContent}>
<div className={styles.label}>
<Trans>Authenticator App</Trans>
</div>
<div className={styles.description}>
{hasTotpMfa ? (
<Trans>Two-factor authentication is enabled</Trans>
) : (
<Trans>Use an authenticator app to generate codes for two-factor authentication</Trans>
)}
</div>
</div>
{hasTotpMfa ? (
<Button
variant="danger-secondary"
small={true}
onClick={() => ModalActionCreators.push(modal(() => <MfaTotpDisableModal />))}
>
<Trans>Disable</Trans>
</Button>
) : (
<Button small={true} onClick={() => ModalActionCreators.push(modal(() => <MfaTotpEnableModal />))}>
<Trans>Enable</Trans>
</Button>
)}
</div>
{hasTotpMfa && (
<div className={styles.divider}>
<div className={styles.row}>
<div className={styles.rowContent}>
<div className={styles.label}>
<Trans>Backup Codes</Trans>
</div>
<div className={styles.description}>
<Trans>View and manage your backup codes for account recovery</Trans>
</div>
</div>
<Button
variant="secondary"
small={true}
onClick={() => ModalActionCreators.push(modal(() => <BackupCodesViewModal />))}
>
<Trans>View Codes</Trans>
</Button>
</div>
</div>
)}
</SettingsTabSection>
<SettingsTabSection
title={<Trans>Passkeys</Trans>}
description={<Trans>Use passkeys for passwordless sign-in and two-factor authentication</Trans>}
>
<div className={styles.row}>
<div className={styles.rowContent}>
<div className={styles.label}>
<Trans>Registered Passkeys</Trans>
</div>
<div className={styles.description}>
<Plural
value={passkeys.length}
_0="No passkeys registered"
one="# passkey registered (max 10)"
other="# passkeys registered (max 10)"
/>
</div>
</div>
<Button small={true} disabled={loadingPasskeys || passkeys.length >= 10} onClick={handleAddPasskey}>
<Trans>Add Passkey</Trans>
</Button>
</div>
{passkeys.length > 0 && (
<div className={styles.divider}>
<div className={styles.passkeyList}>
{passkeys.map((passkey) => {
const createdDate = DateUtils.getRelativeDateString(new Date(passkey.created_at), i18n);
const lastUsedDate = passkey.last_used_at
? DateUtils.getRelativeDateString(new Date(passkey.last_used_at), i18n)
: null;
return (
<div key={passkey.id} className={styles.passkeyItem}>
<div className={styles.passkeyInfo}>
<div className={styles.passkeyName}>{passkey.name}</div>
<div className={styles.passkeyDetails}>
{lastUsedDate ? (
<Trans>
Added: {createdDate} Last used: {lastUsedDate}
</Trans>
) : (
<Trans>Added: {createdDate}</Trans>
)}
</div>
</div>
<div className={styles.passkeyActions}>
<Button variant="secondary" small={true} onClick={() => handleRenamePasskey(passkey.id)}>
<Trans>Rename</Trans>
</Button>
<Button variant="danger-secondary" small={true} onClick={() => handleDeletePasskey(passkey.id)}>
<Trans>Delete</Trans>
</Button>
</div>
</div>
);
})}
</div>
</div>
)}
</SettingsTabSection>
{user.mfaEnabled && (
<>
<SettingsTabSection
title={<Trans>Phone Number</Trans>}
description={<Trans>Manage your phone number for SMS two-factor authentication</Trans>}
>
<div className={styles.row}>
<div className={styles.rowContent}>
<div className={styles.label}>
<Trans>Phone Number</Trans>
</div>
<div className={styles.description}>
{user.phone ? (
<Trans>Phone number added: {user.phone}</Trans>
) : (
<Trans>Add a phone number to enable SMS two-factor authentication</Trans>
)}
</div>
</div>
{user.phone ? (
<Button
variant="danger-secondary"
small={true}
onClick={() => {
ModalActionCreators.push(
modal(() => (
<ConfirmModal
title={t`Remove Phone Number`}
description={
<>
<div>
<Trans>
Are you sure you want to remove your phone number <strong>{user.phone}</strong>?
</Trans>
</div>
{hasSmsMfa && (
<div>
<Trans>
<strong>Warning:</strong> This will also disable SMS two-factor authentication.
</Trans>
</div>
)}
</>
}
primaryText={t`Remove Phone`}
primaryVariant="danger-primary"
onPrimary={async () => {
try {
await UserActionCreators.removePhone();
} catch (error) {
console.error('Failed to remove phone', error);
}
}}
/>
)),
);
}}
>
<Trans>Remove</Trans>
</Button>
) : (
<Button small={true} onClick={() => ModalActionCreators.push(modal(() => <PhoneAddModal />))}>
<Trans>Add Phone</Trans>
</Button>
)}
</div>
</SettingsTabSection>
{user.phone && (
<SettingsTabSection
title={<Trans>SMS Two-Factor Authentication</Trans>}
description={<Trans>Receive verification codes via SMS as a backup authentication method</Trans>}
>
<div className={styles.row}>
<div className={styles.rowContent}>
<div className={styles.label}>
<Trans>SMS Backup</Trans>
</div>
<div className={styles.description}>
{hasSmsMfa ? (
<Trans>SMS two-factor authentication is enabled</Trans>
) : (
<Trans>Enable SMS codes as a backup for your authenticator app</Trans>
)}
</div>
</div>
{hasSmsMfa ? (
<Button
variant="danger-secondary"
small={true}
disabled={disablingSmsMfa}
onClick={handleDisableSmsMfa}
>
<Trans>Disable</Trans>
</Button>
) : isSmsMfaDisabledForUser ? (
<Tooltip text={t`SMS backup is disabled for partners`}>
<div>
<Button small={true} disabled={true}>
<Trans>Enable</Trans>
</Button>
</div>
</Tooltip>
) : (
<Button small={true} disabled={enablingSmsMfa} onClick={handleEnableSmsMfa}>
<Trans>Enable</Trans>
</Button>
)}
</div>
</SettingsTabSection>
)}
</>
)}
</>
);
},
);