Преглед изворни кода

Advanced services status management

Miraty пре 2 година
родитељ
комит
abb9aabf5b
13 измењених фајлова са 143 додато и 109 уклоњено
  1. 9 12
      DOCS/configuration.md
  2. 6 6
      DOCS/translation.md
  3. 3 4
      config.ini
  4. 6 5
      db/schema.sql
  5. 19 0
      fn/ht.php
  6. 18 9
      locales/fr/C/LC_MESSAGES/messages.po
  7. 17 8
      locales/messages.pot
  8. 1 17
      pg-act/auth/register.php
  9. 23 13
      pg-act/auth/unregister.php
  10. 1 1
      pg-view/auth/index.php
  11. 13 22
      pg-view/index.php
  12. 24 12
      router.php
  13. 3 0
      view.php

+ 9 - 12
DOCS/configuration.md

@@ -32,6 +32,15 @@ String defining the displayed identity of the service.
 
 
 Pretty string sometimes prefixed to the service name. Can be empty.
 Pretty string sometimes prefixed to the service name. Can be empty.
 
 
+### `services[]`
+
+Keys `reg`, `ns` and `ht` are required.
+
+Values can be:
+* `enabled`: the service is provided as usual
+* `error`: the service is temporarily unavailable for maintenance/debugging
+* `disabled`: the service is ignored everywhere ; this installation never provides it
+
 ## `[dns]`
 ## `[dns]`
 
 
 This configuration section is used by both the registry (`reg`) and the public name server (`ns`).
 This configuration section is used by both the registry (`reg`) and the public name server (`ns`).
@@ -46,10 +55,6 @@ Filesystem path to the `kdig` binary. Used to authenticate resources possession
 
 
 ## `[reg]`
 ## `[reg]`
 
 
-### `enabled`
-
-Defines whether the web interface for this service is accessible.
-
 ### `suffixes[]`
 ### `suffixes[]`
 
 
 Lists the suffixes that the registry manages.
 Lists the suffixes that the registry manages.
