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.
| Drapeau | Variable | Défaut | Ce qu'elle fait |
|---|---|---|---|
--port | PORT | 8088 | Port d'écoute |
--hostname | HOSTNAME | 0.0.0.0 | Adresse d'attache, 127.0.0.1 avec --auth=proxy |
--lang | DEFAULT_LANG | en | Langue pour qui n'en a pas choisi : en, fr, es, ru |
--baseurl | BASEURL | / | Préfixe de chemin quand le service vit sous un sous-chemin |
--db | DB_PATH | memo.sqlite | Le fichier qui contient tous les tableaux |
--tls | TLS | non | Servir HTTPS directement, pour le développement |
--tlsKey | TLS_KEY | local-dev-key.pem | Clé privée, avec --tls |
--tlsCert | TLS_CERT | local-dev-cert.pem | Certificat, avec --tls |
--logoUrl | LOGO_URL | Met une image à la place du nom écrit | |
--faviconUrl | FAVICON_URL | Remplace la favicône | |
--backupDir | BACKUP_DIR | backups | Où les sauvegardes sont écrites et cherchées |
--backupKeep | BACKUP_KEEP | 14 | Combien de sauvegardes garder |
--maxImages | MAX_IMAGES | 16 | Combien 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.
| Drapeau | Variable | Défaut | Ce qu'elle fait |
|---|---|---|---|
--auth | AUTH | local | local, proxy, ldap ou oidc |
--origin | ORIGIN | L'origine publique, https://memo.example | |
--admin | ADMIN_SUBJECT | Compte à passer admin, une fois, tant qu'aucun admin n'existe | |
--adminGroup | ADMIN_GROUP | Groupe de l'annuaire dont les membres sont admins | |
--closedRegistration | CLOSED_REGISTRATION | non | Empêcher la création de comptes locaux |
--allowAnonymousBoards | ALLOW_ANONYMOUS_BOARDS | non | Laisser créer un tableau sans s'identifier |
--insecureCookies | INSECURE_COOKIES | non | Retirer Secure du cookie de session, développement en HTTP seulement |
--allowPrivateFetch | ALLOW_PRIVATE_FETCH | non | Lire 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.
| Drapeau | Variable | Défaut |
|---|---|---|
--trustedProxies | TRUSTED_PROXIES | |
--proxyUserHeader | PROXY_USER_HEADER | remote-user |
--proxyGroupsHeader | PROXY_GROUPS_HEADER | remote-groups |
--proxyNameHeader | PROXY_NAME_HEADER | remote-name |
--proxyLoginUrl | PROXY_LOGIN_URL | |
--proxyLogoutUrl | PROXY_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é.
| Drapeau | Variable | Défaut |
|---|---|---|
--ldapUrl | LDAP_URL | |
--ldapBindDn | LDAP_BIND_DN | |
LDAP_BIND_PASSWORD | ||
--ldapBaseDn | LDAP_BASE_DN | |
--ldapUserFilter | LDAP_USER_FILTER | (uid={{username}}) |
--ldapUidAttr | LDAP_UID_ATTR | entryUUID |
--ldapGroupAttr | LDAP_GROUP_ATTR | memberOf |
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)
| Drapeau | Variable | Défaut |
|---|---|---|
--oidcIssuer | OIDC_ISSUER | |
--oidcClientId | OIDC_CLIENT_ID | |
OIDC_CLIENT_SECRET | ||
--oidcScopes | OIDC_SCOPES | openid 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
EnvironmentFiledans un service systemd. - Les noms auxquels le serveur répond lui-même,
admin,login,logout,register,s,ws,api,css,imagesetvendorentre 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.