Memo
Installation
Sign in Create an account
Administration

Installation

Memo, c'est un processus et un fichier. Pas de serveur de base de données à installer, pas d'étape de compilation, rien qui doive se trouver à côté du binaire.

Tous les tableaux, tous les comptes, toutes les images et tous les liens de partage tiennent dans un fichier SQLite. Sauvegarder une instance, c'est copier ce fichier ; la déplacer sur une autre machine, c'est l'y copier.

Le démarrer

Un binaire de version n'a besoin de rien d'autre :

./memo --port 8088 --db /var/lib/memo/memo.sqlite

Depuis une copie du dépôt, avec Bun :

bun install
bun run start

La base apparaît au premier lancement, et le serveur affiche l'adresse sur laquelle il écoute. Le premier compte créé sur une installation sans admin devient l'admin : une installation que personne ne peut administrer n'aurait aucun moyen d'en désigner un.

Derrière un reverse proxy

Terminez le TLS au proxy et faites tout passer, /ws compris. Deux choses que le proxy doit faire correctement :

  • La montée en WebSocket sur /ws, avec un délai de lecture assez long pour un tableau au repos. Une minute ne suffit pas : les gens laissent un tableau ouvert tout l'après-midi.
  • --origin https://memo.example, l'adresse publique. Toutes les vérifications CSRF et d'origine WebSocket s'y comparent. Sans elle, la vérification retombe sur l'hôte annoncé par la requête, ce qui arrête encore un envoi de formulaire venu d'ailleurs, mais fait confiance à ce qui est devant.

--baseurl /memo sert le tout sous un sous-chemin quand la machine héberge autre chose.

NixOS

La flake construit le même binaire et fournit un module qui déclare le reste du déploiement : le service, un reverse proxy, un certificat, une sauvegarde de nuit et une minuterie de mise à jour.

services.memo = {
  enable = true;
  domain = "memo.example.org";
  openFirewall = true;

  proxy.server = "nginx";
  proxy.acme = {
    enable = true;
    email = "ops@example.org";
  };

  settings = {
    lang = "fr";
    auth = "oidc";
    oidcIssuer = "https://id.example.org";
    oidcClientId = "memo";
    adminGroup = "memo-admins";
  };

  environmentFile = "/run/secrets/memo.env";

  backup = {
    enable = true;
    keep = 30;
    schedule = "*-*-* 03:30:00";
  };
};

Cela crée l'utilisateur memo, met la base dans /var/lib/memo, attache le serveur à la boucle locale puisque nginx occupe le 443, obtient le certificat et remplit --origin à partir du domaine. settings prend toutes les options ci-dessous par leur nom de drapeau sans les tirets. Le README du dépôt décrit les options propres au module.

Les options

Chaque option est à la fois un --drapeau et une variable d'environnement. Le drapeau l'emporte sur la variable, qui l'emporte sur la valeur par défaut. Les deux mots de passe font exception : ils ne passent que par l'environnement, à dessein, parce que ps montre les arguments d'un service à tous les comptes de la machine.

memo --help affiche la liste entière avec les valeurs par défaut.

DrapeauVariableDéfautCe qu'elle fait
--portPORT8088Port d'écoute
--hostnameHOSTNAME0.0.0.0Adresse d'attache, 127.0.0.1 avec --auth=proxy
--langDEFAULT_LANGenLangue pour qui n'en a pas choisi : en, fr, es, ru
--baseurlBASEURL/Préfixe de chemin quand le service vit sous un sous-chemin
--dbDB_PATHmemo.sqliteLe fichier qui contient tous les tableaux
--tlsTLSnonServir HTTPS directement, pour le développement
--tlsKeyTLS_KEYlocal-dev-key.pemClé privée, avec --tls
--tlsCertTLS_CERTlocal-dev-cert.pemCertificat, avec --tls
--logoUrlLOGO_URLMet une image à la place du nom écrit
--faviconUrlFAVICON_URLRemplace la favicône
--backupDirBACKUP_DIRbackupsOù les sauvegardes sont écrites et cherchées
--backupKeepBACKUP_KEEP14Combien de sauvegardes garder
--maxImagesMAX_IMAGES16Combien d'images un tableau peut porter

Un drapeau est actif par sa seule présence, donc --tls tout seul. En variable il prend 1, true, yes ou on ; tout le reste, 0 compris, vaut non.

DEFAULT_LANG ne s'appelle pas LANG, et c'est voulu : cette dernière est déjà réglée sur votre propre locale sur presque toutes les machines, et en hériter changerait ce que voient les visiteurs selon la façon dont le serveur a été lancé.

Les comptes

Memo tient toujours des comptes et refuse de démarrer sans backend : une installation sans système de comptes n'a pas de page d'administration et ne permet à personne de retrouver les tableaux auxquels il a participé. Ouvrir un tableau et y écrire ne demande toujours aucun compte. C'est en créer un nouveau qui demande de s'identifier.