@@ -78,10 +83,6 @@ Host where the Knot DNS server answers the registry values. Should be a secure (
 
 
 ## `[ns]`
 ## `[ns]`
 
 
-### `enabled`
-
-Defines whether the web interface for this service is accessible.
-
 ### `knot_zones_path`
 ### `knot_zones_path`
 
 
 Filesystem path to the zones directory. The full path to created zonefiles will be `knot_zones_path/<zone-apex-domain>.zone`.
 Filesystem path to the zones directory. The full path to created zonefiles will be `knot_zones_path/<zone-apex-domain>.zone`.
@@ -102,10 +103,6 @@ Administrator email address published in every SOA record. Ends with a `.`, `@`
 
 
 ## `[ht]`
 ## `[ht]`
 
 
-### `enabled`
-
-Defines whether the web interface for this service is accessible. (Users can still use SFTP.)
-
 ### `ht_path`
 ### `ht_path`
 
 
 Filesystem path to the users files base directory. Files of a user are located inside `ht_path/<their-internal-user-id>/`
 Filesystem path to the users files base directory. Files of a user are located inside `ht_path/<their-internal-user-id>/`

+ 6 - 6
DOCS/translation.md

@@ -3,13 +3,13 @@
 ## As a developer
 ## As a developer
 
 
 Extract messages to be translated from the source files and into a Portable Object Template file:
 Extract messages to be translated from the source files and into a Portable Object Template file:
-```
-xgettext --from-code=UTF-8 --no-wrap -d messages -p locales/ --from-code=UTF-8 *.php */*.php */*/*.php
+```shell
+xgettext --from-code=UTF-8 --no-wrap -d messages -p locales/ *.php */*.php */*/*.php
 mv locales/messages.po locales/messages.pot
 mv locales/messages.po locales/messages.pot
 ```
 ```
 
 
 Merge messages into existing Portable Objects:
 Merge messages into existing Portable Objects:
-```
+```shell
 msgmerge --no-wrap locales/fr/C/LC_MESSAGES/messages.po locales/messages.pot -o locales/fr/C/LC_MESSAGES/messages.po
 msgmerge --no-wrap locales/fr/C/LC_MESSAGES/messages.po locales/messages.pot -o locales/fr/C/LC_MESSAGES/messages.po
 ```
 ```
 
 
@@ -17,7 +17,7 @@ msgmerge --no-wrap locales/fr/C/LC_MESSAGES/messages.po locales/messages.pot -o
 
 
 ### To start a new translation
 ### To start a new translation
 
 
-```
+```shell
 mkdir -p locales/fr/C/LC_MESSAGES/
 mkdir -p locales/fr/C/LC_MESSAGES/
 msginit -i locales/messages.pot -o locales/fr/C/LC_MESSAGES/messages.po
 msginit -i locales/messages.pot -o locales/fr/C/LC_MESSAGES/messages.po
 ```
 ```
@@ -26,12 +26,12 @@ msginit -i locales/messages.pot -o locales/fr/C/LC_MESSAGES/messages.po
 
 
 Edit `locales/fr/C/LC_MESSAGES/messages.po` using either
 Edit `locales/fr/C/LC_MESSAGES/messages.po` using either
 * any text editor
 * any text editor
-* a dedicated translation software like [Poedit](https://poedit.net/), [KDE's Lokalize](https://apps.kde.org/lokalize/) or [GNOME Translation Editor](https://wiki.gnome.org/Apps/Gtranslator).
+* dedicated translation software like [Poedit](https://poedit.net/), [KDE's Lokalize](https://apps.kde.org/lokalize/) or [GNOME Translation Editor](https://wiki.gnome.org/Apps/Gtranslator).
 
 
 ## As an administrator
 ## As an administrator
 
 
 To compile Portable Objects into Machine Objects:
 To compile Portable Objects into Machine Objects:
-```
+```shell
 msgfmt locales/fr/C/LC_MESSAGES/messages.po -o locales/fr/C/LC_MESSAGES/messages.mo
 msgfmt locales/fr/C/LC_MESSAGES/messages.po -o locales/fr/C/LC_MESSAGES/messages.mo
 ```
 ```
 
 

+ 3 - 4
config.ini

@@ -6,13 +6,15 @@ public_domains[] = "servnest.test"
 prefix = ""
 prefix = ""
 service_name = "ServNest"
 service_name = "ServNest"
 service_emoji = "🪺"
 service_emoji = "🪺"
+services[reg] = "enabled"
+services[ns] = "enabled"
+services[ht] = "enabled"
 
 
 [dns]
 [dns]
 knotc_path = "/usr/sbin/knotc"
 knotc_path = "/usr/sbin/knotc"
 kdig_path = "/usr/bin/kdig"
 kdig_path = "/usr/bin/kdig"
 
 
 [reg]
 [reg]
-enabled = true
 suffixes[servnest.test.] = "approved"
 suffixes[servnest.test.] = "approved"
 suffixes[test.servnest.test.] = "all"
 suffixes[test.servnest.test.] = "all"
 suffixes[old.sernnest.test.] = "none"
 suffixes[old.sernnest.test.] = "none"
@@ -21,7 +23,6 @@ ttl = 86400
 address = "[::1]:42053"
 address = "[::1]:42053"
 
 
 [ns]
 [ns]
-enabled = true
 knot_zones_path = "/srv/servnest/ns"
 knot_zones_path = "/srv/servnest/ns"
 servers[] = "ns1.servnest.test."
 servers[] = "ns1.servnest.test."
 servers[] = "ns2.servnest.test."
 servers[] = "ns2.servnest.test."
@@ -29,8 +30,6 @@ kzonecheck_path = "/usr/bin/kzonecheck"
 public_soa_email = "hostmaster.invalid."
 public_soa_email = "hostmaster.invalid."
 
 
 [ht]
 [ht]
-enabled = true
-
 ht_path = "/srv/servnest/ht"
 ht_path = "/srv/servnest/ht"
 
 
 subpath_domain = "ht.servnest.test"
 subpath_domain = "ht.servnest.test"

+ 6 - 5
db/schema.sql

@@ -4,11 +4,11 @@ CREATE TABLE IF NOT EXISTS "params" (
 	"value" TEXT NOT NULL,
 	"value" TEXT NOT NULL,
 	PRIMARY KEY("name")
 	PRIMARY KEY("name")
 );
 );
-INSERT INTO "params"("name", "value") VALUES("instance_bucket_tokens", "0");
-INSERT INTO "params"("name", "value") VALUES("instance_bucket_last_update", "0");
-INSERT INTO "params"("name", "value") VALUES("secret_key", "0");
-INSERT INTO "params"("name", "value") VALUES("secret_key_last_change", "0");
-INSERT INTO "params"("name", "value") VALUES("username_salt", "00000000000000000000000000000000"); -- Should be unique and secret ; generate one using `openssl rand -hex 16` ; can't be changed without breaking current accounts login
+INSERT INTO "params"("name", "value") VALUES('instance_bucket_tokens', '0');
+INSERT INTO "params"("name", "value") VALUES('instance_bucket_last_update', '0');
+INSERT INTO "params"("name", "value") VALUES('secret_key', '0');
+INSERT INTO "params"("name", "value") VALUES('secret_key_last_change', '0');
+INSERT INTO "params"("name", "value") VALUES('username_salt', '00000000000000000000000000000000'); -- Should be unique and secret ; generate one using `openssl rand -hex 16` ; can't be changed without breaking current accounts login
 CREATE TABLE IF NOT EXISTS "users" (
 CREATE TABLE IF NOT EXISTS "users" (
 	"id"                 TEXT    NOT NULL UNIQUE,
 	"id"                 TEXT    NOT NULL UNIQUE,
 	"username"           TEXT    NOT NULL UNIQUE,
 	"username"           TEXT    NOT NULL UNIQUE,
@@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS "users" (
 	"bucket_tokens"      INTEGER NOT NULL,
 	"bucket_tokens"      INTEGER NOT NULL,
 	"bucket_last_update" INTEGER NOT NULL,
 	"bucket_last_update" INTEGER NOT NULL,
 	"type"               TEXT    NOT NULL,
 	"type"               TEXT    NOT NULL,
+	"services"           TEXT    NOT NULL,
 	PRIMARY KEY("id")
 	PRIMARY KEY("id")
 );
 );
 CREATE TABLE IF NOT EXISTS "approval-keys" (
 CREATE TABLE IF NOT EXISTS "approval-keys" (

+ 19 - 0
fn/ht.php

@@ -1,5 +1,24 @@
 <?php
 <?php
 
 
+function htSetupUserFs($id) {
+	// Setup SFTP directory
+	umask(0002);
+	if (mkdir(CONF['ht']['ht_path'] . '/' . $id, 0775) !== true)
+		output(500, 'Can\'t create user directory.');
+	exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['chgrp_path'] . ' ' . CONF['ht']['sftpgo_group'] . ' ' . CONF['ht']['ht_path'] . '/' . $id . ' --no-dereference', result_code: $code);
+	if ($code !== 0)
+		output(500, 'Can\'t change user directory group.');
+
+	// Setup Tor config directory
+	if (mkdir(CONF['ht']['tor_config_path'] . '/' . $id, 0755) !== true)
+		output(500, 'Can\'t create Tor config directory.');
+
+	// Setup Tor keys directory
+	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['mkdir_path'] . ' --mode=0700 ' . CONF['ht']['tor_keys_path'] . '/' . $id, result_code: $code);
+	if ($code !== 0)
+		output(500, 'Can\'t create Tor keys directory.');
+}
+
 function checkDomainFormat($domain) {
 function checkDomainFormat($domain) {
 	// If the domain must end without a dot
 	// If the domain must end without a dot
 	if (!filter_var($domain, FILTER_VALIDATE_DOMAIN) OR !preg_match('/^([a-z0-9_-]{1,63}\.){1,126}[a-z0-9]{1,63}$/D', $domain))
 	if (!filter_var($domain, FILTER_VALIDATE_DOMAIN) OR !preg_match('/^([a-z0-9_-]{1,63}\.){1,126}[a-z0-9]{1,63}$/D', $domain))

+ 18 - 9
locales/fr/C/LC_MESSAGES/messages.po

@@ -1,7 +1,7 @@
 msgid ""
 msgid ""
 msgstr ""
 msgstr ""
-"Content-Type: text/plain; charset=UTF-8\n"
 "Language: fr\n"
 "Language: fr\n"
+"Content-Type: text/plain; charset=UTF-8\n"
 
 
 #: pages.php:9
 #: pages.php:9
 msgid "Authentication"
 msgid "Authentication"
@@ -272,11 +272,15 @@ msgstr "Supprimer un accès"
 msgid "Delete an existing HTTP access from a subdirectory of the SFTP space"
 msgid "Delete an existing HTTP access from a subdirectory of the SFTP space"
 msgstr "Retirer un accès HTTP existant d'un sous-dossier de l'espace SFTP"
 msgstr "Retirer un accès HTTP existant d'un sous-dossier de l'espace SFTP"
 
 
-#: router.php:134
+#: router.php:137 view.php:39
+msgid "This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it."
+msgstr "Ce service est en cours de maintenance. Aucune action ne peut être effectuée avant qu'ane administrataire termine de le réparer."
+
+#: router.php:147
 msgid "You need to be logged in to do this."
 msgid "You need to be logged in to do this."
 msgstr "Vous devez être connecté·e à un compte pour faire cela."
 msgstr "Vous devez être connecté·e à un compte pour faire cela."
 
 
-#: router.php:136
+#: router.php:149
 msgid "This account doesn't exist anymore. Log out to end this ghost session."
 msgid "This account doesn't exist anymore. Log out to end this ghost session."
 msgstr "Ce compte n'existe plus. Déconnectez-vous pour terminer cette session fantôme."
 msgstr "Ce compte n'existe plus. Déconnectez-vous pour terminer cette session fantôme."
 
 
@@ -284,12 +288,12 @@ msgstr "Ce compte n'existe plus. Déconnectez-vous pour terminer cette session f
 msgid "Anonymous"
 msgid "Anonymous"
 msgstr "Anonyme"
 msgstr "Anonyme"
 
 
-#: view.php:41
+#: view.php:44
 #, php-format
 #, php-format
 msgid "This form won't be accepted because you need to %slog in%s first."
 msgid "This form won't be accepted because you need to %slog in%s first."
 msgstr "Ce forumulaire ne sera pas accepté car il faut %sse connecter%s d'abord."
 msgstr "Ce forumulaire ne sera pas accepté car il faut %sse connecter%s d'abord."
 
 
-#: view.php:48
+#: view.php:51
 #, php-format
 #, php-format
 msgid "%sSource code%s available under %s."
 msgid "%sSource code%s available under %s."
 msgstr "%sCode source%s disponible sous %s."
 msgstr "%sCode source%s disponible sous %s."
@@ -314,7 +318,7 @@ msgstr "<strong>Erreur de l'utilisataire</strong>&nbsp;: "
 msgid "<strong>Server error</strong>: "
 msgid "<strong>Server error</strong>: "
 msgstr "<strong>Erreur du serveur</strong>&nbsp;: "
 msgstr "<strong>Erreur du serveur</strong>&nbsp;: "
 
 
-#: fn/common.php:133
+#: fn/common.php:129
 msgid "Wrong proof."
 msgid "Wrong proof."
 msgstr "Preuve incorrecte."
 msgstr "Preuve incorrecte."
 
 
@@ -322,7 +326,7 @@ msgstr "Preuve incorrecte."
 msgid "IP address malformed."
 msgid "IP address malformed."
 msgstr "Adresse IP malformée."
 msgstr "Adresse IP malformée."
 
 
-#: fn/dns.php:67 fn/ht.php:6
+#: fn/dns.php:67 fn/ht.php:25
 msgid "Domain malformed."
 msgid "Domain malformed."
 msgstr "Domaine malformé."
 msgstr "Domaine malformé."
 
 
@@ -372,7 +376,12 @@ msgstr "Cet identifiant est déjà pris."
 msgid "Account deletion must be confirmed."
 msgid "Account deletion must be confirmed."
 msgstr "La suppression du compte doit être confirmée."
 msgstr "La suppression du compte doit être confirmée."
 
 
-#: pg-act/auth/unregister.php:29
+#: pg-act/auth/unregister.php:10
+#, php-format
+msgid "Your account can't be deleted because the %s service is currently unavailable."
+msgstr "Votre compte ne peut pas être supprimé car le service %s est actuellement indisponible."
+
+#: pg-act/auth/unregister.php:39
 msgid "Account deleted."
 msgid "Account deleted."
 msgstr "Compte supprimé."
 msgstr "Compte supprimé."
 
 
@@ -566,7 +575,7 @@ msgid "Approved"
 msgstr "Approuvé"
 msgstr "Approuvé"
 
 
 #: pg-view/auth/index.php:27
 #: pg-view/auth/index.php:27
-msgid "It was originally a testing account, but has been approved by an administrator, and is suited for stable usecases:"
+msgid "It was originally a testing account, but has been approved by an administrator, and is suitable for stable use cases:"
 msgstr "C'est originellement un compte de test, mais qui a été approuvé par ane administrataire, et qui permet une utilisation stable&nbsp;:"
 msgstr "C'est originellement un compte de test, mais qui a été approuvé par ane administrataire, et qui permet une utilisation stable&nbsp;:"
 
 
 #: pg-view/auth/index.php:30
 #: pg-view/auth/index.php:30

+ 17 - 8
locales/messages.pot

@@ -271,11 +271,15 @@ msgstr ""
 msgid "Delete an existing HTTP access from a subdirectory of the SFTP space"
 msgid "Delete an existing HTTP access from a subdirectory of the SFTP space"
 msgstr ""
 msgstr ""
 
 
-#: router.php:134
+#: router.php:137 view.php:39
+msgid "This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it."
+msgstr ""
+
+#: router.php:147
 msgid "You need to be logged in to do this."
 msgid "You need to be logged in to do this."
 msgstr ""
 msgstr ""
 
 
-#: router.php:136
+#: router.php:149
 msgid "This account doesn't exist anymore. Log out to end this ghost session."
 msgid "This account doesn't exist anymore. Log out to end this ghost session."
 msgstr ""
 msgstr ""
 
 
@@ -283,12 +287,12 @@ msgstr ""
 msgid "Anonymous"
 msgid "Anonymous"
 msgstr ""
 msgstr ""
 
 
-#: view.php:41
+#: view.php:44
 #, php-format
 #, php-format
 msgid "This form won't be accepted because you need to %slog in%s first."
 msgid "This form won't be accepted because you need to %slog in%s first."
 msgstr ""
 msgstr ""
 
 
-#: view.php:48
+#: view.php:51
 #, php-format
 #, php-format
 msgid "%sSource code%s available under %s."
 msgid "%sSource code%s available under %s."
 msgstr ""
 msgstr ""
@@ -313,7 +317,7 @@ msgstr ""
 msgid "<strong>Server error</strong>: "
 msgid "<strong>Server error</strong>: "
 msgstr ""
 msgstr ""
 
 
-#: fn/common.php:133
+#: fn/common.php:129
 msgid "Wrong proof."
 msgid "Wrong proof."
 msgstr ""
 msgstr ""
 
 
@@ -321,7 +325,7 @@ msgstr ""
 msgid "IP address malformed."
 msgid "IP address malformed."
 msgstr ""
 msgstr ""
 
 
-#: fn/dns.php:67 fn/ht.php:6
+#: fn/dns.php:67 fn/ht.php:25
 msgid "Domain malformed."
 msgid "Domain malformed."
 msgstr ""
 msgstr ""
 
 
@@ -371,7 +375,12 @@ msgstr ""
 msgid "Account deletion must be confirmed."
 msgid "Account deletion must be confirmed."
 msgstr ""
 msgstr ""
 
 
-#: pg-act/auth/unregister.php:29
+#: pg-act/auth/unregister.php:10
+#, php-format
+msgid "Your account can't be deleted because the %s service is currently unavailable."
+msgstr ""
+
+#: pg-act/auth/unregister.php:39
 msgid "Account deleted."
 msgid "Account deleted."
 msgstr ""
 msgstr ""
 
 
@@ -565,7 +574,7 @@ msgid "Approved"
 msgstr ""
 msgstr ""
 
 
 #: pg-view/auth/index.php:27
 #: pg-view/auth/index.php:27
-msgid "It was originally a testing account, but has been approved by an administrator, and is suited for stable usecases:"
+msgid "It was originally a testing account, but has been approved by an administrator, and is suitable for stable use cases:"
 msgstr ""
 msgstr ""
 
 
 #: pg-view/auth/index.php:30
 #: pg-view/auth/index.php:30

+ 1 - 17
pg-act/auth/register.php

@@ -21,25 +21,9 @@ insert('users', [
 	'bucket_tokens' => 0,
 	'bucket_tokens' => 0,
 	'bucket_last_update' => 0,
 	'bucket_last_update' => 0,
 	'type' => 'testing',
 	'type' => 'testing',
+	'services' => '',
 ]);
 ]);
 
 
-// Setup SFTP directory
-umask(0002);
-if (mkdir(CONF['ht']['ht_path'] . '/' . $id, 0775) !== true)
-	output(500, 'Can\'t create user directory.');
-exec(CONF['ht']['sudo_path'] . ' ' . CONF['ht']['chgrp_path'] . ' ' . CONF['ht']['sftpgo_group'] . ' ' . CONF['ht']['ht_path'] . '/' . $id . ' --no-dereference', result_code: $code);
-if ($code !== 0)
-	output(500, 'Can\'t change user directory group.');
-
-// Setup Tor config directory
-if (mkdir(CONF['ht']['tor_config_path'] . '/' . $id, 0755) !== true)
-	output(500, 'Can\'t create Tor config directory.');
-
-// Setup Tor keys directory
-exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['mkdir_path'] . ' --mode=0700 ' . CONF['ht']['tor_keys_path'] . '/' . $id, result_code: $code);
-if ($code !== 0)
-	output(500, 'Can\'t create Tor keys directory.');
-
 stopSession();
 stopSession();
 startSession();
 startSession();
 
 

+ 23 - 13
pg-act/auth/unregister.php

@@ -3,24 +3,34 @@
 if (!isset($_POST['delete']))
 if (!isset($_POST['delete']))
 	output(403, _('Account deletion must be confirmed.'));
 	output(403, _('Account deletion must be confirmed.'));
 
 
-foreach (query('select', 'registry', ['username' => $_SESSION['id']], 'domain') as $domain)
-	regDeleteDomain($domain);
+$user_services = explode(',', query('select', 'users', ['username' => $_SESSION['id']], 'services')[0]);
 
 
-foreach (query('select', 'zones', ['username' => $_SESSION['id']], 'zone') as $zone)
-	nsDeleteZone($zone);
+foreach (SERVICES_USER as $service)
+	if (in_array($service, $user_services, true) AND CONF['common']['services'][$service] !== 'enabled')
+		output(503, sprintf(_('Your account can\'t be deleted because the %s service is currently unavailable.'), '<em>' . PAGES[$service]['index']['title'] . '</em>'));
 
 
-foreach (query('select', 'sites', ['username' => $_SESSION['id']]) as $site)
-	htDeleteSite($site['address'], $site['type']);
+if (in_array('reg', $user_services, true))
+	foreach (query('select', 'registry', ['username' => $_SESSION['id']], 'domain') as $domain)
+		regDeleteDomain($domain);
 
 
-exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'], result_code: $code);
-if ($code !== 0)
-	output(500, 'Can\'t remove Tor keys directory.');
+if (in_array('ns', $user_services, true))
+	foreach (query('select', 'zones', ['username' => $_SESSION['id']], 'zone') as $zone)
+		nsDeleteZone($zone);
 
 
-removeDirectory(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id']);
+if (in_array('ht', $user_services, true)) {
+	foreach (query('select', 'sites', ['username' => $_SESSION['id']]) as $site)
+		htDeleteSite($site['address'], $site['type']);
 
 
-exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['sftpgo_user'] .  ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'], result_code: $code);
-if ($code !== 0)
-	output(500, 'Can\'t remove user\'s directory.');
+	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['tor_user'] . ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['tor_keys_path'] . '/' . $_SESSION['id'], result_code: $code);
+	if ($code !== 0)
+		output(500, 'Can\'t remove Tor keys directory.');
+
+	removeDirectory(CONF['ht']['tor_config_path'] . '/' . $_SESSION['id']);
+
+	exec(CONF['ht']['sudo_path'] . ' -u ' . CONF['ht']['sftpgo_user'] .  ' ' . CONF['ht']['rm_path'] . ' --recursive ' . CONF['ht']['ht_path'] . '/' . $_SESSION['id'], result_code: $code);
+	if ($code !== 0)
+		output(500, 'Can\'t remove user\'s directory.');
+}
 
 
 query('delete', 'users', ['id' => $_SESSION['id']]);
 query('delete', 'users', ['id' => $_SESSION['id']]);
 
 

+ 1 - 1
pg-view/auth/index.php

@@ -24,7 +24,7 @@
 	</dd>
 	</dd>
 	<dt><span aria-hidden="true">👤 </span><em><?= _('Approved') ?></em></dt>
 	<dt><span aria-hidden="true">👤 </span><em><?= _('Approved') ?></em></dt>
 	<dd>
 	<dd>
-		<?= _('It was originally a testing account, but has been approved by an administrator, and is suited for stable usecases:') ?>
+		<?= _('It was originally a testing account, but has been approved by an administrator, and is suitable for stable use cases:') ?>
 		<ul>
 		<ul>
 			<li><?= sprintf(_('%s of SFTP quota'), ((CONF['ht']['user_quota_approved'] >> 30) >= 1) ? CONF['ht']['user_quota_approved'] >> 30 . ' ' . _('<abbr title="gibibyte">GiB</abbr>') : CONF['ht']['user_quota_approved'] >> 20 . ' ' . _('<abbr title="mebibyte">MiB</abbr>')) ?></li>
 			<li><?= sprintf(_('%s of SFTP quota'), ((CONF['ht']['user_quota_approved'] >> 30) >= 1) ? CONF['ht']['user_quota_approved'] >> 30 . ' ' . _('<abbr title="gibibyte">GiB</abbr>') : CONF['ht']['user_quota_approved'] >> 20 . ' ' . _('<abbr title="mebibyte">MiB</abbr>')) ?></li>
 			<li><?= _('Stable Let\'s Encrypt certificates') ?></li>
 			<li><?= _('Stable Let\'s Encrypt certificates') ?></li>

+ 13 - 22
pg-view/index.php

@@ -1,26 +1,17 @@
 <nav>
 <nav>
 	<dl>
 	<dl>
-		<dt><a class="auth" href="auth/"><?= PAGES['auth']['index']['title'] ?></a></dt>
-		<dd>
-			<?= PAGES['auth']['index']['description'] ?>
-		</dd>
-	<?php if (CONF['reg']['enabled'] === true) { ?>
-		<dt><a class="reg" href="reg/"><?= PAGES['reg']['index']['title'] ?></code></a></dt>
-		<dd>
-			<?= PAGES['reg']['index']['description'] ?>
-		</dd>
-	<?php } ?>
-	<?php if (CONF['ns']['enabled'] === true) { ?>
-		<dt><a class="ns" href="ns/"><?= PAGES['ns']['index']['title'] ?></a></dt>
-		<dd>
-			<?= PAGES['ns']['index']['description'] ?>
-		</dd>
-	<?php } ?>
-	<?php if (CONF['ht']['enabled'] === true) { ?>
-		<dt><a class="ht" href="ht/"><?= PAGES['ht']['index']['title'] ?></a></dt>
-		<dd>
-			<?= PAGES['ht']['index']['description'] ?>
-		</dd>
-	<?php } ?>
+<?php
+
+foreach (array_merge(['auth' => 'enabled'], CONF['common']['services']) as $service => $status) {
+	if ($status !== 'enabled' AND $status !== 'error')
+		continue;
+?>
+		<?= ($status === 'error') ? '<s>' : '' ?>
+			<dt><a class="<?= $service ?>" href="<?= $service ?>/"><?= PAGES[$service]['index']['title'] ?></a></dt>
+			<dd>
+				<?= PAGES[$service]['index']['description'] ?>
+			</dd>
+		<?= ($status === 'error') ? '</s>' : '' ?>
+<?php } ?>
 	</dl>
 	</dl>
 </nav>
 </nav>

+ 24 - 12
router.php

@@ -15,13 +15,15 @@ setlocale(LC_MESSAGES, 'C.UTF-8');
 bindtextdomain('messages', 'locales/' . LOCALE);
 bindtextdomain('messages', 'locales/' . LOCALE);
 header('Content-Language: ' . LOCALE);
 header('Content-Language: ' . LOCALE);
 
 
+const SERVICES_USER = ['reg', 'ns', 'ht'];
+
 const LF = "\n";
 const LF = "\n";
 
 
 const PLACEHOLDER_DOMAIN = 'example'; // From RFC2606: Reserved Top Level DNS Names > 2. TLDs for Testing, & Documentation Examples
 const PLACEHOLDER_DOMAIN = 'example'; // From RFC2606: Reserved Top Level DNS Names > 2. TLDs for Testing, & Documentation Examples
 const PLACEHOLDER_IPV6 = '2001:db8::3'; // From RFC3849: IPv6 Address Prefix Reserved for Documentation
 const PLACEHOLDER_IPV6 = '2001:db8::3'; // From RFC3849: IPv6 Address Prefix Reserved for Documentation
 const PLACEHOLDER_IPV4 = '203.0.113.42'; // From RFC5737: IPv4 Address Blocks Reserved for Documentation
 const PLACEHOLDER_IPV4 = '203.0.113.42'; // From RFC5737: IPv4 Address Blocks Reserved for Documentation
 
 
-foreach (array_diff(scandir(CONF['common']['root_path'] . '/fn'), array('..', '.')) as $file)
+foreach (array_diff(scandir(CONF['common']['root_path'] . '/fn'), ['..', '.']) as $file)
 	require CONF['common']['root_path'] . '/fn/' . $file;
 	require CONF['common']['root_path'] . '/fn/' . $file;
 
 
 require 'pages.php';
 require 'pages.php';
@@ -38,7 +40,6 @@ define('PAGE_URL', $pageAddress);
 define('PAGE_ADDRESS', $pageAddress . ((substr($pageAddress, -1) === '/' OR $pageAddress === '') ? 'index' : ''));
 define('PAGE_ADDRESS', $pageAddress . ((substr($pageAddress, -1) === '/' OR $pageAddress === '') ? 'index' : ''));
 define('PAGE_LINEAGE', explode('/', PAGE_ADDRESS));
 define('PAGE_LINEAGE', explode('/', PAGE_ADDRESS));
 define('SERVICE', dirname(PAGE_ADDRESS));
 define('SERVICE', dirname(PAGE_ADDRESS));
-define('PAGE', basename(PAGE_ADDRESS, '.php'));
 
 
 function getPageInformations($pages, $pageElements) {
 function getPageInformations($pages, $pageElements) {
 	if (!isset($pages['index']) OR $pageElements[0] === 'index')
 	if (!isset($pages['index']) OR $pageElements[0] === 'index')
@@ -87,6 +88,7 @@ if (isset($_COOKIE[SESSION_COOKIE_NAME]))
 	startSession(); // Resume session
 	startSession(); // Resume session
 
 
 if (isset($_SESSION['id'])) {
 if (isset($_SESSION['id'])) {
+	// Decrypt display username
 	if (!isset($_COOKIE['display-username-decryption-key']))
 	if (!isset($_COOKIE['display-username-decryption-key']))
 		output(403, 'The display username decryption key has not been sent.');
 		output(403, 'The display username decryption key has not been sent.');
 	$decryption_result = htmlspecialchars(sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
 	$decryption_result = htmlspecialchars(sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
@@ -98,17 +100,18 @@ if (isset($_SESSION['id'])) {
 	if ($decryption_result === false)
 	if ($decryption_result === false)
 		output(403, 'Unable to decrypt display username.');
 		output(403, 'Unable to decrypt display username.');
 	define('DISPLAY_USERNAME', $decryption_result);
 	define('DISPLAY_USERNAME', $decryption_result);
-}
 
 
-if (in_array(SERVICE, ['reg', 'ns', 'ht']) AND CONF[SERVICE]['enabled'] !== true)
-	output(403, 'Ce service est désactivé.');
+	// Enable not already enabled services for this user
+	$user_services = array_filter(explode(',', query('select', 'users', ['id' => $_SESSION['id']], 'services')[0]));
+	if (in_array(SERVICE, SERVICES_USER, true) AND !in_array(SERVICE, $user_services, true) AND CONF['common']['services'][SERVICE] === 'enabled') {
+		$user_services[] = SERVICE;
 
 
-// Protect against cross-site request forgery if a POST request is received
-if ($_POST !== []) {
-	if (isset($_SERVER['HTTP_SEC_FETCH_SITE']) !== true)
-		output(403, 'The <code>Sec-Fetch-Site</code> HTTP header is required when submitting a POST request to prevent Cross-Site Request Forgery (<abbr>CSRF</abbr>).');
-	if ($_SERVER['HTTP_SEC_FETCH_SITE'] !== 'same-origin')
-		output(403, 'The <code>Sec-Fetch-Site</code> HTTP header must be <code>same-origin</code> when submitting a POST request to prevent Cross-Site Request Forgery (<abbr>CSRF</abbr>).');
+		DB->prepare('UPDATE users SET services = :services WHERE id = :id')
+		->execute([':services' => implode(',', $user_services), ':id' => $_SESSION['id']]);
+
+		if (SERVICE === 'ht')
+			htSetupUserFs($_SESSION['id']);
+	}
 }
 }
 
 
 if (isset($_SERVER['SERVER_NAME']) !== true)
 if (isset($_SERVER['SERVER_NAME']) !== true)
@@ -125,18 +128,27 @@ function displayFinalMessage($data) {
 }
 }
 
 
 if ($_POST !== []) {
 if ($_POST !== []) {
+	if (in_array(SERVICE, SERVICES_USER, true) AND CONF['common']['services'][SERVICE] !== 'enabled')
+		output(503, _('This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it.'));
+
+	// Protect against cross-site request forgery if a POST request is received
+	if (isset($_SERVER['HTTP_SEC_FETCH_SITE']) !== true)
+		output(403, 'The <code>Sec-Fetch-Site</code> HTTP header is required when submitting a POST request to prevent Cross-Site Request Forgery (<abbr>CSRF</abbr>).');
+	if ($_SERVER['HTTP_SEC_FETCH_SITE'] !== 'same-origin')
+		output(403, 'The <code>Sec-Fetch-Site</code> HTTP header must be <code>same-origin</code> when submitting a POST request to prevent Cross-Site Request Forgery (<abbr>CSRF</abbr>).');
+
 	if (PAGE_METADATA['require-login'] ?? true !== false) {
 	if (PAGE_METADATA['require-login'] ?? true !== false) {
 		if (isset($_SESSION['id']) !== true)
 		if (isset($_SESSION['id']) !== true)
 			output(403, _('You need to be logged in to do this.'));
 			output(403, _('You need to be logged in to do this.'));
 		if (isset(query('select', 'users', ['id' => $_SESSION['id']], 'id')[0]) !== true)
 		if (isset(query('select', 'users', ['id' => $_SESSION['id']], 'id')[0]) !== true)
 			output(403, _('This account doesn\'t exist anymore. Log out to end this ghost session.'));
 			output(403, _('This account doesn\'t exist anymore. Log out to end this ghost session.'));
 	}
 	}
+
 	if (file_exists('pg-act/' . PAGE_ADDRESS . '.php'))
 	if (file_exists('pg-act/' . PAGE_ADDRESS . '.php'))
 		require 'pg-act/' . PAGE_ADDRESS . '.php';
 		require 'pg-act/' . PAGE_ADDRESS . '.php';
 }
 }
 
 
 function displayPage($data) {
 function displayPage($data) {
-
 	require 'view.php';
 	require 'view.php';
 	exit();
 	exit();
 }
 }

+ 3 - 0
view.php

@@ -35,6 +35,9 @@
 		<main>
 		<main>
 <?php
 <?php
 
 
+if (in_array(SERVICE, SERVICES_USER, true) AND CONF['common']['services'][SERVICE] === 'error')
+	echo '<p><strong>' . _('This service is currently under maintenance. No action can be taken on it until an administrator finishes repairing it.') . '</strong></p>';
+
 	require 'pg-view/' . PAGE_ADDRESS . '.php';
 	require 'pg-view/' . PAGE_ADDRESS . '.php';
 
 
 	if ($_POST === [] AND PAGE_METADATA['require-login'] ?? true !== false AND !isset($_SESSION['id']) AND PAGE_TERMINAL)
 	if ($_POST === [] AND PAGE_METADATA['require-login'] ?? true !== false AND !isset($_SESSION['id']) AND PAGE_TERMINAL)