Skip to content

Commit

Permalink
Merge branch 'master' into project-name-sync
Browse files Browse the repository at this point in the history
  • Loading branch information
Twixes authored Nov 29, 2024
2 parents c6f7f86 + 66592c7 commit a447c92
Show file tree
Hide file tree
Showing 14 changed files with 348 additions and 66 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion frontend/src/layout/ErrorProjectUnavailable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Link } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { PageHeader } from 'lib/components/PageHeader'
import { useEffect, useState } from 'react'
import { CreateOrganizationModal } from 'scenes/organization/CreateOrganizationModal'
import { urls } from 'scenes/urls'
import { userLogic } from 'scenes/userLogic'

Expand Down Expand Up @@ -42,7 +43,9 @@ export function ErrorProjectUnavailable(): JSX.Element {
return (
<div>
<PageHeader />
{user?.team && !user.organization?.teams.some((team) => team.id === user?.team?.id) ? (
{!user?.organization ? (
<CreateOrganizationModal isVisible inline />
) : user?.team && !user.organization?.teams.some((team) => team.id === user?.team?.id) ? (
<>
<h1>Project access has been removed</h1>
<p>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ export const FEATURE_FLAGS = {
EXPERIMENTS_MIGRATION_DISABLE_UI: 'experiments-migration-disable-ui', // owner: @jurajmajerik #team-experiments
CUSTOM_CSS_THEMES: 'custom-css-themes', // owner: @daibhin
WEB_ANALYTICS_WARN_CUSTOM_EVENT_NO_SESSION: 'web-analytics-warn-custom-event-no-session', // owner: @robbie-c #team-web-analytics
TWO_FACTOR_UI: 'two-factor-ui', // owner: @zach
} as const
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/authentication/Setup2FA.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function Setup2FA({ onSuccess }: { onSuccess: () => void }): JSX.Element
/>
</LemonField>
<LemonButton htmlType="submit" data-attr="2fa-setup" fullWidth type="primary" center loading={false}>
Login
Submit
</LemonButton>
</Form>
</>
Expand Down
18 changes: 1 addition & 17 deletions frontend/src/scenes/authentication/login2FALogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import api from 'lib/api'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic'

import { SSOProvider } from '~/types'

import type { login2FALogicType } from './login2FALogicType'
import { handleLoginRedirect } from './loginLogic'

Expand All @@ -15,17 +13,6 @@ export interface AuthenticateResponseType {
errorDetail?: string
}

export interface PrecheckResponseType {
sso_enforcement?: SSOProvider | null
saml_available: boolean
status: 'pending' | 'completed'
}

export interface LoginForm {
email: string
password: string
}

export interface TwoFactorForm {
token: string
}
Expand All @@ -36,7 +23,6 @@ export enum LoginStep {
}

export const login2FALogic = kea<login2FALogicType>([
//<login2FALogicType>([
path(['scenes', 'authentication', 'login2FALogic']),
connect({
values: [preflightLogic, ['preflight'], featureFlagLogic, ['featureFlags']],
Expand All @@ -47,7 +33,6 @@ export const login2FALogic = kea<login2FALogicType>([
clearGeneralError: true,
}),
reducers({
// This is separate from the login form, so that the form can be submitted even if a general error is present
generalError: [
null as { code: string; detail: string } | null,
{
Expand All @@ -71,8 +56,7 @@ export const login2FALogic = kea<login2FALogicType>([
try {
return await api.create('api/login/token', { token })
} catch (e) {
const { code } = e as Record<string, any>
const { detail } = e as Record<string, any>
const { code, detail } = e as Record<string, any>
actions.setGeneralError(code, detail)
throw e
}
Expand Down
79 changes: 72 additions & 7 deletions frontend/src/scenes/authentication/setup2FALogic.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { lemonToast } from '@posthog/lemon-ui'
import { actions, afterMount, connect, kea, listeners, path, props, reducers } from 'kea'
import { actions, afterMount, connect, kea, listeners, path, props, reducers, selectors } from 'kea'
import { forms } from 'kea-forms'
import { loaders } from 'kea-loaders'
import api from 'lib/api'
Expand All @@ -12,6 +12,12 @@ export interface TwoFactorForm {
token: number | null
}

export interface TwoFactorStatus {
is_enabled: boolean
backup_codes: string[]
method: string | null
}

export interface Setup2FALogicProps {
onSuccess?: () => void
}
Expand All @@ -26,16 +32,36 @@ export const setup2FALogic = kea<setup2FALogicType>([
setGeneralError: (code: string, detail: string) => ({ code, detail }),
clearGeneralError: true,
setup: true,
loadStatus: true,
generateBackupCodes: true,
}),
reducers({
// This is separate from the login form, so that the form can be submitted even if a general error is present
generalError: [
null as { code: string; detail: string } | null,
{
setGeneralError: (_, error) => error,
clearGeneralError: () => null,
},
],
status: [
null as TwoFactorStatus | null,
{
loadStatusSuccess: (_, { status }) => status,
generateBackupCodesSuccess: (state, { generatingCodes }) => {
if (!state) {
return null
}
return {
...state,
// Fallback to current backup codes if generating codes fails
backup_codes: generatingCodes?.backup_codes || state.backup_codes,
}
},
},
],
}),
selectors({
is2FAEnabled: [(s) => [s.status], (status): boolean => !!status?.is_enabled],
}),
loaders(() => ({
startSetup: [
Expand All @@ -48,6 +74,36 @@ export const setup2FALogic = kea<setup2FALogicType>([
},
},
],
status: [
null as TwoFactorStatus | null,
{
loadStatus: async () => {
return await api.get('api/users/@me/two_factor_status/')
},
},
],
generatingCodes: [
null as { backup_codes: string[] } | null,
{
generateBackupCodes: async () => {
return await api.create('api/users/@me/two_factor_backup_codes/')
},
},
],
disable2FA: [
false,
{
disable2FA: async () => {
try {
await api.create('api/users/@me/two_factor_disable/')
return true
} catch (e) {
const { code, detail } = e as Record<string, any>
throw { code, detail }
}
},
},
],
})),
forms(({ actions }) => ({
token: {
Expand All @@ -60,20 +116,29 @@ export const setup2FALogic = kea<setup2FALogicType>([
try {
return await api.create('api/users/@me/validate_2fa/', { token })
} catch (e) {
const { code } = e as Record<string, any>
const { detail } = e as Record<string, any>
const { code, detail } = e as Record<string, any>
actions.setGeneralError(code, detail)
throw e
}
},
},
})),
listeners(({ props }) => ({
listeners(({ props, actions }) => ({
submitTokenSuccess: () => {
lemonToast.success('2FA method added successfully')
props.onSuccess && props.onSuccess()
actions.loadStatus()
props.onSuccess?.()
},
disable2FASuccess: () => {
lemonToast.success('2FA disabled successfully')
},
generateBackupCodesSuccess: () => {
lemonToast.success('Backup codes generated successfully')
},
})),

afterMount(({ actions }) => actions.setup()),
afterMount(({ actions }) => {
actions.setup()
actions.loadStatus()
}),
])
122 changes: 107 additions & 15 deletions frontend/src/scenes/settings/user/TwoFactorAuthentication.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,145 @@
import { IconCheckCircle, IconWarning } from '@posthog/icons'
import { IconCheckCircle, IconCopy, IconWarning } from '@posthog/icons'
import { LemonButton, LemonModal } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { FEATURE_FLAGS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { copyToClipboard } from 'lib/utils/copyToClipboard'
import { useState } from 'react'
import { Setup2FA } from 'scenes/authentication/Setup2FA'
import { setup2FALogic } from 'scenes/authentication/setup2FALogic'
import { membersLogic } from 'scenes/organization/membersLogic'
import { userLogic } from 'scenes/userLogic'

export function TwoFactorAuthentication(): JSX.Element {
const { user } = useValues(userLogic)
const { updateUser } = useActions(userLogic)
const { loadMemberUpdates } = useActions(membersLogic)
const [modalVisible, setModalVisible] = useState(false)
const [setupModalVisible, setSetupModalVisible] = useState(false)
const [disableModalVisible, setDisableModalVisible] = useState(false)
const [backupCodesModalVisible, setBackupCodesModalVisible] = useState(false)
const { status } = useValues(setup2FALogic)
const { generateBackupCodes, disable2FA } = useActions(setup2FALogic)
const { featureFlags } = useValues(featureFlagLogic)

const handleSuccess = (): void => {
updateUser({})
loadMemberUpdates()
}

return (
<div className="flex flex-col items-start">
{modalVisible && (
<LemonModal title="Set up or manage 2FA" onClose={() => setModalVisible(false)}>
<>
{setupModalVisible && (
<LemonModal title="Set up 2FA" onClose={() => setSetupModalVisible(false)}>
<div className="max-w-xl">
<b>
Use an authenticator app like Google Authenticator or 1Password to scan the QR code below.
</b>
<Setup2FA
onSuccess={() => {
setModalVisible(false)
updateUser({})
loadMemberUpdates()
setSetupModalVisible(false)
handleSuccess()
}}
/>
</>
</div>
</LemonModal>
)}

{disableModalVisible && (
<LemonModal
title="Disable 2FA"
onClose={() => setDisableModalVisible(false)}
footer={
<>
<LemonButton onClick={() => setDisableModalVisible(false)}>Cancel</LemonButton>
<LemonButton
type="primary"
status="danger"
onClick={() => {
disable2FA()
setDisableModalVisible(false)
handleSuccess()
}}
>
Disable 2FA
</LemonButton>
</>
}
>
<p>
Are you sure you want to disable two-factor authentication? This will make your account less
secure.
</p>
</LemonModal>
)}

{backupCodesModalVisible && (
<LemonModal title="Backup Codes" onClose={() => setBackupCodesModalVisible(false)}>
<div className="space-y-4 max-w-md">
<p>
Save these backup codes in a secure location. Each code can only be used once to sign in if
you lose access to your authentication device.
</p>
{status?.backup_codes?.length ? (
<div className="bg-bg-3000 p-4 rounded font-mono space-y-1 relative">
<LemonButton
icon={<IconCopy />}
size="small"
className="absolute top-4 right-4"
onClick={() => {
void copyToClipboard(status.backup_codes.join('\n') || '', 'backup codes')
}}
>
Copy
</LemonButton>
{status.backup_codes.map((code) => (
<div key={code}>{code}</div>
))}
</div>
) : (
<div className="text-center">
<p className="text-muted mb-2">No backup codes available</p>
</div>
)}
<LemonButton
type="primary"
onClick={() => {
generateBackupCodes()
}}
>
{status?.backup_codes?.length ? 'Generate New Codes' : 'Generate Backup Codes'}
</LemonButton>
</div>
</LemonModal>
)}

{user?.is_2fa_enabled ? (
<>
<div className="mb-2 flex items-center">
<IconCheckCircle color="green" className="text-xl mr-2" />
<span className="font-medium">2FA enabled.</span>
<span className="font-medium">2FA enabled</span>
</div>
<LemonButton type="primary" to="/account/two_factor/" targetBlank>
Manage or disable 2FA
</LemonButton>
{featureFlags[FEATURE_FLAGS.TWO_FACTOR_UI] ? (
<div className="space-x-2 flex items-center">
<LemonButton type="secondary" onClick={() => setBackupCodesModalVisible(true)}>
View Backup Codes
</LemonButton>
<LemonButton type="secondary" status="danger" onClick={() => setDisableModalVisible(true)}>
Disable 2FA
</LemonButton>
</div>
) : (
<LemonButton type="primary" to="/account/two_factor/" targetBlank>
Manage or disable 2FA
</LemonButton>
)}
</>
) : (
<div>
<div className="mb-2 flex items-center">
<IconWarning color="orange" className="text-xl mr-2" />
<span className="font-medium">2FA is not enabled.</span>
<span className="font-medium">2FA is not enabled</span>
</div>
<LemonButton type="primary" onClick={() => setModalVisible(true)}>
<LemonButton type="primary" onClick={() => setSetupModalVisible(true)}>
Set up 2FA
</LemonButton>
</div>
Expand Down
Loading

0 comments on commit a447c92

Please sign in to comment.