diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 0fab09d39..290f6a9da 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -70,8 +70,8 @@ export default function Login() { /** * Sends a request directly to the x509 endpoint of rucio auth server to retrieve a RucioAuthTOken using x509 client certificate provided via the browser - * @param vo - * @param account + * @param vo + * @param account * @returns {@link AuthViewModel} indicating the status of the request */ const handleX509Submit = async (vo: VO, loginViewModel: LoginViewModel, account?: string | undefined): Promise => { @@ -232,9 +232,6 @@ export default function Login() { .then((res) => res.json()) .then((loginViewModel: LoginViewModel) => { setViewModel(loginViewModel) - if (loginViewModel.isLoggedIn) { - router.push(redirectURL) - } } ) }, []); diff --git a/src/component-library/Demos/01_0_Login_MultiVO.stories.ts b/src/component-library/Demos/01_0_Login_MultiVO.stories.ts index 862ceef45..3fe43dd90 100644 --- a/src/component-library/Demos/01_0_Login_MultiVO.stories.ts +++ b/src/component-library/Demos/01_0_Login_MultiVO.stories.ts @@ -66,6 +66,8 @@ export const ABasicLogin: Story = { multiVOEnabled: false, voList: [voAtlas, voCMS], isLoggedIn: false, + accountsAvailable: undefined, + accountActive: undefined, rucioAuthHost: 'https://rucio.cern.ch', }, authViewModel: { @@ -92,6 +94,8 @@ export const ABasicMultiVOLogin: Story = { multiVOEnabled: true, voList: [voAtlas, voCMS, voLHCb], isLoggedIn: false, + accountsAvailable: undefined, + accountActive: undefined, rucioAuthHost: 'https://rucio.cern.ch', }, authViewModel: { diff --git a/src/component-library/Demos/01_1_Login.stories.tsx b/src/component-library/Demos/01_1_Login.stories.tsx index c02305024..87922967b 100644 --- a/src/component-library/Demos/01_1_Login.stories.tsx +++ b/src/component-library/Demos/01_1_Login.stories.tsx @@ -58,6 +58,8 @@ export const Playbook_InitLogin: Story = { multiVOEnabled: true, voList: [voAtlas, voCMS], isLoggedIn: false, + accountsAvailable: undefined, + accountActive: undefined, rucioAuthHost: 'https://rucio.cern.ch', }, authViewModel: { diff --git a/src/component-library/Demos/01_1_Login_OIDC.stories.ts b/src/component-library/Demos/01_1_Login_OIDC.stories.ts index 0fc46dc5b..7fbd493e7 100644 --- a/src/component-library/Demos/01_1_Login_OIDC.stories.ts +++ b/src/component-library/Demos/01_1_Login_OIDC.stories.ts @@ -66,6 +66,8 @@ export const AMultiVOOIDCEnabledLogin: Story = { multiVOEnabled: true, voList: [voAtlas, voCMS, voLHCb], isLoggedIn: false, + accountActive: undefined, + accountsAvailable: undefined, rucioAuthHost: 'https://rucio.cern.ch', }, authViewModel: { diff --git a/src/component-library/Demos/01_2_Login_multi_account.stories.tsx b/src/component-library/Demos/01_2_Login_multi_account.stories.tsx index be48483f0..a48025f4d 100644 --- a/src/component-library/Demos/01_2_Login_multi_account.stories.tsx +++ b/src/component-library/Demos/01_2_Login_multi_account.stories.tsx @@ -57,14 +57,16 @@ export const Playbook_Multi_Account: Story = { oidcProviders: [cernOIDCProvider], multiVOEnabled: true, voList: [voAtlas, voCMS], + accountsAvailable: undefined, + accountActive: undefined, isLoggedIn: false, rucioAuthHost: 'https://rucio.cern.ch', }, authViewModel: { - status: "success", - message: "", + status: "multiple_accounts", + message: "mayank,ddmadmin,tester", rucioAccount: "", - rucioMultiAccount: "mayank,ddmadmin", + rucioMultiAccount: "", rucioAuthType: "", rucioAuthToken: "", rucioIdentity: "", diff --git a/src/component-library/Pages/Layout/AccountDropdown.tsx b/src/component-library/Pages/Layout/AccountDropdown.tsx index 5f05e8625..d56570bab 100644 --- a/src/component-library/Pages/Layout/AccountDropdown.tsx +++ b/src/component-library/Pages/Layout/AccountDropdown.tsx @@ -1,7 +1,77 @@ -import { twMerge } from "tailwind-merge" -import { ForwardedRef, forwardRef, useState } from "react" -import { HiCog, HiSwitchHorizontal, HiLogout } from "react-icons/hi" +import {twMerge} from "tailwind-merge" +import {ForwardedRef, forwardRef, useState} from "react" +import {HiSwitchHorizontal, HiLogout, HiUserAdd} from "react-icons/hi" import Link from "next/link" +import {useRouter} from "next/navigation"; + +const AccountList = (props: { accountList: string[] }) => { + return
+ { + props.accountList.map((account, index) => { + return ( + + + + Switch to + {account} + + + ) + }) + } +
+}; + +const SignOutOfAllButton = () => { + const router = useRouter(); + + const signOut = async () => { + const request = new Request('/api/auth/logout', { + method: 'POST' + }) + //TODO: handle errors + await fetch(request) + router.push('/auth/login') + }; + + return
signOut()} + > + Sign out of all accounts + +
+} + +const SignIntoButton = () => { + return + Sign in to another account + + +} export const AccountDropdown = forwardRef(function AccountDropdown ( @@ -12,79 +82,54 @@ export const AccountDropdown = forwardRef(function AccountDropdown }, ref: ForwardedRef ) { - return ( + const hasAccountChoice = props.accountsPossible.length !== 1; + return ( -
e.preventDefault()} - ref={ref} - > - e.preventDefault()} + ref={ref} > - - - Settings for - {props.accountActive} +
+ + Hello, + {props.accountActive}! - -
- { - (props.accountsPossible.filter( - (account) => account !== props.accountActive - )).map((account, index) => { - return ( - - - - Switch to - {account} - - - ) - }) + +
+ {hasAccountChoice && + account !== props.accountActive)}/> } + + {hasAccountChoice && }
- - Logout - -
- ) -} + ) + } ) \ No newline at end of file diff --git a/src/component-library/Pages/Login/Login.stories.tsx b/src/component-library/Pages/Login/Login.stories.tsx index 93ea7595b..676a6c4e3 100644 --- a/src/component-library/Pages/Login/Login.stories.tsx +++ b/src/component-library/Pages/Login/Login.stories.tsx @@ -57,6 +57,8 @@ LoginPage.args = { multiVOEnabled: true, voList: [voAtlas, voCMS], isLoggedIn: false, + accountsAvailable: undefined, + accountActive: undefined, rucioAuthHost: 'https://rucio.cern.ch', }, authViewModel: { diff --git a/src/component-library/Pages/Login/Login.tsx b/src/component-library/Pages/Login/Login.tsx index d91f9f3ee..ace4a4260 100644 --- a/src/component-library/Pages/Login/Login.tsx +++ b/src/component-library/Pages/Login/Login.tsx @@ -12,9 +12,20 @@ import { Alert } from '../../Misc/Alert'; import { LabelledInput } from './LabelledInput'; import { DefaultVO } from '@/lib/core/entity/auth-models'; import Modal from "react-modal"; -import { Dropdown } from '../../Input/Dropdown'; -import { H2 } from '../../Text/Headings/H2'; -import { P } from '../../Text/Content/P'; +import {Dropdown} from '../../Input/Dropdown'; +import {H2} from '../../Text/Headings/H2'; +import {P} from '../../Text/Content/P'; +import {HiArrowRight} from "react-icons/hi"; +import Link from "next/link"; + +const BackToDashboardButton = (props: {account: string}) => { + return + + +} export type SupportedAuthWorkflows = "oidc" | "x509" | "userpass" | "none" @@ -55,7 +66,7 @@ const MultipleAccountsModal = ({ "border-2", "bg-neutral-0 dark:bg-neutral-800", "flex flex-col space-y-2 p-6", - "justify-center items-center overflow-hidden outline-none focus:outline-none" + "justify-center items-center overflow-y-visible outline-none focus:outline-none" )} contentLabel="Multiaccount Modal" > @@ -90,6 +101,8 @@ export const Login = ({ oidcSubmitHandler: handleOIDCSubmit, }: LoginPageProps) => { + const isLoggedIn = loginViewModel.isLoggedIn && loginViewModel.accountActive !== undefined + const [showUserPassLoginForm, setShowUserPassLoginForm] = useState(false) const [selectedVOTab, setSelectedVOTab] = useState(1) @@ -108,11 +121,21 @@ export const Login = ({ setError(authViewModel.message) } else if (authViewModel.status === 'multiple_accounts') { const accounts = authViewModel.message?.split(',') - setAvailableAccounts(accounts ?? []) + const accountsFiltered = accounts?.filter(account => !loginViewModel.accountsAvailable?.includes(account)) + if (!accountsFiltered || accountsFiltered.length === 0) { + setError('All accounts associated with this identity are signed in to') + } else { + setAvailableAccounts(accountsFiltered) + } } }; const submitX509 = async (account: string | undefined) => { + if (account && loginViewModel.accountsAvailable?.includes(account)) { + setError(`Already authenticated as ${account}`) + return + } + const vo = loginViewModel.voList[selectedVOTab] || DefaultVO const x509AuthViewModel = await handleX509Submit(vo, loginViewModel, account) @@ -124,6 +147,11 @@ export const Login = ({ }; const submitUserPass = async (account: string | undefined) => { + if (account && loginViewModel.accountsAvailable?.includes(account)) { + setError(`Already authenticated as ${account}`) + return + } + handleUserPassSubmit(username, password, loginViewModel.voList[selectedVOTab], account) setLastAuthMethod('userpass') return Promise.resolve() @@ -137,8 +165,8 @@ export const Login = ({ } }, [loginViewModel, authViewModel]) - return ( -
{ + return
{ setError(undefined) } - } /> + }/>
-

Rucio Login

+

Rucio Login

{ - e.preventDefault() - } + onSubmit={(e) => { + e.preventDefault() + } } // TODO handle proper submit! > vo.name)} active={1} - updateActive={(active: number) => { setSelectedVOTab(active) }} + updateActive={(active: number) => { + setSelectedVOTab(active) + }} className={twMerge(loginViewModel.multiVOEnabled ? "" : "hidden")} /> @@ -181,38 +211,39 @@ export const Login = ({ className="flex justify-center flex-col space-y-4" aria-label="Choose Login Method" > - { loginViewModel.oidcEnabled == true ? -
- {loginViewModel.voList[selectedVOTab].oidcProviders.map((provider: OIDCProvider, index: number) => { - return (
- : <>} + {loginViewModel.oidcEnabled == true ? +
+ {loginViewModel.voList[selectedVOTab].oidcProviders.map((provider: OIDCProvider, index: number) => { + return (
+ : <>}
-
-
- ) + + + } + + return isLoggedIn ?
+ + {getLoginForm()} +
: getLoginForm() } diff --git a/src/component-library/outputtailwind.css b/src/component-library/outputtailwind.css index 3c42f3953..3c1e9e3ca 100644 --- a/src/component-library/outputtailwind.css +++ b/src/component-library/outputtailwind.css @@ -958,10 +958,6 @@ html { .max-w-3xl { max-width: 48rem; } -.min-w-full { - min-width: 100%; - -} .max-w-sm { max-width: 24rem; @@ -1184,14 +1180,14 @@ html { align-self: center; } -.overflow-hidden { - overflow: hidden; -} - .overflow-y-auto { overflow-y: auto; } +.overflow-y-visible { + overflow-y: visible; +} + .overflow-y-scroll { overflow-y: scroll; } @@ -1412,6 +1408,11 @@ html { background-color: rgb(34 197 94 / var(--tw-bg-opacity)); } +.bg-base-success-600 { + --tw-bg-opacity: 1; + background-color: rgb(22 163 74 / var(--tw-bg-opacity)); +} + .bg-base-warning-100 { --tw-bg-opacity: 1; background-color: rgb(254 243 199 / var(--tw-bg-opacity)); @@ -1646,10 +1647,10 @@ html { background-color: rgb(133 77 14 / var(--tw-bg-opacity)); } - .bg-opacity-50 { --tw-bg-opacity: 0.5; } + .p-0 { padding: 0px; } @@ -2213,11 +2214,21 @@ html { background-color: rgb(56 189 248 / var(--tw-bg-opacity)); } +.hover\:bg-base-success-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(187 247 208 / var(--tw-bg-opacity)); +} + .hover\:bg-base-success-600:hover { --tw-bg-opacity: 1; background-color: rgb(22 163 74 / var(--tw-bg-opacity)); } +.hover\:bg-base-success-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(21 128 61 / var(--tw-bg-opacity)); +} + .hover\:bg-base-warning-200:hover { --tw-bg-opacity: 1; background-color: rgb(253 230 138 / var(--tw-bg-opacity)); @@ -2796,6 +2807,11 @@ html { background-color: rgb(30 41 59 / var(--tw-bg-opacity)); } + .dark\:hover\:bg-base-success-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(22 163 74 / var(--tw-bg-opacity)); + } + .dark\:hover\:bg-base-warning-600:hover { --tw-bg-opacity: 1; background-color: rgb(217 119 6 / var(--tw-bg-opacity)); diff --git a/src/lib/infrastructure/data/view-model/login.d.ts b/src/lib/infrastructure/data/view-model/login.d.ts index 122bf3015..e58fc976f 100644 --- a/src/lib/infrastructure/data/view-model/login.d.ts +++ b/src/lib/infrastructure/data/view-model/login.d.ts @@ -2,6 +2,8 @@ import { LoginConfigResponse } from "@/lib/core/usecase-models/login-config-usec export interface LoginViewModel extends LoginConfigResponse { isLoggedIn: boolean; + accountActive: string | undefined; + accountsAvailable: string[] | undefined; status: 'success' | 'error'; message?: string; rucioAuthHost: string; diff --git a/src/lib/infrastructure/presenter/login-config-presenter.ts b/src/lib/infrastructure/presenter/login-config-presenter.ts index b09726cc7..cd9a4905c 100644 --- a/src/lib/infrastructure/presenter/login-config-presenter.ts +++ b/src/lib/infrastructure/presenter/login-config-presenter.ts @@ -27,6 +27,8 @@ export default class LoginConfigPresenter extends BasePresenter user.rucioAccount), rucioAuthHost: responseModel.rucioAuthHost, } return { viewModel, status: 200 }; @@ -41,6 +43,8 @@ export default class LoginConfigPresenter extends BasePresenter sessionUser.rucioAccount === account) + + if (userIdx === -1) { + res.status(400).json({ message: 'No authentication found for the specified account' }) + return + } + + const user = session.allUsers?.at(userIdx!) + await addOrUpdateSessionUser(session, user!, true) + res.redirect(callbackUrl as string ?? '/') +} + +export default withSessionRoute(switchUsers) \ No newline at end of file diff --git a/test/component/Login.test.tsx b/test/component/Login.test.tsx index 7025b13b9..7ca53bb82 100644 --- a/test/component/Login.test.tsx +++ b/test/component/Login.test.tsx @@ -140,6 +140,8 @@ describe("Login Page Test", () => { multiVOEnabled: true, voList: getSampleVOs(), isLoggedIn: false, + accountActive: undefined, + accountsAvailable: undefined, status: "success", rucioAuthHost: "https://rucio-auth.cern.ch", } @@ -185,6 +187,8 @@ describe("Login Page Test", () => { multiVOEnabled: true, voList: getSampleVOs(), isLoggedIn: false, + accountActive: undefined, + accountsAvailable: undefined, status: "success", rucioAuthHost: "https://rucio-auth.cern.ch", } @@ -222,6 +226,8 @@ describe("Login Page Test", () => { multiVOEnabled: true, voList: getSampleVOs(), isLoggedIn: false, + accountActive: undefined, + accountsAvailable: undefined, status: "success", rucioAuthHost: "https://rucio-auth.cern.ch", }