Compare commits
92 commits
Author | SHA1 | Date | |
---|---|---|---|
507e8877c8 | |||
44c0e27a72 | |||
d1c112f12a | |||
fd4d721be4 | |||
d419aea412 | |||
11504c00e0 | |||
b1fd3fccae | |||
8d67cb340c | |||
730a023e55 | |||
e354b5b14b | |||
002a0c9ef2 | |||
fafe2bf3fe | |||
7a4839541d | |||
15c73715a8 | |||
8fc232d022 | |||
b45e65d427 | |||
ade36cf053 | |||
8d6a68b33c | |||
9612086790 | |||
05a15b1680 | |||
162cdad7fe | |||
7fbfcf485c | |||
d6a4a6591a | |||
83a80c0969 | |||
9fecf9ace9 | |||
0d38e2f2d4 | |||
112ea773a2 | |||
c24b98bcca | |||
5a521b0497 | |||
1251b431a6 | |||
6e231a73b6 | |||
240baca044 | |||
fcc9ceaedc | |||
8276b35a01 | |||
c1191f3f45 | |||
3d6b9f96e8 | |||
255cf8ba9e | |||
31bf380e12 | |||
3b82283cfd | |||
329fe20553 | |||
ae439b25bb | |||
e012505247 | |||
554cbd25ab | |||
cc8a7a90d8 | |||
0d273bcad2 | |||
84a52032fe | |||
564a138480 | |||
fa368044be | |||
8097745f01 | |||
fa9cd82531 | |||
860d14bcd0 | |||
6b61ada515 | |||
79eb469b20 | |||
9fac0811a0 | |||
e2fdf128ec | |||
6db862848f | |||
3b764f2ac7 | |||
151ad7b5aa | |||
6bfd488036 | |||
d4c1979e79 | |||
6760517c70 | |||
7dafaacee0 | |||
f07c8f960f | |||
06b47ed63e | |||
23f1f9d1c9 | |||
fc079635db | |||
8dd5486250 | |||
cf26c8bd44 | |||
50f3ed4e90 | |||
cab8136e1e | |||
ff233aa5c9 | |||
e496e5ae9a | |||
5bd21a5782 | |||
a69642ac11 | |||
e79f86ada9 | |||
c71a936f8d | |||
0bb3584921 | |||
fdd6b61d08 | |||
5bf70566ac | |||
6a9465d579 | |||
e4cab99f9e | |||
dfb8a9428b | |||
c3c8f7af64 | |||
45b5cc16a2 | |||
abd6c91013 | |||
9f304c4069 | |||
cba8b52b32 | |||
4a1e9cca7e | |||
3324315d8a | |||
ec4b587a31 | |||
6d52e01ba1 | |||
f5fa03fdaa |
1
.gitignore
vendored
|
@ -11,4 +11,3 @@ Cargo.lock
|
|||
**/*.rs.bk
|
||||
|
||||
/config.toml
|
||||
/db/sncf.sqlite
|
||||
|
|
27
Cargo.toml
|
@ -1,23 +1,32 @@
|
|||
[package]
|
||||
name = "sncf"
|
||||
version = "1.0.0"
|
||||
version = "1.5.0"
|
||||
authors = ["Association 42l <contact@noreply.example.org>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = [ "diesel/postgres" ]
|
||||
postgres = [ "diesel/postgres" ]
|
||||
mysql = [ "diesel/mysql" ]
|
||||
sqlite = [ "diesel/sqlite" ]
|
||||
|
||||
[dependencies]
|
||||
actix-rt = "1.0.0"
|
||||
actix-web = "3.0.0-beta.3"
|
||||
actix-files = "0.3.0-beta.1"
|
||||
diesel = { version = "1.4", features = ["sqlite", "r2d2", "chrono"] }
|
||||
actix-rt = "2.2.0"
|
||||
actix-web = "3.3.2"
|
||||
actix-files = "0.5.0"
|
||||
actix-session = "0.4"
|
||||
diesel = { version = "1.4", features = ["r2d2", "chrono"] }
|
||||
diesel_migrations = "1.4"
|
||||
url = "2.0"
|
||||
url = "2.2"
|
||||
toml = "0.5"
|
||||
lazy_static = "1.4"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
regex = "1.3"
|
||||
base64 = "0.12"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
base64 = "0.13"
|
||||
rand = "0.8"
|
||||
askama = "0.10"
|
||||
csrf = "0.4"
|
||||
percent-encoding = "2.1"
|
||||
|
|
21
README.md
|
@ -1,5 +1,8 @@
|
|||
# sncf
|
||||
|
||||
**Warning: Breaking changes introduced on a minor Nextcloud release (>= 22.3.0) broke sncf. Please do not update until it is fixed. It seems easy to fix (use `/login` instead of `/csrftoken`) but I need time, feel free to try to fix it.**
|
||||
|
||||
|
||||
Simple Nextcloud Forms (sncf) is a lightweight proxy written in Rust with the [Actix](https://actix.rs) framework.
|
||||
|
||||
It is meant to make form creation easier, through the use of the [Nextcloud Forms](https://github.com/nextcloud/forms) application, by generating administration links for forms: **users do not need to log in or register**, they just need to keep a link (in the form of `https://your-instance.com/admin/<45-byte base64 key>`) to log them in and give them access to their forms.
|
||||
|
@ -9,7 +12,7 @@ It is meant to make form creation easier, through the use of the [Nextcloud Form
|
|||
I really used black voodoo magic on this one.
|
||||
|
||||
This software acts as a **proxy** between the client and the Nextcloud instance. Here are some of its features :
|
||||
- When a link is created from the main page, sncf connects to the Nextcloud API and creates an account with a random username and password. Those credentials are stored in its SQLite database, along with a randomly-generated token.
|
||||
- When a link is created from the main page, sncf connects to the Nextcloud API and creates an account with a random username and password. Those credentials are stored in its SQLite database, along with a randomly-generated token (used in the administration link).
|
||||
- When an administration link is used, sncf uses its database to find the associated username and password, then fills the login form on the Nextcloud instance (taking in account its CSRF token) and proxies the generated `Set-Cookie` headers to the client (to log the user in), then redirects it to the Forms app.
|
||||
- When a form is created, sncf automatically forges a request to update some fields in the form (set isAnonymous to true, for instance). Those parameters can't be changed by the client.
|
||||
- When a form is updated, sncf parses the requests before proxying it in order to prevent the client to edit some specific fields (isAnonymous or form access policy, which must not set to allow the users of the same instance to see the form). If an unwanted request is made, sncf does not proxy it.
|
||||
|
@ -19,7 +22,7 @@ Those tweaks are completed by server-side CSS edits (using an application) to hi
|
|||
|
||||
### Setup
|
||||
|
||||
See the [dedicated wiki page](wiki/Setting-up-Nextcloud-and-sncf).
|
||||
See the [dedicated wiki page](https://git.42l.fr/neil/sncf/wiki/Setting-up-Nextcloud-and-sncf).
|
||||
|
||||
Note: There is currently no script to make the installation easier (see #12).
|
||||
|
||||
|
@ -30,6 +33,14 @@ Compatibility with sncf has been tested for the following Nextcloud and Nextclou
|
|||
| sncf | Nextcloud | Nextcloud Forms |
|
||||
|--------------|------------|------------------|
|
||||
| 1.0.0 | 19.0.1, 19.0.2 | 2.0.2, 2.0.3 |
|
||||
| 1.0.1, 1.0.2, 1.1.0, 1.2.0 | 19.0.1, 19.0.2, 20.0.0\*, 20.0.1 | 2.0.4 |
|
||||
| **Unsupported** \*\* | above 20.0.1, below 21.x | above 2.0.4, below 2.2.2
|
||||
| 1.3.0, 1.4.0 | 21.0.0 | 2.2.2, 2.2.3, 2.2.4 |
|
||||
| 1.5.0 | 22.2.0 | 2.3.0 |
|
||||
|
||||
|
||||
\* Breaking changes, please check [the wiki](https://git.42l.fr/neil/sncf/wiki/Upgrade-from-a-previous-version) if you need to upgrade from a previous version.
|
||||
\*\* Untested versions, use at your own risk.
|
||||
|
||||
If your NC or NC Forms version isn't in this list, sncf **may** or **may not** work. We **do not** ensure backwards compatibility with older versions.
|
||||
|
||||
|
@ -41,11 +52,11 @@ If you upgrade anyway and notice a breaking change, please file an issue.
|
|||
|
||||
#### Donations
|
||||
|
||||
If you like this work, please donate to the [42l association](https://42l.fr) (maintaining sncf) or [Nextcloud](https://www.bountysource.com/teams/nextcloud) (maintaining Nextcloud and Nextcloud Forms).
|
||||
If you like this work, please donate to the [42l association](https://42l.fr) (maintaining sncf) or [Nextcloud](https://nextcloud.com/include/) (maintaining Nextcloud and Nextcloud Forms).
|
||||
|
||||
#### Translating
|
||||
|
||||
Currently, this software is translated in French and English.
|
||||
Currently, this software is translated in French, English and German (thanks [alpcentaur](https://git.42l.fr/alpcentaur)!)
|
||||
|
||||
Feel free to take a look at the [lang.json](https://git.42l.fr/neil/sncf/src/branch/root/lang.json) file and send a pull request.
|
||||
|
||||
|
@ -68,4 +79,4 @@ That sounds appropriate, but I don't feel like writing PHP and I don't know Next
|
|||
|
||||
#### Are you crazy? This tweak is gonna break at every single update.
|
||||
|
||||
Yeah, well, you're probably right. A Nextcloud app would be more suitable, I guess. But anyway, feel free to use something else if this is too much tweaking for you. But I'd prefer some pull requests to help me keep this software up-to-date with Nextcloud and Nextcloud Forms updates.
|
||||
Yeah, well, you're probably right. A Nextcloud app would be more suitable, I guess. But anyway, feel free to use something else if this is too much tweaking for you. But I'd prefer some pull requests to help me keep this software up-to-date with Nextcloud and Nextcloud Forms updates.
|
||||
|
|
|
@ -6,13 +6,15 @@ listening_port = 8000
|
|||
# includes protocol, FQDN and port, without the trailing slash.
|
||||
sncf_url = "http://localhost:8000"
|
||||
|
||||
# path to the SQLite DB
|
||||
database_path = "./db/sncf.sqlite"
|
||||
# SQLite: path to the SQLite DB
|
||||
# PostgreSQL: postgres://user:password@address:port/database
|
||||
# MySQL: mysql://user:password@address:port/database
|
||||
database_path = ""
|
||||
|
||||
# IP address of the Nextcloud instance, including protocol and port
|
||||
nextcloud_url = "http://10.0.0.0"
|
||||
|
||||
# Nextcloud admin account
|
||||
# Nextcloud admin account credentials
|
||||
admin_username = "adminusername"
|
||||
admin_password = "adminverylongandsecurepassword"
|
||||
|
||||
|
@ -22,5 +24,10 @@ prune_days = 150
|
|||
# Displays route names and a lot of information
|
||||
debug_mode = true
|
||||
|
||||
# Used to encrypt csrf tokens and csrf cookies.
|
||||
# Generate random bytes: openssl rand -base64 32
|
||||
# Then paste the result in this variable
|
||||
cookie_key = ""
|
||||
|
||||
# Don't touch this unless you know what you're doing
|
||||
config_version = 1
|
||||
config_version = 2
|
||||
|
|
280
lang.json
|
@ -1,199 +1,262 @@
|
|||
{
|
||||
"lang_code": {
|
||||
"en": "en",
|
||||
"fr": "fr",
|
||||
"de": "de"
|
||||
},
|
||||
"lang_full": {
|
||||
"en": "English",
|
||||
"fr": "Français"
|
||||
"fr": "Français",
|
||||
"de": "Deutsch"
|
||||
},
|
||||
"meta_description": {
|
||||
"en": "42l Forms : create forms for free, without registration while protecting your privacy",
|
||||
"fr": "42l Formulaires (Forms) : créez des formulaires ou questionnaires gratuitement, sans inscription et dans le respect de votre vie privée"
|
||||
"fr": "42l Formulaires (Forms) : créez des formulaires ou questionnaires gratuitement, sans inscription et dans le respect de votre vie privée",
|
||||
"de": "42l Forms: erstellen Sie gratis Umfragen, ohne Registrierung und unter Wahrung Ihrer Privatssphäre"
|
||||
},
|
||||
"index_title": {
|
||||
"en": "42l Forms",
|
||||
"fr": "42l Formulaires"
|
||||
"fr": "42l Formulaires",
|
||||
"de": "42l Forms"
|
||||
},
|
||||
"index_description": {
|
||||
"en": "Create forms without registration",
|
||||
"fr": "Créez des questionnaires sans inscription"
|
||||
"fr": "Créez des questionnaires sans inscription",
|
||||
"de": "Erstellen Sie Umfragen ohne Registrierung"
|
||||
},
|
||||
"index_beta_tag": {
|
||||
"en": "BETA",
|
||||
"fr": "BETA"
|
||||
"fr": "BETA",
|
||||
"de": "BETA"
|
||||
},
|
||||
"index_nojs": {
|
||||
"en": "Please enable JavaScript in your browser!",
|
||||
"fr": "Veuillez activer JavaScript dans votre navigateur !",
|
||||
"de": "Bitte aktivieren Sie JavaScript in ihrem Browser!"
|
||||
},
|
||||
"index_createform_button": {
|
||||
"en": "Create a form",
|
||||
"fr": "Créer un formulaire"
|
||||
"fr": "Créer un formulaire",
|
||||
"de": "Erstellen einer Umfrage"
|
||||
},
|
||||
"index_continueform_button": {
|
||||
"en": "Access your forms",
|
||||
"fr": "Accéder à vos formulaires"
|
||||
},
|
||||
"index_beta_banner_title": {
|
||||
"en": "Warning: Service in beta.",
|
||||
"fr": "Attention : Service en bêta."
|
||||
"fr": "Attention : Service en bêta.",
|
||||
"de": "Achtung: Seite in Beta Version"
|
||||
},
|
||||
"index_beta_banner_desc1": {
|
||||
"en": "This service is currently under development and might behave in an unexpected way.",
|
||||
"fr": "Ce service est en cours de développement et pourrait se comporter de manière inattendue."
|
||||
"fr": "Ce service est en cours de développement et pourrait se comporter de manière inattendue.",
|
||||
"de": "Diese Seite ist in Entwicklung und könnte sich unerwartet verhalten."
|
||||
},
|
||||
"index_beta_banner_desc2": {
|
||||
"en": "Feel free to send feedbacks on our ",
|
||||
"fr": "Vous pouvez nous envoyer vos retours sur "
|
||||
"fr": "Vous pouvez nous envoyer vos retours sur ",
|
||||
"de": "Feedback gerne an "
|
||||
},
|
||||
"index_beta_banner_desc_link": {
|
||||
"en": "our contact page",
|
||||
"fr": "notre page de contact"
|
||||
"fr": "notre page de contact",
|
||||
"de": "unsere Kontaktseite"
|
||||
},
|
||||
"index_disclaimer1": {
|
||||
"en": "This service is maintained for free, without subscription nor advertising nor tracking or selling of your personal data, on a server hosted in France.",
|
||||
"fr": "Ce service vous est fourni gratuitement, sans inscription, sans publicités, sans pistage ou revente de vos données personnelles, sur un serveur hébergé en France."
|
||||
"fr": "Ce service vous est fourni gratuitement, sans inscription, sans publicités, sans pistage ou revente de vos données personnelles, sur un serveur hébergé en France.",
|
||||
"de": "Diese Seite wird frei, ohne Registrierung, ohne Werbung, ohne Tracking, oder den Verkauf von Ihren persönlichen Daten, auf einem Server in Frankreich betrieben."
|
||||
},
|
||||
"index_disclaimer2": {
|
||||
"en": "If you appreciate our work, please consider donating to ",
|
||||
"fr": "Si vous appréciez notre travail, merci d'envisager de faire un don à "
|
||||
"fr": "Si vous appréciez notre travail, merci d'envisager de faire un don à ",
|
||||
"de": "Wenn Sie unsere Arbeit wertschätzen, Spenden Sie gerne an"
|
||||
},
|
||||
"index_disclaimer2_link_org": {
|
||||
"en": "the 42l association",
|
||||
"fr": "l'association 42l"
|
||||
"fr": "l'association 42l",
|
||||
"de": "die 42l Assoziation"
|
||||
},
|
||||
"index_disclaimer2_or": {
|
||||
"en": " or ",
|
||||
"fr": " ou à "
|
||||
"fr": " ou à ",
|
||||
"de": " oder an"
|
||||
},
|
||||
"index_disclaimer2_nc": {
|
||||
"en": "Nextcloud",
|
||||
"fr": "Nextcloud"
|
||||
"fr": "Nextcloud",
|
||||
"de": "Nextcloud"
|
||||
},
|
||||
"index_panel1_title": {
|
||||
"en": "Responsive and intuitive interface",
|
||||
"fr": "Interface intuitive et compatible mobile"
|
||||
"fr": "Interface intuitive et compatible mobile",
|
||||
"de": "mobil-freundliche und intuitive Benutzeroberfläche"
|
||||
},
|
||||
"index_panel1_desc1": {
|
||||
"en": "Are you searching for a privacy-friendly alternative to Google Forms while keeping its ease of use?",
|
||||
"fr": "Cherchez-vous une alternative éthique à Google Forms qui reste simple d'utilisation ?"
|
||||
"fr": "Cherchez-vous une alternative éthique à Google Forms qui reste simple d'utilisation ?",
|
||||
"de": "Suchen Sie eine ethisch sinnvolle Alternative zu Google Forms, welche gleichzeitig einfach in der Bedienung ist?"
|
||||
},
|
||||
"index_panel1_desc2": {
|
||||
"en": "You've just found it.",
|
||||
"fr": "Vous venez de la trouver."
|
||||
"fr": "Vous venez de la trouver.",
|
||||
"de": "Sie haben sie gefunden."
|
||||
},
|
||||
"index_panel2_title": {
|
||||
"en": "Choose and order your fields",
|
||||
"fr": "Choisissez et ordonnez vos champs"
|
||||
"fr": "Choisissez et ordonnez vos champs",
|
||||
"de": "Wählen und Ordnen Sie ihre Felder"
|
||||
},
|
||||
"index_panel2_desc1": {
|
||||
"en": "The software currently supports five field types.",
|
||||
"fr": "Pour le moment, le logiciel supporte cinq types de champs."
|
||||
"en": "The software currently supports seven field types.",
|
||||
"fr": "Pour le moment, le logiciel supporte sept types de champs.",
|
||||
"de": "Im Moment unterstützt die Software sieben Typen von Feldern."
|
||||
},
|
||||
"index_panel2_desc2": {
|
||||
"en": "New field types are ",
|
||||
"fr": "De nouveaux types de champs sont "
|
||||
"fr": "De nouveaux types de champs sont ",
|
||||
"de": "Neue Typen von Feldern sind "
|
||||
},
|
||||
"index_panel2_desc2_link": {
|
||||
"en": "currently in the works",
|
||||
"fr": "en cours d'élaboration"
|
||||
"fr": "en cours d'élaboration",
|
||||
"de": "momentan in Bearbeitung"
|
||||
},
|
||||
"index_panel3_title": {
|
||||
"en": "Analyze the answers",
|
||||
"fr": "Analysez les réponses"
|
||||
"fr": "Analysez les réponses",
|
||||
"de": "Analysieren Sie die Antworten"
|
||||
},
|
||||
"index_panel3_desc1": {
|
||||
"en": "See detailed graphs of the answers to your form.",
|
||||
"fr": "Visualisez les réponses à votre formulaire avec un graphique."
|
||||
"fr": "Visualisez les réponses à votre formulaire avec un graphique.",
|
||||
"de": "Visualisieren Sie die Antworten Ihrer Umfrage graphisch."
|
||||
},
|
||||
"index_panel4_title": {
|
||||
"en": "Export the answers",
|
||||
"fr": "Exportez les réponses"
|
||||
"fr": "Exportez les réponses",
|
||||
"de": "Export der Antworten"
|
||||
},
|
||||
"index_panel4_desc1": {
|
||||
"en": "Export the raw data of your form in CSV format to integrate the answers in other software (e.g. LibreOffice Calc or Microsoft Excel).",
|
||||
"fr": "Exportez les données brutes de votre formulaire en format CSV pour intégrer les réponses dans d'autres logiciels (ex. LibreOffice Calc ou Microsoft Excel)."
|
||||
"fr": "Exportez les données brutes de votre formulaire en format CSV pour intégrer les réponses dans d'autres logiciels (ex. LibreOffice Calc ou Microsoft Excel).",
|
||||
"de": "Exportieren Sie die Rohdaten Ihrer Umfrage im CSV Format um die Antworten in anderer Software zu integrieren( z.B. LibreOffice Calc)"
|
||||
},
|
||||
"index_panel5_title": {
|
||||
"en": "Edit your form's settings",
|
||||
"fr": "Paramétrez vos formulaires"
|
||||
"fr": "Paramétrez vos formulaires",
|
||||
"de": "Einstellungen Ihrer Umfragen"
|
||||
},
|
||||
"index_panel5_desc1": {
|
||||
"en": "Use the share link to send your form to other people.",
|
||||
"fr": "Utilisez le lien de partage pour envoyer votre formulaire à d'autres personnes."
|
||||
"fr": "Utilisez le lien de partage pour envoyer votre formulaire à d'autres personnes.",
|
||||
"de": "Nutzen Sie den Teilen Link um Ihre Umfrage anderen Menschen zu schicken."
|
||||
},
|
||||
"index_panel5_desc2": {
|
||||
"en": "You can also define an expiration date for your form.",
|
||||
"fr": "Vous pouvez également définir une date d'expiration pour votre formulaire."
|
||||
"fr": "Vous pouvez également définir une date d'expiration pour votre formulaire.",
|
||||
"de": "Sie können auch ein Ablaufdatum für ihre Umfrage festsetzen."
|
||||
},
|
||||
"index_panel6_title": {
|
||||
"en": "All your forms in one place",
|
||||
"fr": "Tous vos formulaires au même endroit"
|
||||
"fr": "Tous vos formulaires au même endroit",
|
||||
"de": "Alle Ihre Umfragen an einem Ort"
|
||||
},
|
||||
"index_panel6_desc1": {
|
||||
"en": "Find all your forms in the same panel.",
|
||||
"fr": "Retrouvez tous vos formulaires sur un même panel."
|
||||
"fr": "Retrouvez tous vos formulaires sur un même panel.",
|
||||
"de": "Finde alle deine Umfragen in einem Panel."
|
||||
},
|
||||
"index_bottom_docs": {
|
||||
"en": "Documentation",
|
||||
"fr": "Documentation"
|
||||
"fr": "Documentation",
|
||||
"de": "Dokumentation"
|
||||
},
|
||||
"index_bottom_source": {
|
||||
"en": "Source code",
|
||||
"fr": "Code source"
|
||||
"fr": "Code source",
|
||||
"de": "Quellcode"
|
||||
},
|
||||
"index_bottom_lic": {
|
||||
"en": "License",
|
||||
"fr": "Licence"
|
||||
"fr": "Licence",
|
||||
"de": "Lizenz"
|
||||
},
|
||||
"index_credits_title": {
|
||||
"en": "Credits",
|
||||
"fr": "Crédits"
|
||||
"fr": "Crédits",
|
||||
"de": "Credits"
|
||||
},
|
||||
"index_credits_desc1": {
|
||||
"en": "The Nextcloud software suite and the Nextcloud Forms application has been developed by ",
|
||||
"fr": "La suite logicielle Nextcloud et l'application Nextcloud Forms a été développée par "
|
||||
"fr": "La suite logicielle Nextcloud et l'application Nextcloud Forms a été développée par ",
|
||||
"de": "Die Nextcloud Software Sammlung und die Nextcloud Forms Applikation wurden entwickelt von "
|
||||
},
|
||||
"index_credits_desc1_link": {
|
||||
"en": "the Nextcloud team",
|
||||
"fr": "l'équipe Nextcloud"
|
||||
"fr": "l'équipe Nextcloud",
|
||||
"de": "dem Nextcloud Team"
|
||||
},
|
||||
"index_credits_desc1_a": {
|
||||
"en": " and its contributors.",
|
||||
"fr": " et ses contributeur·ices."
|
||||
"fr": " et ses contributeur·ices.",
|
||||
"de": " und ihren Kontributor*innen"
|
||||
},
|
||||
"index_credits_desc2": {
|
||||
"en": "The Simple Nextcloud Forms software, which simplifies the form creation process, has been developed by ",
|
||||
"fr": "Le logiciel Simple Nextcloud Forms, qui simplifie la création de formulaires, a été développé par "
|
||||
"fr": "Le logiciel Simple Nextcloud Forms, qui simplifie la création de formulaires, a été développé par ",
|
||||
"de": "Die Simple Nextcloud Forms Software, welche die Erstellung von Umfragen erleichtert, wurde entwickelt von "
|
||||
},
|
||||
"index_credits_desc2_for": {
|
||||
"en": " for ",
|
||||
"fr": " pour "
|
||||
"fr": " pour ",
|
||||
"de": " für "
|
||||
},
|
||||
"index_credits_desc2_org": {
|
||||
"en": "the 42l association",
|
||||
"fr": "l'association 42l"
|
||||
"fr": "l'association 42l",
|
||||
"de": "die 42l Assoziation"
|
||||
},
|
||||
"index_credits_desc3": {
|
||||
"en": "(sources available soon)",
|
||||
"fr": "(sources bientôt disponibles)"
|
||||
"en": "source code",
|
||||
"fr": "code source",
|
||||
"de": "Quellcode"
|
||||
},
|
||||
"link_title": {
|
||||
"en": "Link created",
|
||||
"fr": "Lien créé"
|
||||
"fr": "Lien créé",
|
||||
"de": "Link erstellt"
|
||||
},
|
||||
"link_desc1": {
|
||||
"en": "Here's an <b>administration link</b>, which will allow you to access all your forms and check your answers.",
|
||||
"fr": "Voici un <b>lien d'administration</b>, qui vous permettra d'accéder à tous vos formulaires et de consulter vos réponses."
|
||||
"fr": "Voici un <b>lien d'administration</b>, qui vous permettra d'accéder à tous vos formulaires et de consulter vos réponses.",
|
||||
"de": "Hier ist ein <b>Administrations Link</b>, der es ermöglicht wieder zu ihren Umfragen zu gelangen und die Antworten einzusehen."
|
||||
},
|
||||
"link_desc2": {
|
||||
"en": "<b>Keep it</b> carefully and don't give it away (it'd be the same as giving out your password!).",
|
||||
"fr": "<b>Conservez-le</B> bien précieusement et ne le donnez pas (cela reviendrait à donner un mot de passe !)."
|
||||
"fr": "<b>Conservez-le</B> bien précieusement et ne le donnez pas (cela reviendrait à donner un mot de passe !).",
|
||||
"de": "<b>Bewahren Sie diese</b> gut und sicher auf ( Die Weitergabe entspricht der Weitergabe eines Passwortes! )."
|
||||
},
|
||||
"link_desc3": {
|
||||
"en": "Once your link copied, click on the button below to start editing your forms.",
|
||||
"fr": "Une fois votre lien copié, cliquez sur le bouton ci-dessous pour commencer à éditer vos formulaires."
|
||||
"fr": "Une fois votre lien copié, cliquez sur le bouton ci-dessous pour commencer à éditer vos formulaires.",
|
||||
"de": "Ist der Link kopiert, drücken sie auf den unteren Button um Umfragen zu erstellen oder zu bearbeiten."
|
||||
},
|
||||
"link_access_btn": {
|
||||
"en": "Access the forms",
|
||||
"fr": "Accéder aux formulaires"
|
||||
"fr": "Accéder aux formulaires",
|
||||
"de": "Zugang zu den Umfragen"
|
||||
},
|
||||
"link_note": {
|
||||
"en": "Note: If you don't use your administration link during more than ",
|
||||
"fr": "Note : Si vous n'utilisez pas votre lien d'administration pendant plus de "
|
||||
"fr": "Note : Si vous n'utilisez pas votre lien d'administration pendant plus de ",
|
||||
"de": "Notiz: Wenn Sie den Administrations Link für länger als "
|
||||
},
|
||||
"link_note2": {
|
||||
"en": " days, your forms will be automatically deleted.",
|
||||
"fr": " jours, vos formulaires seront automatiquement supprimés."
|
||||
"fr": " jours, vos formulaires seront automatiquement supprimés.",
|
||||
"de": " Tage nicht benutzen, werden ihre Umfragen automatisch gelöscht."
|
||||
},
|
||||
"link_copy": {
|
||||
"en": "Copy link",
|
||||
|
@ -201,142 +264,187 @@
|
|||
},
|
||||
"link_copied": {
|
||||
"en": "Link copied!",
|
||||
"fr": "Lien copié !"
|
||||
"fr": "Lien copié !",
|
||||
"de": "Link kopiert !"
|
||||
},
|
||||
"error_title": {
|
||||
"en": "Oops!...",
|
||||
"fr": "Oups !..."
|
||||
"fr": "Oups !...",
|
||||
"de": "Ups !..."
|
||||
},
|
||||
"error_description": {
|
||||
"en": "The application encountered a problem:",
|
||||
"fr": "L'application a rencontré un problème :"
|
||||
"fr": "L'application a rencontré un problème :",
|
||||
"de": "Die Anwendung hat ein Problem festgestellt:"
|
||||
},
|
||||
"error_back": {
|
||||
"en": "Back to the main page",
|
||||
"fr": "Retour à la page principale"
|
||||
"fr": "Retour à la page principale",
|
||||
"de": "Zurück zur Hauptseite"
|
||||
},
|
||||
"error_note1": {
|
||||
"en": "We are (probably) aware of this bug, but feel free to contact us if you need assistance.",
|
||||
"fr": "Nous sommes (probablement) au courant, mais n'hésitez pas à nous contacter si vous avez besoin d'aide."
|
||||
"fr": "Nous sommes (probablement) au courant, mais n'hésitez pas à nous contacter si vous avez besoin d'aide.",
|
||||
"de": "Wir sind uns (wahrscheinlich) bewusst, was diesen Fehler angeht. Fühlen sie sich frei uns zu kontaktieren, wenn Sie Hilfe benötigen."
|
||||
},
|
||||
"error_note2": {
|
||||
"en": "Sorry for the inconvenience.",
|
||||
"fr": "Désolés pour les désagréments occasionnés."
|
||||
"fr": "Désolés pour les désagréments occasionnés.",
|
||||
"de": "Entschuldigen Sie die Störung."
|
||||
},
|
||||
"error_forward_req": {
|
||||
"en": "Error while connecting to the Nextcloud instance.",
|
||||
"fr": "Erreur lors de la connexion à l'instance Nextcloud."
|
||||
"fr": "Erreur lors de la connexion à l'instance Nextcloud.",
|
||||
"de": "Fehler beim Verbinden zur Nextcloud Instanz."
|
||||
},
|
||||
"error_forward_resp": {
|
||||
"en": "Error while reading Nextcloud instance's response.",
|
||||
"fr": "Erreur lors de la lecture de la réponse de l'instance Nextcloud."
|
||||
"fr": "Erreur lors de la lecture de la réponse de l'instance Nextcloud.",
|
||||
"de": "Feher beim Lesen der Antwort der Nextcloud Instanz."
|
||||
},
|
||||
"error_forward_isanon": {
|
||||
"en": "Couldn't set the form's isAnonymous value.",
|
||||
"fr": "Échec lors de la définition de la valeur isAnonymous du formulaire."
|
||||
"fr": "Échec lors de la définition de la valeur isAnonymous du formulaire.",
|
||||
"de": "Es ist nicht möglich, die isAnonymous Wert des Formulars zu setzen."
|
||||
},
|
||||
"error_forward_clientresp_newform": {
|
||||
"en": "Failed to send the response body (new form).",
|
||||
"fr": "Échec lors de l'envoi du corps de la réponse (nouveau formulaire)."
|
||||
"fr": "Échec lors de l'envoi du corps de la réponse (nouveau formulaire).",
|
||||
"de": "Fehler beim senden des Response body (neues Formular)."
|
||||
},
|
||||
"error_forward_clientresp_std": {
|
||||
"en": "Failed to send the response body.",
|
||||
"fr": "Échec lors de l'envoi du corps de la réponse."
|
||||
"fr": "Échec lors de l'envoi du corps de la réponse.",
|
||||
"de": "Fehler beim Senden des Response Body."
|
||||
},
|
||||
"error_forwardlogin_db": {
|
||||
"en": "Couldn't connect to the local database.",
|
||||
"fr": "Échec lors de la connexion à la base de données locale."
|
||||
"fr": "Échec lors de la connexion à la base de données locale.",
|
||||
"de": "Fehler beim verbinden zur lokalen Datenbank."
|
||||
},
|
||||
"error_forwardlogin_db_get": {
|
||||
"en": "Error during information retrieval from the local database.",
|
||||
"fr": "Erreur lors de la récupération des informations dans la base de données locale."
|
||||
"fr": "Erreur lors de la récupération des informations dans la base de données locale.",
|
||||
"de": "Fehler beim Empfangen von Daten der lokalen Datenbank."
|
||||
},
|
||||
"error_forwardlogin_notfound": {
|
||||
"en": "The specified token doesn't exist in local database.",
|
||||
"fr": "Le token spécifié n'existe pas dans la base de données locale."
|
||||
"fr": "Le token spécifié n'existe pas dans la base de données locale.",
|
||||
"de": "Der gesetzte Token existiert nicht in der lokalen Datenbank."
|
||||
},
|
||||
"error_login_get": {
|
||||
"en": "The account creation request (GET) to Nextcloud has failed.",
|
||||
"fr": "La requête de création de compte (GET) vers l'instance Nextcloud a échoué."
|
||||
"fr": "La requête de création de compte (GET) vers l'instance Nextcloud a échoué.",
|
||||
"de": "Das Account Erstellungs Request (GET) zu Nextcloud hat nicht funktioniert."
|
||||
},
|
||||
"error_login_get_body": {
|
||||
"en": "Reading response from the account creation request to Nextcloud has failed.",
|
||||
"fr": "La lecture de la réponse à la requête de création de compte vers l'instance Nextcloud a échoué."
|
||||
"fr": "La lecture de la réponse à la requête de création de compte vers l'instance Nextcloud a échoué.",
|
||||
"de": "Das Lesen der Response vom Account Erstellungs Request zu Nextcloud hat nicht funktioniert."
|
||||
},
|
||||
"error_login_post": {
|
||||
"en": "The account creation request (POST) to Nextcloud has failed.",
|
||||
"fr": "La requête de création de compte (POST) vers l'instance Nextcloud a échoué."
|
||||
"fr": "La requête de création de compte (POST) vers l'instance Nextcloud a échoué.",
|
||||
"de": "Der Account Erstellungs Request (POST) zu Nextcloud hat nicht funktioniert. "
|
||||
},
|
||||
"error_login_redir": {
|
||||
"en": "Redirection to Nextcloud account failed.",
|
||||
"fr": "La redirection vers le compte Nextcloud a échoué."
|
||||
"fr": "La redirection vers le compte Nextcloud a échoué.",
|
||||
"de": "Die Weiterleitung zum Nextcloud account hat nicht funktioniert."
|
||||
},
|
||||
"error_createaccount_post": {
|
||||
"en": "Account creation: connection to the Nextcloud API failed.",
|
||||
"fr": "Création de compte : la connexion à l'API Nextcloud a échoué."
|
||||
"fr": "Création de compte : la connexion à l'API Nextcloud a échoué.",
|
||||
"de": "Account Erstellung: Verbindung zur Nextcloud API hat nicht funktioniert."
|
||||
},
|
||||
"error_createaccount_post_body": {
|
||||
"en": "Account creation: reading the answer from the Nextcloud API failed.",
|
||||
"fr": "Création de compte : le traitement de la réponse de l'API Nextcloud a échoué."
|
||||
"fr": "Création de compte : le traitement de la réponse de l'API Nextcloud a échoué.",
|
||||
"de": "Account Erstellung : das Lesen der Antwort der Nextcloud API hat nicht funktioniert."
|
||||
},
|
||||
"error_createaccount_status": {
|
||||
"en": "The Nextcloud instance responded with an unexpected status code.",
|
||||
"fr": "L'instance Nextcloud a répondu avec un code de statut inattendu."
|
||||
"fr": "L'instance Nextcloud a répondu avec un code de statut inattendu.",
|
||||
"de": "Die Nextcloud Instanz hat mit einem unexpected status code geantwortet."
|
||||
},
|
||||
"error_createaccount_ncstatus": {
|
||||
"en": "The Nextcloud API responded with an unexpected status code.",
|
||||
"fr": "L'API Nextcloud a répondu avec un code de statut inattendu."
|
||||
"fr": "L'API Nextcloud a répondu avec un code de statut inattendu.",
|
||||
"de": "Die Nextcloud API hat mit unexpected ncstatus geantwortet."
|
||||
},
|
||||
"error_createaccount_ncstatus_parse": {
|
||||
"en": "Error parsing Nextcloud API's status code.",
|
||||
"fr": "Erreur lors de la lecture du code de statut de l'API Nextcloud."
|
||||
"fr": "Erreur lors de la lecture du code de statut de l'API Nextcloud.",
|
||||
"de": "Fehler beim Lesen des Nextcloud API status codes."
|
||||
},
|
||||
"error_forwardregister_pool": {
|
||||
"en": "Error while connecting to the local database.",
|
||||
"fr": "Erreur lors de la connexion à la base de données locale."
|
||||
"fr": "Erreur lors de la connexion à la base de données locale.",
|
||||
"de": "Fehler beim Verbinden zu der lokalen Datenbank."
|
||||
},
|
||||
"error_forwardregister_db": {
|
||||
"en": "Failed adding the Nextcloud account in the local database.",
|
||||
"fr": "L'ajout du compte Nextcloud dans la base de données locale a échoué."
|
||||
"fr": "L'ajout du compte Nextcloud dans la base de données locale a échoué.",
|
||||
"de": "Fehlre beim Hinzufügen des Nextcloud Accounts zur lokalen Datenbank."
|
||||
},
|
||||
"error_forwardregister_tokenparse": {
|
||||
"en": "Failed parsing the admin token.",
|
||||
"fr": "Échec lors de la lecture du token administrateur."
|
||||
"fr": "Échec lors de la lecture du token administrateur.",
|
||||
"de": "Fehler beim Parsen des Admin Tokens."
|
||||
},
|
||||
"error_login_cookiepair": {
|
||||
"en": "Couldn't read cookies.",
|
||||
"fr": "Échec lors de la lecture de cookies."
|
||||
"fr": "Échec lors de la lecture de cookies.",
|
||||
"de": "Fehler beim Lesen der Cookies"
|
||||
},
|
||||
"error_login_regex": {
|
||||
"en": "Couldn't read the CSRF token.",
|
||||
"fr": "Échec lors de la lecture du token CSRF."
|
||||
"fr": "Échec lors de la lecture du token CSRF.",
|
||||
"de": "Fehler beim Lesen des CSRF Tokens."
|
||||
},
|
||||
"error_login_setcookie": {
|
||||
"en": "Error during cookies transfer.",
|
||||
"fr": "Erreur lors du transfert de cookies."
|
||||
"fr": "Erreur lors du transfert de cookies.",
|
||||
"de": "Feheler beim Transfer der Cookies."
|
||||
},
|
||||
"error_form_insert": {
|
||||
"en": "The local database couldn't be reached.",
|
||||
"fr": "Échec de la connexion avec la base de données locale."
|
||||
"fr": "Échec de la connexion avec la base de données locale.",
|
||||
"de": "Die lokale Datenbank ist nicht erreichbar."
|
||||
},
|
||||
"error_createaccount": {
|
||||
"en": "The Nextcloud API returned an unexpected result.",
|
||||
"fr": "L'API de Nextcloud a retourné un résultat inattendu."
|
||||
"fr": "L'API de Nextcloud a retourné un résultat inattendu.",
|
||||
"de": "Die Nextcloud API hat ein unerwartetes Resultat zurückgesendet."
|
||||
},
|
||||
"error_redirect": {
|
||||
"en": "Failed to redirect.",
|
||||
"fr": "La redirection a échoué."
|
||||
"fr": "La redirection a échoué.",
|
||||
"de": "Weiterleitung (Redirect) hat nicht funktioniert."
|
||||
},
|
||||
"error_csrf_cookie": {
|
||||
"en": "Your CSRF token (cookie) seems incorrect, please retry.",
|
||||
"fr": "Votre token CSRF (cookie) semble incorrect, veuillez réessayer.",
|
||||
"de": "Dein CSRF Token (Cookie) scheint inkorrekt, versuchen Sie es erneut."
|
||||
},
|
||||
"error_csrf_token": {
|
||||
"en": "Your CSRF token seems incorrect, please retry.",
|
||||
"fr": "Votre token CSRF semble incorrect, veuillez réessayer.",
|
||||
"de": "Ihr CSRF Token scheint nicht korrekt, versuchen Sie es erneut. "
|
||||
},
|
||||
"error_dirtyhacker": {
|
||||
"en": "Attempt to access an unauthorized resource.",
|
||||
"fr": "Tentative d'accès à une ressource non autorisée."
|
||||
"fr": "Tentative d'accès à une ressource non autorisée.",
|
||||
"de": "Zugangs-Versuch einer unauthorisierten Quelle."
|
||||
},
|
||||
"error_tplrender": {
|
||||
"en": "Template rendering failed.",
|
||||
"fr": "Le rendu du template a échoué."
|
||||
"fr": "Le rendu du template a échoué.",
|
||||
"de": "Template rendering hat nicht funktioniert."
|
||||
},
|
||||
"error_tplrender_resp": {
|
||||
"en": "Sending response failed.",
|
||||
"fr": "L'envoi de la réponse a échoué."
|
||||
"fr": "L'envoi de la réponse a échoué.",
|
||||
"de": "Senden der Antwort hat nicht funktioniert."
|
||||
}
|
||||
}
|
||||
|
|
8
migrations/mysql/20200809180000_create_form/up.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE form (
|
||||
id INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT UNIQUE,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
lastvisit_at TIMESTAMP NOT NULL,
|
||||
token VARCHAR(128) NOT NULL UNIQUE,
|
||||
nc_username VARCHAR(128) NOT NULL UNIQUE,
|
||||
nc_password VARCHAR(128) NOT NULL
|
||||
);
|
1
migrations/postgres/20200809180000_create_form/down.sql
Normal file
|
@ -0,0 +1 @@
|
|||
DELETE TABLE form;
|
8
migrations/postgres/20200809180000_create_form/up.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE form (
|
||||
id serial4 PRIMARY KEY UNIQUE NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
lastvisit_at TIMESTAMP NOT NULL,
|
||||
token VARCHAR UNIQUE NOT NULL,
|
||||
nc_username VARCHAR UNIQUE NOT NULL,
|
||||
nc_password VARCHAR NOT NULL
|
||||
);
|
1
migrations/sqlite/20200809180000_create_form/down.sql
Normal file
|
@ -0,0 +1 @@
|
|||
DELETE TABLE form;
|
1
rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
reorder_imports = true
|
104
src/account.rs
|
@ -1,14 +1,15 @@
|
|||
use actix_web::client::Client;
|
||||
use actix_web::{http, web, HttpRequest, HttpResponse};
|
||||
use base64::URL_SAFE_NO_PAD;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::Rng;
|
||||
use rand::RngCore;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::PROXY_TIMEOUT;
|
||||
use crate::config::{ADJ_LIST, NAME_LIST};
|
||||
use crate::config::{ADJ_LIST, NAME_LIST, PROXY_TIMEOUT, USER_AGENT};
|
||||
use crate::debug;
|
||||
use crate::errors::{crash, TrainCrash};
|
||||
use crate::templates::get_lang;
|
||||
|
@ -35,15 +36,6 @@ pub fn is_logged_in(req: &HttpRequest) -> Option<&str> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn has_admintoken(req: &HttpRequest) -> Option<&str> {
|
||||
let c = req.headers().get("Cookie")?.to_str().ok()?;
|
||||
if c.contains("sncf_admin_token") {
|
||||
Some(c)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// attempts to create the account from Nextcloud's API
|
||||
// returns the newly created username.
|
||||
// if it fails (bad return code), returns None.
|
||||
|
@ -134,11 +126,11 @@ pub async fn login(
|
|||
) -> Result<HttpResponse, TrainCrash> {
|
||||
debug(&format!("Sending forged login for user {}", user));
|
||||
|
||||
// 1. GET /login
|
||||
// 1. GET /csrftoken
|
||||
let mut login_get = client
|
||||
.get(format!("{}/{}", CONFIG.nextcloud_url, "login"))
|
||||
.get(format!("{}/{}", CONFIG.nextcloud_url, "csrftoken"))
|
||||
.timeout(Duration::new(PROXY_TIMEOUT, 0))
|
||||
.header("User-Agent", "Actix-web")
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
|
@ -148,20 +140,59 @@ pub async fn login(
|
|||
|
||||
// rewrite cookie headers from GET to POST
|
||||
let mut str_cookiepair = String::new();
|
||||
for h_value in login_get.headers().get_all("set-cookie") {
|
||||
str_cookiepair = format!(
|
||||
"{}; {}",
|
||||
str_cookiepair,
|
||||
h_value.clone().to_str().map_err(|e| {
|
||||
eprintln!("error_login_cookiepair: {}", e);
|
||||
crash(get_lang(&req), "error_login_cookiepair")
|
||||
})?
|
||||
);
|
||||
|
||||
// remove duplicate oc<id> cookie (nextcloud bug)
|
||||
// leading to sncf being unable to forge logins
|
||||
let cookie_set = login_get.headers().get_all("set-cookie");
|
||||
let mut cookie_map: HashMap<String, String> = HashMap::new();
|
||||
for c in cookie_set {
|
||||
// get str version of cookie header
|
||||
let c_str = c.to_str().map_err(|e| {
|
||||
eprintln!("error_login_cookiepair (1): {}", e);
|
||||
crash(get_lang(&req), "error_login_cookiepair")
|
||||
})?;
|
||||
|
||||
// percent decode
|
||||
let c_str = percent_decode_str(c_str).decode_utf8_lossy();
|
||||
|
||||
//then remove values after ';'
|
||||
let c_str_arr = c_str.split(';').collect::<Vec<&str>>();
|
||||
|
||||
let c_str = c_str_arr
|
||||
.first()
|
||||
.expect("error: cookiepair split does not have a first value. shouldn't happen.");
|
||||
|
||||
// split cookie key and cookie value
|
||||
// split_once would work best but it's nightly-only for now
|
||||
let c_str_arr = c_str.split('=').collect::<Vec<&str>>();
|
||||
|
||||
let c_key = c_str_arr
|
||||
.first()
|
||||
.expect("error: cookie key split does not have a first value, shouldn't happen.");
|
||||
|
||||
let c_value = c_str.replace(&format!("{}=", c_key), "");
|
||||
|
||||
if c_key != c_str {
|
||||
// if the key already exists in hashmap, replace its value
|
||||
// else, insert it
|
||||
if let Some(c_sel) = cookie_map.get_mut(*c_key) {
|
||||
*c_sel = c_value;
|
||||
} else {
|
||||
cookie_map.insert(c_key.to_string(), c_value);
|
||||
}
|
||||
} else {
|
||||
eprintln!("error_login_cookiepair (2)");
|
||||
return Err(crash(get_lang(&req), "error_login_cookiepair"));
|
||||
}
|
||||
}
|
||||
|
||||
for (cookie_k, cookie_v) in cookie_map {
|
||||
str_cookiepair.push_str(&format!("{}={}; ", cookie_k, cookie_v));
|
||||
}
|
||||
|
||||
// load requesttoken regex
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r#"requesttoken="(?P<token>.*)""#)
|
||||
static ref RE: Regex = Regex::new(r#"\{"token":"(?P<token>[^"]*)"\}"#)
|
||||
.expect("Error while parsing the requesttoken regex");
|
||||
}
|
||||
|
||||
|
@ -189,7 +220,7 @@ pub async fn login(
|
|||
let mut login_post = client
|
||||
.post(format!("{}/{}", CONFIG.nextcloud_url, "login"))
|
||||
.timeout(Duration::new(PROXY_TIMEOUT, 0))
|
||||
.header("User-Agent", "Actix-web");
|
||||
.header("User-Agent", USER_AGENT);
|
||||
|
||||
// include all NC cookies in one cookie (cookie pair)
|
||||
login_post = login_post.header("Cookie", str_cookiepair);
|
||||
|
@ -211,6 +242,7 @@ pub async fn login(
|
|||
|
||||
// 3. set the same cookies in the user's browser
|
||||
let mut user_response = HttpResponse::SeeOther();
|
||||
|
||||
for item in response_post.headers().clone().get_all("set-cookie") {
|
||||
user_response.header(
|
||||
"Set-Cookie",
|
||||
|
@ -245,21 +277,35 @@ pub fn check_token(token: &str) -> bool {
|
|||
}
|
||||
|
||||
// generates a new token
|
||||
pub fn gen_token() -> String {
|
||||
pub fn gen_token(size: usize) -> String {
|
||||
// Using /dev/random to generate random bytes
|
||||
let mut r = OsRng;
|
||||
|
||||
let mut my_secure_bytes = vec![0u8; 45];
|
||||
let mut my_secure_bytes = vec![0u8; size];
|
||||
r.fill_bytes(&mut my_secure_bytes);
|
||||
base64::encode_config(my_secure_bytes, URL_SAFE_NO_PAD)
|
||||
}
|
||||
|
||||
// generates a random username composed of
|
||||
// an adjective, a name and a 4-byte base64-encoded token.
|
||||
// with the default list, that represents:
|
||||
// 141 * 880 = 124 080
|
||||
// 255^4 / 2 = 2 114 125 312 (we lose approx. the half because of uppercase)
|
||||
// 2 114 125 312 * 124 080 = 2.623206687*10^14 possible combinations??
|
||||
pub fn gen_name() -> String {
|
||||
format!("{}{}", list_rand(&ADJ_LIST), list_rand(&NAME_LIST))
|
||||
// uppercasing gen_token because NC would probably refuse two
|
||||
// users with the same name but a different case
|
||||
// and that'd be a pain to debug
|
||||
format!(
|
||||
"{}{}-{}",
|
||||
list_rand(&ADJ_LIST),
|
||||
list_rand(&NAME_LIST),
|
||||
gen_token(4).to_uppercase()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn list_rand(list: &[String]) -> &String {
|
||||
let mut rng = rand::thread_rng();
|
||||
let roll = rng.gen_range(0, list.len() - 1);
|
||||
let roll = rng.gen_range(0..list.len() - 1);
|
||||
&list[roll]
|
||||
}
|
||||
|
|
|
@ -9,12 +9,14 @@ pub const PAYLOAD_LIMIT: usize = 10_000_000;
|
|||
pub const PROXY_TIMEOUT: u64 = 15;
|
||||
|
||||
pub const CONFIG_FILE: &str = "./config.toml";
|
||||
pub const CONFIG_VERSION: u8 = 1;
|
||||
pub const CONFIG_VERSION: u8 = 2;
|
||||
|
||||
pub const ADJ_LIST_FILE: &str = "./adj-list.txt";
|
||||
pub const NAME_LIST_FILE: &str = "./name-list.txt";
|
||||
pub const LOC_FILE: &str = "./lang.json";
|
||||
|
||||
pub const USER_AGENT: &str = "Actix-web";
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONFIG: Config = Config::init();
|
||||
pub static ref ADJ_LIST: Vec<String> =
|
||||
|
@ -24,6 +26,7 @@ lazy_static! {
|
|||
pub static ref LOC: Value = init_lang();
|
||||
}
|
||||
|
||||
// Open LOC_FILE and store it in memory (LOC)
|
||||
fn init_lang() -> Value {
|
||||
let mut file = File::open(LOC_FILE).expect("init_lang: Can't open translations file");
|
||||
let mut data = String::new();
|
||||
|
@ -32,6 +35,7 @@ fn init_lang() -> Value {
|
|||
serde_json::from_str(&data).expect("init_lang(): Can't parse translations file")
|
||||
}
|
||||
|
||||
// Open a file from its path
|
||||
fn lines_from_file(filename: impl AsRef<Path>) -> io::Result<Vec<String>> {
|
||||
BufReader::new(File::open(filename)?).lines().collect()
|
||||
}
|
||||
|
@ -47,11 +51,13 @@ pub struct Config {
|
|||
pub admin_password: String,
|
||||
pub prune_days: u16,
|
||||
pub debug_mode: bool,
|
||||
pub cookie_key: String,
|
||||
pub config_version: u8,
|
||||
}
|
||||
|
||||
// totally not copypasted from rs-short
|
||||
impl Config {
|
||||
// open and parse CONFIG_FILE
|
||||
pub fn init() -> Self {
|
||||
let mut conffile = File::open(CONFIG_FILE).expect(
|
||||
r#"Config file config.toml not found.
|
||||
|
@ -61,12 +67,21 @@ impl Config {
|
|||
conffile
|
||||
.read_to_string(&mut confstr)
|
||||
.expect("Couldn't read config to string");
|
||||
toml::from_str(&confstr).expect("Couldn't deserialize the config")
|
||||
toml::from_str(&confstr).expect("Couldn't deserialize the config. Please update at https://git.42l.fr/neil/sncf/wiki/Upgrade-from-a-previous-version --- Error")
|
||||
}
|
||||
|
||||
// if config.config_version doesn't match the hardcoded version,
|
||||
// ask the admin to manually upgrade its config file
|
||||
pub fn check_version(&self) {
|
||||
if self.config_version != CONFIG_VERSION {
|
||||
eprintln!("Your configuration file is obsolete! Please update it with config.toml.sample and update its version to {}.", CONFIG_VERSION);
|
||||
eprintln!("Your configuration file is obsolete!\nPlease update it following the instructions in https://git.42l.fr/neil/sncf/wiki/Upgrade-from-a-previous-version and update its version to {}.", CONFIG_VERSION);
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_csrf_key() -> [u8; 32] {
|
||||
let mut key: [u8; 32] = Default::default();
|
||||
key.copy_from_slice(&CONFIG.cookie_key.clone().into_bytes()[..32]);
|
||||
key
|
||||
}
|
||||
|
|
|
@ -5,16 +5,16 @@ use diesel::prelude::*;
|
|||
use crate::database::schema::form;
|
||||
use crate::database::schema::form::dsl::*;
|
||||
use crate::database::structs::Form;
|
||||
use crate::SqliteConnection;
|
||||
use crate::DbConn;
|
||||
|
||||
#[table_name = "form"]
|
||||
#[derive(Serialize, Insertable)]
|
||||
pub struct InsertableForm<'b> {
|
||||
#[table_name = "form"]
|
||||
pub struct InsertableForm {
|
||||
pub created_at: NaiveDateTime,
|
||||
pub lastvisit_at: NaiveDateTime,
|
||||
pub token: &'b str,
|
||||
pub nc_username: &'b str,
|
||||
pub nc_password: &'b str,
|
||||
pub token: String,
|
||||
pub nc_username: String,
|
||||
pub nc_password: String,
|
||||
}
|
||||
|
||||
impl Form {
|
||||
|
@ -22,7 +22,7 @@ impl Form {
|
|||
// also updates lastvisit_at.
|
||||
pub fn get_from_token(
|
||||
i_token: &str,
|
||||
conn: &SqliteConnection,
|
||||
conn: &DbConn,
|
||||
) -> Result<Option<Form>, diesel::result::Error> {
|
||||
if let Some(formdata) = form
|
||||
.filter(token.eq(i_token))
|
||||
|
@ -38,19 +38,16 @@ impl Form {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn update_lastvisit(
|
||||
&self,
|
||||
conn: &SqliteConnection,
|
||||
) -> Result<usize, diesel::result::Error> {
|
||||
pub fn update_lastvisit(&self, conn: &DbConn) -> Result<usize, diesel::result::Error> {
|
||||
diesel::update(form.find(self.id))
|
||||
.set(lastvisit_at.eq(Utc::now().naive_utc()))
|
||||
.execute(conn)
|
||||
}
|
||||
|
||||
pub fn insert<'b>(
|
||||
i_form: InsertableForm<'b>,
|
||||
conn: &SqliteConnection,
|
||||
) -> Result<InsertableForm<'b>, diesel::result::Error> {
|
||||
pub fn insert(
|
||||
i_form: InsertableForm,
|
||||
conn: &DbConn,
|
||||
) -> Result<InsertableForm, diesel::result::Error> {
|
||||
match diesel::insert_into(form).values(&i_form).execute(conn) {
|
||||
Ok(_) => Ok(i_form),
|
||||
Err(e) => Err(e),
|
||||
|
|
|
@ -4,8 +4,8 @@ use chrono::NaiveDateTime;
|
|||
use crate::database::schema::form;
|
||||
//use crate::config::CONFIG;
|
||||
|
||||
#[table_name = "form"]
|
||||
#[derive(Serialize, Queryable, Insertable, Debug, Clone)]
|
||||
#[table_name = "form"]
|
||||
pub struct Form {
|
||||
pub id: i32,
|
||||
pub created_at: NaiveDateTime,
|
||||
|
|
266
src/forward.rs
|
@ -1,12 +1,14 @@
|
|||
use actix_web::client::{Client, ClientRequest};
|
||||
use actix_web::{http, web, HttpRequest, HttpResponse};
|
||||
use actix_session::Session;
|
||||
use askama::Template;
|
||||
use chrono::Utc;
|
||||
use regex::Regex;
|
||||
use csrf::{AesGcmCsrfProtection, CsrfProtection};
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
use crate::account::*;
|
||||
use crate::config::get_csrf_key;
|
||||
use crate::config::PAYLOAD_LIMIT;
|
||||
use crate::config::PROXY_TIMEOUT;
|
||||
use crate::database::methods::InsertableForm;
|
||||
|
@ -29,7 +31,14 @@ pub async fn forward(
|
|||
// if check_route returns true,
|
||||
// the user supposedly tried to access a restricted page.
|
||||
// They get redirected to the main page.
|
||||
if check_route(route) {
|
||||
if route.starts_with("/apps/files") {
|
||||
// exception for /apps/files: always redirect to /apps/forms
|
||||
debug(&format!("Files route blocked: {}", route));
|
||||
return Ok(web_redir("/apps/forms").await.map_err(|e| {
|
||||
eprintln!("error_redirect: {}", e);
|
||||
crash(get_lang(&req), "error_redirect")
|
||||
})?);
|
||||
} else if check_route(route) {
|
||||
debug(&format!("Restricted route blocked: {}", route));
|
||||
return Ok(web_redir("/").await.map_err(|e| {
|
||||
eprintln!("error_redirect: {}", e);
|
||||
|
@ -43,8 +52,8 @@ pub async fn forward(
|
|||
// (prevents the user from sending some specific POST requests)
|
||||
if check_request(route, &body) {
|
||||
debug(&format!(
|
||||
"Restricted request: {}",
|
||||
String::from_utf8_lossy(&body)
|
||||
"Restricted request: {}",
|
||||
String::from_utf8_lossy(&body)
|
||||
));
|
||||
return Err(crash(get_lang(&req), "error_dirtyhacker"));
|
||||
}
|
||||
|
@ -61,16 +70,16 @@ pub async fn forward(
|
|||
// and basic-auth, because this feature is not needed.
|
||||
for (header_name, header_value) in res
|
||||
.headers()
|
||||
.iter()
|
||||
.filter(|(h, _)| *h != "connection" && *h != "content-encoding")
|
||||
.iter()
|
||||
.filter(|(h, _)| *h != "connection" && *h != "content-encoding")
|
||||
{
|
||||
client_resp.header(header_name.clone(), header_value.clone());
|
||||
}
|
||||
|
||||
|
||||
// sparing the use of a mutable body when not needed
|
||||
// For now, the body only needs to be modified when the route
|
||||
// is "create a new form" route
|
||||
if route == "/apps/forms/api/v1/form" {
|
||||
if route == "/ocs/v2.php/apps/forms/api/v1.1/form" {
|
||||
// retreive the body from the request result
|
||||
let response_body = res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| {
|
||||
eprintln!("error_forward_resp: {}", e);
|
||||
|
@ -82,15 +91,20 @@ pub async fn forward(
|
|||
let form_id = check_new_form(&response_body);
|
||||
if form_id > 0 {
|
||||
debug(&format!(
|
||||
"New form. Forging request to set isAnonymous for id {}",
|
||||
form_id
|
||||
"New form. Forging request to set isAnonymous for id {}",
|
||||
form_id
|
||||
));
|
||||
|
||||
let forged_body = format!(
|
||||
r#"{{"id":{},"keyValuePairs":{{"isAnonymous":true}}}}"#,
|
||||
form_id
|
||||
);
|
||||
let update_req = forge_from("/apps/forms/api/v1/form/update", &req, &url, &client)
|
||||
let update_req = forge_from(
|
||||
"/ocs/v2.php/apps/forms/api/v1.1/form/update",
|
||||
&req,
|
||||
&url,
|
||||
&client,
|
||||
)
|
||||
.set_header("content-length", forged_body.len())
|
||||
.set_header("content-type", "application/json;charset=utf-8");
|
||||
|
||||
|
@ -104,19 +118,19 @@ pub async fn forward(
|
|||
eprintln!("error_forward_clientresp_newform: {}", e);
|
||||
crash(get_lang(&req), "error_forward_clientresp_newform")
|
||||
})?)
|
||||
} else {
|
||||
Ok(
|
||||
client_resp.body(res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| {
|
||||
eprintln!("error_forward_clientresp_newform: {}", e);
|
||||
crash(get_lang(&req), "error_forward_clientresp_std")
|
||||
})?),
|
||||
)
|
||||
}
|
||||
else {
|
||||
Ok(client_resp.body(res.body().limit(PAYLOAD_LIMIT).await.map_err(|e| {
|
||||
eprintln!("error_forward_clientresp_newform: {}", e);
|
||||
crash(get_lang(&req), "error_forward_clientresp_std")
|
||||
})?))
|
||||
}
|
||||
|
||||
|
||||
// check the response before returning it (unused)
|
||||
/*if check_response(route, &response_body) {
|
||||
return Ok(web_redir("/"));
|
||||
}*/
|
||||
return Ok(web_redir("/"));
|
||||
}*/
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -124,19 +138,18 @@ pub struct LoginToken {
|
|||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CsrfToken {
|
||||
pub csrf_token: String,
|
||||
}
|
||||
|
||||
pub async fn forward_login(
|
||||
req: HttpRequest,
|
||||
s: Session,
|
||||
params: web::Path<LoginToken>,
|
||||
client: web::Data<Client>,
|
||||
dbpool: web::Data<DbPool>,
|
||||
) -> Result<HttpResponse, TrainCrash> {
|
||||
// if the user is already logged in, redirect to the Forms app
|
||||
if is_logged_in(&req).is_some() {
|
||||
return Ok(web_redir("/apps/forms").await.map_err(|e| {
|
||||
eprintln!("error_redirect (1:/apps/forms/): {}", e);
|
||||
crash(get_lang(&req), "error_redirect")
|
||||
})?);
|
||||
}
|
||||
|
||||
// check if the provided token seems valid. If not, early return.
|
||||
if !check_token(¶ms.token) {
|
||||
|
@ -150,75 +163,91 @@ pub async fn forward_login(
|
|||
crash(get_lang(&req), "error_forwardlogin_db")
|
||||
})?;
|
||||
|
||||
let moved_token = params.token.clone();
|
||||
// check if the link exists in DB. if it does, update lastvisit_at.
|
||||
let formdata = Form::get_from_token(¶ms.token, &conn)
|
||||
let formdata = web::block(move || Form::get_from_token(¶ms.token, &conn))
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("error_forwardlogin_db_get (diesel error): {}", e);
|
||||
crash(get_lang(&req), "error_forwardlogin_db_get")
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
debug("Token not found.");
|
||||
crash(get_lang(&req), "error_forwardlogin_notfound")
|
||||
})?;
|
||||
.ok_or_else(|| {
|
||||
debug("error: Token not found.");
|
||||
crash(get_lang(&req), "error_forwardlogin_notfound")
|
||||
})?;
|
||||
|
||||
// copy the token in cookies.
|
||||
s.set("sncf_admin_token", &moved_token).map_err(|e| {
|
||||
eprintln!("error_login_setcookie (in login): {}", e);
|
||||
crash(get_lang(&req),"error_login_setcookie")
|
||||
})?;
|
||||
|
||||
// if the user is already logged in, skip the login process
|
||||
// we don't care if someone edits their cookies, Nextcloud will properly
|
||||
// check them anyway
|
||||
if let Some(nc_username) = is_logged_in(&req) {
|
||||
if nc_username.contains(&format!("nc_username={}", formdata.nc_username)) {
|
||||
return Ok(web_redir("/apps/forms").await.map_err(|e| {
|
||||
eprintln!("error_redirect (1:/apps/forms/): {}", e);
|
||||
crash(get_lang(&req), "error_redirect")
|
||||
})?);
|
||||
}
|
||||
}
|
||||
|
||||
// else, try to log the user in with DB data, then redirect.
|
||||
// try to log the user in with DB data, then redirect.
|
||||
login(&client, &req, &formdata.nc_username, &formdata.nc_password).await
|
||||
}
|
||||
|
||||
// creates a NC account using a random name and password.
|
||||
// the account gets associated with a token in sqlite DB.
|
||||
// POST /link route
|
||||
pub async fn forward_register(
|
||||
req: HttpRequest,
|
||||
s: Session,
|
||||
csrf_post: web::Form<CsrfToken>,
|
||||
client: web::Data<Client>,
|
||||
dbpool: web::Data<DbPool>,
|
||||
) -> Result<HttpResponse, TrainCrash> {
|
||||
let lang = get_lang(&req);
|
||||
|
||||
// if the user is already logged in, redirect to the Forms app
|
||||
if is_logged_in(&req).is_some() {
|
||||
return Ok(web_redir("/apps/forms").await.map_err(|e| {
|
||||
eprintln!("error_redirect (2:/apps/forms/): {}", e);
|
||||
crash(get_lang(&req), "error_redirect")
|
||||
})?);
|
||||
}
|
||||
// do not check for existing admin tokens and force a new registration
|
||||
|
||||
// if the user has already generated an admin token, redirect too
|
||||
if let Some(token) = has_admintoken(&req) {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r#"sncf_admin_token=(?P<token>[0-9A-Za-z_\-]*)"#)
|
||||
.expect("Error while parsing the sncf_admin_token regex");
|
||||
}
|
||||
let admin_token = RE
|
||||
.captures(&token)
|
||||
.ok_or_else(|| {
|
||||
eprintln!("error_forwardregister_tokenparse (no capture)");
|
||||
crash(get_lang(&req), "error_forwardregister_tokenparse")
|
||||
})?
|
||||
.name("token")
|
||||
.ok_or_else(|| {
|
||||
eprintln!("error_forwardregister_tokenparse (no capture named token)");
|
||||
crash(get_lang(&req), "error_forwardregister_tokenparse")
|
||||
})?
|
||||
.as_str();
|
||||
// sanitize the token beforehand, cookies are unsafe
|
||||
if check_token(&admin_token) {
|
||||
return Ok(web_redir(
|
||||
&format!(
|
||||
"{}/admin/{}",
|
||||
CONFIG.sncf_url, &admin_token)
|
||||
).await.map_err(|e| {
|
||||
eprintln!("error_redirect (admin): {}", e);
|
||||
crash(get_lang(&req), "error_redirect")
|
||||
})?);
|
||||
} else {
|
||||
debug("Incorrect admin token given in cookies.");
|
||||
debug(&format!("Token: {:#?}", &admin_token));
|
||||
return Err(crash(lang, "error_dirtyhacker"));
|
||||
// check if the csrf token is OK
|
||||
let cookie_csrf_token = s.get::<String>("sncf_csrf_token").map_err(|e| {
|
||||
eprintln!("error_csrf_cookie: {}", e);
|
||||
crash(get_lang(&req), "error_csrf_cookie")
|
||||
})?;
|
||||
if let Some(cookie_token) = cookie_csrf_token {
|
||||
let raw_ctoken =
|
||||
base64::decode_config(cookie_token.as_bytes(), base64::URL_SAFE_NO_PAD).map_err(
|
||||
|e| {
|
||||
eprintln!("error_csrf_cookie (base64): {}", e);
|
||||
crash(get_lang(&req), "error_csrf_cookie")
|
||||
},
|
||||
)?;
|
||||
|
||||
let raw_token =
|
||||
base64::decode_config(csrf_post.csrf_token.as_bytes(), base64::URL_SAFE_NO_PAD)
|
||||
.map_err(|e| {
|
||||
eprintln!("error_csrf_token (base64): {}", e);
|
||||
crash(get_lang(&req), "error_csrf_token")
|
||||
})?;
|
||||
|
||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key());
|
||||
let parsed_token = seed.parse_token(&raw_token).expect("error: token not parsed");
|
||||
let parsed_cookie = seed.parse_cookie(&raw_ctoken).expect("error: cookie not parsed");
|
||||
if !seed.verify_token_pair(&parsed_token, &parsed_cookie) {
|
||||
debug("warn: CSRF token doesn't match.");
|
||||
return Err(crash(lang, "error_csrf_token"));
|
||||
}
|
||||
} else {
|
||||
debug("warn: missing CSRF token.");
|
||||
return Err(crash(lang, "error_csrf_cookie"));
|
||||
}
|
||||
|
||||
let nc_username = gen_name();
|
||||
let nc_password = gen_token();
|
||||
println!("gen_name: {}", nc_username);
|
||||
let nc_password = gen_token(45);
|
||||
// attempts to create the account
|
||||
create_account(&client, &nc_username, &nc_password, lang.clone()).await?;
|
||||
|
||||
|
@ -229,42 +258,49 @@ pub async fn forward_register(
|
|||
crash(lang.clone(), "error_forwardregister_pool")
|
||||
})?;
|
||||
|
||||
let token = gen_token();
|
||||
let token = gen_token(45);
|
||||
|
||||
let token_mv = token.clone();
|
||||
|
||||
// store the result in DB
|
||||
let form_result = Form::insert(
|
||||
InsertableForm {
|
||||
created_at: Utc::now().naive_utc(),
|
||||
lastvisit_at: Utc::now().naive_utc(),
|
||||
token: &token,
|
||||
nc_username: &nc_username,
|
||||
nc_password: &nc_password,
|
||||
},
|
||||
&conn,
|
||||
);
|
||||
let form_result = web::block(move || {
|
||||
Form::insert(
|
||||
InsertableForm {
|
||||
created_at: Utc::now().naive_utc(),
|
||||
lastvisit_at: Utc::now().naive_utc(),
|
||||
token: token_mv,
|
||||
nc_username,
|
||||
nc_password,
|
||||
},
|
||||
&conn,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
if form_result.is_err() {
|
||||
return Err(crash(lang, "error_forwardregister_db"));
|
||||
}
|
||||
|
||||
s.set("sncf_admin_token", &token).map_err(|e| {
|
||||
eprintln!("error_login_setcookie (in register): {}", e);
|
||||
crash(lang.clone(), "error_login_setcookie")
|
||||
})?;
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.set_header(
|
||||
"Set-Cookie",
|
||||
format!("sncf_admin_token={}; HttpOnly; SameSite=Strict", &token),
|
||||
)
|
||||
.body(
|
||||
TplLink {
|
||||
lang: &lang,
|
||||
admin_token: &token,
|
||||
config: &CONFIG,
|
||||
}
|
||||
.render()
|
||||
.map_err(|e| {
|
||||
eprintln!("error_tplrender (TplLink): {}", e);
|
||||
crash(lang.clone(), "error_tplrender")
|
||||
})?,
|
||||
).await.map_err(|e| {
|
||||
}
|
||||
.render()
|
||||
.map_err(|e| {
|
||||
eprintln!("error_tplrender (TplLink): {}", e);
|
||||
crash(lang.clone(), "error_tplrender")
|
||||
})?,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("error_tplrender_resp (TplLink): {}", e);
|
||||
crash(lang, "error_tplrender_resp")
|
||||
})?)
|
||||
|
@ -302,17 +338,37 @@ fn web_redir(location: &str) -> HttpResponse {
|
|||
.finish()
|
||||
}
|
||||
|
||||
pub async fn index(req: HttpRequest) -> Result<HttpResponse, TrainCrash> {
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
TplIndex {
|
||||
lang: &get_lang(&req),
|
||||
}
|
||||
.render()
|
||||
pub async fn index(req: HttpRequest, s: Session) -> Result<HttpResponse, TrainCrash> {
|
||||
let seed = AesGcmCsrfProtection::from_key(get_csrf_key());
|
||||
let (csrf_token, csrf_cookie) = seed
|
||||
.generate_token_pair(None, 43200)
|
||||
.expect("couldn't generate token/cookie pair");
|
||||
|
||||
s.set("sncf_csrf_token", &base64::encode_config(&csrf_cookie.value(), base64::URL_SAFE_NO_PAD)).map_err(|e| {
|
||||
eprintln!("error_login_setcookie (in index): {}", e);
|
||||
crash(get_lang(&req), "error_login_setcookie")
|
||||
})?;
|
||||
|
||||
let cookie_admin_token = s.get::<String>("sncf_admin_token").map_err(|e| {
|
||||
eprintln!("error_forwardregister_tokenparse (index): {}", e);
|
||||
crash(get_lang(&req), "error_forwardregister_tokenparse")
|
||||
})?;
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(
|
||||
TplIndex {
|
||||
lang: &get_lang(&req),
|
||||
csrf_token: &base64::encode_config(&csrf_token.value(), base64::URL_SAFE_NO_PAD),
|
||||
sncf_admin_token: cookie_admin_token,
|
||||
}
|
||||
.render()
|
||||
.map_err(|e| {
|
||||
eprintln!("error_tplrender (TplIndex): {}", e);
|
||||
crash(get_lang(&req), "error_tplrender")
|
||||
})?,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("error_tplrender (TplIndex): {}", e);
|
||||
crash(get_lang(&req), "error_tplrender")
|
||||
})?,
|
||||
).await.map_err(|e| {
|
||||
eprintln!("error_tplrender_resp (TplIndex): {}", e);
|
||||
crash(get_lang(&req), "error_tplrender_resp")
|
||||
})?)
|
||||
|
|
46
src/main.rs
|
@ -7,6 +7,9 @@ extern crate diesel;
|
|||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
|
||||
use actix_session::CookieSession;
|
||||
use actix_web::cookie::SameSite;
|
||||
use actix_files::Files;
|
||||
use actix_web::client::Client;
|
||||
use actix_web::{web, App, FromRequest, HttpServer};
|
||||
|
@ -26,11 +29,30 @@ mod forward;
|
|||
mod sniff;
|
||||
mod templates;
|
||||
|
||||
type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;
|
||||
// default to postgres
|
||||
#[cfg(feature = "default")]
|
||||
type DbConn = PgConnection;
|
||||
#[cfg(feature = "default")]
|
||||
embed_migrations!("migrations/postgres");
|
||||
|
||||
embed_migrations!();
|
||||
#[cfg(feature = "postgres")]
|
||||
type DbConn = PgConnection;
|
||||
#[cfg(feature = "postgres")]
|
||||
embed_migrations!("migrations/postgres");
|
||||
|
||||
#[actix_rt::main]
|
||||
#[cfg(feature = "sqlite")]
|
||||
type DbConn = SqliteConnection;
|
||||
#[cfg(feature = "sqlite")]
|
||||
embed_migrations!("migrations/sqlite");
|
||||
|
||||
#[cfg(feature = "mysql")]
|
||||
type DbConn = MysqlConnection;
|
||||
#[cfg(feature = "mysql")]
|
||||
embed_migrations!("migrations/mysql");
|
||||
|
||||
type DbPool = r2d2::Pool<ConnectionManager<DbConn>>;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
/* std::env::set_var("RUST_LOG", "actix_web=debug");
|
||||
env_logger::init();*/
|
||||
|
@ -40,8 +62,13 @@ async fn main() -> std::io::Result<()> {
|
|||
println!("Checking configuration file...");
|
||||
CONFIG.check_version();
|
||||
|
||||
println!("Opening database {}", CONFIG.database_path);
|
||||
let manager = ConnectionManager::<SqliteConnection>::new(&CONFIG.database_path);
|
||||
if CONFIG.database_path.is_empty() {
|
||||
println!("No database specified. Please enter a MySQL, PostgreSQL or SQLite connection string in config.toml.");
|
||||
}
|
||||
|
||||
debug(&format!("Opening database {}", CONFIG.database_path));
|
||||
|
||||
let manager = ConnectionManager::<DbConn>::new(&CONFIG.database_path);
|
||||
let pool = r2d2::Pool::builder()
|
||||
.build(manager)
|
||||
.expect("ERROR: main: Failed to create the database pool.");
|
||||
|
@ -65,12 +92,19 @@ async fn main() -> std::io::Result<()> {
|
|||
.data(pool.clone())
|
||||
.data(Client::new())
|
||||
.data(forward_url.clone())
|
||||
.wrap(
|
||||
CookieSession::signed(&[0; 32])
|
||||
.secure(true)
|
||||
.same_site(SameSite::Strict)
|
||||
.http_only(true)
|
||||
.name("sncf_cookies")
|
||||
)
|
||||
/*.route("/mimolette", web::get().to(login))*/
|
||||
/*.route("/login", web::post().to(forward))*/
|
||||
/*.wrap(middleware::Compress::default())*/
|
||||
.service(Files::new("/assets/", "./templates/assets/").index_file("index.html"))
|
||||
.route("/", web::get().to(index))
|
||||
.route("/link", web::get().to(forward_register))
|
||||
.route("/link", web::post().to(forward_register))
|
||||
.route("/admin/{token}", web::get().to(forward_login))
|
||||
.default_service(web::route().to(forward))
|
||||
.data(String::configure(|cfg| cfg.limit(PAYLOAD_LIMIT)))
|
||||
|
|
24
src/sniff.rs
|
@ -7,7 +7,7 @@ use crate::debug;
|
|||
// if it returns true, cancels the request
|
||||
pub fn check_request(route: &str, body: &web::Bytes) -> bool {
|
||||
match route {
|
||||
"/apps/forms/api/v1/form/update" => rq_form_update(body),
|
||||
"/ocs/v2.php/apps/forms/api/v1.1/form/update" => rq_form_update(body),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ fn rq_form_update(body: &web::Bytes) -> bool {
|
|||
// this part may need code quality improvements
|
||||
// the body MUST come from the "create new form" route
|
||||
// (this is checked upstream)
|
||||
// returns the form UID and the request body
|
||||
pub fn check_new_form(body: &web::Bytes) -> u64 {
|
||||
let req = String::from_utf8_lossy(body);
|
||||
|
||||
|
@ -51,24 +52,36 @@ pub fn check_new_form(body: &web::Bytes) -> u64 {
|
|||
Value::Null
|
||||
});
|
||||
|
||||
if v != Value::Null && v["id"] != Value::Null && v["isAnonymous"] == Value::Null {
|
||||
v["id"].as_u64().unwrap_or_else(|| {
|
||||
if v != Value::Null
|
||||
&& v["ocs"].is_object()
|
||||
&& v["ocs"]["data"].is_object()
|
||||
&& v["ocs"]["data"]["id"] != Value::Null
|
||||
&& v["ocs"]["data"]["isAnonymous"] == Value::Null
|
||||
{
|
||||
//getting form id
|
||||
v["ocs"]["data"]["id"].as_u64().unwrap_or_else(|| {
|
||||
eprintln!("check_new_form: failed to parse formid: {}", v);
|
||||
0
|
||||
})
|
||||
} else {
|
||||
eprintln!("error: check_new_form: can't find formid: {}", v);
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// those routes won't be redirected
|
||||
const BLOCKED_ROUTES: &[&str] = &[
|
||||
"/apps/settings",
|
||||
"/login",
|
||||
"/settings",
|
||||
"/ocs/v",
|
||||
"/remote.php",
|
||||
"/core/templates/filepicker.html",
|
||||
];
|
||||
|
||||
// ...except if they are in this list
|
||||
const ALLOWED_ROUTES: &[&str] = &["/ocs/v2.php/apps/forms/", "/status.php"];
|
||||
|
||||
// checks if the accessed route is allowed for the user.
|
||||
// if it returns true, redirects elsewhere
|
||||
pub fn check_route(route: &str) -> bool {
|
||||
|
@ -76,6 +89,11 @@ pub fn check_route(route: &str) -> bool {
|
|||
|
||||
for r in BLOCKED_ROUTES {
|
||||
if route.starts_with(r) {
|
||||
for s in ALLOWED_ROUTES {
|
||||
if route.starts_with(s) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ use crate::config::Config;
|
|||
#[template(path = "index.html")]
|
||||
pub struct TplIndex<'a> {
|
||||
pub lang: &'a str,
|
||||
pub csrf_token: &'a str,
|
||||
pub sncf_admin_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
|
|
@ -5,6 +5,30 @@
|
|||
font-style: normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
scrollbar-color: #4684f9 #c8dbfd;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #568aec;
|
||||
border-radius: 20px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: Ubuntu,"Ubuntu-R",sans-serif;
|
||||
}
|
||||
|
@ -90,6 +114,7 @@ p {
|
|||
.page-heading-text {
|
||||
width: auto;
|
||||
margin: auto;
|
||||
padding: 1rem;
|
||||
|
||||
}
|
||||
|
||||
|
@ -139,6 +164,10 @@ body, html {
|
|||
transition: all .25s ease-in-out;
|
||||
}
|
||||
|
||||
.ncstyle-button:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.margin-bottom {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
@ -160,6 +189,10 @@ body, html {
|
|||
width: 50vw;
|
||||
}
|
||||
|
||||
.click {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#script-copy {
|
||||
display: none;
|
||||
}
|
||||
|
@ -232,3 +265,40 @@ body, html {
|
|||
50% { opacity: 1; }
|
||||
100% { transform:translate(0,20px); opacity: 0; }
|
||||
}
|
||||
|
||||
|
||||
.lds-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 8px;
|
||||
border: 8px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
}
|
||||
.lds-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.lds-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes lds-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
|
36
templates/assets/index.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
// on clicking Previous button in browser, reset the page
|
||||
// needed to get another CSRF token and remove the spinning wheel
|
||||
window.onpageshow = function() {
|
||||
if (performance.getEntriesByType("navigation")[0].type == "back_forward") {
|
||||
location.reload(false);
|
||||
}
|
||||
}
|
||||
|
||||
let browse_forms_button = get('browse_forms_button');
|
||||
let new_link_button = get('new_link_button');
|
||||
|
||||
// csrf_token is retrieved from server-side template
|
||||
new_link_button.addEventListener('click', function() {
|
||||
get("csrf_token").value = csrf_token;
|
||||
get("new_link").submit();
|
||||
hideButtonsAndSpin();
|
||||
});
|
||||
|
||||
if (browse_forms_button != undefined) {
|
||||
browse_forms_button.addEventListener('click', function () {
|
||||
hideButtonsAndSpin();
|
||||
});
|
||||
}
|
||||
|
||||
function hideButtonsAndSpin() {
|
||||
new_link_button.classList.add("hidden");
|
||||
// hide the access forms button if it exists
|
||||
if (browse_forms_button != undefined) {
|
||||
browse_forms_button.classList.add("hidden");
|
||||
}
|
||||
get('loading_ring').classList.remove("hidden");
|
||||
}
|
||||
|
||||
function get(elemId) {
|
||||
return document.getElementById(elemId);
|
||||
}
|
BIN
templates/assets/screen/en/fields.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
templates/assets/screen/en/formslist.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
templates/assets/screen/en/params.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
templates/assets/screen/en/question.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
templates/assets/screen/en/responses-export.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
templates/assets/screen/en/responses.png
Normal file
After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="{{ lang }}">
|
||||
<html lang="{{ "lang_code"|tr(lang) }}">
|
||||
<head>
|
||||
<title>{{ "index_title"|tr(lang) }} – {{ "index_description"|tr(lang) }}</title>
|
||||
<meta charset="utf-8" />
|
||||
|
@ -7,111 +7,127 @@
|
|||
<meta name="description" content="{{ "meta_description"|tr(lang) }}" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/assets/favicon.ico" />
|
||||
<link rel="stylesheet" href="/assets/index.css?v=1.0" />
|
||||
<link rel="stylesheet" href="/assets/index.css?v=1.2" />
|
||||
<link rel="stylesheet" href="/assets/cloud.css?v=1.0" />
|
||||
<body>
|
||||
<div class="flex page-heading fullheight">
|
||||
<div class="flex page-heading-text">
|
||||
<div class="flex">
|
||||
<a class="flex" href="https://42l.fr"><img class="logo" src="/assets/logo.svg" /></a>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="title">{{ "index_title"|tr(lang) }}<sup class="beta-tag">{{ "index_beta_tag"|tr(lang) }}</sup></h1>
|
||||
<h2 class="title">{{ "index_description"|tr(lang) }}</h2>
|
||||
</div>
|
||||
<noscript><style> .jsonly { display: none } </style></noscript>
|
||||
<script>const csrf_token = "{{ csrf_token }}";</script>
|
||||
<script src="/assets/index.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex page-heading fullheight">
|
||||
<div class="flex page-heading-text">
|
||||
<div class="flex">
|
||||
<a class="flex" href="https://42l.fr"><img class="logo" src="/assets/logo.svg" /></a>
|
||||
</div>
|
||||
<div class="fullwidth flex">
|
||||
<a class="ncstyle-button margin-bottom" href="/link">{{ "index_createform_button"|tr(lang) }}</a>
|
||||
<div>
|
||||
<h1 class="title">{{ "index_title"|tr(lang) }}<sup class="beta-tag">{{ "index_beta_tag"|tr(lang) }}</sup></h1>
|
||||
<h2 class="title">{{ "index_description"|tr(lang) }}</h2>
|
||||
</div>
|
||||
<a class="scroll-down-link scroll-down-arrow"></a>
|
||||
</div>
|
||||
<div class="has-text-centered beta-banner">
|
||||
<h3>{{ "index_beta_banner_desc1"|tr(lang) }}</h3>
|
||||
<p>{{ "index_beta_banner_desc2"|tr(lang) }}<a href="https://42l.fr/Contact">{{ "index_beta_banner_desc_link"|tr(lang) }}</a>.</p>
|
||||
<div class="fullwidth flex">
|
||||
</div>
|
||||
<div>
|
||||
<div class="c-flex c-jumbo c-color-inverted c-color-mailred">
|
||||
<div class="c-fullwidth">
|
||||
<div class="has-text-centered">
|
||||
<br />
|
||||
<br />
|
||||
<p>{{ "index_disclaimer1"|tr(lang) }}</p>
|
||||
<p>{{ "index_disclaimer2"|tr(lang) }}<a href="https://42l.fr/Faire-un-don">{{ "index_disclaimer2_link_org"|tr(lang) }}</a>{{ "index_disclaimer2_or"|tr(lang) }}<a href="https://www.bountysource.com/teams/nextcloud">{{ "index_disclaimer2_nc"|tr(lang) }}</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="c-flex c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen-question.png"><img class="c-img-shadow" alt="" src="/assets/screen-question.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel1_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel1_desc1"|tr(lang) }}</p>
|
||||
<p>{{ "index_panel1_desc2"|tr(lang) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-flex c-flex-reverse c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen-fields.png"><img class="c-img-shadow" alt="" src="/assets/screen-fields.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel2_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel2_desc1"|tr(lang) }}</p>
|
||||
<p>{{ "index_panel2_desc2"|tr(lang) }}<a href="https://github.com/nextcloud/forms/issues?q=is%3Aissue+is%3Aopen+label%3A%22feature%3A+%E2%9D%93+question+types%22">{{ "index_panel2_desc2_link"|tr(lang) }}</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-flex c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen-responses.png"><img class="c-img-shadow" alt="" src="/assets/screen-responses.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel3_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel3_desc1"|tr(lang) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-flex c-flex-reverse c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen-responses-export.png"><img class="c-img-shadow" alt="" src="/assets/screen-responses-export.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel4_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel4_desc1"|tr(lang) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-flex c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen-params.png"><img class="c-img-shadow" alt="" src="/assets/screen-params.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel5_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel5_desc1"|tr(lang) }}</p>
|
||||
<p>{{ "index_panel5_desc2"|tr(lang) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-flex c-flex-reverse c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen-formslist.png"><img class="c-img-shadow" alt="" src="/assets/screen-formslist.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel6_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel5_desc1"|tr(lang) }}</p>
|
||||
<div class="fullwidth flex">
|
||||
<noscript>
|
||||
<a class="ncstyle-button margin-bottom">{{ "index_nojs"|tr(lang) }}</a>
|
||||
</noscript>
|
||||
{% if sncf_admin_token.is_some() %}
|
||||
<a id="browse_forms_button" href="/admin/{{ sncf_admin_token.as_ref().unwrap() }}" class="ncstyle-button margin-bottom">{{ "index_continueform_button"|tr(lang) }}</a>
|
||||
{% endif %}
|
||||
<form id="new_link" action="/link" method="post">
|
||||
<input id="csrf_token" name="csrf_token" type="text" class="hidden">
|
||||
<a id="new_link_button" class="click jsonly ncstyle-button margin-bottom">{{ "index_createform_button"|tr(lang) }}</a>
|
||||
</form>
|
||||
<div id="loading_ring" class="hidden lds-ring"><div></div><div></div><div></div><div></div></div>
|
||||
</div>
|
||||
<a class="scroll-down-link scroll-down-arrow"></a>
|
||||
</div>
|
||||
<div class="has-text-centered beta-banner">
|
||||
<h3>{{ "index_beta_banner_desc1"|tr(lang) }}</h3>
|
||||
<p>{{ "index_beta_banner_desc2"|tr(lang) }}<a href="https://42l.fr/Contact">{{ "index_beta_banner_desc_link"|tr(lang) }}</a>.</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="c-flex c-jumbo c-color-inverted c-color-mailred">
|
||||
<div class="c-fullwidth">
|
||||
<div class="has-text-centered">
|
||||
<br />
|
||||
<br />
|
||||
<p>{{ "index_disclaimer1"|tr(lang) }}</p>
|
||||
<p>{{ "index_disclaimer2"|tr(lang) }}<a href="https://42l.fr/Faire-un-don">{{ "index_disclaimer2_link_org"|tr(lang) }}</a>{{ "index_disclaimer2_or"|tr(lang) }}<a href="https://nextcloud.com/include/">{{ "index_disclaimer2_nc"|tr(lang) }}</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="c-flex c-jumbo c-blue">
|
||||
<a href="https://42l.fr/Rapport-technique" class="c-button" target="_blank">{{ "index_bottom_docs"|tr(lang) }}</a>
|
||||
<a href="https://github.com/nextcloud/forms" class="c-button" target="_blank">{{ "index_bottom_source"|tr(lang) }}</a>
|
||||
<a href="https://github.com/nextcloud/forms/blob/master/LICENSE" class="c-button" target="_blank">{{ "index_bottom_lic"|tr(lang) }}</a>
|
||||
<div class="c-flex c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/question.png"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/question.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel1_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel1_desc1"|tr(lang) }}</p>
|
||||
<p>{{ "index_panel1_desc2"|tr(lang) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-flex c-flex-reverse c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/fields.png"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/fields.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel2_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel2_desc1"|tr(lang) }}</p>
|
||||
<p>{{ "index_panel2_desc2"|tr(lang) }}<a href="https://github.com/nextcloud/forms/issues?q=is%3Aissue+is%3Aopen+label%3A%22feature%3A+%E2%9D%93+question+types%22">{{ "index_panel2_desc2_link"|tr(lang) }}</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-flex c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/responses.png"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/responses.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel3_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel3_desc1"|tr(lang) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-flex c-flex-reverse c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/responses-export.png"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/responses-export.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel4_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel4_desc1"|tr(lang) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-flex c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/params.png"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/params.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel5_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel5_desc1"|tr(lang) }}</p>
|
||||
<p>{{ "index_panel5_desc2"|tr(lang) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-flex c-flex-reverse c-jumbo">
|
||||
<div class="c-subelem">
|
||||
<a target="_blank" href="/assets/screen/{{ "lang_code"|tr(lang) }}/formslist.png"><img class="c-img-shadow" alt="" src="/assets/screen/{{ "lang_code"|tr(lang) }}/formslist.png" /></a>
|
||||
</div>
|
||||
<div class="c-subelem">
|
||||
<h3>{{ "index_panel6_title"|tr(lang) }}</h3>
|
||||
<p>{{ "index_panel6_desc1"|tr(lang) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="c-flex c-jumbo c-blue">
|
||||
<a href="https://42l.fr/Rapport-technique" class="c-button" target="_blank">{{ "index_bottom_docs"|tr(lang) }}</a>
|
||||
<a href="https://git.42l.fr/neil/sncf" class="c-button" target="_blank">{{ "index_bottom_source"|tr(lang) }}</a>
|
||||
<a href="https://git.42l.fr/neil/sncf/src/branch/root/LICENSE" class="c-button" target="_blank">{{ "index_bottom_lic"|tr(lang) }}</a>
|
||||
</div>
|
||||
<br />
|
||||
<div class="has-text-centered page-heading">
|
||||
<br />
|
||||
<div class="has-text-centered page-heading">
|
||||
<br />
|
||||
<h3 class="title">Crédits</h3>
|
||||
<p>{{ "index_credits_desc1"|tr(lang) }}<a href="https://nextcloud.com/">{{ "index_credits_desc1_link"|tr(lang) }}</a>{{ "index_credits_desc1_a"|tr(lang) }}</p>
|
||||
<p>{{ "index_credits_desc2"|tr(lang) }}<a href="https://shelter.moe/@Neil">Neil</a>{{ "index_credits_desc2_for"|tr(lang) }}<a href="https://42l.fr">{{ "index_credits_desc2_org"|tr(lang) }}</a> {{"index_credits_desc3"|tr(lang) }}.</p>
|
||||
<br />
|
||||
</div>
|
||||
</body>
|
||||
<h3 class="title">Crédits</h3>
|
||||
<p>{{ "index_credits_desc1"|tr(lang) }}<a href="https://nextcloud.com/">{{ "index_credits_desc1_link"|tr(lang) }}</a>{{ "index_credits_desc1_a"|tr(lang) }}</p>
|
||||
<p>{{ "index_credits_desc2"|tr(lang) }}<a href="https://shelter.moe/@Neil">Neil</a>{{ "index_credits_desc2_for"|tr(lang) }}<a href="https://42l.fr">{{ "index_credits_desc2_org"|tr(lang) }}</a> (<a href="https://git.42l.fr/neil/sncf">{{"index_credits_desc3"|tr(lang) }}</a>).</p>
|
||||
<br />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|