Skip to content

Commit

Permalink
Merge branch 'master' into Nekku_meal_integration
Browse files Browse the repository at this point in the history
  • Loading branch information
tahtee committed Feb 7, 2025
2 parents 76ba308 + 4c0ed94 commit 10f3ebf
Show file tree
Hide file tree
Showing 66 changed files with 2,155 additions and 254 deletions.
5 changes: 4 additions & 1 deletion apigw/src/enduser/routes/auth-weak-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { z } from 'zod'

import { EvakaSessionUser } from '../../shared/auth/index.js'
import { toRequestHandler } from '../../shared/express.js'
import { logAuditEvent } from '../../shared/logging.js'
import { logAuditEvent, logWarn } from '../../shared/logging.js'
import { RedisClient } from '../../shared/redis-client.js'
import { citizenWeakLogin } from '../../shared/service-client.js'
import { Sessions } from '../../shared/session.js'
Expand Down Expand Up @@ -45,6 +45,9 @@ export const authWeakLogin = (
const expirySeconds = 60 * 60
await redis.multi().incr(key).expire(key, expirySeconds).exec()
} else {
logWarn('Login request hit rate limit', req, {
username: body.username
})
res.sendStatus(429)
return
}
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/citizen-frontend/async-rendering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
import { useTranslation } from './localization'

function useFailureMessage() {
return useTranslation().common.errors.genericGetError
const t = useTranslation().common.errors
return {
generic: t.genericGetError,
http403: t.http403Error
}
}