Un seul backend vit par processus. Les comptes sont indexés sur le backend et le sujet : changer --auth sur une instance existante laisse les anciens comptes en place mais hors d'atteinte, celui de l'admin compris.

DrapeauVariableDéfautCe qu'elle fait
--authAUTHlocallocal, proxy, ldap ou oidc
--originORIGINL'origine publique, https://memo.example
--adminADMIN_SUBJECTCompte à passer admin, une fois, tant qu'aucun admin n'existe
--adminGroupADMIN_GROUPGroupe de l'annuaire dont les membres sont admins
--closedRegistrationCLOSED_REGISTRATIONnonEmpêcher la création de comptes locaux
--allowAnonymousBoardsALLOW_ANONYMOUS_BOARDSnonLaisser créer un tableau sans s'identifier
--insecureCookiesINSECURE_COOKIESnonRetirer Secure du cookie de session, développement en HTTP seulement
--allowPrivateFetchALLOW_PRIVATE_FETCHnonLire des adresses de cette machine et de son réseau, développement seul

--adminGroup donne le dernier mot à l'annuaire : qui perd le groupe en amont perd le rôle ici à sa prochaine connexion. Sans lui, le rôle est celui que la page d'administration a réglé en dernier. --admin n'accorde le rôle que tant que l'instance n'a aucun admin, si bien que l'oublier dans un fichier de service ne peut pas le rétablir en douce après un retrait.

Les mots de passe gardés ici (--auth=local)

Hachés en argon2id, dix caractères au moins. Les inscriptions sont ouvertes sauf si --closedRegistration dit le contraire.

Derrière un proxy d'authentification (--auth=proxy)

Authelia, et tout ce qui authentifie devant puis passe le résultat en en-têtes. Memo n'ouvre aucune session à lui dans ce mode : se déconnecter en amont déconnecte ici.

DrapeauVariableDéfaut
--trustedProxiesTRUSTED_PROXIES
--proxyUserHeaderPROXY_USER_HEADERremote-user
--proxyGroupsHeaderPROXY_GROUPS_HEADERremote-groups
--proxyNameHeaderPROXY_NAME_HEADERremote-name
--proxyLoginUrlPROXY_LOGIN_URL
--proxyLogoutUrlPROXY_LOGOUT_URL

--trustedProxies est obligatoire et le serveur refuse de démarrer sans. Un en-tête n'est qu'un en-tête : quiconque atteint le port peut envoyer Remote-User: alice et être alice. Le drapeau prend des adresses ou des plages CIDR, comparées à l'adresse du pair de la connexion et jamais à quoi que ce soit dans la requête.

Le proxy doit écraser ces en-têtes et non s'y ajouter, sinon un client peut placer les siens à côté. Il doit aussi couvrir /ws avec la même authentification. L'oublier, c'est avoir chaque page authentifiée pendant que chaque socket est anonyme, ce qui ressemble à un bug intermittent plutôt qu'à une faille.

L'entrée comme la sortie appartiennent au portail. --proxyLoginUrl et --proxyLogoutUrl disent où elles sont ; {back} dans l'une ou l'autre est l'adresse de retour, encodée pour une URL, et {back64} la même en base64. Sans elles, Memo n'affiche ni l'un ni l'autre lien, parce qu'une sortie qui ne déconnecte personne est pire que pas de sortie du tout.

--proxyLoginUrl  'https://auth.example/?rd={back}'
--proxyLogoutUrl 'https://auth.example/logout'

Sous le SSOwat de YunoHost, qui est un proxy d'authentification, les noms d'en-têtes ne sont pas une préférence, ce sont la frontière de sécurité. SSOwat efface tout en-tête envoyé par un client dont le nom commence par ynh_ ou ynh- avant de poser les siens, et ceux-là seulement. Pointez --proxyUserHeader ailleurs et un visiteur peut envoyer cet en-tête lui-même.

--auth proxy
--trustedProxies 127.0.0.1
--proxyUserHeader YNH_USER
--proxyNameHeader YNH_USER_FULLNAME
--proxyLoginUrl  'https://memo.example/yunohost/sso?r={back64}'
--proxyLogoutUrl 'https://memo.example/yunohost/portalapi/logout'
--origin https://memo.example

SSOwat n'envoie pas de groupes, --adminGroup n'a donc rien à comparer. Nommez le premier admin avec --admin, ou donnez le rôle depuis /admin.

Un annuaire (--auth=ldap)

Memo cherche avec un compte de service, puis se connecte au nom de la personne avec le mot de passe qu'elle a tapé.

DrapeauVariableDéfaut
--ldapUrlLDAP_URL
--ldapBindDnLDAP_BIND_DN
LDAP_BIND_PASSWORD
--ldapBaseDnLDAP_BASE_DN
--ldapUserFilterLDAP_USER_FILTER(uid={{username}})
--ldapUidAttrLDAP_UID_ATTRentryUUID
--ldapGroupAttrLDAP_GROUP_ATTRmemberOf

