Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1080: Add start day #1090

Merged
merged 21 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3eb3880
1080: add start day extension, add start day to csv, add start day to…
f1sh1918 Aug 17, 2023
e4974f3
1080: fix uppercase vars
f1sh1918 Aug 17, 2023
343e6be
1076: add startDay column, add test for startDay, rename var, fix pad…
f1sh1918 Aug 21, 2023
68acf67
1076: update graphql schema
f1sh1918 Aug 21, 2023
0981817
Update administration/src/cards/extensions/StartDayExtension.tsx
f1sh1918 Aug 21, 2023
3d7ab9c
Update administration/src/cards/extensions/StartDayExtension.tsx
f1sh1918 Aug 21, 2023
9873353
Update backend/src/main/kotlin/app/ehrenamtskarte/backend/verificatio…
f1sh1918 Aug 21, 2023
9b2ef9f
1076: fix minor issues
f1sh1918 Aug 21, 2023
bd4da2a
Merge remote-tracking branch 'origin/main' into 1080-add-start-date
f1sh1918 Aug 21, 2023
2e276f0
1080: added test for administrtion card hash with startDate, added te…
f1sh1918 Aug 21, 2023
b5d34a1
1080: log card hash
f1sh1918 Aug 21, 2023
fec7935
1080: pass single extension, add TODO
f1sh1918 Aug 22, 2023
6a68f7d
1080: remove form dependency
f1sh1918 Aug 22, 2023
ee66162
Merge branch 'main' into 1080-add-start-date
f1sh1918 Aug 22, 2023
1dbae00
1080: add check for form and csv if startDay is before expirationDay
f1sh1918 Aug 22, 2023
0366631
Update frontend/lib/identification/card_detail_view/card_detail_view.…
f1sh1918 Aug 22, 2023
71a9d59
1080: move expiration check to cardBlueprint, adjust order of validit…
f1sh1918 Aug 23, 2023
6d8fd6c
Merge remote-tracking branch 'origin/1080-add-start-date' into 1080-a…
f1sh1918 Aug 23, 2023
31b7803
Merge branch 'main' into 1080-add-start-date
f1sh1918 Aug 23, 2023
03af797
1080: move card notYetValid before invalid for CardStatus
f1sh1918 Aug 23, 2023
2a7061a
Merge remote-tracking branch 'origin/1080-add-start-date' into 1080-a…
f1sh1918 Aug 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Name,Ablaufdatum,Geburtsdatum,Passnummer,Adresszeile 1,Adresszeile 2,PLZ,Ort,Pass-ID
Thea Test,03.3.2026,05.04.1992,12345678,Wertachstraße 29,,86153,Augsburg,35252
Tilo Torvitz,03.3.2026,13.03.1988,76543210,Teststraße 2,,85353,Augsburg,87456424
Torben Trost,03.3.2026,15.03.2010,54216782,2. Tür rechts,Ringstraße 16,81375,München,4234
Torben Trost,03.3.2026,15.03.2010,54216782,,,,,74532
Name,Ablaufdatum,Startdatum,Geburtsdatum,Passnummer,Adresszeile 1,Adresszeile 2,PLZ,Ort,Pass-ID
Thea Test,03.3.2026,03.3.2024,05.04.1992,12345678,Wertachstraße 29,,86153,Augsburg,35252
Tilo Torvitz,03.3.2026,03.5.2024,13.03.1988,76543210,Teststraße 2,,85353,Augsburg,87456424
Torben Trost,03.3.2026,07.3.2026,15.03.2010,54216782,2. Tür rechts,Ringstraße 16,81375,München,4234
Torben Trost,03.3.2026,03.9.2024,15.03.2010,54216782,,,,,74532
10 changes: 2 additions & 8 deletions administration/src/bp-modules/cards/AddCardForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,13 @@ interface CreateCardsFormProps {
onRemove: () => void
}

export const maxCardValidity = { years: 99 }
const ExtensionForm = ({ extension, onUpdate }: ExtensionFormProps) => {
return extension.createForm(() => {
onUpdate()
})
}

const maxCardValidity = { years: 99 }
const hasCardExpirationError = (expirationDate: PlainDate): boolean => {
const today = PlainDate.fromLocalDate(new Date())

return expirationDate.isBefore(today) || expirationDate.isAfter(today.add(maxCardValidity))
}

