diff --git a/.editorconfig b/.editorconfig
index 3fd4319..8eca03d 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,7 +8,7 @@ end_of_line = lf
[*.html]
indent_style = space
-indent_size = 2
+indent_size = 4
[*.{js,json,yml}]
indent_style = space
diff --git a/.gitignore b/.gitignore
index 4b49537..79165cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
-# Created by https://www.toptal.com/developers/gitignore/api/node,code,linux,vim,windows,macos
-# Edit at https://www.toptal.com/developers/gitignore?templates=node,code,linux,vim,windows,macos
+# Created by https://www.toptal.com/developers/gitignore/api/vim,node,code,macos,linux,windows,jetbrains+all
+# Edit at https://www.toptal.com/developers/gitignore?templates=vim,node,code,macos,linux,windows,jetbrains+all
### Code ###
.vscode/*
@@ -10,6 +10,95 @@
!.vscode/extensions.json
*.code-workspace
+### JetBrains+all ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### JetBrains+all Patch ###
+# Ignores the whole .idea folder and all .iml files
+# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
+
+.idea/
+
+# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
+
+*.iml
+modules.xml
+.idea/misc.xml
+*.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
### Linux ###
*~
@@ -210,4 +299,4 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
-# End of https://www.toptal.com/developers/gitignore/api/node,code,linux,vim,windows,macos
+# End of https://www.toptal.com/developers/gitignore/api/vim,node,code,macos,linux,windows,jetbrains+all
diff --git a/package-lock.json b/package-lock.json
index d5c57da..cbbcbbc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3112,6 +3112,11 @@
"randomfill": "^1.0.3"
}
},
+ "crypto-js": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz",
+ "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q=="
+ },
"css-blank-pseudo": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz",
@@ -6180,6 +6185,11 @@
"yallist": "^2.1.2"
}
},
+ "lz-string": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
+ "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY="
+ },
"magic-string": {
"version": "0.22.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
@@ -9516,6 +9526,15 @@
"xmlchars": "^2.1.1"
}
},
+ "secure-ls": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/secure-ls/-/secure-ls-1.2.6.tgz",
+ "integrity": "sha512-g8vUSKl6elSfyAUHodybnNkuZW+mUYEOWj4SZIDg+xoQ1dq5ddktBoOFrtxQBUl88ZyAJOtGWQ1PRaOxkTAuZQ==",
+ "requires": {
+ "crypto-js": "^3.1.6",
+ "lz-string": "^1.4.4"
+ }
+ },
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
diff --git a/package.json b/package.json
index de35f8d..f000d3c 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,8 @@
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"bootstrap": "^4.5.3",
"pdf-lib": "^1.11.2",
- "qrcode": "^1.4.4"
+ "qrcode": "^1.4.4",
+ "secure-ls": "^1.2.6"
},
"browserslist": [
"last 5 versions"
diff --git a/src/css/main.css b/src/css/main.css
index 5601120..d37457a 100644
--- a/src/css/main.css
+++ b/src/css/main.css
@@ -154,6 +154,10 @@ p {
transform: translateY(-2px);
}
+#form-profile #formgroup-storedata {
+ user-select: none;
+}
+
@media (prefers-color-scheme: dark) {
#form-profile .form-radio-label .form-check-label {
color: #ddd;
@@ -563,7 +567,7 @@ input:valid+span:after {
}
}
-#snackbar {
+#snackbar, #snackbar-cleardata {
min-width: 250px;
color: #fff;
text-align: center;
@@ -580,7 +584,7 @@ input:valid+span:after {
transition: all 0.5s ease-in-out;
}
-#snackbar.show {
+#snackbar.show, #snackbar-cleardata {
opacity: 1;
}
@@ -645,4 +649,4 @@ input:valid+span:after {
.fieldset-error {
border: 3px solid red;
-}
\ No newline at end of file
+}
diff --git a/src/index.html b/src/index.html
index e0869e0..0eebcba 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1,391 +1,408 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Attestation de déplacement dérogatoire
-
-
-
-
-
-
-
-
-
-
-
- Le code source de ce service est consultable sur GitHub .
-
-
- Ministère de l'Intérieur - DNUM - SDIT
-
-
-
-
-
-
-
-
-
-
- Une nouvelle version est disponible. Cliquer sur le bouton pour l'obtenir.
-
- Mettre à jour
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Attestation de déplacement dérogatoire
+
+
+
+
+
+
+
+
+
+
+
+ Le code source de ce service est consultable sur GitHub .
+
+
+ Ministère de l'Intérieur - DNUM - SDIT
+
+
+
+
+
+
+
+
+
+
+ Une nouvelle version est disponible. Cliquer sur le bouton pour l'obtenir.
+
+ Mettre à jour
+
+
+
+
+
+
diff --git a/src/js/form-util.js b/src/js/form-util.js
index a142c37..409e451 100644
--- a/src/js/form-util.js
+++ b/src/js/form-util.js
@@ -1,156 +1,228 @@
-import { $, $$, downloadBlob } from './dom-utils'
-import { addSlash, getFormattedDate } from './util'
-import pdfBase from '../certificate.pdf'
-import { generatePdf } from './pdf-util'
-
-const formInputs = $$('#form-profile input')
-const snackbar = $('#snackbar')
-const reasonInputs = [...$$('input[name="field-reason"]')]
-const reasonFieldset = $('#reason-fieldset')
-const reasonAlert = reasonFieldset.querySelector('.msg-alert')
-const releaseDateInput = $('#field-datesortie')
-
-const conditions = {
- '#field-firstname': {
- length: 1,
- },
- '#field-lastname': {
- length: 1,
- },
- '#field-birthday': {
- pattern: /^([0][1-9]|[1-2][0-9]|30|31)\/([0][1-9]|10|11|12)\/(19[0-9][0-9]|20[0-1][0-9]|2020)/g,
- },
- '#field-lieunaissance': {
- length: 1,
- },
- '#field-address': {
- length: 1,
- },
- '#field-town': {
- length: 1,
- },
- '#field-zipcode': {
- pattern: /\d{5}/g,
- },
- '#field-datesortie': {
- pattern: /\d{4}-\d{2}-\d{2}/g,
- },
- '#field-heuresortie': {
- pattern: /\d{2}:\d{2}/g,
- },
-}
-
-function validateAriaFields () {
- return Object.keys(conditions)
- .map((field) => {
- const fieldData = conditions[field]
- const pattern = fieldData.pattern
- const length = fieldData.length
- const isInvalidPattern = pattern && !$(field).value.match(pattern)
- const isInvalidLength = length && !$(field).value.length
-
- const isInvalid = !!(isInvalidPattern || isInvalidLength)
-
- $(field).setAttribute('aria-invalid', isInvalid)
- if (isInvalid) {
- $(field).focus()
- }
- return isInvalid
- })
- .includes(true)
-}
-
-export function setReleaseDateTime () {
- const loadedDate = new Date()
- releaseDateInput.value = getFormattedDate(loadedDate)
-}
-
-export function getProfile () {
- const fields = {}
- for (const field of formInputs) {
- if (field.id === 'field-datesortie') {
- const dateSortie = field.value.split('-')
- fields[
- field.id.substring('field-'.length)
- ] = `${dateSortie[2]}/${dateSortie[1]}/${dateSortie[0]}`
- } else {
- fields[field.id.substring('field-'.length)] = field.value
- }
- }
- return fields
-}
-
-export function getReason () {
- const checkedReasonInput = reasonInputs.find(input => input.checked)
- const val = checkedReasonInput?.value
- return val
-}
-
-export function prepareInputs () {
- formInputs.forEach((input) => {
- const exempleElt = input.parentNode.parentNode.querySelector('.exemple')
- const validitySpan = input.parentNode.parentNode.querySelector('.validity')
- if (input.placeholder && exempleElt) {
- input.addEventListener('input', (event) => {
- if (input.value) {
- exempleElt.innerHTML = 'ex. : ' + input.placeholder
- validitySpan.removeAttribute('hidden')
- } else {
- exempleElt.innerHTML = ''
- }
- })
- }
- })
-
- $('#field-birthday').addEventListener('keyup', function (event) {
- event.preventDefault()
- const input = event.target
- const key = event.keyCode || event.charCode
- if (key !== 8 && key !== 46) {
- input.value = addSlash(input.value)
- }
- })
-
- reasonInputs.forEach(radioInput => {
- radioInput.addEventListener('change', function (event) {
- const isInError = reasonInputs.every(input => !input.checked)
- reasonFieldset.classList.toggle('fieldset-error', isInError)
- reasonAlert.classList.toggle('hidden', !isInError)
- })
- })
-
- $('#generate-btn').addEventListener('click', async (event) => {
- event.preventDefault()
-
- const reason = getReason()
- if (!reason) {
- reasonFieldset.classList.add('fieldset-error')
- reasonAlert.classList.remove('hidden')
- reasonFieldset.scrollIntoView && reasonFieldset.scrollIntoView()
- return
- }
-
- const invalid = validateAriaFields()
- if (invalid) {
- return
- }
-
- const pdfBlob = await generatePdf(getProfile(), reason, pdfBase)
-
- const creationInstant = new Date()
- const creationDate = creationInstant.toLocaleDateString('fr-CA')
- const creationHour = creationInstant
- .toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
- .replace(':', '-')
-
- downloadBlob(pdfBlob, `attestation-${creationDate}_${creationHour}.pdf`)
-
- snackbar.classList.remove('d-none')
- setTimeout(() => snackbar.classList.add('show'), 100)
-
- setTimeout(function () {
- snackbar.classList.remove('show')
- setTimeout(() => snackbar.classList.add('d-none'), 500)
- }, 6000)
- })
-}
+import { $, $$, downloadBlob } from './dom-utils'
+import { addSlash, getFormattedDate } from './util'
+import pdfBase from '../certificate.pdf'
+import { generatePdf } from './pdf-util'
+import SecureLS from 'secure-ls'
+
+const secureLS = new SecureLS({ encodingType: 'aes' })
+const formProfile = $('#form-profile')
+const formInputs = $$('#form-profile input')
+const snackbar = $('#snackbar')
+const clearDataSnackbar = $('#snackbar-cleardata')
+const reasonInputs = [...$$('input[name="field-reason"]')]
+const reasonFieldset = $('#reason-fieldset')
+const reasonAlert = reasonFieldset.querySelector('.msg-alert')
+const releaseDateInput = $('#field-datesortie')
+const releaseTimeInput = $('#field-heuresortie')
+const storeDataInput = $('#field-storedata')
+
+const conditions = {
+ '#field-firstname': {
+ length: 1,
+ },
+ '#field-lastname': {
+ length: 1,
+ },
+ '#field-birthday': {
+ pattern: /^([0][1-9]|[1-2][0-9]|30|31)\/([0][1-9]|10|11|12)\/(19[0-9][0-9]|20[0-1][0-9]|2020)/g,
+ },
+ '#field-lieunaissance': {
+ length: 1,
+ },
+ '#field-address': {
+ length: 1,
+ },
+ '#field-town': {
+ length: 1,
+ },
+ '#field-zipcode': {
+ pattern: /\d{5}/g,
+ },
+ '#field-datesortie': {
+ pattern: /\d{4}-\d{2}-\d{2}/g,
+ },
+ '#field-heuresortie': {
+ pattern: /\d{2}:\d{2}/g,
+ },
+}
+
+function validateAriaFields () {
+ return Object.keys(conditions)
+ .map((field) => {
+ const fieldData = conditions[field]
+ const pattern = fieldData.pattern
+ const length = fieldData.length
+ const isInvalidPattern = pattern && !$(field).value.match(pattern)
+ const isInvalidLength = length && !$(field).value.length
+
+ const isInvalid = !!(isInvalidPattern || isInvalidLength)
+
+ $(field).setAttribute('aria-invalid', isInvalid)
+ if (isInvalid) {
+ $(field).focus()
+ }
+ return isInvalid
+ })
+ .includes(true)
+}
+
+function updateSecureLS () {
+ if (wantDataToBeStored() === true) {
+ secureLS.set('profile', getProfile())
+ secureLS.set('reason', getReason())
+ } else {
+ clearSecureLS()
+ }
+}
+
+function clearSecureLS () {
+ secureLS.clear()
+}
+
+function clearForm () {
+ formProfile.reset()
+}
+
+function setCurrentDate () {
+ const currentDate = new Date()
+
+ releaseDateInput.value = getFormattedDate(currentDate)
+ releaseTimeInput.value = currentDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
+}
+
+function showSnackbar (snackbarToShow, showDuration = 6000) {
+ snackbarToShow.classList.remove('d-none')
+ setTimeout(() => snackbarToShow.classList.add('show'), 100)
+
+ setTimeout(function () {
+ snackbarToShow.classList.remove('show')
+ setTimeout(() => snackbarToShow.classList.add('d-none'), 500)
+ }, showDuration)
+}
+
+export function setReleaseDateTime () {
+ const loadedDate = new Date()
+ releaseDateInput.value = getFormattedDate(loadedDate)
+}
+
+export function getProfile () {
+ const fields = {}
+ for (const field of formInputs) {
+ if (field.id === 'field-datesortie') {
+ const dateSortie = field.value.split('-')
+ fields[
+ field.id.substring('field-'.length)
+ ] = `${dateSortie[2]}/${dateSortie[1]}/${dateSortie[0]}`
+ } else {
+ fields[field.id.substring('field-'.length)] = field.value
+ }
+ }
+ return fields
+}
+
+export function getReason () {
+ const checkedReasonInput = reasonInputs.find(input => input.checked)
+ return checkedReasonInput?.value
+}
+
+export function wantDataToBeStored () {
+ return storeDataInput.checked
+}
+
+export function prepareInputs () {
+ const lsProfile = secureLS.get('profile')
+ const lsReason = secureLS.get('reason')
+ const currentDate = new Date()
+ const formattedDate = getFormattedDate(currentDate)
+ const formattedTime = currentDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
+
+ formInputs.forEach((input) => {
+ switch (input.name) {
+ case 'datesortie' :
+ input.value = formattedDate
+ break
+ case 'heuresortie' :
+ input.value = formattedTime
+ break
+ case 'field-reason' :
+ if (lsReason && input.value === lsReason) input.checked = true
+ break
+ case 'storedata' :
+ if (lsReason || lsProfile) input.checked = true
+ break
+ default :
+ if (lsProfile) input.value = lsProfile[input.name]
+ }
+ const exempleElt = input.parentNode.parentNode.querySelector('.exemple')
+ const validitySpan = input.parentNode.parentNode.querySelector('.validity')
+ if (input.placeholder && exempleElt) {
+ input.addEventListener('input', (event) => {
+ if (input.value) {
+ exempleElt.innerHTML = 'ex. : ' + input.placeholder
+ validitySpan.removeAttribute('hidden')
+ } else {
+ exempleElt.innerHTML = ''
+ }
+ })
+ }
+ })
+
+ $('#field-birthday').addEventListener('keyup', function (event) {
+ event.preventDefault()
+ const input = event.target
+ const key = event.keyCode || event.charCode
+ if (key !== 8 && key !== 46) {
+ input.value = addSlash(input.value)
+ }
+ })
+
+ reasonInputs.forEach(radioInput => {
+ radioInput.addEventListener('change', function (event) {
+ const isInError = reasonInputs.every(input => !input.checked)
+ reasonFieldset.classList.toggle('fieldset-error', isInError)
+ reasonAlert.classList.toggle('hidden', !isInError)
+ })
+ })
+
+ $('#formgroup-storedata').addEventListener('click', (event) => {
+ (storeDataInput.checked) ? storeDataInput.checked = false : storeDataInput.checked = true
+ })
+
+ $('#cleardata').addEventListener('click', (event) => {
+ clearSecureLS()
+ clearForm()
+ setCurrentDate()
+ showSnackbar(clearDataSnackbar, 1200)
+ })
+
+ $('#generate-btn').addEventListener('click', async (event) => {
+ event.preventDefault()
+
+ const reason = getReason()
+ if (!reason) {
+ reasonFieldset.classList.add('fieldset-error')
+ reasonAlert.classList.remove('hidden')
+ reasonFieldset.scrollIntoView && reasonFieldset.scrollIntoView()
+ return
+ }
+
+ const invalid = validateAriaFields()
+ if (invalid) {
+ return
+ }
+
+ updateSecureLS()
+
+ const pdfBlob = await generatePdf(getProfile(), reason, pdfBase)
+
+ const creationInstant = new Date()
+ const creationDate = creationInstant.toLocaleDateString('fr-CA')
+ const creationHour = creationInstant
+ .toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
+ .replace(':', '-')
+
+ downloadBlob(pdfBlob, `attestation-${creationDate}_${creationHour}.pdf`)
+
+ showSnackbar(snackbar)
+ })
+}