Coder Social home page Coder Social logo

betagouv / secretariat Goto Github PK

View Code? Open in Web Editor NEW
7.0 39.0 22.0 26.92 MB

La webapp espace membre de l’incubateur đŸ’â€â™‚ïžđŸ‘©â€đŸ’»

License: MIT License

JavaScript 4.64% CSS 0.84% Dockerfile 0.02% Shell 0.03% EJS 10.45% TypeScript 84.02% Procfile 0.01%

secretariat's Introduction

Espace Membre

L'espace membre de l’incubateur

Dev de l'app Espace Membre

Variables d'environnement

  • Variables d'environnement nĂ©cessaires :
    • OVH_APP_KEY - Obtenir les credentials OVH pour dĂ©bugger
    • OVH_APP_SECRET
    • OVH_CONSUMER_KEY
    • SESSION_SECRET - ClĂ© de 32 caractĂšre alĂ©atoires, important en prod
    • MAIL_SERVICE - Service gĂ©rĂ© par nodemailer (DĂ©bugger SMTP en local). Si absente, MAIL_HOST, MAIL_PORT, et MAIL_IGNORE_TLS seront utilisĂ©es.
    • MAIL_USER
    • MAIL_PASS
    • SECURE - true si https sinon false
    • HOSTNAME - Par exemple localhost pour le dĂ©veloppement en local
    • CHAT_WEBHOOK_URL_SECRETARIAT - Adresse d'envoi des notifications Mattermost (anciennement Slack) pour le canal "#secretariat" - par ex. : https://hooks.mattermost.com/services/... (DĂ©bugger sans Mattermost)
    • CHAT_WEBHOOK_URL_GENERAL - Adresse d'envoi des notifications Mattermost (anciennement Slack) pour le canal "#general" - par ex. : https://hooks.mattermost.com/services/... (DĂ©bugger sans Mattermost)
    • DATABASE_URL - Le string de connexion pour se connecter Ă  Postgres (pensez Ă  Ă©chapper les caractĂšres spĂ©ciaux s'il s'agit d'une URI). Le string de connexion doit contenir le user, password, host, port et le nom de la base de donnĂ©es.
    • GITHUB_TOKEN - Le Personal Access Token du compte Github utilisĂ© pour crĂ©er les PR des nouvelles recrues
    • GITHUB_REPOSITORY - Le repository Github qui contient les fiches des utilisateurs (par ex: betagouv/beta.gouv.fr)
    • GITHUB_FORK - Le fork du GITHUB_REPOSITORY utilisĂ© pour crĂ©er les PRs, (par ex: test-user/beta.gouv.fr).
    • GITHUB_ORG_ADMIN_TOKEN - Le Personal Access Token du compte Github utilisĂ© pour gĂ©rer les membres de la communautĂ©
  • Variables d'environnement optionnelles :
    • SECRETARIAT_DOMAIN - Domaine OVH Ă  utiliser (DĂ©bugger avec un autre domaine OVH)
    • USERS_API - API User Ă  utiliser (DĂ©bugger avec une autre API)
    • POSTGRES_PASSWORD - Cette variable sert Ă  lancer l'image docker de postgres et donc seulement nĂ©cessaire si Docker est utilisĂ© pour le dĂ©veloppement.
    • MAIL_HOST - Si la variable MAIL_SERVICE est absente, cette variable sera utilisĂ©e pour spĂ©cifier le hostname ou adresse IP Ă  utiliser pour l'envoi d'emails avec Nodemailer.
    • MAIL_PORT - Si la variable MAIL_SERVICE est absente, cette variable sera utilisĂ©e pour spĂ©cifier le port Ă  utiliser pour l'envoi d'emails avec Nodemailer.
    • MAIL_IGNORE_TLS - Si la variable MAIL_SERVICE est absente, cette variable sera utilisĂ©e pour l'utilisation de TLS dans la connexion email avec Nodemailer.
    • NEWSLETTER_HASH_SECRET - ClĂ© pour gĂ©nĂ©rer un id pour les newsletters, important en prod
    • INVESTIGATION_REPORTS_IFRAME_URL - URL de l'iframe Airtable contenant les bilans d'investigations et qui est intĂ©grĂ©e Ă  la page ressources
    • GITHUB_ORGANIZATION_NAME - Nom de l'organization github Ă  laquelle inviter les membres
    • MATTERMOST_INVITE_ID - ID secret de l'invitation qui permet de se crĂ©er un compte sur l'espace mattermost https://mattermost.incubateur.net/signup_user_complete/?id=[ID]
    • MATTERMOST_TEAM_ID - ID de la team CommunautĂ©
    • MATTERMOST_BOT_TOKEN - Token du bot mattermost qui permet de faire les requĂȘtes Ă  l'api

Lancer en mode développement

Une fois Postgres lancé, vous pouvez démarrer l'application avec ces commandes :

  • CrĂ©er le fichier de configuration : cp .env.example .env et le remplir avec les identifiants OVH obtenus plus haut.
» npm install # RécupÚre les dépendances
» npm run migrate # Applique les migrations
» npm run seed # Ajoute des utilisateurs dans la base users. Utilisez l'un des primary_email présent dans la bdd pour vous connecter
» npm run dev
   ...
   Running on port: 8100

L'application sera disponible sur http://localhost:8100 (8100 est le port par défaut, vous pouvez le changer avec la variable d'env PORT)