const CreateCardForm = ({ cardBlueprint, onRemove, onUpdate }: CreateCardsFormProps) => {
const today = PlainDate.fromLocalDate(new Date())
return (
Expand Down Expand Up @@ -66,7 +60,7 @@ const CreateCardForm = ({ cardBlueprint, onRemove, onUpdate }: CreateCardsFormPr
type='date'
required
size='small'
error={cardBlueprint.expirationDate ? hasCardExpirationError(cardBlueprint.expirationDate) : true}
error={!cardBlueprint.isExpirationDateValid()}
value={cardBlueprint.expirationDate ? cardBlueprint.expirationDate.toString() : null}
sx={{ '& input[value=""]:not(:focus)': { color: 'transparent' }, '& fieldset': { borderRadius: 0 } }}
inputProps={{
Expand Down
18 changes: 16 additions & 2 deletions administration/src/cards/CardBlueprint.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { PartialMessage } from '@bufbuild/protobuf'

import { maxCardValidity } from '../bp-modules/cards/AddCardForm'
import { CardExtensions, CardInfo, DynamicActivationCode, QrCode, StaticVerificationCode } from '../generated/card_pb'
import { Region } from '../generated/graphql'
import { CardConfig } from '../project-configs/getProjectConfig'
import PlainDate from '../util/PlainDate'
import { isContentLengthValid } from '../util/qrcode'
import RegionExtension from './extensions/RegionExtension'
import { Extension, ExtensionInstance, JSONExtension } from './extensions/extensions'
import StartDayExtension from './extensions/StartDayExtension'
import { Extension, ExtensionInstance, JSONExtension, findExtension } from './extensions/extensions'
import { PEPPER_LENGTH } from './hashCardInfo'

// Due to limited space on the cards
Expand Down Expand Up @@ -83,9 +85,21 @@ export class CardBlueprint {
)
}

isStartDayBeforeExpirationDay = (expirationDate: PlainDate): boolean => {
const startDayExtension = findExtension(this.extensions, StartDayExtension)
return startDayExtension?.state?.startDay
? PlainDate.fromDaysSinceEpoch(startDayExtension.state.startDay).isBefore(expirationDate)
: true
}

isExpirationDateValid(): boolean {
const today = PlainDate.fromLocalDate(new Date())
return this.expirationDate !== null && this.expirationDate.isAfter(today)
return (
this.expirationDate !== null &&
this.expirationDate.isAfter(today) &&
!this.expirationDate.isAfter(today.add(maxCardValidity)) &&
this.isStartDayBeforeExpirationDay(this.expirationDate)
)
}

setExpirationDate(value: string) {
Expand Down
2 changes: 2 additions & 0 deletions administration/src/cards/createCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ async function createCards(client: ApolloClient<object>, activationCodes: Codes,
const codeType = code instanceof DynamicActivationCode ? CodeType.Dynamic : CodeType.Static
const cardInfoHash = await hashCardInfo(code.info!, code.pepper)
const expirationDay = code.info!.expirationDay
const startDay = code.info!.extensions?.extensionStartDay?.startDay
const activationSecretBase64 =
code instanceof DynamicActivationCode ? uint8ArrayToBase64(code.activationSecret) : null
return {
Expand All @@ -36,6 +37,7 @@ async function createCards(client: ApolloClient<object>, activationCodes: Codes,
activationSecretBase64: activationSecretBase64,
regionId: region.id,
codeType,
cardStartDay: startDay,
}
})
)
Expand Down
87 changes: 87 additions & 0 deletions administration/src/cards/extensions/StartDayExtension.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { FormGroup } from '@blueprintjs/core'
import { PartialMessage } from '@bufbuild/protobuf'
import { TextField } from '@mui/material'

import { CardExtensions } from '../../generated/card_pb'
import PlainDate from '../../util/PlainDate'
import { Extension } from './extensions'

type StartDayState = { startDay: number }

class StartDayExtension extends Extension<StartDayState, null> {
public readonly name = StartDayExtension.name

setInitialState() {
const today = PlainDate.fromLocalDate(new Date())
this.state = { startDay: today.toDaysSinceEpoch() }
}

createForm(onUpdate: () => void) {
const startDayDate =
this.state?.startDay !== undefined
? PlainDate.fromDaysSinceEpoch(this.state.startDay)
: PlainDate.fromLocalDate(new Date())

return (
<FormGroup label='Startdatum'>
<TextField
fullWidth
type='date'
required
size='small'
error={!this.isValid()}
value={startDayDate.toString()}
sx={{ '& input[value=""]:not(:focus)': { color: 'transparent' }, '& fieldset': { borderRadius: 0 } }}
inputProps={{
style: { fontSize: 14, padding: '6px 10px' },
}}
onChange={e => {
if (e.target.value !== null) {
try {
const date = PlainDate.from(e.target.value)
this.state = { startDay: date.toDaysSinceEpoch() }
onUpdate()
} catch (error) {
console.error("Could not parse date from string '" + e.target.value + "'.", error)
}
}
}}
/>
</FormGroup>
)
}

causesInfiniteLifetime() {
return false
}

setProtobufData(message: PartialMessage<CardExtensions>) {
message.extensionStartDay = {
startDay: this.state?.startDay,
}
}

isValid() {
return this.state !== null
}

/**
* fromString is only used for the CSV import.
* The expected format is dd.MM.yyyy
* @param value The date formatted using dd.MM.yyyy
*/
fromString(value: string) {
try {
const startDay = PlainDate.fromCustomFormat(value, 'dd.MM.yyyy')
this.state = { startDay: startDay.toDaysSinceEpoch() }
} catch (e) {
this.state = null
}
}

toString() {
return this.state ? PlainDate.fromDaysSinceEpoch(this.state.startDay).format('dd.MM.yyyy') : ''
}
}

export default StartDayExtension
2 changes: 2 additions & 0 deletions administration/src/cards/extensions/extensions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import BirthdayExtension from './BirthdayExtension'
import NuernbergPassIdExtension from './NuernbergPassIdExtension'
import NuernbergPassNumberExtension from './NuernbergPassNumberExtension'
import RegionExtension from './RegionExtension'
import StartDayExtension from './StartDayExtension'

export const findExtension = <E extends ExtensionClass>(
array: ExtensionInstance[],
Expand Down Expand Up @@ -39,5 +40,6 @@ export type ExtensionClass =
| typeof NuernbergPassNumberExtension
| typeof NuernbergPassIdExtension
| typeof RegionExtension
| typeof StartDayExtension
| (typeof AddressExtensions)[number]
export type ExtensionInstance = InstanceType<ExtensionClass>
54 changes: 54 additions & 0 deletions administration/src/cards/hashCardInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CardInfo,
NuernbergPassNumberExtension,
RegionExtension,
StartDayExtension,
} from '../generated/card_pb'
import { base64ToUint8Array, uint8ArrayToBase64 } from '../util/base64'
import hashCardInfo, { messageToJsonObject } from './hashCardInfo'
Expand Down Expand Up @@ -99,6 +100,36 @@ describe('messageToJsonObject', () => {
},
})
})

it('should map a cardInfo for a Nuernberg Pass with startDay correctly', () => {
const cardInfo = new CardInfo({
fullName: 'Max Mustermann',
expirationDay: 365 * 40, // Equals 14.600
extensions: new CardExtensions({
extensionBirthday: new BirthdayExtension({
birthday: -365 * 10,
}),
extensionNuernbergPassNumber: new NuernbergPassNumberExtension({
passNumber: 99999999,
}),
extensionRegion: new RegionExtension({
regionId: 93,
}),
extensionStartDay: new StartDayExtension({ startDay: 365 * 2 }),
}),
})

expect(messageToJsonObject(cardInfo)).toEqual({
'1': 'Max Mustermann',
'2': '14600',
'3': {
'1': { '1': '93' }, // extensionRegion
'2': { '1': '-3650' }, // extensionBirthday
'3': { '1': '99999999' }, // extensionNuernbergPassNumber
'5': { '1': '730' }, // extensionStartDay
},
})
})
})

