diff --git a/README.md b/README.md index 73be58b..886b898 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,24 @@ Créer ou mettre à jour la base de données : bundle exec ruby bin/migrate ``` -Lors de la première installation, créer le compte du premier administrateur : +Créer un ou plusieurs comptes de test : ```sh -bundle exec ruby bin/initialize-admin +bundle exec ruby bin/initialize-account ``` -Cette commande est à exécuter une seule fois. Elle crée un compte vérifié et -lui attribue les droits techniques d'administration. Elle ne crée pas de -demande d'adhésion. +Cette commande crée un compte vérifié et sa fiche de membre. +Elle peut être exécutée plusieurs fois. Les comptes créés ne possèdent +aucune adhésion ni responsabilité au sein du Noyau. + +Pour tester l'espace du Noyau, désigner un ou plusieurs comptes de test : + +```sh +bundle exec ruby bin/initialize-core +``` + +En développement, un seul membre suffit. Cette commande peut être +réexécutée pour ajouter d'autres membres au Noyau. Démarrer le serveur de développement : @@ -60,16 +69,28 @@ bundle exec irb -r dotenv/load -r ./netig.rb Docker Compose fournit les variables d'environnement via `env_file` et définit `RACK_ENV=production`. -Exécuter les migrations dans le conteneur applicatif avant de démarrer la -nouvelle version : +Exécuter les migrations dans le conteneur applicatif avant de démarrer +la nouvelle version : ```sh bundle exec ruby bin/migrate ``` -Lors de la première installation, créer le premier administrateur depuis le -conteneur applicatif : +### Initialisation du premier Noyau + +Une fois l'inscription publique disponible, les membres fondateurs créent +leurs comptes depuis le site et vérifient leurs adresses électroniques. + +Après leur désignation par l'assemblée constitutive, initialiser le Noyau +depuis le conteneur applicatif : ```sh -bundle exec ruby bin/initialize-admin +bundle exec ruby bin/initialize-core ``` + +Cette commande doit être exécutée une seule fois en production. +Elle exige au moins trois personnes physiques disposant d'un compte vérifié. +Elle enregistre leurs adhésions comme acceptées et leur attribue la qualité +de membre du Noyau. + +La date de l'assemblée constitutive est demandée lors de l'exécution. diff --git a/bin/initialize-admin b/bin/initialize-account similarity index 66% rename from bin/initialize-admin rename to bin/initialize-account index 37c5781..cea7cd8 100644 --- a/bin/initialize-admin +++ b/bin/initialize-account @@ -1,6 +1,11 @@ #!/usr/bin/env ruby -require "dotenv/load" if ENV.fetch("RACK_ENV", "development") == "development" +environment = ENV.fetch("RACK_ENV", "development") +require "dotenv/load" if environment == "development" + +abort "Ce script est réservé au développement." unless + ENV.fetch("RACK_ENV", "development") == "development" + require "io/console" require "rodauth" @@ -8,16 +13,17 @@ require_relative "../lib/database" console = IO.console or abort "Terminal interactif requis." -abort "Un administrateur existe déjà." if - DB[:members].where(admin: true).count.positive? - console.print "Adresse électronique : " -email = console.gets&.strip +email = console.gets&.strip&.downcase abort "Adresse électronique obligatoire." if email.to_s.empty? -abort "Ce compte existe déjà." if - DB[:accounts].where(email: email).count.positive? +existing_account = DB[:accounts] + .where(Sequel.function(:lower, :email) => email) + .count + .positive? + +abort "Ce compte existe déjà." if existing_account console.print "Prénom : " first_name = console.gets&.strip @@ -31,16 +37,9 @@ abort "Prénom et nom obligatoires." if password = console.getpass("Mot de passe : ") confirmation = console.getpass("Confirmer le mot de passe : ") -abort "Mot de passe obligatoire." if password.empty? -abort "Les mots de passe ne correspondent pas." unless password == confirmation - -console.puts -console.puts "Administrateur : #{first_name} #{last_name} (#{email})" -console.print "Confirmer la création ? [o/N] " - -abort "Opération annulée." unless %w[o oui].include?( - console.gets&.strip&.downcase -) +abort "Mot de passe obligatoire." if password.to_s.empty? +abort "Les mots de passe ne correspondent pas." unless + password == confirmation verified_status_id = DB[:account_statuses] .where(name: "Verified") @@ -48,6 +47,15 @@ verified_status_id = DB[:account_statuses] abort "Statut Verified introuvable." unless verified_status_id +console.puts +console.puts "Compte : #{first_name} #{last_name} (#{email})" +console.puts +console.print "Confirmer la création ? [o/N] " + +abort "Opération annulée." unless %w[o oui].include?( + console.gets&.strip&.downcase +) + auth = Rodauth.lib do db DB enable :create_account @@ -73,8 +81,9 @@ DB.transaction do first_name: first_name, last_name: last_name, organization_name: nil, - admin: true + core_member: false ) end -puts "Premier administrateur créé." +puts "Compte de développement créé." + diff --git a/bin/initialize-core b/bin/initialize-core new file mode 100644 index 0000000..c2988ae --- /dev/null +++ b/bin/initialize-core @@ -0,0 +1,169 @@ +#!/usr/bin/env ruby + +environment = ENV.fetch("RACK_ENV", "development") +require "dotenv/load" if environment == "development" + +abort "Environnement non pris en charge." unless + %w[development production].include?( + ENV.fetch("RACK_ENV", "development") + ) + +require "date" +require "io/console" + +require_relative "../lib/database" + +console = IO.console or abort "Terminal interactif requis." + +if environment == "production" && + DB[:members].where(core_member: true).count.positive? + abort "Un Noyau existe déjà." +end + +verified_status_id = DB[:account_statuses] + .where(name: "Verified") + .get(:id) + +abort "Statut Verified introuvable." unless verified_status_id + +find_member = lambda do |email| + account = DB[:accounts] + .where(Sequel.function(:lower, :email) => email) + .first + + abort "Compte introuvable : #{email}" unless account + + abort "Adresse électronique non vérifiée : #{email}" unless + account[:status_id] == verified_status_id + + member = DB[:members] + .where(account_id: account[:id]) + .first + + abort "Fiche membre introuvable : #{email}" unless member + + abort "Une personne physique est requise : #{email}" unless + member[:kind] == "individual" + + member +end + +console.print "Date de l'assemblée (AAAA-MM-JJ) : " +date_input = console.gets&.strip + +unless date_input&.match?(/\A\d{4}-\d{2}-\d{2}\z/) + abort "Format attendu : AAAA-MM-JJ." +end + +begin + assembly_date = Date.iso8601(date_input) +rescue Date::Error + abort "Date invalide." +end + +abort "La date ne peut pas être dans le futur." if + assembly_date > Date.today + +assembly_datetime = Time.utc( + assembly_date.year, + assembly_date.month, + assembly_date.day +) + +console.puts +console.puts "Saisissez les adresses électroniques des membres désignés." +console.puts "Une adresse par ligne. Entrée pour terminer." +console.puts + +emails = [] + +loop do + console.print "Adresse électronique : " + email = console.gets&.strip&.downcase + + break if email.to_s.empty? + + abort "Adresse déjà saisie : #{email}" if emails.include?(email) + + emails << email +end + +minimum_members = environment == "production" ? 3 : 1 + +abort "Au moins #{minimum_members} membre(s) sont nécessaires." if + emails.length < minimum_members + +members = emails.map do |email| + [email, find_member.call(email)] +end + +console.puts +console.print "Adresse de la personne qui enregistre la décision : " +recorded_by_email = console.gets&.strip&.downcase + +abort "Adresse électronique obligatoire." if + recorded_by_email.to_s.empty? + +recorded_by = find_member.call(recorded_by_email) + +console.puts +console.puts "Environnement : #{environment}" +console.puts "Date de l'assemblée : #{assembly_date}" +console.puts +console.puts "Membres du Noyau :" + +members.each do |email, member| + name = "#{member[:first_name]} #{member[:last_name]}" + console.puts " #{name} (#{email})" +end + +console.puts +console.puts "Décision enregistrée par : #{recorded_by_email}" +console.puts +console.print "Confirmer l'initialisation ? [o/N] " + +abort "Opération annulée." unless %w[o oui].include?( + console.gets&.strip&.downcase +) + +if environment == "production" && + DB[:members].where(core_member: true).count.positive? + abort "Un Noyau existe déjà." +end + +DB.transaction do + members.each do |_email, member| + membership = DB[:memberships] + .where( + member_id: member[:id], + status: %w[pending accepted], + ended_at: nil + ) + .first + + if membership && membership[:status] == "pending" + DB[:memberships] + .where(id: membership[:id]) + .update( + status: "accepted", + decided_at: assembly_datetime, + recorded_by: recorded_by[:id] + ) + elsif membership.nil? + DB[:memberships].insert( + member_id: member[:id], + status: "accepted", + requested_at: assembly_datetime, + decided_at: assembly_datetime, + recorded_by: recorded_by[:id] + ) + end + + DB[:members] + .where(id: member[:id]) + .update(core_member: true) + end +end + +puts "Noyau initialisé (#{members.length} membres)." + diff --git a/db/migrations/002_create_members.rb b/db/migrations/002_create_members.rb index df00c99..9cdfcf7 100644 --- a/db/migrations/002_create_members.rb +++ b/db/migrations/002_create_members.rb @@ -12,7 +12,7 @@ Sequel.migration do String :last_name String :organization_name - TrueClass :admin, null: false, default: false + TrueClass :core_member, null: false, default: false index :account_id, unique: true @@ -28,6 +28,9 @@ Sequel.migration do "AND last_name IS NULL AND organization_name IS NOT NULL " \ "AND trim(organization_name) <> '')" ) + + constraint :members_core_member_individual, + Sequel.lit("core_member = FALSE OR kind = 'individual'") end end diff --git a/doc/legal.md b/doc/legal.md index f598d64..0a840f4 100644 --- a/doc/legal.md +++ b/doc/legal.md @@ -1,9 +1,9 @@ # Informations légales et conditions d’utilisation NETig est un projet associatif régi par la loi du 1er juillet 1901. -L’association n’a pas encore été déclarée en préfecture et ne dispose donc -pas encore de la personnalité morale ni de la capacité juridique attachée -à une association déclarée. Cette information sera mise à jour après la +L’association n’a pas encore été déclarée en préfecture et ne dispose +donc pas encore de la personnalité morale ni de la capacité juridique +attachée à une association déclarée. Cette information sera mise à jour après la déclaration et sa publication au Journal officiel des associations et fondations d’entreprises (JOAFE). @@ -30,30 +30,29 @@ Finlande. Les services NETig sont proposés sans facturation liée à leur utilisation. Certains sont accessibles au public ; d’autres sont réservés aux membres -à jour de leur cotisation. +de l’association. -Conformément aux statuts, toute demande d’adhésion est examinée par le Noyau. -L’adhésion prend effet après acceptation de la demande et paiement de la -cotisation annuelle fixée par l’Assemblée générale. Elle dure un an et se -renouvelle à chaque échéance si la cotisation est payée. La création d’un -compte utilisateur, lorsqu’elle sera proposée, ne vaudra pas à elle seule -adhésion à l’association. +Toute personne physique ou morale peut demander à adhérer à l’association, +à condition d’accepter ses statuts et ses principes. Le Noyau examine les +demandes et décide de l’admission. L’adhésion prend effet à la date de +son acceptation et est valable sans limitation de durée. La création d’un compte +utilisateur ne vaut pas, à elle seule, adhésion à l’association. -En cas d’impayé, une relance laisse trente jours pour régler la cotisation. -Pendant ce délai, le membre n’est plus considéré comme à jour : il ne peut -pas voter en Assemblée générale et ses accès aux services réservés aux -membres à jour peuvent être suspendus. À défaut de paiement à l’expiration -de ce délai, l’adhésion prend fin. - -Les dons sont libres et volontaires. Ils contribuent au financement des -infrastructures, ne remplacent pas la cotisation nécessaire à l’adhésion -et ne donnent droit à aucun accès ou avantage particulier. +Aucune cotisation ni aucun don n’est exigé pour adhérer, conserver la +qualité de membre ou exercer les droits qui y sont attachés. Les dons sont +libres, volontaires et sans contrepartie. NETig invite les membres et les +utilisateurs qui le peuvent à soutenir financièrement l’association, selon +leurs moyens. Cette participation contribue au financement des +infrastructures et à la pérennité des services, mais ne conditionne ni +l’adhésion, ni les droits des membres, ni l’accès aux services et ne donne +droit à aucun avantage particulier. L’accès à chaque service dépend des ressources disponibles et peut être limité. Une disponibilité permanente n’est pas garantie. L’état des services -est consultable sur la [page de statut](https://status.netig.net/status/services). -Les interruptions de maintenance peuvent être annoncées par la -[liste de diffusion dédiée](https://list.netig.net/subscription/form). +est consultable sur la page de statut : +[page de statut](https://status.netig.net/status/services). Les interruptions +de maintenance peuvent être annoncées par la liste de diffusion dédiée : +[liste de diffusion](https://list.netig.net/subscription/form). ### Utilisation des services @@ -71,11 +70,12 @@ propres copies lorsque cela est possible. La personne responsable d’un service peut limiter ou suspendre un accès de manière proportionnée en cas d’abus ou de risque pour la sécurité, -les données, le fonctionnement ou les ressources du service. Elle informe -la personne concernée et le Noyau des motifs de la mesure. +les données, le fonctionnement ou les ressources du service. Elle en +informe la personne concernée ainsi que le Noyau, en indiquant les motifs +de la mesure. La personne concernée peut demander au Noyau de réexaminer cette mesure. -Le Noyau répond sous quinze jours. Si le Noyau a pris ou confirmé la mesure, +Celui-ci répond sous quinze jours. Si le Noyau a pris ou confirmé la mesure, la personne concernée ou tout membre peut demander son réexamen par l’Assemblée générale. La suspension d’un accès ne met pas fin à la qualité de membre. @@ -101,13 +101,16 @@ la personne ou de la structure qui détermine actuellement les finalités et les moyens des traitements]. La présente notice décrit les traitements réalisés sur le site netig.net, -notamment par son formulaire de contact. Les services hébergés sur d’autres -adresses peuvent mettre en œuvre des traitements propres ; leurs modalités -spécifiques doivent être présentées dans les services concernés. +notamment par son formulaire de contact et par les fonctions +d’authentification réservées aux comptes créés par l’administration. Les +services hébergés sur d’autres adresses peuvent mettre en œuvre des +traitements propres ; leurs modalités spécifiques doivent être présentées +dans les services concernés. -À la date de cette notice, le site netig.net ne propose pas encore de -création de compte utilisateur ni d’adhésion en ligne. Les informations -relatives à ces fonctions seront ajoutées avant leur mise en service. +À la date de cette notice, le site netig.net ne propose pas encore +d’inscription publique ni de demande d’adhésion en ligne. Ces fonctionnalités +et les traitements associés feront l’objet d’une mise à jour de la notice +avant leur mise en service. ### Données traitées et finalités @@ -115,10 +118,18 @@ Le formulaire de contact recueille le nom, l’adresse électronique, l’objet et le contenu du message. Ces informations sont utilisées pour répondre aux demandes et assurer le suivi des échanges. +Pour les comptes déjà créés, l’adresse électronique sert d’identifiant. +Une empreinte du mot de passe est conservée pour permettre +l’authentification ; le mot de passe n’est pas enregistré en clair. Les +informations d’identité associées au compte, lorsqu’elles sont renseignées, +peuvent comprendre le type de membre, le prénom, le nom ou la dénomination +de la personne morale. Ces données sont utilisées pour gérer les comptes +et l’administration du site. + Des données techniques nécessaires au fonctionnement et à la sécurité du -site peuvent également être traitées, notamment dans les journaux du -serveur. Leur nature et leur durée de conservation doivent être adaptées -à leur utilité réelle. +site peuvent également être traitées, notamment les données de session et +les journaux du serveur. Leur nature et leur durée de conservation doivent +être adaptées à leur utilité réelle. Les données ne sont ni vendues ni utilisées à des fins publicitaires. @@ -126,16 +137,19 @@ Les données ne sont ni vendues ni utilisées à des fins publicitaires. Les messages sont traités dans le cadre de l’intérêt légitime à répondre aux sollicitations adressées au projet NETig et à assurer le suivi des -échanges. Les données techniques nécessaires à la sécurité sont traitées -dans l’intérêt légitime de protéger le site, les services et leurs -utilisateurs. +échanges. Les données nécessaires à l’authentification et à la gestion +des comptes sont traitées dans l’intérêt légitime de sécuriser +l’administration du site. Les données techniques nécessaires à la sécurité +sont traitées dans l’intérêt légitime de protéger le site, les services +et leurs utilisateurs. ### Destinataires et hébergement -Les messages sont transmis à l’adresse [contact@netig.net](mailto:contact@netig.net) -et peuvent être consultés par les personnes habilitées à répondre aux -demandes. Les données techniques peuvent être accessibles aux personnes -chargées de l’administration des infrastructures et aux prestataires +Les messages sont transmis à l’adresse +[contact@netig.net](mailto:contact@netig.net) et peuvent être consultés par +les personnes habilitées à répondre aux demandes. Les données des comptes +et les données techniques peuvent être accessibles aux personnes habilitées +à administrer le site et les infrastructures, ainsi qu’aux prestataires techniques concernés. L’hébergement principal est assuré par Hetzner Online GmbH. Des sauvegardes @@ -147,12 +161,15 @@ Les messages reçus sont conservés dans la messagerie le temps nécessaire au traitement de la demande et au suivi des échanges, puis supprimés, sauf nécessité de conservation plus longue ou obligation légale. -Les journaux techniques et les sauvegardes peuvent conserver certaines -données après leur suppression des systèmes actifs. Les durées de -conservation effectivement appliquées doivent être précisées avant -l’ouverture des services concernés : +Les données des comptes sont conservées pendant la durée nécessaire à leur +gestion et à la sécurisation du site. Les journaux techniques et les +sauvegardes peuvent conserver certaines données après leur suppression des +systèmes actifs. Les durées de conservation effectivement appliquées doivent +être précisées avant l’ouverture des services concernés : - **Messages de contact :** [durée à définir]. +- **Comptes et fiches associées :** [durée à définir]. +- **Données de session :** [durée à définir]. - **Journaux techniques :** [durée à définir]. - **Sauvegardes :** [durée de rétention à définir]. @@ -163,15 +180,15 @@ l’accès à vos données, leur rectification ou leur effacement, la limitation de leur traitement et vous opposer à certains traitements. Vous pouvez également demander la portabilité lorsque ce droit s’applique. -Pour exercer vos droits, écrivez à [contact@netig.net](mailto:contact@netig.net). -Vous pouvez également introduire une réclamation auprès de la -[CNIL](https://www.cnil.fr/). +Pour exercer vos droits, écrivez à +[contact@netig.net](mailto:contact@netig.net). Vous pouvez également +introduire une réclamation auprès de la [CNIL](https://www.cnil.fr/). ### Cookies Le site utilise des cookies techniques nécessaires au fonctionnement -des sessions et à la sécurité. Ils ne sont pas utilisés à des fins de -publicité ou de suivi de navigation. +des sessions et à la sécurité, notamment pour l’authentification. Ils ne +sont pas utilisés à des fins de publicité ou de suivi de navigation. ## Contact diff --git a/lib/authorization.rb b/lib/authorization.rb new file mode 100644 index 0000000..9349eb5 --- /dev/null +++ b/lib/authorization.rb @@ -0,0 +1,27 @@ +class Netig + private + + def require_core_member!(r) + rodauth.require_account + + core_member = DB[:members] + .join(:memberships, member_id: :id) + .where( + Sequel[:members][:account_id] => rodauth.account_id, + Sequel[:members][:kind] => "individual", + Sequel[:members][:core_member] => true, + Sequel[:memberships][:status] => "accepted", + Sequel[:memberships][:ended_at] => nil + ) + .count + .positive? + + return if core_member + + r.halt [ + 403, + { "Content-Type" => "text/plain; charset=utf-8" }, + ["Accès interdit."] + ] + end +end diff --git a/netig.rb b/netig.rb index 6f2c7e3..4c45126 100644 --- a/netig.rb +++ b/netig.rb @@ -35,7 +35,8 @@ class Netig < Roda logout_redirect "/" end - require_relative "routes/admin" + require_relative "lib/authorization" + require_relative "routes/noyau" require_relative "routes/contact" route do |r| diff --git a/public/css/style.css b/public/css/style.css index 8ba43bf..3c53568 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -490,23 +490,23 @@ summary:focus-visible { /* Admin ------------------------------------------------------------------------ */ -.admin-table { +.members-table { width: 100%; border-collapse: collapse; text-align: left; } -.admin-table th, -.admin-table td { +.members-table th, +.members-table td { padding: 10px 16px; border-bottom: 1px solid var(--color-shadow-light); } -.admin-table th { +.members-table th { color: var(--color-highlight); } -.admin-table td { +.members-table td { vertical-align: top; } diff --git a/routes/admin.rb b/routes/admin.rb deleted file mode 100644 index 920ea34..0000000 --- a/routes/admin.rb +++ /dev/null @@ -1,51 +0,0 @@ -class Netig - hash_branch "admin" do |r| - require_admin!(r) - - @page_title = "Admin — NETig" - @page_description = "Interface d'administration de NETig." - @canonical_url = "https://netig.net/admin" - - r.is do - r.get do - view "admin" - end - end - - r.on "members" do - r.is do - r.get do - @page_title = "Adhérents — Admin — NETig" - - @members = DB[:members] - .join(:accounts, id: :account_id) - .select_all(:members) - .select_append(Sequel[:accounts][:email].as(:email)) - .order(Sequel[:members][:id]) - .all - - view "admin/members" - end - end - end - end - - private - - def require_admin!(r) - rodauth.require_account - - admin = DB[:members].where( - account_id: rodauth.account_id, - admin: true - ).get(:admin) - - return if admin - - r.halt [ - 403, - { "Content-Type" => "text/plain; charset=utf-8" }, - ["Accès interdit."] - ] - end -end diff --git a/routes/noyau.rb b/routes/noyau.rb new file mode 100644 index 0000000..cb47301 --- /dev/null +++ b/routes/noyau.rb @@ -0,0 +1,34 @@ + +class Netig + hash_branch "noyau" do |r| + require_core_member!(r) + + @page_title = "Noyau — NETig" + @page_description = "Espace de gestion de l'association NETig." + @canonical_url = "https://netig.net/noyau" + + r.is do + r.get do + view "noyau" + end + end + + r.on "members" do + r.is do + r.get do + @page_title = "Adhérents — Noyau — NETig" + + @members = DB[:members] + .join(:accounts, id: :account_id) + .select_all(:members) + .select_append(Sequel[:accounts][:email].as(:email)) + .order(Sequel[:members][:id]) + .all + + view "noyau/members" + end + end + end + end +end + diff --git a/views/index.erb b/views/index.erb index e281061..dc6a976 100644 --- a/views/index.erb +++ b/views/index.erb @@ -113,8 +113,7 @@

Aidez-nous à continuer !

-

Les services sont financés grâce à vos adhésions et vos dons. Votre soutien nous aide à couvrir les frais d'hébergement et à maintenir les services accessibles.

- Adhérer ➜ +

Les services ne sont financés que par vos dons. Votre soutien nous aide à couvrir les frais d'hébergement et à maintenir les services accessibles.

Faire un don ➜
diff --git a/views/admin.erb b/views/noyau.erb similarity index 61% rename from views/admin.erb rename to views/noyau.erb index ce4012f..db70eea 100644 --- a/views/admin.erb +++ b/views/noyau.erb @@ -1,9 +1,9 @@
-

Administration de NETig

+

Espace du Noyau

- + Gérer les adhérents

diff --git a/views/admin/members.erb b/views/noyau/members.erb similarity index 87% rename from views/admin/members.erb rename to views/noyau/members.erb index e691762..49bc675 100644 --- a/views/admin/members.erb +++ b/views/noyau/members.erb @@ -5,13 +5,12 @@ <% if @members.empty? %>

Aucun adhérent enregistré.

<% else %> - +
- @@ -28,7 +27,6 @@ "Personne physique" : "Personne morale" %> - <% end %> @@ -36,3 +34,4 @@ <% end %> +
Identité Type Adresse électroniqueAdministration
<%= member[:email] %><%= member[:admin] ? "Oui" : "Non" %>