Compare commits

..
72 Commits
Author SHA1 Message Date
julien a6ba3681b2 Manage membership 2026-11-03 21:38:38 +01:00
julien 491457c50e Display membership 2026-11-03 20:52:39 +01:00
julien 4a6ca8b569 Fix 2026-11-03 20:29:23 +01:00
julien 311f25f6fd Typo 2026-11-03 20:26:52 +01:00
julien 9ea0bee39d Add member account 2026-11-03 20:21:21 +01:00
julien bf633b7060 Doc 2026-11-03 18:24:23 +01:00
julien acc4c43250 Doc 2026-11-03 18:17:18 +01:00
julien 4d8c7c7c71 Doc 2026-11-03 18:13:39 +01:00
julien 06312e8d83 Doc 2026-11-03 17:50:16 +01:00
julien 21ebe95cbb Remove rackup 2026-11-03 17:37:16 +01:00
julien 5402026dae Typo 2026-11-03 17:05:02 +01:00
julien 4ba81f3a6b Add contact flash 2026-11-03 16:47:04 +01:00
julien 9d1cda213e Improve style 2026-11-03 16:20:12 +01:00
julien e4d688171d Improve style 2026-11-03 16:14:38 +01:00
julien 189ad45eea Improve flash messages 2026-11-03 15:59:30 +01:00
julien 2d72b20cad Improve script 2026-11-03 15:18:09 +01:00
julien a57fa574eb Imrpove style 2026-11-03 14:34:14 +01:00
julien 5cf45c2084 Improve style 2026-11-03 13:51:03 +01:00
julien b8fd7863d6 Improve statutes 2026-11-03 12:53:54 +01:00
julien e5ec0ff1a1 Add registration 2026-11-03 12:08:57 +01:00
julien 137e53618b Imrpove migrations 2026-11-03 10:47:07 +01:00
julien 49b53451b7 Display member statut in core page 2026-11-02 22:56:47 +01:00
julien 9aa3da5191 Imrpove migrations 2026-11-02 22:46:41 +01:00
julien db316b2914 Remove admin account 2026-11-02 22:00:14 +01:00
julien 4f28a59c7e Imrpove migrations 2026-11-02 19:32:48 +01:00
julien f386499d2f Add admin style 2026-11-02 17:38:41 +01:00
julien 9358dd39a1 Add login/logout 2026-11-02 17:20:48 +01:00
julien cde5e651a6 Add first admin creation script 2026-11-02 15:46:50 +01:00
julien 9a554a764a Remove rake 2026-11-02 15:03:40 +01:00
julien cde8e13705 Add database schema and migration tasks 2026-11-02 14:08:29 +01:00
julien 7a4805f749 Add rodauth 2026-11-02 11:11:42 +01:00
julien 747dd5c78f Typo 2026-11-02 10:41:52 +01:00
julien cb90a80d49 Add sqlite database 2026-11-02 10:38:59 +01:00
julien d1210a3ea0 Add temporary page for pages that does not yet exist 2026-11-02 10:18:37 +01:00
julien ba49d0aad9 Add temporary page for pages that does not yet exists 2026-11-02 10:15:31 +01:00
julien 145dd95b24 Add markdown to html 2026-11-01 14:46:05 +01:00
julien 5fee20ef9e Add temporary notice about NETig's registration status 2026-11-01 13:37:45 +01:00
julien 5eddb8890e Improve statutes document 2026-10-30 18:22:09 +01:00
julien 2924a55e43 Improve statutes document 2026-10-30 15:55:32 +01:00
julien 335d385fba Todo 2026-10-30 14:17:20 +01:00
julien 4e7cfaddd7 Improve statutes document 2026-10-29 23:50:25 +01:00
julien 24b6a417cb Improve statutes document 2026-10-29 21:55:14 +01:00
julien acf30d1ebe Improve statutes document 2026-10-29 20:22:36 +01:00
julien 7dfdbcfe38 Improve statutes document 2026-10-29 18:50:14 +01:00
julien 76fd6cce90 Improve statutes document 2026-10-29 18:41:36 +01:00
julien 92e6e5fb19 Improve statutes document 2026-10-29 18:34:31 +01:00
julien 5ed382e1ac Improve statutes document 2026-10-29 18:18:27 +01:00
julien 030f13d609 Improve statutes document 2026-10-29 17:35:08 +01:00
julien 9c9e4a6ba0 Improve statutes document 2026-10-29 17:34:53 +01:00
julien f5d340a2b5 Improve statutes document 2026-10-29 15:43:59 +01:00
julien 974f4f3679 Improve statutes document 2026-10-29 10:51:23 +01:00
julien 50b798e1cc Improve statutes document 2026-10-28 21:13:36 +01:00
julien 231446e7ed Improve statutes document 2026-10-28 20:47:10 +01:00
julien 28932abc94 Improve statutes document 2026-10-28 19:31:43 +01:00
julien 4b01d710e4 Improve statutes document 2026-10-27 20:55:15 +01:00
julien 22d5ee8ebd Improve statutes document 2026-10-27 20:45:52 +01:00
julien c1147a0cd9 Improve statutes document 2026-10-27 20:43:03 +01:00
julien 7de846fcd2 Improve statutes document 2026-10-27 20:41:56 +01:00
julien bbe1f09ec8 Improve statutes document 2026-10-27 20:37:40 +01:00
julien e7f433c39a Improve statutes document 2026-10-27 20:25:13 +01:00
julien a298c90914 Improve statutes document 2026-10-27 20:19:02 +01:00
julien 9de79f1e15 Improve statutes document 2026-10-27 19:55:38 +01:00
julien 417632673e Improve statutes document 2026-10-27 19:48:27 +01:00
julien c17db449bd Remove internal regulations document 2026-10-27 19:00:31 +01:00
julien ab2d8845a2 Improve statutes document and add internal regulations document 2026-10-27 16:33:59 +01:00
julien b2cddd727a Add draft legal document 2026-10-27 09:52:31 +01:00
julien 3a78cf9889 Update README to use Puma 2026-10-27 09:24:55 +01:00
julien 38962c93b4 Rename statutes document 2026-10-27 09:09:53 +01:00
julien ad4226f884 Add statutes articles 1–7 2026-10-27 09:09:30 +01:00
julien 5642a4692f Add association statutes 2026-10-27 09:08:47 +01:00
julien a79de1a398 Modernize Ruby hash syntax 2026-10-27 09:08:33 +01:00
julien 455e90fb27 Enable global CSRF protection 2026-10-27 09:08:05 +01:00
37 changed files with 2402 additions and 232 deletions

No files matched your search