describe('hashCardInfo', () => {
Expand Down Expand Up @@ -162,4 +193,27 @@ describe('hashCardInfo', () => {

expect(uint8ArrayToBase64(hash)).toBe('zogEJOhnSSp//8qhym/DdorQYgL/763Kfq4slWduxMg=')
})

it('should be stable for a Nuernberg Pass with startDay', async () => {
const cardInfo = new CardInfo({
fullName: 'Max Mustermann',
expirationDay: 365 * 40, // Equals 14.600
extensions: new CardExtensions({
extensionRegion: new RegionExtension({
regionId: 93,
}),
extensionBirthday: new BirthdayExtension({
birthday: -365 * 10,
}),
extensionNuernbergPassNumber: new NuernbergPassNumberExtension({
passNumber: 99999999,
}),
extensionStartDay: new StartDayExtension({ startDay: 365 * 2 }),
}),
})
const pepper = base64ToUint8Array('MvMjEqa0ulFDAgACElMjWA==')
const hash = await hashCardInfo(cardInfo, pepper)

expect(uint8ArrayToBase64(hash)).toBe('1ChHiAvWygwu+bH2yOZOk1zdmwTDZ4mkvu079cyuLjE=')
})
})
3 changes: 3 additions & 0 deletions administration/src/project-configs/nuernberg/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import BirthdayExtension from '../../cards/extensions/BirthdayExtension'
import NuernbergPassIdExtension from '../../cards/extensions/NuernbergPassIdExtension'
import NuernbergPassNumberExtension from '../../cards/extensions/NuernbergPassNumberExtension'
import RegionExtension from '../../cards/extensions/RegionExtension'
import StartDayExtension from '../../cards/extensions/StartDayExtension'
import { ProjectConfig } from '../getProjectConfig'
import ActivityLogEntry from './ActivityLogEntry'
import { DataPrivacyBaseText, dataPrivacyBaseHeadline } from './dataPrivacyBase'
Expand All @@ -17,6 +18,7 @@ const config: ProjectConfig = {
nameColumnName: 'Name',
expiryColumnName: 'Ablaufdatum',
extensionColumnNames: [
'Startdatum',
'Geburtsdatum',
'Passnummer',
'Pass-ID',
Expand All @@ -28,6 +30,7 @@ const config: ProjectConfig = {
],
defaultValidity: { years: 1 },
extensions: [
StartDayExtension,
BirthdayExtension,
NuernbergPassNumberExtension,
NuernbergPassIdExtension,
Expand Down
3 changes: 2 additions & 1 deletion administration/src/project-configs/nuernberg/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ const renderPdfDetails = ({ info, cardBlueprint }: InfoParams) => {
const passId = findExtension(cardBlueprint.extensions, NuernbergPassIdExtension)?.state?.nuernbergPassId
const expirationDate = PlainDate.fromDaysSinceEpoch(expirationDay)
const birthdayDate = PlainDate.fromDaysSinceEpoch(info.extensions?.extensionBirthday?.birthday ?? 0)
const startDate = PlainDate.fromDaysSinceEpoch(info.extensions?.extensionStartDay?.startDay ?? 0)
return `${info.fullName}
Pass-ID: ${passId ?? ''}
Geburtsdatum: ${birthdayDate.format('dd.MM.yyyy')}
Gültig bis: ${expirationDate.format('dd.MM.yyyy')}`
Gültig: ${startDate.format('dd.MM.yyyy')} bis ${expirationDate.format('dd.MM.yyyy')}`
f1sh1918 marked this conversation as resolved.
Show resolved Hide resolved
}

const createAddressFormFields = (form: PDFForm, pageIdx: number, { info, cardBlueprint }: InfoParams) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ object MigrationsRegistry {
V0003_AddFirstActivationDate(),
V0004_AddNotificationSettings(),
V0005_AddRegionApplicationActivation(),
V0006_AddStoreCreatedDate()
V0006_AddStoreCreatedDate(),
V0007_AddStartDay()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package app.ehrenamtskarte.backend.migration.migrations

import app.ehrenamtskarte.backend.migration.Migration
import app.ehrenamtskarte.backend.migration.Statement

/**
* Add startDay column to cards. startDay can be null.
*/
@Suppress("ClassName")
internal class V0007_AddStartDay() : Migration() {
override val migrate: Statement = {
exec(
"""
ALTER TABLE cards ADD "startDay" BIGINT NULL;
""".trimIndent()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,18 @@ class ValidityPeriodUtil {
fun daysSinceEpochToDate(days: Long): LocalDate {
return LocalDate.of(1970, Month.JANUARY, 1).plusDays(days)
}

fun isOnOrAfterToday(maxInclusiveDay: LocalDate, timezone: ZoneId): Boolean {
return isOnOrAfterToday(maxInclusiveDay, Clock.system(timezone))
}

fun isOnOrAfterToday(maxInclusiveDay: LocalDate, clock: Clock): Boolean {
// Cards issues for day == 0 are never valid!
if (maxInclusiveDay.isEqual(LocalDate.of(1970, Month.JANUARY, 1))) {
return false
}
// not before includes the current day
return !LocalDate.now(clock).isBefore(maxInclusiveDay)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ object Cards : IntIdTable() {

// Days since 1970-01-01. For more information refer to the card.proto,
// Using long because unsigned ints are not available, but we want to be able to represent them.
// If this field is null, the card is valid forever.
val expirationDay = long("expirationDay").nullable()
val issueDate = timestamp("issueDate").defaultExpression(CurrentTimestamp())
val revoked = bool("revoked")
Expand All @@ -36,6 +37,11 @@ object Cards : IntIdTable() {
val codeType = enumeration("codeType", CodeType::class)
val firstActivationDate = timestamp("firstActivationDate").nullable()

// startDay describes the first day on which the card is valid.
// If this field is null, the card is valid until `expirationDay` without explicitly stating when the validity period started.
// Days since 1970-01-01. For more information refer to the card.proto,
val startDay = long("startDay").nullable()
f1sh1918 marked this conversation as resolved.
Show resolved Hide resolved

init {
check("CodeTypeConstraint") {
(
Expand All @@ -61,4 +67,5 @@ class CardEntity(id: EntityID<Int>) : IntEntity(id) {
var issuerId by Cards.issuerId
var codeType by Cards.codeType
var firstActivationDate by Cards.firstActivationDate
var startDay by Cards.startDay
}
Loading