├── .devcontainer.json ├── .dockerignore ├── .eslintrc.cjs ├── .github └── workflows │ ├── deploy.yml │ ├── main.yml │ └── renew_certificates.yml ├── .gitignore ├── .node-version ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── README.md ├── docs └── screenshots │ ├── event.jpg │ ├── main.jpg │ └── test.jpg ├── fetch_certificates.js ├── netlify.toml ├── package-lock.json ├── package.json ├── src ├── app.html ├── assets │ ├── DCC.combined-schema.1.3.0.json │ ├── Digital_Green_Certificate_Signing_Keys.json │ ├── blacklist_text.json │ └── validity_data.json ├── global.d.ts ├── hooks.ts ├── lib │ ├── 2ddoc.ts │ ├── 2ddoc_check_signature.ts │ ├── TooltipFix.svelte │ ├── common_certificate_info.ts │ ├── database.ts │ ├── detect_certificate.ts │ ├── digital_green_certificate.ts │ ├── digital_green_certificate_types.ts │ ├── event.ts │ ├── file_types.ts │ ├── global_config.ts │ ├── http.ts │ ├── invitees.ts │ ├── random_key.ts │ ├── sha256.ts │ ├── storage.ts │ └── tac_verif_rules.ts ├── routes │ ├── LICENSE.svelte │ ├── _Certificate.svelte │ ├── _Certificate2ddocDetails.svelte │ ├── _Certificate2ddocTestInfo.svelte │ ├── _Certificate2ddocVaccineInfo.svelte │ ├── _CertificateDGCDetails.svelte │ ├── _CodeFound.svelte │ ├── _QrCodeVideoReader.svelte │ ├── __error.svelte │ ├── __layout.svelte │ ├── _invitedToStore.ts │ ├── _myWalletStore.ts │ ├── _showPromiseError.svelte │ ├── api-pass-sanitaire.svelte │ ├── api │ │ ├── borne │ │ │ └── [key].ts │ │ ├── create_event.ts │ │ ├── event-[private_code] │ │ │ ├── event.json.ts │ │ │ └── invite.json.ts │ │ ├── file │ │ │ ├── [key].ts │ │ │ └── config.json.ts │ │ ├── publicevent-[public_code] │ │ │ ├── event.json.ts │ │ │ └── invite.json.ts │ │ ├── test.ts │ │ └── validate_pass.ts │ ├── apropos.svelte │ ├── articles.svelte │ ├── borne │ │ ├── _connectionIndicator.svelte │ │ ├── _scan.svelte │ │ ├── _scan_stats_modal.svelte │ │ ├── _slideshow.svelte │ │ ├── _stats_chart.svelte │ │ ├── _stats_storage.ts │ │ ├── _validationMessage.svelte │ │ ├── config │ │ │ ├── _config.ts │ │ │ ├── _config_layout.svelte │ │ │ ├── _config_storage.ts │ │ │ ├── _external_request_config.svelte │ │ │ ├── _file_upload.svelte │ │ │ ├── _sound_picker.svelte │ │ │ ├── _tab_display.svelte │ │ │ ├── _tab_technical.svelte │ │ │ ├── custom-css-documentation.svelte │ │ │ ├── index.svelte │ │ │ ├── lecteur-physique.svelte │ │ │ └── simple.svelte │ │ ├── index.svelte │ │ └── statistiques.svelte │ ├── events │ │ ├── [eventId].svelte │ │ ├── _addInvitee.svelte │ │ ├── _communicate.svelte │ │ ├── _inviteesList.svelte │ │ ├── _my_events.svelte │ │ └── index.svelte │ ├── french-health-pass.svelte │ ├── fullscreen.svelte │ ├── import │ │ ├── __layout.svelte │ │ ├── file.svelte │ │ ├── text.svelte │ │ └── video.svelte │ ├── index.svelte │ ├── migration.svelte │ ├── offline.html.svelte │ ├── probleme-pass-sanitaire.svelte │ └── wallet.svelte ├── service-worker.ts └── types │ └── cbor-web │ └── index.d.ts ├── static ├── bong.mp3 ├── documentation │ ├── Description.png │ ├── Fond d'écran.png │ ├── Logo supérieur.png │ ├── Message affiché lorsqu'un passe est refusé.png │ ├── Message affiché lorsqu'un passe est valide.png │ ├── Message d'accueil.png │ ├── Médias affichés au dessus du message d'accueil.png │ └── Titre.png ├── favicon.ico ├── favicon │ ├── about.txt │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico ├── hein.mp3 ├── invalid.mp3 ├── mytest.png ├── plop.mp3 ├── robots.txt ├── sanipasse.svg ├── screenshot-qrscanner.png ├── security.txt ├── site.webmanifest ├── sitemap.xml ├── tin-lin.mp3 ├── tulut.mp3 └── valid.mp3 ├── svelte.config.js └── tsconfig.json /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/vscode/devcontainers/typescript-node:16", 3 | "remoteUser": "node", 4 | "forwardPorts": [3000] 5 | } 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .DS_Store 3 | node_modules 4 | /.svelte-kit 5 | /build 6 | /functions 7 | /sanipasse.db 8 | /docs 9 | LICENSE 10 | netlify.toml 11 | sanipasse.db 12 | README.md 13 | launch.json 14 | .github 15 | .vscode -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript') 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2019 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true 19 | }, 20 | rules: { 21 | '@typescript-eslint/no-unused-vars': [ 22 | 'warn', 23 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' } 24 | ] 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | tags: [v*] 7 | 8 | jobs: 9 | deploy: 10 | concurrency: deploy 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Setup repo 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | - name: Deploy 19 | uses: dokku/github-action@db5e3b84461e5e73c56d8b0f6a67aab0df25256c 20 | with: 21 | git_remote_url: ssh://dokku@185.132.67.32/sanipasse 22 | ssh_private_key: ${{ secrets.DEPLOY_KEY }} 23 | push_to_registry: 24 | name: Push Docker image to Docker Hub 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Check out the repo 28 | uses: actions/checkout@v2 29 | - name: Set env 30 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 31 | - name: Set up QEMU 32 | uses: docker/setup-qemu-action@v1 33 | - name: Set up Docker Buildx 34 | id: buildx 35 | uses: docker/setup-buildx-action@v1 36 | - name: Log in to Docker Hub 37 | uses: docker/login-action@v1 38 | with: 39 | username: lovasoa 40 | password: ${{ secrets.DOCKER_PASSWORD }} 41 | - name: Build and Push to Docker Hub 42 | uses: docker/build-push-action@v2 43 | with: 44 | push: true 45 | tags: lovasoa/sanipasse:latest,lovasoa/sanipasse:${{ env.RELEASE_VERSION }} 46 | platforms: linux/amd64 47 | cache-from: type=gha 48 | cache-to: type=gha,mode=max 49 | - name: Build and Push to Docker Hub (all architectures) 50 | if: startsWith(github.ref, 'refs/tags/v') 51 | uses: docker/build-push-action@v2 52 | with: 53 | push: true 54 | tags: lovasoa/sanipasse:${{ env.RELEASE_VERSION }} 55 | platforms: linux/amd64,linux/arm64,linux/arm/v7 56 | cache-from: type=gha 57 | cache-to: type=gha,mode=max 58 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: npm test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: { node-version: '16' } 18 | - run: npm ci 19 | - run: npm test 20 | - run: npm run build 21 | - name: Check that the code was formatted with "npm run format" 22 | run: npx prettier --check --plugin-search-dir=. . 23 | -------------------------------------------------------------------------------- /.github/workflows/renew_certificates.yml: -------------------------------------------------------------------------------- 1 | name: Renew certificate list 2 | 3 | on: 4 | schedule: # every day at 3:37 and 13:37 UTC 5 | - cron: '37 3,13 * * *' 6 | push: 7 | paths: 8 | - '.github/workflows/renew_certificates.yml' 9 | - 'fetch_certificates.js' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: { node-version: '16' } 18 | - run: npm ci 19 | - run: npm run update-certificates 20 | env: 21 | TACV_TOKEN: ${{ secrets.TACV_TOKEN }} 22 | - run: npm test 23 | - uses: actions/upload-artifact@v2 24 | with: 25 | name: tacv_data.json 26 | path: /tmp/tacv_data.json 27 | - name: Create Pull Request 28 | uses: peter-evans/create-pull-request@6bb739433928fbc2bdc635d41105e124d0dce021 29 | with: 30 | base: master 31 | commit-message: Auto-update certificates from TAC-Verif 32 | branch: auto-certificate-updates 33 | title: '[Bot] Update Digital Green Certificate signing keys' 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /.svelte-kit 4 | /build 5 | /functions 6 | /sanipasse.db 7 | .env 8 | .vscode/settings.json 9 | *.mp4 10 | *.webm -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .svelte-kit/** 2 | static/** 3 | build/** 4 | node_modules/** 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "runtimeExecutable": "/usr/bin/chromium-browser", 9 | "name": "Launch Chrome", 10 | "request": "launch", 11 | "type": "pwa-chrome", 12 | "url": "http://localhost:3000", 13 | "webRoot": "${workspaceFolder}" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | WORKDIR /code 4 | 5 | # Need to install python for node-gyp on platforms without prebuilt binaries 6 | RUN case "$(arch)" in \ 7 | (*x86*) break;; \ 8 | *) \ 9 | apk add --update python3 make g++ musl-dev && \ 10 | ln -sf python3 /usr/bin/python;\ 11 | esac 12 | 13 | RUN echo update-notifier=false >> ~/.npmrc 14 | # Create a docker layer with only dependencies 15 | COPY package.json package-lock.json /code/ 16 | RUN npm ci 17 | COPY . /code 18 | 19 | # Build the code 20 | RUN SVELTEKIT_ADAPTER=node npm run build 21 | 22 | # You can mount /data on the host to persist data 23 | RUN mkdir /data 24 | RUN chown daemon:daemon /data 25 | 26 | ENV DATA_FOLDER='/data' 27 | ENV MAX_FILESIZE=5000000 28 | 29 | USER daemon 30 | 31 | # Listen on the port specified by the PORT environment variable, or 3000 by default 32 | EXPOSE 3000 33 | CMD ["node", "build"] 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sanipasse 2 | 3 | Application opensource de vérification de passeport sanitaire et d'organisation d'événements zéro-COVID. 4 | 5 | Cette application sait lire les codes 2D-DOC français ainsi que les "digital green certificates" européens (sous forme de QR code). 6 | 7 | ## Screenshots 8 | 9 |
![]() | ![]() | ![]() |
34 | 👤 35 | {info.first_name.toLocaleLowerCase()} 36 | {info.last_name} 37 |
38 |🎂 Né(e) le {info.date_of_birth.toLocaleDateString('fr')}
39 | {#if !error && 'end' in validity && Date.now() + 1000 * 3600 * 24 * 365 > validity.end.getTime()} 40 | 41 |📅 Certificat valide jusqu'au {validity.end.toLocaleDateString('fr')}
42 | {/if} 43 | 44 |⚠️ {error}
48 | {/if} 49 |Version 2D-Doc | 19 |{certificate.document_version} | 20 |
---|---|
Date de création | 23 |{certificate.creation_date 25 | ? certificate.creation_date.toLocaleDateString('fr-FR') 26 | : ' - '} | 28 |
Date de signature | 31 |{certificate.signature_date 33 | ? certificate.signature_date.toLocaleDateString('fr-FR') 34 | : ' - '} | 36 |
Autorité de certification | 39 |{getCertificateAuthority(certificate.certificate_authority_id)} 41 | ({certificate.certificate_authority_id}) | 43 |
Clé de chiffrement | 46 |{getPublicKey(certificate.public_key_id)} 48 | ({certificate.public_key_id}) | 50 |
Type de document | 53 |{certificate.document_type == 'B2' 55 | ? 'Résultat de test virologique' 56 | : 'Attestation vaccinale'} ({certificate.document_type}) | 58 |
Périmètre | 61 |{certificate.document_perimeter} | 62 |
Pays émetteur | 65 |{certificate.document_country} | 66 |
Code 2D-Doc | 87 |{certificate.code} |
88 |
Prénom(s) | 23 |{certificate.tested_first_name} | 24 |
---|---|
Nom | 28 |{certificate.tested_last_name} | 29 |
Date de naissance | 33 |{certificate.tested_birth_date.toLocaleDateString('fr-FR')} | 34 |
Genre | 38 |{getSex(certificate.sex)} ({certificate.sex}) | 39 |
Code LOINC | 43 |44 | {certificate.analysis_code} | 46 |
Résultat de l'analyse | 50 |{getAnalysisResult(certificate.analysis_result)} 52 | ({certificate.analysis_result}) | 54 |
Date prélèvement | 58 |{certificate.analysis_datetime.toLocaleDateString('fr-FR')} | 59 |
Prénom(s) | 16 |{certificate.vaccinated_first_name} | 17 |
---|---|
Nom | 21 |{certificate.vaccinated_last_name} | 22 |
Date de naissance | 26 |{certificate.vaccinated_birth_date.toLocaleDateString('fr-FR')} | 27 |
Maladie couverte | 31 |{certificate.disease} | 32 |
Agent prophylactique | 40 |{certificate.prophylactic_agent} | 41 |
Nom du vaccin | 45 |{certificate.vaccine} | 46 |
Fabricant du vaccin | 50 |{certificate.vaccine_maker} | 51 |
Doses reçues | 55 |{certificate.doses_received} | 56 |
Doses attendues | 60 |{certificate.doses_expected} | 61 |
Dernière dose le | 65 |{certificate.last_dose_date.toLocaleDateString('fr-FR')} | 66 |
Vaccination | 70 |{certificate.cycle_state === 'TE' ? 'Terminée' : 'En cours'} 72 | ({certificate.cycle_state}) | 74 |
{error}
45 | Votre participation à l'événement est confirmée.
62 |78 | Votre certificat ne sera pas stocké par Sanipasse, ni visible par l'organisateur de 79 | l'événement {$invitedTo.event?.name || ''}. 80 |
81 | {:else if status === 'sending'} 82 | 86 | {/if} 87 | {:else if $wallet.includes(info.code)} 88 | 92 | {:else} 93 | 97 |98 | Votre carnet de test est enregistré localement sur votre appareil et n'est pas envoyé sur 99 | les serveurs de sanipasse. Il est disponible depuis la page d'accueil. 100 |
101 | {/if} 102 |{error}
97 |
98 | 99 | Votre caméra fonctionne normalement sur d'autres sites et applications, vous avez bien 100 | autorisé sanipasse à y accéder, et vous pensez que c'est un bug dans sanipasse ? Vous pouvez ouvrir un rapport de bug. 106 |
107 |{error.name} : {error.message}26 |
28 | Vous pouvez ouvrir un rapport de bug. 39 |
40 |
82 | sanipasse.fr
migre vers
83 | sanipasse.ophir.dev
84 |
{e.message || e}
14 | 17 |22 |18 | J'aimerais bien passer des heures à implémenter base45, CBOR, COSE, et 3 algorithmes 19 | différents de signature numérique 20 |
21 |
26 | Sanipasse.fr expose désormais une API HTTP pour vérifier les passes sanitaires. Elle permet 27 | de faire décoder et vérifier des passes par le serveur de sanipasse, tout en respectant les 28 | engagements de sécurité et de respect de la vie privée de sanipasse. 29 |
30 | 31 |35 | L'API de sanipasse propose uniquement le décodage de la forme textuelle des passes sanitaires. Il 36 | vous faut donc d'abord utiliser une bibliothèque de détection et de lecture des QR codes. Vous 37 | pouvez utiliser pour cela n'importe laquelle des bibliothèques de lecture existante, comme par 38 | exemple la bibliothèque libre ZXing. 41 |
42 |
43 | Lorsque vous décodez un passe sanitaire européen, vous obtenez une suite de lettres et de chiffres
44 | qui commence par HC1:
. C'est cette chaîne de caractères que vous devrez fournir à
45 | sanipasse.
46 |
48 | Attention: Avant d'utiliser l'API, assurez-vous d'avoir correctement décodé le QR 49 | code ou le DataMatrix. Les codes 50 | datamatrix contiennent notemment des 51 | caractères de contrôle, invisibles, que certains lecteurs physiques ne lisent pas correctement. Pour vous assurer que 55 | vous avez bien décodé un code, vous pouvez le réencoder en QR code ou en datamatrix, et vérifier 56 | que l'image résultante peut bien être scannée dans sanipasse ou dans TousAntiCovid-Verif. 57 |
58 |
60 | L'API demande de s'authentifier pour vérifier les passes sanitaires. Pour vous authentifier, il
61 | vous faut fournir une clef unique, qui vous sera fournie si vous en faites la demande à
62 | contact@ophir.dev. Pour la suite, nous supposerons que vous avez une clef valide, que nous définirons pour cette
65 | démonstration à XXXX
.
66 |
Pour vérifier un passe, il faut envoyer une requête POST
à l'adresse
https://sanipasse.ophir.dev/api/validate_pass
avec le Content-Type application/json
et le corps de requête JSON suivant:
73 | {JSON.stringify(code_example, null, ' ')}74 |
Pour tester l'API en ligne de commande, on peut utiliser curl:
75 |
76 | curl https://sanipasse.ophir.dev/api/validate_pass -H 'Content-Type: application/json' --data '{JSON.stringify(
78 | code_example
79 | )}' -v
80 |
81 |
84 | ou python, avec la bibliothèque requests: 86 |
87 |
88 | import requests
89 |
90 | validation = requests.post(
91 | 'https://sanipasse.ophir.dev/api/validate_pass',
92 | json={JSON.stringify(code_example)}
93 | ).json()
94 |
95 |
96 | 98 | Si la vérification fonctionne, quel qu'en soit le résultat (code valide ou non), l'API retournera 99 | un code HTTP 200. 100 |
101 |103 | Si le passe est valide, l'API retourne les informations nécessaires à la vérification d'identité 104 | de son porteur au format JSON 105 |
106 |{`{ 107 | "validated": true, 108 | "person": { 109 | "first_name": "PIERRE", 110 | "last_name": "LEGENDRE", 111 | "date_of_birth": "1990-01-01T00:00:00.000Z" 112 | } 113 | }`}114 | 115 |
Si le passe est invalide, l'API retourne un message expliquant pourquoi le passe est invalide
117 |{`{ 118 | "validated": false, 119 | "error":"Vous n'avez reçu que 1 dose sur les 2 que ce vaccin demande.", 120 | "person": { 121 | "first_name":"PIERRE", 122 | "last_name":"LEGENDRE", 123 | "date_of_birth":"1990-01-01T00:00:00.000Z" 124 | } 125 | }`}126 |
127 | L'object person peut ne pas être présent si le passe présenté n'a pas un format correct. 128 |
129 | 130 | 139 | -------------------------------------------------------------------------------- /src/routes/api/borne/[key].ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from '@sveltejs/kit'; 2 | import { BorneConfig } from '$lib/database'; 3 | import { MAX_FILESIZE } from '$lib/global_config'; 4 | 5 | export const get: RequestHandler = async ({ params: { key } }) => { 6 | const found = await (await BorneConfig).findOne({ 7 | where: { key } 8 | }); 9 | if (!found) return { status: 404, body: { error: `config "${key}" does not exist` } }; 10 | const body = JSON.parse(found.get('config') as string); 11 | return { body }; 12 | }; 13 | 14 | export const put: RequestHandler = async ({ params: { key }, request }) => { 15 | const rawBody = await request.text(); 16 | if (rawBody.length > MAX_FILESIZE) 17 | return { 18 | status: 400, 19 | body: `Configuration trop volumineuse: ${(rawBody.length / 1e6).toFixed( 20 | 1 21 | )} Mo au lieu d'un maximum autorisé de ${(MAX_FILESIZE / 1e6).toFixed(1)} Mo.` 22 | }; 23 | if (!key || key.length < 12) return { status: 400, body: 'key is too short' }; 24 | const [_configObj, created] = await (await BorneConfig).upsert({ key, config: rawBody }); 25 | return { 26 | status: 201, 27 | body: { created } 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/routes/api/create_event.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '$lib/database'; 2 | import type { DBEvent, EventData } from '$lib/event'; 3 | import type { RequestHandler } from '@sveltejs/kit'; 4 | 5 | export const put: RequestHandler = async ({ request }) => { 6 | const body: EventData = await request.json(); 7 | const date = new Date(body.date); 8 | if (isNaN(+date)) throw new Error('invalid date'); 9 | const created = await (await Event).create({ 10 | name: `${body.name || ''}`, 11 | date 12 | }); 13 | return { 14 | status: 201, // created 15 | body: created.toJSON55 | {description} 56 |
57 |{date.toLocaleDateString()}
58 |Dernière synchronisation réussie le {last_sync.toLocaleString('fr')}.
30 | {#if last_update !== last_sync} 31 |Mise à jour de configuration le {last_update.toLocaleString('fr')}.
32 | {/if} 33 |34 | Sanipasse v{process.env.SANIPASSE_VERSION}, mis à jour le {new Date( 35 | process.env.SANIPASSE_BUILD_DATE || 0 36 | ).toLocaleString('fr')} 37 |
38 |39 | Passes validés avec les 40 | {#if config.validation_ruleset === 'tousAntiCovidDefaultRules'} 41 | règles par défaut de TousAntiCovid 42 | {:else if config.validation_ruleset === 'tousAntiCovidVaccineRules'} 43 | règles du passe vaccinal 44 | {:else if config.validation_ruleset === 'tousAntiCovidHealthRules'} 45 | règles du passe sanitaire (tests acceptés) 46 | {:else} 47 | règles personnalisées définies par l'opérateur 48 | {/if} 49 | . 50 |
51 |Vous êtes {online ? 'en ligne' : 'hors ligne'}.
52 |159 | {#each config.description.split('\n') as p} 160 |
{p}
161 | {/each} 162 | 163 |{code}
179 |Code length: {code.length}
180 |Last key pressed: {JSON.stringify(last_event?.key, null, ' ')}
181 |{err}195 | {/await} 196 |
201 |
{paragraph}
49 | {/each} 50 |55 | Cette configuration est utilisable depuis n'importe quel appareil en chargeant l'adresse 56 | suivante : 57 | {typeof window === 'object' 59 | ? window.location.host 60 | : 'sanipasse.ophir.dev'}/borne?key={configKey}. 62 |
63 | {#if with_technical} 64 |65 | La configuration complète est modifiable à partir de 66 | l'URL de la page actuelle. Une 67 | URL de configuration simplifiée, ne donnant 68 | accès qu'aux paramètres d'affichage est également disponible. 69 |
70 | {/if} 71 | {:else} 72 |73 | Sanipasse borne est un logiciel libre et gratuit à installer sur une borne de contrôle automatique 74 | des pass sanitaires. 75 |
76 |77 | Cette page vous permet de configurer l'interface de scan et de contrôle des passes. Une fois sur 78 | la page de scan, il vous faudra soit 79 | un lecteur physique de QR code, 80 | soit une webcam pour lire les passes sanitaires. 81 |
82 | {/if} 83 | 84 | 128 | -------------------------------------------------------------------------------- /src/routes/borne/config/_config_storage.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigProperties } from './_config'; 2 | import { DEFAULT_CONFIG } from './_config'; 3 | import { get_from_local_store, store_locally } from '$lib/storage'; 4 | import { get } from '$lib/http'; 5 | 6 | const STORAGE_KEY = 'borne_config'; 7 | 8 | export async function save_config(config: ConfigProperties) { 9 | await store_locally(STORAGE_KEY, config); 10 | } 11 | 12 | export async function load_config(): Promise28 | Vous pouvez demander à sanipasse d'envoyer une requête externe lorsqu'un passe est validé ou 29 | refusé. Cela permet de s'interfacer avec d'autres services ou du matériel comme une porte 30 | automatique, une ampoule, une imprimante... 31 |
32 |33 | Si vous voulez contrôler du matériel depuis une système embarqué sous linux (tel qu'un Raspberry 34 | Pi), vous pouvez utiliser le logiciel 35 | http-gpio, développé par le même auteur que 36 | sanipasse. Voir la vidéo de démonstration. 37 |
38 |39 | Vous pouvez aussi envoyer une requête vers n'importe quel serveur HTTP, local ou sur internet, 40 | mais il est important de faire attention aux éléments suivants: 41 |
42 |59 | Si vous avez besoin de conseil sur l'interfaçage de votre service ou de votre solution matérielle 60 | avec sanipasse borne, vous pouvez me contacter sur contact@ophir.dev. 63 |
64 | 65 | 102 | -------------------------------------------------------------------------------- /src/routes/borne/config/_file_upload.svelte: -------------------------------------------------------------------------------- 1 | 106 | 107 |18 | Sanipasse borne offre la possibilité de configurer très précisément tous les aspects de la 19 | présentation visuelle de l'interface de vérification des passes sanitaires. 20 |
21 | 22 |23 | Ceci est une fonctionnalité avancée qui, si elle est mal utilisée, peut empêcher le bon 24 | fonctionnement de l'interface. Pour l'utiliser, il vous faudra apprendre à maîtriser le language CSS. Soyez sûr de bien comprendre le fonctionnement et la portée des règles CSS que vous entrez ici 29 | avant de les appliquer. 30 |
31 | 32 |33 | Une fois que vous avez compris le fonctionnement du langage CSS et que vous avez en tête les 34 | personnalisations que vous souhaitez appliquer, vous pouvez commencer à configurer l'interface en 35 | appliquant vos règles CSS aux sélecteurs CSS décrits ci-dessous. 36 |
37 | 38 |41 | L'interface de sanipasse borne est composée de plusieurs parties, chacune peut être sélectionnée à 42 | l'aide d'un sélecteur CSS tel que décrit ci-dessous. 43 |
44 |{m.selector + ' {\n\t...\n}'}
61 | 4 | Sanipasse borne peut fonctionner de deux manières différentes : soit en mode "lecteur physique" 5 | soit en mode "webcam". 6 |
7 |8 | Le mode webcam est plus simple à mettre en place, car il ne nécessite pas de matériel spécial en 9 | dehors de la webcam déjà intégrée dans la plupart des appareils. Mais le mode "lecteur physique" 10 | est plus puissant et plus flexible, car il permet de scanner des codes de manière plus rapide et 11 | plus fiable même dans de mauvaises conditions de luminosité. 12 |
13 |14 | Pour mettre en place le mode "lecteur physique", il vous faudra vous procurer un lecteur de QR 15 | code USB compatible. 16 |
17 |18 | Les lecteurs suivants ont été testés par leur distributeur, qui garantit leur fonctionnement avec 19 | sanipasse borne: 20 |
21 |35 | Scanner que l'on tient dans la main, qui permet de scanner les codes de barres dans n'importe 36 | quel endroit et dans n'importe quelle position. 37 |
38 | 39 | 43 |52 | Utilisation idéale en magasin, pour intégration dans un kiosque, une borne ou dans un plan de 53 | travail, pour créer votre propre solution matérielle. 54 |
55 | 56 |58 | Une fois que vous avez connecté votre scanner de QR code à votre appareil, il doit être reconnu 59 | comme un clavier, et vous devez vous assurer que la disposition clavier associée à l'appareil est 60 | correcte. Il doit être configuré pour ne remonter que le contenu du QR code, sans aucune 61 | modification: il ne doit pas ajouter d'espace ou de saut de ligne après la fin du contenu du code. 62 | Référez-vous à la documentation de votre appareil pour plus d'informations. 63 |
64 |65 | Dans la page de configuration de sanipasse borne, vous pouvez ensuite 66 | choisir le mode de scan "Scanneur de QR code USB physique", comme indiqué sur la capture d'écran 67 | ci-dessous: 68 |
69 |86 | Cette page présente les statistiques du nombre de passes scannés avec sanipasse borne sur cet 87 | appareil. Ces informations sont stockées localement et ne sont pas accessibles depuis 88 | l'extérieur, même depuis le serveur de sanipasse. 89 |
90 | 91 | 94 | {/if} 95 | 96 | {#await config then config} 97 | {#if !config.store_statistics} 98 |101 | L'enregistrement des statistiques est désactivé dans la configuration locale de cet 102 | appareil. Vous pouvez l'activer depuis la page de configuration. 103 |
104 |110 | 111 |
112 |
119 | Vous utilisez actuellement Sanipasse
120 |
121 |
Date | 141 |Heure | 142 |Passes validés | 143 |Passes refusés | 144 |
---|---|---|---|
{show_date.name} | 156 | {/if} 157 |{show_time} | 158 |{valid} | 159 |{invalid} | 160 |
Aucune statistique disponible | 164 |
Vous pouvez envoyer le lien suivant à vos invités pour leur permettre de s'inscrire :
19 |33 | 34 | Attention, ne confondez pas! Envoyez bien le lien indiqué ci-dessus, et non l'adresse de la page 35 | actuelle à vos invités. La page actuelle est réservée aux administrateurs de l'évènement. 36 | 37 |
38 |40 | Les réponses des invités sont affichées uniquement sur cette page, dont l'adresse 41 | est confidentielle et réservée aux administrateurs de l'évènement. Il est conseillé d'ajouter 42 | cette page aux marque-pages de votre navigateur pour pouvoir y revenir facilement. 43 |
44 | -------------------------------------------------------------------------------- /src/routes/events/_inviteesList.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |Aucun invité pour le moment
62 |63 | La création d'une liste d'invités à l'avance n'est pas obligatoire. Vous pouvez simplement 64 | {#if typeof navigator === 'object' && navigator.share && event} 65 | 77 | {:else} 78 | envoyer 79 | {/if} 80 | dès maintenant le lien d'invitation fourni dans la section 81 | Communiquer auprès de vos invités, et le nom des personnes ayant validé leur 82 | invitation à l'aide d'un passe sanitaire apparaîtra ici. 83 |
84 |16 | Vos événements: 17 | {#each events as event} 18 | {event.name}{/each}. 21 |
22 | {/if} 23 | {/await} 24 | -------------------------------------------------------------------------------- /src/routes/events/index.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 |Certificat invalide: {e}30 | {/await} 31 | {#if $wallet.includes(code)} 32 | {#if $wallet[0] !== code} 33 | 39 | {/if} 40 | 47 | {:else} 48 | 54 | {/if} 55 |
Chargement du document...
114 |119 | Après votre test ou votre vaccination, un certificat numérique vous a été communiqué. Vous 120 | pouvez l'importer dans Sanipasse ci-dessous. 121 |
122 |{error}
138 | 12 | Les QR codes sur les certificats de test et de vaccination contiennent un lien. Vous pouvez coller 13 | ce lien ici directement. 14 |
15 | 20 |25 | Si l'accès direct à votre webcam ne fonctionne pas, vous pouvez utiliser 26 | la lecture depuis un fichier. 27 |
28 | {:else} 29 |30 | Sanipasse demande la permission d'accéder à votre webcam pour lire le QR code. 31 |
32 |33 | Votre navigateur va vous demander si vous souhaitez autoriser {domain} à utiliser votre webcam. 34 |
35 |36 | Cliquez sur Autoriser. 37 |
38 | {/if} 39 | 40 |36 | Ceci est une invitation à l'évènement 37 | {$invitedTo.event?.name || '...'}, qui aura lieu le 38 | {$invitedTo.event ? new Date($invitedTo.event.date).toLocaleString('fr') : '...'}. 39 |
40 |41 | Pour confirmer votre participation, vous devez scanner un certificat de test de moins de 72h, 42 | ou un certificat de vaccination (de seconde dose si le vaccin en demande deux). 43 |
44 |45 | Votre passe sanitaire est strictement privé. Il ne sera 46 | pas conservé 47 | sur notre serveur, ni visible par l'organisateur de l’événement. 48 |
49 | {:else} 50 |51 | Sanipasse est un logiciel libre de vérification des certificats de test ou de vaccination, et 52 | d'organisation d’événements respectueux des règles sanitaires. 53 |
54 |55 | La vérification proposée ci-dessous est strictement privée, et 56 | vos données ne quittent jamais votre appareil. 57 |
58 | {/if} 59 |70 | Vous pouvez également Créer un événement, pour construire une 71 | liste d'invités zéro-COVID. Sanipasse générera un lien privé à 72 | envoyer à vos invités, depuis lequel ils pourront confirmer leur participation en validant 73 | leur passe sanitaire. 74 |
75 |80 | Sanipasse borne permet de mettre en place une borne autonome de contrôle des passes sanitaires, 81 | munie d'un scanner de QR code physique ou d’une webcam. 82 |
83 |4 | Le projet sanipasse a été un beau succès, et a été de loin le logiciel libre d'analyse de passes 5 | sanitaires et de bornes de validation le plus populaire. 6 |
7 | 8 |9 | Aujourd'hui, en 2023, son utilisation a beaucoup baissé, et c'est tant mieux! Personne ne regrette 10 | les années COVID. 11 |
12 | 13 |
14 | Cependant, le nom de domaine sanipasse.fr
a un coût. Ce n'est pas très cher (1€ par mois),
15 | mais étant donnée l'utilisation actuelle du site, je pense qu'il n'est plus crucial pour sanipasse
16 | d'avoir son propre nom de domaine.
17 |
20 | Je compte donc migrer sanipasse vers sanipasse.ophir.dev
23 | (un sous-domaine de mon nom de domaine personnel, que je continuerai à payer).
24 |
27 | Si vous voulez conserver vos données enregistrées sur sanipasse.fr
28 | (passes enrégistrés dans l'espace "Mon Carnet", et configurations de bornes enregistrées localement),
29 | il vous faudra les ré-importer sur
30 | sanipasse.ophir.dev
.
31 |
La migration se passera en 3 temps:
34 |sanipasse.ophir.dev
.
41 | sanipasse.fr
ne sera plus disponible, et
44 | sanipasse ne sera plus accessible qu'à travers
45 | sanipasse.ophir.dev
46 |
50 | Si vous souhaitez reprendre à vos frais le projet sanipasse et le nom de domaine sanipasse.fr
, vous pouvez toujours me contacter sur
53 | contat@sanipasse.fr.
54 |
Le 18 mai 2021, l'instauration d'un système de passe sanitaire a été voté au Sénat.
14 |15 | À partir du 9 juin 2021, un certificat de test négatif, de rétablissement, ou de vaccination 16 | contre le COVID-19 sera obligatoire pour participer à certains événements de plus de 1000 17 | participants. 18 |
19 |
20 | Le texte voté par le Sénat précise que
21 |
22 |
25 | La présentation, sur papier ou sous format numérique, des documents mentionnés au premier
26 | alinéa du présent B est réalisée sous une forme ne permettant pas aux personnes habilitées
27 | ou aux services autorisés à en assurer le contrôle de connaître la nature du document ni les
28 | données qu’il contient.
29 |
24 |
31 |
40 | Pourtant, ni le format de données utilisé dans les codes barres présents sur les certificats de 41 | test et de vaccination émis aujourd'hui en France (2D-DOC), ni le format du futur "passe sanitaire européen" (DGC) ne permettent techniquement d'offrir cette garantie s'ils sont présentés directement aux 46 | personnes habilitées. 47 |
48 | 49 |50 | En effet, les codes contenant ces certificats contiennent toutes leurs informations en clair, et 51 | peuvent être lus par tout un chacun avec un simple lecteur de QR code, ou avec le logiciel libre 52 | sanipasse présent sur ce site pour en mettre en forme tout le contenu, par exemple. 53 |
54 | 55 |56 | La solution technique vers laquelle nous nous dirigeons est donc probablement la simple 57 | distribution aux agents de vérification d'une application privée et propriétaire qui est 58 | volontairement bridée pour ne pas afficher tous les champs de données contenus dans les passes des 59 | personnes qu'ils contrôlent. Cette solution pose problème pour plusieurs raisons: 60 |
61 |81 | Puisque les informations privées sont contenues en clair dans les certificats, le seul moyen de 82 | les cacher aux organisateurs d'événement est de ne pas leur montrer les certificats. 83 |
84 |85 | Si l'on veut quand même avoir un système permettant d'organiser de grands événements sans risque 86 | de contamination, alors il faut instaurer un tiers de confiance entre les organisateurs 87 | d'événements et les participants. 88 |
89 |90 | Sanipasse tente de répondre à ce problème en offrant un système open-source 91 | d’organisation d’événements dans lequel les invités scannent eux-mêmes leur passe 92 | sanitaire, qui est vérifié de manière sécurisée, et ensuite immédiatement supprimé. Cela offre ainsi 93 | la garantie que les organisateurs n’ont accès à aucune autre information que ce qu'ils ont légalement 94 | l'autorisation de vérifier. 95 |
96 | 97 | 101 | 102 | 108 | -------------------------------------------------------------------------------- /src/routes/wallet.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |20 | Pour valider votre participation à l'événement {$invitedTo.event?.name || ''} avec l'un des 21 | certificats ci-dessous, cliquez simplement dessus. 22 |
23 | {/if} 24 | 25 | {#if $wallet.length > 0} 26 | {#await is_volatile then can_loose_data} 27 | {#if can_loose_data} 28 |30 | Attention ! Votre carnet n'est pas enregistré de manière permanente par votre 31 | navigateur. 32 |
33 | 40 |53 | Aucun certificat dans votre carnet pour le moment. Après avoir scanné un certificat, vous 54 | pourrez l'ajouter à votre carnet pour y avoir accès ici. 55 |
56 | {/if} 57 | 58 | {#each $wallet as cert} 59 | {#await parse_any(cert)} 60 |