const { UnwrapResult, renderResult } = makeHelpers(useFailureMessage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,9 @@ const WeakCredentialsFormModal = React.memo(function WeakCredentialsFormModal({
message={t.unacceptablePassword}
/>
)}
{isUsernameConflict && <AlertBox message={t.usernameConflict} />}
{isUsernameConflict && (
<AlertBox message={t.usernameConflict(username)} />
)}
</FixedSpaceColumn>
</form>
</MutateFormModal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
// SPDX-License-Identifier: LGPL-2.1-or-later

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React, { useCallback, useContext } from 'react'
import React, { useCallback, useContext, useState } from 'react'
import styled from 'styled-components'

import { Failure } from 'lib-common/api'
import { boolean, string } from 'lib-common/form/fields'
import {
chained,
Expand Down Expand Up @@ -507,6 +508,14 @@ const EmailVerificationForm = React.memo(function EmailVerificationForm({
)
const verificationCode = useFormField(form, 'verificationCode')

const [isUsernameConflict, setUsernameConflict] = useState<boolean>(false)
const onFailure = useCallback(
(failure: Failure<unknown>) => {
setUsernameConflict(failure.statusCode === 409)
},
[setUsernameConflict]
)

return (
<FixedSpaceColumn>
<LabelLike>
Expand Down Expand Up @@ -541,11 +550,18 @@ const EmailVerificationForm = React.memo(function EmailVerificationForm({
close={closeInfo}
/>
)}
{isUsernameConflict && (
<AlertBox
message={i18n.personalDetails.loginDetailsSection.usernameConflict(
verification.email
)}
/>
)}
<Gap size="m" />
<MutateButton
data-qa="verify-email"
primary
disabled={!form.isValid()}
disabled={!form.isValid() || isUsernameConflict}
text={t.confirmVerification}
mutation={verifyEmailMutation}
onClick={() => ({
Expand All @@ -562,6 +578,7 @@ const EmailVerificationForm = React.memo(function EmailVerificationForm({
: t.verifyEmail.toast
})
}}
onFailure={onFailure}
/>
</FixedSpaceColumn>
)
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/e2e-test/generated/api-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { DevDocumentTemplate } from './api-types'
import { DevEmployee } from './api-types'
import { DevEmployeePin } from './api-types'
import { DevFamilyContact } from './api-types'
import { DevFinanceNote } from './api-types'
import { DevFosterParent } from './api-types'
import { DevFridgeChild } from './api-types'
import { DevFridgePartner } from './api-types'
Expand Down Expand Up @@ -1091,6 +1092,29 @@ export async function createFeeThresholds(
}


/**
* Generated from fi.espoo.evaka.shared.dev.DevApi.createFinanceNotes
*/
export async function createFinanceNotes(
request: {
body: DevFinanceNote[]
},
options?: { mockedTime?: HelsinkiDateTime }
): Promise<void> {
try {
const { data: json } = await devClient.request<JsonOf<void>>({
url: uri`/finance-notes`.toString(),
method: 'POST',
headers: { EvakaMockedTime: options?.mockedTime?.formatIso() },
data: request.body satisfies JsonCompatible<DevFinanceNote[]>
})
return json
} catch (e) {
throw new DevApiError(e)
}
}


/**
* Generated from fi.espoo.evaka.shared.dev.DevApi.createFosterParent
*/
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/e2e-test/generated/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,14 @@ export interface DevFamilyContact {
priority: number
}

/**
* Generated from fi.espoo.evaka.shared.dev.DevApi.DevFinanceNote
*/
export interface DevFinanceNote {
content: string
personId: PersonId
}

/**
* Generated from fi.espoo.evaka.shared.dev.DevFosterParent
*/
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/e2e-test/pages/employee/guardian-information.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import HelsinkiDateTime from 'lib-common/helsinki-date-time'
import LocalDate from 'lib-common/local-date'
import { formatCents } from 'lib-common/money'
import { UUID } from 'lib-common/types'
Expand Down Expand Up @@ -655,6 +656,16 @@ class InvoiceCorrectionNoteModal extends Modal {
note = new TextInput(this.findByDataQa('note-textarea'))
}

class FinanceNotesAndMessagesSection extends Section {
#noteCreatedAt = this.findAll(`[data-qa="finance-note-created-at"]`)

async checkNoteCreatedAt(nth: number, expectedCreatedAt: HelsinkiDateTime) {
await this.#noteCreatedAt
.nth(nth)
.assertTextEquals(expectedCreatedAt.format())
}
}

const collapsibles = {
personInfo: {
selector: '[data-qa="person-info-collapsible"]',
Expand Down Expand Up @@ -707,6 +718,10 @@ const collapsibles = {
invoiceCorrections: {
selector: '[data-qa="person-invoice-corrections-collapsible"]',
section: InvoiceCorrectionsSection
},
financeNotesAndMessages: {
selector: '[data-qa="person-finance-notes-and-messages-collapsible"]',
section: FinanceNotesAndMessagesSection
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2017-2025 City of Espoo
//
// SPDX-License-Identifier: LGPL-2.1-or-later

import HelsinkiDateTime from 'lib-common/helsinki-date-time'

import config from '../../config'
import { Fixture, testAdult } from '../../dev-api/fixtures'
import {
createFinanceNotes,
resetServiceState
} from '../../generated/api-clients'
import GuardianInformationPage from '../../pages/employee/guardian-information'
import { Page } from '../../utils/page'
import { employeeLogin } from '../../utils/user'

let page: Page
let guardianPage: GuardianInformationPage

beforeEach(async () => {
await resetServiceState()
await Fixture.person(testAdult).saveAdult()
const financeAdmin = await Fixture.employee().financeAdmin().save()

page = await Page.open({})
await employeeLogin(page, financeAdmin)

await page.goto(config.employeeUrl)
guardianPage = new GuardianInformationPage(page)
})

describe('person finance notes', () => {
test('Notes are sorted by created date', async () => {
const createdAtFirst = HelsinkiDateTime.now().subHours(3 * 24)
const createdAtSecond = createdAtFirst.addHours(24)
const createdAtThird = createdAtSecond.addHours(24)

const createFinanceNoteFixture = async (createdAt: HelsinkiDateTime) => {
await createFinanceNotes(
{
body: [{ personId: testAdult.id, content: 'foobar' }]
},
{
mockedTime: createdAt
}
)
}

await createFinanceNoteFixture(createdAtFirst)
await createFinanceNoteFixture(createdAtSecond)
await createFinanceNoteFixture(createdAtThird)

await guardianPage.navigateToGuardian(testAdult.id)
const notes = await guardianPage.openCollapsible('financeNotesAndMessages')

await notes.checkNoteCreatedAt(0, createdAtThird)
await notes.checkNoteCreatedAt(1, createdAtSecond)
await notes.checkNoteCreatedAt(2, createdAtFirst)
})
})
10 changes: 9 additions & 1 deletion frontend/src/employee-frontend/components/PersonProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { UserContext } from '../state/user'
import { getLayout, Layouts } from './layouts'
import FosterChildren from './person-profile/FosterChildren'
import FamilyOverview from './person-profile/PersonFamilyOverview'
import PersonFinanceNotesAndMessages from './person-profile/PersonFinanceNotesAndMessages'
import PersonInvoiceCorrections from './person-profile/PersonInvoiceCorrections'
import PersonVoucherValueDecisions from './person-profile/PersonVoucherValueDecisions'

Expand Down Expand Up @@ -135,11 +136,16 @@ const components = {
PersonApplications,
'READ_APPLICATIONS'
),
decisions: requireOneOfPermittedActions(PersonDecisions, 'READ_DECISIONS')
decisions: requireOneOfPermittedActions(PersonDecisions, 'READ_DECISIONS'),
'notes-and-messages': requireOneOfPermittedActions(
PersonFinanceNotesAndMessages,
'READ_FINANCE_NOTES'
)
}

const layouts: Layouts<typeof components> = {
['ADMIN']: [
{ component: 'notes-and-messages', open: true },
{ component: 'family-overview', open: true },
{ component: 'partners', open: false },
{ component: 'fridge-children', open: false },
Expand Down Expand Up @@ -168,6 +174,7 @@ const layouts: Layouts<typeof components> = {
{ component: 'voucherValueDecisions', open: false }
],
['FINANCE_ADMIN']: [
{ component: 'notes-and-messages', open: true },
{ component: 'family-overview', open: true },
{ component: 'income', open: true },
{ component: 'fee-decisions', open: false },
Expand All @@ -180,6 +187,7 @@ const layouts: Layouts<typeof components> = {
{ component: 'fosterChildren', open: false }
],
['FINANCE_STAFF']: [
{ component: 'notes-and-messages', open: true },
{ component: 'family-overview', open: true },
{ component: 'fee-decisions', open: false },
{ component: 'invoices', open: false },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { useTranslation } from '../state/i18n'

function useFailureMessage() {
const { i18n } = useTranslation()
return i18n.common.loadingFailed
return {
generic: i18n.common.loadingFailed,
http403: i18n.common.noAccess
}
}

const { UnwrapResult, renderResult } = makeHelpers(useFailureMessage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import React from 'react'

import { scopedRoles } from 'lib-common/api-types/employee-auth'
import { boolean, string } from 'lib-common/form/fields'
import { boolean, localDate, string } from 'lib-common/form/fields'
import {
array,
object,
Expand All @@ -25,12 +25,14 @@ import {
EmployeeId,
UserRole
} from 'lib-common/generated/api-types/shared'
import LocalDate from 'lib-common/local-date'
import { SelectF } from 'lib-components/atoms/dropdowns/Select'
import TreeDropdown, {
sortTreeByText,
TreeNode
} from 'lib-components/atoms/dropdowns/TreeDropdown'
import { FixedSpaceColumn } from 'lib-components/layout/flex-helpers'
import { DatePickerF } from 'lib-components/molecules/date-picker/DatePicker'
import { MutateFormModal } from 'lib-components/molecules/modals/FormModal'
import { Label } from 'lib-components/typography'

Expand All @@ -54,7 +56,9 @@ const treeNode = (): Form<DaycareTreeNode, never, DaycareTreeNode, unknown> =>
const form = transformed(
object({
daycareTree: array(treeNode()),
role: required(oneOf<UserRole>())
role: required(oneOf<UserRole>()),
startDate: required(localDate()),
endDate: localDate()
}),
(res) => {
const daycareIds = res.daycareTree
Expand All @@ -64,9 +68,14 @@ const form = transformed(

if (daycareIds.length === 0) return ValidationError.of('required')

if (res.endDate && res.endDate.isBefore(res.startDate))
return ValidationError.field('endDate', 'dateTooEarly')

return ValidationSuccess.of<UpsertEmployeeDaycareRolesRequest>({
daycareIds,
role: res.role
role: res.role,
startDate: res.startDate,
endDate: res.endDate ?? null
})
}
)
Expand All @@ -80,7 +89,8 @@ export default React.memo(function DaycareRolesModal({
units: Daycare[]
onClose: () => void
}) {
const { i18n } = useTranslation()
const { i18n, lang } = useTranslation()

const boundForm = useForm(
form,
() => ({
Expand Down Expand Up @@ -113,12 +123,18 @@ export default React.memo(function DaycareRolesModal({
domValue: r,
label: i18n.roles.adRoles[r]
}))
}
},
startDate: localDate.fromDate(LocalDate.todayInHelsinkiTz(), {
minDate: LocalDate.todayInHelsinkiTz()
}),
endDate: localDate.fromDate(null, {
minDate: LocalDate.todayInHelsinkiTz()
})
}),
i18n.validationErrors
)

const { daycareTree, role } = useFormFields(boundForm)
const { daycareTree, role, startDate, endDate } = useFormFields(boundForm)

return (
<MutateFormModal
Expand All @@ -144,6 +160,14 @@ export default React.memo(function DaycareRolesModal({
<Label>{i18n.employees.editor.unitRoles.role}</Label>
<SelectF bind={role} />
</FixedSpaceColumn>
<FixedSpaceColumn spacing="xs">
<Label>{i18n.employees.editor.unitRoles.startDate}</Label>
<DatePickerF bind={startDate} locale={lang} />
</FixedSpaceColumn>
<FixedSpaceColumn spacing="xs">
<Label>{i18n.employees.editor.unitRoles.endDate}</Label>
<DatePickerF bind={endDate} locale={lang} />
</FixedSpaceColumn>
</FixedSpaceColumn>
</MutateFormModal>
)
Expand Down
Loading

0 comments on commit 10f3ebf

Please sign in to comment.