Compare commits
72
Commits
6bc0e7ba62
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6ba3681b2 | ||
|
|
491457c50e | ||
|
|
4a6ca8b569 | ||
|
|
311f25f6fd | ||
|
|
9ea0bee39d | ||
|
|
bf633b7060 | ||
|
|
acc4c43250 | ||
|
|
4d8c7c7c71 | ||
|
|
06312e8d83 | ||
|
|
21ebe95cbb | ||
|
|
5402026dae | ||
|
|
4ba81f3a6b | ||
|
|
9d1cda213e | ||
|
|
e4d688171d | ||
|
|
189ad45eea | ||
|
|
2d72b20cad | ||
|
|
a57fa574eb | ||
|
|
5cf45c2084 | ||
|
|
b8fd7863d6 | ||
|
|
e5ec0ff1a1 | ||
|
|
137e53618b | ||
|
|
49b53451b7 | ||
|
|
9aa3da5191 | ||
|
|
db316b2914 | ||
|
|
4f28a59c7e | ||
|
|
f386499d2f | ||
|
|
9358dd39a1 | ||
|
|
cde5e651a6 | ||
|
|
9a554a764a | ||
|
|
cde8e13705 | ||
|
|
7a4805f749 | ||
|
|
747dd5c78f | ||
|
|
cb90a80d49 | ||
|
|
d1210a3ea0 | ||
|
|
ba49d0aad9 | ||
|
|
145dd95b24 | ||
|
|
5fee20ef9e | ||
|
|
5eddb8890e | ||
|
|
2924a55e43 | ||
|
|
335d385fba | ||
|
|
4e7cfaddd7 | ||
|
|
24b6a417cb | ||
|
|
acf30d1ebe | ||
|
|
7dfdbcfe38 | ||
|
|
76fd6cce90 | ||
|
|
92e6e5fb19 | ||
|
|
5ed382e1ac | ||
|
|
030f13d609 | ||
|
|
9c9e4a6ba0 | ||
|
|
f5d340a2b5 | ||
|
|
974f4f3679 | ||
|
|
50b798e1cc | ||
|
|
231446e7ed | ||
|
|
28932abc94 | ||
|
|
4b01d710e4 | ||
|
|
22d5ee8ebd | ||
|
|
c1147a0cd9 | ||
|
|
7de846fcd2 | ||
|
|
bbe1f09ec8 | ||
|
|
e7f433c39a | ||
|
|
a298c90914 | ||
|
|
9de79f1e15 | ||
|
|
417632673e | ||
|
|
c17db449bd | ||
|
|
ab2d8845a2 | ||
|
|
b2cddd727a | ||
|
|
3a78cf9889 | ||
|
|
38962c93b4 | ||
|
|
ad4226f884 | ||
|
|
5642a4692f | ||
|
|
a79de1a398 | ||
|
|
455e90fb27 |
No files matched your search
@@ -1,2 +1,3 @@
|
||||
.ruby-lsp/
|
||||
.env
|
||||
/db/*.sqlite3*
|
||||
@@ -2,14 +2,28 @@
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
# Development
|
||||
group :development do
|
||||
gem "irb"
|
||||
gem "dotenv"
|
||||
end
|
||||
|
||||
# Base
|
||||
gem "roda"
|
||||
gem "rackup"
|
||||
gem "puma"
|
||||
gem "tilt"
|
||||
gem "erubi"
|
||||
|
||||
# Database
|
||||
gem "sequel"
|
||||
gem "sqlite3"
|
||||
|
||||
# Mail
|
||||
gem "mail"
|
||||
|
||||
# Markdown
|
||||
gem "commonmarker"
|
||||
|
||||
# Authentication
|
||||
gem "rodauth"
|
||||
gem "bcrypt"
|
||||
+20
-5
@@ -1,6 +1,9 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
bcrypt (3.1.22)
|
||||
bigdecimal (4.1.3)
|
||||
commonmarker (2.10.1-x86_64-linux)
|
||||
date (3.6.0)
|
||||
dotenv (3.2.0)
|
||||
erb (6.0.7)
|
||||
@@ -36,8 +39,6 @@ GEM
|
||||
puma (8.0.2)
|
||||
nio4r (~> 2.0)
|
||||
rack (3.2.7)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
rbs (4.2.0)
|
||||
logger
|
||||
prism (>= 1.6.0)
|
||||
@@ -51,25 +52,37 @@ GEM
|
||||
io-console (~> 0.5)
|
||||
roda (3.109.0)
|
||||
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)
|
||||
timeout (0.6.1)
|
||||
tsort (0.2.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
bcrypt
|
||||
commonmarker
|
||||
dotenv
|
||||
erubi
|
||||
irb
|
||||
mail
|
||||
puma
|
||||
rackup
|
||||
roda
|
||||
rodauth
|
||||
sequel
|
||||
sqlite3
|
||||
tilt
|
||||
|
||||
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
|
||||
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
|
||||
erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
|
||||
@@ -89,11 +102,13 @@ CHECKSUMS
|
||||
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
||||
puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
|
||||
rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
|
||||
rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868
|
||||
rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674
|
||||
rdoc (8.1.0) sha256=befc37880a5cfd36dcb176c748b5fd5ee39dd3536a75e81824e4972ce230f60a
|
||||
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
|
||||
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
|
||||
timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
|
||||
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
||||
|
||||
@@ -1,34 +1,243 @@
|
||||
# netig.net
|
||||
|
||||
## À faire
|
||||
|
||||
- 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épendances
|
||||
|
||||
Installer les dépendances :
|
||||
|
||||
```sh
|
||||
bundle install
|
||||
```
|
||||
|
||||
### Variables d'environnement
|
||||
|
||||
Créer un fichier `.env` à la racine du projet :
|
||||
|
||||
```dotenv
|
||||
RACK_ENV=development
|
||||
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`.
|
||||
|
||||
Démarrer le serveur de développement :
|
||||
Générer `SESSION_SECRET` et `RODAUTH_HMAC_SECRET` avec :
|
||||
|
||||
```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
|
||||
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
|
||||
|
||||
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.
|
||||
@@ -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
@@ -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,4 +1,3 @@
|
||||
require "dotenv/load" if ENV.fetch("RACK_ENV", "development") == "development"
|
||||
require_relative "netig"
|
||||
|
||||
run Netig.freeze.app
|
||||
@@ -0,0 +1,4 @@
|
||||
if ENV.fetch("RACK_ENV", "development") == "development"
|
||||
require "dotenv"
|
||||
Dotenv.load(File.expand_path("../.env", __dir__))
|
||||
end
|
||||
+7
-7
@@ -2,12 +2,12 @@ require "mail"
|
||||
|
||||
Mail.defaults do
|
||||
delivery_method :smtp, {
|
||||
:address => "mail.netig.net",
|
||||
:port => 587,
|
||||
:domain => "netig.net",
|
||||
:user_name => "noreply@netig.net",
|
||||
:password => ENV.fetch("SMTP_PASSWORD"),
|
||||
:authentication => "plain",
|
||||
:enable_starttls => :always
|
||||
address: "mail.netig.net",
|
||||
port: 587,
|
||||
domain: "netig.net",
|
||||
user_name: "noreply@netig.net",
|
||||
password: ENV.fetch("SMTP_PASSWORD"),
|
||||
authentication: "plain",
|
||||
enable_starttls: :always
|
||||
}
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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).
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
# Statuts de l’association NETig
|
||||
|
||||
## Article 1 — Constitution
|
||||
|
||||
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 **NETig**.
|
||||
Sa durée est illimitée et son siège social est fixé à Brasparts (Finistère).
|
||||
|
||||
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.
|
||||
|
||||
## Article 2 — Objet et 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.
|
||||
|
||||
Ses principes sont :
|
||||
|
||||
- le respect de la vie privée, la protection des données personnelles et
|
||||
des libertés fondamentales ;
|
||||
- l’utilisation exclusive de logiciels libres ;
|
||||
- le partage équitable des ressources disponibles ;
|
||||
- le partage ouvert des savoirs et des compétences informatiques.
|
||||
|
||||
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.
|
||||
|
||||
## Article 3 — Adhésion
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
L’adhésion prend fin par démission notifiée, décès, dissolution de la
|
||||
personne morale ou exclusion (article 7).
|
||||
|
||||
## Article 4 — Assemblée générale
|
||||
|
||||
### Convocation et ordre du jour
|
||||
|
||||
L’AG se réunit au moins une fois par an, au cours du premier trimestre.
|
||||
Le Noyau convoque l’AG annuelle.
|
||||
|
||||
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.
|
||||
|
||||
Le Noyau dispose de quinze jours après réception d’une demande d’AG pour
|
||||
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.
|
||||
|
||||
La convocation indique l’ordre du jour et est envoyée au moins quinze jours
|
||||
avant la réunion. Seules les questions inscrites à l’ordre du jour peuvent
|
||||
être votées.
|
||||
|
||||
Un dispositif électronique gère les demandes et les convocations sans
|
||||
révéler la liste des membres ni leurs coordonnées. L’AG peut se tenir en
|
||||
présentiel, à distance ou en format hybride, si l’identité des participants
|
||||
et la sincérité des votes sont garanties.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
### 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.
|
||||
|
||||
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é.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -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
@@ -6,6 +6,8 @@ class ContactForm
|
||||
"message" => 5000
|
||||
}.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)
|
||||
@params = params
|
||||
end
|
||||
@@ -26,7 +28,8 @@ class ContactForm
|
||||
|
||||
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|
|
||||
@params[key].match?(/[\r\n]/)
|
||||
end
|
||||
|
||||
@@ -6,9 +6,9 @@ class ContactMailer
|
||||
end
|
||||
|
||||
def build
|
||||
email = @params["email"]
|
||||
name = @params["name"]
|
||||
subject = @params["subject"]
|
||||
email = @params["email"].strip
|
||||
name = @params["name"].strip
|
||||
subject = @params["subject"].strip
|
||||
message = @params["message"]
|
||||
|
||||
Mail.new do
|
||||
|
||||
@@ -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"]
|
||||
)
|
||||
@@ -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
|
||||
@@ -1,35 +1,81 @@
|
||||
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 "lib/contact_form"
|
||||
require_relative "lib/contact_mailer"
|
||||
require_relative "lib/document_renderer"
|
||||
|
||||
# Configuration de Roda
|
||||
class Netig < Roda
|
||||
plugin :render, escape: true
|
||||
plugin :public
|
||||
plugin :default_headers, "Content-Type" => "text/html; charset=utf-8"
|
||||
plugin :sessions, secret: ENV.fetch("SESSION_SECRET")
|
||||
plugin :route_csrf
|
||||
plugin :route_csrf, csrf_failure: :empty_403
|
||||
plugin :hash_branches
|
||||
plugin :flash
|
||||
end
|
||||
|
||||
require_relative "routes/contact"
|
||||
# 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"
|
||||
|
||||
# Routes des pages publiques
|
||||
class Netig
|
||||
route do |r|
|
||||
r.rodauth
|
||||
|
||||
r.public
|
||||
check_csrf!
|
||||
r.hash_branches
|
||||
|
||||
# Accueil
|
||||
r.root do
|
||||
@canonical_url = "https://netig.net/"
|
||||
view "index"
|
||||
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
|
||||
@page_title = "Mentions légales — NETig"
|
||||
@page_description = "Mentions légales de l'association NETig."
|
||||
@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
|
||||
+398
-83
@@ -58,9 +58,9 @@ html {
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
@@ -70,12 +70,26 @@ body {
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
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 {
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
@@ -91,19 +105,6 @@ main > section:nth-of-type(odd):not(:only-of-type),
|
||||
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 {
|
||||
flex: 1;
|
||||
padding: clamp(24px, 5vw, 72px) 20px;
|
||||
@@ -112,7 +113,6 @@ main > section:nth-of-type(odd):not(:only-of-type),
|
||||
.page-panel {
|
||||
max-width: 1000px;
|
||||
padding: clamp(24px, 4vw, 48px);
|
||||
border-radius: 16px;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
@@ -120,11 +120,6 @@ main > section:nth-of-type(odd):not(:only-of-type),
|
||||
max-width: 72ch;
|
||||
}
|
||||
|
||||
.section-services,
|
||||
.section-hosting {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
/* Typographie et liens
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
@@ -175,6 +170,15 @@ a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main p a:not(.button),
|
||||
footer a {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.15em;
|
||||
}
|
||||
|
||||
/* Navigation clavier et focus
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
a:focus-visible,
|
||||
button:focus-visible,
|
||||
input:focus-visible,
|
||||
@@ -208,9 +212,9 @@ summary:focus-visible {
|
||||
min-height: 70px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
background: var(--color-surface);
|
||||
border-bottom: 1px solid rgb(202 210 197 / 18%);
|
||||
background: var(--color-surface);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.topbar a {
|
||||
@@ -221,26 +225,47 @@ summary:focus-visible {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.topbar a.topbar__account {
|
||||
margin-left: auto;
|
||||
margin-right: 20px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
.topbar > form {
|
||||
margin: 0 20px 0 auto;
|
||||
}
|
||||
|
||||
.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);
|
||||
color: var(--color-background);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.topbar button.topbar__account {
|
||||
appearance: none;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.topbar img {
|
||||
display: block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
/* Accueil et présentation
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -280,9 +305,14 @@ summary:focus-visible {
|
||||
display: inline-block;
|
||||
padding: 10px 16px;
|
||||
border: 2px solid var(--color-accent);
|
||||
border-radius: 0;
|
||||
background: var(--color-accent);
|
||||
color: var(--color-background);
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -308,7 +338,9 @@ summary:focus-visible {
|
||||
margin-top: 80px;
|
||||
padding: 20px 20px 40px;
|
||||
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 {
|
||||
@@ -326,34 +358,6 @@ summary:focus-visible {
|
||||
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 {
|
||||
padding: 20px;
|
||||
color: var(--color-text);
|
||||
@@ -370,7 +374,9 @@ summary:focus-visible {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Couleur des cartes selon leur position. */
|
||||
/* Cartes des services
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
.section-services .box:nth-child(3n + 1) {
|
||||
--service-accent: var(--color-link);
|
||||
}
|
||||
@@ -391,31 +397,85 @@ summary:focus-visible {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
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;
|
||||
}
|
||||
|
||||
.contact-form input,
|
||||
.rodauth .form-control,
|
||||
.contact-form input:not([type="submit"]),
|
||||
.contact-form textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
border: 0;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Ne pas appliquer le fond clair au bouton d'envoi. */
|
||||
.contact-form input:not([type="submit"]),
|
||||
.contact-form textarea {
|
||||
border: 1px solid var(--color-text);
|
||||
border-radius: 0;
|
||||
background: var(--color-text);
|
||||
color: var(--color-background);
|
||||
font-family: inherit;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.contact-form textarea {
|
||||
@@ -424,19 +484,83 @@ summary:focus-visible {
|
||||
box-shadow: 0 0 10px var(--color-surface-dark);
|
||||
}
|
||||
|
||||
.contact-form input:not([type="submit"]):focus-visible,
|
||||
.contact-form textarea:focus-visible {
|
||||
outline-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.submit {
|
||||
.rodauth .btn-primary,
|
||||
.contact-form .submit {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 10px 16px;
|
||||
border: 2px solid var(--color-accent);
|
||||
border-radius: 0;
|
||||
background: var(--color-accent);
|
||||
color: var(--color-background);
|
||||
font-family: inherit;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
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 {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
@@ -445,24 +569,129 @@ summary:focus-visible {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Notifications
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
.flash-container {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-block: 1.5rem;
|
||||
}
|
||||
|
||||
.flash {
|
||||
margin: 0 0 1rem;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 0.75rem 1rem;
|
||||
border-left: 3px solid;
|
||||
}
|
||||
|
||||
.flash--notice {
|
||||
background-color: var(--color-flash-notice-background);
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-flash-notice-background);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.flash--error {
|
||||
background-color: var(--color-flash-error-background);
|
||||
border-color: var(--color-flash-error-border);
|
||||
background: var(--color-flash-error-background);
|
||||
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
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
@@ -484,7 +713,7 @@ footer {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
/* Adaptations et accessibilité
|
||||
/* Adaptations pour les écrans larges
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
@media screen and (min-width: 1080px) {
|
||||
@@ -508,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) {
|
||||
.box__link:hover {
|
||||
transform: scale(1.04);
|
||||
@@ -519,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) {
|
||||
.box {
|
||||
margin-top: 60px;
|
||||
@@ -530,11 +826,30 @@ footer {
|
||||
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 {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Préférences d'accessibilité
|
||||
------------------------------------------------------------------------ */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
|
||||
@@ -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
|
||||
+4
-6
@@ -1,18 +1,16 @@
|
||||
class Netig
|
||||
hash_branch "contact" do |r|
|
||||
@page_title = "Contact — NETig"
|
||||
@page_description = "Contacter NETig pour toute question concernant ses services."
|
||||
@canonical_url = "https://netig.net/contact"
|
||||
|
||||
r.is do
|
||||
r.get do
|
||||
@page_title = "Contact — NETig"
|
||||
@page_description = "Contacter NETig pour toute question concernant ses services."
|
||||
@canonical_url = "https://netig.net/contact"
|
||||
|
||||
view "contact"
|
||||
end
|
||||
end
|
||||
|
||||
r.post "send" do
|
||||
check_csrf!
|
||||
|
||||
form = ContactForm.new(r.params)
|
||||
|
||||
if form.spam?
|
||||
|
||||
+135
@@ -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
|
||||
@@ -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
@@ -1,19 +1,24 @@
|
||||
<section class="page-section">
|
||||
<div class="content page-panel">
|
||||
<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>
|
||||
<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>
|
||||
<% 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">
|
||||
<%== csrf_tag("/contact/send") %>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<section class="page-section">
|
||||
<div class="content prose page-panel">
|
||||
<%== @document_html %>
|
||||
</div>
|
||||
</section>
|
||||
+5
-5
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
<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">
|
||||
@@ -43,7 +43,7 @@
|
||||
<section id="hosting" class="section-hosting">
|
||||
<div class="content">
|
||||
<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="box">
|
||||
<h3 class="box__title">NETmail — Adresse mail</h3>
|
||||
@@ -105,7 +105,7 @@
|
||||
</div>
|
||||
<p>
|
||||
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>
|
||||
</div>
|
||||
</section>
|
||||
@@ -113,7 +113,7 @@
|
||||
<section id="association" class="section-association">
|
||||
<div class="content">
|
||||
<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>
|
||||
<a class="button" href="/#association">Faire un don ➜</a>
|
||||
<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="/temporary_page">Faire un don ➜</a>
|
||||
</div>
|
||||
</section>
|
||||
+30
-4
@@ -1,4 +1,3 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -31,16 +30,39 @@
|
||||
<a class="skip-link" href="#main-content">Aller au contenu</a>
|
||||
|
||||
<header>
|
||||
<nav class="topbar" aria-label="Accès aux services">
|
||||
<nav class="topbar" aria-label="Navigation principale">
|
||||
<% unless request.path == "/" %>
|
||||
<a href="/">
|
||||
<img src="/images/favicon.svg" alt="Accueil NETig">
|
||||
</a>
|
||||
<% end %>
|
||||
|
||||
<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://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>
|
||||
</header>
|
||||
|
||||
@@ -51,12 +73,16 @@
|
||||
<footer>
|
||||
<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><a href="/statutes">Statuts</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>
|
||||
</div>
|
||||
|
||||
<div class="botbar">
|
||||
<p>* L'association NETig est en cours de constitution et n'est pas encore déclarée.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user