Skip to content

Commit

Permalink
feat: add okta login method (#1513)
Browse files Browse the repository at this point in the history
* feat(auth): add okta settings

* feat(auth): handle login with okta

fix(auth): update following new design

* refactor: improve addValuesToState util

refactor(util): write tests

* feat(auth): handle okta invitation

fix(invitation): add error message

* feat: add translations

fix: codegen

fix: linter

fix: translations

fix: translations

* fix(access): update premium check to include integrations

* fix: codegen

* fix(Yup): add method for domain

* fix(Settings): add permission for auth page
  • Loading branch information
keellyp committed May 28, 2024
1 parent 972f030 commit 06cd5f2
Show file tree
Hide file tree
Showing 27 changed files with 1,939 additions and 65 deletions.
42 changes: 39 additions & 3 deletions ditto/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,44 @@
"text_649c54823c9089006247625a": "Can’t be prorated due to {{chargeModel}} charge model defined above.",
"text_649c49bcebd91c0082d84446": "Full charge",
"text_649c54823c90890062476259": "Based on the event timestamp and end date of the billing period.",
"text_664c732c264d7eed1c74fd96": "Authentication",
"text_664c732c264d7eed1c74fd9c": "Manage how to authenticate to your Lago organization.",
"text_664c732c264d7eed1c74fda2": "Okta",
"text_664c732c264d7eed1c74fda8": "Allows logins using SSO by connecting Lago and Okta.",
"text_664c732c264d7eed1c74fdb4": "{{integration}} integration successfully deleted",
"text_664c732c264d7eed1c74fd88": "Connect Lago and Okta",
"text_664c732c264d7eed1c74fd8e": "To connect to Okta, please enter the following information",
"text_664c732c264d7eed1c74fd94": "Domain name",
"text_664c732c264d7eed1c74fd9a": "Type your domain name",
"text_664c732c264d7eed1c74fda0": "e.g. acme.com",
"text_664c732c264d7eed1c74fda6": "Okta public key",
"text_664c732c264d7eed1c74fdac": "Type your Okta public key",
"text_664c732c264d7eed1c74fdb2": "Okta private key",
"text_664c732c264d7eed1c74fdb7": "Type your Okta private key",
"text_664c732c264d7eed1c74fdbb": "Okta organization name",
"text_664c732c264d7eed1c74fdbf": "Type your organization name",
"text_664c732c264d7eed1c74fdc3": ".okta.com",
"text_664c732c264d7eed1c74fdcb": "Connect to Okta",
"text_664c732c264d7eed1c74fdaa": "Edit connection",
"text_664c732c264d7eed1c74fdb0": "Delete connection",
"text_664c732c264d7eed1c74fdbd": "Identity provider",
"text_664c732c264d7eed1c74fdc5": "Connection details",
"text_664c732c264d7eed1c74fde6": "{{integration}} integration successfully connected",
"text_664c732c264d7eed1c74fde8": "{{integration}} integration successfully edited",
"text_664c732c264d7eed1c74fe03": "Please fill this input with a domain format to move forward",
"text_664c8fa719b5e7ad81c86018": "Edit Okta connection",
"text_664c8fa719b5e7ad81c86019": "To edit Okta connection, please edit the following information",
"text_664c900d2d312a01546bd84b": "Delete connection to Okta",
"text_664c900d2d312a01546bd84c": "By deleting the connection, it will not be used anymore and you’ll be not able to access to your Lago organization via Okta SSO. Are you sure?",
"text_664c90c9b2b6c2012aa50bce": "Log In with Okta",
"text_664c90c9b2b6c2012aa50bd0": "Please log in with your enterprise account",
"text_664c90c9b2b6c2012aa50bd6": "Okta provider was not assigned for this domain, please contact an admin.",
"text_664c90c9b2b6c2012aa50bda": "Log In with another method? <a data-text=\"Log In\" href=\"{{linkLogin}}\">-</a>",
"text_664c90c9b2b6c2012aa50bcd": "Join {{orgnisationName}}",
"text_664c90c9b2b6c2012aa50bd1": "Oops! Looks like your Okta provider was not assigned for this domain, please contact an admin.",
"text_664c90c9b2b6c2012aa50bd3": "Join with Google",
"text_664c90c9b2b6c2012aa50bd5": "Join with Okta",
"text_664c98989d08a3f733357f73": "There was an error while fetching user info from Okta. Please try again.",
"text_66141e30699a0631f0b2ec7f": "There is no invoices with dispute lost",
"text_66141e30699a0631f0b2ec87": "Disputed lost invoices occur when a customer chooses not to pay, requests a dispute, and demands a chargeback.",
"text_66141e30699a0631f0b2ec59": "Mark this invoice as disputed",
Expand Down Expand Up @@ -946,7 +984,7 @@
"text_63208bfc99e69a28211ec7fd": "Show more invitations",
"text_63208c711ce25db78140755d": "Member successfully removed",
"text_63208c701ce25db78140748f": "Invite members",
"text_63208c701ce25db78140749b": "To generate a link for inviting a member to this organization, please enter the new member’s email.",
"text_63208c701ce25db78140749b": "To generate a link for inviting a member to this organization, please enter the member’s email and select a role.",
"text_63208c701ce25db7814074ab": "Email address",
"text_63208c711ce25db7814074c1": "[email protected]",
"text_63208c711ce25db7814074cd": "Cancel",
Expand All @@ -968,7 +1006,6 @@
"text_6321a076b94bd1b32494e9ee": "Something went wrong",
"text_6321a076b94bd1b32494e9f0": "We can’t fetch the members list, please refresh the section or contact us if the error persists.",
"text_6321a076b94bd1b32494e9f2": "Refresh the section",
"text_63246f875e2228ab7b63dcd0": "Sign up to {{orgnisationName}}",
"text_63246f875e2228ab7b63dcd4": "Create your account and access to this organization.",
"text_63246f875e2228ab7b63dcdc": "Email address",
"text_63246f875e2228ab7b63dce9": "Set your password",
Expand All @@ -991,7 +1028,6 @@
"text_660bf95c75dd928ced0ecb21": "Continue with Google",
"text_660bf95c75dd928ced0ecb33": "Type your organization name",
"text_660bf95c75dd928ced0ecb2b": "Oops! Looks like your Google account email doesn't match the invitation. Give it another shot!",
"text_660bf95c75dd928ced0ecb37": "Sign up with Google",
"text_660bfaa2cbc95800a63f48b1": "Sorry, it seems there are no accounts associated with the provided credentials.",
"text_642707b0da1753a9bb6672b5": "Forgot password",
"text_642707b0da1753a9bb66728e": "Password reset email sent!",
Expand Down
3 changes: 3 additions & 0 deletions ditto/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -358,5 +358,8 @@ sources:
fileName: 👍 [Ready for dev] - Wallets - Real time prepaid credit improvements
- name: 👍 [Ready for dev] - B.Metrics | Plans - Custom aggregation | price
id: 663dea541d89e5df98fbc05b
fileName: 👍 [Ready for dev] - B.Metrics | Plans - Custom aggregation | price
- name: ⚙️ [WIP] - Onboarding - Log In | Join Lago via OKTA SSO
id: 664c7329c182a2ec5807ffec
format: flat
variants: true
4 changes: 4 additions & 0 deletions ditto/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ const wip___customers___subscription_on_anniversary_date = require('./wip---cust
const wip___general___fe_environment_infos = require('./wip---general---fe-environment-infos__base.json');
const wip___integration___lago_eu_tax_integration = require('./wip---integration---lago-eu-tax-integration__base.json');
const wip___invoices___dispute_payment_intent = require('./wip---invoices---dispute-payment-intent__base.json');
const wip___onboarding___log_in_join_lago_via_okta_sso = require('./wip---onboarding---log-in-join-lago-via-okta-sso__base.json');
const wip___plan___charges_paid_in_advance = require('./wip---plan---charges-paid-in-advance__base.json');
const wip___settings___create_tax_rate_object_apply_on_org_cus = require('./wip---settings---create-tax-rate-object-apply-on-org-cus__base.json');
const wip___settings___define_preferred_doc_language_generation = require('./wip---settings---define-preferred-doc-language-generation__base.json');
Expand Down Expand Up @@ -430,6 +431,9 @@ module.exports = {
"project_66141e2ffa16c75cb553dbc1": {
"base": {...wip___invoices___dispute_payment_intent}
},
"project_664c7329c182a2ec5807ffec": {
"base": {...wip___onboarding___log_in_join_lago_via_okta_sso}
},
"project_646e2d05cf47b79ad4b5ccf5": {
"base": {...wip___plan___charges_paid_in_advance}
},
Expand Down
10 changes: 7 additions & 3 deletions src/components/auth/GoogleAuthButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,13 @@ const GoogleAuthButton = ({
}

if (data?.googleAuthUrl?.url) {
window.location.href = addValuesToUrlState(data.googleAuthUrl.url, {
mode,
...(!!invitationToken && { invitationToken }),
window.location.href = addValuesToUrlState({
url: data.googleAuthUrl.url,
stateType: 'object',
values: {
mode,
...(!!invitationToken && { invitationToken }),
},
})
}
}}
Expand Down
2 changes: 2 additions & 0 deletions src/components/designSystem/Icon/mapping.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import Mail from '~/public/icons/mail.svg'
import Map from '~/public/icons/map.svg'
import Micro from '~/public/icons/micro.svg'
import Minus from '~/public/icons/minus.svg'
import Okta from '~/public/icons/okta.svg'
import Outside from '~/public/icons/outside.svg'
import Paperclip from '~/public/icons/paperclip.svg'
import PauseCircleFilled from '~/public/icons/pause-circle-filled.svg'
Expand Down Expand Up @@ -184,6 +185,7 @@ export const ALL_ICONS = {
map: Map,
micro: Micro,
minus: Minus,
okta: Okta,
outside: Outside,
paperclip: Paperclip,
'pause-circle-filled': PauseCircleFilled,
Expand Down
154 changes: 154 additions & 0 deletions src/components/settings/authentication/AddOktaDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { InputAdornment, Stack } from '@mui/material'
import { forwardRef, RefObject, useImperativeHandle, useRef, useState } from 'react'
import styled from 'styled-components'

import { Button, Dialog, DialogRef } from '~/components/designSystem'
import { TextInputField } from '~/components/form'
import { DeleteOktaIntegrationDialogRef } from '~/components/settings/authentication/DeleteOktaIntegrationDialog'
import {
useOktaIntegration,
UseOktaIntegrationProps,
} from '~/components/settings/authentication/useOktaIntegration'
import { AddOktaIntegrationDialogFragment } from '~/generated/graphql'
import { useInternationalization } from '~/hooks/core/useInternationalization'
import { theme } from '~/styles'

type AddOktaDialogProps = Partial<{
integration: AddOktaIntegrationDialogFragment
deleteModalRef: RefObject<DeleteOktaIntegrationDialogRef>
deleteDialogCallback: Function
callback?: UseOktaIntegrationProps['onSubmit']
}>

export interface AddOktaDialogRef {
openDialog: (props?: AddOktaDialogProps) => unknown
closeDialog: () => unknown
}

export const AddOktaDialog = forwardRef<AddOktaDialogRef>((_, ref) => {
const { translate } = useInternationalization()

const dialogRef = useRef<DialogRef>(null)
const [localData, setLocalData] = useState<AddOktaDialogProps | undefined>()

const integration = localData?.integration
const isEdition = !!integration

const { formikProps } = useOktaIntegration({
initialValues: integration,
onSubmit: (id) => {
localData?.callback?.(id)
dialogRef.current?.closeDialog()
},
})

useImperativeHandle(ref, () => ({
openDialog: (data) => {
setLocalData(data)
dialogRef.current?.openDialog()
},
closeDialog: () => dialogRef.current?.closeDialog(),
}))

return (
<>
<Dialog
ref={dialogRef}
title={translate(
isEdition ? 'text_664c8fa719b5e7ad81c86018' : 'text_664c732c264d7eed1c74fd88',
)}
description={translate(
isEdition ? 'text_664c8fa719b5e7ad81c86019' : 'text_664c732c264d7eed1c74fd8e',
)}
onClose={formikProps.resetForm}
actions={({ closeDialog }) => (
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
width={isEdition ? '100%' : 'inherit'}
spacing={3}
>
{isEdition && localData?.deleteDialogCallback && (
<Button
danger
variant="quaternary"
onClick={() => {
closeDialog()
localData?.deleteModalRef?.current?.openDialog({
integration,
callback: localData.deleteDialogCallback,
})
}}
>
{translate('text_65845f35d7d69c3ab4793dad')}
</Button>
)}

<Stack direction="row" spacing={3} alignItems="center" marginLeft="auto !important">
<Button variant="quaternary" onClick={closeDialog}>
{translate('text_63eba8c65a6c8043feee2a14')}
</Button>
<Button
variant="primary"
disabled={!formikProps.isValid || !formikProps.dirty}
onClick={formikProps.submitForm}
>
{translate(
isEdition ? 'text_664c732c264d7eed1c74fdaa' : 'text_664c732c264d7eed1c74fdcb',
)}
</Button>
</Stack>
</Stack>
)}
>
<Content>
<TextInputField
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
formikProps={formikProps}
name="domain"
label={translate('text_664c732c264d7eed1c74fd94')}
placeholder={translate('text_664c732c264d7eed1c74fd9a')}
helperText={translate('text_664c732c264d7eed1c74fda0')}
/>
<TextInputField
formikProps={formikProps}
name="clientId"
label={translate('text_664c732c264d7eed1c74fda6')}
placeholder={translate('text_664c732c264d7eed1c74fdac')}
/>
<TextInputField
formikProps={formikProps}
name="clientSecret"
label={translate('text_664c732c264d7eed1c74fdb2')}
placeholder={translate('text_664c732c264d7eed1c74fdb7')}
/>
<TextInputField
formikProps={formikProps}
name="organizationName"
label={translate('text_664c732c264d7eed1c74fdbb')}
placeholder={translate('text_664c732c264d7eed1c74fdbf')}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{translate('text_664c732c264d7eed1c74fdc3')}
</InputAdornment>
),
}}
/>
</Content>
</Dialog>
</>
)
})

const Content = styled.div`
margin-bottom: ${theme.spacing(8)};
> *:not(:last-child) {
margin-bottom: ${theme.spacing(6)};
}
`

AddOktaDialog.displayName = 'AddOktaDialog'
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { gql } from '@apollo/client'
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'

import { WarningDialog, WarningDialogRef } from '~/components/WarningDialog'
import { addToast } from '~/core/apolloClient'
import {
DeleteOktaIntegrationDialogFragment,
useDestroyIntegrationMutation,
} from '~/generated/graphql'
import { useInternationalization } from '~/hooks/core/useInternationalization'

gql`
fragment DeleteOktaIntegrationDialog on OktaIntegration {
id
name
}
mutation DestroyIntegration($input: DestroyIntegrationInput!) {
destroyIntegration(input: $input) {
id
}
}
`

type DeleteOktaIntegrationDialogProps = {
integration: DeleteOktaIntegrationDialogFragment | undefined
callback?: Function
}

export interface DeleteOktaIntegrationDialogRef {
openDialog: ({ integration, callback }: DeleteOktaIntegrationDialogProps) => unknown
closeDialog: () => unknown
}

export const DeleteOktaIntegrationDialog = forwardRef<DeleteOktaIntegrationDialogRef>((_, ref) => {
const { translate } = useInternationalization()

const dialogRef = useRef<WarningDialogRef>(null)
const [localData, setLocalData] = useState<DeleteOktaIntegrationDialogProps>()

const integration = localData?.integration

const [deleteIntegration] = useDestroyIntegrationMutation({
onCompleted(data) {
if (data && data.destroyIntegration) {
dialogRef.current?.closeDialog()
localData?.callback?.()

addToast({
message: translate('text_664c732c264d7eed1c74fdb4', {
integration: translate('text_664c732c264d7eed1c74fda2'),
}),
severity: 'success',
})
}
},
update(cache) {
cache.evict({ id: `OktaIntegration:${integration?.id}` })
},
})

useImperativeHandle(ref, () => ({
openDialog: (data) => {
setLocalData(data)
dialogRef.current?.openDialog()
},
closeDialog: () => dialogRef.current?.closeDialog(),
}))

return (
<WarningDialog
ref={dialogRef}
title={translate('text_664c900d2d312a01546bd84b')}
description={translate('text_664c900d2d312a01546bd84c')}
onContinue={async () =>
await deleteIntegration({
variables: {
input: {
id: integration?.id ?? '',
},
},
})
}
continueText={translate('text_645d071272418a14c1c76a81')}
/>
)
})

DeleteOktaIntegrationDialog.displayName = 'DeleteOktaIntegrationDialog'
Loading

0 comments on commit 06cd5f2

Please sign in to comment.