1
0
Fork 0
mirror of https://git.42l.fr/neil/sncf.git synced 2024-05-17 05:06:35 +02:00

Compare commits

...

84 commits
1.0.1 ... root

Author SHA1 Message Date
neil 507e8877c8 Merge branch 'root' of ssh://git.42l.fr:42084/neil/sncf into root 2022-02-24 10:56:12 +01:00
neil 44c0e27a72 announcing breaking change 2022-02-24 10:56:02 +01:00
neil d1c112f12a Mise à jour de 'README.md' 2021-10-11 15:56:24 +00:00
neil fd4d721be4 messed up description in index.html. fixes #37 2021-10-11 16:59:36 +02:00
neil d419aea412 Merge branch 'root' of ssh://git.42l.fr:42084/neil/sncf into root 2021-10-11 16:55:05 +02:00
neil 11504c00e0 compatibility with nextcloud 22 and Forms API v1.1 2021-10-11 16:54:55 +02:00
neil b1fd3fccae adding nc22 / forms 2.3.x info 2021-09-28 19:26:20 +00:00
neil 8d67cb340c fix compilation warning with rust update 2021-07-14 22:42:09 +02:00
neil 730a023e55 fixing number of field types in index 2021-04-15 23:59:58 +02:00
neil e354b5b14b remove token check when clicking on the new form button on index 2021-04-15 23:43:17 +02:00
neil 002a0c9ef2 bumping version 2021-04-15 22:31:42 +02:00
neil fafe2bf3fe Merge branch 'root' of ssh://git.42l.fr:42084/neil/sncf into root 2021-04-15 12:03:36 +02:00
neil 7a4839541d customize scrollbar on firefox and chromium-based. fixes #33 2021-04-15 12:03:07 +02:00
neil 15c73715a8 also replacing bountysource link in readme
Signed-off-by: neil <neil@noreply.example.org>
2021-04-14 22:04:23 +00:00
neil 8fc232d022 replaced bountysource NC link in index (https://help.nextcloud.com/t/dont-use-bountysource-anymore/84943/64) 2021-04-14 23:03:19 +02:00
neil b45e65d427 now refreshing the page when previous browser button is hit. fixes #29 2021-04-14 22:49:31 +02:00
neil ade36cf053 do not check if user is logged in when registering, fix #27 2021-04-14 00:36:31 +02:00
neil 8d6a68b33c adds sncf token admin on login, fixes #28 2021-04-14 00:32:38 +02:00
neil 9612086790 now force login on /admin/<token> route, partial fix for #27 2021-04-14 00:31:18 +02:00
neil 05a15b1680 separate JS and HTML, fixes #32 2021-04-13 23:51:32 +02:00
neil 162cdad7fe fully restrict /apps/files from src/forward 2021-04-13 23:03:09 +02:00
neil 7fbfcf485c force redirection from /apps/files to /apps/forms 2021-04-13 21:16:02 +02:00
neil d6a4a6591a fixing incorrect admin link 2021-04-13 21:15:13 +02:00
neil 83a80c0969 add translation for form access button 2021-04-13 00:03:23 +02:00
neil 9fecf9ace9 implement access your forms button, fixes #30 2021-04-13 00:03:02 +02:00
neil 0d38e2f2d4 add admin token in server-side templates 2021-04-13 00:02:09 +02:00
neil 112ea773a2 create sncf_cookies actix session 2021-04-13 00:01:33 +02:00
neil c24b98bcca use cookies properly, fixes #31 2021-04-13 00:01:08 +02:00
neil 5a521b0497 deprecate old get/set cookie functions 2021-04-12 23:59:22 +02:00
neil 1251b431a6 adding actix-session and updating actix-rt 2021-04-12 22:49:43 +02:00
neil 6e231a73b6 adding Secure cookie param to sncf_admin_token and sncf_csrf_cookie 2021-04-12 20:39:47 +02:00
neil 240baca044 adding compatibility for NC forms until 2.2.4 2021-04-04 09:07:56 +00:00
neil fcc9ceaedc Mise à jour de 'README.md' 2021-03-24 19:48:42 +00:00
neil 8276b35a01 cargo clippy 2021-03-24 20:29:24 +01:00
neil c1191f3f45 cargo fmt 2021-03-24 20:17:08 +01:00
neil 3d6b9f96e8 bumping version 2021-03-24 19:50:40 +01:00
neil 255cf8ba9e updating isAnonymous interception process with new OCS API 2021-03-24 19:50:28 +01:00
neil 31bf380e12 fixing the login forgery process to work around nextcloud issue 2021-03-24 19:49:27 +01:00
neil 3b82283cfd setting user_agent back to actix-web 2021-03-24 19:48:59 +01:00
neil 329fe20553 moving user_agent to constants 2021-03-24 19:48:17 +01:00
neil ae439b25bb adding percent-encoding as a dependency 2021-03-24 19:46:04 +01:00
neil e012505247 updating deps 2021-03-21 23:52:17 +01:00
neil 554cbd25ab Mise à jour de 'README.md' 2021-03-16 17:27:46 +00:00
neil cc8a7a90d8 Merge pull request 'Update lang.json with german translation' (#24) from alpcentaur/sncf:root into root
Reviewed-on: https://git.42l.fr/neil/sncf/pulls/24
2021-03-16 17:17:24 +00:00
neil 0d273bcad2 fixing json syntax 2021-03-16 17:15:59 +00:00
alpcentaur 84a52032fe Update lang.json with german translation 2021-03-10 21:45:11 +00:00
neil 564a138480 Mise à jour de 'README.md' 2021-03-09 16:54:48 +00:00
neil fa368044be Mise à jour de 'README.md' 2020-12-16 17:34:37 +00:00
neil 8097745f01 adding 1.2.0 to compatibility table 2020-11-05 16:20:14 +01:00
neil fa9cd82531 setting csrf token duration to 12 hours 2020-11-05 16:16:19 +01:00
neil 860d14bcd0 tweaking CSS, adding loading ring 2020-11-04 20:04:50 +01:00
neil 6b61ada515 implementing csrf token in backend. forward_register (/link endpont) is now POST 2020-11-04 20:04:29 +01:00
neil 79eb469b20 adding csrf strings 2020-11-04 20:03:05 +01:00
neil 9fac0811a0 downgrading cookie key size from 48 to 32 2020-11-04 20:02:48 +01:00
neil e2fdf128ec bumping version, add csrf dependency 2020-11-04 20:02:21 +01:00
neil 6db862848f implementing csrf in the frontend, adding spambot protection using javascript. Fixes #16. Fixes #9. 2020-11-04 20:01:55 +01:00
neil 3b764f2ac7 adding entry for nojs translation 2020-11-03 19:06:26 +01:00
neil 151ad7b5aa adding cookie_key field, better error message on outdated config. bump cfg version 2020-11-03 19:05:08 +01:00
neil 6bfd488036 adding cookie_key field in config.toml 2020-11-03 19:03:57 +01:00
neil d4c1979e79 Merge branch 'root' of ssh://git.42l.fr:42084/neil/sncf into root 2020-11-03 17:02:07 +01:00
neil 6760517c70 now using database pool for db. Fixes #21 2020-11-03 17:02:03 +01:00
neil 7dafaacee0 Mise à jour de 'README.md' 2020-11-01 17:43:11 +00:00
neil f07c8f960f updating deps 2020-11-01 18:28:03 +01:00
neil 06b47ed63e removing db folder 2020-11-01 18:06:17 +01:00
neil 23f1f9d1c9 Merge branch 'root' of ssh://git.42l.fr:42084/neil/sncf into root 2020-11-01 18:04:41 +01:00
neil fc079635db null'ing the database path parameter by default 2020-11-01 18:04:35 +01:00
neil 8dd5486250 implementing multiple database systems support! 2020-11-01 18:03:42 +01:00
neil cf26c8bd44 adding fmt rules 2020-11-01 18:02:41 +01:00
neil 50f3ed4e90 do not disclose database path in stdout unless debug_mode is enabled 2020-11-01 18:02:26 +01:00
neil cab8136e1e adding feature flags 2020-11-01 18:00:55 +01:00
neil ff233aa5c9 checked compatibility for NC 20.0.1 2020-10-31 12:28:55 +00:00
neil e496e5ae9a Mise à jour de 'README.md' 2020-10-07 14:39:49 +00:00
neil 5bd21a5782 Mise à jour de 'README.md' 2020-10-07 14:39:24 +00:00
neil a69642ac11 Merge branch 'root' of ssh://git.42l.fr:42084/neil/sncf into root 2020-10-03 21:05:47 +02:00
neil e79f86ada9 adding lang_code entry in lang.json, fixes #19 2020-10-03 21:05:40 +02:00
neil c71a936f8d adding 1.0.2 in compatibility table 2020-09-16 20:32:42 +00:00
neil 0bb3584921 updating deps 2020-09-16 22:24:55 +02:00
neil fdd6b61d08 editing the source code links --> sncf, seems more appropriate (?) 2020-09-16 22:08:49 +02:00
neil 5bf70566ac adding comments in config.rs 2020-09-16 22:08:19 +02:00
neil 6a9465d579 adding random token at the end of generated names. Fixes #14 2020-09-16 20:19:21 +02:00
neil e4cab99f9e adding padding on page-heading-text. Fixes #11 2020-09-16 19:12:10 +02:00
neil dfb8a9428b Merge branch 'root' of ssh://git.42l.fr:42084/neil/sncf into root 2020-09-16 17:03:52 +02:00
neil c3c8f7af64 translated screenshots and reworked assets directory structure. Fixes #15 2020-09-16 17:03:43 +02:00
neil 45b5cc16a2 adding compatibility for 2.0.4 2020-09-08 16:53:02 +00:00
36 changed files with 795 additions and 364 deletions

1
.gitignore vendored
View file

@ -11,4 +11,3 @@ Cargo.lock
**/*.rs.bk
/config.toml
/db/sncf.sqlite

View file

@ -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"

View file

@ -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.
@ -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.

View file

@ -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

View file

278
lang.json
View file

@ -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": "source code",
"fr": "code source"
"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."
}
}

View 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
);

View file

@ -0,0 +1 @@
DELETE TABLE form;

View 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
);

View file

@ -0,0 +1 @@
DELETE TABLE form;

1
rustfmt.toml Normal file
View file

@ -0,0 +1 @@
reorder_imports = true

View file

@ -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]
}

View file

@ -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,8 +67,11 @@ 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!\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);
@ -70,3 +79,9 @@ impl Config {
}
}
}
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
}

View file

@ -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),

View file

@ -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,

View file

@ -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 == "/ocs/v2.php/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("/ocs/v2.php/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(&params.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(&params.token, &conn)
let formdata = web::block(move || Form::get_from_token(&params.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")
})?)

View file

@ -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)))

View file

@ -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 {
"/ocs/v2.php/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,12 +52,19 @@ 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
}
}
@ -68,13 +76,11 @@ const BLOCKED_ROUTES: &[&str] = &[
"/settings",
"/ocs/v",
"/remote.php",
"/apps/files",
"/core/templates/filepicker.html",
];
// ...except if they are in this list
const ALLOWED_ROUTES: &[&str] = &[
"/ocs/v2.php/apps/forms/",
];
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

View file

@ -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)]

View file

@ -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
View 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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View file

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -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> (<a href="https://git.42l.fr/neil/sncf">{{"index_credits_desc3"|tr(lang) }}</a>).</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>