diff --git a/src/i18n/en-US.json b/src/i18n/en-US.json index 67379230405..2d8f9f90179 100644 --- a/src/i18n/en-US.json +++ b/src/i18n/en-US.json @@ -263,6 +263,7 @@ "clientManager.headline": "Remove a device", "clientManager.logout": "Cancel process", "clientManager.subhead": "Remove one of your other devices to start using {brandName} on this one.", + "clientManager.oauth": "You are adding a new device. Only 7 devices can be active. Remove one of your devices to start using Wire on this one ({device}).", "collectionSectionAudio": "Audio", "collectionSectionFiles": "Files", "collectionSectionImages": "Images", @@ -956,6 +957,18 @@ "notificationTitleGroup": "{{user}} in {{conversation}}", "notificationVoiceChannelActivate": "Calling", "notificationVoiceChannelDeactivate": "Called", + "oauth.headline": "Permissions", + "oauth.logout": "Switch account", + "oauth.cancel": "Don't Allow", + "oauth.allow": "Allow", + "oauth.subhead": "{app} requires your permission to:", + "oauth.learnMore": "Learn more about these permissions in the settings.", + "oauth.details": "If you allow the permissions listed, Wire™ will be able to connect to your calendar. It won’t see the content of your calendar, just the ones happening with Wire. If you don’t grant the permissions, you can’t use this add-in.", + "oauth.privacypolicy": "Wire's Privacy Policy", + "oauth.scope.write_conversations": "Create conversations", + "oauth.scope.write_conversations_code": "Create conversation guest links", + "oauth.scope.read_self": "Access user information", + "oauth.scope.read_feature_configs": "Access team feature configurations", "ongoingAudioCall": "Ongoing audio call with {{conversationName}}.", "ongoingGroupAudioCall": "Ongoing conference call with {{conversationName}}.", "ongoingGroupVideoCall": "Ongoing video conference call with {{conversationName}}, your camera is {{cameraStatus}}.", @@ -1309,4 +1322,4 @@ "wireMacos": "{{brandName}} for macOS", "wireWindows": "{{brandName}} for Windows", "wire_for_web": "{{brandName}} for Web" -} +} \ No newline at end of file diff --git a/src/script/auth/component/AcceptNewsModal.tsx b/src/script/auth/component/AcceptNewsModal.tsx index e0a02891e7d..774fb0ce3d9 100644 --- a/src/script/auth/component/AcceptNewsModal.tsx +++ b/src/script/auth/component/AcceptNewsModal.tsx @@ -46,8 +46,7 @@ const AcceptNewsModal = ({onConfirm, onDecline}: Props) => { {chunks}) as any, + strong: (...chunks: any[]) => {chunks}, }} /> diff --git a/src/script/auth/component/AccountForm.tsx b/src/script/auth/component/AccountForm.tsx index 4036e6994c4..90df7b436fe 100644 --- a/src/script/auth/component/AccountForm.tsx +++ b/src/script/auth/component/AccountForm.tsx @@ -264,8 +264,7 @@ const AccountFormComponent = ({account, ...props}: Props & ConnectedProps & Disp ( + privacypolicy: (...chunks: string[] | React.ReactNode[]) => ( {chunks} - )) as any, - // eslint-disable-next-line react/display-name - terms: ((...chunks: string[] | React.ReactNode[]) => ( + ), + terms: (...chunks: string[] | React.ReactNode[]) => ( {chunks} - )) as any, + ), }} /> ) : ( ( + terms: (...chunks: string[] | React.ReactNode[]) => ( {chunks} - )) as any, + ), }} /> )} diff --git a/src/script/auth/component/ClientList.tsx b/src/script/auth/component/ClientList.tsx index 26820835c3b..6267330c64b 100644 --- a/src/script/auth/component/ClientList.tsx +++ b/src/script/auth/component/ClientList.tsx @@ -24,6 +24,7 @@ import {connect} from 'react-redux'; import {useNavigate} from 'react-router-dom'; import {AnyAction, Dispatch} from 'redux'; +import {UrlUtil} from '@wireapp/commons'; import {ContainerXS, Loading} from '@wireapp/react-ui-kit'; import {getLogger} from 'Util/Logger'; @@ -56,6 +57,7 @@ const ClientListComponent = ({ }: Props & ConnectedProps & DispatchProps) => { const navigate = useNavigate(); const [showLoading, setShowLoading] = React.useState(false); + const isOauth = UrlUtil.hasURLParameter(QUERY_KEY.SCOPE); const [currentlySelectedClient, setCurrentlySelectedClient] = React.useState(null); const setSelectedClient = (clientId: string) => { @@ -76,6 +78,11 @@ const ClientListComponent = ({ await doInitializeClient(persist ? ClientType.PERMANENT : ClientType.TEMPORARY, password, SFAcode, entropy); removeLocalStorage(QUERY_KEY.CONVERSATION_CODE); removeLocalStorage(QUERY_KEY.JOIN_EXPIRES); + + if (isOauth) { + return navigate(ROUTE.AUTHORIZE); + } + return navigate(ROUTE.HISTORY_INFO); } catch (error) { logger.error(error); diff --git a/src/script/auth/component/UnsupportedBrowser.tsx b/src/script/auth/component/UnsupportedBrowser.tsx index f4ea9eff53c..7d4a32ff434 100644 --- a/src/script/auth/component/UnsupportedBrowser.tsx +++ b/src/script/auth/component/UnsupportedBrowser.tsx @@ -84,7 +84,6 @@ export const UnsupportedBrowserComponent = ({ ) : (

{_(unsupportedStrings.subheadBrowser, { - // eslint-disable-next-line react/display-name strong: (...chunks: any[]) => {chunks}, })}

diff --git a/src/script/auth/component/WirelessContainer.tsx b/src/script/auth/component/WirelessContainer.tsx index dbd7bb9f4fc..87b7b7a2d76 100644 --- a/src/script/auth/component/WirelessContainer.tsx +++ b/src/script/auth/component/WirelessContainer.tsx @@ -69,8 +69,7 @@ const WirelessContainer: React.FC = ({showCookiePolicyBanner, onCookiePol {...cookiePolicyStrings.bannerText} values={{ newline:
, - // eslint-disable-next-line react/display-name - strong: ((...chunks: any[]) => {chunks}) as any, + strong: (...chunks: any[]) => {chunks}, }} /> diff --git a/src/script/auth/module/action/AuthAction.test.ts b/src/script/auth/module/action/AuthAction.test.ts index 76bac439066..a5242beac28 100644 --- a/src/script/auth/module/action/AuthAction.test.ts +++ b/src/script/auth/module/action/AuthAction.test.ts @@ -18,6 +18,7 @@ */ import {ClientType} from '@wireapp/api-client/lib/client/'; +import {RecursivePartial} from '@wireapp/commons/lib/util/TypeUtil'; import {StatusCodes as HTTP_STATUS} from 'http-status-codes'; import type {APIClient} from '@wireapp/api-client'; @@ -28,7 +29,7 @@ import {AuthActionCreator} from './creator/'; import {mockStoreFactory} from '../../util/test/mockStoreFactory'; -import {actionRoot} from './'; +import {ActionRoot, actionRoot} from './'; describe('AuthAction', () => { it('authenticates a user successfully', async () => { @@ -129,14 +130,12 @@ describe('AuthAction', () => { backendError.code = HTTP_STATUS.FORBIDDEN; backendError.label = 'invalid-credentials'; backendError.message = 'Authentication failed.'; - const spies = { - generateClientPayload: jasmine.createSpy().and.returnValue({}), - }; + const mockedActions = { clientAction: { - generateClientPayload: spies.generateClientPayload, + generateClientPayload: jest.fn().mockReturnValue({}), }, - }; + } as RecursivePartial; const mockedCore = { login: () => Promise.reject(backendError), }; diff --git a/src/script/auth/module/action/AuthAction.ts b/src/script/auth/module/action/AuthAction.ts index c473940f734..c7d21dc2e6a 100644 --- a/src/script/auth/module/action/AuthAction.ts +++ b/src/script/auth/module/action/AuthAction.ts @@ -21,6 +21,9 @@ import type {DomainData} from '@wireapp/api-client/lib/account/DomainData'; import type {LoginData, RegisterData, SendLoginCode} from '@wireapp/api-client/lib/auth/'; import {VerificationActionType} from '@wireapp/api-client/lib/auth/VerificationActionType'; import {ClientType} from '@wireapp/api-client/lib/client/'; +import {OAuthBody} from '@wireapp/api-client/lib/oauth/OAuthBody'; +import {OAuthClient} from '@wireapp/api-client/lib/oauth/OAuthClient'; +import type {TeamData} from '@wireapp/api-client/lib/team/'; import {LowDiskSpaceError} from '@wireapp/store-engine/lib/engine/error'; import {StatusCodes as HTTP_STATUS, StatusCodes} from 'http-status-codes'; @@ -145,6 +148,20 @@ export class AuthAction { }; }; + doPostOAuthCode = (oauthBody: OAuthBody): ThunkAction> => { + return async (dispatch, getState, {apiClient}) => { + dispatch(AuthActionCreator.startSendOAuthCode()); + try { + const url = await apiClient.api.oauth.postOAuthCode(oauthBody); + dispatch(AuthActionCreator.successfulSendOAuthCode()); + return url; + } catch (error) { + dispatch(AuthActionCreator.failedSendOAuthCode(error)); + throw error; + } + }; + }; + doSendTwoFactorLoginCode = (email: string): ThunkAction => { return async (dispatch, getState, {apiClient}) => { dispatch(AuthActionCreator.startSendTwoFactorCode()); @@ -185,6 +202,34 @@ export class AuthAction { }; }; + doGetTeamData = (teamId: string): ThunkAction> => { + return async (dispatch, getState, {apiClient}) => { + dispatch(AuthActionCreator.startFetchTeam()); + try { + const teamData = await apiClient.api.teams.team.getTeam(teamId); + dispatch(AuthActionCreator.successfulFetchTeam(teamData)); + return teamData; + } catch (error) { + dispatch(AuthActionCreator.failedFetchTeam(error)); + throw error; + } + }; + }; + + doGetOAuthApplication = (applicationId: string): ThunkAction> => { + return async (dispatch, getState, {apiClient}) => { + dispatch(AuthActionCreator.startFetchOAuth()); + try { + const application = await apiClient.api.oauth.getClient(applicationId); + dispatch(AuthActionCreator.successfulFetchOAuth(application)); + return application; + } catch (error) { + dispatch(AuthActionCreator.failedFetchOAuth(error)); + throw error; + } + }; + }; + validateSSOCode = (code: string): ThunkAction => { return async (dispatch, getState, {apiClient}) => { const mapError = (error: any) => { diff --git a/src/script/auth/module/action/ClientAction.ts b/src/script/auth/module/action/ClientAction.ts index 3dc4fd5e8b5..81c4614e040 100644 --- a/src/script/auth/module/action/ClientAction.ts +++ b/src/script/auth/module/action/ClientAction.ts @@ -78,7 +78,7 @@ export class ClientAction { }; }; - generateClientPayload = (clientType: ClientType): ClientInfo | undefined => { + private generateClientPayload = (clientType: ClientType): ClientInfo | undefined => { if (clientType === ClientType.NONE) { return undefined; } diff --git a/src/script/auth/module/action/creator/AuthActionCreator.ts b/src/script/auth/module/action/creator/AuthActionCreator.ts index 90e1429f042..146163edb5f 100644 --- a/src/script/auth/module/action/creator/AuthActionCreator.ts +++ b/src/script/auth/module/action/creator/AuthActionCreator.ts @@ -19,6 +19,8 @@ import type {SSOSettings} from '@wireapp/api-client/lib/account/SSOSettings'; import type {RegisterData} from '@wireapp/api-client/lib/auth/'; +import {OAuthClient} from '@wireapp/api-client/lib/oauth/OAuthClient'; +import {TeamData} from '@wireapp/api-client/lib/team'; import type {LoginDataState, RegistrationDataState} from '../../reducer/authReducer'; @@ -29,6 +31,12 @@ export enum AUTH_ACTION { ENTER_GENERIC_INVITATION_FLOW = 'ENTER_GENERIC_INVITATION_FLOW', ENTER_PERSONAL_CREATION_FLOW = 'ENTER_PERSONAL_CREATION_FLOW', ENTER_TEAM_CREATION_FLOW = 'ENTER_TEAM_CREATION_FLOW', + FETCH_TEAM_FAILED = 'FETCH_TEAM_FAILED', + FETCH_TEAM_START = 'FETCH_TEAM_START', + FETCH_TEAM_SUCCESS = 'FETCH_TEAM_SUCCESS', + FETCH_OAUTH_APP_FAILED = 'FETCH_OAUTH_APP_FAILED', + FETCH_OAUTH_APP_START = 'FETCH_OAUTH_APP_START', + FETCH_OAUTH_APP_SUCCESS = 'FETCH_OAUTH_APP_SUCCESS', GET_SSO_SETTINGS_FAILED = 'GET_SSO_SETTINGS_FAILED', GET_SSO_SETTINGS_START = 'GET_SSO_SETTINGS_START', GET_SSO_SETTINGS_SUCCESS = 'GET_SSO_SETTINGS_SUCCESS', @@ -60,6 +68,9 @@ export enum AUTH_ACTION { SEND_PHONE_LOGIN_CODE_FAILED = 'SEND_PHONE_LOGIN_CODE_FAILED', SEND_PHONE_LOGIN_CODE_START = 'SEND_PHONE_LOGIN_CODE_START', SEND_PHONE_LOGIN_CODE_SUCCESS = 'SEND_PHONE_LOGIN_CODE_SUCCESS', + SEND_OAUTH_CODE_FAILED = 'SEND_OAUTH_CODE_FAILED', + SEND_OAUTH_CODE_START = 'SEND_OAUTH_CODE_START', + SEND_OAUTH_CODE_SUCCESS = 'SEND_OAUTH_CODE_SUCCESS', SEND_TWO_FACTOR_CODE_FAILED = 'SEND_TWO_FACTOR_CODE_FAILED', SEND_TWO_FACTOR_CODE_START = 'SEND_TWO_FACTOR_CODE_START', SEND_TWO_FACTOR_CODE_SUCCESS = 'SEND_TWO_FACTOR_CODE_SUCCESS', @@ -77,9 +88,18 @@ export type AuthActions = | SendPhoneLoginCodeStartAction | SendPhoneLoginCodeSuccessAction | SendPhoneLoginCodeFailedAction + | SendOAuthCodeStartAction + | SendOAuthCodeSuccessAction + | SendOAuthCodeFailedAction | RegisterTeamStartAction | RegisterTeamSuccessAction | RegisterTeamFailedAction + | FetchTeamStartAction + | FetchTeamSuccessAction + | FetchTeamFailedAction + | FetchApplicationStartAction + | FetchApplicationSuccessAction + | FetchApplicationFailedAction | RegisterPersonalStartAction | RegisterPersonalSuccessAction | RegisterPersonalFailedAction @@ -134,6 +154,17 @@ export interface SendPhoneLoginCodeFailedAction extends AppAction { readonly type: AUTH_ACTION.SEND_PHONE_LOGIN_CODE_FAILED; } +export interface SendOAuthCodeStartAction extends AppAction { + readonly type: AUTH_ACTION.SEND_OAUTH_CODE_START; +} +export interface SendOAuthCodeSuccessAction extends AppAction { + readonly type: AUTH_ACTION.SEND_OAUTH_CODE_SUCCESS; +} +export interface SendOAuthCodeFailedAction extends AppAction { + readonly error: Error; + readonly type: AUTH_ACTION.SEND_OAUTH_CODE_FAILED; +} + export interface SendTwoFactorCodeStartAction extends AppAction { readonly type: AUTH_ACTION.SEND_TWO_FACTOR_CODE_START; } @@ -157,6 +188,30 @@ export interface RegisterTeamFailedAction extends AppAction { readonly type: AUTH_ACTION.REGISTER_TEAM_FAILED; } +export interface FetchTeamStartAction extends AppAction { + readonly type: AUTH_ACTION.FETCH_TEAM_START; +} +export interface FetchTeamSuccessAction extends AppAction { + readonly payload: TeamData; + readonly type: AUTH_ACTION.FETCH_TEAM_SUCCESS; +} +export interface FetchTeamFailedAction extends AppAction { + readonly error: Error; + readonly type: AUTH_ACTION.FETCH_TEAM_FAILED; +} + +export interface FetchApplicationStartAction extends AppAction { + readonly type: AUTH_ACTION.FETCH_OAUTH_APP_START; +} +export interface FetchApplicationSuccessAction extends AppAction { + readonly payload: OAuthClient; + readonly type: AUTH_ACTION.FETCH_OAUTH_APP_SUCCESS; +} +export interface FetchApplicationFailedAction extends AppAction { + readonly error: Error; + readonly type: AUTH_ACTION.FETCH_OAUTH_APP_FAILED; +} + export interface RegisterPersonalStartAction extends AppAction { readonly type: AUTH_ACTION.REGISTER_PERSONAL_START; } @@ -294,6 +349,19 @@ export class AuthActionCreator { type: AUTH_ACTION.SEND_PHONE_LOGIN_CODE_FAILED, }); + static startSendOAuthCode = (): SendOAuthCodeStartAction => ({ + type: AUTH_ACTION.SEND_OAUTH_CODE_START, + }); + + static successfulSendOAuthCode = (): SendOAuthCodeSuccessAction => ({ + type: AUTH_ACTION.SEND_OAUTH_CODE_SUCCESS, + }); + + static failedSendOAuthCode = (error: Error): SendOAuthCodeFailedAction => ({ + error, + type: AUTH_ACTION.SEND_OAUTH_CODE_FAILED, + }); + static startSendTwoFactorCode = (): SendTwoFactorCodeStartAction => ({ type: AUTH_ACTION.SEND_TWO_FACTOR_CODE_START, }); @@ -321,6 +389,33 @@ export class AuthActionCreator { type: AUTH_ACTION.REGISTER_TEAM_FAILED, }); + static startFetchTeam = (): FetchTeamStartAction => ({ + type: AUTH_ACTION.FETCH_TEAM_START, + }); + + static successfulFetchTeam = (teamData: TeamData): FetchTeamSuccessAction => ({ + payload: teamData, + type: AUTH_ACTION.FETCH_TEAM_SUCCESS, + }); + + static failedFetchTeam = (error: Error): FetchTeamFailedAction => ({ + error, + type: AUTH_ACTION.FETCH_TEAM_FAILED, + }); + static startFetchOAuth = (): FetchApplicationStartAction => ({ + type: AUTH_ACTION.FETCH_OAUTH_APP_START, + }); + + static successfulFetchOAuth = (application: OAuthClient): FetchApplicationSuccessAction => ({ + payload: application, + type: AUTH_ACTION.FETCH_OAUTH_APP_SUCCESS, + }); + + static failedFetchOAuth = (error: Error): FetchApplicationFailedAction => ({ + error, + type: AUTH_ACTION.FETCH_OAUTH_APP_FAILED, + }); + static startRegisterPersonal = (): RegisterPersonalStartAction => ({ type: AUTH_ACTION.REGISTER_PERSONAL_START, }); diff --git a/src/script/auth/module/reducer/authReducer.ts b/src/script/auth/module/reducer/authReducer.ts index d136f22059f..82746f58c6c 100644 --- a/src/script/auth/module/reducer/authReducer.ts +++ b/src/script/auth/module/reducer/authReducer.ts @@ -20,6 +20,7 @@ import type {SSOSettings} from '@wireapp/api-client/lib/account/SSOSettings'; import {LoginData} from '@wireapp/api-client/lib/auth'; import {ClientType} from '@wireapp/api-client/lib/client/'; +import {OAuthClient} from '@wireapp/api-client/lib/oauth/OAuthClient'; import type {TeamData} from '@wireapp/api-client/lib/team/'; import type {UserAsset} from '@wireapp/api-client/lib/user/'; @@ -56,6 +57,7 @@ export type AuthState = { readonly isAuthenticated: boolean; readonly isSendingTwoFactorCode: boolean; readonly loginData: LoginDataState; + readonly oAuthApp?: OAuthClient; readonly ssoSettings?: SSOSettings; }; @@ -86,6 +88,7 @@ export const initialAuthState: AuthState = { loginData: { clientType: Config.getConfig().FEATURE.DEFAULT_LOGIN_TEMPORARY_CLIENT ? ClientType.TEMPORARY : ClientType.PERMANENT, }, + oAuthApp: undefined, ssoSettings: { default_sso_code: undefined, }, @@ -124,6 +127,8 @@ export function authReducer(state: AuthState = initialAuthState, action: AppActi isSendingTwoFactorCode: false, }; } + case AUTH_ACTION.FETCH_OAUTH_APP_START: + case AUTH_ACTION.FETCH_TEAM_START: case AUTH_ACTION.REFRESH_START: { return { ...state, @@ -150,6 +155,14 @@ export function authReducer(state: AuthState = initialAuthState, action: AppActi isAuthenticated: false, }; } + case AUTH_ACTION.FETCH_OAUTH_APP_FAILED: + case AUTH_ACTION.FETCH_TEAM_FAILED: { + return { + ...state, + error: action.error, + fetching: false, + }; + } case AUTH_ACTION.LOGIN_SUCCESS: case AUTH_ACTION.REFRESH_SUCCESS: case AUTH_ACTION.REGISTER_JOIN_SUCCESS: @@ -164,6 +177,24 @@ export function authReducer(state: AuthState = initialAuthState, action: AppActi isAuthenticated: true, }; } + case AUTH_ACTION.FETCH_TEAM_SUCCESS: { + return { + ...state, + account: {...state.account, team: action.payload}, + error: null, + fetched: true, + fetching: false, + }; + } + case AUTH_ACTION.FETCH_OAUTH_APP_SUCCESS: { + return { + ...state, + oAuthApp: action.payload, + error: null, + fetched: true, + fetching: false, + }; + } case AUTH_ACTION.GET_SSO_SETTINGS_START: { return { ...state, diff --git a/src/script/auth/page/ClientManager.tsx b/src/script/auth/page/ClientManager.tsx index 456250981ef..96a6baebeec 100644 --- a/src/script/auth/page/ClientManager.tsx +++ b/src/script/auth/page/ClientManager.tsx @@ -23,6 +23,7 @@ import {useIntl} from 'react-intl'; import {connect} from 'react-redux'; import {AnyAction, Dispatch} from 'redux'; +import {UrlUtil, StringUtil, Runtime} from '@wireapp/commons'; import {Button, ButtonVariant, ContainerXS, H1, Muted, useTimeout} from '@wireapp/react-ui-kit'; import {Page} from './Page'; @@ -39,6 +40,9 @@ type Props = React.HTMLProps; const ClientManagerComponent = ({doGetAllClients, doLogout}: Props & ConnectedProps & DispatchProps) => { const {formatMessage: _} = useIntl(); const SFAcode = localStorage.getItem(QUERY_KEY.CONVERSATION_CODE); + const isOauth = UrlUtil.hasURLParameter(QUERY_KEY.SCOPE); + const device = StringUtil.capitalize(Runtime.getBrowserName()); + const timeRemaining = JSON.parse(localStorage.getItem(QUERY_KEY.JOIN_EXPIRES) ?? '{}')?.data ?? Date.now(); // Automatically log the user out if ten minutes passes and they are a 2fa user. @@ -75,7 +79,9 @@ const ClientManagerComponent = ({doGetAllClients, doLogout}: Props & ConnectedPr {_(clientManagerStrings.headline)} - {_(clientManagerStrings.subhead, {brandName: Config.getConfig().BRAND_NAME})} + {isOauth + ? _(clientManagerStrings.oauth, {device}) + : _(clientManagerStrings.subhead, {brandName: Config.getConfig().BRAND_NAME})} + + + + ( + + {chunks} + + ), + }} + /> + + + )} + + + ); +}; + +type ConnectedProps = ReturnType; +const mapStateToProps = (state: RootState) => ({ + selfUser: SelfSelector.getSelf(state), + selfTeamId: SelfSelector.getSelfTeamId(state), +}); + +type DispatchProps = ReturnType; +const mapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators( + { + getSelf: actionRoot.selfAction.fetchSelf, + getOAuthApp: actionRoot.authAction.doGetOAuthApplication, + doLogout: actionRoot.authAction.doLogout, + getTeam: actionRoot.authAction.doGetTeamData, + postOauthCode: actionRoot.authAction.doPostOAuthCode, + }, + dispatch, + ); + +const OAuthPermissions = connect(mapStateToProps, mapDispatchToProps)(OAuthPermissionsComponent); + +export {OAuthPermissions}; diff --git a/src/script/auth/page/OauthPermissions.styles.ts b/src/script/auth/page/OauthPermissions.styles.ts new file mode 100644 index 00000000000..54b75fb9e50 --- /dev/null +++ b/src/script/auth/page/OauthPermissions.styles.ts @@ -0,0 +1,69 @@ +/* + * Wire + * Copyright (C) 2023 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react'; + +import {COLOR_V2} from '@wireapp/react-ui-kit'; + +export const teamImageCSS: CSSObject = { + width: '22px', + height: '22px', + borderRadius: '6px', + border: 'black 1px solid', + padding: '2px', + margin: '15px', +}; + +export const containerCSS: CSSObject = { + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', +}; + +export const headerCSS: CSSObject = { + fontWeight: 500, + lineHeight: '28.64px', + fontSize: '24px', +}; + +export const boxCSS: CSSObject = { + marginBottom: '24px', + background: COLOR_V2.GRAY_20, + borderColor: COLOR_V2.GRAY_20, + padding: '8px', +}; + +export const textCSS: CSSObject = {fontSize: '12px', lineHeight: '16px', display: 'block'}; + +export const buttonsCSS: CSSObject = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + marginTop: '74px', + gap: '16px', +}; + +export const buttonCSS: CSSObject = {margin: 'auto', width: 200}; + +export const listCSS: CSSObject = { + marginTop: 0, + paddingInlineStart: '20px', + fontSize: '12px', +}; diff --git a/src/script/auth/page/Root.tsx b/src/script/auth/page/Root.tsx index 3e46c7a2c10..54082626c19 100644 --- a/src/script/auth/page/Root.tsx +++ b/src/script/auth/page/Root.tsx @@ -40,6 +40,7 @@ import {HistoryInfo} from './HistoryInfo'; import {Index} from './Index'; import {InitialInvite} from './InitialInvite'; import {Login} from './Login'; +import {OAuthPermissions} from './OAuthPermissions'; import {PhoneLogin} from './PhoneLogin'; import {SetAccountType} from './SetAccountType'; import {SetEmail} from './SetEmail'; @@ -106,6 +107,7 @@ const RootComponent: FC = ({ }; const isAuthenticatedCheck = (page: any): any => (page ? (isAuthenticated ? page : navigate('/auth')) : null); + const isOAuthCheck = (page: any): any => (page ? isAuthenticated ? page : : null); const ProtectedHistoryInfo = () => isAuthenticatedCheck(); const ProtectedInitialInvite = () => isAuthenticatedCheck(); @@ -114,6 +116,7 @@ const RootComponent: FC = ({ const ProtectedSetHandle = () => isAuthenticatedCheck(); const ProtectedSetEmail = () => isAuthenticatedCheck(); const ProtectedSetPassword = () => isAuthenticatedCheck(); + const ProtectedOAuthPermissions = () => isOAuthCheck(); const brandName = Config.getConfig().BRAND_NAME; return ( @@ -143,6 +146,7 @@ const RootComponent: FC = ({ )} } /> } /> + } /> { + const params = new URLSearchParams(location.search); + return Object.fromEntries(params) as unknown as OAuthBody; +}; + +/** + * Takes the oauth body and returns the scopes as an array of Scopes accepted by the app. + * @param oauthBody oauth body object + * @returns Scope[] + */ +export const oAuthScope = (oauthBody: OAuthBody) => + oauthBody.scope.split(/\+|%20|\s/).filter(scope => Object.values(Scope).includes(scope as Scope)) as Scope[]; diff --git a/src/script/strings.ts b/src/script/strings.ts index c780c542e4c..2a6e25ce3ee 100644 --- a/src/script/strings.ts +++ b/src/script/strings.ts @@ -42,6 +42,7 @@ import {defineMessages} from 'react-intl'; import {BackendError} from './auth/module/action/BackendError'; import {LabeledError} from './auth/module/action/LabeledError'; import {ValidationError} from './auth/module/action/ValidationError'; +import {Scope} from './auth/page/OAuthPermissions'; import {LOGOUT_REASON} from './auth/route'; import {BackendClientError} from './error/BackendClientError'; @@ -870,6 +871,11 @@ export const clientManagerStrings = defineMessages({ defaultMessage: 'Remove one of your other devices to start using {brandName} on this one.', id: 'clientManager.subhead', }, + oauth: { + defaultMessage: + 'You are adding a new device. Only 7 devices can be active. Remove one of your devices to start using Wire on this one ({device}).', + id: 'clientManager.oauth', + }, }); export const clientItemStrings = defineMessages({ @@ -897,3 +903,55 @@ export const historyInfoStrings = defineMessages({ id: 'historyInfo.ok', }, }); + +export const oauthStrings = defineMessages({ + headline: { + defaultMessage: 'Permissions', + id: 'oauth.headline', + }, + logout: { + defaultMessage: 'Switch account', + id: 'oauth.logout', + }, + cancel: { + defaultMessage: "Don't Allow", + id: 'oauth.cancel', + }, + allow: { + defaultMessage: 'Allow', + id: 'oauth.allow', + }, + subhead: { + defaultMessage: '{app} requires your permission to:', + id: 'oauth.subhead', + }, + learnMore: { + defaultMessage: 'Learn more about these permissions in the settings.', + id: 'oauth.learnMore', + }, + details: { + defaultMessage: + 'If you allow the permissions listed, Wire™ will be able to connect to your calendar. It won’t see the content of your calendar, just the ones happening with Wire. If you don’t grant the permissions, you can’t use this add-in.', + id: 'oauth.details', + }, + privacyPolicy: { + defaultMessage: "Wire's Privacy Policy", + id: 'oauth.privacypolicy', + }, + [Scope.WRITE_CONVERSATIONS]: { + defaultMessage: 'Create conversations', + id: 'oauth.scope.write_conversations', + }, + [Scope.WRITE_CONVERSATIONS_CODE]: { + defaultMessage: 'Create conversation guest links', + id: 'oauth.scope.write_conversations_code', + }, + [Scope.READ_SELF]: { + defaultMessage: 'Access user information', + id: 'oauth.scope.read_self', + }, + [Scope.READ_FEATURE_CONFIGS]: { + defaultMessage: 'Access team feature configurations', + id: 'oauth.scope.read_feature_configs', + }, +});