Skip to content

Commit

Permalink
Improve weak login form
Browse files Browse the repository at this point in the history
- replaces Keycloak link when the feature flag is enabled
  • Loading branch information
Gekkio committed Jan 22, 2025
1 parent 1fbb18f commit 573a419
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 109 deletions.
9 changes: 9 additions & 0 deletions frontend/src/citizen-frontend/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import IncomeStatementView from './income-statements/IncomeStatementView'
import IncomeStatements from './income-statements/IncomeStatements'
import { Localization, useTranslation } from './localization'
import LoginPage from './login/LoginPage'
import LoginFormPage from './login/WeakLoginFormPage'
import MapPage from './map/MapPage'
import MessagesPage from './messages/MessagesPage'
import { MessageContextProvider } from './messages/state'
Expand Down Expand Up @@ -141,6 +142,14 @@ export default createBrowserRouter([
path: '/',
element: <App />,
children: [
{
path: '/login/form',
element: (
<ScrollToTop>
<LoginFormPage />
</ScrollToTop>
)
},
{
path: '/login',
element: (
Expand Down
126 changes: 26 additions & 100 deletions frontend/src/citizen-frontend/login/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,13 @@
// SPDX-License-Identifier: LGPL-2.1-or-later

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { useMemo, useState } from 'react'
import { Link, Navigate, useSearchParams } from 'react-router'
import React, { useState } from 'react'
import { Link, Navigate, useNavigate, useSearchParams } from 'react-router'
import styled from 'styled-components'

import { wrapResult } from 'lib-common/api'
import { string } from 'lib-common/form/fields'
import { object, validated } from 'lib-common/form/form'
import { useForm, useFormFields } from 'lib-common/form/hooks'
import { useQueryResult } from 'lib-common/query'
import { parseUrlWithOrigin } from 'lib-common/utils/parse-url-with-origin'
import Main from 'lib-components/atoms/Main'
import { AsyncButton } from 'lib-components/atoms/buttons/AsyncButton'
import LinkButton from 'lib-components/atoms/buttons/LinkButton'
import { InputFieldF } from 'lib-components/atoms/form/InputField'
import Container, { ContentArea } from 'lib-components/layout/Container'
import { FixedSpaceColumn } from 'lib-components/layout/flex-helpers'
import {
Expand All @@ -34,10 +27,13 @@ import { featureFlags } from 'lib-customizations/citizen'
import { farMap } from 'lib-icons'

import Footer from '../Footer'
import { authWeakLogin } from '../auth/api'
import { useUser } from '../auth/state'
import { useTranslation } from '../localization'
import { getStrongLoginUri, getWeakLoginUri } from '../navigation/const'
import {
getStrongLoginUri,
getWeakKeycloakLoginUri,
getWeakLoginUri
} from '../navigation/const'

import { systemNotificationsQuery } from './queries'

Expand All @@ -51,6 +47,7 @@ export default React.memo(function LoginPage() {

const [searchParams] = useSearchParams()
const unvalidatedNextPath = searchParams.get('next')
const navigate = useNavigate()

const [showInfoBoxText1, setShowInfoBoxText1] = useState(false)
const [showInfoBoxText2, setShowInfoBoxText2] = useState(false)
Expand Down Expand Up @@ -104,14 +101,24 @@ export default React.memo(function LoginPage() {
/>
)}
<Gap size="s" />
<LinkButton
href={getWeakLoginUri(unvalidatedNextPath ?? '/')}
data-qa="weak-login"
>
{i18n.loginPage.login.link}
</LinkButton>
{featureFlags.weakLogin && (
<WeakLoginForm unvalidatedNextPath={unvalidatedNextPath} />
{featureFlags.weakLogin ? (
<LinkButton
href={getWeakLoginUri(unvalidatedNextPath ?? '/')}
onClick={(e) => {
e.preventDefault()
void navigate(getWeakLoginUri(unvalidatedNextPath ?? '/'))
}}
data-qa="weak-login"
>
{i18n.loginPage.login.link}
</LinkButton>
) : (
<LinkButton
href={getWeakKeycloakLoginUri(unvalidatedNextPath ?? '/')}
data-qa="weak-login"
>
{i18n.loginPage.login.link}
</LinkButton>
)}
</ContentArea>
<ContentArea opaque>
Expand Down Expand Up @@ -159,87 +166,6 @@ export default React.memo(function LoginPage() {
)
})

const weakLoginForm = validated(
object({
username: string(),
password: string()
}),
(form) => {
if (form.username.length === 0 || form.password.length === 0) {
return 'required'
}
return undefined
}
)

const authWeakLoginResult = wrapResult(authWeakLogin)

const WeakLoginForm = React.memo(function WeakLogin({
unvalidatedNextPath
}: {
unvalidatedNextPath: string | null
}) {
const i18n = useTranslation()
const [rateLimitError, setRateLimitError] = useState(false)

const nextUrl = useMemo(
() =>
unvalidatedNextPath
? parseUrlWithOrigin(window.location, unvalidatedNextPath)
: undefined,
[unvalidatedNextPath]
)

const form = useForm(
weakLoginForm,
() => ({ username: '', password: '' }),
i18n.validationErrors
)
const { username, password } = useFormFields(form)
return (
<>
<Gap size="m" />
<form action="" onSubmit={(e) => e.preventDefault()}>
<FixedSpaceColumn spacing="xs">
{rateLimitError && (
<AlertBox message={i18n.loginPage.login.rateLimitError} />
)}
<InputFieldF
autoComplete="email"
bind={username}
placeholder={i18n.loginPage.login.email}
width="L"
hideErrorsBeforeTouched={true}
/>
<InputFieldF
autoComplete="current-password"
bind={password}
type="password"
placeholder={i18n.loginPage.login.password}
width="L"
hideErrorsBeforeTouched={true}
/>
<AsyncButton
primary
type="submit"
text={i18n.loginPage.login.link}
disabled={!form.isValid()}
onClick={() =>
authWeakLoginResult(form.state.username, form.state.password)
}
onSuccess={() => window.location.replace(nextUrl ?? '/')}
onFailure={(error) => {
if (error.statusCode === 429) {
setRateLimitError(true)
}
}}
/>
</FixedSpaceColumn>
</form>
</>
)
})

const MapLink = styled(Link)`
text-decoration: none;
display: inline-block;
Expand Down
148 changes: 148 additions & 0 deletions frontend/src/citizen-frontend/login/WeakLoginFormPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2017-2025 City of Espoo
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import React, { useMemo, useState } from 'react'
import { Navigate, useSearchParams } from 'react-router'

import { authWeakLogin } from 'citizen-frontend/auth/api'
import { wrapResult } from 'lib-common/api'
import { string } from 'lib-common/form/fields'
import { object, required, validated } from 'lib-common/form/form'
import { useForm, useFormFields } from 'lib-common/form/hooks'
import { parseUrlWithOrigin } from 'lib-common/utils/parse-url-with-origin'
import Main from 'lib-components/atoms/Main'
import { AsyncButton } from 'lib-components/atoms/buttons/AsyncButton'
import ReturnButton from 'lib-components/atoms/buttons/ReturnButton'
import { InputFieldF } from 'lib-components/atoms/form/InputField'
import Container, { ContentArea } from 'lib-components/layout/Container'
import { FixedSpaceColumn } from 'lib-components/layout/flex-helpers'
import {
MobileOnly,
TabletAndDesktop
} from 'lib-components/layout/responsive-layout'
import ExpandingInfo from 'lib-components/molecules/ExpandingInfo'
import { AlertBox } from 'lib-components/molecules/MessageBoxes'
import { H1, Label } from 'lib-components/typography'
import { Gap } from 'lib-components/white-space'

import Footer from '../Footer'
import { useUser } from '../auth/state'
import { useTranslation } from '../localization'

export default React.memo(function WeakLoginFormPage() {
const i18n = useTranslation()
const user = useUser()

const [searchParams] = useSearchParams()
const unvalidatedNextPath = searchParams.get('next')

if (user) {
return <Navigate to="/" replace />
}

return (
<Main>
<TabletAndDesktop>
<Gap size="L" />
</TabletAndDesktop>
<MobileOnly>
<Gap size="xs" />
</MobileOnly>
<Container>
<FixedSpaceColumn spacing="s">
<ReturnButton label={i18n.common.goBack} data-qa="navigate-back" />
<ContentArea opaque>
<H1 noMargin>{i18n.loginPage.login.title}</H1>
<Gap size="m" />
<WeakLoginForm unvalidatedNextPath={unvalidatedNextPath} />
</ContentArea>
</FixedSpaceColumn>
</Container>
<Footer />
</Main>
)
})

const weakLoginForm = object({
username: validated(required(string()), (v) =>
v.length > 0 ? undefined : 'required'
),
password: validated(required(string()), (v) =>
v.length > 0 ? undefined : 'required'
)
})

const authWeakLoginResult = wrapResult(authWeakLogin)

const WeakLoginForm = React.memo(function WeakLogin({
unvalidatedNextPath
}: {
unvalidatedNextPath: string | null
}) {
const i18n = useTranslation()
const t = i18n.loginPage.login
const [rateLimitError, setRateLimitError] = useState(false)

const nextUrl = useMemo(
() =>
unvalidatedNextPath
? parseUrlWithOrigin(window.location, unvalidatedNextPath)
: undefined,
[unvalidatedNextPath]
)

const form = useForm(
weakLoginForm,
() => ({ username: '', password: '' }),
i18n.validationErrors
)
const { username, password } = useFormFields(form)
return (
<form action="" onSubmit={(e) => e.preventDefault()}>
<FixedSpaceColumn spacing="L">
{rateLimitError && <AlertBox message={t.rateLimitError} />}
<FixedSpaceColumn spacing="zero">
<Label htmlFor="username">{t.username}</Label>
<InputFieldF
autoComplete="username"
bind={username}
placeholder={t.username}
width="L"
hideErrorsBeforeTouched={true}
/>
</FixedSpaceColumn>
<FixedSpaceColumn spacing="zero">
<Label htmlFor="email">{t.password}</Label>
<InputFieldF
autoComplete="current-password"
bind={password}
type="password"
placeholder={t.password}
width="L"
hideErrorsBeforeTouched={true}
/>
</FixedSpaceColumn>
<AsyncButton
primary
type="submit"
text={t.link}
disabled={!form.isValid()}
onClick={() =>
authWeakLoginResult(form.state.username, form.state.password)
}
onSuccess={() => window.location.replace(nextUrl ?? '/')}
onFailure={(error) => {
if (error.statusCode === 429) {
setRateLimitError(true)
}
}}
/>
<ExpandingInfo info={t.forgotPasswordInfo}>
{t.forgotPassword}
</ExpandingInfo>
<ExpandingInfo info={t.noUsernameInfo}>{t.noUsername}</ExpandingInfo>
</FixedSpaceColumn>
</form>
)
})
6 changes: 5 additions & 1 deletion frontend/src/citizen-frontend/navigation/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

export const logoutUrl = `/api/citizen/auth/logout?RelayState=/`

export const getWeakLoginUri = (
export const getWeakKeycloakLoginUri = (
url = `${window.location.pathname}${window.location.search}${window.location.hash}`
) => `/api/citizen/auth/keycloak/login?RelayState=${encodeURIComponent(url)}`

export const getWeakLoginUri = (
url = `${window.location.pathname}${window.location.search}${window.location.hash}`
) => `/login/form?next=${encodeURIComponent(url)}`

export const getStrongLoginUri = (
url = `${window.location.pathname}${window.location.search}${window.location.hash}`
) => `/api/citizen/auth/sfi/login?RelayState=${encodeURIComponent(url)}`
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/lib-customizations/defaults/citizen/i18n/en.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,14 @@ const en: Translations = {
</P>
</>
),
email: 'E-mail',
username: 'Username',
password: 'Password',
rateLimitError:
'Your account has been temporarily locked due to a large number of login attempts. Please try again later.'
'Your account has been temporarily locked due to a large number of login attempts. Please try again later.',
forgotPassword: 'Forgot password?',
forgotPasswordInfo: 'TODO',
noUsername: 'No username?',
noUsernameInfo: 'TODO'
},
applying: {
title: 'Sign in using Suomi.fi',
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/lib-customizations/defaults/citizen/i18n/fi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export default {
},
closeModal: 'Sulje ponnahdusikkuna',
close: 'Sulje',
goBack: 'Takaisin',
duplicatedChild: {
identifier: {
DAYCARE: {
Expand Down Expand Up @@ -212,10 +213,16 @@ export default {
</P>
</>
),
email: 'Sähköpostiosoite',
username: 'Käyttäjätunnus',
password: 'Salasana',
rateLimitError:
'Käyttäjätunnuksesi on väliaikaisesti lukittu kirjautumisyritysten määrästä johtuen. Kokeile myöhemmin uudelleen.'
'Käyttäjätunnuksesi on väliaikaisesti lukittu kirjautumisyritysten määrästä johtuen. Kokeile myöhemmin uudelleen.',
forgotPassword: 'Unohditko salasanasi?',
forgotPasswordInfo:
'Voit vaihtaa salasanan omissa tiedoissasi kirjautumalla pankkitunnuksilla.',
noUsername: 'Ei käyttäjätunnuksia?',
noUsernameInfo:
'Voit luoda käyttäjätunnuksen kirjautumalla pankkitunnuksilla ja sallimalla kirjautumisen sähköpostilla "Omat tiedot"-sivulla'
},
applying: {
title: 'Kirjaudu Suomi.fi:ssä',
Expand Down
Loading

0 comments on commit 573a419

Please sign in to comment.