Lancer avec docker-compose

  • CrĂ©er le fichier de configuration : cp .env.example .env et le remplir avec les identifiants OVH obtenus plus haut.
  • Lancer le service et initialiser la base de donnĂ©es : docker-compose up - disponible sur http://localhost:8100
  • Pour ajouter des donnĂ©es Ă  la base de donnĂ©es (facultatif): docker-compose run web npm run seed;
  • Lancer les tests : docker-compose run web npm test

Lancer avec docker sans docker-compose

  • Exemple pour dĂ©velopper dans un container :
    • docker run --rm --env-file ../.env.secretariat.dev -v $(pwd):/app -w /app -ti -p 8100 node /bin/bash (avec vos variables d'environnement dans ../.env.secretariat.dev)

Lancer en mode production

» npm run start
   ...
   Running on port: 8100

Lancer les tests

» npm run test

Générer clé API OVH

Si vous n'avez pas les droits pour générer les credentials OVH, postez un message sur #incubateur-amélioration-secretariat.

Lien : https://eu.api.ovh.com/createToken/

  • NĂ©cessaires pour les fonctionalitĂ©s en cours
GET /email/domain/beta.gouv.fr/*
POST /email/domain/beta.gouv.fr/account
DELETE /email/domain/beta.gouv.fr/account/*
POST /email/domain/beta.gouv.fr/redirection
DELETE /email/domain/beta.gouv.fr/redirection/*
POST /email/domain/beta.gouv.fr/account/*/changePassword
  • NĂ©cessaires pour les prochaines fonctionalitĂ©s
POST /email/domain/beta.gouv.fr/mailingList
POST /email/domain/beta.gouv.fr/mailingList/*/subscriber
DELETE /email/domain/beta.gouv.fr/mailingList/*/subscriber/*
GET /email/domain/beta.gouv.fr/mailingList/*/subscriber
GET /email/domain/beta.gouv.fr/responder/*
POST /email/domain/beta.gouv.fr/responder
PUT /email/domain/beta.gouv.fr/responder/*
DELETE /email/domain/beta.gouv.fr/responder/*

Debug avec le serveur SMTP Maildev

Maildev est un serveur SMTP avec une interface web conçus pour le développement et les tests.

Sans docker: Une fois installé et lancé, il suffit de mettre la variable d'environnement MAIL_SERVICE à maildev pour l'utiliser. MAIL_USER et MAIL_PASS ne sont pas nécessaires.

Avec docker: ne pas préciser de MAIL_SERVICE, les bonnes variables d'environnement sont déjà précisées dans le docker-compose

Tous les emails envoyés par le code de l'espace membre seront visibles depuis l'interface web de Maildev (http://localhost:1080/).

Debug sans notifications Mattermost

Pour certaines actions, l'espace membre envoie une notification Mattermost. En local, vous pouvez mettre les variables d'environnements CHAT_WEBHOOK_URL_SECRETARIAT et CHAT_WEBHOOK_URL_GENERAL Ă  un service qui reçoit des requĂȘtes POST et rĂ©pond avec un 200 OK systĂ©matiquement.

Beeceptor permet de le faire avec une interface en ligne sans besoin de télécharger quoi que ce soit.

Sinon, certains outils gratuits comme Mockoon ou Postman permettent de créer des serveurs mock facilement aussi (Guide Postman).

Debug avec un autre domaine OVH

Lorsqu'on utilise un autre domaine OVH (par exemple, un domain bac-Ă -sable pour le dĂ©veloppement), la variable SECRETARIAT_DOMAIN doit ĂȘtre renseignĂ©e. Par dĂ©faut, le domaine est beta.gouv.fr.

Debug avec une autre API utilisateur

Configurer la variable d'environnement USERS_API (par défaut à https://beta.gouv.fr/api/v1.6/authors.json)

Créer des migrations

KnexJS permet de créer des migrations de base de données. Un shortcut a été ajouté au package.json pour créer une migration :

npm run makeMigration <nom de la migration>

Une fois la migration créée, vous pouvez l'appliquer avec :

npm run migrate

Pour utiliser d'autres commandes, le CLI de KnexJS est disponible avec ./node_modules/knex/bin/cli.js. Par exemple, pour faire un rollback :

./node_modules/knex/bin/cli.js migrate:rollback

Scripts pour faire des taches en local

Générer le graphe des redirections emails

  • Configurer les variables d'environnements : OVH_APP_KEY, OVH_APP_SECRET et OVH_CONSUMER_KEY (Avec une clĂ© ayant un accĂšs aux emails)
  • Lancer le script : node ./scripts/export_redirections_to_dot.ts > redirections.dot
  • Lancer graphviz : dot -Tpdf redirections.dot -o redirections.pdf (Format disponible : svg,png, ...)

Supprimer une redirection

  • Configurer les variables d'environnements : OVH_APP_KEY, OVH_APP_SECRET et OVH_CONSUMER_KEY (Avec une clĂ© ayant un accĂšs aux emails)
  • Lancer le script : node ./scripts/delete_redirections.js [email protected] [email protected]

Explication du fonctionnement des pull requests Github

En prod, l'app sécrétariat génÚre des pulls requests sur le github reposity betagouv/beta.gouv.fr. Pour cela une une branche est créée sur un fork du repository betagrouv/beta.gouv.fr et une pull request est effectuée depuis ce fork vers ce repository initial betagouv/beta.gouv.fr.

Pourquoi utiliser un fork ?

Afin de créer une branche et faire une pull request sur un repository, on donne les droits d'accÚs en écriture sur ce repository via un token (GITHUB_TOKEN) utilisé par le code. Pour prévenir tout problÚme ces droits sont donnés sur le repository "fork", et non sur le repository principal.

Il faut donc préciser ces variables d'environnement:

  • GITHUB_TOKEN - Le Personal Access Token du compte Github utilisĂ© pour crĂ©er les PR des nouvelles recrues. Ce token doit contenir les droits d'Ă©criture Ă  GITHUB_FORK.
  • GITHUB_REPOSITORY - Le repository Github qui contient les fiches des utilisateurs (par ex: betagouv/beta.gouv.fr). L'application n'a pas des droits d'Ă©criture sur ce repository.
  • GITHUB_FORK - Le fork du GITHUB_REPOSITORY auquel l'application a des droits d'accĂšs en Ă©criture

En dev : le GITHUB_REPOSITORY est un fork de betagouv/beta.gouv.fr, et le GITHUB_FORK un fork de ce fork.

Pour simplifier, on peut utiliser des repos communs entre dev. Demandez a vos collÚgues le nom des repository à spécifer dans le .env ainsi que les droits d'accÚs au resository GITHUB_FORK

secretariat's People

Contributors

adenoix avatar alemangui avatar antoineaugusti avatar astranchet avatar camillegarrigue avatar celiacheff avatar chaibax avatar dependabot-preview[bot] avatar dependabot[bot] avatar eml-trm avatar estellecomment avatar guillett avatar ishanb avatar jbuget avatar jdauphant avatar l-vincent-l avatar lbois avatar lucascharrier avatar mlbiche avatar n-b avatar nabellaleen avatar polomarcus avatar rap2hpoutre avatar raphodn avatar revolunet avatar risseraka avatar sblondon avatar totakoko avatar tristanrobert avatar vinyll avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

secretariat's Issues

Regarder l'UX pour séparer les fonctionnalités

VoilĂ , 2 pages “Mon compte” et “CommunautĂ©â€œ. Sur ces maquettes, je n’ai pas modifiĂ©/rajoutĂ© de fonctionnalitĂ© Ă  l’existant (mĂȘme si ça me dĂ©mange 😄), simplement rĂ©organisĂ©. Ça permet d’imaginer comment rajouter simplement des nouvelles pages ensuite :

Desktop : https://www.figma.com/proto/PwjUHlF7UIt0hywZP2CeJD/betagouv?node-id=330%3A32&scaling=min-zoom
Responsive : https://www.figma.com/proto/PwjUHlF7UIt0hywZP2CeJD/betagouv?node-id=415%3A139&scaling=scale-down

Ajouter des logs de tracabilités

Table Events :

  • id
  • create_by_username
  • action_code
  • action_description
  • action_on_username?
  • creation_date

Préparation

  • Lister les Ă©vĂ©nements et donner un code Ă  chaque Ă©venement

Rediriger vers next aprĂšs le login

Certains utilisateurs peuvent se rendre sur le secrétariat en suivant un lien - par exemple https://secretariat.beta.gouv.fr/users/prenom.nom - mais sont redirigés vers la page de login si ils ne sont pas connectés. Une fois connecté, la page /users s'affiche ensuite par défaut.

Au moment de la redirection vers le login, on pourrait garder leur url initiale dans un paramÚtre next https://secretariat.beta.gouv.fr/login?next=/users/prenom.nom et les y rediriger une fois connectés (ou plus précisément : leur fournir un lien de connexion qui pointe directement vers cette url)

Priorité : basse

RĂ©gression sur l'utilisateur sans compte email

On a essayé de créer l'adresse pour @lucas-bch de CartoBio.

On a créé sa fiche sur Github : https://github.com/betagouv/beta.gouv.fr/blob/6321a796cc69bda0bcd7fabc587d761c04604b24/content/_authors/lucas.bchini.md
Je me suis loggée sur le secrétariat sur https://secretariat.beta.gouv.fr/ avec mon compte sabine.safi - ça a marché.

L'autocomplete m'a bien proposé lucas.bchini.
Ça m'a amenĂ©e sur cette page :
Capture d’écran 2020-01-07 à 19 37 13

@jdauphant sur Slack :
Il y a un bug dans l'app
On a une régression sur l'utilisateur sans compte email

Maintenir les champs renseignés en cas d'erreur (onboarding view)

Aujourd'hui, si le POST /onboarding Ă©choue (par exemple Ă  cause d'une fiche du mĂȘme nom qui existe dĂ©jĂ ), on est redirigĂ© vers GET /onboarding sans l'information rentrĂ©e prĂ©cĂ©demment, ce qui provoque la perte des donnĂ©es dans le formulaire.

Il faut qu'on puisse passer ces informations (par exemple via req.flash) et qu'on les utilise pour pré-remplir les champs dans onboarding.ejs.

Ajouter du linting

DĂ©finir des rĂšgles d'Ă©criture du code en s'appuyant sur https://eslint.org/
Cela s'appliquera au fichiers .js, mais aussi au .ejs
Idéalement avoir un hook pre-commit pour éviter de commiter des erreurs de linting (que le CI détectera, et qu'il faudra corriger, etc)

Permettre la réinitialisation du mot de passe par mail

Si j'ai choisi de bénéficier d'une redirection dans un premier temps mais que je souhaite évoluer vers un compte mail beta.gouv.fr "normal", ou bien si j'ai oublié mon mot de passe, je souhaite pouvoir demander une réinitialisation sécurisée de mon mot de passe.

Idée d'implémentation:

  • crĂ©er une case Ă  cocher "envoyer Ă  mon adresse secondaire" Ă  cĂŽtĂ© du bouton "Connexion par email"
  • envoyer le lien d'authentification Ă  l'adresse secondaire, ce qui permet de s'appuyer sur l'implĂ©mentation de #13

Alternative 1:

  • crĂ©er une page et un formulaire spĂ©cifique "RĂ©initialiser mon mot de passe"
  • dans ce formulaire demander l'identifiant beta.gouv.fr de la personne
  • envoyer un lien Ă  l'adresse secondaire correspondante contenant un jeton sĂ©curisĂ©
  • sur prĂ©sentation de ce jeton sĂ©curisĂ©, donner accĂšs au formulaire de changement de mot de passe pour ce membre

Alternative 2:

  • crĂ©er une page et un formulaire spĂ©cifique "RĂ©initialiser mon mot de passe"
  • dans ce formulaire demander l'adresse mail secondaire de la personne
  • envoyer un lien Ă  cette adresse correspondante contenant un jeton sĂ©curisĂ©
  • sur prĂ©sentation de ce jeton sĂ©curisĂ©, donner accĂšs au formulaire de changement de mot de passe pour l'identifiant beta.gouv.fr associĂ© Ă  cette adresse mail

Dans tous ces cas:

  • pour les nouveaux membres, stocker dans une base de donnĂ©es les adresses mail secondaires
  • attention, ceci implique Ă©galement de modifier le comportement de la commande Slack /compte
  • pour les membres existants, complĂ©ter ces informations manuellement

DĂ©part d'un membre

  • Bouton sur la fiche utilisateur "Ce membre est parti" si la date de fin est dĂ©passĂ©e
  • Ă  l'appui du bouton :
    • suppression du compte et redirection
    • redirection de l'email vers l'adresse "[email protected]"
    • retirer les droits sur github
  • Proposer que l'utilisateur puisse supprimĂ© son propre compte

L'attribut SameSite des réponses HTTP Set-Cookie n'est pas bien renseigné

Les cookies token et session n'ont pas une valeur pour l'attribut SameSite, d'oĂč le warning sous Firefox :

Cookie “token” will be soon rejected because it has the “sameSite” attribute set to “none” or an invalid value, without the “secure” attribute. To know more about the “sameSite“ attribute, read https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite

D'aprĂšs le site MDN : Cet alerte apparaĂźt dans les cas oĂč des cookies requiĂšrent l'attribut SameSite=None et ne sont pas marquĂ©s Secure, Ă©tant donc refusĂ©s par le navigateur.

Nous avons deux options :

  1. Marquer les cookies "secure" quand on utilise https en utilisant config.secure (pour pouvoir continuer à développer en http)
    Par exemple avec le cookie session:
    app.use(session({ cookie: { maxAge: 300000, secure: config.secure } }));

  2. Ajouter une politique sameSite lax ou strict.
    Les cookies ne seront envoyĂ©s qu'avec les requĂȘtes effectuĂ©es sur le domaine de mĂȘme niveau, et ne seront pas envoyĂ©es sur les requĂȘtes vers des sites tiers.
    app.use(session({ cookie: { maxAge: 300000, sameSite: 'strict' ou 'lax' } }));

Des avis ?

Serveur OAuth (Pour connecter d'autres services)

Ajouter un serveur OAuth pour permettre de se connecter Ă  d'autres services (par exemple les pads mais aussi la future app de compta et je cherchais un outil OAuth pour rendre accessible les archives de Slack)

Permettre le changement de mot de passe mail

Pour avoir un meilleur niveau de sécurité, je souhaite pouvoir changer mon mot de passe (par exemple si j'ai choisi il y a longtemps un mot de passe de faible entropie, ou un mot de passe réutilisé qui a été compromis). Le mode opératoire actuel n'est pas satisfaisant, car je dois demander à un administrateur de beta.gouv de le faire et ne suis pas autonome.

Fichiers des polices référencés dans le CSS ne sont pas trouvés

Les fichiers Evolventa-Bold.ttf et SourceSansPro-Regular.otf sont référencés dans template.min.css et non trouvés.

GEThttps://secretariat.beta.gouv.fr/static/css/fonts/Evolventa/Evolventa-Bold.ttf
[HTTP/2 404 Not Found 25ms]

GEThttps://secretariat.beta.gouv.fr/static/css/fonts/Source%20Sans%20Pro/SourceSansPro-Regular.otf
[HTTP/2 404 Not Found 24ms]

Proposition de fonctionnalitĂ© : crĂ©ation d’email automatique

Proposition (Par Anne-So)

  • 1. La nouvelle recrue de la communautĂ© clique sur un bouton “CrĂ©er un compte”
  • 2. Il remplit un formulaire (nom, prĂ©nom, date de dĂ©but et de fin de mission, email pro ou perso...)
  • 3. Ça fait une PR sur Github pour crĂ©er la fiche (grosse valeur ajoutĂ©e car c’est une manipulation trĂšs fastidieuse pour tous les non devs de la communautĂ©)
  • 4. Ça crĂ©Ă© l’email
  • 5. Ça assigne un marrain ou une parraine (#391)

Gestion de la date dans le formulaire pour la création des fiches Github (onboarding view)

Aujourd'hui, les champs date sont des champs texte classiques avec une regexp pour enforcer le format yyyy-mm-dd. Deux améliorations sont nécessaires :

1- Utiliser un date-picker.
L'utilisation du est la solution la plus simple, mais le support n'est pas trĂšs complet (notamment Safari). Si on prend cette direction il faudra prĂ©voir un fallback. À noter que la date de dĂ©part pourrait ĂȘtre prĂ©-remplie avec la date d'aujourd'hui.

2- Validation de la date
En front et en back on doit s'assurer que les dates soient des dates valides et que la date de fin soit aprÚs la date de début.

Tirage au sort d'un.e marrain.ne

Constat

Beaucoup de personnes dans la communauté n'ont pas de lien avec quelqu'un de la communauté en dehors de leur équipe.
Quand il cherche un.e marrain.ne c'est compliqué à trouver.

Solution

  • On peut faire la demande d'un.e marrain.ne dans l'app
  • L'app tire au sort dans la communautĂ© une personne prĂ©sente depuis + de 6 mois
  • La personne tirĂ© au sort reçois un email, elle a 24h pour accepter l'invitation (Il y a 2 liens : Oui ou Non )
  • Si au bout de 24h la personne n'a pas acceptĂ© ou si elle refuse avant 24h, on tire au sort une nouvelle personne
  • Si une personne accepte, on met en relation

ProblÚme message d'erreur (j'essai de créé 2 fois un compte par exemple)

Si j'essai de créé 2 fois le compte le message d'erreur ne s'affiche pas
image

Log :

2020-07-24T22:51:41.050981+00:00 app[web.1]: Error: Vous n'avez pas le droits de créer le compte email de l'utilisateur
2020-07-24T22:51:41.050992+00:00 app[web.1]:     at module.exports.postEmail (/app/controllers/usersController.js:84:13)
2020-07-24T22:51:41.050993+00:00 app[web.1]:     at runNextTicks (internal/process/task_queues.js:62:5)
2020-07-24T22:51:41.050993+00:00 app[web.1]:     at processImmediate (internal/timers.js:429:9)

Solution :
Remplacer req.flash('error', err); par req.flash('error', err.message);

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.