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

Compare commits

...

92 commits
1.0.0 ... 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
neil abd6c91013 moving to forms OCSv2 API 2020-09-08 18:48:47 +02:00
neil 9f304c4069 adding ALLOWED_ROUTES 2020-09-08 18:36:27 +02:00
neil cba8b52b32 adding a link to the source code in index 2020-08-31 16:44:19 +02:00
neil 4a1e9cca7e mentioning migration page 2020-08-31 16:02:10 +02:00
neil 3324315d8a Merge branch 'root' of ssh://git.42l.fr:42084/neil/sncf into root 2020-08-31 12:31:14 +02:00
neil ec4b587a31 adding /apps/files to forbidden routes 2020-08-31 12:31:04 +02:00
neil 6d52e01ba1 adding some details in the readme 2020-08-31 09:20:49 +00:00
neil f5fa03fdaa Fixing dead wiki link in the readme 2020-08-29 20:25:43 +00:00
36 changed files with 807 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.
@ -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.

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

280
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": "(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."
}
}

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

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 == "/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(&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 {
"/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;
}
}

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