Le compte est indexé sur --ldapUidAttr, qui doit être quelque chose qui ne change jamais. Pas le DN : déplacer quelqu'un d'une unité d'organisation à une autre rendrait orphelins tous ses tableaux.

C'est le seul backend où Memo manipule un mot de passe d'annuaire. Si l'annuaire est déjà derrière Authelia ou Keycloak, --auth=proxy ou --auth=oidc atteint les mêmes comptes sans cela.

OpenID Connect (--auth=oidc)

DrapeauVariableDéfaut
--oidcIssuerOIDC_ISSUER
--oidcClientIdOIDC_CLIENT_ID
OIDC_CLIENT_SECRET
--oidcScopesOIDC_SCOPESopenid profile email groups

Déclarez <origine>/auth/callback comme URI de redirection. Code d'autorisation avec PKCE, et les claims viennent du point d'accès userinfo.

La page d'administration

Un admin dispose de /admin : tous les tableaux et tous les comptes, de quoi cocher plusieurs tableaux et les supprimer d'un coup, et un onglet Sauvegardes qui montre les copies de la base présentes sur le serveur, permet d'en prendre une sur-le-champ, de les télécharger et de les effacer. Une sauvegarde contient tout ce que l'instance contient, les tableaux privés et les empreintes des mots de passe compris, et se traite donc comme le serveur lui-même.

L'apparence de l'instance

L'onglet Apparence liste toutes les couleurs et toutes les tailles que la feuille de style déclare, avec la valeur qu'elles prennent en mode clair à côté de celle qu'elles prennent en mode sombre. Couleurs des post-it, couleurs des boîtes, bordure d'un tableau, encre d'une carte mentale, fond d'une page : chacune est un champ, et chaque champ montre sa valeur d'origine tant que vous n'écrivez pas par-dessus. Un champ laissé vide veut dire que rien ne change.

La page applique ce que vous tapez pendant que vous le tapez, vous voyez donc une couleur tomber avant de l'enregistrer. Le sélecteur en bas à gauche bascule la page entre clair et sombre, c'est ainsi qu'on vérifie l'autre colonne.

Sous la liste, une zone reçoit du CSS à vous, ajouté à chaque page après tout le reste. Rien ne le vérifie : une accolade oubliée et la suite ne fait plus rien. Enregistrez par petites touches.

Ce que vous enregistrez est gardé en base et servi à tout le monde ici sous /css/instance.css. Revenir aux valeurs d'origine vide tout, votre CSS compris.

Les sauvegardes

VACUUM INTO prend une copie cohérente pendant que le serveur tourne : pas d'interruption et pas de lecture à moitié écrite. L'onglet Sauvegardes en prend une, et le script du dépôt aussi :

bun run backup                       # -> backups/memo-<horodatage>.sqlite
bun run backup --out /srv/backups --keep 14

Il ouvre la copie et vérifie que les tables attendues y sont avant d'élaguer les plus anciennes, et sort en erreur si elles n'y sont pas. Une minuterie systemd ou une ligne de cron est la façon habituelle de le lancer chaque nuit ; sur NixOS, backup.enable s'en charge.

Restaurer, c'est copier une sauvegarde par-dessus --db, serveur arrêté.

Les images

Un tableau porte un fond et jusqu'à --maxImages images, seize par défaut, et un compte porte la sienne. Elles vivent dans la base plutôt qu'à côté, si bien qu'un seul fichier reste l'instance entière : une sauvegarde emporte tout et restaurer est une copie. Supprimer un tableau emporte ses images avec lui.

Tout est réencodé à l'entrée et seul le résultat est gardé : WebP, jamais plus large que 1920, jamais agrandi, sous 600 ko. /admin montre ce que pèsent les images par tableau et au total, ce qui est l'instrument plutôt que la limite.

Bon à savoir

  • Les secrets ne sont lus que dans l'environnement. Utilisez EnvironmentFile dans un service systemd.
  • Les noms auxquels le serveur répond lui-même, admin, login, logout, register, s, ws, api, css, images et vendor entre autres, ne peuvent pas être des noms de tableaux. Un tableau existant qui porte un tel nom est laissé tranquille et signalé au démarrage ; renommez-le depuis la page d'administration.
  • Fermer un tableau vous en rend propriétaire, et en créer un en étant identifié aussi. Les liens de partage existent en lecture seule et en écriture, sont stockés hachés, et ne sont montrés qu'une fois, à leur création. Retirer un accès ou un lien déconnecte qui s'en servait.
  • Une clé API faite par un compte admin porte le rôle d'admin. Le guide développeur dit ce qu'une clé atteint.

Documentation

Getting started Installation API

On this page