diff --git a/web/packages/build/package.json b/web/packages/build/package.json index 60e0b0b31d2dc..4767f347684e7 100644 --- a/web/packages/build/package.json +++ b/web/packages/build/package.json @@ -87,6 +87,7 @@ "react-dom": "^18.2.0", "react-is": "^16.8.0", "react-refresh": "^0.14.0", + "react-select-event": "^5.5.1", "react-test-renderer": "^18.2.0", "react-transition-group": "^4.4.2", "rollup-plugin-visualizer": "^5.9.0", diff --git a/web/packages/teleport/src/Account/ManageDevices/AddAuthDeviceWizard/AddAuthDeviceWizard.tsx b/web/packages/teleport/src/Account/ManageDevices/AddAuthDeviceWizard/AddAuthDeviceWizard.tsx index a31e389846f7d..4c05043708175 100644 --- a/web/packages/teleport/src/Account/ManageDevices/AddAuthDeviceWizard/AddAuthDeviceWizard.tsx +++ b/web/packages/teleport/src/Account/ManageDevices/AddAuthDeviceWizard/AddAuthDeviceWizard.tsx @@ -21,7 +21,6 @@ import Box from 'design/Box'; import { ButtonPrimary, ButtonSecondary } from 'design/Button'; import Dialog from 'design/Dialog'; import Flex from 'design/Flex'; -import * as Icon from 'design/Icon'; import Image from 'design/Image'; import Indicator from 'design/Indicator'; import { RadioGroup } from 'design/RadioGroup'; @@ -35,7 +34,8 @@ import { useAsync } from 'shared/hooks/useAsync'; import useAttempt from 'shared/hooks/useAttemptNext'; import { Auth2faType } from 'shared/services'; import createMfaOptions, { MfaOption } from 'shared/utils/createMfaOptions'; -import styled from 'styled-components'; + +import { PasskeyIcons } from 'teleport/components/PasskeyIcons'; import { DialogHeader } from 'teleport/Account/DialogHeader'; import useReAuthenticate from 'teleport/components/ReAuthenticate/useReAuthenticate'; @@ -517,18 +517,7 @@ function PasskeyBlurb() { borderRadius={3} p={3} > - - - - - - - - - - - - +

Teleport supports passkeys, a password replacement that validates your identity using touch, facial recognition, a device password, or a PIN. @@ -540,12 +529,3 @@ function PasskeyBlurb() { ); } - -const OverlappingChip = styled.span` - display: inline-block; - background: ${props => props.theme.colors.levels.surface}; - border: ${props => props.theme.borders[1]}; - border-color: ${props => props.theme.colors.interactive.tonal.neutral[2]}; - border-radius: 50%; - margin-right: -6px; -`; diff --git a/web/packages/teleport/src/Login/Login.test.tsx b/web/packages/teleport/src/Login/Login.test.tsx index 6bb8318933214..4a3247783f7d0 100644 --- a/web/packages/teleport/src/Login/Login.test.tsx +++ b/web/packages/teleport/src/Login/Login.test.tsx @@ -17,6 +17,8 @@ */ import React from 'react'; +import { userEvent, UserEvent } from '@testing-library/user-event'; +import selectEvent from 'react-select-event'; import { render, fireEvent, screen, waitFor } from 'design/utils/testing'; import auth from 'teleport/services/auth/auth'; @@ -25,10 +27,13 @@ import cfg from 'teleport/config'; import { Login } from './Login'; +let user: UserEvent; + beforeEach(() => { jest.restoreAllMocks(); jest.spyOn(history, 'push').mockImplementation(); jest.spyOn(history, 'getRedirectParam').mockImplementation(() => '/'); + user = userEvent.setup(); }); test('basic rendering', () => { @@ -58,6 +63,34 @@ test('login with redirect', async () => { expect(history.push).toHaveBeenCalledWith('http://localhost/web', true); }); +test('login with MFA, changing method to OTP', async () => { + jest.spyOn(cfg, 'getAuth2faType').mockImplementation(() => 'optional'); + jest.spyOn(auth, 'login').mockResolvedValue(null); + + render(); + + // fill form + const username = screen.getByLabelText(/username/i); + const password = screen.getByLabelText(/password/i); + const mfaType = screen.getByLabelText(/multi-factor type/i); + await user.type(username, 'username'); + await user.type(password, '123'); + + expect( + screen.queryByLabelText(/authenticator code/i) + ).not.toBeInTheDocument(); + await selectEvent.select(mfaType, 'Authenticator App'); + const authCode = screen.getByLabelText(/authenticator code/i); + await user.type(authCode, '987654'); + + // test login and redirect + fireEvent.click(screen.getByText('Sign In')); + await waitFor(() => { + expect(auth.login).toHaveBeenCalledWith('username', '123', '987654'); + }); + expect(history.push).toHaveBeenCalledWith('http://localhost/web', true); +}); + test('login with SSO', () => { jest.spyOn(cfg, 'getAuth2faType').mockImplementation(() => 'otp'); jest.spyOn(cfg, 'getPrimaryAuthType').mockImplementation(() => 'sso'); @@ -80,6 +113,19 @@ test('login with SSO', () => { ); }); +test('passwordless login', async () => { + jest.spyOn(cfg, 'getPrimaryAuthType').mockReturnValue('passwordless'); + jest.spyOn(auth, 'loginWithWebauthn').mockResolvedValue(undefined); + + render(); + + await user.click( + screen.getByRole('button', { name: 'Sign in with a Passkey' }) + ); + expect(auth.loginWithWebauthn).toHaveBeenCalledWith(undefined); // No credentials + expect(history.push).toHaveBeenCalledWith('http://localhost/web', true); +}); + describe('test MOTD', () => { test('show motd only if motd is set', async () => { // default login form diff --git a/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx b/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx index 6da0ee11b2ad9..049cbb04fd5a8 100644 --- a/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx +++ b/web/packages/teleport/src/components/FormLogin/FormLogin.story.tsx @@ -21,7 +21,6 @@ import React from 'react'; import FormLogin, { Props } from './FormLogin'; const props: Props = { - title: 'Custom Title', attempt: { isFailed: false, isSuccess: false, @@ -74,7 +73,6 @@ export const LocalWithOnAndPwdless = () => ( export const Cloud = () => ( null} @@ -89,7 +87,7 @@ export const ServerError = () => { 'invalid credentials with looooooooooooooooooooooooooooooooong text', }; - return ; + return ; }; export const LocalWithSso = () => { @@ -98,7 +96,7 @@ export const LocalWithSso = () => { { name: 'google', type: 'oidc', url: '' } as const, ]; - return ; + return ; }; export const LocalWithSsoAndPwdless = () => { @@ -114,7 +112,6 @@ export const LocalWithSsoAndPwdless = () => { return ( @@ -130,7 +127,6 @@ export const LocalDisabledWithSso = () => { return ( @@ -138,7 +134,7 @@ export const LocalDisabledWithSso = () => { }; export const LocalDisabledNoSso = () => ( - + ); export const PrimarySso = () => { @@ -160,12 +156,7 @@ export const PrimarySso = () => { ]; return ( - + ); }; @@ -178,7 +169,6 @@ export const PrimarySsoWithPwdless = () => { return ( { return ( { return ( { return ( diff --git a/web/packages/teleport/src/components/FormLogin/FormLogin.test.tsx b/web/packages/teleport/src/components/FormLogin/FormLogin.test.tsx index 02369d39c7e28..2d8e2e16dcb55 100644 --- a/web/packages/teleport/src/components/FormLogin/FormLogin.test.tsx +++ b/web/packages/teleport/src/components/FormLogin/FormLogin.test.tsx @@ -41,7 +41,7 @@ test('primary username and password with mfa off', () => { target: { value: '123' }, }); - fireEvent.click(screen.getByText(/sign in/i)); + fireEvent.click(screen.getByRole('button', { name: /sign in/i })); expect(onLogin).toHaveBeenCalledWith('username', '123', ''); }); @@ -64,7 +64,7 @@ test('auth2faType: otp', () => { fireEvent.change(screen.getByPlaceholderText(/123 456/i), { target: { value: '456' }, }); - fireEvent.click(screen.getByText(/sign in/i)); + fireEvent.click(screen.getByRole('button', { name: /sign in/i })); expect(onLogin).toHaveBeenCalledWith('username', '123', '456'); }); @@ -91,7 +91,7 @@ test('auth2faType: webauthn', async () => { target: { value: '123' }, }); - fireEvent.click(screen.getByText(/sign in/i)); + fireEvent.click(screen.getByRole('button', { name: /sign in/i })); expect(onLoginWithWebauthn).toHaveBeenCalledWith({ username: 'username', password: '123', @@ -113,7 +113,7 @@ test('input validation error handling', async () => { /> ); - fireEvent.click(screen.getByText(/sign in/i)); + fireEvent.click(screen.getByRole('button', { name: /sign in/i })); expect(onLogin).not.toHaveBeenCalled(); expect(onLoginWithSso).not.toHaveBeenCalled(); diff --git a/web/packages/teleport/src/components/FormLogin/FormLogin.tsx b/web/packages/teleport/src/components/FormLogin/FormLogin.tsx index 4244ecdec27ab..ac0e908fce85c 100644 --- a/web/packages/teleport/src/components/FormLogin/FormLogin.tsx +++ b/web/packages/teleport/src/components/FormLogin/FormLogin.tsx @@ -26,8 +26,9 @@ import { ButtonPrimary, Box, ButtonText, + ButtonSecondary, + Button, } from 'design'; -import { Key, ArrowForward } from 'design/Icon'; import * as Alerts from 'design/Alert'; import { AuthProvider, @@ -48,67 +49,63 @@ import { StepSlider, StepComponentProps } from 'design/StepSlider'; import { UserCredentials } from 'teleport/services/auth'; +import { PasskeyIcons } from '../PasskeyIcons'; + import SSOButtonList from './SsoButtons'; +const allAuthTypes: PrimaryAuthType[] = ['passwordless', 'sso', 'local']; + export default function LoginForm(props: Props) { const { - title, attempt, isLocalAuthEnabled = true, + isPasswordlessEnabled, authProviders = [], + primaryAuthType, } = props; const ssoEnabled = authProviders?.length > 0; // If local auth was not enabled, disregard any primary auth type config // and display sso providers if any. - if (!isLocalAuthEnabled && ssoEnabled) { - return ( - - - {title} - - {attempt.isFailed && ( - - {attempt.message} - - )} - - - ); - } + const actualPrimaryType = isLocalAuthEnabled ? primaryAuthType : 'sso'; - if (!isLocalAuthEnabled) { - return ( - - - {title} - - Login has not been enabled - - The ability to login has not been enabled. Please contact your system - administrator for more information. - - - ); + const allowedAuthTypes = allAuthTypes.filter(t => { + if (!isLocalAuthEnabled) return ssoEnabled && t === 'sso'; + if (!isPasswordlessEnabled && t === 'passwordless') return false; + if (!ssoEnabled && t === 'sso') return false; + return true; + }); + const otherAuthTypes = allowedAuthTypes.filter(t => t !== actualPrimaryType); + + let errorMessage = ''; + if (allowedAuthTypes.length === 0) { + errorMessage = 'Login has not been enabled'; + } else if (attempt.isFailed) { + errorMessage = attempt.message; } // Everything below requires local auth to be enabled. return ( - - - {title} + + + Sign in to Teleport - {attempt.isFailed && ( - - {attempt.message} - + {errorMessage && {errorMessage}} + {allowedAuthTypes.length > 0 ? ( + + flows={loginViews} + currFlow={'default'} + otherAuthTypes={otherAuthTypes} + {...props} + primaryAuthType={actualPrimaryType} + /> + ) : ( + + The ability to login has not been enabled. Please contact your system + administrator for more information. + )} - - flows={loginViews} - currFlow={'default'} - {...props} - /> ); } @@ -126,7 +123,6 @@ const SsoList = ({ const { isProcessing } = attempt; return ( { + primary, +}: Props & { hasTransitionEnded: boolean; primary: boolean }) => { const ref = useRefAutoFocus({ shouldFocus: hasTransitionEnded && autoFocus, }); // Firefox currently does not support passwordless and when - // logging in, it will return an ambigugous error. + // logging in, it will return an ambiguous error. // We display a soft warning because firefox may provide // support in the near future: https://github.com/gravitational/webapps/pull/876 const isFirefox = window.navigator?.userAgent ?.toLowerCase() .includes('firefox'); return ( - + {isFirefox && ( Firefox may not support passwordless login. Please try Chrome or Safari. )} - onLoginWithWebauthn()} - disabled={attempt.isProcessing} + - - - - - Passwordless - - Follow the prompt from your browser - - - - - - +

+ +
+
+ + Your browser will prompt you for a device key. + +
+ + ); }; @@ -247,8 +242,6 @@ const LocalForm = ({ {({ validator }) => ( onSetMfaOption(opt as MfaOption, validator)} @@ -303,10 +296,13 @@ const LocalForm = ({ mb={0} isDisabled={isProcessing} menuIsOpen={true} + // Needed to prevent the menu from causing scroll bars to + // appear. + menuPosition="fixed" /> {mfaType.value === 'otp' && ( onLoginClick(e, validator)} @@ -346,127 +340,76 @@ const LocalForm = ({ ); }; -// Primary determines which authentication type to display -// on initial render of the login form. -const Primary = ({ +// Displays the primary login options and a list of secondary options. +const LoginOptions = ({ next, refCallback, - hasTransitionEnded, + otherAuthTypes, ...otherProps -}: Props & StepComponentProps) => { - const ssoEnabled = otherProps.authProviders?.length > 0; - let otherOptionsAvailable = true; - let $primary; - - switch (otherProps.primaryAuthType) { - case 'passwordless': - $primary = ( - { + return ( + + + {otherAuthTypes.length > 0 && } + {otherAuthTypes.map(authType => ( + + ))} + + ); +}; + +function AuthMethod({ + authType, + primary, + autoFocus, + next, + ...otherProps +}: { + authType: PrimaryAuthType; + primary?: boolean; +} & Props & + StepComponentProps) { + switch (authType) { + case 'passwordless': + return ( + ); - break; case 'sso': - $primary = ( - - ); - break; + return ; case 'local': - otherOptionsAvailable = otherProps.isPasswordlessEnabled || ssoEnabled; - $primary = ( - + return primary ? ( + + ) : ( + + + Sign in with username and password + + ); - break; } +} - return ( - - {$primary} - {otherOptionsAvailable && ( - - { - otherProps.clearAttempt(); - next(); - }} - > - Other sign-in options - - - )} - - ); -}; - -// Secondary determines what other forms of authentication -// is allowed for the user to login with. -// -// There can be multiple authn types available, which will -// be visually separated by a divider. -const Secondary = ({ +// Displays a standalone local login form. +const LocalLogin = ({ prev, refCallback, ...otherProps }: Props & StepComponentProps) => { - const ssoEnabled = otherProps.authProviders?.length > 0; - const { primaryAuthType, isPasswordlessEnabled } = otherProps; - - let $secondary; - switch (primaryAuthType) { - case 'passwordless': - if (ssoEnabled) { - $secondary = ( - <> - - - - - ); - } else { - $secondary = ; - } - break; - case 'sso': - if (isPasswordlessEnabled) { - $secondary = ( - <> - - - - - ); - } else { - $secondary = ; - } - break; - case 'local': - if (isPasswordlessEnabled) { - $secondary = ( - <> - - {otherProps.isPasswordlessEnabled && ssoEnabled && } - {ssoEnabled && } - - ); - } else { - $secondary = ; - } - break; - } return ( - - {$secondary} + + ( flexDirection="column" borderBottom={1} borderColor="text.muted" - mx={5} - mt={5} - mb={2} + my={3} > Or ); -const StyledPaswordlessBtn = styled(ButtonText)` - display: block; - text-align: left; - border: 1px solid ${({ theme }) => theme.colors.buttons.border.border}; - - &:hover, - &:focus { - background: ${({ theme }) => theme.colors.buttons.border.hover}; - text-decoration: none; - } - - &:active { - background: ${({ theme }) => theme.colors.buttons.border.active}; - } - - &[disabled] { - pointer-events: none; - background: ${({ theme }) => theme.colors.buttons.bgDisabled}; - } -`; - const StyledOr = styled.div` background: ${props => props.theme.colors.levels.surface}; display: flex; @@ -528,11 +448,13 @@ const StyledOr = styled.div` justify-content: center; position: absolute; z-index: 1; + text-transform: uppercase; `; -const loginViews = { default: [Primary, Secondary] }; +const loginViews = { default: [LoginOptions, LocalLogin] }; export type Props = { + // Deprecated. TODO(bl-nero): Remove after e/ is updated. title?: string; isLocalAuthEnabled?: boolean; isPasswordlessEnabled: boolean; diff --git a/web/packages/teleport/src/components/FormLogin/SsoButtons.tsx b/web/packages/teleport/src/components/FormLogin/SsoButtons.tsx index d1e070d77f4ca..ae304e787a47b 100644 --- a/web/packages/teleport/src/components/FormLogin/SsoButtons.tsx +++ b/web/packages/teleport/src/components/FormLogin/SsoButtons.tsx @@ -17,17 +17,18 @@ */ import React, { forwardRef } from 'react'; +import styled from 'styled-components'; import { Box, Text } from 'design'; import ButtonSso, { guessProviderType } from 'shared/components/ButtonSso'; import { AuthProvider } from 'shared/services'; const SSOBtnList = forwardRef( - ({ providers, prefixText, isDisabled, onClick, autoFocus = false }, ref) => { + ({ providers, isDisabled, onClick, autoFocus = false }, ref) => { + const style = providers.length === 1 ? { gridColumnEnd: 'span 2' } : {}; const $btns = providers.map((item, index) => { let { name, type, displayName } = item; - const title = displayName || `${prefixText} ${name}`; + const title = displayName || name; const ssoType = guessProviderType(title, type); - const len = providers.length - 1; return ( ( title={title} ssoType={ssoType} disabled={isDisabled} - mt={3} - mb={index < len ? 3 : 0} autoFocus={index === 0 && autoFocus} + style={style} onClick={e => { e.preventDefault(); onClick(item); @@ -54,16 +54,11 @@ const SSOBtnList = forwardRef( ); } - return ( - - {$btns} - - ); + return {$btns}; } ); type Props = { - prefixText: string; isDisabled: boolean; onClick(provider: AuthProvider): void; providers: AuthProvider[]; @@ -71,4 +66,10 @@ type Props = { autoFocus?: boolean; }; +const Container = styled(Box)` + display: grid; + grid-template-columns: 1fr 1fr; + gap: ${p => p.theme.space[3]}px; +`; + export default SSOBtnList; diff --git a/web/packages/teleport/src/components/FormLogin/__snapshots__/FormLogin.story.test.tsx.snap b/web/packages/teleport/src/components/FormLogin/__snapshots__/FormLogin.story.test.tsx.snap index 7e3578d95e985..e73634a4fb7cd 100644 --- a/web/packages/teleport/src/components/FormLogin/__snapshots__/FormLogin.story.test.tsx.snap +++ b/web/packages/teleport/src/components/FormLogin/__snapshots__/FormLogin.story.test.tsx.snap @@ -7,8 +7,9 @@ exports[`auth2faType: off 1`] = ` margin-right: auto; margin-top: 32px; margin-bottom: 32px; + padding-top: 24px; padding-bottom: 24px; - width: 464px; + width: 650px; } .c3 { @@ -17,25 +18,28 @@ exports[`auth2faType: off 1`] = ` .c5 { box-sizing: border-box; - padding-top: 16px; - padding-left: 32px; - padding-right: 32px; + padding-left: 24px; + padding-right: 24px; +} + +.c7 { + box-sizing: border-box; border-bottom-right-radius: 8px; border-bottom-left-radius: 8px; } -.c7 { +.c9 { box-sizing: border-box; margin-bottom: 16px; } -.c10 { +.c12 { box-sizing: border-box; margin-bottom: 0px; width: 100%; } -.c11 { +.c13 { line-height: 1.5; margin: 0; display: inline-flex; @@ -59,21 +63,19 @@ exports[`auth2faType: off 1`] = ` min-height: 40px; font-size: 12px; padding: 0px 40px; - margin-bottom: 4px; - margin-top: 16px; width: 100%; } -.c11:hover, -.c11:focus { +.c13:hover, +.c13:focus { background: #B29DFF; } -.c11:active { +.c13:active { background: #C5B6FF; } -.c11:disabled { +.c13:disabled { background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.3); cursor: auto; @@ -92,11 +94,11 @@ exports[`auth2faType: off 1`] = ` font-size: 22px; line-height: 32px; margin: 0px; - padding-top: 24px; + margin-bottom: 24px; text-align: center; } -.c9 { +.c11 { appearance: none; border: 1px solid rgba(255,255,255,0.54); border-radius: 4px; @@ -112,31 +114,31 @@ exports[`auth2faType: off 1`] = ` margin-top: 4px; } -.c9:hover, -.c9:focus, -.c9:active { +.c11:hover, +.c11:focus, +.c11:active { border: 1px solid rgba(255,255,255,0.72); } -.c9::-ms-clear { +.c11::-ms-clear { display: none; } -.c9::placeholder { +.c11::placeholder { color: rgba(255,255,255,0.54); opacity: 1; } -.c9:read-only { +.c11:read-only { cursor: not-allowed; } -.c9:disabled { +.c11:disabled { color: rgba(255,255,255,0.36); border-color: rgba(255,255,255,0.36); } -.c8 { +.c10 { color: #FFFFFF; display: block; font-size: 12px; @@ -145,6 +147,12 @@ exports[`auth2faType: off 1`] = ` } .c6 { + display: flex; + flex-direction: column; + gap: 16px; +} + +.c8 { display: flex; justify-content: center; flex-direction: column; @@ -196,12 +204,12 @@ exports[`auth2faType: off 1`] = `
- Custom Title + Sign in to Teleport
- + +
+
- Login with google - +
`; @@ -3190,8 +3265,9 @@ exports[`sso providers rendering 1`] = ` margin-right: auto; margin-top: 32px; margin-bottom: 32px; + padding-top: 24px; padding-bottom: 24px; - width: 464px; + width: 650px; } .c3 { @@ -3200,64 +3276,25 @@ exports[`sso providers rendering 1`] = ` .c5 { box-sizing: border-box; - padding-bottom: 8px; - padding-top: 8px; - padding-left: 40px; - padding-right: 40px; + padding-left: 24px; + padding-right: 24px; } -.c15 { - box-sizing: border-box; - margin-top: -4px; - padding-top: 16px; - text-align: center; -} - -.c6 { - line-height: 1.5; - margin: 0; - display: inline-flex; - justify-content: center; - align-items: center; +.c16 { box-sizing: border-box; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: inherit; - font-weight: 600; - outline: none; - position: relative; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: all 0.3s; - -webkit-font-smoothing: antialiased; - color: #000000; - background: #9F85FF; - min-height: 32px; - font-size: 12px; - padding: 0px 24px; - margin-bottom: 16px; margin-top: 16px; - width: 100%; -} - -.c6:hover, -.c6:focus { - background: #B29DFF; -} - -.c6:active { - background: #C5B6FF; + margin-bottom: 16px; + border-bottom: 1px solid; + border-color: rgba(255,255,255,0.54); } -.c6:disabled { - background: rgba(255,255,255,0.12); - color: rgba(255,255,255,0.3); - cursor: auto; +.c19 { + box-sizing: border-box; + padding-top: 8px; + padding-bottom: 8px; } -.c13 { +.c8 { line-height: 1.5; margin: 0; display: inline-flex; @@ -3281,27 +3318,25 @@ exports[`sso providers rendering 1`] = ` min-height: 32px; font-size: 12px; padding: 0px 24px; - margin-bottom: 0px; - margin-top: 16px; width: 100%; } -.c13:hover, -.c13:focus { +.c8:hover, +.c8:focus { background: #B29DFF; } -.c13:active { +.c8:active { background: #C5B6FF; } -.c13:disabled { +.c8:disabled { background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.3); cursor: auto; } -.c16 { +.c20 { line-height: 1.5; margin: 0; display: inline-flex; @@ -3321,26 +3356,29 @@ exports[`sso providers rendering 1`] = ` transition: all 0.3s; -webkit-font-smoothing: antialiased; color: #FFFFFF; - background: none; - text-transform: none; - min-height: 32px; + background: rgba(255,255,255,0.07); + min-height: 40px; font-size: 12px; - padding: 0px 24px; + padding: 0px 40px; + width: 100%; } -.c16:hover, -.c16:focus { - background: none; - text-decoration: underline; +.c20:hover, +.c20:focus { + background: rgba(255,255,255,0.13); } -.c16:disabled { - background: none; +.c20:active { + background: rgba(255,255,255,0.18); +} + +.c20:disabled { + background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.3); cursor: auto; } -.c9 { +.c11 { display: inline-flex; align-items: center; justify-content: center; @@ -3360,10 +3398,23 @@ exports[`sso providers rendering 1`] = ` font-size: 22px; line-height: 32px; margin: 0px; - padding-top: 24px; + margin-bottom: 24px; text-align: center; } +.c6 { + display: flex; + flex-direction: column; + gap: 16px; +} + +.c17 { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + .c4 .prev-slide-enter { transform: translateX(-100%); opacity: 0; @@ -3408,7 +3459,7 @@ exports[`sso providers rendering 1`] = ` transition: transform 500ms ease; } -.c7 { +.c9 { background-color: #444444; display: block; width: 100%; @@ -3419,17 +3470,17 @@ exports[`sso providers rendering 1`] = ` box-sizing: border-box; } -.c7:hover, -.c7:focus { +.c9:hover, +.c9:focus { background: rgb(61,61,61); border: 1px solid rgb(142,142,142); } -.c7 svg { +.c9 svg { opacity: 0.87; } -.c10 { +.c12 { background-color: #dd4b39; display: block; width: 100%; @@ -3440,17 +3491,17 @@ exports[`sso providers rendering 1`] = ` box-sizing: border-box; } -.c10:hover, -.c10:focus { +.c12:hover, +.c12:focus { background: rgb(198,67,51); border: 1px solid rgb(234,147,136); } -.c10 svg { +.c12 svg { opacity: 0.87; } -.c11 { +.c13 { background-color: #205081; display: block; width: 100%; @@ -3461,17 +3512,17 @@ exports[`sso providers rendering 1`] = ` box-sizing: border-box; } -.c11:hover, -.c11:focus { +.c13:hover, +.c13:focus { background: rgb(28,72,116); border: 1px solid rgb(121,150,179); } -.c11 svg { +.c13 svg { opacity: 0.87; } -.c12 { +.c14 { background-color: #f7931e; display: block; width: 100%; @@ -3482,17 +3533,17 @@ exports[`sso providers rendering 1`] = ` box-sizing: border-box; } -.c12:hover, -.c12:focus { +.c14:hover, +.c14:focus { background: rgb(222,132,27); border: 1px solid rgb(250,190,120); } -.c12 svg { +.c14 svg { opacity: 0.87; } -.c14 { +.c15 { background-color: #2672ec; display: block; width: 100%; @@ -3503,17 +3554,17 @@ exports[`sso providers rendering 1`] = ` box-sizing: border-box; } -.c14:hover, -.c14:focus { +.c15:hover, +.c15:focus { background: rgb(34,102,212); border: 1px solid rgb(124,170,243); } -.c14 svg { +.c15 svg { opacity: 0.87; } -.c8 { +.c10 { align-items: center; display: flex; justify-content: center; @@ -3527,14 +3578,33 @@ exports[`sso providers rendering 1`] = ` border-right: 1px solid rgba(0,0,0,0.12); } +.c7 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.c18 { + background: #222C59; + display: flex; + align-items: center; + font-size: 10px; + height: 32px; + width: 32px; + justify-content: center; + position: absolute; + z-index: 1; + text-transform: uppercase; +} +
- Welcome! + Sign in to Teleport
+
+ Or +
+
+
diff --git a/web/packages/teleport/src/components/PasskeyIcons/PasskeyIcons.tsx b/web/packages/teleport/src/components/PasskeyIcons/PasskeyIcons.tsx new file mode 100644 index 0000000000000..fd98a28205c98 --- /dev/null +++ b/web/packages/teleport/src/components/PasskeyIcons/PasskeyIcons.tsx @@ -0,0 +1,49 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +import React from 'react'; +import styled from 'styled-components'; +import * as Icon from 'design/Icon'; + +export function PasskeyIcons() { + return ( + <> + + + + + + + + + + + + + + ); +} + +const OverlappingChip = styled.span` + display: inline-block; + background: ${props => props.theme.colors.levels.surface}; + border: ${props => props.theme.borders[1]}; + border-color: ${props => props.theme.colors.interactive.tonal.neutral[2]}; + border-radius: 50%; + margin-right: -6px; +`; diff --git a/web/packages/teleport/src/components/PasskeyIcons/index.ts b/web/packages/teleport/src/components/PasskeyIcons/index.ts new file mode 100644 index 0000000000000..cf0c7f9c18640 --- /dev/null +++ b/web/packages/teleport/src/components/PasskeyIcons/index.ts @@ -0,0 +1,19 @@ +/** + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +export { PasskeyIcons } from './PasskeyIcons'; diff --git a/yarn.lock b/yarn.lock index 8bdf0e1edf775..c6705019b43cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3487,6 +3487,20 @@ dependencies: defer-to-connect "^2.0.0" +"@testing-library/dom@>=7": + version "9.3.4" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.4.tgz#50696ec28376926fec0a1bf87d9dbac5e27f60ce" + integrity sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.1.3" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + "@testing-library/dom@^9.0.0": version "9.3.3" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.3.tgz#108c23a5b0ef51121c26ae92eb3179416b0434f5" @@ -13278,6 +13292,13 @@ react-router@5.1.1: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-select-event@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/react-select-event/-/react-select-event-5.5.1.tgz#d67e04a6a51428b1534b15ecb1b82afbe5edddcb" + integrity sha512-goAx28y0+iYrbqZA2FeRTreHHs/ZtSuKxtA+J5jpKT5RHPCbVZJ4MqACfPnWyFXsEec+3dP5bCrNTxIX8oYe9A== + dependencies: + "@testing-library/dom" ">=7" + react-select@^3.0.8: version "3.2.0" resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.2.0.tgz#de9284700196f5f9b5277c5d850a9ce85f5c72fe"