Compare commits

...

168 Commits
v1.0 ... master

Author SHA1 Message Date
divlo
36f41da726 chore: archive the repository 2021-04-17 00:02:56 +02:00
dependabot[bot]
6bfcb74ad9
build(deps): bump elliptic from 6.5.3 to 6.5.4 in /website () 2021-03-10 13:28:11 +01:00
divlo
92cd105ed4 docs: add issues templates files 2020-12-28 13:12:33 +01:00
divlo
0fc5716da7 style: fix standard issues 2020-12-28 13:11:02 +01:00
divlo
c285d3b69e feat: add PWA support 2020-12-28 13:07:21 +01:00
divlo
c594577415 ci: delete s-divlo-fr part 2020-12-28 11:46:44 +01:00
divlo
3ed686cddd ci: add GitHub Actions 2020-12-28 11:44:23 +01:00
divlo
7921d06785 build(deps): update latest version 2020-12-28 11:43:08 +01:00
divlo
80ec73f3df build(deps): bump MySQL 5.7 to 8.0.22 2020-12-28 11:16:48 +01:00
dependabot[bot]
739b8cacb1
build(deps): bump date-and-time from 0.14.1 to 0.14.2 in /website () 2020-12-24 22:51:47 +01:00
divlo
413bea05b9 build(deps): update latest version 2020-12-22 13:45:15 +01:00
dependabot[bot]
02afd47f23
build(deps): bump ini from 1.3.5 to 1.3.8 in /s.divlo.fr () 2020-12-12 13:43:11 +01:00
dependabot[bot]
2cbaeda4d8
build(deps): bump ini from 1.3.5 to 1.3.8 in /website () 2020-12-12 13:41:45 +01:00
dependabot[bot]
b60da974db
build(deps): bump ini from 1.3.5 to 1.3.8 in /api () 2020-12-12 13:39:51 +01:00
divlo
4efd702ca3 fix: s.divlo.fr database password environment 2020-10-30 22:37:04 +01:00
Divlo
cbe82f74a9
Merge pull request from Divlo/release/v2.2
Release/v2.2
2020-10-30 17:28:58 +01:00
divlo
df105364f8 style: add whitespace 2020-10-30 17:26:54 +01:00
divlo
20f9259ab3 docs: update version and README 2020-10-30 17:25:25 +01:00
divlo
944d5c4972 feat(api): rate limiting 2020-10-30 17:16:53 +01:00
divlo
ffec0058e5 feat: add docker support and update deps 2020-10-30 16:58:27 +01:00
Divlo
f5a8be5000
Merge pull request from Divlo/dependabot/npm_and_yarn/website/next-9.5.4
build(deps): bump next from 9.5.1 to 9.5.4 in /website
2020-10-08 22:46:08 +02:00
dependabot[bot]
dbe02aa8bb
build(deps): bump next from 9.5.1 to 9.5.4 in /website
Bumps [next](https://github.com/vercel/next.js) from 9.5.1 to 9.5.4.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v9.5.1...v9.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-08 20:16:57 +00:00
divlo
55c467f1e4 linkShortener: Count clicks 2020-08-04 12:12:27 +02:00
divlo
9f068614f5 Fix UTC time 2020-08-04 11:42:21 +02:00
divlo
3877e974c5 linkShortener: Add a link in the list too 2020-08-04 11:36:20 +02:00
Divlo
d8137a8c5b
Merge pull request from Divlo/v2.1
v2.1
2020-08-03 21:25:50 +02:00
divlo
b031379575 🙈 Ignore .env.production 2020-08-03 20:59:10 +02:00
divlo
cc899a020a Website : ShortLinks 2020-08-03 20:56:01 +02:00
divlo
eba7858caf API: ShortLinks - GET/POST/PUT/DELETE 2020-08-03 18:39:35 +02:00
divlo
fbfd8899e9 s.divlo.fr : linkShortener 2020-08-03 17:15:32 +02:00
divlo
653068c9c7 🎨 website/utils API_URL + standardJS 2020-08-03 14:37:05 +02:00
divlo
4be7a46a10 🎨 standardJS all files 2020-08-03 14:14:45 +02:00
divlo
dc962c9120 ️ UserCard: React.memo + <a> instead of <div> 2020-08-03 13:18:58 +02:00
divlo
337868cf5f 🐛 getUserInfo : quotes isValidated: true 2020-08-03 12:54:19 +02:00
divlo
c687a153c3 🐛 normalizeEmail is now disabled 2020-08-03 12:38:58 +02:00
divlo
c4b7fdce76 🚀 Update version number (2.1) 2020-08-03 12:28:48 +02:00
divlo
b118bd5981 ️ FunctionCard: React.memo + <a> instead of <div> 2020-08-03 12:21:35 +02:00
divlo
5f2de3a108 🎨 .jsx files extensions 2020-08-03 12:11:08 +02:00
divlo
58f47c7480 🎨 Configure standardJS 2020-08-03 12:04:07 +02:00
Divlo
e22e62a749
Merge pull request from Divlo/dependabot/npm_and_yarn/website/elliptic-6.5.3
build(deps): bump elliptic from 6.5.2 to 6.5.3 in /website
2020-08-01 20:18:39 +02:00
dependabot[bot]
a046d03a05
build(deps): bump elliptic from 6.5.2 to 6.5.3 in /website
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-01 17:57:52 +00:00
Divlo
ce4abcbbdc
Merge pull request from Divlo/dependabot/npm_and_yarn/website/lodash-4.17.19
build(deps): bump lodash from 4.17.15 to 4.17.19 in /website
2020-07-20 02:38:03 +02:00
Divlo
5ae0e94060
Merge pull request from Divlo/dependabot/npm_and_yarn/api/lodash-4.17.19
build(deps): bump lodash from 4.17.15 to 4.17.19 in /api
2020-07-20 02:37:50 +02:00
dependabot[bot]
7f79c1d7d1
build(deps): bump lodash from 4.17.15 to 4.17.19 in /website
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-19 23:24:41 +00:00
dependabot[bot]
05f1e640b1
build(deps): bump lodash from 4.17.15 to 4.17.19 in /api
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-19 23:16:08 +00:00
Divlo
02078ecc7a 🐛 FIX: Changement de logique pour rightPrice 2020-05-15 20:49:59 +02:00
Divlo
008f518f96 🚀 RELEASE: Version 2.0 2020-05-13 19:37:24 +02:00
Divlo
46dce42826 📖 DOC: backup.sql 2020-05-13 00:25:23 +02:00
Divlo
45df3a6851 👌 IMPROVE: Custom express server - NextJS website 2020-05-13 00:23:43 +02:00
Divlo
a11725d6ec 🐛 FIX: Small fixes 2020-05-13 00:08:51 +02:00
Divlo
84348df7d1 📦 NEW: Ajouts des articles de certaines fonctions 2020-05-08 15:56:20 +02:00
Divlo
0db96aaeb9 📦 NEW: Ajout de la fonction sortArray 2020-05-07 16:45:05 +02:00
Divlo
cd2f635ca3 📦 NEW: Ajout de la fonction fibonacci 2020-05-07 16:03:27 +02:00
Divlo
4f6f448e40 📦 NEW: Ajout de la fonction findLongestWord 2020-05-07 15:35:04 +02:00
Divlo
a2ee5e4f44 👌 IMPROVE: Commentaires sur profil en Markdown 2020-05-07 11:55:53 +02:00
Divlo
6c8e57540b 📦 NEW: Ajout de la fonction isPalindrome 2020-05-07 11:36:22 +02:00
Divlo
aca93676e9 🐛 FIX: randomQuote + backup fixes 2020-05-06 12:17:43 +02:00
Divlo
015b0be11f 📦 NEW: react-markdown pour les commentaires 2020-05-06 10:39:22 +02:00
Divlo
e52847171d 📦 NEW: Supprimer une fonction frontend 2020-05-06 09:55:27 +02:00
Divlo
23779087a6 📦 NEW: Édition d'un commentaire (frontend) 2020-05-05 13:11:18 +02:00
Divlo
ea5ee96845 📦 NEW: backend PUT /comments 2020-05-04 17:19:51 +02:00
Divlo
6848c790ed 📖 DOC: Ajout de la page /about + numéro version 2020-05-04 15:32:44 +02:00
Divlo
1342df0210 📖 DOC: README.md 2020-05-04 15:00:09 +02:00
Divlo
516be91519 📦 NEW: frontend /users 2020-05-04 12:58:36 +02:00
Divlo
db3f940da5 📦 NEW: GET /users 2020-05-02 15:51:51 +02:00
Divlo
650bcac05a 👌 IMPROVE: Frontend refactoring, fixes etc 2020-05-01 21:01:03 +02:00
Divlo
c36b0a46ab 👌 IMPROVE: getPagesHelper refactoring code API 2020-05-01 20:41:59 +02:00
Divlo
56728e227d 🐛 FIX: Import config CommentCard 2020-05-01 02:29:10 +02:00
Divlo
b628aaecce 👌 IMPROVE: Gestion erreur images fonctions 2020-05-01 02:27:30 +02:00
Divlo
0e591ae62f 👌 IMPROVE: Petits changements 2020-04-29 23:29:42 +02:00
Divlo
075d455fbd 📦 NEW: Ajout de chronometerTimer 2020-04-29 18:51:30 +02:00
Divlo
b21d410a88 👌 IMPROVE: import useEffect en trop rightPrice 2020-04-28 19:28:15 +02:00
Divlo
d30b8c4049 📖 DOC: backup.sql 2020-04-28 18:48:16 +02:00
Divlo
82884e7d4a 📦 NEW: rightPrice frontend 2020-04-28 18:46:13 +02:00
Divlo
df512dea12 🐛 FIX: rightPrice est convertit en Number 2020-04-28 18:06:12 +02:00
Divlo
fbe225fbbb 📦 NEW: rightPrice backend 2020-04-27 22:06:37 +02:00
Divlo
1ae744d3db 👌 IMPROVE: API et frontend /admin + API https 2020-04-27 18:24:30 +02:00
Divlo
bc249f00e4 🐛 FIX: FunctionsList changement de page/catégorie 2020-04-25 00:11:34 +02:00
Divlo
8d7b3278c7 📦 NEW: Ajout de la fonction toDoList 2020-04-24 14:43:52 +02:00
Divlo
699ce6ec36 🐛 FIX: /users à la place de /profile 2020-04-23 17:53:39 +02:00
Divlo
cb6233843a 🐛 FIX: /forgotPassword = /users/forgotPassword 2020-04-23 17:47:35 +02:00
Divlo
316d2d1107 👌 IMPROVE: Changements URL /profile en /users 2020-04-23 17:45:21 +02:00
Divlo
3ff7f40f7b 👌 IMPROVE: Refactoring /functions/[slug] 2020-04-23 17:31:36 +02:00
Divlo
3e6b7fcd66 👌 IMPROVE: Refactoring /functions/randomQuote 2020-04-23 16:58:12 +02:00
Divlo
2c69526d28 📦 NEW: backend: Tasks 2020-04-23 15:44:39 +02:00
Divlo
de44b1db98 📦 NEW: Function linkShortener backend 2020-04-23 11:55:38 +02:00
Divlo
f2be062754 📖 DOC: Mise en place du fichier backup.sql 2020-04-23 10:14:56 +02:00
Divlo
4454f2e089 🐛 FIX: Bug et améliorations globale 2020-04-23 01:08:54 +02:00
Divlo
032ef566c7 📦 NEW: Profil dernières citations proposées 2020-04-22 22:01:18 +02:00
Divlo
43add978b6 📦 NEW: randomQuote page personnalisée 2020-04-22 20:44:06 +02:00
Divlo
f2df72fe29 📦 NEW: backend: randomQuote Function 2020-04-22 12:21:02 +02:00
Divlo
88e4e8c0cc 📦 NEW: FunctionForm type 'select' 2020-04-21 18:03:16 +02:00
Divlo
25582e4cc7 👌 IMPROVE: Ajustements et Hotfix 2020-04-20 17:53:44 +02:00
Divlo
c1fe81a921 📦 NEW: Modifier formulaire d'une fonction - admin 2020-04-16 19:44:19 +02:00
Divlo
cda263fd75 🐛 FIX: Downgrade suneditor-react + fixes 2020-04-16 00:19:31 +02:00
Divlo
9c5d1fc06b 📦 NEW: frontend: Modifier info et Article 2020-04-15 22:50:40 +02:00
Divlo
5eb64d200b 👌 IMPROVE: backend modifier isOnline 2020-04-15 21:32:25 +02:00
Divlo
9336bf12fd 👌 IMPROVE: GET /admin/functions/:slug 2020-04-15 16:40:09 +02:00
Divlo
5d791f34d6 📦 NEW: PUT /admin/functions/form et article/:id 2020-04-15 14:17:57 +02:00
Divlo
43d2180209 👌 IMPROVE: backend - Hotfix suppression fichiers 2020-04-15 13:55:07 +02:00
Divlo
3d0488a605 backend: PUT /admin/functions/:id 2020-04-13 22:11:34 +02:00
Divlo
20d89d7002 frontend: Hotfix isEditing category 2020-04-13 18:29:42 +02:00
Divlo
5a26d28e34 frontend: Modifier/supprimer une catégorie /admin 2020-04-13 18:26:45 +02:00
Divlo
f7c047120d backend: /admin/categories 2020-04-12 22:38:13 +02:00
Divlo
887b4b4c2c Hotfix: README.md github logo 2020-04-12 17:47:14 +02:00
Divlo
20bbe9b553 Update README.md 2020-04-12 17:39:05 +02:00
Divlo
d902d40e6e frontend: manageCategories et Hotfix date-and-time 2020-04-12 12:40:56 +02:00
Divlo
42193066a8 frontend et backend: Crée une nouvelle fonction 2020-04-11 23:29:22 +02:00
Divlo
c157f7e922 frontend: FunctionsList et début de /admin 2020-04-11 21:07:13 +02:00
Divlo
02058fc2fb frontend: Ajouter/Retirer une fonction des favoris 2020-04-10 22:50:24 +02:00
Divlo
0ac2fbf3ab frontend et backend: Supprimer un commentaire 2020-04-10 20:58:19 +02:00
Divlo
39a332a867 frontend: Voir et Poster des commentaires 2020-04-10 20:20:07 +02:00
Divlo
903590fb08 backend: GET, POST, DELETE /favorites/:functionId 2020-04-10 00:01:39 +02:00
Divlo
9152313110 backend: GET et POST /comments/:functionId 2020-04-09 14:31:33 +02:00
Divlo
84dae869ef frontend: Hotfix inputState pour modifier profil 2020-04-09 11:32:33 +02:00
Divlo
42672399ff Hotfix: maxAge cookie 'user' - expires in 1 week 2020-04-08 20:15:35 +02:00
Divlo
ca0c77a522 Hotfix: withoutAuth + ajustements UserContext 2020-04-08 15:26:18 +02:00
Divlo
5d048f3010 frontend: Édition profil + corrections backend 2020-04-08 00:47:24 +02:00
Divlo
3ed605af1b backend: PUT /users - Supprime les anciens logo 2020-04-07 17:15:01 +02:00
Divlo
5bf9a2ade6 backend: PUT /users - Modifier le profil 2020-04-07 16:21:48 +02:00
Divlo
de28cbe706 frontend: Mot de passe oublié + ajustement backend 2020-04-07 13:56:56 +02:00
Divlo
7d69561499 frontend et backend : Profil Public 2020-04-06 23:06:21 +02:00
Divlo
76298b6087 frontend et backend: Connexion d'un utilisateur 2020-04-06 16:46:03 +02:00
Divlo
8c37dbaaf4 backend: Mot de passe oublié + Securité Next maj
TODO: Page de connexion frontend et profil public
2020-03-31 07:48:00 +02:00
Divlo
49da9d5d48 back: DELETE /admin/functions/:id 2020-03-25 23:30:33 +01:00
Divlo
cec6881308 backend: POST /admin/functions créé une fonction 2020-03-25 22:59:08 +01:00
Divlo
96af78f967 frontend: Hotfix loading 2020-03-25 18:46:23 +01:00
Divlo
7d49422108 frontend: Page d'inscription + Hotfix backend 2020-03-25 18:22:03 +01:00
Divlo
2239df70b3 backend: Login/Inscription 2020-03-25 16:23:43 +01:00
Divlo
84712eb251 backend: Hotfix convertRomanToArabic + exports 2020-03-24 17:02:30 +01:00
Divlo
2c61a22787 frontend et backend: Formulaire dynamique 2020-03-24 16:11:55 +01:00
Divlo
b479e1501f frontend: FunctionTabs en plusieurs components 2020-03-24 09:54:50 +01:00
Divlo
53330095be frontend: Système d'onglets pour /functions/slug 2020-03-23 21:22:41 +01:00
Divlo
0f8c1d3569 backend: GET /functions/:slug 2020-03-23 16:50:31 +01:00
Divlo
96f18ead42 Hotfix et Modèle (backend) complet d'une fonction 2020-03-23 14:42:26 +01:00
Divlo
9d9aed8bcf frontend: Landing page 2020-03-22 16:21:19 +01:00
Divlo
02b3ef84f0 frontend: Custom server Next.js 2020-03-22 03:52:04 +01:00
Divlo
60d6d56bba Hotfix: Nom des dossiers + Ajout /functions/slug 2020-03-21 23:54:08 +01:00
Divlo
fd7fc44fe7 frontend: Preloader & 404 Error page 2020-03-21 23:03:30 +01:00
Divlo
3debd85a70 frontend: NProgress Loader entre chaque page 2020-03-21 20:48:46 +01:00
Divlo
c7115144bf frontend: Hotfix Loader height 2020-03-21 18:36:57 +01:00
Divlo
ea13959c91 frontend: Pagination au scroll 2020-03-21 18:24:10 +01:00
Divlo
895d0c7f6b frontend: Loader + Refactoring 2020-03-21 16:43:37 +01:00
Divlo
a0fb5ee13a frontend: Console.log index.js 2020-03-20 23:38:30 +01:00
Divlo
c6116b9da1 frontend: Trier par catégorie et recherche 2020-03-20 23:35:51 +01:00
Divlo
404af8be6a backend: Chercher par catégorie et recherche 2020-03-20 22:57:43 +01:00
Divlo
cb3ca25aac frontend: FunctionCard CSS Function__info bottom 2020-03-20 18:36:48 +01:00
Divlo
5edf5e7069 frontend: Affichage dynamique des fonctions
TODO: Rendre fonctionnel la rechercher et le tri par catégorie
2020-03-20 18:26:55 +01:00
Divlo
c9f817930c backend: Ajout de Sequelize, GET /functions
TODO: Trier par catégorie et chercher les fonctions
2020-03-20 16:49:45 +01:00
Divlo
19dfe6ae8d backend: Ajout de convertEncoding 2020-03-20 12:21:23 +01:00
Divlo
ccfa35d24e backend: Hotfiix next not defined weatherRequest 2020-03-20 11:49:45 +01:00
Divlo
a605b781f4 backend: AJout de Heap's algorithm + .env.example 2020-03-20 11:45:19 +01:00
Divlo
b0ce56a035 backend: Small Hotfix 2020-03-19 23:21:20 +01:00
Divlo
a2b5a02f6b backend: Error handling middleware 2020-03-19 22:59:06 +01:00
Divlo
c324676e89 backend: Ajout de calculateAge 2020-03-19 21:46:54 +01:00
Divlo
e281b3dbec backend: ajout de convertCurrency 2020-03-19 19:49:43 +01:00
Divlo
280982a8d5 frontend: Select et Search responsive 2020-03-19 17:46:43 +01:00
Divlo
d66c4037db frontend: Rechercher et trier les fonctions 2020-03-19 17:19:59 +01:00
Divlo
4d1f31afe3 frontend: Mise à jour grid.css pour FunctionCard 2020-03-19 16:36:43 +01:00
Divlo
0be0abf13c frontend: Functions page et FunctionCard 2020-03-19 16:13:51 +01:00
Divlo
d4fb1e3aba frontend: Layout (Header, Content, Footer) _app.js 2020-03-19 09:07:29 +01:00
Divlo
a82c1d8416 frontend: Mise en place de NextJS 2020-03-18 16:26:18 +01:00
Divlo
6a90fc764d backend: Limitation d'appel à l'api openweathermap.org pour la fonction weatherRequest 2020-03-17 22:14:25 +01:00
Divlo
27d6531a98 backend: Ajout de la fonction weatherRequest + Hotfix: armstrongNumber → NaN 2020-03-17 19:37:45 +01:00
Divlo
fd3224ee42 backend: Ajouts de fonctions à exécuter pour /functions/:functionName 2020-03-17 18:35:03 +01:00
Divlo
f631873893 frontend: Structure fichier + Header et navigation 2020-03-17 15:34:26 +01:00
Divlo
1b4a7a1c09 backend: Première route /functions/:functionName + Gestion des erreurs 404 et 500 2020-03-17 12:02:12 +01:00
Divlo
c8ffeea5da Initial commit 2020-03-16 22:55:32 +01:00
379 changed files with 46970 additions and 18046 deletions
.github
.gitignoreLICENSEREADME.md
api
css

131
.github/CODE_OF_CONDUCT.md vendored Normal file

@ -0,0 +1,131 @@
# Code de conduite _Contributor Covenant_
## Notre engagement
En tant que membres, contributeur•trice•s et dirigeant•e•s, nous nous
engageons à faire de la participation à notre communauté
une expérience sans harcèlement, quel que soit l'âge,
la taille corporelle, le handicap visible ou invisible, l'appartenance ethnique,
les caractéristiques sexuelles, l'identité et l'expression de genre,
le niveau d'expérience, l'éducation, le statut socio-économique,
la nationalité, l'apparence personnelle, la race, la religion,
ou l'identité et l'orientation sexuelle.
Nous nous engageons à agir et interagir de manière à contribuer à une communauté
ouverte, accueillante, diversifiée, inclusive et saine.
## Nos critères
Exemples de comportements qui contribuent à créer un environnement positif :
- Faire preuve d'empathie et de bienveillance envers les autres
- Être respectueux des opinions, points de vue et expériences divergents
- Donner et recevoir avec grâce les critiques constructives
- Assumer ses responsabilités et s'excuser auprès des personnes affectées par nos erreurs et apprendre de ces expériences
- Se concentrer sur ce qui est le meilleur non pas uniquement pour nous en tant qu'individu, mais aussi pour l'ensemble de la communauté
Exemples de comportements inacceptables :
- L'utilisation de langage ou d'images sexualisés et d'attentions ou d'avances sexuelles de toute nature
- Le _trolling_, les commentaires insultants ou désobligeants et les attaques
personnelles ou d'ordre politique
- Le harcèlement en public ou en privé
- La publication d'informations privées d'autrui, telle qu'une
adresse postale ou une adresse électronique, sans leur autorisation explicite
- Toute autre conduite qui pourrait raisonnablement être considérée comme inappropriée
dans un cadre professionnel
## Responsabilités d'application
Les dirigeant•e•s de la communauté sont chargé•e•s de clarifier et de faire respecter nos normes de
comportements acceptables et prendront des mesures correctives appropriées et équitables en
réponse à tout comportement qu'ils ou elles jugent inapproprié, menaçant, offensant ou nuisible.
Les dirigeant•e•s de la communauté ont le droit et la responsabilité de supprimer, modifier ou rejeter
les commentaires, les contributions, le code, les modifications de wikis, les rapports d'incidents ou de bogues et autres contributions qui
ne sont pas alignés sur ce code de conduite, et communiqueront les raisons des décisions de modération
le cas échéant.
## Portée d'application
Ce code de conduite s'applique à la fois au sein des espaces du projet ainsi que
dans les espaces publics lorsqu'un individu représente officiellement le projet ou sa
communauté. Font parties des exemples de représentation d'un projet ou d'une
communauté l'utilisation d'une adresse électronique officielle, la publication sur
les réseaux sociaux à l'aide d'un compte officiel ou le fait d'agir en tant que représentant•e désigné•e
lors d'un événement en ligne ou hors-ligne.
## Application
Les cas de comportements abusifs, harcelants ou tout autre comportement
inacceptables peuvent être signalés aux dirigeant•e•s de la communauté responsables de l'application du code de conduite à
contact@divlo.fr.
Toutes les plaintes seront examinées et feront l'objet d'une enquête rapide et équitable.
Tou•te•s les dirigeant•e•s de la communauté sont tenu•e•s de respecter la vie privée et la sécurité des
personnes ayant signalé un incident.
## Directives d'application
Les dirigeant•e•s de communauté suivront ces directives d'application sur l'impact communautaire afin de déterminer
les conséquences de toute action qu'ils jugent contraire au présent code de conduite :
### 1. Correction
**Impact communautaire** : utilisation d'un langage inapproprié ou tout autre comportement jugé
non professionnel ou indésirable dans la communauté.
**Conséquence** : un avertissement écrit et privé de la part des dirigeant•e•s de la communauté, clarifiant
la nature du non-respect et expliquant pourquoi
le comportement était inapproprié. Des excuses publiques peuvent être demandées.
### 2. Avertissement
**Impact communautaire** : un non-respect par un seul incident ou une série d'actions.
**Conséquence** : un avertissement avec des conséquences dû à la poursuite du comportement.
Aucune interaction avec les personnes concernées, y compris l'interaction non sollicitée avec
celles et ceux qui sont chargé•e•s de l'application de ce code de conduite, pendant une période déterminée.
Cela comprend le fait d'éviter les interactions dans les espaces communautaires ainsi que sur les canaux externes
comme les médias sociaux. Le non-respect de ces conditions peut entraîner
un bannissement temporaire ou permanent.
### 3. Bannissement temporaire
**Impact communautaire** : un non-respect grave des normes communautaires, notamment
un comportement inapproprié soutenu.
**Conséquence** : un bannissement temporaire de toutes formes d'interactions ou de
communications avec la communauté pendant une période déterminée. Aucune interaction publique ou
privée avec les personnes concernées, y compris les interactions non sollicitées
avec celles et ceux qui appliquent ce code de conduite, n'est autorisée pendant cette période.
Le non-respect de ces conditions peut entraîner un bannissement permanent.
### 4. Bannissement permanent
**Impact communautaire** : démontrer un schéma récurrent de non-respect des normes de la
communauté y compris un comportement inapproprié soutenu, le harcèlement d'un individu
ainsi que l'agression ou le dénigrement de catégories d'individus.
**Conséquence** : un bannissement permanent de toutes formes d'interactions publiques au sein de
la communauté.
## Attributions
Ce code de conduite est adapté du
[Contributor Covenant](https://www.contributor-covenant.org), version 2.0,
disponible à
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Les Directives d'application ont été inspirées par le
[Code of conduct enforcement ladder][mozilla coc] de Mozilla.
Pour obtenir des réponses aux questions courantes sur ce code de conduite, consultez la FAQ à
[https://www.contributor-covenant.org/faq][faq]. Les traductions sont disponibles
sur [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

14
.github/ISSUE_TEMPLATE/BUG.md vendored Normal file

@ -0,0 +1,14 @@
---
name: '🐛 Rapport de bug'
about: 'Signalez un problème inattendu ou un comportement involontaire.'
labels: 'bug'
---
## Étapes à suivre pour reproduire le bug
1. Étape 1
2. Étape 2
## Comportement actuel
## Comportement attendu

@ -0,0 +1,7 @@
---
name: '✨ Ajout d'une fonctionnalité'
about: 'Suggérer une nouvelle fonctionnalité.'
labels: 'feature request'
---
### Description

7
.github/ISSUE_TEMPLATE/QUESTION.md vendored Normal file

@ -0,0 +1,7 @@
---
name: '🙋 Question'
about: 'Des informations complémentaires sont demandées.'
labels: 'question'
---
### Question

32
.github/backup.sql vendored Normal file

File diff suppressed because one or more lines are too long

BIN
.github/images/FunctionProject.png vendored Normal file

Binary file not shown.

After

(image error) Size: 23 KiB

18
.github/workflows/commitlint.yml vendored Normal file

@ -0,0 +1,18 @@
# For more information see: https://github.com/marketplace/actions/commit-linter
name: 'Lint Commit Messages'
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
commitlint:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v2'
with:
fetch-depth: 0
- uses: 'wagoid/commitlint-github-action@v2'

61
.github/workflows/nodejs.yml vendored Normal file

@ -0,0 +1,61 @@
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: 'Node.js CI'
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
ci_website:
runs-on: 'ubuntu-latest'
defaults:
run:
working-directory: 'website'
strategy:
matrix:
node-version: [14.x]
steps:
- uses: 'actions/checkout@v2'
- name: Use Node.js ${{ matrix.node-version }}
uses: 'actions/setup-node@v2.1.2'
with:
node-version: ${{ matrix.node-version }}
- name: 'Cache dependencies'
uses: 'actions/cache@v2'
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- run: 'npm install'
- run: 'npm run lint'
- run: 'npm run build'
ci_api:
runs-on: 'ubuntu-latest'
defaults:
run:
working-directory: 'api'
strategy:
matrix:
node-version: [14.x]
steps:
- uses: 'actions/checkout@v2'
- name: Use Node.js ${{ matrix.node-version }}
uses: 'actions/setup-node@v2.1.2'
with:
node-version: ${{ matrix.node-version }}
- name: 'Cache dependencies'
uses: 'actions/cache@v2'
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- run: 'npm install'
- run: 'npm run lint'

3
.gitignore vendored

@ -1,3 +0,0 @@
htaccess
php/keyVariable.php
php/config_database

23
LICENSE

@ -1,8 +1,21 @@
The MIT License (MIT)
Copyright © 2020 Divlo
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Copyright (c) Divlo
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

122
README.md

@ -1,41 +1,107 @@
# FunctionProject
![Badge Status](https://cloud.divlo.fr/public_files/others/Trash/under_dev.svg)
<h1 align="center"><a href="https://function.divlo.fr/">FunctionProject</a></h1>
[À propos de Divlo](https://divlo.fr/) | [Youtube](https://www.youtube.com/c/Divlo) | [Twitch](https://www.twitch.tv/divlo) | [Twitter](https://twitter.com/Divlo_FR) | [E-mail](mailto:contact@divlo.fr)
<p align="center">
<strong>⚠️ Le projet n'est plus maintenu.</strong>
</p>
## À propos de FunctionProject
[FunctionProject](https://function.divlo.fr/) est un projet créé par [Divlo](https://divlo.fr/) qui a pour but de rassembler plein de mini-programmes permettant de faire plusieurs choses comme **savoir la météo**, générer un **nombre aléatoire**, etc.
<p align="center">
<strong>Apprenez la programmation grâce à l'apprentissage par projet alias fonction.</strong>
</p>
Le projet est disponible sur [function.divlo.fr](https://function.divlo.fr/).
<p align="center">
<a href="https://github.com/Divlo/FunctionProject/actions?query=workflow%3A%22Node.js+CI%22"><img src="https://github.com/Divlo/FunctionProject/workflows/Node.js%20CI/badge.svg" alt="Node.js CI" /></a>
<a href="https://standardjs.com"><img alt="JavaScript Style Guide" src="https://img.shields.io/badge/code_style-standard-brightgreen.svg"/></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="Licence MIT"/></a>
<a href="https://conventionalcommits.org"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg" alt="Conventional Commits" /></a>
<a href="./.github/CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Contributor Covenant" /></a>
<br/> <br/>
<a href="https://function.divlo.fr/"><img src="https://raw.githubusercontent.com/Divlo/FunctionProject/master/.github/images/FunctionProject.png" alt="FunctionProject" /></a>
</p>
[![FunctionProject](./img/FunctionProject.png)](https://function.divlo.fr/)
## ⚙️ À propos
## Installation
**Note :** Vous aurez besoin dun serveur Apache pour utiliser PHP (exemple: [XAMPP](https://www.apachefriends.org)).
**FunctionProject** regroupe plein de **fonctions** sous différentes catégories. Chaque fonction dispose d'une partie "**Utilisation**", et d'une partie "**Article**" pour expliquer le code de celle-çi (le plus souvent, le code est rédigé en **Javascript**).
Cloner le dépôt en utilisant git :
```text
git clone https://github.com/Divlo/FunctionProject
En plus de présenter des fonctions, FunctionProject est un **blog** ce qui permet la publication d'article à propos du **développement web** et plus généralement de la **programmation informatique**.
Si vous aimez le projet, vous pouvez aider à **le faire connaître** en utilisant [#FunctionProject](https://twitter.com/hashtag/FunctionProject) sur **Twitter**. 🐦
Les dernières versions publiées : [https://github.com/Divlo/FunctionProject/releases](https://github.com/Divlo/FunctionProject/releases)
Le projet est disponible sur [function.divlo.fr](https://function.divlo.fr/) (actuellement en version 2.3).
## 🚀 Open Source
Le partage est essentiel afin de progresser, l'**Open Source** permet l'entraide et le **partage de connaissance** entre développeurs.
Si vous voulez **contribuer**, avant toute chose écrivez une **"[issue](https://github.com/Divlo/FunctionProject/issues)" sur GitHub** à propos des changements que vous voulez apporter et on pourra en **discuter avec grand plaisir**. 😉
## 🌐 Installation
**Note :** En installant, la version locale vous n'aurez pas accès aux données. Seulement une **base de donnée vide**.
Si vous voulez avoir les données des catégories et des fonctions, vous pouvez d'abord lancer l'API pour que Sequelize crée les tables SQl et ensuite exécuter le fichier SQL [backup.sql](./.github/backup.sql).
### Prérequis
- [Node.js](https://nodejs.org/) >= 14
- [npm](https://www.npmjs.com/) >= 7
- [MySQL](https://www.mysql.com/) >= 8
### Commandes (à suivre dans l'ordre)
```sh
# Cloner le projet
git clone https://github.com/Divlo/FunctionProject.git FunctionProject
# Aller à la racine du projet
cd FunctionProject
# Installer les packages/dépendances
cd ./api
npm install
cd ../website
npm install
```
Puis créer un nouveau fichier dans ```/php``` du nom de ```keyVariable.php```, puis vous mettrez ce code à l'intérieur, il faudra modifier la valeur de la variable '$apiWeather' par votre clé d'api de [openweathermap.org](https://openweathermap.org/).
```php
<?php
$apiWeather = 'votre clé api pour openweathermap.org';
Vous devrez ensuite configurer les variables d'environnements en créant un fichier `.env` à la racine du dossier `/api`, `/website` et `s.divlo.fr` pour prendre exemple du fichier `.env.example` avec votre configuration.
### Lancer l'environnement de développement
#### Avec [docker](https://www.docker.com/)
```sh
# Setup and run all the services for you
docker-compose up --build
```
Sachez tout de même que vous ne pourrez pas utiliser la fonction linkShortener car elle dépend de [short-links.divlo.fr/](https://short-links.divlo.fr/).
Cependant, vous avez accès à la structure de la base de donnée dans ```/php/database_structure/short_links.sql```.
**Services started :**
Enjoy! =D
- api : `http://localhost:8080`
- s.divlo.fr : `http://localhost:7000`
- website : `http://localhost:3000`
- [phpmyadmin](https://www.phpmyadmin.net/) : `http://localhost:8000`
- [MailDev](https://maildev.github.io/maildev/) : `http://localhost:1080`
- [MySQL database](https://www.mysql.com/) (with PORT 3006)
## Librairies
* [Fontawesome](https://fontawesome.com/)
* [Bootstrap](https://getbootstrap.com/)
* [jQuery](https://jquery.com/)
* [jQuery UI](https://jqueryui.com/)
* [Moment.js](https://momentjs.com/)
* [Marked.js](https://github.com/markedjs/marked)
#### Sans docker
## Licence
Ce projet est sous licence MIT - voir le fichier [LICENSE](./LICENSE) pour plus de détails.
Dans deux terminals séparés :
- Lancer le front-end en allant dans `/website`
```sh
npm run dev # front-end lancé sur http://localhost:3000
```
- Lancer l'api en allant dans `/api`
```sh
npm run dev # API lancé sur http://localhost:8080
```
Enjoy! 😃
## 📄 License
[MIT](./LICENSE)

2
api/.dockerignore Normal file

@ -0,0 +1,2 @@
node_modules
build

15
api/.env.example Normal file

@ -0,0 +1,15 @@
COMPOSE_PROJECT_NAME="function.divlo.fr-api"
HOST="http://localhost:8080"
FRONT_END_HOST="http://localhost:3000"
OpenWeatherMap_API_KEY=""
Scraper_API_KEY=""
DATABASE_HOST="functionproject-database"
DATABASE_NAME="functionproject"
DATABASE_USER="root"
DATABASE_PASSWORD="password"
DATABASE_PORT=3306
JWT_SECRET=""
EMAIL_HOST="functionproject-maildev"
EMAIL_USER="no-reply@functionproject.fr"
EMAIL_PASSWORD="password"
EMAIL_PORT=25

29
api/.gitignore vendored Normal file

@ -0,0 +1,29 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# envs
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.production
# misc
.DS_Store
/temp
/assets/images/users
npm-debug.log*
yarn-debug.log*
yarn-error.log*

13
api/Dockerfile Normal file

@ -0,0 +1,13 @@
FROM node:14.16.1
WORKDIR /app
COPY ./package*.json ./
RUN npm install
COPY ./ ./
# docker-compose-wait
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
RUN chmod +x /wait
CMD /wait && npm run dev

110
api/app.js Normal file

@ -0,0 +1,110 @@
/* Modules */
require('dotenv').config()
const path = require('path')
const express = require('express')
const helmet = require('helmet')
const cors = require('cors')
const morgan = require('morgan')
const { redirectToHTTPS } = require('express-http-to-https')
const rateLimit = require('express-rate-limit')
/* Files Imports & Variables */
const sequelize = require('./assets/utils/database')
const { PORT } = require('./assets/config/config')
const errorHandling = require('./assets/utils/errorHandling')
const isAuth = require('./middlewares/isAuth')
const isAdmin = require('./middlewares/isAdmin')
const app = express()
/* Middlewares */
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'))
} else if (process.env.NODE_ENV === 'production') {
app.use(redirectToHTTPS())
const requestPerSecond = 2
const seconds = 60
const windowMs = seconds * 1000
app.enable('trust proxy')
app.use(
rateLimit({
windowMs,
max: seconds * requestPerSecond,
handler: (_req, res) => {
return res.status(429).json({ message: 'Too many requests' })
}
})
)
}
app.use(helmet())
app.use(cors())
app.use(express.json())
/* Routes */
app.use('/images', express.static(path.join(__dirname, 'assets', 'images')))
app.use('/functions', require('./routes/functions'))
app.use('/categories', require('./routes/categories'))
app.use('/users', require('./routes/users'))
app.use('/admin', isAuth, isAdmin, require('./routes/admin'))
app.use('/favorites', require('./routes/favorites'))
app.use('/comments', require('./routes/comments'))
app.use('/quotes', require('./routes/quotes'))
app.use('/tasks', require('./routes/tasks'))
app.use('/links', require('./routes/links_shortener'))
/* Errors Handling */
app.use((_req, _res, next) =>
errorHandling(next, { statusCode: 404, message: "La route n'existe pas!" })
)
app.use((error, _req, res, _next) => {
console.log(error)
const { statusCode, message } = error
return res.status(statusCode || 500).json({ message })
})
/* Database Relations */
const Functions = require('./models/functions')
const Categories = require('./models/categories')
const Users = require('./models/users')
const Favorites = require('./models/favorites')
const Comments = require('./models/comments')
const Quotes = require('./models/quotes')
const Tasks = require('./models/tasks')
const ShortLinks = require('./models/short_links')
// A function has a category
Categories.hasOne(Functions, { constraints: true, onDelete: 'CASCADE' })
Functions.belongsTo(Categories)
// Users can have favorites functions
Users.hasMany(Favorites)
Favorites.belongsTo(Users, { constraints: false })
Functions.hasMany(Favorites)
Favorites.belongsTo(Functions, { constraints: false })
// Users can post comments on functions
Users.hasMany(Comments)
Comments.belongsTo(Users, { constraints: false })
Functions.hasMany(Comments)
Comments.belongsTo(Functions, { constraints: false })
// Users can suggest new quotes
Users.hasMany(Quotes)
Quotes.belongsTo(Users, { constraints: false })
// Users can have tasks
Users.hasMany(Tasks)
Tasks.belongsTo(Users, { constraints: false })
// Users can have links
Users.hasMany(ShortLinks)
ShortLinks.belongsTo(Users, { constraints: false })
/* Server */
sequelize
.sync()
.then(() => {
app.listen(PORT, () =>
console.log('\x1b[36m%s\x1b[0m', `Started on port ${PORT}.`)
)
})
.catch(error => console.log(error))

@ -0,0 +1,33 @@
const dotenv = require('dotenv')
dotenv.config()
const EMAIL_PORT = parseInt(process.env.EMAIL_PORT ?? '465', 10)
const config = {
PORT: process.env.PORT || 8080,
HOST: process.env.HOST,
FRONT_END_HOST: process.env.FRONT_END_HOST,
WEATHER_API_KEY: process.env.OpenWeatherMap_API_KEY,
SCRAPER_API_KEY: process.env.Scraper_API_KEY,
DATABASE: {
host: process.env.DATABASE_HOST,
name: process.env.DATABASE_NAME,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
port: parseInt(process.env.DATABASE_PORT ?? '3306', 10)
},
JWT_SECRET: process.env.JWT_SECRET,
EMAIL_INFO: {
host: process.env.EMAIL_HOST,
port: EMAIL_PORT,
secure: EMAIL_PORT === 465,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
},
ignoreTLS: process.env.NODE_ENV !== 'production'
},
TOKEN_LIFE: '1 week'
}
module.exports = config

118
api/assets/config/emails.js Normal file

@ -0,0 +1,118 @@
exports.emailQuoteTemplate = (isValid, quote, frontendLink) => `
<center>
<table border="0" cellpadding="20" cellspacing="0" height="100%" width="100%" style="background-color:#181818">
<tbody>
<tr>
<td align="center" valign="top">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;border-radius:6px">
<tbody>
<tr>
<td align="center" valign="top">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px">
<tbody>
<tr>
<td>
<h1 style="font-family:Arial, Helvetica, sans-serif;color:#ffd800;font-size:28px;line-height:110%;margin-bottom:30px;margin-top:0;padding:0">FunctionProject</h1>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="center" valign="top">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;border-radius:6px;">
<tbody>
<tr>
<td align="left" valign="top" style="line-height:150%;font-family:Helvetica;font-size:14px;color:rgb(222, 222, 222);padding:30px;box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, .25);border: 1px solid black;border-radius: 1rem;">
<h2 style="font-size:22px;line-height:28px;margin:0 0 12px 0;">
La citation que vous avez proposée a été ${
isValid
? 'validée'
: 'supprimée'
}.
</h2>
<p style="margin: 0 0 12px 0;">
<a style="color: #ffd800;" href="${frontendLink}/functions/randomQuote">Lien vers la fonction randomQuote de FunctionProject.</a>
</p>
${
!isValid
? `
<p style="margin: 0 0 12px 0;">
Si votre citation a été supprimée et vous pensez que c'est une erreur, contactez-moi à cette adresse email : <a style="color: #ffd800;" href="mailto:contact@divlo.fr">contact@divlo.fr</a>.
</p>
`
: ''
}
<div>
<p style="padding:0 0 10px 0">
La citation en question : <br/>
" ${quote.quote} "
<br/>
- ${quote.author}
</p>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</center>
`
exports.emailUserTemplate = (subtitle, buttonText, url, footerText) => `
<center>
<table border="0" cellpadding="20" cellspacing="0" height="100%" width="100%" style="background-color:#181818">
<tbody>
<tr>
<td align="center" valign="top">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;border-radius:6px">
<tbody>
<tr>
<td align="center" valign="top">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px">
<tbody>
<tr>
<td>
<h1 style="font-family:Arial, Helvetica, sans-serif;color:#ffd800;font-size:28px;line-height:110%;margin-bottom:30px;margin-top:0;padding:0">FunctionProject</h1>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="center" valign="top">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width:600px;border-radius:6px;">
<tbody>
<tr>
<td align="left" valign="top" style="line-height:150%;font-family:Helvetica;font-size:14px;color:rgb(222, 222, 222);padding:30px;box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, .25);border: 1px solid black;border-radius: 1rem;">
<h2 style="font-size:22px;line-height:28px;margin:0 0 12px 0;">
${subtitle}
</h2>
<a href="${url}" style="display:inline-block;font-weight:500;font-size:16px;line-height:42px;font-family:'Helvetica',Arial,sans-serif;width:auto;white-space:nowrap;height:42px;margin:12px 5px 12px 0;padding:0 22px;text-decoration:none;text-align:center;border:0;border-radius:3px;vertical-align:top;background-color: #343a40;border-color: #343a40;" target="_blank" rel="noopener noreferrer"><span style="display:inline;font-family:'Helvetica',Arial,sans-serif;text-decoration:none;font-weight:500;font-style:normal;font-size:16px;line-height:42px;border:none;color: #fff;">${buttonText}</span></a>
<br>
<div>
<p style="padding:0 0 10px 0">${footerText}</p>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</center>
`

@ -0,0 +1,18 @@
const errors = {
generalError: {
message: "Vous n'avez pas rentré de valeur valide.",
statusCode: 400
},
serverError: {
message: "Le serveur n'a pas pu traiter votre requête.",
statusCode: 500
},
requiredFields: {
message: 'Vous devez remplir tous les champs...',
statusCode: 400
}
}
module.exports = errors

@ -0,0 +1,6 @@
const nodemailer = require('nodemailer')
const { EMAIL_INFO } = require('./config')
const transporter = nodemailer.createTransport(EMAIL_INFO)
module.exports = transporter

@ -0,0 +1,42 @@
const { randomNumberOutput } = require('./main/randomNumber')
const convertRomanArabicNumbersOutput = require('./main/convertRomanArabicNumbers')
const convertDistanceOutput = require('./main/convertDistance')
const convertTemperatureOutput = require('./main/convertTemperature')
const armstrongNumberOutput = require('./main/armstrongNumber')
const weatherRequestOutput = require('./main/weatherRequest')
const convertCurrencyOutput = require('./main/convertCurrency')
const calculateAgeOutput = require('./main/calculateAge')
const heapAlgorithmOutput = require('./main/heapAlgorithm')
const convertEncodingOutput = require('./main/convertEncoding')
const randomQuote = require('./main/randomQuote')
const rightPriceOutput = require('./main/rightPrice')
const isPalindromeOutput = require('./main/isPalindrome')
const findLongestWordOutput = require('./main/findLongestWord')
const fibonacciOutput = require('./main/fibonacci')
const sortArrayOutput = require('./main/sortArray')
const functionObject = {
randomNumber: randomNumberOutput,
convertRomanArabicNumbers: convertRomanArabicNumbersOutput,
convertDistance: convertDistanceOutput,
convertTemperature: convertTemperatureOutput,
armstrongNumber: armstrongNumberOutput,
weatherRequest: weatherRequestOutput,
convertCurrency: convertCurrencyOutput,
calculateAge: calculateAgeOutput,
heapAlgorithm: heapAlgorithmOutput,
convertEncoding: convertEncodingOutput,
randomQuote: randomQuote,
rightPrice: rightPriceOutput,
isPalindrome: isPalindromeOutput,
findLongestWord: findLongestWordOutput,
fibonacci: fibonacciOutput,
sortArray: sortArrayOutput
}
// Choisi la fonction à exécuter
function functionToExecute (option) {
return functionObject[option]
}
module.exports = functionToExecute

@ -0,0 +1,59 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields } = require('../../config/errors')
const formatNumberResult = require('../secondary/formatNumberResult')
/**
* @description Vérifie si un nombre fait partie des nombres d'Armstrong.
* @param {Number} number - Le nombre à tester
* @returns {Object} Un objet contenant l'explication en html et le booléen si oui ou non c'est un nombre d'armstrong
* @examples armstrongNumber(153) 153 est un nombre d'Armstrong, car 1<sup>3</sup> + 5<sup>3</sup> + 3<sup>3</sup> = 153.
*/
function armstrongNumber (number) {
const numberString = number.toString()
const numberStringLength = numberString.length
let result = 0
let resultString = ''
for (let index = 0; index < numberStringLength; index++) {
result = result + parseInt(numberString[index]) ** numberStringLength
resultString =
resultString +
' + ' +
numberString[index] +
'<sup>' +
numberStringLength +
'</sup>'
}
const formattedNumber = formatNumberResult(number)
const isArmstrongNumber = result === number
return {
isArmstrongNumber,
resultHTML: `<p>${formattedNumber} ${
isArmstrongNumber ? 'est' : "n'est pas"
} un nombre d'Armstrong, car ${resultString.slice(
2
)} = ${formatNumberResult(result)}.</p>`
}
}
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
let { number } = argsObject
// S'il n'y a pas les champs obligatoire
if (!number) {
return errorHandling(next, requiredFields)
}
// Si ce n'est pas un nombre
number = parseInt(number)
if (isNaN(number) || number <= 0) {
return errorHandling(next, {
message: 'Veuillez rentré un nombre valide.',
statusCode: 400
})
}
return res.status(200).json(armstrongNumber(number))
}

@ -0,0 +1,60 @@
const errorHandling = require('../../utils/errorHandling')
const moment = require('moment')
const { requiredFields } = require('../../config/errors')
function calculateAge (
currentDate,
{ birthDateDay, birthDateMonth, birthDateYear }
) {
const day = currentDate.getDate()
const month = currentDate.getMonth()
const currentDateMoment = moment([currentDate.getFullYear(), month, day])
const birthDateMoment = moment([birthDateYear, birthDateMonth, birthDateDay])
// Calcule l'âge - Moment.js
const ageYears = currentDateMoment.diff(birthDateMoment, 'year')
birthDateMoment.add(ageYears, 'years')
const ageMonths = currentDateMoment.diff(birthDateMoment, 'months')
birthDateMoment.add(ageMonths, 'months')
const ageDays = currentDateMoment.diff(birthDateMoment, 'days')
const isBirthday = birthDateDay === day && birthDateMonth === month
return { ageYears, ageMonths, ageDays, isBirthday }
}
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
const { birthDate } = argsObject
// S'il n'y a pas les champs obligatoire
if (!birthDate) {
return errorHandling(next, requiredFields)
}
const birthDateDay = parseInt(birthDate.substring(0, 2))
const birthDateMonth = parseInt(birthDate.substring(3, 5) - 1)
const birthDateYear = parseInt(birthDate.substring(6, 10))
// Si ce n'est pas une date valide
const currentDate = new Date()
const birthDateObject = new Date(birthDateYear, birthDateMonth, birthDateDay)
const result = calculateAge(currentDate, {
birthDateYear,
birthDateMonth,
birthDateDay
})
if (currentDate < birthDateObject || isNaN(result.ageYears)) {
return errorHandling(next, {
message: 'Veuillez rentré une date valide...',
statusCode: 400
})
}
let resultHTML
if (result.isBirthday) {
resultHTML = `<p>Vous avez ${result.ageYears} ans. Joyeux Anniversaire! 🥳</p>`
} else {
resultHTML = `<p>Vous avez ${result.ageYears} ans, ${result.ageMonths} mois et ${result.ageDays} jour(s).</p>`
}
return res.status(200).json({ ...result, resultHTML })
}

@ -0,0 +1,53 @@
const axios = require('axios')
const errorHandling = require('../../utils/errorHandling')
const { requiredFields } = require('../../config/errors')
const formatNumberResult = require('../secondary/formatNumberResult')
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
let { number, baseCurrency, finalCurrency } = argsObject
// S'il n'y a pas les champs obligatoire
if (!(number && baseCurrency && finalCurrency)) {
return errorHandling(next, requiredFields)
}
// Si ce n'est pas un nombre
number = parseFloat(number)
if (isNaN(number)) {
return errorHandling(next, {
message: 'Veuillez rentré un nombre valide.',
statusCode: 400
})
}
axios
.get(`https://api.exchangeratesapi.io/latest?base=${baseCurrency}`)
.then(response => {
const rate = response.data.rates[finalCurrency]
if (!rate) {
return errorHandling(next, {
message: "La devise n'existe pas.",
statusCode: 404
})
}
const result = rate * number
const dateObject = new Date(response.data.date)
const year = dateObject.getFullYear()
const day = ('0' + dateObject.getDate()).slice(-2)
const month = ('0' + (dateObject.getMonth() + 1)).slice(-2)
const date = `${day}/${month}/${year}`
const resultHTML = `<p>${formatNumberResult(number)} ${
response.data.base
} = ${formatNumberResult(
result.toFixed(2)
)} ${finalCurrency}</p><p>Dernier rafraîchissement du taux d'échange : ${date}</p>`
return res.status(200).json({ date, result, resultHTML })
})
.catch(() =>
errorHandling(next, {
message: "La devise n'existe pas.",
statusCode: 404
})
)
}

@ -0,0 +1,84 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields, generalError } = require('../../config/errors')
const formatNumberResult = require('../secondary/formatNumberResult')
const correspondancesDistance = [
'pm',
null,
null,
'nm',
null,
null,
'µm',
null,
null,
'mm',
'cm',
'dm',
'm',
'dam',
'hm',
'km',
null,
null,
'Mm',
null,
null,
'Gm',
null,
null,
'Tm'
]
/**
* @description Convertis la longueur (distance) avec les unités allant de picomètre au Téramètre.
* @requires {@link correspondancesDistance}
* @param {Number} firstValue - Le nombre que vous voulez convertir
* @param {String} unitFirstValue - L'unité du nombre que vous voulez convertir
* @param {String} unitFinalValue - L'unité de votre nombre après la conversion
* @returns {Object|Boolean} false si arguments non valides et sinon un objet contenant la string et le nombre résultat
* @examples convertDistance(500, 'cm', 'm') { resultNumber: 5, resultString: "5 m" }
*/
function convertDistance (firstValue, unitFirstValue, unitFinalValue) {
const index1 = correspondancesDistance.indexOf(unitFirstValue)
const index2 = correspondancesDistance.indexOf(unitFinalValue)
if (index1 !== -1 && index2 !== -1) {
const difference = index1 - index2
const result = firstValue * Math.pow(10, difference)
return {
result,
resultHTML: `<p>${formatNumberResult(
firstValue
)} ${unitFirstValue} = ${formatNumberResult(
result
)} ${unitFinalValue}</p>`
}
}
return false
}
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
let { number, numberUnit, finalUnit } = argsObject
// S'il n'y a pas les champs obligatoire
if (!(number && numberUnit && finalUnit)) {
return errorHandling(next, requiredFields)
}
// Si ce n'est pas un nombre
number = parseFloat(number)
if (isNaN(number)) {
return errorHandling(next, {
message: 'Veuillez rentré un nombre valide.',
statusCode: 400
})
}
const result = convertDistance(number, numberUnit, finalUnit)
if (!result) {
return errorHandling(next, generalError)
}
return res.status(200).json(result)
}

@ -0,0 +1,268 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields, generalError } = require('../../config/errors')
/**
* @description Convertis un nombre décimal en binaire.
* @param {String} value - Le nombre à convertir en string
* @returns {String} - Le nombre en binaire
* @examples decimalToBinary('2') '10'
*/
function decimalToBinary (value) {
value = Number(value)
if (isNaN(value)) {
return false
} else {
return value.toString(2)
}
}
/**
* @description Convertis un nombre binaire en décimal.
* @param {String} value - Le nombre à convertir
* @returns {(Number|String)} - Le nombre en décimal soit en nombre ou soit en string si supérieur à 1000 car pour 1000 par exemple formatNumberResult renvoie '1 000'
* @examples binaryToDecimal('10') 2
*/
function binaryToDecimal (value) {
const result = parseInt(Number(value), 2)
if (isNaN(result)) {
return false
} else {
return result
}
}
/**
* @description Convertis un nombre décimal en hexadécimal.
* @param {String} value - Le nombre à convertir
* @returns {String} - Le nombre en hexadécimal
* @examples decimalToHexadecimal('15') 'F'
*/
function decimalToHexadecimal (value) {
value = Number(value)
if (isNaN(value)) {
return false
} else {
return value.toString(16).toUpperCase()
}
}
/**
* @description Convertis un nombre hexadécimal en décimal.
* @param {String} value - Le nombre à convertir
* @returns {(Number|String)} - Le nombre en décimal soit en nombre ou soit en string si supérieur à 1000 car pour 1000 par exemple formatNumberResult renvoie '1 000'
* @examples hexadecimalToDecimal('F') 15
*/
function hexadecimalToDecimal (value) {
const result = parseInt(value, 16)
if (isNaN(result)) {
return false
} else {
return result
}
}
/**
* @description Convertis un nombre binaire en hexadécimal.
* @param {String} value - Le nombre à convertir
* @returns {String} - Le nombre en hexadécimal
* @examples binaryToHexadecimal('1111') 'F'
*/
function binaryToHexadecimal (value) {
value = Number(value)
value = parseInt(value, 2)
if (isNaN(value)) {
return false
} else {
return parseInt(value)
.toString(16)
.toUpperCase()
}
}
/**
* @description Convertis un nombre hexadécimal en binaire.
* @param {String} value - Le nombre à convertir
* @returns {String} - Le nombre en binaire
* @examples hexadecimalToBinary('F') '1111'
*/
function hexadecimalToBinary (value) {
value = parseInt(value, 16)
if (isNaN(value)) {
return false
} else {
return parseInt(value).toString(2)
}
}
// Convertis des nombres de différentes bases et convertis en UTF-8. (source : http://jsfiddle.net/47zwb41o)
/**
* @description Convertis chaque caractère d'une string en codePoint Unicode.
* @param {String} value - La chaîne de caractère à convertir
* @returns {String}
* @examples textToNumberUnicode('abc') '97 98 99'
*/
function textToNumberUnicode (string) {
try {
let resultat = ''
for (const index in string) {
resultat = resultat + string.codePointAt(index) + ' '
}
return resultat
} catch (error) {
return false
}
}
/**
* @description Convertis chaque codePoint Unicode en caractère.
* @param {String} string - Nombre Unicode à convertir espacé par un espace à chaque fois
* @returns {String}
* @examples numberUnicodeToText('97 98 99') 'abc'
*/
function numberUnicodeToText (string) {
try {
const array = string.split(' ')
let resultat = ''
for (const index in array) {
resultat += String.fromCodePoint(parseInt(array[index]).toString())
}
return resultat
} catch (error) {
return false
}
}
/**
* @description Convertis un Texte en Binaire (UTF-8).
* @param {String} s - La chaîne de caractère à convertir
* @returns {String}
* @examples textToBinary('abc') '01100001 01100010 01100011'
*/
function textToBinary (s) {
try {
s = unescape(encodeURIComponent(s))
let chr
let i = 0
const l = s.length
let out = ''
for (; i < l; i++) {
chr = s.charCodeAt(i).toString(2)
while (chr.length % 8 !== 0) {
chr = '0' + chr
}
out += chr
}
return out.replace(/(\d{8})/g, '$1 ').replace(/(^\s+|\s+$)/, '')
} catch (error) {
return false
}
}
/**
* @description Convertis du Binaire (UTF-8) en Texte.
* @param {String} s - La chaîne de caractère contenant tous les octets à convertir
* @returns {String}
* @examples binaryToText('01100001 01100010 01100011') 'abc'
*/
function binaryToText (s) {
try {
s = s.replace(/\s/g, '')
let i = 0
const l = s.length
let chr
let out = ''
for (; i < l; i += 8) {
chr = parseInt(s.substr(i, 8), 2).toString(16)
out += '%' + (chr.length % 2 === 0 ? chr : '0' + chr)
}
return decodeURIComponent(out)
} catch (error) {
return false
}
}
/**
* @description Convertis un Texte en Hexadécimal (UTF-8).
* @param {String} s - La chaîne de caractère à convertir
* @returns {String}
* @examples textToHexadecimal('abc') '61 62 63'
*/
function textToHexadecimal (s) {
try {
s = unescape(encodeURIComponent(s))
let chr
let i = 0
const l = s.length
let out = ''
for (; i < l; i++) {
chr = s.charCodeAt(i).toString(16)
out += chr.length % 2 === 0 ? chr : '0' + chr
out += ' '
}
return out.toUpperCase()
} catch (error) {
return false
}
}
/**
* @description Convertis de l'Hexadécimal (UTF-8) en Texte.
* @param {String} s - La chaîne de caractère contenant tous les nombres Hexadécimal à convertir
* @returns {String}
* @examples hexadecimalToText('61 62 63') 'abc'
*/
function hexadecimalToText (s) {
try {
s = s.replace(/\s/g, '')
return decodeURIComponent(s.replace(/../g, '%$&'))
} catch (error) {
return false
}
}
/* OUTPUTS */
const convertEncoding = {
decimalToBinary,
binaryToDecimal,
decimalToHexadecimal,
hexadecimalToDecimal,
binaryToHexadecimal,
hexadecimalToBinary,
textToNumberUnicode,
numberUnicodeToText,
textToBinary,
binaryToText,
textToHexadecimal,
hexadecimalToText
}
function executeFunction (option, value) {
return convertEncoding[option](value)
}
module.exports = ({ res, next }, argsObject) => {
const { value, functionName } = argsObject
// S'il n'y a pas les champs obligatoire
if (!(value && functionName)) {
return errorHandling(next, requiredFields)
}
// Si la fonction n'existe pas
// eslint-disable-next-line
if (!convertEncoding.hasOwnProperty(functionName)) {
return errorHandling(next, {
message: "Cette conversion n'existe pas.",
statusCode: 400
})
}
const result = executeFunction(functionName, value)
// Mauvaise valeur entrée
if (!result) {
return errorHandling(next, generalError)
}
return res.status(200).json({ result, resultHTML: `<p>${result}</p>` })
}

@ -0,0 +1,143 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields, generalError } = require('../../config/errors')
const formatNumberResult = require('../secondary/formatNumberResult')
/* Variable pour convertRomanArabicNumbers */
const correspondancesRomainArabe = [
[1000, 'M'],
[900, 'CM'],
[500, 'D'],
[400, 'CD'],
[100, 'C'],
[90, 'XC'],
[50, 'L'],
[40, 'XL'],
[10, 'X'],
[9, 'IX'],
[5, 'V'],
[4, 'IV'],
[1, 'I']
]
/**
* @description Convertis un nombre arabe en nombre romain.
* @param {number} nombre - Le nombre arabe à convertir
* @returns {string}
* @examples convertArabicToRoman(24) 'XXIV'
*/
function convertArabicToRoman (nombre) {
// Initialisation de la variable qui va contenir le résultat de la conversion
let chiffresRomains = ''
function extraireChiffreRomain (valeurLettre, lettres) {
while (nombre >= valeurLettre) {
chiffresRomains = chiffresRomains + lettres
nombre = nombre - valeurLettre
}
}
correspondancesRomainArabe.forEach(correspondance => {
extraireChiffreRomain(correspondance[0], correspondance[1])
})
return chiffresRomains
}
/**
* @description Convertis un nombre romain en nombre arabe.
* @param {string} string - Le nombre romain à convertir
* @return {number}
* @example convertRomanToArabic('XXIV') 24
*/
function convertRomanToArabic (string) {
let result = 0
correspondancesRomainArabe.forEach(correspondance => {
while (string.indexOf(correspondance[1]) === 0) {
// Ajout de la valeur décimale au résultat
result += correspondance[0]
// Supprimer la lettre romaine correspondante du début
string = string.replace(correspondance[1], '')
}
})
if (string !== '') {
result = 0
}
return result
}
/* OUTPUTS */
const convertRomanToArabicOutput = ({ res, next }, number) => {
// S'il n'y a pas les champs obligatoire
if (!number) {
return errorHandling(next, requiredFields)
}
// Formate le paramètre
number = number.toUpperCase()
const result = convertRomanToArabic(number)
if (result === 0) {
return errorHandling(next, generalError)
}
return res
.status(200)
.json({
result,
resultHTML: `<p><span class="important">${number}</span> s'écrit <span class="important">${result}</span> en chiffres arabes.</p>`
})
}
const convertArabicToRomanOutput = ({ res, next }, number) => {
// S'il n'y a pas les champs obligatoire
if (!number) {
return errorHandling(next, requiredFields)
}
// Si ce n'est pas un nombre
number = parseInt(number)
if (isNaN(number)) {
return errorHandling(next, {
message: 'Veuillez rentré un nombre valide.',
statusCode: 400
})
}
const result = convertArabicToRoman(number)
return res
.status(200)
.json({
result,
resultHTML: `<p><span class="important">${formatNumberResult(
number
)}</span> s'écrit <span class="important">${result}</span> en chiffres romains.</p>`
})
}
const convertRomanArabicObject = {
convertRomanToArabicOutput,
convertArabicToRomanOutput
}
function executeFunction (option, value, { res, next }) {
return convertRomanArabicObject[option]({ res, next }, value)
}
module.exports = ({ res, next }, argsObject) => {
const { value, functionName } = argsObject
// S'il n'y a pas les champs obligatoire
if (!(value && functionName)) {
return errorHandling(next, requiredFields)
}
// Si la fonction n'existe pas
// eslint-disable-next-line
if (!convertRomanArabicObject.hasOwnProperty(functionName)) {
return errorHandling(next, {
message: "Cette conversion n'existe pas.",
statusCode: 400
})
}
executeFunction(functionName, value, { res, next })
}

@ -0,0 +1,53 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields, generalError } = require('../../config/errors')
const formatNumberResult = require('../secondary/formatNumberResult')
/**
* @description Convertis des °C en °F et l'inverse aussi.
* @param {Number} degree - Nombre de degrès
* @param {String} unit - Unité du nombre (°C ou °F) après conversion
* @returns {Object} false si arguments non valides et sinon un objet contenant la string et le nombre résultat
* @examples convertTemperature(23, '°F') { result: 73.4, resultHTML: "73.4 °F" }
*/
function convertTemperature (degree, unit) {
let temperatureValue = 0
if (unit === '°C') {
temperatureValue = ((degree - 32) * 5) / 9
} else if (unit === '°F') {
temperatureValue = (degree * 9) / 5 + 32
} else {
return false
}
return {
result: temperatureValue,
resultHTML: `<p>${formatNumberResult(degree)} ${
unit === '°C' ? '°F' : '°C'
} = ${formatNumberResult(temperatureValue)} ${unit}</p>`
}
}
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
let { degree, unitToConvert } = argsObject
// S'il n'y a pas les champs obligatoire
if (!(degree && unitToConvert)) {
return errorHandling(next, requiredFields)
}
// Si ce n'est pas un nombre
degree = parseFloat(degree)
if (isNaN(degree)) {
return errorHandling(next, {
message: 'Veuillez rentré un nombre valide.',
statusCode: 400
})
}
const result = convertTemperature(degree, unitToConvert)
if (!result) {
return errorHandling(next, generalError)
}
return res.status(200).json(result)
}

@ -0,0 +1,55 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields } = require('../../config/errors')
const formatNumberResult = require('../secondary/formatNumberResult')
/**
* @description Calcule les counter premiers nombres de la suite de fibonacci.
* @param {number} counter
*/
function fibonacci (counter, result = [], a = 0, b = 1) {
if (counter === 0) {
return result
}
counter--
result.push(a)
return fibonacci(counter, result, b, a + b)
}
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
let { counter } = argsObject
// S'il n'y a pas les champs obligatoire
if (!counter) {
return errorHandling(next, requiredFields)
}
// Si ce n'est pas un nombre
counter = parseInt(counter)
if (isNaN(counter)) {
return errorHandling(next, {
message: 'Veuillez rentré un nombre valide.',
statusCode: 400
})
}
// Si le nombre dépasse LIMIT_COUNTER
const LIMIT_COUNTER = 51
if (counter >= LIMIT_COUNTER) {
return errorHandling(next, {
message: `Par souci de performance, vous ne pouvez pas exécuter cette fonction avec un compteur dépassant ${LIMIT_COUNTER -
1}.`,
statusCode: 400
})
}
const result = fibonacci(counter)
const resultFormatted = result.map(number => formatNumberResult(number))
return res.status(200).json({
result,
resultFormatted,
resultHTML: `<p>Les ${counter} premiers nombres de la suite de fibonacci :<br/> ${resultFormatted.join(
', '
)}</p>`
})
}

@ -0,0 +1,39 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields } = require('../../config/errors')
/**
* @description Renvoie le mot le plus long d'une chaîne de caractères
* @param {string} string
* @returns {string}
* @example findLongestWord('Chaîne de caractères') 'caractères'
*/
function findLongestWord (string) {
const arrayString = string.split(' ')
let stringLength = 0
let result = ''
arrayString.forEach(element => {
if (element.length > stringLength) {
result = element
stringLength = element.length
}
})
return result
}
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
const { string } = argsObject
// S'il n'y a pas les champs obligatoire
if (!string) {
return errorHandling(next, requiredFields)
}
const result = findLongestWord(string)
return res.status(200).json({
result,
resultHTML: `<p>Le mot le plus long est : <br/>"${result}"</p>`
})
}

@ -0,0 +1,64 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields } = require('../../config/errors')
const formatNumberResult = require('../secondary/formatNumberResult')
/**
* @description Retourne un tableau contenant toutes les possibilités d'anagramme d'un mot.
* @param {String} string - La chaîne de caractère à permuter
* @returns {Array}
* @examples heapAlgorithm('abc') ["abc", "acb", "bac", "bca", "cab", "cba"]
*/
function heapAlgorithm (string) {
const results = []
if (string.length === 1) {
results.push(string)
return results
}
for (let indexString = 0; indexString < string.length; indexString++) {
const firstChar = string[indexString]
const charsLeft =
string.substring(0, indexString) + string.substring(indexString + 1)
const innerPermutations = heapAlgorithm(charsLeft)
for (
let indexPermutation = 0;
indexPermutation < innerPermutations.length;
indexPermutation++
) {
results.push(firstChar + innerPermutations[indexPermutation])
}
}
return results
}
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
const { string } = argsObject
// S'il n'y a pas les champs obligatoire
if (!string) {
return errorHandling(next, requiredFields)
}
// Si la chaîne de caractère dépasse LIMIT_CHARACTERS caractères
const LIMIT_CHARACTERS = 7
if (string.length > LIMIT_CHARACTERS) {
return errorHandling(next, {
message: `Par souci de performance, vous ne pouvez pas exécuter cette fonction avec un mot dépassant ${LIMIT_CHARACTERS} caractères.`,
statusCode: 400
})
}
const result = heapAlgorithm(string)
let resultHTML = `<p>Il y a ${formatNumberResult(
result.length
)} possibilités d'anagramme pour le mot "${string}" qui contient ${
string.length
} caractères, la liste : <br/><br/>`
result.forEach(string => {
resultHTML += string + '<br/>'
})
resultHTML += '</p>'
return res.status(200).json({ result, resultHTML })
}

@ -0,0 +1,58 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields } = require('../../config/errors')
/**
* @description Inverse la chaîne de caractère
* @param {string} string
* @returns {string}
* @example reverseString('Hello') 'olleH'
*/
function reverseString (string) {
return string
.split('')
.reverse()
.join('')
}
/**
* @description Vérifie si un mot est un palindrome (un mot qui peut s'écrire dans les deux sens)
* @requires reverseString
* @param {string} string
* @param {string} reverseStringResult La chaîne de caractères inversée
* @returns {boolean}
* @example isPalindrome('kayak') true
*/
function isPalindrome (string, reverseStringResult) {
return string === reverseStringResult
}
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
let { string } = argsObject
// S'il n'y a pas les champs obligatoire
if (!string) {
return errorHandling(next, requiredFields)
}
if (typeof string !== 'string') {
return errorHandling(next, {
message: 'Vous devez rentré une chaîne de caractère valide.',
statusCode: 400
})
}
string = string.toLowerCase()
const reverseStringResult = reverseString(string)
const isPalindromeResult = isPalindrome(string, reverseStringResult)
return res.status(200).json({
isPalindrome: isPalindromeResult,
reverseString: reverseStringResult,
resultHTML: `<p>"${string}" ${
isPalindromeResult ? 'est' : "n'est pas"
} un palindrome car <br/> "${string}" ${
isPalindromeResult ? '===' : '!=='
} "${reverseStringResult}"</p>`
})
}

@ -0,0 +1,47 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields } = require('../../config/errors')
const formatNumberResult = require('../secondary/formatNumberResult')
/**
* @description Génère un nombre aléatoire entre un minimum inclus et un maximum inclus.
* @param {Number} min Nombre Minimum
* @param {Number} max Nombre Maximum
* @returns {Number} Nombre aléatoire
* @examples randomNumber(1, 2) retourne soit 1 ou 2
*/
function randomNumber (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
/* OUTPUTS */
const randomNumberOutput = ({ res, next }, argsObject) => {
let { min, max } = argsObject
// S'il n'y a pas les champs obligatoire
if (!(min && max)) {
return errorHandling(next, requiredFields)
}
// Si ce ne sont pas des nombres
min = parseInt(min)
max = parseInt(max)
if (isNaN(min) || isNaN(max)) {
return errorHandling(next, {
message: 'Les paramètres min et max doivent être des nombres...',
statusCode: 400
})
}
const result = randomNumber(min, max)
return res
.status(200)
.json({
result,
resultHTML: `<p>Nombre aléatoire compris entre ${min} inclus et ${max} inclus : <strong>${formatNumberResult(
result
)}</strong></p>`
})
}
exports.randomNumber = randomNumber
exports.randomNumberOutput = randomNumberOutput

@ -0,0 +1,24 @@
const errorHandling = require('../../utils/errorHandling')
const { serverError } = require('../../config/errors')
const Quotes = require('../../../models/quotes')
const Users = require('../../../models/users')
const sequelize = require('../../utils/database')
module.exports = async ({ res, next }, _argsObject) => {
try {
const quote = await Quotes.findOne({
order: sequelize.random(),
include: [{ model: Users, attributes: ['name', 'logo'] }],
attributes: {
exclude: ['isValidated']
},
where: {
isValidated: 1
}
})
return res.status(200).json(quote)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}

@ -0,0 +1,60 @@
const { randomNumber } = require('./randomNumber')
const errorHandling = require('../../utils/errorHandling')
const { serverError } = require('../../config/errors')
const { SCRAPER_API_KEY } = require('../../config/config')
const axios = require('axios')
const { JSDOM } = require('jsdom')
const subjectList = [
'smartphone',
'pc+gamer',
'pc+portable',
'TV',
'casque',
'clavier',
'souris',
'ecran',
'jeux+vidéos'
]
function getRandomArrayElement (array) {
return array[randomNumber(0, array.length - 1)]
}
async function getAmazonProductList (subject) {
const url = `https://www.amazon.fr/s?k=${subject}`
const { data } = await axios.get(
`http://api.scraperapi.com/?api_key=${SCRAPER_API_KEY}&url=${url}`
)
const { document } = new JSDOM(data).window
const amazonProductList = document.querySelectorAll('.s-result-item')
const productsList = []
for (const indexProduct in amazonProductList) {
try {
const elementProduct = amazonProductList[indexProduct]
const productImage = elementProduct.querySelector('.s-image')
const originalPrice = elementProduct.querySelector('.a-price-whole')
.innerHTML
productsList.push({
name: productImage.alt,
image: productImage.src,
price: Number(originalPrice.replace(',', '.').replace(' ', ''))
})
} catch (_error) {
continue
}
}
return productsList
}
module.exports = async ({ res, next }, _argsObject) => {
const subject = getRandomArrayElement(subjectList)
try {
const productsList = await getAmazonProductList(subject)
const randomProduct = getRandomArrayElement(productsList)
return res.status(200).json({ subject, ...randomProduct })
} catch (error) {
console.error(error)
return errorHandling(next, serverError)
}
}

@ -0,0 +1,69 @@
const errorHandling = require('../../utils/errorHandling')
const { requiredFields } = require('../../config/errors')
const formatNumberResult = require('../secondary/formatNumberResult')
function minNumber (array) {
let minNumber = { index: 0, value: array[0] }
for (let index = 1; index < array.length; index++) {
const number = array[index]
if (number < minNumber.value) {
minNumber = { index: index, value: array[index] }
}
}
return minNumber
}
function sortArray (array) {
const arrayDuplicated = [...array]
const resultArray = []
while (array.length !== resultArray.length) {
const min = minNumber(arrayDuplicated)
resultArray.push(min.value)
arrayDuplicated.splice(min.index, 1)
}
return resultArray
}
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
const { numbersList } = argsObject
// S'il n'y a pas les champs obligatoire
if (!numbersList) {
return errorHandling(next, requiredFields)
}
const numbersListArray = numbersList
.split(',')
.map(number => number.trim().replace(' ', ''))
.map(Number)
// Si ce n'est pas une liste de nombres
if (numbersListArray.includes(NaN)) {
return errorHandling(next, {
message:
'Vous devez rentrer une liste de nombres séparée par des virgules valide.',
statusCode: 400
})
}
// Si la taille du tableau dépasse LIMIT_ARRAY_LENGTH
const LIMIT_ARRAY_LENGTH = 31
if (numbersListArray.length >= LIMIT_ARRAY_LENGTH) {
return errorHandling(next, {
message: `Par souci de performance, vous ne pouvez pas exécuter cette fonction avec une liste de nombres dépassant ${LIMIT_ARRAY_LENGTH -
1} nombres.`,
statusCode: 400
})
}
const result = sortArray(numbersListArray)
const resultFormatted = result.map(number => formatNumberResult(number))
return res.status(200).json({
result,
resultFormatted,
resultHTML: `<p>La liste de nombres dans l'ordre croissant :<br/> ${resultFormatted.join(
', '
)}</p>`
})
}

@ -0,0 +1,73 @@
const axios = require('axios')
const Queue = require('smart-request-balancer')
const errorHandling = require('../../utils/errorHandling')
const { requiredFields } = require('../../config/errors')
const { WEATHER_API_KEY } = require('../../config/config')
const dateTimeUTC = require('../secondary/dateTimeManagement')
const capitalize = require('../secondary/capitalize')
const queue = new Queue({
/*
rate: number of requests
per
limit: number of seconds
*/
rules: {
weatherRequest: {
rate: 50,
limit: 60,
priority: 1
}
}
})
/* OUTPUTS */
module.exports = ({ res, next }, argsObject) => {
let { cityName } = argsObject
// S'il n'y a pas les champs obligatoire
if (!cityName) {
return errorHandling(next, requiredFields)
}
cityName = cityName.split(' ').join('+')
// Récupère les données météo grâce à l'API : openweathermap.org. (→ avec limite de 50 requêtes par minute)
queue.request(
() => {
axios
.get(
`https://api.openweathermap.org/data/2.5/weather?q=${cityName}&lang=fr&units=metric&appid=${WEATHER_API_KEY}`
)
.then(response => {
const json = response.data
const showDateTimeValue = dateTimeUTC(
(json.timezone / 60 / 60).toString()
).showDateTimeValue
const resultHTML = `<p>🌎 Position : <a href="https://www.google.com/maps/search/?api=1&query=${
json.coord.lat
},${json.coord.lon}" rel="noopener noreferrer" target="_blank">${
json.name
}, ${
json.sys.country
}</a><br/> Date et heure : ${showDateTimeValue} <br/> Météo : ${capitalize(
json.weather[0].description
)}<br/>🌡 Température : ${json.main.temp} °C<br/> 💧 Humidité : ${
json.main.humidity
}% <br/> <img src="https://openweathermap.org/img/wn/${
json.weather[0].icon
}@2x.png"/></p>`
return res.status(200).json({ result: json, resultHTML })
})
.catch(() =>
errorHandling(next, {
message:
"La ville n'existe pas (dans l'API de openweathermap.org).",
statusCode: 404
})
)
},
'everyone',
'weatherRequest'
)
}

@ -0,0 +1,12 @@
/**
* @description Majuscule à la 1ère lettre d'une string.
* @param {String} s
* @returns {String}
* @examples capitalize('hello world!') 'Hello world!'
*/
function capitalize (s) {
if (typeof s !== 'string') return ''
return s.charAt(0).toUpperCase() + s.slice(1)
}
module.exports = capitalize

@ -0,0 +1,44 @@
/**
* @description Donne la date et l'heure selon l'UTC (Universal Time Coordinated).
* @param {String} utc Heure de décalage par rapport à l'UTC
* @returns {Function} showDateTime(enteredOffset) Retourne l'exécution de la fonction showDateTime
* @examples dateTimeUTC('0')
*/
function dateTimeUTC (utc) {
const timeNow = new Date()
const utcOffset = timeNow.getTimezoneOffset()
timeNow.setMinutes(timeNow.getMinutes() + utcOffset)
const enteredOffset = parseFloat(utc) * 60
timeNow.setMinutes(timeNow.getMinutes() + enteredOffset)
return showDateTime(timeNow)
}
/**
* @description Affiche la date et l'heure (format : dd/mm/yyyy - 00:00:00).
* @requires {@link fonctions_annexes.js: showDateTime}
* @param {String} utc Heure de décalage par rapport à l'UTC
* @returns {Object} Retourne un objet contenant l'année, le mois, le jour, l'heure, les minutes, les secondes et la date formaté
* @examples dateTimeUTC('0') dateTimeUTC vous renvoie l'exécution de showDateTime
*/
function showDateTime (timeNow) {
const year = timeNow.getFullYear()
const month = ('0' + (timeNow.getMonth() + 1)).slice(-2)
const day = ('0' + timeNow.getDate()).slice(-2)
const hour = ('0' + timeNow.getHours()).slice(-2)
const minute = ('0' + timeNow.getMinutes()).slice(-2)
const second = ('0' + timeNow.getSeconds()).slice(-2)
const showDateTimeValue =
day + '/' + month + '/' + year + ' - ' + hour + ':' + minute + ':' + second
const objectDateTime = {
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
showDateTimeValue: showDateTimeValue
}
return objectDateTime
}
module.exports = dateTimeUTC

@ -0,0 +1,14 @@
/**
* @description Formate un nombre avec des espaces.
* @param {Number} number
* @param {String} separator Le séparateur utilisé pour la virgule (exemple: "." ou ",")
* @returns {String} - Le nombre formaté
* @examples formatNumberResult(76120) '76 120'
*/
function formatNumberResult (number, separator = '.') {
const parts = number.toString().split(separator)
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
return parts.join(separator)
}
module.exports = formatNumberResult

Binary file not shown.

After

(image error) Size: 9.2 KiB

Binary file not shown.

After

(image error) Size: 1.5 KiB

Binary file not shown.

After

(image error) Size: 16 KiB

Binary file not shown.

After

(image error) Size: 21 KiB

Binary file not shown.

After

(image error) Size: 12 KiB

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

After

(image error) Size: 23 KiB

Binary file not shown.

After

(image error) Size: 1.8 KiB

Binary file not shown.

After

(image error) Size: 29 KiB

Binary file not shown.

After

(image error) Size: 5.8 KiB

Binary file not shown.

After

(image error) Size: 16 KiB

Binary file not shown.

After

(image error) Size: 11 KiB

Binary file not shown.

After

(image error) Size: 1.5 KiB

Binary file not shown.

After

(image error) Size: 16 KiB

Binary file not shown.

After

(image error) Size: 2.3 KiB

Binary file not shown.

After

(image error) Size: 9.5 KiB

Binary file not shown.

After

(image error) Size: 10 KiB

Binary file not shown.

After

(image error) Size: 17 KiB

Binary file not shown.

After

(image error) Size: 11 KiB

Binary file not shown.

After

(image error) Size: 2.0 KiB

Binary file not shown.

After

(image error) Size: 8.8 KiB

Binary file not shown.

After

(image error) Size: 16 KiB

Binary file not shown.

After

(image error) Size: 9.1 KiB

@ -0,0 +1,15 @@
const Sequelize = require('sequelize')
const { DATABASE } = require('../config/config')
const sequelize = new Sequelize(
DATABASE.name,
DATABASE.user,
DATABASE.password,
{
dialect: 'mysql',
host: DATABASE.host,
port: DATABASE.port
}
)
module.exports = sequelize

@ -0,0 +1,19 @@
const fs = require('fs')
const path = require('path')
function deleteFilesNameStartWith (pattern, dirPath, callback) {
fs.readdir(path.resolve(dirPath), (_error, fileNames) => {
for (const name of fileNames) {
const splitedName = name.split('.')
if (splitedName.length === 2) {
const fileName = splitedName[0]
if (fileName === pattern && name !== 'default.png') {
return fs.unlink(path.join(dirPath, name), callback)
}
}
}
return callback()
})
}
module.exports = deleteFilesNameStartWith

@ -0,0 +1,7 @@
function errorHandling (next, { statusCode, message }) {
const error = new Error(message)
error.statusCode = statusCode
next(error)
}
module.exports = errorHandling

@ -0,0 +1,38 @@
const errorHandling = require('../utils/errorHandling')
const { serverError } = require('../config/errors')
const helperQueryNumber = require('../utils/helperQueryNumber')
const DEFAULT_OPTIONS = {
order: [['createdAt', 'DESC']]
}
/**
* @description Permet de faire un système de pagination sur un model Sequelize
* @param {Object} Object { req, res, next }
* @param {*} Model Model Sequelize
* @param {Object} options Options avec clause where etc.
*/
async function getPagesHelper (
{ req, res, next },
Model,
options = DEFAULT_OPTIONS
) {
const page = helperQueryNumber(req.query.page, 1)
const limit = helperQueryNumber(req.query.limit, 10)
const offset = (page - 1) * limit
try {
const result = await Model.findAndCountAll({
limit,
offset,
...options
})
const { count, rows } = result
const hasMore = page * limit < count
return res.status(200).json({ totalItems: count, hasMore, rows })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
module.exports = getPagesHelper

@ -0,0 +1,6 @@
function helperQueryNumber (value, defaultValue) {
if (value && !isNaN(value)) return parseInt(value)
return defaultValue
}
module.exports = helperQueryNumber

424
api/controllers/admin.js Normal file

@ -0,0 +1,424 @@
const path = require('path')
const fs = require('fs')
const { validationResult } = require('express-validator')
const errorHandling = require('../assets/utils/errorHandling')
const { serverError } = require('../assets/config/errors')
const Functions = require('../models/functions')
const Categories = require('../models/categories')
const Quotes = require('../models/quotes')
const Users = require('../models/users')
const helperQueryNumber = require('../assets/utils/helperQueryNumber')
const getPagesHelper = require('../assets/utils/getPagesHelper')
const Sequelize = require('sequelize')
const deleteFilesNameStartWith = require('../assets/utils/deleteFilesNameStartWith')
const { EMAIL_INFO, FRONT_END_HOST } = require('../assets/config/config')
const transporter = require('../assets/config/transporter')
const { emailQuoteTemplate } = require('../assets/config/emails')
const handleEditFunction = async (
res,
resultFunction,
{ title, slug, description, type, categorieId, isOnline },
imageName = false
) => {
resultFunction.title = title
resultFunction.slug = slug
resultFunction.description = description
resultFunction.type = type
resultFunction.categorieId = categorieId
resultFunction.isOnline = isOnline
if (imageName) {
resultFunction.image = `/images/functions/${imageName}`
}
const result = await resultFunction.save()
res.status(200).json({ message: 'La fonction a bien été modifié!', result })
}
exports.getFunctions = async (req, res, next) => {
const categoryId = helperQueryNumber(req.query.categoryId, 0)
let search = req.query.search
try {
search = search.toLowerCase()
} catch {}
const options = {
where: {
// Trie par catégorie
...(categoryId !== 0 && { categorieId: categoryId }),
// Recherche
...(search != null && {
[Sequelize.Op.or]: [
{
title: Sequelize.where(
Sequelize.fn('LOWER', Sequelize.col('title')),
'LIKE',
`%${search}%`
)
},
{
slug: Sequelize.where(
Sequelize.fn('LOWER', Sequelize.col('slug')),
'LIKE',
`%${search}%`
)
},
{
description: Sequelize.where(
Sequelize.fn('LOWER', Sequelize.col('description')),
'LIKE',
`%${search}%`
)
}
]
})
},
include: [{ model: Categories, attributes: ['name', 'color'] }],
attributes: {
exclude: ['updatedAt', 'utilizationForm', 'article', 'isOnline']
},
order: [['createdAt', 'DESC']]
}
return await getPagesHelper({ req, res, next }, Functions, options)
}
exports.getFunctionBySlug = (req, res, next) => {
const { slug } = req.params
Functions.findOne({
where: { slug },
include: [{ model: Categories, attributes: ['name', 'color'] }]
})
.then(result => {
if (!result) {
return errorHandling(next, {
message: "La fonction n'existe pas.",
statusCode: 404
})
}
try {
result.utilizationForm = JSON.parse(result.utilizationForm)
} catch {}
return res.status(200).json(result)
})
.catch(error => {
console.log(error)
return errorHandling(next, serverError)
})
}
exports.postFunction = (req, res, next) => {
const { title, slug, description, type, categorieId } = req.body
const image = req.files.image
const errors = validationResult(req)
if (!errors.isEmpty()) {
return errorHandling(next, {
message: errors.array()[0].msg,
statusCode: 400
})
}
if (
(!image || image.truncated) &&
(image.mimetype !== 'image/png' ||
image.mimetype !== 'image/jpg' ||
image.mimetype !== 'image/jpeg')
) {
return errorHandling(next, {
message: 'La fonction doit avoir une image valide.',
statusCode: 400
})
}
const splitedImageName = image.name.split('.')
if (splitedImageName.length !== 2) return errorHandling(next, serverError)
const imageName = slug + '.' + splitedImageName[1]
image.mv(
path.join(__dirname, '..', 'assets', 'images', 'functions') +
'/' +
imageName,
async error => {
if (error) return errorHandling(next, serverError)
try {
const result = await Functions.create({
title,
slug,
description,
type,
categorieId,
image: `/images/functions/${imageName}`
})
return res
.status(201)
.json({ message: 'La fonction a été correctement ajouté!', result })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
)
}
exports.putFunction = async (req, res, next) => {
const { id } = req.params
const { title, slug, description, type, categorieId, isOnline } = req.body
const image = req.files.image
const errors = validationResult(req)
if (!errors.isEmpty()) {
return errorHandling(next, {
message: errors.array()[0].msg,
statusCode: 400
})
}
try {
// Vérifie si la fonction existe
const resultFunction = await Functions.findOne({ where: { id } })
if (!resultFunction) {
return errorHandling(next, {
message: "La fonction n'existe pas.",
statusCode: 404
})
}
// Vérifie si le slug existe déjà
const FunctionSlug = await Functions.findOne({ where: { slug } })
if (!FunctionSlug && FunctionSlug.id !== resultFunction.id) {
return errorHandling(next, {
message: 'Le slug existe déjà...',
statusCode: 404
})
}
// Sauvegarde de la fonction
if (image != null) {
if (
image.truncated &&
(image.mimetype !== 'image/png' ||
image.mimetype !== 'image/jpg' ||
image.mimetype !== 'image/jpeg')
) {
return errorHandling(next, {
message: 'La fonction doit avoir une image valide.',
statusCode: 400
})
}
const splitedImageName = image.name.split('.')
if (splitedImageName.length !== 2) return errorHandling(next, serverError)
const imageName = slug + '.' + splitedImageName[1]
// Supprime les anciennes images
const functionPath = path.join(
__dirname,
'..',
'assets',
'images',
'functions'
)
deleteFilesNameStartWith(slug, functionPath, () => {
image.mv(path.join(functionPath, imageName), async error => {
if (error) return errorHandling(next, serverError)
return await handleEditFunction(
res,
resultFunction,
{ title, slug, description, type, categorieId, isOnline },
imageName
)
})
})
} else {
return await handleEditFunction(res, resultFunction, {
title,
slug,
description,
type,
categorieId,
isOnline
})
}
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.putFunctionArticle = async (req, res, next) => {
const { id } = req.params
const { article } = req.body
try {
// Vérifie si la fonction existe
const resultFunction = await Functions.findOne({ where: { id } })
if (!resultFunction) {
return errorHandling(next, {
message: "La fonction n'existe pas.",
statusCode: 404
})
}
resultFunction.article = article
const result = await resultFunction.save()
return res.status(200).json(result)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.putFunctionForm = async (req, res, next) => {
const { id } = req.params
const { form } = req.body
try {
// Vérifie si la fonction existe
const resultFunction = await Functions.findOne({ where: { id } })
if (!resultFunction) {
return errorHandling(next, {
message: "La fonction n'existe pas.",
statusCode: 404
})
}
resultFunction.utilizationForm = JSON.stringify(form)
const result = await resultFunction.save()
return res.status(200).json(result)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.deleteFunction = async (req, res, next) => {
const { id } = req.params
try {
const result = await Functions.findOne({ where: { id } })
if (!result) {
return errorHandling(next, {
message: "La fonction n'existe pas.",
statusCode: 404
})
}
if (result.image !== '/images/functions/default.png') {
const filePath = path.join(__dirname, '..', 'assets', result.image)
fs.unlinkSync(filePath) // supprime le fichier
}
await result.destroy()
res
.status(200)
.json({ message: 'La fonction a été correctement supprimé!' })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.postCategory = async (req, res, next) => {
const { name, color } = req.body
if (!(name && color)) {
return errorHandling(next, {
message: 'La catégorie doit avoir un nom et une couleur.'
})
}
try {
const result = await Categories.create({ name, color })
return res
.status(201)
.json({ message: 'La catégorie a bien été crée!', result })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.putCategory = async (req, res, next) => {
const { name, color } = req.body
const { id } = req.params
if (!(name && color && id)) {
return errorHandling(next, {
message: 'La catégorie doit avoir un nom, une couleur et un id.'
})
}
try {
const category = await Categories.findOne({ where: { id } })
if (!category) {
return errorHandling(next, { message: "La catégorie n'existe pas." })
}
category.name = name
category.color = color
const result = await category.save()
return res
.status(200)
.json({ message: 'La catégorie a bien été modifiée!', result })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.deleteCategory = async (req, res, next) => {
const { id } = req.params
try {
const category = await Categories.findOne({ where: { id } })
if (!category) {
return errorHandling(next, { message: "La catégorie n'existe pas." })
}
await category.destroy()
return res
.status(200)
.json({ message: 'La catégorie a bien été supprimée!' })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.getQuotes = async (req, res, next) => {
const options = {
where: {
isValidated: 0
},
include: [{ model: Users, attributes: ['name', 'logo'] }],
order: [['createdAt', 'DESC']]
}
return await getPagesHelper({ req, res, next }, Quotes, options)
}
exports.putQuote = async (req, res, next) => {
const { id } = req.params
const { isValid } = req.body
try {
if (typeof isValid !== 'boolean') {
return errorHandling(next, {
message: 'isValid doit être un booléen.',
statusCode: 400
})
}
const quote = await Quotes.findOne({
where: {
id,
isValidated: 0
},
include: [{ model: Users, attributes: ['name', 'email'] }]
})
if (!quote) {
return errorHandling(next, {
message: "La citation n'existe pas (ou est déjà validé).",
statusCode: 404
})
}
await transporter.sendMail({
from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
to: quote.user.email,
subject: 'FunctionProject - Citation proposée',
html: emailQuoteTemplate(isValid, quote, FRONT_END_HOST)
})
if (isValid) {
quote.isValidated = true
await quote.save()
return res
.status(200)
.json({ message: 'La citation a bien été validée!' })
} else {
await quote.destroy()
return res
.status(200)
.json({ imessage: 'La citation a bien été supprimée!' })
}
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}

@ -0,0 +1,14 @@
const errorHandling = require('../assets/utils/errorHandling')
const Categories = require('../models/categories')
const { serverError } = require('../assets/config/errors')
exports.getCategories = (_req, res, next) => {
Categories.findAll()
.then(result => {
res.status(200).json(result)
})
.catch(error => {
console.log(error)
return errorHandling(next, serverError)
})
}

@ -0,0 +1,99 @@
const errorHandling = require('../assets/utils/errorHandling')
const Comments = require('../models/comments')
const Users = require('../models/users')
const Functions = require('../models/functions')
const getPagesHelper = require('../assets/utils/getPagesHelper')
const { serverError } = require('../assets/config/errors')
exports.getCommentsByFunctionId = async (req, res, next) => {
const { functionId } = req.params
const options = {
where: { functionId },
include: [{ model: Users, attributes: ['name', 'logo'] }],
order: [['createdAt', 'DESC']]
}
return await getPagesHelper({ req, res, next }, Comments, options)
}
exports.postCommentsByFunctionId = async (req, res, next) => {
const { functionId } = req.params
const { message } = req.body
try {
const resultFunction = await Functions.findOne({
where: { id: functionId }
})
if (!resultFunction) {
return errorHandling(next, {
message: "La fonction n'existe pas.",
statusCode: 404
})
}
if (!message) {
return errorHandling(next, {
message: 'Vous ne pouvez pas poster de commentaire vide.',
statusCode: 400
})
}
const comment = await Comments.create({
message,
userId: req.userId,
functionId
})
return res.status(201).json(comment)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.deleteCommentById = async (req, res, next) => {
const { commentId } = req.params
try {
const comment = await Comments.findOne({
where: { userId: req.userId, id: parseInt(commentId) }
})
if (!comment) {
return errorHandling(next, {
message: "Le commentaire n'existe pas.",
statusCode: 404
})
}
await comment.destroy()
return res
.status(200)
.json({ message: 'Le commentaire a bien été supprimé.' })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.putCommentsById = async (req, res, next) => {
const { commentId } = req.params
const { message } = req.body
if (!message) {
return errorHandling(next, {
message: 'Vous ne pouvez pas poster de commentaire vide.',
statusCode: 400
})
}
try {
const comment = await Comments.findOne({
where: { userId: req.userId, id: parseInt(commentId) }
})
if (!comment) {
return errorHandling(next, {
message: "Le commentaire n'existe pas.",
statusCode: 404
})
}
comment.message = message
await comment.save()
return res
.status(200)
.json({ message: 'Le commentaire a bien été modifié.' })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}

@ -0,0 +1,92 @@
const errorHandling = require('../assets/utils/errorHandling')
const { serverError } = require('../assets/config/errors')
const Favorites = require('../models/favorites')
const Functions = require('../models/functions')
exports.getFavoriteByFunctionId = async (req, res, next) => {
const { functionId } = req.params
const { userId } = req
try {
const favorite = await Favorites.findOne({
where: {
userId,
functionId
}
})
if (!favorite) {
return res.status(200).json({ isFavorite: false })
}
return res.status(200).json({ isFavorite: true })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.postFavoriteByFunctionId = async (req, res, next) => {
const { functionId } = req.params
const { userId } = req
try {
const resultFunction = await Functions.findOne({
where: { id: functionId }
})
if (!resultFunction) {
return errorHandling(next, {
message: "La fonction n'existe pas.",
statusCode: 404
})
}
const favorite = await Favorites.findOne({
where: {
userId,
functionId
}
})
if (!favorite) {
await Favorites.create({ userId, functionId })
return res.status(201).json({ result: 'Le favoris a bien été ajouté!' })
}
return errorHandling(next, {
message: 'La fonction est déjà en favoris.',
statusCode: 400
})
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.deleteFavoriteByFunctionId = async (req, res, next) => {
const { functionId } = req.params
const { userId } = req
try {
const resultFunction = await Functions.findOne({
where: { id: functionId }
})
if (!resultFunction) {
return errorHandling(next, {
message: "La fonction n'existe pas.",
statusCode: 404
})
}
const favorite = await Favorites.findOne({
where: {
userId,
functionId
}
})
if (!favorite) {
return errorHandling(next, {
message: "Le fonction n'est pas en favoris.",
statusCode: 400
})
}
await favorite.destroy()
return res
.status(200)
.json({ message: 'Le fonction a bien été supprimé des favoris.' })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}

@ -0,0 +1,93 @@
const errorHandling = require('../assets/utils/errorHandling')
const { serverError } = require('../assets/config/errors')
const Functions = require('../models/functions')
const Categories = require('../models/categories')
const functionToExecute = require('../assets/functions/functionObject')
const helperQueryNumber = require('../assets/utils/helperQueryNumber')
const getPagesHelper = require('../assets/utils/getPagesHelper')
const Sequelize = require('sequelize')
exports.getFunctions = async (req, res, next) => {
const categoryId = helperQueryNumber(req.query.categoryId, 0)
let { search } = req.query
try {
search = search.toLowerCase()
} catch {}
const options = {
where: {
isOnline: 1,
// Trie par catégorie
...(categoryId !== 0 && { categorieId: categoryId }),
// Recherche
...(search != null && {
[Sequelize.Op.or]: [
{
title: Sequelize.where(
Sequelize.fn('LOWER', Sequelize.col('title')),
'LIKE',
`%${search}%`
)
},
{
slug: Sequelize.where(
Sequelize.fn('LOWER', Sequelize.col('slug')),
'LIKE',
`%${search}%`
)
},
{
description: Sequelize.where(
Sequelize.fn('LOWER', Sequelize.col('description')),
'LIKE',
`%${search}%`
)
}
]
})
},
include: [{ model: Categories, attributes: ['name', 'color'] }],
attributes: {
exclude: ['updatedAt', 'utilizationForm', 'article', 'isOnline']
},
order: [['createdAt', 'DESC']]
}
return await getPagesHelper({ req, res, next }, Functions, options)
}
exports.getFunctionBySlug = (req, res, next) => {
const { slug } = req.params
Functions.findOne({
where: { slug, isOnline: 1 },
attributes: {
exclude: ['updatedAt', 'isOnline']
},
include: [{ model: Categories, attributes: ['name', 'color'] }]
})
.then(result => {
if (!result) {
return errorHandling(next, {
message: "La fonction n'existe pas.",
statusCode: 404
})
}
try {
result.utilizationForm = JSON.parse(result.utilizationForm)
} catch {}
return res.status(200).json(result)
})
.catch(error => {
console.log(error)
return errorHandling(next, serverError)
})
}
exports.executeFunctionBySlug = (req, res, next) => {
const functionOutput = functionToExecute(req.params.slug)
if (functionOutput !== undefined) {
return functionOutput({ res, next }, req.body)
}
return errorHandling(next, {
message: "La fonction n'existe pas.",
statusCode: 404
})
}

@ -0,0 +1,192 @@
const validator = require('validator')
const errorHandling = require('../assets/utils/errorHandling')
const { requiredFields, serverError } = require('../assets/config/errors')
const ShortLinks = require('../models/short_links')
const getPagesHelper = require('../assets/utils/getPagesHelper')
const Sequelize = require('sequelize')
const shortLinkBaseURL = 'https://s.divlo.fr'
exports.getLinks = async (req, res, next) => {
const { userId } = req
const options = {
where: { userId },
order: [['createdAt', 'DESC']]
}
return await getPagesHelper({ req, res, next }, ShortLinks, options)
}
exports.postLink = async (req, res, next) => {
const { userId } = req
let { url, shortcutName } = req.body
// S'il n'y a pas les champs obligatoire
if (!(url && shortcutName)) {
return errorHandling(next, requiredFields)
}
// Si ce n'est pas une url
if (!validator.isURL(url)) {
return errorHandling(next, {
message: 'Veuillez entré une URL valide.',
statusCode: 400
})
}
// Si ce n'est pas de type slug
if (!validator.isSlug(shortcutName)) {
return errorHandling(next, {
message:
"Le nom de votre raccourci doit être de type slug (ne pas contenir d'espaces, ni de caractères spéciaux).",
statusCode: 400
})
}
// Sanitize shortcutName
shortcutName = validator.escape(shortcutName)
shortcutName = validator.trim(shortcutName)
shortcutName = validator.blacklist(shortcutName, ' ')
try {
// Si l'url a déjà été raccourcie
const urlInDatabase = await ShortLinks.findOne({ where: { url } })
if (urlInDatabase) {
const urlShort = `${shortLinkBaseURL}/${urlInDatabase.shortcut}`
return errorHandling(next, {
message: `L'url a déjà été raccourcie... <br/> <br/> <a target="_blank" rel="noopener noreferrer" href="${urlShort}">${urlShort}</a>`,
statusCode: 400
})
}
// Si le nom du raccourci existe déjà
const shortcutInDatabase = await ShortLinks.findOne({
where: { shortcut: shortcutName }
})
if (shortcutInDatabase) {
const urlShort = `${shortLinkBaseURL}/${shortcutInDatabase.shortcut}`
return errorHandling(next, {
message: `Le nom du raccourci a déjà été utilisé... <br/> <br/> <a target="_blank" rel="noopener noreferrer" href="${urlShort}">${urlShort}</a>`,
statusCode: 400
})
}
// Ajout du lien raccourci
const result = await ShortLinks.create({
url,
shortcut: shortcutName,
userId
})
const shortcutLinkResult = `${shortLinkBaseURL}/${result.shortcut}`
return res.status(200).json({
resultHTML: `URL Raccourcie : <br/> <br/> <a target="_blank" rel="noopener noreferrer" href="${shortcutLinkResult}">${shortcutLinkResult}</a>`,
result: shortcutLinkResult,
linkDatabase: result
})
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.putLink = async (req, res, next) => {
const { id } = req.params
const { userId } = req
let { url, shortcutName } = req.body
// S'il n'y a pas les champs obligatoire
if (!(url && shortcutName)) {
return errorHandling(next, requiredFields)
}
// Si ce n'est pas une url
if (!validator.isURL(url)) {
return errorHandling(next, {
message: 'Veuillez entré une URL valide.',
statusCode: 400
})
}
// Si ce n'est pas de type slug
if (!validator.isSlug(shortcutName)) {
return errorHandling(next, {
message:
"Le nom de votre raccourci doit être de type slug (ne pas contenir d'espaces, ni de caractères spéciaux).",
statusCode: 400
})
}
// Sanitize shortcutName
shortcutName = validator.escape(shortcutName)
shortcutName = validator.trim(shortcutName)
shortcutName = validator.blacklist(shortcutName, ' ')
try {
// Si l'url a déjà été raccourcie par quelqu'un d'autre
const urlInDatabase = await ShortLinks.findOne({
where: { url, [Sequelize.Op.not]: { userId } }
})
if (urlInDatabase) {
const urlShort = `${shortLinkBaseURL}/${urlInDatabase.shortcut}`
return errorHandling(next, {
message: `L'url a déjà été raccourcie... <br/> <br/> <a target="_blank" rel="noopener noreferrer" href="${urlShort}">${urlShort}</a>`,
statusCode: 400
})
}
// Si le nom du raccourci existe déjà par quelqu'un d'autre
const shortcutInDatabase = await ShortLinks.findOne({
where: { shortcut: shortcutName, [Sequelize.Op.not]: { userId } }
})
if (shortcutInDatabase) {
const urlShort = `${shortLinkBaseURL}/${shortcutInDatabase.shortcut}`
return errorHandling(next, {
message: `Le nom du raccourci a déjà été utilisé... <br/> <br/> <a target="_blank" rel="noopener noreferrer" href="${urlShort}">${urlShort}</a>`,
statusCode: 400
})
}
// Modification du lien raccourci
const result = await ShortLinks.findOne({
where: { id, userId }
})
console.log(result)
if (!result) {
return errorHandling(next, {
statusCode: 404,
message: "Le raccourci n'existe pas..."
})
}
result.url = url
result.shortcut = shortcutName
const { shortcut } = await result.save()
const shortcutLinkResult = `${shortLinkBaseURL}/${shortcut}`
return res.status(200).json({
resultHTML: `URL Raccourcie : <br/> <br/> <a target="_blank" rel="noopener noreferrer" href="${shortcutLinkResult}">${shortcutLinkResult}</a>`,
result: shortcutLinkResult
})
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.deleteLink = async (req, res, next) => {
const { id } = req.params
const { userId } = req
try {
const linkResult = await ShortLinks.findOne({
where: { id, userId }
})
if (!linkResult) {
return errorHandling(next, {
message: "Le lien raccourci n'existe pas.",
statusCode: 404
})
}
await linkResult.destroy()
return res.status(200).json({ message: 'La lien a bien été supprimé!' })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}

40
api/controllers/quotes.js Normal file

@ -0,0 +1,40 @@
const errorHandling = require('../assets/utils/errorHandling')
const { serverError, requiredFields } = require('../assets/config/errors')
const Quotes = require('../models/quotes')
const Users = require('../models/users')
const getPagesHelper = require('../assets/utils/getPagesHelper')
exports.getQuotes = async (req, res, next) => {
const options = {
where: {
isValidated: 1
},
include: [{ model: Users, attributes: ['name', 'logo'] }],
attributes: {
exclude: ['isValidated']
},
order: [['createdAt', 'DESC']]
}
return await getPagesHelper({ req, res, next }, Quotes, options)
}
exports.postQuote = (req, res, next) => {
const { quote, author } = req.body
// S'il n'y a pas les champs obligatoire
if (!(quote && author)) {
return errorHandling(next, requiredFields)
}
Quotes.create({ quote, author, userId: req.userId })
.then(_result => {
return res
.status(200)
.json({
message:
"La citation a bien été ajoutée, elle est en attente de confirmation d'un administrateur."
})
})
.catch(error => {
console.log(error)
return errorHandling(next, serverError)
})
}

83
api/controllers/tasks.js Normal file

@ -0,0 +1,83 @@
const errorHandling = require('../assets/utils/errorHandling')
const { serverError, requiredFields } = require('../assets/config/errors')
const Tasks = require('../models/tasks')
exports.getTasks = async (req, res, next) => {
try {
const tasks = await Tasks.findAll({
where: {
userId: req.userId
},
order: [['createdAt', 'DESC']]
})
return res.status(200).json(tasks)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.postTask = async (req, res, next) => {
const { task } = req.body
try {
if (!task) {
return errorHandling(next, requiredFields)
}
const taskResult = await Tasks.create({ task, userId: req.userId })
return res.status(201).json(taskResult)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.putTask = async (req, res, next) => {
const { id } = req.params
const { isCompleted } = req.body
try {
if (typeof isCompleted !== 'boolean') {
return errorHandling(next, {
message: 'isCompleted doit être un booléen.',
statusCode: 400
})
}
const taskResult = await Tasks.findOne({
where: { id, userId: req.userId }
})
if (!taskResult) {
return errorHandling(next, {
message: 'La "tâche à faire" n\'existe pas.',
statusCode: 404
})
}
taskResult.isCompleted = isCompleted
const taskSaved = await taskResult.save()
return res.status(200).json(taskSaved)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.deleteTask = async (req, res, next) => {
const { id } = req.params
try {
const taskResult = await Tasks.findOne({
where: { id, userId: req.userId }
})
if (!taskResult) {
return errorHandling(next, {
message: 'La "tâche à faire" n\'existe pas.',
statusCode: 404
})
}
await taskResult.destroy()
return res
.status(200)
.json({ message: 'La "tâche à faire" a bien été supprimée!' })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}

454
api/controllers/users.js Normal file

@ -0,0 +1,454 @@
const path = require('path')
const { validationResult } = require('express-validator')
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const ms = require('ms')
const uuid = require('uuid')
const Sequelize = require('sequelize')
const errorHandling = require('../assets/utils/errorHandling')
const { serverError, generalError } = require('../assets/config/errors')
const {
JWT_SECRET,
FRONT_END_HOST,
EMAIL_INFO,
HOST,
TOKEN_LIFE
} = require('../assets/config/config')
const transporter = require('../assets/config/transporter')
const { emailUserTemplate } = require('../assets/config/emails')
const Users = require('../models/users')
const Favorites = require('../models/favorites')
const Functions = require('../models/functions')
const Categories = require('../models/categories')
const Comments = require('../models/comments')
const Quotes = require('../models/quotes')
const deleteFilesNameStartWith = require('../assets/utils/deleteFilesNameStartWith')
const getPagesHelper = require('../assets/utils/getPagesHelper')
async function handleEditUser (
res,
{ name, email, biography, isPublicEmail },
userId,
logoName
) {
const user = await Users.findOne({ where: { id: userId } })
user.name = name
if (user.email !== email) {
const tempToken = uuid.v4()
user.email = email
user.isConfirmed = false
user.tempToken = tempToken
await transporter.sendMail({
from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
to: email,
subject: "FunctionProject - Confirmer l'email",
html: emailUserTemplate(
"Veuillez confirmer l'email",
'Oui, je confirme.',
`${HOST}/users/confirm-email/${tempToken}`,
'Si vous avez reçu ce message par erreur, il suffit de le supprimer. Votre email ne serez pas confirmé si vous ne cliquez pas sur le lien de confirmation ci-dessus.'
)
})
}
if (biography != null) {
user.biography = biography
}
user.isPublicEmail = isPublicEmail
if (logoName != null && `/images/users/${logoName}` !== user.logo) {
user.logo = `/images/users/${logoName}`
}
await user.save()
return res
.status(200)
.json({
id: user.id,
name: user.name,
email: user.email,
biography: user.biography,
logo: user.logo,
isPublicEmail: user.isPublicEmail,
isAdmin: user.isAdmin,
createdAt: user.createdAt
})
}
exports.getUsers = async (req, res, next) => {
let { search } = req.query
try {
search = search.toLowerCase()
} catch {}
const options = {
where: {
isConfirmed: true,
// Recherche
...(search != null && {
name: Sequelize.where(
Sequelize.fn('LOWER', Sequelize.col('name')),
'LIKE',
`%${search}%`
)
})
},
attributes: {
exclude: [
'updatedAt',
'isAdmin',
'isConfirmed',
'password',
'tempToken',
'tempExpirationToken',
'isPublicEmail',
'email'
]
},
order: [['createdAt', 'DESC']]
}
return await getPagesHelper({ req, res, next }, Users, options)
}
exports.putUser = async (req, res, next) => {
const { name, email, biography, isPublicEmail } = req.body
const logo = req.files.logo
const errors = validationResult(req)
if (!errors.isEmpty()) {
return errorHandling(next, {
message: errors.array()[0].msg,
statusCode: 400
})
}
if (logo != null) {
if (
(!logo || logo.truncated) &&
(logo.mimetype !== 'image/png' ||
logo.mimetype !== 'image/jpg' ||
logo.mimetype !== 'image/jpeg' ||
logo.mimetype !== 'image/gif')
) {
return errorHandling(next, {
message:
'Le profil doit avoir une image valide (PNG, JPG, GIF) et moins de 5mo.',
statusCode: 400
})
}
const splitedLogoName = logo.name.split('.')
if (splitedLogoName.length !== 2) return errorHandling(next, serverError)
const logoName = name + req.userId + '.' + splitedLogoName[1]
// Supprime les anciens logo
try {
deleteFilesNameStartWith(
`${name + req.userId}`,
path.join(__dirname, '..', 'assets', 'images', 'users'),
async () => {
logo.mv(
path.join(__dirname, '..', 'assets', 'images', 'users', logoName),
async error => {
if (error) return errorHandling(next, serverError)
return await handleEditUser(
res,
{ name, email, biography, isPublicEmail },
req.userId,
logoName
)
}
)
}
)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
} else {
try {
return await handleEditUser(
res,
{ name, email, biography, isPublicEmail },
req.userId,
null
)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
}
exports.register = async (req, res, next) => {
const { name, email, password } = req.body
const errors = validationResult(req)
if (!errors.isEmpty()) {
return errorHandling(next, {
message: errors.array()[0].msg,
statusCode: 400
})
}
try {
const hashedPassword = await bcrypt.hash(password, 12)
const tempToken = uuid.v4()
await Users.create({ email, name, password: hashedPassword, tempToken })
await transporter.sendMail({
from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
to: email,
subject: "FunctionProject - Confirmer l'inscription",
html: emailUserTemplate(
"Veuillez confirmer l'inscription",
"Oui, je m'inscris.",
`${HOST}/users/confirm-email/${tempToken}`,
'Si vous avez reçu ce message par erreur, il suffit de le supprimer. Vous ne serez pas inscrit si vous ne cliquez pas sur le lien de confirmation ci-dessus.'
)
})
return res
.status(201)
.json({
result:
"Vous y êtes presque, veuillez vérifier vos emails pour confirmer l'inscription."
})
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.login = async (req, res, next) => {
const { email, password } = req.body
const errors = validationResult(req)
if (!errors.isEmpty()) {
return errorHandling(next, {
message: errors.array()[0].msg,
statusCode: 400
})
}
try {
const user = await Users.findOne({ where: { email } })
if (!user) {
return errorHandling(next, {
message: "Le mot de passe ou l'adresse email n'est pas valide.",
statusCode: 400
})
}
const isEqual = await bcrypt.compare(password, user.password)
if (!isEqual) {
return errorHandling(next, {
message: "Le mot de passe ou l'adresse email n'est pas valide.",
statusCode: 400
})
}
if (!user.isConfirmed) {
return errorHandling(next, {
message:
'Vous devez valider votre adresse email pour votre première connexion.',
statusCode: 400
})
}
const token = jwt.sign(
{
email: user.email,
userId: user.id
},
JWT_SECRET,
{ expiresIn: TOKEN_LIFE }
)
return res
.status(200)
.json({
token,
id: user.id,
name: user.name,
email: user.email,
biography: user.biography,
logo: user.logo,
isPublicEmail: user.isPublicEmail,
isAdmin: user.isAdmin,
createdAt: user.createdAt,
expiresIn: Math.round(ms(TOKEN_LIFE) / 1000)
})
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.confirmEmail = async (req, res, next) => {
const { tempToken } = req.params
if (!tempToken) {
return errorHandling(next, generalError)
}
try {
const user = await Users.findOne({
where: { tempToken, isConfirmed: false }
})
if (!user) {
return errorHandling(next, {
message: "Le token n'est pas valide.",
statusCode: 400
})
}
user.tempToken = null
user.isConfirmed = true
await user.save()
return res.redirect(`${FRONT_END_HOST}/users/login?isConfirmed=true`)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.resetPassword = async (req, res, next) => {
const { email } = req.body
const errors = validationResult(req)
if (!errors.isEmpty()) {
return errorHandling(next, {
message: errors.array()[0].msg,
statusCode: 400
})
}
try {
const user = await Users.findOne({ where: { email, tempToken: null } })
if (!user) {
return errorHandling(next, {
message:
"L'adresse email n'existe pas ou une demande est déjà en cours.",
statusCode: 400
})
}
const tempToken = uuid.v4()
user.tempExpirationToken = Date.now() + 3600000 // 1 heure
user.tempToken = tempToken
await user.save()
await transporter.sendMail({
from: `"FunctionProject" <${EMAIL_INFO.auth.user}>`,
to: email,
subject: 'FunctionProject - Réinitialisation du mot de passe',
html: emailUserTemplate(
'Veuillez confirmer la réinitialisation du mot de passe',
'Oui, je change mon mot de passe.',
`${FRONT_END_HOST}/users/newPassword?token=${tempToken}`,
'Si vous avez reçu ce message par erreur, il suffit de le supprimer. Votre mot de passe ne sera pas réinitialiser si vous ne cliquez pas sur le lien ci-dessus. Par ailleurs, pour la sécurité de votre compte, la réinitialisation du mot de passe est disponible pendant un délai de 1 heure, passez ce temps, la réinitialisation ne sera plus valide.'
)
})
return res
.status(200)
.json({
result:
'Demande de réinitialisation du mot de passe réussi, veuillez vérifier vos emails!'
})
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.newPassword = async (req, res, next) => {
const { tempToken, password } = req.body
const errors = validationResult(req)
if (!errors.isEmpty()) {
return errorHandling(next, {
message: errors.array()[0].msg,
statusCode: 400
})
}
try {
const user = await Users.findOne({ where: { tempToken } })
if (!user && parseInt(user.tempExpirationToken) < Date.now()) {
return errorHandling(next, {
message: "Le token n'est pas valide.",
statusCode: 400
})
}
const hashedPassword = await bcrypt.hash(password, 12)
user.password = hashedPassword
user.tempToken = null
user.tempExpirationToken = null
await user.save()
return res
.status(200)
.json({ result: 'Le mot de passe a bien été modifié!' })
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}
exports.getUserInfo = async (req, res, next) => {
const { name } = req.params
try {
const user = await Users.findOne({
where: { name, isConfirmed: true },
attributes: {
exclude: [
'updatedAt',
'isAdmin',
'isConfirmed',
'password',
'tempToken',
'tempExpirationToken'
]
}
})
if (!user) {
return errorHandling(next, {
message: "L'utilisateur n'existe pas.",
statusCode: 404
})
}
const favorites = await Favorites.findAll({
where: { userId: user.id },
include: [
{
model: Functions,
attributes: {
exclude: ['updatedAt', 'utilizationForm', 'article', 'isOnline']
},
include: { model: Categories, attributes: ['name', 'color'] }
}
],
order: [['createdAt', 'DESC']],
limit: 5
})
const favoritesArray = favorites.map(favorite => favorite.function)
const comments = await Comments.findAll({
where: { userId: user.id },
include: [
{
model: Functions,
attributes: {
exclude: ['updatedAt', 'utilizationForm', 'article', 'isOnline']
}
}
],
order: [['createdAt', 'DESC']],
limit: 5
})
const commentsArray = comments.map(commentObject => {
return {
id: commentObject.id,
message: commentObject.message,
createdAt: commentObject.createdAt,
function: commentObject.function.dataValues
}
})
const quotesArray = await Quotes.findAll({
where: { userId: user.id, isValidated: 1 },
attributes: {
exclude: ['updatedAt', 'createdAt', 'isValidated', 'userId', 'id']
},
order: [['createdAt', 'DESC']],
limit: 5
})
const userObject = {
// Si Public Email
...(user.isPublicEmail && { email: user.email }),
isPublicEmail: user.isPublicEmail,
name: user.name,
biography: user.biography,
logo: user.logo,
createdAt: user.createdAt,
favoritesArray,
commentsArray,
quotesArray
}
return res.status(200).json(userObject)
} catch (error) {
console.log(error)
return errorHandling(next, serverError)
}
}

@ -0,0 +1,32 @@
const errorHandling = require('../assets/utils/errorHandling')
const { serverError } = require('../assets/config/errors')
const Users = require('../models/users')
module.exports = (req, _res, next) => {
if (!req.userId) {
return errorHandling(next, {
message: "Vous n'êtes pas connecté.",
statusCode: 403
})
}
Users.findOne({ where: { id: req.userId } })
.then(user => {
if (!user) {
return errorHandling(next, {
message: "Le mot de passe ou l'adresse email n'est pas valide.",
statusCode: 403
})
}
if (!user.isAdmin) {
return errorHandling(next, {
message: "Vous n'êtes pas administrateur.",
statusCode: 403
})
}
next()
})
.catch(error => {
console.log(error)
return errorHandling(next, serverError)
})
}

33
api/middlewares/isAuth.js Normal file

@ -0,0 +1,33 @@
const jwt = require('jsonwebtoken')
const errorHandling = require('../assets/utils/errorHandling')
const { JWT_SECRET } = require('../assets/config/config')
module.exports = (req, _res, next) => {
const token = req.get('Authorization')
if (!token) {
return errorHandling(next, {
message: 'Vous devez être connecter pour effectuer cette opération.',
statusCode: 403
})
}
let decodedToken
try {
decodedToken = jwt.verify(token, JWT_SECRET)
} catch (error) {
return errorHandling(next, {
message: 'Vous devez être connecter pour effectuer cette opération.',
statusCode: 403
})
}
if (!decodedToken) {
return errorHandling(next, {
message: 'Vous devez être connecter pour effectuer cette opération.',
statusCode: 403
})
}
req.userId = decodedToken.userId
next()
}

13
api/models/categories.js Normal file

@ -0,0 +1,13 @@
const Sequelize = require('sequelize')
const sequelize = require('../assets/utils/database')
module.exports = sequelize.define('categorie', {
name: {
type: Sequelize.STRING,
allowNull: false
},
color: {
type: Sequelize.STRING,
allowNull: false
}
})

9
api/models/comments.js Normal file

@ -0,0 +1,9 @@
const Sequelize = require('sequelize')
const sequelize = require('../assets/utils/database')
module.exports = sequelize.define('comment', {
message: {
type: Sequelize.TEXT,
allowNull: false
}
})

3
api/models/favorites.js Normal file

@ -0,0 +1,3 @@
const sequelize = require('../assets/utils/database')
module.exports = sequelize.define('favorite', {})

39
api/models/functions.js Normal file

@ -0,0 +1,39 @@
const Sequelize = require('sequelize')
const sequelize = require('../assets/utils/database')
module.exports = sequelize.define('function', {
title: {
type: Sequelize.STRING,
allowNull: false
},
slug: {
type: Sequelize.STRING,
allowNull: false
},
description: {
type: Sequelize.STRING,
allowNull: false
},
image: {
type: Sequelize.STRING,
allowNull: false,
defaultValue: '/images/functions/default.png'
},
type: {
type: Sequelize.STRING,
allowNull: false
},
article: {
type: Sequelize.TEXT,
allowNull: true
},
utilizationForm: {
type: Sequelize.TEXT,
allowNull: true
},
isOnline: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: 0
}
})

18
api/models/quotes.js Normal file

@ -0,0 +1,18 @@
const Sequelize = require('sequelize')
const sequelize = require('../assets/utils/database')
module.exports = sequelize.define('quote', {
quote: {
type: Sequelize.STRING,
allowNull: false
},
author: {
type: Sequelize.STRING,
allowNull: false
},
isValidated: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: 0
}
})

18
api/models/short_links.js Normal file

@ -0,0 +1,18 @@
const Sequelize = require('sequelize')
const sequelize = require('../assets/utils/database')
module.exports = sequelize.define('short_link', {
url: {
type: Sequelize.TEXT,
allowNull: false
},
shortcut: {
type: Sequelize.TEXT,
allowNull: false
},
count: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
}
})

14
api/models/tasks.js Normal file

@ -0,0 +1,14 @@
const Sequelize = require('sequelize')
const sequelize = require('../assets/utils/database')
module.exports = sequelize.define('task', {
task: {
type: Sequelize.STRING,
allowNull: false
},
isCompleted: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: 0
}
})

45
api/models/users.js Normal file

@ -0,0 +1,45 @@
const Sequelize = require('sequelize')
const sequelize = require('../assets/utils/database')
module.exports = sequelize.define('user', {
name: {
type: Sequelize.STRING,
allowNull: false
},
email: {
type: Sequelize.STRING,
allowNull: false
},
password: {
type: Sequelize.STRING,
allowNull: false
},
biography: {
type: Sequelize.TEXT,
defaultValue: ''
},
logo: {
type: Sequelize.STRING,
defaultValue: '/images/users/default.png'
},
isConfirmed: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
isPublicEmail: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
isAdmin: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
tempToken: {
type: Sequelize.TEXT,
allowNull: true
},
tempExpirationToken: {
type: Sequelize.DATE,
allowNull: true
}
})

9753
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
api/package.json Normal file

@ -0,0 +1,46 @@
{
"name": "api",
"version": "2.3.0",
"description": "Backend REST API for FunctionProject",
"standard": {
"files": [
"./**/*.js"
],
"envs": [
"node"
]
},
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"lint": "standard | snazzy"
},
"dependencies": {
"axios": "^0.21.1",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-fileupload": "^1.2.1",
"express-http-to-https": "^1.1.4",
"express-rate-limit": "^5.2.6",
"express-validator": "^6.10.0",
"helmet": "^4.4.1",
"jsdom": "^16.5.3",
"jsonwebtoken": "^8.5.1",
"moment": "^2.29.1",
"morgan": "^1.10.0",
"ms": "^2.1.3",
"mysql2": "^2.2.5",
"nodemailer": "^6.5.0",
"sequelize": "^6.6.2",
"smart-request-balancer": "^2.1.1",
"uuid": "^8.3.2",
"validator": "^13.5.2"
},
"devDependencies": {
"nodemon": "^2.0.7",
"snazzy": "^9.0.0",
"standard": "^16.0.3"
}
}

223
api/routes/admin.js Normal file

@ -0,0 +1,223 @@
const { Router } = require('express')
const fileUpload = require('express-fileupload')
const { body } = require('express-validator')
const adminController = require('../controllers/admin')
const Functions = require('../models/functions')
const Categories = require('../models/categories')
const AdminRouter = Router()
AdminRouter.route('/functions')
// Récupère les fonctions
.get(adminController.getFunctions)
// Permet de créé une fonction
.post(
fileUpload({
useTempFiles: true,
safeFileNames: true,
preserveExtension: Number,
limits: { fileSize: 5 * 1024 * 1024 }, // 5mb,
parseNested: true
}),
[
body('title')
.not()
.isEmpty()
.withMessage('La fonction doit avoir un titre.')
.isLength({ max: 100 })
.withMessage('Le titre est trop long.')
.custom(title => {
if (title === 'undefined') {
return Promise.reject(new Error('La fonction doit avoir un titre.'))
}
return true
}),
body('slug')
.not()
.isEmpty()
.withMessage('La fonction doit avoir un slug.')
.isLength({ max: 100 })
.withMessage('Le slug est trop long.')
.custom(slug => {
if (slug === 'undefined') {
return Promise.reject(new Error('La fonction doit avoir un slug.'))
}
return true
})
.custom(async slug => {
try {
const FunctionSlug = await Functions.findOne({ where: { slug } })
if (FunctionSlug) {
return Promise.reject(new Error('Le slug existe déjà...'))
}
} catch (error) {
console.log(error)
}
return true
}),
body('description')
.not()
.isEmpty()
.withMessage('La fonction doit avoir une description.')
.isLength({ max: 255, min: 1 })
.withMessage('La description est trop longue.')
.custom(description => {
if (description === 'undefined') {
return Promise.reject(
new Error('La fonction doit avoir une description.')
)
}
return true
}),
body('categorieId')
.not()
.isEmpty()
.withMessage('La fonction doit avoir une catégorie.')
.custom(async categorieId => {
try {
const categorieFound = await Categories.findOne({
where: { id: parseInt(categorieId) }
})
if (!categorieFound) {
return Promise.reject(new Error("La catégorie n'existe pas!"))
}
} catch (error) {
console.log(error)
}
return true
}),
body('type').custom(type => {
if (!(type === 'article' || type === 'form' || type === 'page')) {
return Promise.reject(
new Error(
'Le type de la fonction peut être : article, form ou page.'
)
)
}
return true
})
],
adminController.postFunction
)
AdminRouter.route('/functions/:slug')
// Récupère les informations d'une fonction
.get(adminController.getFunctionBySlug)
AdminRouter.route('/functions/:id')
// Modifie information basique d'une fonction
.put(
fileUpload({
useTempFiles: true,
safeFileNames: true,
preserveExtension: Number,
limits: { fileSize: 5 * 1024 * 1024 }, // 5mb,
parseNested: true
}),
[
body('title')
.not()
.isEmpty()
.withMessage('La fonction doit avoir un titre.')
.isLength({ max: 100 })
.withMessage('Le titre est trop long.')
.custom(title => {
if (title === 'undefined') {
return Promise.reject(new Error('La fonction doit avoir un titre.'))
}
return true
}),
body('slug')
.not()
.isEmpty()
.withMessage('La fonction doit avoir un slug.')
.isLength({ max: 100 })
.withMessage('Le slug est trop long.')
.custom(slug => {
if (slug === 'undefined') {
return Promise.reject(new Error('La fonction doit avoir un slug.'))
}
return true
}),
body('description')
.not()
.isEmpty()
.withMessage('La fonction doit avoir une description.')
.isLength({ max: 255, min: 1 })
.withMessage('La description est trop longue.')
.custom(description => {
if (description === 'undefined') {
return Promise.reject(
new Error('La fonction doit avoir une description.')
)
}
return true
}),
body('categorieId')
.not()
.isEmpty()
.withMessage('La fonction doit avoir une catégorie.')
.custom(async categorieId => {
try {
const categorieFound = await Categories.findOne({
where: { id: parseInt(categorieId) }
})
if (!categorieFound) {
return Promise.reject(new Error("La catégorie n'existe pas!"))
}
} catch (error) {
console.log(error)
}
return true
}),
body('type').custom(type => {
if (!(type === 'article' || type === 'form' || type === 'page')) {
return Promise.reject(
new Error(
'Le type de la fonction peut être : article, form ou page.'
)
)
}
return true
})
],
adminController.putFunction
)
// Supprime une fonction avec son id
.delete(adminController.deleteFunction)
AdminRouter.route('/functions/article/:id')
.put(adminController.putFunctionArticle)
AdminRouter.route('/functions/form/:id')
.put(adminController.putFunctionForm)
AdminRouter.route('/categories')
// Crée une catégorie
.post(adminController.postCategory)
AdminRouter.route('/categories/:id')
// Modifier une catégorie avec son id
.put(adminController.putCategory)
// Supprime une catégorie avec son id
.delete(adminController.deleteCategory)
AdminRouter.route('/quotes')
// Récupère les citations pas encore validées
.get(adminController.getQuotes)
AdminRouter.route('/quotes/:id')
// Valide ou supprime une citation
.put(adminController.putQuote)
module.exports = AdminRouter

11
api/routes/categories.js Normal file

@ -0,0 +1,11 @@
const { Router } = require('express')
const categoriesController = require('../controllers/categories')
const CategoriesRouter = Router()
CategoriesRouter.route('/')
// Récupère les catégories
.get(categoriesController.getCategories)
module.exports = CategoriesRouter

23
api/routes/comments.js Normal file

@ -0,0 +1,23 @@
const { Router } = require('express')
const commentsController = require('../controllers/comments')
const isAuth = require('../middlewares/isAuth')
const CommentsRouter = Router()
CommentsRouter.route('/:commentId')
// Modifier un commentaire
.put(isAuth, commentsController.putCommentsById)
// Supprime un commentaire
.delete(isAuth, commentsController.deleteCommentById)
CommentsRouter.route('/:functionId')
// Récupère les commentaires
.get(commentsController.getCommentsByFunctionId)
// Permet à un utilisateur de poster un commentaire sur une fonction
.post(isAuth, commentsController.postCommentsByFunctionId)
module.exports = CommentsRouter

18
api/routes/favorites.js Normal file

@ -0,0 +1,18 @@
const { Router } = require('express')
const favoritesController = require('../controllers/favorites')
const isAuth = require('../middlewares/isAuth')
const FavoritesRouter = Router()
FavoritesRouter.route('/:functionId')
// Récupère si une fonction est en favoris (d'un utilisateur)
.get(isAuth, favoritesController.getFavoriteByFunctionId)
// Permet à un utilisateur d'ajouter une fonction aux favoris
.post(isAuth, favoritesController.postFavoriteByFunctionId)
// Supprime une fonction des favoris d'un utilisateur
.delete(isAuth, favoritesController.deleteFavoriteByFunctionId)
module.exports = FavoritesRouter

19
api/routes/functions.js Normal file

@ -0,0 +1,19 @@
const { Router } = require('express')
const functionsController = require('../controllers/functions')
const FunctionsRouter = Router()
FunctionsRouter.route('/')
// Récupère les fonctions
.get(functionsController.getFunctions)
FunctionsRouter.route('/:slug')
// Récupère les informations de la fonction par son slug
.get(functionsController.getFunctionBySlug)
// Exécute la fonction demandée en paramètre
.post(functionsController.executeFunctionBySlug)
module.exports = FunctionsRouter

@ -0,0 +1,23 @@
const { Router } = require('express')
const linksShortenerController = require('../controllers/links_shortener')
const isAuth = require('../middlewares/isAuth')
const LinksShortenerRouter = Router()
LinksShortenerRouter.route('/')
// Récupère les liens d'un utilisateur
.get(isAuth, linksShortenerController.getLinks)
// Ajouter un lien à raccourcir d'un utilisateur
.post(isAuth, linksShortenerController.postLink)
LinksShortenerRouter.route('/:id')
// Permet de modifier le lien raccourci d'un utilisateur
.put(isAuth, linksShortenerController.putLink)
// Supprimer un lien d'un utilisateur
.delete(isAuth, linksShortenerController.deleteLink)
module.exports = LinksShortenerRouter

15
api/routes/quotes.js Normal file

@ -0,0 +1,15 @@
const { Router } = require('express')
const quotesController = require('../controllers/quotes')
const isAuth = require('../middlewares/isAuth')
const QuotesRouter = Router()
QuotesRouter.route('/')
// Récupère les citations
.get(quotesController.getQuotes)
// Proposer une citation
.post(isAuth, quotesController.postQuote)
module.exports = QuotesRouter

23
api/routes/tasks.js Normal file

@ -0,0 +1,23 @@
const { Router } = require('express')
const tasksController = require('../controllers/tasks')
const isAuth = require('../middlewares/isAuth')
const TasksRouter = Router()
TasksRouter.route('/')
// Récupère les tâches à faire d'un user
.get(isAuth, tasksController.getTasks)
// Poster une nouvelle tâche à faire
.post(isAuth, tasksController.postTask)
TasksRouter.route('/:id')
// Permet de mettre une tâche à faire en isCompleted ou !isCompleted
.put(isAuth, tasksController.putTask)
// Supprimer une tâche à faire
.delete(isAuth, tasksController.deleteTask)
module.exports = TasksRouter

167
api/routes/users.js Normal file

@ -0,0 +1,167 @@
const { Router } = require('express')
const { body } = require('express-validator')
const fileUpload = require('express-fileupload')
const usersController = require('../controllers/users')
const { requiredFields } = require('../assets/config/errors')
const Users = require('../models/users')
const isAuth = require('../middlewares/isAuth')
const UsersRouter = Router()
UsersRouter.route('/')
// Récupère les utilisateurs
.get(usersController.getUsers)
// Permet de modifier son profil
.put(
isAuth,
fileUpload({
useTempFiles: true,
safeFileNames: true,
preserveExtension: Number,
limits: { fileSize: 5 * 1024 * 1024 }, // 5mb,
parseNested: true
}),
[
body('email')
.isEmail()
.withMessage('Veuillez rentré une adresse mail valide.')
.custom(async email => {
try {
const user = await Users.findOne({ where: { email } })
if (user && user.email !== email) {
return Promise.reject(new Error("L'adresse email existe déjà..."))
}
} catch (error) {
return console.log(error)
}
return true
})
.normalizeEmail(),
body('name')
.trim()
.not()
.isEmpty()
.withMessage('Vous devez avoir un nom (ou pseudo).')
.isAlphanumeric()
.withMessage(
'Votre nom ne peut contenir que des lettres ou/et des nombres.'
)
.isLength({ max: 30 })
.withMessage('Votre nom est trop long')
.custom(async name => {
try {
const user = await Users.findOne({ where: { name } })
if (user && user.name !== name) {
return Promise.reject(new Error('Le nom existe déjà...'))
}
} catch (error) {
console.log(error)
}
return true
}),
body('isPublicEmail')
.isBoolean()
.withMessage(
"L'adresse email peut être public ou privé, rien d'autre."
),
body('biography')
.trim()
.escape()
],
usersController.putUser
)
// Permet de se connecter
UsersRouter.post(
'/login',
[
body('email')
.not()
.isEmpty()
.withMessage(requiredFields.message),
body('password')
.not()
.isEmpty()
.withMessage(requiredFields.message)
],
usersController.login
)
// Récupère les informations public d'un profil
UsersRouter.get('/:name', usersController.getUserInfo)
// Permet de s'inscrire
UsersRouter.post(
'/register',
[
body('email')
.isEmail()
.withMessage('Veuillez rentré une adresse mail valide.')
.custom(async email => {
try {
const user = await Users.findOne({ where: { email } })
if (user) {
return Promise.reject(new Error("L'adresse email existe déjà..."))
}
} catch (error) {
return console.log(error)
}
return true
}),
body('password')
.isLength({ min: 4 })
.withMessage('Votre mot de passe est trop court!'),
body('name')
.trim()
.not()
.isEmpty()
.withMessage('Vous devez avoir un nom (ou pseudo).')
.isAlphanumeric()
.withMessage(
'Votre nom ne peut contenir que des lettres ou/et des nombres.'
)
.isLength({ max: 30 })
.withMessage('Votre nom est trop long')
.custom(async name => {
try {
const user = await Users.findOne({ where: { name } })
if (user) {
return Promise.reject(new Error('Le nom existe déjà...'))
}
} catch (error) {
console.log(error)
}
return true
})
],
usersController.register
)
// Confirme l'inscription
UsersRouter.get('/confirm-email/:tempToken', usersController.confirmEmail)
UsersRouter.route('/reset-password')
// Demande une réinitialisation du mot de passe
.post(
[
body('email')
.isEmail()
.withMessage('Veuillez rentré une adresse mail valide.')
],
usersController.resetPassword
)
// Nouveau mot de passe
.put(
[
body('password')
.isLength({ min: 4 })
.withMessage('Votre mot de passe est trop court!')
],
usersController.newPassword
)
module.exports = UsersRouter

@ -1,88 +0,0 @@
main {
transition: opacity 400ms ease-out;
}
.animate-down {
opacity: 0;
visibility: hidden;
-webkit-transform: translate(0, -100px);
-moz-transform: translate(0, -100px);
transform: translate(0, -100px);
}
.animate-down.animated {
opacity: 1;
visibility: visible;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translate(0, 0);
-moz-transform: translate(0, 0);
transform: translate(0, 0);
-webkit-transition: all 400ms ease-out 100ms;
-moz-transition: all 400ms ease-out 100ms;
transition: all 400ms ease-out 100ms;
transition: 5s;
}
.animate-up {
opacity: 0;
visibility: hidden;
-webkit-transform: translate(0, 100px);
-moz-transform: translate(0, 100px);
transform: translate(0, 100px);
}
.animate-up.animated {
opacity: 1;
visibility: visible;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translate(0, 0);
-moz-transform: translate(0, 0);
transform: translate(0, 0);
-webkit-transition: -webkit-transform 400ms ease-out 100ms, opacity 400ms ease-out 100ms;
-moz-transition: -moz-transform 400ms ease-out 100ms, opacity 400ms ease-out 100ms;
transition: transform 400ms ease-out 100ms, opacity 400ms ease-out 100ms;
}
.animate-right {
opacity: 0;
visibility: hidden;
transform: translate(-100px, 0);
}
.animate-right.animated {
opacity: 1;
visibility: visible;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translate(0, 0);
-moz-transform: translate(0, 0);
transform: translate(0, 0);
-webkit-transition: -webkit-transform 400ms ease-out 100ms, opacity 400ms ease-out 100ms;
-moz-transition: -moz-transform 400ms ease-out 100ms, opacity 400ms ease-out 100ms;
transition: transform 400ms ease-out 100ms, opacity 400ms ease-out 100ms;
}
.animate-left {
opacity: 0;
visibility: hidden;
transform: translate(100px, 0);
}
.animate-left.animated {
opacity: 1;
visibility: visible;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
backface-visibility: hidden;
-webkit-transform: translate(0, 0);
-moz-transform: translate(0, 0);
transform: translate(0, 0);
-webkit-transition: -webkit-transform 400ms ease-out 100ms, opacity 400ms ease-out 100ms;
-moz-transition: -moz-transform 400ms ease-out 100ms, opacity 400ms ease-out 100ms;
transition: transform 400ms ease-out 100ms, opacity 400ms ease-out 100ms;
}

@ -1,752 +0,0 @@
/*!
* Datepicker for Bootstrap v1.4.1 (https://github.com/eternicode/bootstrap-datepicker)
*
* Copyright 2012 Stefan Petre
* Improvements by Andrew Rowls
* Licensed under the Apache License v2.0 (http://www.apache.org/licenses/LICENSE-2.0)
*/
.datepicker {
padding: 4px;
border-radius: 4px;
direction: ltr;
}
.datepicker-inline {
width: 220px;
}
.datepicker.datepicker-rtl {
direction: rtl;
}
.datepicker.datepicker-rtl table tr td span {
float: right;
}
.datepicker-dropdown {
top: 0;
left: 0;
}
.datepicker-dropdown:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-top: 0;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
}
.datepicker-dropdown:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
border-top: 0;
position: absolute;
}
.datepicker-dropdown.datepicker-orient-left:before {
left: 6px;
}
.datepicker-dropdown.datepicker-orient-left:after {
left: 7px;
}
.datepicker-dropdown.datepicker-orient-right:before {
right: 6px;
}
.datepicker-dropdown.datepicker-orient-right:after {
right: 7px;
}
.datepicker-dropdown.datepicker-orient-top:before {
top: -7px;
}
.datepicker-dropdown.datepicker-orient-top:after {
top: -6px;
}
.datepicker-dropdown.datepicker-orient-bottom:before {
bottom: -7px;
border-bottom: 0;
border-top: 7px solid #999;
}
.datepicker-dropdown.datepicker-orient-bottom:after {
bottom: -6px;
border-bottom: 0;
border-top: 6px solid #fff;
}
.datepicker > div {
display: none;
}
.datepicker.days .datepicker-days,
.datepicker.months .datepicker-months,
.datepicker.years .datepicker-years {
display: block;
}
.datepicker table {
margin: 0;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.datepicker table tr td,
.datepicker table tr th {
text-align: center;
width: 30px;
height: 30px;
border-radius: 4px;
border: none;
}
.table-striped .datepicker table tr td,
.table-striped .datepicker table tr th {
background-color: transparent;
}
.datepicker table tr td.day:hover,
.datepicker table tr td.day.focused {
background: #eeeeee;
cursor: pointer;
}
.datepicker table tr td.old,
.datepicker table tr td.new {
color: #999999;
}
.datepicker table tr td.disabled,
.datepicker table tr td.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td.today,
.datepicker table tr td.today:hover,
.datepicker table tr td.today.disabled,
.datepicker table tr td.today.disabled:hover {
color: #000000;
background-color: #ffdb99;
border-color: #ffb733;
}
.datepicker table tr td.today:hover,
.datepicker table tr td.today:hover:hover,
.datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today.disabled:hover:hover,
.datepicker table tr td.today:focus,
.datepicker table tr td.today:hover:focus,
.datepicker table tr td.today.disabled:focus,
.datepicker table tr td.today.disabled:hover:focus,
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.today,
.open .dropdown-toggle.datepicker table tr td.today:hover,
.open .dropdown-toggle.datepicker table tr td.today.disabled,
.open .dropdown-toggle.datepicker table tr td.today.disabled:hover {
color: #000000;
background-color: #ffcd70;
border-color: #f59e00;
}
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.today,
.open .dropdown-toggle.datepicker table tr td.today:hover,
.open .dropdown-toggle.datepicker table tr td.today.disabled,
.open .dropdown-toggle.datepicker table tr td.today.disabled:hover {
background-image: none;
}
.datepicker table tr td.today.disabled,
.datepicker table tr td.today:hover.disabled,
.datepicker table tr td.today.disabled.disabled,
.datepicker table tr td.today.disabled:hover.disabled,
.datepicker table tr td.today[disabled],
.datepicker table tr td.today:hover[disabled],
.datepicker table tr td.today.disabled[disabled],
.datepicker table tr td.today.disabled:hover[disabled],
fieldset[disabled] .datepicker table tr td.today,
fieldset[disabled] .datepicker table tr td.today:hover,
fieldset[disabled] .datepicker table tr td.today.disabled,
fieldset[disabled] .datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today:hover.disabled:hover,
.datepicker table tr td.today.disabled.disabled:hover,
.datepicker table tr td.today.disabled:hover.disabled:hover,
.datepicker table tr td.today[disabled]:hover,
.datepicker table tr td.today:hover[disabled]:hover,
.datepicker table tr td.today.disabled[disabled]:hover,
.datepicker table tr td.today.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td.today:hover,
fieldset[disabled] .datepicker table tr td.today:hover:hover,
fieldset[disabled] .datepicker table tr td.today.disabled:hover,
fieldset[disabled] .datepicker table tr td.today.disabled:hover:hover,
.datepicker table tr td.today.disabled:focus,
.datepicker table tr td.today:hover.disabled:focus,
.datepicker table tr td.today.disabled.disabled:focus,
.datepicker table tr td.today.disabled:hover.disabled:focus,
.datepicker table tr td.today[disabled]:focus,
.datepicker table tr td.today:hover[disabled]:focus,
.datepicker table tr td.today.disabled[disabled]:focus,
.datepicker table tr td.today.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td.today:focus,
fieldset[disabled] .datepicker table tr td.today:hover:focus,
fieldset[disabled] .datepicker table tr td.today.disabled:focus,
fieldset[disabled] .datepicker table tr td.today.disabled:hover:focus,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today:hover.disabled:active,
.datepicker table tr td.today.disabled.disabled:active,
.datepicker table tr td.today.disabled:hover.disabled:active,
.datepicker table tr td.today[disabled]:active,
.datepicker table tr td.today:hover[disabled]:active,
.datepicker table tr td.today.disabled[disabled]:active,
.datepicker table tr td.today.disabled:hover[disabled]:active,
fieldset[disabled] .datepicker table tr td.today:active,
fieldset[disabled] .datepicker table tr td.today:hover:active,
fieldset[disabled] .datepicker table tr td.today.disabled:active,
fieldset[disabled] .datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today:hover.disabled.active,
.datepicker table tr td.today.disabled.disabled.active,
.datepicker table tr td.today.disabled:hover.disabled.active,
.datepicker table tr td.today[disabled].active,
.datepicker table tr td.today:hover[disabled].active,
.datepicker table tr td.today.disabled[disabled].active,
.datepicker table tr td.today.disabled:hover[disabled].active,
fieldset[disabled] .datepicker table tr td.today.active,
fieldset[disabled] .datepicker table tr td.today:hover.active,
fieldset[disabled] .datepicker table tr td.today.disabled.active,
fieldset[disabled] .datepicker table tr td.today.disabled:hover.active {
background-color: #ffdb99;
border-color: #ffb733;
}
.datepicker table tr td.today:hover:hover {
color: #000;
}
.datepicker table tr td.today.active:hover {
color: #fff;
}
.datepicker table tr td.range,
.datepicker table tr td.range:hover,
.datepicker table tr td.range.disabled,
.datepicker table tr td.range.disabled:hover {
background: #eeeeee;
border-radius: 0;
}
.datepicker table tr td.range.today,
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today.disabled:hover {
color: #000000;
background-color: #f7ca77;
border-color: #f1a417;
border-radius: 0;
}
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today:hover:hover,
.datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today.disabled:hover:hover,
.datepicker table tr td.range.today:focus,
.datepicker table tr td.range.today:hover:focus,
.datepicker table tr td.range.today.disabled:focus,
.datepicker table tr td.range.today.disabled:hover:focus,
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.range.today,
.open .dropdown-toggle.datepicker table tr td.range.today:hover,
.open .dropdown-toggle.datepicker table tr td.range.today.disabled,
.open .dropdown-toggle.datepicker table tr td.range.today.disabled:hover {
color: #000000;
background-color: #f4bb51;
border-color: #bf800c;
}
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.range.today,
.open .dropdown-toggle.datepicker table tr td.range.today:hover,
.open .dropdown-toggle.datepicker table tr td.range.today.disabled,
.open .dropdown-toggle.datepicker table tr td.range.today.disabled:hover {
background-image: none;
}
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today:hover.disabled,
.datepicker table tr td.range.today.disabled.disabled,
.datepicker table tr td.range.today.disabled:hover.disabled,
.datepicker table tr td.range.today[disabled],
.datepicker table tr td.range.today:hover[disabled],
.datepicker table tr td.range.today.disabled[disabled],
.datepicker table tr td.range.today.disabled:hover[disabled],
fieldset[disabled] .datepicker table tr td.range.today,
fieldset[disabled] .datepicker table tr td.range.today:hover,
fieldset[disabled] .datepicker table tr td.range.today.disabled,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today:hover.disabled:hover,
.datepicker table tr td.range.today.disabled.disabled:hover,
.datepicker table tr td.range.today.disabled:hover.disabled:hover,
.datepicker table tr td.range.today[disabled]:hover,
.datepicker table tr td.range.today:hover[disabled]:hover,
.datepicker table tr td.range.today.disabled[disabled]:hover,
.datepicker table tr td.range.today.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td.range.today:hover,
fieldset[disabled] .datepicker table tr td.range.today:hover:hover,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:hover,
.datepicker table tr td.range.today.disabled:focus,
.datepicker table tr td.range.today:hover.disabled:focus,
.datepicker table tr td.range.today.disabled.disabled:focus,
.datepicker table tr td.range.today.disabled:hover.disabled:focus,
.datepicker table tr td.range.today[disabled]:focus,
.datepicker table tr td.range.today:hover[disabled]:focus,
.datepicker table tr td.range.today.disabled[disabled]:focus,
.datepicker table tr td.range.today.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td.range.today:focus,
fieldset[disabled] .datepicker table tr td.range.today:hover:focus,
fieldset[disabled] .datepicker table tr td.range.today.disabled:focus,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:focus,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today:hover.disabled:active,
.datepicker table tr td.range.today.disabled.disabled:active,
.datepicker table tr td.range.today.disabled:hover.disabled:active,
.datepicker table tr td.range.today[disabled]:active,
.datepicker table tr td.range.today:hover[disabled]:active,
.datepicker table tr td.range.today.disabled[disabled]:active,
.datepicker table tr td.range.today.disabled:hover[disabled]:active,
fieldset[disabled] .datepicker table tr td.range.today:active,
fieldset[disabled] .datepicker table tr td.range.today:hover:active,
fieldset[disabled] .datepicker table tr td.range.today.disabled:active,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today:hover.disabled.active,
.datepicker table tr td.range.today.disabled.disabled.active,
.datepicker table tr td.range.today.disabled:hover.disabled.active,
.datepicker table tr td.range.today[disabled].active,
.datepicker table tr td.range.today:hover[disabled].active,
.datepicker table tr td.range.today.disabled[disabled].active,
.datepicker table tr td.range.today.disabled:hover[disabled].active,
fieldset[disabled] .datepicker table tr td.range.today.active,
fieldset[disabled] .datepicker table tr td.range.today:hover.active,
fieldset[disabled] .datepicker table tr td.range.today.disabled.active,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover.active {
background-color: #f7ca77;
border-color: #f1a417;
}
.datepicker table tr td.selected,
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected.disabled:hover {
color: #ffffff;
background-color: #999999;
border-color: #555555;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected:hover:hover,
.datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected.disabled:hover:hover,
.datepicker table tr td.selected:focus,
.datepicker table tr td.selected:hover:focus,
.datepicker table tr td.selected.disabled:focus,
.datepicker table tr td.selected.disabled:hover:focus,
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.selected,
.open .dropdown-toggle.datepicker table tr td.selected:hover,
.open .dropdown-toggle.datepicker table tr td.selected.disabled,
.open .dropdown-toggle.datepicker table tr td.selected.disabled:hover {
color: #ffffff;
background-color: #858585;
border-color: #373737;
}
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.selected,
.open .dropdown-toggle.datepicker table tr td.selected:hover,
.open .dropdown-toggle.datepicker table tr td.selected.disabled,
.open .dropdown-toggle.datepicker table tr td.selected.disabled:hover {
background-image: none;
}
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected:hover.disabled,
.datepicker table tr td.selected.disabled.disabled,
.datepicker table tr td.selected.disabled:hover.disabled,
.datepicker table tr td.selected[disabled],
.datepicker table tr td.selected:hover[disabled],
.datepicker table tr td.selected.disabled[disabled],
.datepicker table tr td.selected.disabled:hover[disabled],
fieldset[disabled] .datepicker table tr td.selected,
fieldset[disabled] .datepicker table tr td.selected:hover,
fieldset[disabled] .datepicker table tr td.selected.disabled,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected:hover.disabled:hover,
.datepicker table tr td.selected.disabled.disabled:hover,
.datepicker table tr td.selected.disabled:hover.disabled:hover,
.datepicker table tr td.selected[disabled]:hover,
.datepicker table tr td.selected:hover[disabled]:hover,
.datepicker table tr td.selected.disabled[disabled]:hover,
.datepicker table tr td.selected.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td.selected:hover,
fieldset[disabled] .datepicker table tr td.selected:hover:hover,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:hover,
.datepicker table tr td.selected.disabled:focus,
.datepicker table tr td.selected:hover.disabled:focus,
.datepicker table tr td.selected.disabled.disabled:focus,
.datepicker table tr td.selected.disabled:hover.disabled:focus,
.datepicker table tr td.selected[disabled]:focus,
.datepicker table tr td.selected:hover[disabled]:focus,
.datepicker table tr td.selected.disabled[disabled]:focus,
.datepicker table tr td.selected.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td.selected:focus,
fieldset[disabled] .datepicker table tr td.selected:hover:focus,
fieldset[disabled] .datepicker table tr td.selected.disabled:focus,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:focus,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected:hover.disabled:active,
.datepicker table tr td.selected.disabled.disabled:active,
.datepicker table tr td.selected.disabled:hover.disabled:active,
.datepicker table tr td.selected[disabled]:active,
.datepicker table tr td.selected:hover[disabled]:active,
.datepicker table tr td.selected.disabled[disabled]:active,
.datepicker table tr td.selected.disabled:hover[disabled]:active,
fieldset[disabled] .datepicker table tr td.selected:active,
fieldset[disabled] .datepicker table tr td.selected:hover:active,
fieldset[disabled] .datepicker table tr td.selected.disabled:active,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected:hover.disabled.active,
.datepicker table tr td.selected.disabled.disabled.active,
.datepicker table tr td.selected.disabled:hover.disabled.active,
.datepicker table tr td.selected[disabled].active,
.datepicker table tr td.selected:hover[disabled].active,
.datepicker table tr td.selected.disabled[disabled].active,
.datepicker table tr td.selected.disabled:hover[disabled].active,
fieldset[disabled] .datepicker table tr td.selected.active,
fieldset[disabled] .datepicker table tr td.selected:hover.active,
fieldset[disabled] .datepicker table tr td.selected.disabled.active,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover.active {
background-color: #999999;
border-color: #555555;
}
.datepicker table tr td.active,
.datepicker table tr td.active:hover,
.datepicker table tr td.active.disabled,
.datepicker table tr td.active.disabled:hover {
color: #ffffff;
background-color: #428bca;
border-color: #357ebd;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.active:hover,
.datepicker table tr td.active:hover:hover,
.datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active.disabled:hover:hover,
.datepicker table tr td.active:focus,
.datepicker table tr td.active:hover:focus,
.datepicker table tr td.active.disabled:focus,
.datepicker table tr td.active.disabled:hover:focus,
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.active,
.open .dropdown-toggle.datepicker table tr td.active:hover,
.open .dropdown-toggle.datepicker table tr td.active.disabled,
.open .dropdown-toggle.datepicker table tr td.active.disabled:hover {
color: #ffffff;
background-color: #3276b1;
border-color: #285e8e;
}
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.active,
.open .dropdown-toggle.datepicker table tr td.active:hover,
.open .dropdown-toggle.datepicker table tr td.active.disabled,
.open .dropdown-toggle.datepicker table tr td.active.disabled:hover {
background-image: none;
}
.datepicker table tr td.active.disabled,
.datepicker table tr td.active:hover.disabled,
.datepicker table tr td.active.disabled.disabled,
.datepicker table tr td.active.disabled:hover.disabled,
.datepicker table tr td.active[disabled],
.datepicker table tr td.active:hover[disabled],
.datepicker table tr td.active.disabled[disabled],
.datepicker table tr td.active.disabled:hover[disabled],
fieldset[disabled] .datepicker table tr td.active,
fieldset[disabled] .datepicker table tr td.active:hover,
fieldset[disabled] .datepicker table tr td.active.disabled,
fieldset[disabled] .datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active:hover.disabled:hover,
.datepicker table tr td.active.disabled.disabled:hover,
.datepicker table tr td.active.disabled:hover.disabled:hover,
.datepicker table tr td.active[disabled]:hover,
.datepicker table tr td.active:hover[disabled]:hover,
.datepicker table tr td.active.disabled[disabled]:hover,
.datepicker table tr td.active.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td.active:hover,
fieldset[disabled] .datepicker table tr td.active:hover:hover,
fieldset[disabled] .datepicker table tr td.active.disabled:hover,
fieldset[disabled] .datepicker table tr td.active.disabled:hover:hover,
.datepicker table tr td.active.disabled:focus,
.datepicker table tr td.active:hover.disabled:focus,
.datepicker table tr td.active.disabled.disabled:focus,
.datepicker table tr td.active.disabled:hover.disabled:focus,
.datepicker table tr td.active[disabled]:focus,
.datepicker table tr td.active:hover[disabled]:focus,
.datepicker table tr td.active.disabled[disabled]:focus,
.datepicker table tr td.active.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td.active:focus,
fieldset[disabled] .datepicker table tr td.active:hover:focus,
fieldset[disabled] .datepicker table tr td.active.disabled:focus,
fieldset[disabled] .datepicker table tr td.active.disabled:hover:focus,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active:hover.disabled:active,
.datepicker table tr td.active.disabled.disabled:active,
.datepicker table tr td.active.disabled:hover.disabled:active,
.datepicker table tr td.active[disabled]:active,
.datepicker table tr td.active:hover[disabled]:active,
.datepicker table tr td.active.disabled[disabled]:active,
.datepicker table tr td.active.disabled:hover[disabled]:active,
fieldset[disabled] .datepicker table tr td.active:active,
fieldset[disabled] .datepicker table tr td.active:hover:active,
fieldset[disabled] .datepicker table tr td.active.disabled:active,
fieldset[disabled] .datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active:hover.disabled.active,
.datepicker table tr td.active.disabled.disabled.active,
.datepicker table tr td.active.disabled:hover.disabled.active,
.datepicker table tr td.active[disabled].active,
.datepicker table tr td.active:hover[disabled].active,
.datepicker table tr td.active.disabled[disabled].active,
.datepicker table tr td.active.disabled:hover[disabled].active,
fieldset[disabled] .datepicker table tr td.active.active,
fieldset[disabled] .datepicker table tr td.active:hover.active,
fieldset[disabled] .datepicker table tr td.active.disabled.active,
fieldset[disabled] .datepicker table tr td.active.disabled:hover.active {
background-color: #428bca;
border-color: #357ebd;
}
.datepicker table tr td span {
display: block;
width: 23%;
height: 54px;
line-height: 54px;
float: left;
margin: 1%;
cursor: pointer;
border-radius: 4px;
}
.datepicker table tr td span:hover {
background: #eeeeee;
}
.datepicker table tr td span.disabled,
.datepicker table tr td span.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td span.active,
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active.disabled:hover {
color: #ffffff;
background-color: #428bca;
border-color: #357ebd;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active:hover:hover,
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active.disabled:hover:hover,
.datepicker table tr td span.active:focus,
.datepicker table tr td span.active:hover:focus,
.datepicker table tr td span.active.disabled:focus,
.datepicker table tr td span.active.disabled:hover:focus,
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td span.active,
.open .dropdown-toggle.datepicker table tr td span.active:hover,
.open .dropdown-toggle.datepicker table tr td span.active.disabled,
.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover {
color: #ffffff;
background-color: #3276b1;
border-color: #285e8e;
}
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td span.active,
.open .dropdown-toggle.datepicker table tr td span.active:hover,
.open .dropdown-toggle.datepicker table tr td span.active.disabled,
.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover {
background-image: none;
}
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active:hover.disabled,
.datepicker table tr td span.active.disabled.disabled,
.datepicker table tr td span.active.disabled:hover.disabled,
.datepicker table tr td span.active[disabled],
.datepicker table tr td span.active:hover[disabled],
.datepicker table tr td span.active.disabled[disabled],
.datepicker table tr td span.active.disabled:hover[disabled],
fieldset[disabled] .datepicker table tr td span.active,
fieldset[disabled] .datepicker table tr td span.active:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active:hover.disabled:hover,
.datepicker table tr td span.active.disabled.disabled:hover,
.datepicker table tr td span.active.disabled:hover.disabled:hover,
.datepicker table tr td span.active[disabled]:hover,
.datepicker table tr td span.active:hover[disabled]:hover,
.datepicker table tr td span.active.disabled[disabled]:hover,
.datepicker table tr td span.active.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td span.active:hover,
fieldset[disabled] .datepicker table tr td span.active:hover:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover,
.datepicker table tr td span.active.disabled:focus,
.datepicker table tr td span.active:hover.disabled:focus,
.datepicker table tr td span.active.disabled.disabled:focus,
.datepicker table tr td span.active.disabled:hover.disabled:focus,
.datepicker table tr td span.active[disabled]:focus,
.datepicker table tr td span.active:hover[disabled]:focus,
.datepicker table tr td span.active.disabled[disabled]:focus,
.datepicker table tr td span.active.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td span.active:focus,
fieldset[disabled] .datepicker table tr td span.active:hover:focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active:hover.disabled:active,
.datepicker table tr td span.active.disabled.disabled:active,
.datepicker table tr td span.active.disabled:hover.disabled:active,
.datepicker table tr td span.active[disabled]:active,
.datepicker table tr td span.active:hover[disabled]:active,
.datepicker table tr td span.active.disabled[disabled]:active,
.datepicker table tr td span.active.disabled:hover[disabled]:active,
fieldset[disabled] .datepicker table tr td span.active:active,
fieldset[disabled] .datepicker table tr td span.active:hover:active,
fieldset[disabled] .datepicker table tr td span.active.disabled:active,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active:hover.disabled.active,
.datepicker table tr td span.active.disabled.disabled.active,
.datepicker table tr td span.active.disabled:hover.disabled.active,
.datepicker table tr td span.active[disabled].active,
.datepicker table tr td span.active:hover[disabled].active,
.datepicker table tr td span.active.disabled[disabled].active,
.datepicker table tr td span.active.disabled:hover[disabled].active,
fieldset[disabled] .datepicker table tr td span.active.active,
fieldset[disabled] .datepicker table tr td span.active:hover.active,
fieldset[disabled] .datepicker table tr td span.active.disabled.active,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover.active {
background-color: #428bca;
border-color: #357ebd;
}
.datepicker table tr td span.old,
.datepicker table tr td span.new {
color: #999999;
}
.datepicker .datepicker-switch {
width: 145px;
}
.datepicker thead tr:first-child th,
.datepicker tfoot tr th {
cursor: pointer;
}
.datepicker thead tr:first-child th:hover,
.datepicker tfoot tr th:hover {
background: #eeeeee;
}
.datepicker .cw {
font-size: 10px;
width: 12px;
padding: 0 2px 0 5px;
vertical-align: middle;
}
.datepicker thead tr:first-child .cw {
cursor: default;
background-color: transparent;
}
.input-group.date .input-group-addon {
cursor: pointer;
}
.input-daterange {
width: 100%;
}
.input-daterange input {
text-align: center;
}
.input-daterange input:first-child {
border-radius: 3px 0 0 3px;
}
.input-daterange input:last-child {
border-radius: 0 3px 3px 0;
}
.input-daterange .input-group-addon {
width: auto;
min-width: 16px;
padding: 4px 5px;
font-weight: normal;
line-height: 1.42857143;
text-align: center;
text-shadow: 0 1px 0 #fff;
vertical-align: middle;
background-color: #eeeeee;
border: solid #cccccc;
border-width: 1px 0;
margin-left: -5px;
margin-right: -5px;
}

Some files were not shown because too many files have changed in this diff Show More