+1
View File
@@ -1,2 +1,3 @@
.ruby-lsp/ .ruby-lsp/
.env .env
/db/*.sqlite3*
+15 -1
View File
@@ -2,14 +2,28 @@
source "https://rubygems.org" source "https://rubygems.org"
# Development
group :development do group :development do
gem "irb" gem "irb"
gem "dotenv" gem "dotenv"
end end
# Base
gem "roda" gem "roda"
gem "rackup"
gem "puma" gem "puma"
gem "tilt" gem "tilt"
gem "erubi" gem "erubi"
# Database
gem "sequel"
gem "sqlite3"
# Mail
gem "mail" gem "mail"
# Markdown
gem "commonmarker"
# Authentication
gem "rodauth"
gem "bcrypt"
+20 -5
View File
@@ -1,6 +1,9 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
bcrypt (3.1.22)
bigdecimal (4.1.3)
commonmarker (2.10.1-x86_64-linux)
date (3.6.0) date (3.6.0)
dotenv (3.2.0) dotenv (3.2.0)
erb (6.0.7) erb (6.0.7)
@@ -36,8 +39,6 @@ GEM
puma (8.0.2) puma (8.0.2)
nio4r (~> 2.0) nio4r (~> 2.0)
rack (3.2.7) rack (3.2.7)
rackup (2.3.1)
rack (>= 3)
rbs (4.2.0) rbs (4.2.0)
logger logger
prism (>= 1.6.0) prism (>= 1.6.0)
@@ -51,25 +52,37 @@ GEM
io-console (~> 0.5) io-console (~> 0.5)
roda (3.109.0) roda (3.109.0)
rack rack
rodauth (2.49.0)
roda (>= 2.6.0)
sequel (>= 4)
sequel (5.110.0)
bigdecimal
sqlite3 (2.10.2-x86_64-linux-gnu)
tilt (2.9.0) tilt (2.9.0)
timeout (0.6.1) timeout (0.6.1)
tsort (0.2.0) tsort (0.2.0)
PLATFORMS PLATFORMS
ruby
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
bcrypt
commonmarker
dotenv dotenv
erubi erubi
irb irb
mail mail
puma puma
rackup
roda roda
rodauth
sequel
sqlite3
tilt tilt
CHECKSUMS CHECKSUMS
bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032
bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66
commonmarker (2.10.1-x86_64-linux) sha256=4b2c4b9084baca131146c92dde02bcb40ef3d2768a6772648c1970bb51842734
date (3.6.0) sha256=18ac945c4e24ebd93534f8eab8c2fd8c5604b3e977105b3798d62a1e8d27b580 date (3.6.0) sha256=18ac945c4e24ebd93534f8eab8c2fd8c5604b3e977105b3798d62a1e8d27b580
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92 erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
@@ -89,11 +102,13 @@ CHECKSUMS
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7 rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674 rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674
rdoc (8.1.0) sha256=befc37880a5cfd36dcb176c748b5fd5ee39dd3536a75e81824e4972ce230f60a rdoc (8.1.0) sha256=befc37880a5cfd36dcb176c748b5fd5ee39dd3536a75e81824e4972ce230f60a
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
roda (3.109.0) sha256=470f60355043a7732edb08ece2d12eb2e6912866ac87106c92ded5f6f8a5a8dc roda (3.109.0) sha256=470f60355043a7732edb08ece2d12eb2e6912866ac87106c92ded5f6f8a5a8dc
rodauth (2.49.0) sha256=c4450072ca7b01ba59e51f0c41fbbd0884a2356ccccf141519b45857d646a255
sequel (5.110.0) sha256=c316335b352adb4109bb02ade237b1a6dbd493493def34542ce05893f62138ae
sqlite3 (2.10.2-x86_64-linux-gnu) sha256=ca92731ab464df9cae503fa1a439a549e2724f6953c8c05a7769b59366e38f15
tilt (2.9.0) sha256=da5735d0280bba96e9a91041bb14aee435ccad5c17b0fa519249ae543d9aa3a5 tilt (2.9.0) sha256=da5735d0280bba96e9a91041bb14aee435ccad5c17b0fa519249ae543d9aa3a5
timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
+215 -10
View File
@@ -1,38 +1,243 @@
# netig.net # netig.net
## TODO ## À faire
* limiter les soumissions à /contact/send - traduire le message Rodauth « Please login to continue » ;
- limiter le spam sur `/contact/send` ;
- afficher les recettes et les dépenses sur la page d'accueil ;
- gérer les accès aux services selon la situation des membres ;
- traiter les demandes de démission et de suppression des comptes.
## Développement ## Développement
### Dépendances
Installer les dépendances : Installer les dépendances :
```sh ```sh
bundle install bundle install
``` ```
### Variables d'environnement
Créer un fichier `.env` à la racine du projet : Créer un fichier `.env` à la racine du projet :
```dotenv ```dotenv
RACK_ENV=development
SESSION_SECRET=... SESSION_SECRET=...
SMTP_PASSWORD=... RODAUTH_HMAC_SECRET=...
RODAUTH_DOMAIN=netig.net
BASE_URL=http://[::1]:9292
SMTP_PASSWORD='...'
``` ```
Générer `SESSION_SECRET` avec `openssl rand -hex 64` et conserver la valeur dans `.env`. Générer `SESSION_SECRET` et `RODAUTH_HMAC_SECRET` avec :
Démarrer le serveur de développement :
```sh ```sh
bundle exec rackup openssl rand -hex 64
``` ```
Ou ouvrir un shell interactif : Utiliser une valeur différente pour chaque secret. Les valeurs contenant
`#`, des espaces ou des caractères interprétés spécialement peuvent être
entourées d'apostrophes simples.
Le fichier `.env` est chargé automatiquement en développement par
`config/boot.rb`. Ce fichier est chargé par `netig.rb` et directement
par les scripts de `bin/` qui ont besoin de la configuration.
Ne pas versionner le fichier `.env`.
### Base de données
En développement, la base est créée par défaut dans :
```text
db/development.sqlite3
```
Ce chemin peut être remplacé par la variable d'environnement
`DATABASE_PATH`. Un chemin relatif est résolu à partir du répertoire
de `lib/database.rb`, et non du répertoire courant.
Créer ou mettre à jour la base de données :
```sh ```sh
bundle exec irb -r dotenv/load -r ./netig.rb bundle exec ruby bin/migrate
``` ```
### Serveur
Démarrer le serveur de développement depuis la racine du projet :
```sh
bundle exec puma
```
Puma charge automatiquement `config.ru`, qui démarre l'application.
Ouvrir la page de création de compte :
```text
http://[::1]:9292/create-account
```
Les personnes physiques et les personnes morales peuvent créer un compte.
La création du compte crée également une fiche dans `members`. L'adresse
électronique doit être vérifiée avant de pouvoir se connecter.
### Espaces utilisateurs
Après avoir vérifié son adresse électronique, chaque personne connectée
accède à son espace personnel :
```text
http://[::1]:9292/account
```
Cet espace permet notamment de consulter sa situation d'adhésion et
de formuler une demande d'adhésion à l'association.
Les membres du Noyau disposent également d'un accès à la gestion de
l'association :
```text
http://[::1]:9292/noyau
```
Cet espace permet notamment de consulter les demandes d'adhésion,
de les accepter ou de les refuser, et de consulter les comptes
de l'association.
Les demandes en attente sont accessibles à l'adresse :
```text
http://[::1]:9292/noyau/memberships
```
La consultation et la gestion de l'association sont réservées
aux membres du Noyau.
### Initialisation du Noyau
Pour ajouter une personne au Noyau, utiliser le script :
```sh
bundle exec ruby bin/add-core-member adresse@example.net
```
Le compte doit exister, son adresse électronique doit être vérifiée
et la fiche membre doit correspondre à une personne physique.
Une adhésion au statut `accepted` doit également déjà exister
dans `memberships`.
Le script ne crée ni le compte ni l'adhésion. Il attribue uniquement
la qualité de membre du Noyau à une fiche membre qui remplit ces
conditions. Il doit être exécuté séparément pour chaque personne.
Les demandes d'adhésion sont formulées depuis l'espace personnel
(`/account`) et examinées par le Noyau depuis l'espace de gestion
de l'association (`/noyau/memberships`).
Pour constituer le premier Noyau, les adhésions des personnes
désignées doivent être enregistrées comme acceptées avant
l'utilisation du script.
Les statuts prévoient un Noyau composé d'au moins trois personnes
physiques.
### Shell interactif
Ouvrir un shell interactif avec l'application chargée :
```sh
bundle exec irb -r ./netig.rb
```
`netig.rb` charge également `config/boot.rb` : les variables du fichier
`.env` sont donc disponibles en développement.
## Production ## Production
Docker Compose fournit les variables avec `env_file` et définit `RACK_ENV=production`. En production, les variables d'environnement sont fournies au conteneur
par Docker Compose. Le service doit définir `RACK_ENV=production` et
transmettre le fichier d'environnement avec `env_file`.
Exemple de configuration :
```yaml
services:
netig:
env_file:
- .env
environment:
RACK_ENV: production
```
La variable définie dans `environment` prend le dessus sur une éventuelle
valeur de `RACK_ENV` présente dans `.env`.
Le fichier `.env` de production doit notamment contenir :
```dotenv
DATABASE_PATH=../db/netig.sqlite3
SESSION_SECRET=...
RODAUTH_HMAC_SECRET=...
RODAUTH_DOMAIN=netig.net
BASE_URL=https://netig.net
SMTP_PASSWORD='...'
```
Générer `SESSION_SECRET` et `RODAUTH_HMAC_SECRET` avec :
```sh
openssl rand -hex 64
```
Utiliser des secrets propres à la production et ne pas versionner le
fichier contenant ces variables.
### Base de données
La base de production est stockée dans :
```text
db/netig.sqlite3
```
Le chemin `../db/netig.sqlite3` est relatif au répertoire de
`lib/database.rb`. Il désigne donc le dossier `db/` à la racine du
projet, dans le conteneur.
Le dossier `db/` doit être conservé entre les déploiements, monté sur
un stockage persistant et accessible en écriture par l'utilisateur
qui exécute Puma. Il doit également être inclus dans les sauvegardes.
Créer ou mettre à jour la base dans le conteneur applicatif, avec les
mêmes variables d'environnement et le même stockage persistant que
l'application, avant de démarrer la nouvelle version :
```sh
docker compose run --rm netig bundle exec ruby bin/migrate
```
Cette commande suppose que le service Compose s'appelle `netig`.
### Initialisation du premier Noyau
Chaque personne désignée pour siéger au Noyau doit d'abord créer un
compte depuis le site et vérifier son adresse électronique. Sa fiche
membre doit également disposer d'une adhésion au statut `accepted`.
Pour chaque personne, exécuter dans le conteneur applicatif :
```sh
docker compose run --rm netig \
bundle exec ruby bin/add-core-member adresse@example.net
```
Cette commande suppose que le service Compose s'appelle `netig`.
Elle ne crée pas l'adhésion : elle vérifie qu'elle existe et qu'elle
est acceptée, puis attribue la qualité de membre du Noyau.
Les statuts prévoient un Noyau composé d'au moins trois personnes
physiques.
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require_relative "../lib/database"
abort "Usage: ruby bin/add-core-member EMAIL" unless ARGV.one?
email = ARGV.first.strip.downcase
abort "Email manquant." if email.empty?
begin
DB.transaction do
verified_status_id = DB[:account_statuses]
.where(name: "Verified")
.get(:id)
raise "Statut Verified introuvable." unless verified_status_id
account = DB[:accounts]
.where(Sequel.function(:lower, :email) => email)
.first
raise "Compte introuvable : #{email}" unless account
raise "Email non vérifié : #{email}" unless
account[:status_id] == verified_status_id
member = DB[:members].where(account_id: account[:id]).first
raise "Fiche membre introuvable." unless member
raise "Une personne physique est requise." unless
member[:kind] == "individual"
raise "Ce membre appartient déjà au Noyau." if member[:core_member]
membership = DB[:memberships]
.where(member_id: member[:id], status: "accepted")
.first
raise "Ce membre doit avoir une adhésion acceptée." unless membership
updated = DB[:members]
.where(id: member[:id], core_member: false)
.update(core_member: true)
raise "Impossible d'ajouter ce membre au Noyau." unless updated == 1
end
rescue StandardError => e
abort "Erreur : #{e.message}"
end
puts "#{email} a été ajouté au Noyau."
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require_relative "../lib/database"
Sequel.extension :migration
migrations_path = File.expand_path("../db/migrations", __dir__)
Sequel::Migrator.run(DB, migrations_path)
puts "Database schema is up to date."
-1
View File
@@ -1,4 +1,3 @@
require "dotenv/load" if ENV.fetch("RACK_ENV", "development") == "development"
require_relative "netig" require_relative "netig"
run Netig.freeze.app run Netig.freeze.app
+4
View File
@@ -0,0 +1,4 @@
if ENV.fetch("RACK_ENV", "development") == "development"
require "dotenv"
Dotenv.load(File.expand_path("../.env", __dir__))
end
+184
View File
@@ -0,0 +1,184 @@
class Netig
plugin :rodauth do
db DB
enable :login, :logout, :create_account, :verify_account
domain ENV.fetch("RODAUTH_DOMAIN")
base_url ENV.fetch("BASE_URL")
hmac_secret ENV.fetch("RODAUTH_HMAC_SECRET")
account_unverified_status_value 1
account_open_status_value 2
normalize_login { |login| login.to_s.strip.downcase }
email_from "NETig <noreply@netig.net>"
title_instance_variable :@page_title
# Connexion
login_page_title "Connexion — NETig"
login_label "Email"
password_label "Mot de passe"
login_button "Se connecter"
login_notice_flash nil
login_error_flash "La connexion a échoué."
attempt_to_login_to_unverified_account_error_flash(
"Votre adresse électronique n'a pas encore été vérifiée."
)
login_redirect "/account"
login_return_to_requested_location? true
# Déconnexion
logout_notice_flash "Vous êtes maintenant déconnecté."
# Création de compte
create_account_page_title "Créer un compte — NETig"
create_account_button "Créer mon compte"
create_account_link_text "Créer un compte"
create_account_redirect "/create-account"
create_account_error_flash(
"Impossible de créer votre compte. Vérifiez les informations saisies."
)
before_create_account do
first_name = param("first_name").to_s.strip
last_name = param("last_name").to_s.strip
organization_name = param("organization_name").to_s.strip
individual =
!first_name.empty? &&
!last_name.empty? &&
organization_name.empty?
organization =
first_name.empty? &&
last_name.empty? &&
!organization_name.empty?
unless individual || organization
throw_error_status(
422,
"first_name",
"Renseignez soit le prénom et le nom, soit la dénomination."
)
end
end
after_create_account do
first_name = param("first_name").to_s.strip
last_name = param("last_name").to_s.strip
organization_name = param("organization_name").to_s.strip
member = if organization_name.empty?
{
kind: "individual",
first_name: first_name,
last_name: last_name,
organization_name: nil
}
else
{
kind: "organization",
first_name: nil,
last_name: nil,
organization_name: organization_name
}
end
db[:members].insert(
member.merge(
account_id: account[:id],
core_member: false
)
)
end
# Vérification de l'adresse électronique
verify_account_page_title "Vérifier votre email — NETig"
verify_account_button "Confirmer mon email"
verify_account_set_password? true
verify_account_redirect "/account"
verify_account_email_subject(
"Confirmez votre email — NETig"
)
verify_account_email_body do
<<~EMAIL
Bonjour,
Pour vérifier votre email et activer votre compte
NETig, cliquez sur le lien suivant :
#{verify_account_email_link}
Si vous n'êtes pas à l'origine de cette demande, ignorez ce message.
Le Noyau de NETig
EMAIL
end
verify_account_email_sent_notice_flash(
"Un email de vérification vient de vous être envoyé."
)
verify_account_notice_flash(
"Votre email est maintenant vérifié."
)
verify_account_error_flash(
"Impossible de vérifier votre adresse électronique."
)
no_matching_verify_account_key_error_flash(
"Le lien de vérification est invalide ou a expiré."
)
attempt_to_create_unverified_account_error_flash(
"Un compte utilisant cette adresse attend déjà sa vérification."
)
# Renvoi du courriel de vérification
resend_verify_account_page_title(
"Renvoyer l'email de vérification — NETig"
)
verify_account_resend_explanatory_text(
"Si vous n'avez pas reçu l'email de vérification, indiquez " \
"l'email associé à votre compte pour demander " \
"un nouvel envoi."
)
verify_account_resend_button "Renvoyer l'email"
verify_account_resend_link_text(
"Renvoyer l'email de vérification"
)
verify_account_resend_error_flash(
"Impossible de renvoyer l'email de vérification. " \
"Vérifiez l'email saisi ou réessayez plus tard."
)
verify_account_email_recently_sent_error_flash(
"Un email de vérification vient de vous être envoyé. " \
"Veuillez patienter avant d'en demander un autre."
)
verify_account_email_sent_redirect "/verify-account-resend"
verify_account_email_recently_sent_redirect "/verify-account-resend"
# Sessions
already_logged_in do
request.redirect("/account")
end
end
end
@@ -0,0 +1,70 @@
Sequel.migration do
up do
create_table(:account_statuses) do
Integer :id, primary_key: true
String :name, null: false, unique: true
end
from(:account_statuses).import(
[:id, :name],
[
[1, "Unverified"],
[2, "Verified"],
[3, "Closed"]
]
)
db = self
create_table(:accounts) do
primary_key :id, type: :Bignum
foreign_key :status_id, :account_statuses,
null: false,
default: 1
String :email, null: false
if db.supports_partial_indexes?
index :email, unique: true, where: { status_id: [1, 2] }
else
index :email, unique: true
end
end
create_table(:account_password_hashes) do
foreign_key :id, :accounts,
primary_key: true,
type: :Bignum,
on_delete: :cascade
String :password_hash, null: false
end
create_table(:account_verification_keys) do
foreign_key :id, :accounts,
primary_key: true,
type: :Bignum,
on_delete: :cascade
String :key, null: false
DateTime :requested_at,
null: false,
default: Sequel::CURRENT_TIMESTAMP
DateTime :email_last_sent,
null: false,
default: Sequel::CURRENT_TIMESTAMP
end
end
down do
drop_table(
:account_verification_keys,
:account_password_hashes,
:accounts,
:account_statuses
)
end
end
+41
View File
@@ -0,0 +1,41 @@
Sequel.migration do
up do
create_table(:members) do
primary_key :id, type: :Bignum
foreign_key :account_id, :accounts,
type: :Bignum,
null: false,
on_delete: :cascade
String :kind, null: false
String :first_name
String :last_name
String :organization_name
TrueClass :core_member, null: false, default: false
index :account_id, unique: true
constraint :members_kind_valid,
Sequel.lit("kind IN ('individual', 'organization')")
constraint :members_identity_matches_kind,
Sequel.lit(
"(kind = 'individual' AND first_name IS NOT NULL " \
"AND trim(first_name) <> '' AND last_name IS NOT NULL " \
"AND trim(last_name) <> '' AND organization_name IS NULL) " \
"OR (kind = 'organization' AND first_name IS NULL " \
"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
down do
drop_table(:members)
end
end
+50
View File
@@ -0,0 +1,50 @@
Sequel.migration do
up do
create_table(:memberships) do
primary_key :id, type: :Bignum
foreign_key :member_id, :members,
type: :Bignum,
null: false,
on_delete: :cascade
String :status, null: false, default: "pending"
DateTime :requested_at,
null: false,
default: Sequel::CURRENT_TIMESTAMP
DateTime :decided_at
foreign_key :recorded_by, :members,
type: :Bignum,
null: true,
on_delete: :set_null
index :member_id, name: :memberships_member_id_all_index
index :recorded_by
index :status
# Un seul dossier en attente ou une seule adhésion acceptée.
index :member_id,
unique: true,
name: :memberships_active_member_id_index,
where: { status: ["pending", "accepted"] }
constraint :memberships_status_valid,
Sequel.lit("status IN ('pending', 'accepted', 'rejected')")
constraint :memberships_decision_consistent,
Sequel.lit(
"(status = 'pending' AND decided_at IS NULL " \
"AND recorded_by IS NULL) " \
"OR (status IN ('accepted', 'rejected') " \
"AND decided_at IS NOT NULL)"
)
end
end
down do
drop_table(:memberships)
end
end
+197
View File
@@ -0,0 +1,197 @@
# 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
déclaration et sa publication au Journal officiel des associations et
fondations d’entreprises (JOAFE).
## Mentions légales
**Éditeur du site :** [nom, prénom et coordonnées de la personne physique
qui édite actuellement le site]
**Directeur de la publication :** [nom et prénom]
**Courriel :** [contact@netig.net](mailto:contact@netig.net)
**Hébergement :** Hetzner Online GmbH, Industriestr. 25, 91710 Gunzenhausen,
Allemagne — [www.hetzner.com](https://www.hetzner.com/) — téléphone :
+49 (0)9831 505-0.
Les infrastructures sont administrées bénévolement. Des sauvegardes
quotidiennes sont conservées sur une Storage Box de Hetzner située en
Finlande.
## Conditions d’utilisation
### Accès et gratuité
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
de 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.
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 :
[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
Chaque utilisateur doit respecter la loi, la vie privée d’autrui et la
sécurité des services. Les services ne doivent pas être utilisés pour
diffuser des contenus illicites, envoyer des communications abusives ou
perturber le fonctionnement des infrastructures.
Chaque utilisateur reste responsable des contenus qu’il publie, envoie
ou stocke. Les services ne doivent pas être considérés comme l’unique moyen
de conserver des données importantes ; il est recommandé de garder ses
propres copies lorsque cela est possible.
### Suspension des accès
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 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.
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.
Ces dispositions ne remplacent pas la procédure d’exclusion prévue par
les statuts.
### Désactivation et récupération des données
Pour toute demande concernant un compte ou un service, notamment sa
désactivation, l’export ou la suppression des données associées, écrivez à
[contact@netig.net](mailto:contact@netig.net). L’export est fourni lorsqu’il
est techniquement possible. La suppression reste soumise aux obligations
légales de conservation et aux délais nécessaires à l’effacement des
sauvegardes.
## Données personnelles
### Responsable du traitement et périmètre
**Responsable du traitement :** [à compléter : identité et coordonnées de
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 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
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
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 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.
### Base juridique
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 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 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
quotidiennes sont conservées sur une Storage Box située en Finlande.
### Conservation
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 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].
### Vos droits
Dans les conditions prévues par la réglementation, vous pouvez demander
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/).
### Cookies
Le site utilise des cookies techniques nécessaires au fonctionnement
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
Pour toute question concernant le site, les services, les présentes
conditions ou les données personnelles, écrivez à
[contact@netig.net](mailto:contact@netig.net).
+294 -50
View File
@@ -1,99 +1,343 @@
# Statuts de l’association NETig # Statuts de l’association NETig
## Article 1 — Dénomination ## Article 1 — Constitution
Il est fondé entre les adhérents aux présents statuts une association régie Il est fondé entre les adhérents aux présents statuts une association régie
par la loi du 1er juillet 1901 et le décret du 16 août 1901, dénommée par la loi du 1er juillet 1901 et le décret du 16 août 1901, dénommée **NETig**.
NETig. Sa durée est illimitée et son siège social est fixé à Brasparts (Finistère).
## Article 2 — Objet L’association répond de ses dettes sur son propre patrimoine. Être membre
ou faire partie du Noyau (article 5) n’entraîne pas, à lui seul, l’obligation
de les payer sur ses fonds personnels. La responsabilité personnelle d’un
membre ou dirigeant peut toutefois être engagée dans les conditions prévues
par la loi, notamment s’il agit en son nom propre, hors de ses attributions
ou commet une faute détachable de ses fonctions.
L’association a pour objet de proposer à toutes et tous une alternative aux ## Article 2 — Objet et principes
services des géants du Web. Elle promeut et défend une approche fondée sur
les logiciels libres, le respect de la vie privée et la préservation des
libertés fondamentales.
## Article 3 — Principes NETig œuvre pour la décentralisation du Web et la mise en commun de
ressources et de connaissances, afin de proposer des services numériques
transparents et respectueux des utilisateurs.
L’association structure son action autour de cinq principes fondamentaux : Ses principes sont :
- Le respect de la vie privée et la préservation des libertés fondamentales. - le respect de la vie privée, la protection des données personnelles et
- L’utilisation exclusive de logiciels libres. des libertés fondamentales ;
- La gratuité des services pour toutes et tous, financés par des dons - l’utilisation exclusive de logiciels libres ;
volontaires. - le partage équitable des ressources disponibles ;
- Le partage équitable des ressources disponibles. - le partage ouvert des savoirs et des compétences informatiques.
- L’ouverture et le partage des connaissances techniques.
## Article 4 — Siège social L'association est sans but lucratif. Ses services sont proposés sans
facturation liée à leur utilisation. Les dons volontaires contribuent au
financement des infrastructures et à la pérennité des services. L’association
invite les membres et les utilisateurs qui le peuvent à la soutenir
financièrement, selon leurs moyens. Cette participation est importante pour
assurer la continuité des services. Les dons sont libres, sans contrepartie
et ne conditionnent ni l’adhésion, ni les droits des membres, ni l’accès aux
services. Aucun bénéfice n’est distribué aux membres.
Le siège social est fixé à Brasparts. Il peut être transféré librement ## Article 3 — Adhésion
par décision du Noyau (article 11).
## Article 5 — Durée Peut adhérer toute personne physique ou morale acceptant les statuts et
les principes de l’association. Le Noyau examine les demandes et décide
de l’admission.
La durée de l’association est illimitée. L’adhésion prend effet à la date de son acceptation et est valable sans
limitation de durée. 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.
## Article 6 — Membres L’adhésion prend fin par démission notifiée, décès, dissolution de la
personne morale ou exclusion (article 7).
L’association est ouverte à toute personne, physique ou morale, qui adhère ## Article 4 — Assemblée générale
aux présents statuts et aux principes de NETig.
Tous les membres disposent des mêmes droits. La participation aux activités ### Convocation et ordre du jour
et à la gestion de l’association est libre et ne constitue pas une condition
de maintien de l’adhésion.
Les personnes morales membres désignent une personne physique pour les L’AG se réunit au moins une fois par an, au cours du premier trimestre.
représenter au sein de l’association. Le Noyau convoque l’AG annuelle.
## Article 7 — Adhésion Toute demande d’AG émanant d’un ou plusieurs membres est adressée au Noyau
et précise les sujets souhaités à l’ordre du jour. Tout membre peut
également lui demander l’inscription d’une question avant l’envoi de la
convocation. Le Noyau motive tout refus de convoquer l’AG ou d’inscrire
une question à l’ordre du jour.
Toute demande d’adhésion implique l’acceptation des présents statuts et Le Noyau dispose de quinze jours après réception d’une demande d’AG pour
des principes de NETig. envoyer la convocation. En l’absence de convocation dans ce délai, en cas
de refus de convoquer l’AG ou d’inscrire à l’ordre du jour une proposition
présentée dans les conditions de l’article 9, les membres représentant
au moins un cinquième des voix délibératives peuvent la convoquer eux-mêmes,
sous réserve des procédures particulières prévues aux articles 5, 7 et 9.
Le Noyau examine les demandes et valide celles qui sont conformes aux présents La convocation indique l’ordre du jour et est envoyée au moins quinze jours
statuts. Tout refus doit être motivé et peut faire l’objet d’un recours avant la réunion. Seules les questions inscrites à l’ordre du jour peuvent
auprès de l’Assemblée générale, qui statue définitivement. être votées.
La pérennité de NETig repose sur la mise en commun des ressources Un dispositif électronique gère les demandes et les convocations sans
nécessaires à son fonctionnement et au développement de ses services. Chaque révéler la liste des membres ni leurs coordonnées. L’AG peut se tenir en
membre est encouragé à contribuer selon ses moyens et ses possibilités, par présentiel, à distance ou en format hybride, si l’identité des participants
ses ressources financières ou matérielles, ses compétences ou son temps. et la sincérité des votes sont garanties.
Aucune contribution financière n’est obligatoire.
## Article 8 — Fin de l’adhésion Si l’ordre du jour prévoit la révocation d’un membre du Noyau ou l’exclusion
d’un membre dont l’exclusion relève de l’AG, celle-ci peut pourvoir à son
remplacement lors de la même réunion. L’article 9 s’applique aux
modifications statutaires et à la dissolution.
### Voix et procurations
Chaque membre, personne physique ou morale, dispose d’une voix délibérative.
La voix d’une personne morale est exercée par son représentant habilité.
Une personne physique également membre à titre personnel qui représente
une ou plusieurs personnes morales membres exerce une voix en son nom et
une voix pour chacune d’elles.
## Article 9 — Ressources Tout membre peut se faire représenter par un membre personne physique.
Une personne morale donne procuration par son représentant habilité.
Une même personne peut détenir au plus deux procurations, en plus des voix
qu’elle exerce en son nom ou comme représentant d’une ou plusieurs personnes
morales.
Chaque voix délibérative compte séparément pour le calcul des seuils et des
quorums prévus par les statuts ; les fractions sont arrondies à l’entier
supérieur.
### Décisions
## Article 10 — Assemblée générale L’AG fixe les orientations. Elle peut demander au Noyau et aux
responsables les informations nécessaires, réexaminer leurs décisions
et modifier ou annuler celles qui contredisent les statuts, les principes
ou ses orientations. Elle désigne les membres du Noyau, peut les révoquer
à tout moment et les remplacer.
Pour tous les votes, les abstentions, votes blancs et votes nuls ne sont
pas comptés parmi les suffrages exprimés. En cas d’égalité, la proposition
est rejetée.
Sauf disposition particulière des statuts, les décisions de l’AG sont
prises à la majorité simple des suffrages exprimés, sans quorum.
## Article 11 — Noyau Lorsqu’un vote porte sur l’exclusion d’une personne physique ou morale,
la révocation d’une personne physique de ses fonctions, ou une mesure
individuelle défavorable à son égard, la personne concernée ou, s’il
s’agit d’une personne morale, son représentant reçoit par écrit les
motifs envisagés au moins quinze jours avant la réunion. Cette personne
peut présenter ses observations, mais ne participe ni à la délibération
ni au vote concernant cette situation, y compris au titre des voix
qu’elle détient par procuration ou exerce pour une personne morale.
Elle ne préside ni ne rédige le compte rendu de cette partie. Son absence
ou son silence n’empêche pas l’AG de statuer.
Au début de chaque AG, les participants désignent une personne pour la
présider et une autre pour rédiger le compte rendu. Celui-ci est conservé
et accessible aux membres, sous réserve de la confidentialité, de la
protection des données et de la sécurité.
## Article 5 — Noyau
## Article 12 — Décisions ### Composition et mandat
Le Noyau administre l’association et applique les orientations de l’AG.
Il constitue l’organe d’administration et de direction de l’association.
Il comprend au moins trois personnes physiques désignées par l’AG
parmi les membres adhérant à titre personnel.
La qualité de représentant d’une personne morale adhérente ne confère
pas, à elle seule, la qualité de membre à titre personnel. Un représentant
qui est également membre à titre personnel peut être désigné au Noyau,
mais y siège en son nom propre et non au titre de cette représentation.
## Article 13 — Représentation Le mandat est personnel. Il prend fin à la clôture de la prochaine AG
annuelle appelée à statuer sur les comptes de l’exercice écoulé, qui peut
le renouveler ou le remplacer. Il prend également fin par démission,
révocation ou perte de la qualité de membre à titre personnel.
### Fonctionnement et responsabilités
Le Noyau fixe librement ses règles de décision, à condition que chaque
membre puisse participer. À défaut de règle particulière, les décisions
sont prises à la majorité des suffrages exprimés, avec la participation
d’au moins la moitié des membres en exercice, arrondie à l’entier
supérieur, et d’au moins deux membres, sauf s’il n’en reste qu’un. En cas
d’égalité, la proposition est rejetée. Ses décisions sont consignées par
écrit et communiquées aux membres, sous réserve de confidentialité, de
protection des données et de sécurité.
## Article 14 — Gestion financière Le Noyau veille à la continuité des services proposés par l’association,
dans la limite de ses moyens humains, techniques et financiers. Il organise
leur suivi et répartit les responsabilités entre ses membres ou les
confie, avec leur accord, à d’autres membres. Il en fixe le périmètre,
l’autonomie et les modalités de compte rendu ; il peut les créer,
modifier, regrouper ou retirer et en informe les membres.
Les responsables agissent de façon autonome dans leur périmètre,
respectent les statuts, principes, orientations de l’AG et décisions
du Noyau relevant de leur domaine, et lui signalent leurs décisions
importantes.
Le Noyau désigne parmi ses membres un ou plusieurs représentants légaux
et définit par écrit leurs pouvoirs, exercés seuls ou conjointement,
notamment pour représenter l'association en justice et dans tous les actes
de la vie civile. Il accomplit dans les délais légaux les formalités
déclaratives requises, notamment en cas de changement d’administration,
de siège, de statuts ou de dissolution.
## Article 15 — Modification des statuts Chaque année, il rend compte à l’AG de son activité, notamment des
adhésions, exclusions et suspensions d’accès.
### Vacance et cooptation
Si le Noyau compte moins de trois membres, les restants peuvent coopter
assez de volontaires remplissant les conditions de composition pour
rétablir l’effectif. La cooptation suit les règles de décision du Noyau ;
s’il ne reste qu’un membre, il peut décider seul. Jusqu’à leur
ratification par la prochaine AG, les personnes cooptées se limitent à
la gestion courante et aux mesures urgentes nécessaires à la continuité
de l’association. En cas de refus de ratification par l’AG, le mandat
de la personne cooptée cesse immédiatement.
## Article 16 — Règlement intérieur Si aucun membre ne reste ou si l’effectif minimal n’est pas rétabli dans
les quinze jours suivant le passage sous trois membres, les membres
représentant au moins un cinquième des voix délibératives peuvent
convoquer directement, selon l’article 4 et sans demande préalable au
Noyau, une AG extraordinaire chargée de désigner ses membres.
### Urgences
Cette procédure concerne les interventions exceptionnelles qui dépassent
le périmètre ou les pouvoirs définis pour les responsabilités confiées.
## Article 17 — Dissolution En cas de risque grave et immédiat pour les personnes, services, données,
infrastructures ou ressources, le responsable du domaine concerné peut
prendre les mesures temporaires nécessaires, même au-delà de son
périmètre ou de ses pouvoirs habituels. S'il n'est pas en mesure
d'intervenir, un membre du Noyau prend le relais. Si aucun membre du
Noyau n’est en mesure de le faire, toute personne disposant déjà de
l’accès concerné peut prendre les mesures strictement nécessaires.
L’intervenant informe dès que possible le Noyau ou les autres responsables
des motifs et mesures prises. Le Noyau réexamine la situation et peut
maintenir, modifier ou arrêter les mesures. Si un membre du Noyau est
intervenu seul, ou si le Noyau ne peut pas réexaminer les mesures,
l’intervenant en rend compte à la prochaine AG, qui peut également les
maintenir, les modifier ou les arrêter. Celles-ci ne peuvent déroger aux
principes de l’article 2 ni modifier les statuts.
## Article 6 — Ressources et comptabilité
Les ressources comprennent les dons volontaires, les subventions, les
recettes d’activités compatibles avec l’objet et les principes de
l’association et toute autre ressource autorisée par la loi.
Les fonctions sont bénévoles. Les frais engagés pour l’association peuvent
être remboursés sur justificatifs, selon les modalités du Noyau. Nul ne
peut approuver ses propres dépenses. Celles d’un membre du Noyau sont
approuvées par les autres membres non concernés ou, si nécessaire, par
une personne désignée par l’AG.
Le Noyau confie la gestion de la trésorerie à une ou plusieurs personnes
et autorise les accès bancaires nécessaires. S’il n’y a plus de personne
chargée de la trésorerie, le Noyau en désigne une sans délai. Seules les
personnes autorisées peuvent consulter les comptes ou effectuer des
paiements, dans les limites prévues par la banque.
L’exercice comptable va du 1er janvier au 31 décembre. Chaque année,
le Noyau présente à l’AG les comptes et un rapport financier. Le rapport
d’activité, les comptes et le rapport financier figurent obligatoirement
à l’ordre du jour de l’AG annuelle et sont communiqués avec la convocation.
L’AG se prononce sur leur approbation. Si les comptes ne sont pas
approuvés, le Noyau répond aux observations et les soumet à une AG
ultérieure.
## Article 7 — Exclusion et suspension des accès
### Exclusion
Le Noyau peut exclure un membre pour manquement grave aux statuts ou
aux principes de l’association, ou pour comportement portant gravement
atteinte à l’association, à ses membres ou à ses services. L’exclusion
d’un membre siégeant au Noyau relève exclusivement de l’AG.
Avant de statuer, l’instance appelée à décider communique par écrit à
la personne concernée les motifs envisagés. La personne concernée dispose
de quinze jours pour répondre et peut demander à être entendue.
Toute décision d’exclusion est motivée, notifiée et prend effet à sa
notification. Elle met fin aux responsabilités, délégations et accès
réservés aux membres ; les accès indépendants de l’adhésion restent régis
par les règles des services.
Une personne exclue par le Noyau peut demander le réexamen de la décision
selon les modalités du présent article. L’exclusion reste en vigueur
jusqu’à la décision de l’AG. L’AG peut confirmer ou annuler l’exclusion ;
si elle l’annule, la qualité de membre est rétablie. Le rétablissement
de la qualité de membre ne rétablit pas automatiquement les
responsabilités, délégations ou accès retirés.
### Suspension des accès
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 en
informe la personne concernée ainsi que le Noyau, et indique les motifs.
La personne concernée peut demander au Noyau de réexaminer la mesure.
Celui-ci répond sous quinze jours ; sans réponse, la mesure est réputée
confirmée. Si le Noyau l’a prise ou confirmée, la personne concernée ou
tout membre peut demander son réexamen par l’AG. Celle-ci peut maintenir,
modifier ou supprimer la mesure. La suspension ne met pas fin à la
qualité de membre.
### Réexamen par l’AG
Toute demande de réexamen par l’AG prévue au présent article est adressée
au Noyau et inscrite à l’ordre du jour de la prochaine AG dont la
convocation n’a pas encore été envoyée. Le réexamen reste possible même
si la personne concernée n’est pas membre.
## Article 8 — Règlement intérieur
Un règlement intérieur, facultatif, peut être proposé par le Noyau ou
tout membre. S’il existe, l’AG l’approuve et peut le modifier, y compris
lors d’une AG tenue à distance si l’identification des participants et
la sincérité du vote sont garanties. Il précise le fonctionnement
pratique sans déroger aux statuts.
## Article 9 — Modification des statuts et dissolution
La modification des statuts et la dissolution sont décidées par une AG
extraordinaire, sur proposition du Noyau ou d’au moins un cinquième des
voix délibératives, arrondi à l’entier supérieur. Une proposition des
membres est adressée au Noyau, accompagnée du projet exact, et vaut
demande d’AG au sens de l’article 4.
La question et le projet exact figurent à l’ordre du jour et sont
communiqués à tous les membres au moins trente jours avant la première AG.
Le projet donne le texte des changements statutaires ou, en cas de
dissolution, l’affectation envisagée de l’actif net. Toute modification
substantielle impose une nouvelle AG et fait repartir le délai de
trente jours.
La première AG ne délibère que si deux tiers au moins des voix
délibératives sont présentes ou représentées. À défaut de quorum, une
seconde AG est convoquée par le Noyau ou, si la première AG a été
convoquée par des membres, par ces mêmes membres. Sa convocation est
envoyée dans les quinze jours suivant le constat de l’absence de quorum.
Elle se tient au moins quinze jours après l’envoi de sa convocation.
Elle reprend le même ordre du jour et le même projet, et ne délibère
valablement que si un tiers au moins des voix délibératives est présent
ou représenté.
Si le quorum de la seconde AG n’est pas atteint, le projet n’est pas
adopté. Il peut être soumis à une nouvelle procédure dans les conditions
du présent article.
La modification ou la dissolution est adoptée à la majorité des deux
tiers des suffrages exprimés. Toutefois, toute modification statutaire
ayant pour effet d’augmenter les engagements des membres requiert
l’accord unanime de tous les membres. Cet accord peut être recueilli
par écrit en dehors de l’AG.
En cas de dissolution, l’AG désigne un ou plusieurs liquidateurs et
attribue l’actif net, après règlement des dettes, à un organisme à but
non lucratif poursuivant un objet compatible avec celui de NETig.
Aucun actif n’est distribué aux membres.
+30
View File
@@ -0,0 +1,30 @@
class Netig
def core_member?
return false unless rodauth.logged_in?
return @core_member unless @core_member.nil?
@core_member = DB[:members]
.join(:memberships, member_id: :id)
.where(
Sequel[:members][:account_id] => rodauth.session_value,
Sequel[:members][:kind] => "individual",
Sequel[:members][:core_member] => true,
Sequel[:memberships][:status] => "accepted"
)
.count
.positive?
end
private
def require_core_member!(r)
rodauth.require_account
return if core_member?
r.halt [
403,
{ "Content-Type" => "text/plain; charset=utf-8" },
["Accès interdit."]
]
end
end
+4 -1
View File
@@ -6,6 +6,8 @@ class ContactForm
"message" => 5000 "message" => 5000
}.freeze }.freeze
EMAIL_PATTERN = /\A[A-Z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[A-Z0-9!#$%&'*+\/=?^_`{|}~-]+)*@[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?(?:\.[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?)+\z/i
def initialize(params) def initialize(params)
@params = params @params = params
end end
@@ -26,7 +28,8 @@ class ContactForm
return false unless fields_valid return false unless fields_valid
email_valid = @params["email"].strip.match?(/\A[^\s@]+@[^\s@]+\.[^\s@]+\z/) email_valid = @params["email"].strip.match?(EMAIL_PATTERN)
headers_valid = %w[name subject].none? do |key| headers_valid = %w[name subject].none? do |key|
@params[key].match?(/[\r\n]/) @params[key].match?(/[\r\n]/)
end end
+3 -3
View File
@@ -6,9 +6,9 @@ class ContactMailer
end end
def build def build
email = @params["email"] email = @params["email"].strip
name = @params["name"] name = @params["name"].strip
subject = @params["subject"] subject = @params["subject"].strip
message = @params["message"] message = @params["message"]
Mail.new do Mail.new do
+15
View File
@@ -0,0 +1,15 @@
require "fileutils"
require "sequel"
database_path = File.expand_path(
ENV.fetch("DATABASE_PATH", "../db/development.sqlite3"),
__dir__
)
FileUtils.mkdir_p(File.dirname(database_path))
DB = Sequel.sqlite(
database_path,
timeout: 5_000,
connect_sqls: ["PRAGMA foreign_keys = ON"]
)
+24
View File
@@ -0,0 +1,24 @@
require "commonmarker"
class DocumentRenderer
DOCUMENTS = {
legal: "legal.md",
statutes: "statutes.md"
}.freeze
def self.render(name)
filename = DOCUMENTS.fetch(name)
path = File.expand_path("../doc/#{filename}", __dir__)
markdown = File.read(path, encoding: "UTF-8")
Commonmarker.to_html(
markdown,
options: {
render: {
hardbreaks: false,
unsafe: false
}
}
)
end
end
+47 -2
View File
@@ -1,9 +1,18 @@
require "roda" # Initialisation de l'environnement
require_relative "config/boot"
# Dépendances externes
require "roda"
require "rodauth"
# Composants de l'application
require_relative "lib/database"
require_relative "config/mail" require_relative "config/mail"
require_relative "lib/contact_form" require_relative "lib/contact_form"
require_relative "lib/contact_mailer" require_relative "lib/contact_mailer"
require_relative "lib/document_renderer"
# Configuration de Roda
class Netig < Roda class Netig < Roda
plugin :render, escape: true plugin :render, escape: true
plugin :public plugin :public
@@ -12,25 +21,61 @@ class Netig < Roda
plugin :route_csrf, csrf_failure: :empty_403 plugin :route_csrf, csrf_failure: :empty_403
plugin :hash_branches plugin :hash_branches
plugin :flash plugin :flash
end
# Configuration de l'authentification
require_relative "config/rodauth"
# Autorisations et routes dédiées
require_relative "lib/authorization"
require_relative "routes/account"
require_relative "routes/noyau"
require_relative "routes/contact" require_relative "routes/contact"
# Routes des pages publiques
class Netig
route do |r| route do |r|
r.rodauth
r.public r.public
check_csrf! check_csrf!
r.hash_branches r.hash_branches
# Accueil
r.root do r.root do
@canonical_url = "https://netig.net/" @canonical_url = "https://netig.net/"
view "index" view "index"
end end
# Statuts
r.get "statutes" do
@page_title = "Statuts — NETig"
@page_description = "Statuts de l'association NETig."
@canonical_url = "https://netig.net/statutes"
@document_html = DocumentRenderer.render(:statutes)
view "document"
end
# Mentions légales
r.get "legal" do r.get "legal" do
@page_title = "Mentions légales — NETig" @page_title = "Mentions légales — NETig"
@page_description = "Mentions légales de l'association NETig." @page_description = "Mentions légales de l'association NETig."
@canonical_url = "https://netig.net/legal" @canonical_url = "https://netig.net/legal"
view "legal" @document_html = DocumentRenderer.render(:legal)
view "document"
end
# Page temporaire
r.get "temporary_page" do
@page_title = "Page temporaire — NETig"
@page_description = "Cette page n'existe pas encore."
@canonical_url = "https://netig.net/temporary_page"
view "temporary_page"
end end
end end
end end
+392 -83
View File
@@ -58,9 +58,9 @@ html {
body { body {
display: flex; display: flex;
flex-direction: column;
min-height: 100vh; min-height: 100vh;
min-height: 100dvh; min-height: 100dvh;
flex-direction: column;
margin: 0; margin: 0;
background: var(--color-background); background: var(--color-background);
color: var(--color-text); color: var(--color-text);
@@ -70,12 +70,26 @@ body {
main { main {
display: flex; display: flex;
flex: 1;
min-width: 0; min-width: 0;
flex: 1;
flex-direction: column; flex-direction: column;
} }
/* Image sur les sections alternées de l'accueil et les pages intérieures. */ .content {
width: 100%;
max-width: 1200px;
margin-inline: auto;
padding: 20px;
}
.prose {
max-width: 72ch;
line-height: 1.6;
}
/* Sections et pages intérieures
------------------------------------------------------------------------ */
main > section { main > section {
background-color: var(--color-background); background-color: var(--color-background);
} }
@@ -91,19 +105,6 @@ main > section:nth-of-type(odd):not(:only-of-type),
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.content {
width: 100%;
max-width: 1200px;
margin-inline: auto;
padding: 20px;
}
.prose {
max-width: 72ch;
line-height: 1.6;
}
/* Une seule feuille de lecture sur les pages intérieures. */
.page-section { .page-section {
flex: 1; flex: 1;
padding: clamp(24px, 5vw, 72px) 20px; padding: clamp(24px, 5vw, 72px) 20px;
@@ -119,11 +120,6 @@ main > section:nth-of-type(odd):not(:only-of-type),
max-width: 72ch; max-width: 72ch;
} }
.section-services,
.section-hosting {
padding-bottom: 100px;
}
/* Typographie et liens /* Typographie et liens
------------------------------------------------------------------------ */ ------------------------------------------------------------------------ */
@@ -174,13 +170,15 @@ a:hover {
text-decoration: underline; text-decoration: underline;
} }
/* Liens dans les paragraphes et le pied de page. */
main p a:not(.button), main p a:not(.button),
footer a { footer a {
text-decoration: underline; text-decoration: underline;
text-underline-offset: 0.15em; text-underline-offset: 0.15em;
} }
/* Navigation clavier et focus
------------------------------------------------------------------------ */
a:focus-visible, a:focus-visible,
button:focus-visible, button:focus-visible,
input:focus-visible, input:focus-visible,
@@ -214,9 +212,9 @@ summary:focus-visible {
min-height: 70px; min-height: 70px;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
text-align: left;
background: var(--color-surface);
border-bottom: 1px solid rgb(202 210 197 / 18%); border-bottom: 1px solid rgb(202 210 197 / 18%);
background: var(--color-surface);
text-align: left;
} }
.topbar a { .topbar a {
@@ -227,26 +225,47 @@ summary:focus-visible {
color: var(--color-text); color: var(--color-text);
} }
.topbar a.topbar__account { .topbar > form {
margin-left: auto; margin: 0 20px 0 auto;
margin-right: 20px;
padding: 8px 14px;
border: 1px solid var(--color-accent);
color: var(--color-accent);
} }
.topbar a.topbar__account:hover { .topbar a.topbar__account {
margin-right: 20px;
margin-left: auto;
}
.topbar .topbar__account {
display: inline-block;
padding: 8px 14px;
border: 1px solid var(--color-accent);
background: transparent;
color: var(--color-accent);
font: inherit;
line-height: normal;
text-decoration: none;
}
.topbar .topbar__account:hover {
background: var(--color-accent); background: var(--color-accent);
color: var(--color-background); color: var(--color-background);
text-decoration: none; text-decoration: none;
} }
.topbar button.topbar__account {
appearance: none;
border-radius: 0;
cursor: pointer;
}
.topbar img { .topbar img {
display: block; display: block;
width: 48px; width: 48px;
height: 48px; height: 48px;
} }
/* Accueil et présentation
------------------------------------------------------------------------ */
.logo { .logo {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -286,9 +305,14 @@ summary:focus-visible {
display: inline-block; display: inline-block;
padding: 10px 16px; padding: 10px 16px;
border: 2px solid var(--color-accent); border: 2px solid var(--color-accent);
border-radius: 0;
background: var(--color-accent); background: var(--color-accent);
color: var(--color-background); color: var(--color-background);
font: inherit;
font-weight: 700; font-weight: 700;
line-height: 1.4;
text-align: center;
cursor: pointer;
transition: filter 0.2s ease; transition: filter 0.2s ease;
} }
@@ -314,7 +338,9 @@ summary:focus-visible {
margin-top: 80px; margin-top: 80px;
padding: 20px 20px 40px; padding: 20px 20px 40px;
border: 2px solid var(--color-text); border: 2px solid var(--color-text);
box-shadow: 0 0 5px var(--color-shadow-light), 0 4px 4px var(--color-shadow-dark); box-shadow:
0 0 5px var(--color-shadow-light),
0 4px 4px var(--color-shadow-dark);
} }
.box img { .box img {
@@ -332,34 +358,6 @@ summary:focus-visible {
translate: 0 -55px; translate: 0 -55px;
} }
/* Bordure supérieure interrompue autour des titres, sans masquer la photo. */
.section-hosting .box {
border-top: 0;
background: rgb(15 40 55 / 30%);
}
.section-hosting .box__title {
display: flex;
align-items: center;
gap: 8px;
width: auto;
margin-inline: -22px;
padding: 10px 0;
background: transparent;
translate: 0 calc(-50% - 20px);
}
.section-hosting .box__title::before,
.section-hosting .box__title::after {
content: "";
flex: 1;
border-top: 2px solid var(--color-text);
}
.section-hosting .box__title::before {
flex: 0 0 20px;
}
.box__link { .box__link {
padding: 20px; padding: 20px;
color: var(--color-text); color: var(--color-text);
@@ -376,7 +374,9 @@ summary:focus-visible {
margin-bottom: 10px; margin-bottom: 10px;
} }
/* Couleur des cartes selon leur position. */ /* Cartes des services
------------------------------------------------------------------------ */
.section-services .box:nth-child(3n + 1) { .section-services .box:nth-child(3n + 1) {
--service-accent: var(--color-link); --service-accent: var(--color-link);
} }
@@ -397,31 +397,85 @@ summary:focus-visible {
border-color: var(--service-accent); border-color: var(--service-accent);
} }
/* Formulaires et messages .section-services,
.section-hosting {
padding-bottom: 100px;
}
/* Hébergement et infrastructure
------------------------------------------------------------------------ */ ------------------------------------------------------------------------ */
.section-hosting .box {
border-top: 0;
background: rgb(15 40 55 / 30%);
}
.section-hosting .box__title {
display: flex;
width: auto;
align-items: center;
gap: 8px;
margin-inline: -22px;
padding: 10px 0;
background: transparent;
translate: 0 calc(-50% - 20px);
}
.section-hosting .box__title::before,
.section-hosting .box__title::after {
content: "";
flex: 1;
border-top: 2px solid var(--color-text);
}
.section-hosting .box__title::before {
flex: 0 0 20px;
}
/* Formulaires
------------------------------------------------------------------------ */
.rodauth,
.contact-form { .contact-form {
display: grid; display: grid;
gap: 10px; gap: 12px;
font-size: 1rem; font-size: 1rem;
font-weight: 400;
line-height: 1.5;
}
.rodauth > p,
.contact-form > p {
margin: 0;
}
.rodauth .form-group {
display: grid;
gap: 8px;
margin: 0;
}
.rodauth .form-label,
.contact-form label,
.contact-form legend {
font-weight: 700; font-weight: 700;
} }
.contact-form input, .rodauth .form-control,
.contact-form input:not([type="submit"]),
.contact-form textarea { .contact-form textarea {
display: block;
width: 100%; width: 100%;
margin: 0; margin: 0;
padding: 10px; padding: 10px;
border: 0; border: 1px solid var(--color-text);
font-family: inherit; border-radius: 0;
font-size: 1.2rem;
}
/* Ne pas appliquer le fond clair au bouton d'envoi. */
.contact-form input:not([type="submit"]),
.contact-form textarea {
background: var(--color-text); background: var(--color-text);
color: var(--color-background); color: var(--color-background);
font-family: inherit;
font-size: 1.2rem;
font-weight: 400;
line-height: 1.4;
} }
.contact-form textarea { .contact-form textarea {
@@ -430,19 +484,83 @@ summary:focus-visible {
box-shadow: 0 0 10px var(--color-surface-dark); box-shadow: 0 0 10px var(--color-surface-dark);
} }
.contact-form input:not([type="submit"]):focus-visible, .rodauth .btn-primary,
.contact-form textarea:focus-visible { .contact-form .submit {
outline-color: var(--color-surface); display: block;
}
.submit {
width: 100%; width: 100%;
margin: 0;
padding: 10px 16px;
border: 2px solid var(--color-accent);
border-radius: 0;
background: var(--color-accent); background: var(--color-accent);
color: var(--color-background); color: var(--color-background);
font-family: inherit;
font-size: 1.2rem;
font-weight: 700; font-weight: 700;
line-height: 1.4;
cursor: pointer; cursor: pointer;
} }
.rodauth .btn-primary:hover,
.contact-form .submit:hover {
filter: brightness(1.12);
}
.contact-form fieldset {
display: grid;
min-width: 0;
gap: 10px;
margin: 0;
padding: 16px;
border: 1px solid var(--color-shadow-light);
}
.contact-form legend {
padding-inline: 8px;
color: var(--color-highlight);
}
.contact-form .form-description {
margin: 0;
font-weight: 400;
line-height: 1.5;
}
.contact-form .form-choice-separator {
margin: 0;
text-align: center;
}
.auth-panel {
max-width: 520px;
}
.auth-panel h1 {
margin-top: 0;
}
.auth-links {
margin-top: 28px;
padding-top: 20px;
border-top: 1px solid var(--color-shadow-light);
}
.auth-links h2 {
margin: 0 0 14px;
padding: 0;
border: 0;
color: var(--color-highlight);
font-size: 1.4rem;
}
.auth-links ul {
display: grid;
gap: 10px;
margin: 0;
padding: 0;
list-style: none;
}
.honeypot { .honeypot {
position: absolute; position: absolute;
left: -10000px; left: -10000px;
@@ -451,24 +569,129 @@ summary:focus-visible {
overflow: hidden; overflow: hidden;
} }
/* Notifications
------------------------------------------------------------------------ */
.flash-container {
display: grid;
gap: 10px;
margin-block: 1.5rem;
}
.flash { .flash {
margin: 0 0 1rem; width: 100%;
max-width: 1000px;
margin: 0 auto;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border-left: 3px solid; border-left: 3px solid;
} }
.flash--notice { .flash--notice {
background-color: var(--color-flash-notice-background);
border-color: var(--color-accent); border-color: var(--color-accent);
background: var(--color-flash-notice-background);
color: var(--color-text); color: var(--color-text);
} }
.flash--error { .flash--error {
background-color: var(--color-flash-error-background);
border-color: var(--color-flash-error-border); border-color: var(--color-flash-error-border);
background: var(--color-flash-error-background);
color: var(--color-text); color: var(--color-text);
} }
/* Tableaux
------------------------------------------------------------------------ */
.members-table {
width: 100%;
border-collapse: collapse;
text-align: left;
}
.members-table th,
.members-table td {
padding: 10px 16px;
border-bottom: 1px solid var(--color-shadow-light);
}
.members-table th {
color: var(--color-highlight);
}
.members-table td {
vertical-align: top;
}
/* Demandes d'adhésion
------------------------------------------------------------------------ */
.table-wrapper {
width: 100%;
margin-block: 1.5rem;
}
.memberships-table {
width: 100%;
table-layout: fixed;
font-size: 1rem;
}
.membership-col-identity {
width: 35%;
}
.membership-col-email {
width: 23%;
}
.membership-col-date {
width: 17%;
}
.membership-col-actions {
width: 25%;
}
.memberships-table th,
.memberships-table td {
padding: 10px 8px;
white-space: nowrap;
vertical-align: middle;
}
.memberships-table td strong,
.membership-kind {
display: inline;
}
.membership-kind {
margin-left: 0.25rem;
color: var(--color-link);
font-size: 0.85rem;
}
.membership-date {
font-size: 0.9rem;
}
.membership-actions {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0.5rem;
}
.membership-actions form {
flex: 0 0 auto;
margin: 0;
}
.membership-actions .button {
padding: 8px 10px;
font-size: 1rem;
line-height: 1.2;
white-space: nowrap;
}
/* Pied de page /* Pied de page
------------------------------------------------------------------------ */ ------------------------------------------------------------------------ */
@@ -490,7 +713,7 @@ footer {
margin-inline: auto; margin-inline: auto;
} }
/* Adaptations et accessibilité /* Adaptations pour les écrans larges
------------------------------------------------------------------------ */ ------------------------------------------------------------------------ */
@media screen and (min-width: 1080px) { @media screen and (min-width: 1080px) {
@@ -514,7 +737,9 @@ footer {
} }
} }
/* Effets au survol sur les écrans et périphériques adaptés. */ /* Effets au survol
------------------------------------------------------------------------ */
@media (hover: hover) and (min-width: 1080px) { @media (hover: hover) and (min-width: 1080px) {
.box__link:hover { .box__link:hover {
transform: scale(1.04); transform: scale(1.04);
@@ -525,6 +750,71 @@ footer {
} }
} }
/* Adaptations pour les écrans intermédiaires et petits
------------------------------------------------------------------------ */
@media screen and (max-width: 1100px) {
.memberships-table,
.memberships-table tbody {
display: block;
}
.memberships-table thead {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
.memberships-table tbody {
display: grid;
gap: 1rem;
}
.memberships-table tr {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem 1rem;
padding: 1rem;
border: 1px solid var(--color-shadow-light);
background: var(--color-surface-dark);
}
.memberships-table td {
display: block;
min-width: 0;
padding: 0.35rem 0;
border: 0;
white-space: normal;
overflow-wrap: anywhere;
}
.memberships-table td::before {
display: block;
margin-bottom: 0.25rem;
color: var(--color-highlight);
font-weight: 700;
content: attr(data-label);
}
.memberships-table td[data-label="Décision"] {
grid-column: 1 / -1;
}
.membership-kind {
font-size: 0.9rem;
}
.membership-actions {
flex-wrap: wrap;
}
}
/* Adaptations pour les petits écrans
------------------------------------------------------------------------ */
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
.box { .box {
margin-top: 60px; margin-top: 60px;
@@ -536,11 +826,30 @@ footer {
padding-bottom: 60px; padding-bottom: 60px;
} }
.memberships-table tr {
grid-template-columns: minmax(0, 1fr);
}
.memberships-table td[data-label="Décision"] {
grid-column: auto;
}
.membership-actions form {
flex: 1 1 auto;
}
.membership-actions .button {
width: 100%;
}
footer { footer {
padding: 24px 20px; padding: 24px 20px;
} }
} }
/* Préférences d'accessibilité
------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
html { html {
scroll-behavior: auto; scroll-behavior: auto;
+48
View File
@@ -0,0 +1,48 @@
class Netig
hash_branch "account" do |r|
rodauth.require_account
member = DB[:members]
.where(account_id: rodauth.session_value)
.first
r.halt [404, {}, ["Fiche membre introuvable."]] unless member
@page_title = "Mon espace — NETig"
@page_description = "Consultez vos informations personnelles et suivez votre adhésion à l'association NETig."
@canonical_url = "https://netig.net/account"
r.is do
r.get do
@membership = DB[:memberships]
.where(member_id: member[:id])
.order(Sequel.desc(:requested_at), Sequel.desc(:id))
.first
view "account"
end
end
r.post "membership" do
active_membership = DB[:memberships]
.where(
member_id: member[:id],
status: %w[pending accepted]
)
.first
if active_membership
flash["error"] = "Une demande d'adhésion est déjà en cours ou acceptée."
else
begin
DB[:memberships].insert(member_id: member[:id])
flash["notice"] = "Votre demande d'adhésion a été transmise au Noyau."
rescue Sequel::UniqueConstraintViolation
flash["error"] = "Une demande d'adhésion est déjà en cours."
end
end
r.redirect "/account"
end
end
end
+135
View File
@@ -0,0 +1,135 @@
class Netig
private
# Enregistrement d'une décision d'adhésion
def record_membership_decision!(r, membership_id, status)
member_id = DB[:members]
.where(account_id: rodauth.session_value)
.get(:id)
unless member_id
r.halt [403, {}, ["Accès interdit."]]
end
message = case status
when "accepted"
"La demande d'adhésion a été acceptée."
when "rejected"
"La demande d'adhésion a été refusée."
end
updated = DB[:memberships]
.where(id: membership_id, status: "pending")
.update(
status: status,
decided_at: Sequel::CURRENT_TIMESTAMP,
recorded_by: member_id
)
if updated == 1
flash["notice"] = message
else
flash["error"] = "Cette demande n'est plus en attente ou n'existe pas."
end
r.redirect "/noyau/memberships"
end
hash_branch "noyau" do |r|
require_core_member!(r)
@page_title = "Gestion de l'association — NETig"
@page_description = "Gestion collective de l'association NETig par le Noyau."
@canonical_url = "https://netig.net/noyau"
# Accueil
r.is do
r.get do
view "noyau"
end
end
# Demandes d'adhésion
r.on "memberships" do
r.is do
r.get do
@page_title = "Demandes d'adhésion — NETig"
@page_description = "Examen des demandes d'adhésion à NETig."
@canonical_url = "https://netig.net/noyau/memberships"
@memberships = DB[:memberships]
.join(:members, id: :member_id)
.join(:accounts, id: Sequel[:members][:account_id])
.where(
Sequel[:memberships][:status] => "pending",
Sequel[:accounts][:status_id] => 2
)
.select_all(:memberships)
.select_append(
Sequel[:members][:kind].as(:member_kind),
Sequel[:members][:first_name].as(:member_first_name),
Sequel[:members][:last_name].as(:member_last_name),
Sequel[:members][:organization_name].as(:member_organization_name),
Sequel[:accounts][:email].as(:account_email)
)
.order(
Sequel.asc(Sequel[:memberships][:requested_at]),
Sequel.asc(Sequel[:memberships][:id])
)
.all
view "noyau/memberships"
end
end
# Décision sur une demande
r.on Integer do |membership_id|
r.post "accept" do
record_membership_decision!(r, membership_id, "accepted")
end
r.post "reject" do
record_membership_decision!(r, membership_id, "rejected")
end
end
end
# Gestion des comptes
r.on "members" do
r.is do
r.get do
@page_title = "Comptes — NETig"
@page_description = "Consultation des comptes de l'association NETig."
@canonical_url = "https://netig.net/noyau/members"
@members = DB[:members]
.join(:accounts, id: :account_id)
.where(Sequel[:accounts][:status_id] => 2)
.select_all(:members)
.select_append(Sequel[:accounts][:email].as(:email))
.order(Sequel[:members][:id])
.all
memberships_by_member = DB[:memberships]
.order(Sequel.desc(:requested_at), Sequel.desc(:id))
.all
.group_by { |membership| membership[:member_id] }
@members.each do |member|
memberships = memberships_by_member.fetch(member[:id], [])
membership = memberships.find do |item|
%w[pending accepted].include?(item[:status])
end
membership ||= memberships.first
member[:membership_status] = membership && membership[:status]
end
view "noyau/members"
end
end
end
end
end
+61
View File
@@ -0,0 +1,61 @@
<section class="page-section">
<div class="content page-panel">
<h1>Mon espace</h1>
<% if flash["notice"] || flash["error"] %>
<div class="flash-container">
<% if notice = flash["notice"] %>
<p class="flash flash--notice" role="status"><%= notice %></p>
<% end %>
<% if error = flash["error"] %>
<p class="flash flash--error" role="alert"><%= error %></p>
<% end %>
</div>
<% end %>
<p>
Retrouvez vos informations personnelles et suivez votre adhésion.
</p>
<section aria-labelledby="membership-heading">
<h2 id="membership-heading">Mon adhésion</h2>
<% if @membership.nil? %>
<p>Vous n'avez pas encore demandé à adhérer à NETig.</p>
<% elsif @membership[:status] == "pending" %>
<p>Votre demande d'adhésion est en cours d'examen par le Noyau.</p>
<% elsif @membership[:status] == "accepted" %>
<p>Votre adhésion à NETig est acceptée.</p>
<% elsif @membership[:status] == "rejected" %>
<p>Votre demande d'adhésion a été refusée.</p>
<% end %>
<% if @membership.nil? || @membership[:status] == "rejected" %>
<form action="/account/membership" method="post">
<%== rodauth.csrf_tag("/account/membership") %>
<button class="button" type="submit">
Demander à adhérer
</button>
</form>
<% end %>
</section>
<% if core_member? %>
<section aria-labelledby="core-heading">
<h2 id="core-heading">Vie de l'association</h2>
<p>
Accédez aux outils de gestion et de travail collectif du Noyau.
</p>
<a class="button button--secondary" href="/noyau">
Espace du Noyau →
</a>
</section>
<% end %>
</div>
</section>
+13 -8
View File
@@ -1,19 +1,24 @@
<section class="page-section"> <section class="page-section">
<div class="content page-panel"> <div class="content page-panel">
<h1>Contact</h1> <h1>Contact</h1>
<% if flash["error"] || flash["notice"] %>
<div class="flash-container">
<% if error = flash["error"] %>
<p class="flash flash--error" role="alert"><%= error %></p>
<% end %>
<% if notice = flash["notice"] %>
<p class="flash flash--notice" role="status"><%= notice %></p>
<% end %>
</div>
<% end %>
<div class="col--1-1"> <div class="col--1-1">
<div> <div>
<p>Vous avez un projet en tête ? Un service vous intéresse ? Ou simplement une question ? Ce formulaire est là pour ça !</p> <p>Vous avez un projet en tête ? Un service vous intéresse ? Ou simplement une question ? Ce formulaire est là pour ça !</p>
</div> </div>
<div> <div>
<% if notice = flash["notice"] %>
<p class="flash flash--notice" role="status"><%= notice %></p>
<% end %>
<% if error = flash["error"] %>
<p class="flash flash--error" role="alert"><%= error %></p>
<% end %>
<form action="/contact/send" method="post" class="contact-form" accept-charset="UTF-8"> <form action="/contact/send" method="post" class="contact-form" accept-charset="UTF-8">
<%== csrf_tag("/contact/send") %> <%== csrf_tag("/contact/send") %>
+130
View File
@@ -0,0 +1,130 @@
<section class="page-section">
<div class="content page-panel">
<h1>Créer un compte</h1>
<% if flash["error"] || flash["notice"] %>
<div class="flash-container">
<% if error = flash["error"] %>
<p class="flash flash--error" role="alert"><%= error %></p>
<% end %>
<% if notice = flash["notice"] %>
<p class="flash flash--notice" role="status"><%= notice %></p>
<% end %>
</div>
<% end %>
<p>
Créez un compte pour accéder à votre espace personnel
et, si vous le souhaitez, demander à rejoindre NETig.
</p>
<form
action="<%= rodauth.create_account_path %>"
method="post"
class="contact-form"
accept-charset="UTF-8"
>
<%== rodauth.csrf_tag %>
<p class="form-description">
Choisissez l'une des deux possibilités suivantes.
</p>
<fieldset>
<legend>Compte personnel</legend>
<p class="form-description">
Pour créer un compte en votre nom (personne physique).
</p>
<label for="first_name">Prénom</label>
<input
id="first_name"
name="first_name"
type="text"
maxlength="128"
autocomplete="given-name"
value="<%= request.params["first_name"] %>"
>
<% if error = rodauth.field_error("first_name") %>
<p class="flash flash--error"><%= error %></p>
<% end %>
<label for="last_name">Nom</label>
<input
id="last_name"
name="last_name"
type="text"
maxlength="128"
autocomplete="family-name"
value="<%= request.params["last_name"] %>"
>
<% if error = rodauth.field_error("last_name") %>
<p class="flash flash--error"><%= error %></p>
<% end %>
</fieldset>
<p class="form-choice-separator">ou</p>
<fieldset>
<legend>Compte au nom d'une organisation</legend>
<p class="form-description">
Pour créer un compte au nom d'une association,
d'une entreprise ou d'une autre personne morale.
</p>
<label for="organization_name">Dénomination</label>
<input
id="organization_name"
name="organization_name"
type="text"
maxlength="255"
autocomplete="organization"
value="<%= request.params["organization_name"] %>"
>
<% if error = rodauth.field_error("organization_name") %>
<p class="flash flash--error"><%= error %></p>
<% end %>
</fieldset>
<p>
<strong>Email</strong>
</p>
<input
id="<%= rodauth.login_param %>"
name="<%= rodauth.login_param %>"
type="<%= rodauth.login_input_type %>"
maxlength="254"
autocomplete="email"
value="<%= request.params[rodauth.login_param] %>"
required
>
<% if error = rodauth.field_error(rodauth.login_param) %>
<p class="flash flash--error"><%= error %></p>
<% end %>
<p class="form-description">
Nous vous enverrons un email pour vérifier votre adresse
électronique. Vous choisirez votre mot de passe à cette étape.
</p>
<input
type="submit"
value="<%= rodauth.create_account_button %>"
class="submit"
>
</form>
<p>
Vous avez déjà un compte ?
<a href="<%= rodauth.login_path %>">Se connecter</a>.
</p>
</div>
</section>
+5
View File
@@ -0,0 +1,5 @@
<section class="page-section">
<div class="content prose page-panel">
<%== @document_html %>
</div>
</section>
+5 -5
View File
@@ -5,7 +5,7 @@
</div> </div>
<div> <div>
<h1>NETig</h1> <h1>NETig</h1>
<p class="hero__lead">Fournisseur associatif de <strong>services numériques libres, simples et indépendants</strong>, respectueux de la vie privée, à destination des particuliers, des associations et des petites entreprises.</p> <p class="hero__lead">Fournisseur associatif* de <strong>services numériques libres, simples et indépendants</strong>, respectueux de la vie privée, à destination des particuliers, des associations et des petites entreprises.</p>
<div class="hero__actions"> <div class="hero__actions">
<a class="button" href="/#association">Découvrir l'association ➜</a> <a class="button" href="/#association">Découvrir l'association ➜</a>
<a class="button button--secondary" href="https://status.netig.net/status/services" target="_blank" rel="noopener noreferrer"> <a class="button button--secondary" href="https://status.netig.net/status/services" target="_blank" rel="noopener noreferrer">
@@ -43,7 +43,7 @@
<section id="hosting" class="section-hosting"> <section id="hosting" class="section-hosting">
<div class="content"> <div class="content">
<h2>Services réservés aux adhérents</h2> <h2>Services réservés aux adhérents</h2>
<p><strong>L'adhésion à NETig est gratuite !</strong> <a href="/url/provisoire">Créez votre compte</a> pour demander à rejoindre l'association et profiter de ses services.</p> <p><a href="/create-account">Créez votre compte</a> pour rejoindre l'association et profiter de ses services !</p>
<div class="col--1-1"> <div class="col--1-1">
<div class="box"> <div class="box">
<h3 class="box__title">NETmail — Adresse mail</h3> <h3 class="box__title">NETmail — Adresse mail</h3>
@@ -105,7 +105,7 @@
</div> </div>
<p> <p>
Les services NETig reposent sur des logiciels libres. Les services NETig reposent sur des logiciels libres.
<a href="/documentation/logiciels">Découvrez les logiciels utilisés ➜</a> <a href="/temporary_page">Découvrez les logiciels utilisés</a>.
</p> </p>
</div> </div>
</section> </section>
@@ -113,7 +113,7 @@
<section id="association" class="section-association"> <section id="association" class="section-association">
<div class="content"> <div class="content">
<h2>Aidez-nous à continuer !</h2> <h2>Aidez-nous à continuer !</h2>
<p>L'adhésion est gratuite et les services sont financés grâce aux dons. Votre soutien nous aide à couvrir les frais d'hébergement et à maintenir les services accessibles.</p> <p>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.</p>
<a class="button" href="/#association">Faire un don ➜</a> <a class="button" href="/temporary_page">Faire un don ➜</a>
</div> </div>
</section> </section>
+30 -4
View File
@@ -1,4 +1,3 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -31,16 +30,39 @@
<a class="skip-link" href="#main-content">Aller au contenu</a> <a class="skip-link" href="#main-content">Aller au contenu</a>
<header> <header>
<nav class="topbar" aria-label="Accès aux services"> <nav class="topbar" aria-label="Navigation principale">
<% unless request.path == "/" %> <% unless request.path == "/" %>
<a href="/"> <a href="/">
<img src="/images/favicon.svg" alt="Accueil NETig"> <img src="/images/favicon.svg" alt="Accueil NETig">
</a> </a>
<% end %> <% end %>
<a href="https://webmail.netig.net" target="_blank" rel="noopener noreferrer">NETmail ↗</a> <a href="https://webmail.netig.net" target="_blank" rel="noopener noreferrer">NETmail ↗</a>
<a href="https://cloud.netig.net" target="_blank" rel="noopener noreferrer">NETcloud ↗</a> <a href="https://cloud.netig.net" target="_blank" rel="noopener noreferrer">NETcloud ↗</a>
<a href="https://git.netig.net" target="_blank" rel="noopener noreferrer">NETgit ↗</a> <a href="https://git.netig.net" target="_blank" rel="noopener noreferrer">NETgit ↗</a>
<a class="topbar__account" href="/login">Mon espace ➜</a>
<% if rodauth.logged_in? %>
<% if request.path == "/account" %>
<form method="post" action="<%= rodauth.logout_path %>">
<%== rodauth.csrf_tag(rodauth.logout_path) %>
<button class="topbar__account" type="submit">
Déconnexion
</button>
</form>
<% elsif request.path.start_with?("/noyau/") %>
<a class="topbar__account" href="/noyau">
Gestion de l'association ➜
</a>
<% else %>
<a class="topbar__account" href="/account">
Mon espace ➜
</a>
<% end %>
<% elsif request.path != rodauth.login_path %>
<a class="topbar__account" href="<%= rodauth.login_path %>">
Mon espace ➜
</a>
<% end %>
</nav> </nav>
</header> </header>
@@ -51,12 +73,16 @@
<footer> <footer>
<div class="botbar"> <div class="botbar">
<p>Contenu du site par Julien Debrousse — <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="license noopener noreferrer">CC BY 4.0</a></p> <p>Contenu du site par Julien Debrousse — <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="license noopener noreferrer">CC BY 4.0</a></p>
<p><a href="/statutes">Statuts</a></p>
<p><a href="/legal">Mentions légales</a></p> <p><a href="/legal">Mentions légales</a></p>
<p>Fait avec ❤️ et <a href="https://roda.jeremyevans.net/" target="_blank" rel="noopener noreferrer">Roda</a> !</p> <p>Fait avec ❤️ et <a href="https://roda.jeremyevans.net/" target="_blank" rel="noopener noreferrer">Roda</a> !</p>
</div> </div>
<div class="botbar">
<p>* L'association NETig est en cours de constitution et n'est pas encore déclarée.</p>
</div>
</footer> </footer>
</body> </body>
</html> </html>
-59
View File
@@ -1,59 +0,0 @@
<section class="page-section">
<div class="content prose page-panel">
<h1>Mentions légales</h1>
<p>
NETig — Association loi 1901<br>
Membres fondateurs : Julien Debrousse et John Doe<br>
Siège social : [adresse]<br>
RNA : [numéro]<br>
Directeur de la publication : [nom]
</p>
<h2 id="contact">Contact</h2>
<p>Il est possible de prendre contact par courriel à : <a href="mailto:contact@netig.net">contact@netig.net</a>.</p>
<h2 id="hebergement-des-donnees">Hébergement des données</h2>
<p>Les services sont hébergés sur des infrastructures administrées bénévolement par l'association chez <a href="https://www.hetzner.com/" target="_blank" rel="noopener noreferrer">Hetzner</a>. Le VPS principal fonctionne sous Debian. Des sauvegardes quotidiennes sont conservées sur une Storage Box Hetzner située en Finlande.</p>
<p>Hébergeur : Hetzner Online GmbH — [adresse et téléphone à compléter].</p>
<h2 id="conditions-generales-d-utilisation">Conditions générales d'utilisation</h2>
<h3 id="disponibilite">Disponibilité</h3>
<p>Malgré les efforts faits pour assurer la meilleure disponibilité possible, un accès permanent aux services n'est pas garanti. Vous pouvez consulter l'état des services en temps réel depuis un <a href="https://status.netig.net/status/services" target="_blank" rel="noopener noreferrer">outil de surveillance</a> mis à disposition. Vous pouvez aussi vous inscrire à la <a href="https://list.netig.net/subscription/form" target="_blank" rel="noopener noreferrer">liste de diffusion de maintenance</a>, afin d'être informé des interruptions de service.</p>
<h3 id="responsabilite">Responsabilité</h3>
<p>Vous êtes responsable des contenus diffusés et êtes tenu de respecter la législation en vigueur. Le non-respect des conditions d'utilisation ou des lois en vigueur peut entraîner une suspension des services ou une suppression des données et comptes concernés.</p>
<h3 id="donnees-personnelles">Données personnelles</h3>
<h4 id="confidentialite">Confidentialité</h4>
<p>Les données ne sont ni vendues ni utilisées à des fins publicitaires. Elles sont traitées uniquement pour gérer les adhésions, fournir, administrer et sécuriser les services, sous réserve des obligations légales applicables.</p>
<h4 id="conservation">Conservation</h4>
<p>Les données personnelles sont conservées pendant la durée nécessaire à la gestion des adhésions et à la mise à disposition des services, sous réserve des obligations légales de conservation. Elles peuvent persister pendant la durée de rétention des sauvegardes après la clôture des services.</p>
<h4 id="recuperation">Récupération</h4>
<p>Vous pouvez à tout moment récupérer vos données personnelles, soit à l'aide d'une fonctionnalité intégrée à un service, soit sur demande.</p>
<h4 id="suppression">Suppression</h4>
<p>Vous pouvez demander la suppression de vos données personnelles, soit à l'aide d'une fonctionnalité intégrée à un service, soit sur demande écrite, sous réserve des obligations légales de conservation.</p>
<h4 id="droits">Vos droits</h4>
<p>Vous pouvez exercer vos droits d'accès, de rectification, d'effacement et les autres droits prévus par la réglementation applicable en écrivant à <a href="mailto:contact@netig.net">contact@netig.net</a>. Vous pouvez également introduire une réclamation auprès de la CNIL.</p>
<h3 id="services-en-libre-acces">Services en libre accès</h3>
<p>Ces services sont accessibles à tous afin de proposer une alternative aux produits des GAFAM ou « Géants du web », respectueuse des données de chacun. Les ressources matérielles ne sont pas sans limites : utilisez-les avec bon sens. En cas d'abus et après un premier avertissement lorsque cela est possible, votre adresse IP pourra être bannie.</p>
<h3 id="services-adherents">Services réservés aux adhérents</h3>
<h4 id="adhesion">Adhésion</h4>
<p>L'adhésion à l'association est gratuite et soumise à validation conformément à ses statuts. Elle permet de demander l'accès aux services réservés aux adhérents, dans la limite des ressources disponibles. L'adhésion ne garantit pas l'attribution automatique d'un service.</p>
<h4 id="dons">Dons</h4>
<p>L'association est financée par des dons libres et volontaires destinés à couvrir ses frais de fonctionnement. Aucun don n'est obligatoire pour adhérer ou bénéficier des services. Les dons ne donnent droit à aucun accès ou avantage supplémentaire.</p>
<h4 id="suspension-et-resiliation">Suspension et résiliation</h4>
<p>Le non-respect des conditions d'utilisation peut entraîner une suspension ou une suppression des services concernés, notamment en cas d'abus. Vous pouvez à tout moment demander la désactivation d'un service et la suppression des données associées, sous réserve des obligations légales de conservation.</p>
</div>
</section>
+34
View File
@@ -0,0 +1,34 @@
<section class="page-section">
<div class="content page-panel auth-panel">
<h1>Connexion</h1>
<% if flash["error"] || flash["notice"] %>
<div class="flash-container">
<% if error = flash["error"] %>
<p class="flash flash--error" role="alert"><%= error %></p>
<% end %>
<% if notice = flash["notice"] %>
<p class="flash flash--notice" role="status"><%= notice %></p>
<% end %>
</div>
<% end %>
<%== rodauth.login_form_header %>
<%== rodauth.render("login-form") %>
<% links = rodauth.login_form_footer_links %>
<% unless links.empty? %>
<nav class="auth-links" aria-label="Autres possibilités">
<h2>Autres possibilités</h2>
<ul>
<% links.each do |_, url, text| %>
<li><a href="<%= url %>"><%= text %></a></li>
<% end %>
</ul>
</nav>
<% end %>
</div>
</section>
+33
View File
@@ -0,0 +1,33 @@
<section class="page-section">
<div class="content page-panel">
<h1>Gestion de l'association</h1>
<p>
Gestion collective de l'association par le Noyau.
</p>
<section aria-labelledby="membership-heading">
<h2 id="membership-heading">Adhésions</h2>
<p>
Consultez et examinez les demandes d'adhésion à NETig.
</p>
<a class="button" href="/noyau/memberships">
Demandes d'adhésion
</a>
</section>
<section aria-labelledby="members-heading">
<h2 id="members-heading">Comptes</h2>
<p>
Consultez les comptes et les informations des membres.
</p>
<a class="button" href="/noyau/members">
Consulter les comptes
</a>
</section>
</div>
</section>
+53
View File
@@ -0,0 +1,53 @@
<section class="page-section">
<div class="content">
<h1>Comptes</h1>
<% if @members.empty? %>
<p>Aucun adhérent enregistré.</p>
<% else %>
<table class="members-table">
<thead>
<tr>
<th>Identité</th>
<th>Type</th>
<th>Membre du Noyau</th>
<th>Adhésion</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<% @members.each do |member| %>
<tr>
<td>
<%= member[:kind] == "individual" ?
"#{member[:first_name]} #{member[:last_name]}" :
member[:organization_name] %>
</td>
<td>
<%= member[:kind] == "individual" ?
"Personne physique" : "Personne morale" %>
</td>
<td>
<%= if member[:kind] == "individual"
member[:core_member] ? "Oui" : "Non"
else
"—"
end %>
</td>
<td>
<%= case member[:membership_status]
when "pending" then "En attente"
when "accepted" then "Acceptée"
when "rejected" then "Refusée"
else "Aucune demande"
end %>
</td>
<td><%= member[:email] %></td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
</div>
</section>
+96
View File
@@ -0,0 +1,96 @@
<section class="page-section">
<div class="content page-panel">
<h1>Demandes d'adhésion</h1>
<% if flash["notice"] || flash["error"] %>
<div class="flash-container">
<% if notice = flash["notice"] %>
<p class="flash flash--notice" role="status"><%= notice %></p>
<% end %>
<% if error = flash["error"] %>
<p class="flash flash--error" role="alert"><%= error %></p>
<% end %>
</div>
<% end %>
<p>
Consultez les demandes d'adhésion en attente d'examen
par le Noyau.
</p>
<% if @memberships.empty? %>
<p>Aucune demande d'adhésion en attente.</p>
<% else %>
<div class="table-wrapper">
<table class="members-table memberships-table">
<colgroup>
<col class="membership-col-identity">
<col class="membership-col-email">
<col class="membership-col-date">
<col class="membership-col-actions">
</colgroup>
<thead>
<tr>
<th scope="col">Identité</th>
<th scope="col">Email</th>
<th scope="col">Demande</th>
<th scope="col">Décision</th>
</tr>
</thead>
<tbody>
<% @memberships.each do |membership| %>
<tr>
<td data-label="Identité">
<% if membership[:member_kind] == "individual" %>
<strong><%= "#{membership[:member_first_name]} #{membership[:member_last_name]}" %></strong>
<small class="membership-kind">· Personne physique</small>
<% else %>
<strong><%= membership[:member_organization_name] %></strong>
<small class="membership-kind">· Personne morale</small>
<% end %>
</td>
<td data-label="Email">
<%= membership[:account_email] %>
</td>
<td data-label="Demande">
<time class="membership-date">
<%= membership[:requested_at].strftime("%d/%m/%Y à %H:%M") %>
</time>
</td>
<td data-label="Décision">
<div class="membership-actions">
<form
method="post"
action="/noyau/memberships/<%= membership[:id] %>/accept"
>
<%== rodauth.csrf_tag("/noyau/memberships/#{membership[:id]}/accept") %>
<button class="button" type="submit">
Accepter
</button>
</form>
<form
method="post"
action="/noyau/memberships/<%= membership[:id] %>/reject"
>
<%== rodauth.csrf_tag("/noyau/memberships/#{membership[:id]}/reject") %>
<button class="button button--secondary" type="submit">
Refuser
</button>
</form>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
</section>
+6
View File
@@ -0,0 +1,6 @@
<section class="page-section">
<div class="content prose page-panel">
<h1>Page temporaire</h1>
<p>Cette page n'existe pas encore !</p>
</div>
</section>
+46
View File
@@ -0,0 +1,46 @@
<section class="page-section">
<div class="content page-panel auth-panel">
<h1>Renvoyer le courriel de vérification</h1>
<% if flash["error"] || flash["notice"] %>
<div class="flash-container">
<% if error = flash["error"] %>
<p class="flash flash--error" role="alert"><%= error %></p>
<% end %>
<% if notice = flash["notice"] %>
<p class="flash flash--notice" role="status"><%= notice %></p>
<% end %>
</div>
<% end %>
<form
action="<%= rodauth.verify_account_resend_path %>"
method="post"
class="rodauth"
role="form"
id="verify-account-resend-form"
>
<%== rodauth.verify_account_resend_additional_form_tags %>
<%== rodauth.csrf_tag(rodauth.verify_account_resend_path) %>
<p><%= rodauth.verify_account_resend_explanatory_text %></p>
<%== rodauth.param_or_nil(rodauth.login_param) ?
rodauth.login_hidden_field :
rodauth.render("login-field") %>
<%== rodauth.button(rodauth.verify_account_resend_button) %>
</form>
<nav class="auth-links" aria-label="Autres possibilités">
<ul>
<li>
<a href="<%= rodauth.login_path %>">
Retour à la connexion
</a>
</li>
</ul>
</nav>
</div>
</section>
+39
View File
@@ -0,0 +1,39 @@
<section class="page-section">
<div class="content page-panel auth-panel">
<h1>Vérifier votre email</h1>
<p>
Pour confirmer votre email et activer
votre compte, choisissez un mot de passe.
</p>
<form
action="<%= rodauth.verify_account_path %>"
method="post"
class="rodauth"
role="form"
id="verify-account-form"
>
<%== rodauth.verify_account_additional_form_tags %>
<%== rodauth.csrf_tag %>
<%== rodauth.render("password-field") if rodauth.verify_account_set_password? %>
<% if rodauth.verify_account_set_password? && rodauth.require_password_confirmation? %>
<%== rodauth.render("password-confirm-field") %>
<% end %>
<%== rodauth.button(rodauth.verify_account_button) %>
</form>
<nav class="auth-links" aria-label="Autres possibilités">
<ul>
<li>
<a href="<%= rodauth.verify_account_resend_path %>">
Renvoyer l'email de vérification
</a>
</li>
</ul>
</nav>
</div>
</section>