diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..66e334b --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +MONGO_URI=mongodb://mongodb:27017 diff --git a/.gitignore b/.gitignore index 6d1139f..6e7675a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ air.log # Environment variables .env .env.localgit -uploads \ No newline at end of file +uploads +app/back/.env diff --git a/README.md b/README.md index 7a25ab0..4bb307f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Ce projet, développé en Go, met en place un Content Delivery Network (CDN) afi Lancer l’application en mode développement avec hot-reload : ```bash -docker compose up app-dev +docker compose -f docker-compose.dev.yml up ``` - Accessible via [http://localhost:8080](http://localhost:8080) @@ -45,7 +45,7 @@ docker compose up app-dev Démarrer en mode production : ```bash -docker compose up app-prod +docker compose -f docker-compose.prod.yml up ``` - Optimisé pour un environnement de production diff --git a/app/front/package-lock.json b/app/front/package-lock.json index c8ceae2..0d4f0dd 100644 --- a/app/front/package-lock.json +++ b/app/front/package-lock.json @@ -8,6 +8,10 @@ "name": "cdn", "version": "0.0.0", "dependencies": { + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-slot": "^1.1.1", "@tailwindcss/postcss": "^4.0.3", "@tailwindcss/vite": "^4.0.3", @@ -921,6 +925,40 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1078,6 +1116,58 @@ "node": ">= 8" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.3.tgz", + "integrity": "sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", @@ -1093,11 +1183,297 @@ } } }, - "node_modules/@radix-ui/react-slot": { + "node_modules/@radix-ui/react-context": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", - "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", + "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", + "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, @@ -1111,6 +1487,107 @@ } } }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.32.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.1.tgz", @@ -1907,7 +2384,7 @@ "version": "19.0.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.0.tgz", "integrity": "sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -2239,6 +2716,17 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -2567,6 +3055,11 @@ "node": ">=0.10" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -3000,6 +3493,14 @@ "node": ">=6.9.0" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-tsconfig": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", @@ -3854,6 +4355,72 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4106,6 +4673,11 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/tsx": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", @@ -4662,6 +5234,47 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", diff --git a/app/front/package.json b/app/front/package.json index 2d0493b..c3bd230 100644 --- a/app/front/package.json +++ b/app/front/package.json @@ -10,6 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-slot": "^1.1.1", "@tailwindcss/postcss": "^4.0.3", "@tailwindcss/vite": "^4.0.3", diff --git a/app/front/src/app.tsx b/app/front/src/app.tsx new file mode 100644 index 0000000..2b6d8e1 --- /dev/null +++ b/app/front/src/app.tsx @@ -0,0 +1,36 @@ +import { createRouter, RouterProvider } from "@tanstack/react-router"; +import useAuth, { AuthProvider } from "./hooks/useAuth"; +import { routeTree } from "./routeTree.gen"; +import { StrictMode } from "react"; + +// Create a new router instance +const router = createRouter({ + routeTree, + context: { + auth: { + isAuth: false, + user: null, + accessToken: null, + }, + }, +}); + + +// Register the router instance for type safety +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +export function App(){ + const auth = useAuth(); + + return ( + + + + + + ) + } \ No newline at end of file diff --git a/app/front/src/components/Upload.tsx b/app/front/src/components/Upload.tsx index 3a788ff..be1ee98 100644 --- a/app/front/src/components/Upload.tsx +++ b/app/front/src/components/Upload.tsx @@ -1,4 +1,4 @@ -import { FileData } from '@/pages/Index'; +import { FileData } from '@/pages/drive'; import { Loader, Upload, X } from 'lucide-react'; import { useState } from 'react'; diff --git a/app/front/src/components/header.tsx b/app/front/src/components/header.tsx new file mode 100644 index 0000000..7215c37 --- /dev/null +++ b/app/front/src/components/header.tsx @@ -0,0 +1,262 @@ +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import useAuth from "@/hooks/useAuth"; +import { useNavigate, useParams, useRouter } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { Button } from "./ui/button"; +import { PlusIcon } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "./ui/dialog"; +import { Input } from "./ui/input"; + +interface FolderItem { + id: string; + name: string; +} + +export function SiteHeader() { + const navigate = useNavigate(); + const router = useRouter(); + const params = useParams({ + from: "/_auth/drive/$folderPath", + }); + + const folderPath = params?.folderPath ?? undefined; + const { logout, isAuth, user, accessToken } = useAuth(); + const [email, setEmail] = useState(null); + const [folderName, setFolderName] = useState(""); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isFileDialogOpen, setIsFileDialogOpen] = useState(false); + + useEffect(() => { + if (!isAuth) { + navigate({ to: "/login" }); + } else { + setEmail(user?.email ?? ""); + } + }, [isAuth, navigate, user?.email]); + + const handleLogout = async () => { + await logout(); + + navigate({ to: "/login" }); + }; + + const handleCreateFolder = async () => { + if (!folderName.trim()) return; + + let parentId = folderPath; + if (!parentId) { + try { + const foldersResponse = await fetch( + "http://localhost:8082/api/folders", + { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + } + ); + if (!foldersResponse.ok) { + throw new Error("Error recuperating folder"); + } + const folders = await foldersResponse.json(); + const rootFolder = folders.find( + (folder: { id: string; name: string }) => + folder.name.toLowerCase() === "root" + ); + if (!rootFolder) { + throw new Error("Root folder not found"); + } + parentId = rootFolder.name; + } catch (error) { + console.error(error); + return; + } + } + + try { + const response = await fetch("http://localhost:8082/api/folders", { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: folderName, + parent_id: parentId, + }), + }); + + if (!response.ok) { + throw new Error("Error creating folder"); + } + + router.invalidate(); + + setFolderName(""); + setIsDialogOpen(false); + } catch (error) { + console.error(error); + } + }; + + const handleNewFile = async (e: React.FormEvent) => { + e.preventDefault(); + + const target = e.target as typeof e.target & { + file: { files: FileList }; + }; + + const file = target.file.files?.[0]; + if (!file) { + throw Error('No file found') + } + + let currentfolderName = folderPath; + if (!currentfolderName) { + try { + const foldersResponse = await fetch( + "http://localhost:8082/api/folders", + { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + } + ); + + if (!foldersResponse.ok) { + throw new Error("Error recuperating file"); + } + + const folders: FolderItem[] = await foldersResponse.json(); + const rootFolder = folders.find( + (folder) => folder.name.toLowerCase() === "root" + ); + + if (!rootFolder) { + throw new Error("Root folder not found"); + } + + currentfolderName = rootFolder.name; + } catch (error) { + console.error(error); + return; + } + } + + const formData = new FormData(); + formData.append("file", file); + formData.append("folder_id", currentfolderName); + + + const res = await fetch("http://localhost:8082/api/files", { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + body: formData, + }); + + if (res.ok) { + setIsFileDialogOpen(false); + router.invalidate(); + } else { + throw new Error('Error when sending file') + } + }; + + return ( +
+
+ + + + + + + Create a new folder + + setFolderName(e.target.value)} + /> + + +
+ + +
+
+
+ + + + + + + Add a new file + + Please select a file to upload + + +
+ + +
+
+
+
+ + + + + {email ? email.slice(0, 2).toUpperCase() : "U"} + + + + +
+ {email ? email : "Utilisateur"} + +
+
+
+
+ ); +} diff --git a/app/front/src/components/ui/avatar.tsx b/app/front/src/components/ui/avatar.tsx new file mode 100644 index 0000000..991f56e --- /dev/null +++ b/app/front/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/app/front/src/components/ui/card.tsx b/app/front/src/components/ui/card.tsx new file mode 100644 index 0000000..f62edea --- /dev/null +++ b/app/front/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/app/front/src/components/ui/dialog.tsx b/app/front/src/components/ui/dialog.tsx new file mode 100644 index 0000000..c23630e --- /dev/null +++ b/app/front/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/app/front/src/components/ui/input.tsx b/app/front/src/components/ui/input.tsx new file mode 100644 index 0000000..68551b9 --- /dev/null +++ b/app/front/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/app/front/src/components/ui/label.tsx b/app/front/src/components/ui/label.tsx new file mode 100644 index 0000000..683faa7 --- /dev/null +++ b/app/front/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/app/front/src/components/ui/popover.tsx b/app/front/src/components/ui/popover.tsx new file mode 100644 index 0000000..bbba7e0 --- /dev/null +++ b/app/front/src/components/ui/popover.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/app/front/src/hooks/hooksTypes.ts b/app/front/src/hooks/hooksTypes.ts new file mode 100644 index 0000000..67288d3 --- /dev/null +++ b/app/front/src/hooks/hooksTypes.ts @@ -0,0 +1,16 @@ +export type User = { + id: string; + email: string; +}; + +export type AuthState = { + user: User | null; + accessToken: string | null; + isAuth: boolean; +}; + +export interface AuthFunctions { + login: (user: User, jwt: string) => Promise; + logout: () => Promise; + update: (user: Partial) => Promise; +} diff --git a/app/front/src/hooks/useAuth.tsx b/app/front/src/hooks/useAuth.tsx new file mode 100644 index 0000000..212d8fb --- /dev/null +++ b/app/front/src/hooks/useAuth.tsx @@ -0,0 +1,108 @@ +import * as React from "react"; +import { AuthState, AuthFunctions, User } from "./hooksTypes"; + +interface AuthContext extends AuthState, AuthFunctions {} + +const AuthContext = React.createContext(null); + +const USER_KEY = "goofycontentdeliverynetwork.auth.user"; +const USER_TOKEN = "goofycontentdeliverynetwork.auth.token"; + +function getStoredUser() { + const user = localStorage.getItem(USER_KEY); + + if (user) { + return JSON.parse(user); + } + + console.warn("no user found"); + + return null; +} + +function getStoredJwt() { + return localStorage.getItem(USER_TOKEN); +} + +function setStoredUser(user: User | null) { + if (user) { + localStorage.setItem(USER_KEY, JSON.stringify(user)); + } else { + localStorage.removeItem(USER_KEY); + } +} + +function updateStoredUser(user: Partial) { + const storedUser = getStoredUser(); + + if (storedUser) { + setStoredUser({ ...storedUser, ...user }); + } +} + +function setStoredJwt(jwt: string | null) { + if (jwt) { + localStorage.setItem(USER_TOKEN, jwt); + } else { + localStorage.removeItem(USER_TOKEN); + } +} + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = React.useState(getStoredUser()); + const [accessToken, setAccessToken] = React.useState( + getStoredJwt() + ); + + const isAuth = !!user; + + const logout = React.useCallback(async () => { + setStoredUser(null); + setStoredJwt(null); + setUser(null); + setAccessToken(null); + }, []); + + const login = React.useCallback(async (user: User, jwt: string) => { + setStoredUser(user); + setStoredJwt(jwt); + setUser(user); + setAccessToken(jwt); + }, []); + + const update = React.useCallback(async (user: Partial) => { + updateStoredUser(user); + setUser((prev) => { + if (!prev) { + return null; + } + + return { + ...prev, + ...user, + }; + }); + }, []); + + React.useEffect(() => { + setUser(getStoredUser()); + }, []); + + return ( + + {children} + + ); +} + +function useAuth() { + const context = React.useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +}; + +export default useAuth; diff --git a/app/front/src/main.tsx b/app/front/src/main.tsx index 6b6ab63..c266036 100644 --- a/app/front/src/main.tsx +++ b/app/front/src/main.tsx @@ -1,20 +1,10 @@ import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; -import { RouterProvider, createRouter } from "@tanstack/react-router"; import "./index.css"; // Import the generated route tree -import { routeTree } from "./routeTree.gen"; - -// Create a new router instance -const router = createRouter({ routeTree }); - -// Register the router instance for type safety -declare module "@tanstack/react-router" { - interface Register { - router: typeof router; - } -} +import { AuthProvider } from "./hooks/useAuth"; +import { App } from "./app"; // Render the app const rootElement = document.getElementById("root")!; @@ -22,7 +12,9 @@ if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( - + + + ); } diff --git a/app/front/src/pages/Index.tsx b/app/front/src/pages/drive.tsx similarity index 100% rename from app/front/src/pages/Index.tsx rename to app/front/src/pages/drive.tsx diff --git a/app/front/src/pages/login.tsx b/app/front/src/pages/login.tsx index c246fec..0e9f34f 100644 --- a/app/front/src/pages/login.tsx +++ b/app/front/src/pages/login.tsx @@ -1,8 +1,12 @@ import Input from "@/components/Input"; -import { Link } from "@tanstack/react-router"; +import useAuth from "@/hooks/useAuth"; +import { Link, useNavigate } from "@tanstack/react-router"; import React, { ChangeEvent, useState } from "react"; const Login: React.FC = () => { + const navigate = useNavigate(); + const { login } = useAuth(); + const [showPassword, setShowPassword] = useState(false); const [formData, setFormData] = useState({ email: '', @@ -17,10 +21,40 @@ const Login: React.FC = () => { })); }; - const handleSubmit = (e: ChangeEvent) => { - e.preventDefault(); - console.log('Form submitted:', formData); - }; + const handleSubmit = async (e: ChangeEvent) => { + e.preventDefault(); + + try { + const response = await fetch("http://localhost:8080/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + Email: formData.email, + Password: formData.password, + }), + }); + + if (!response.ok) { + throw new Error('Login failed') + } + + const data = await response.json(); + + // Vérifiez que la réponse contient bien un token + if (data.token) { + await login({ id: formData.email, email: formData.email }, data.token); + } else { + throw new Error('No token received') + } + + console.log('Login succesfully') + navigate({ to: "/drive" }); + } catch (error) { + throw new Error(`Error during login: ${error}`) + } + }; return (
diff --git a/app/front/src/pages/register.tsx b/app/front/src/pages/register.tsx index 91e5df2..1deae09 100644 --- a/app/front/src/pages/register.tsx +++ b/app/front/src/pages/register.tsx @@ -1,8 +1,9 @@ import Input from "@/components/Input"; -import { Link } from "@tanstack/react-router"; +import { Link, useNavigate } from "@tanstack/react-router"; import React, { ChangeEvent, useState } from "react"; const Register: React.FC = () => { + const navigate = useNavigate(); const [showPassword, setShowPassword] = useState(false); const [formData, setFormData] = useState({ firstName: '', @@ -20,9 +21,31 @@ const Register: React.FC = () => { })); }; - const handleSubmit = (e: ChangeEvent) => { + + const handleSubmit = async (e: ChangeEvent) => { e.preventDefault(); - console.log('Form submitted:', formData); + + try { + const response = await fetch("http://localhost:8080/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + Email: formData.email, + Password: formData.password, + }), + }); + + if (!response.ok) { + throw new Error('Registering failed') + } + + console.log('Registered succesfully') + navigate({ to: "/login" }); + } catch (error) { + throw new Error(`Error during registering: ${error}`) + } }; return ( diff --git a/app/front/src/routeTree.gen.ts b/app/front/src/routeTree.gen.ts index d94c3ef..27e462f 100644 --- a/app/front/src/routeTree.gen.ts +++ b/app/front/src/routeTree.gen.ts @@ -13,7 +13,10 @@ import { Route as rootRoute } from './routes/__root' import { Route as RegisterImport } from './routes/register' import { Route as LoginImport } from './routes/login' +import { Route as AuthImport } from './routes/_auth' import { Route as IndexImport } from './routes/index' +import { Route as AuthDriveIndexImport } from './routes/_auth/drive/index' +import { Route as AuthDriveFolderPathImport } from './routes/_auth/drive/$folderPath' // Create/Update Routes @@ -29,12 +32,29 @@ const LoginRoute = LoginImport.update({ getParentRoute: () => rootRoute, } as any) +const AuthRoute = AuthImport.update({ + id: '/_auth', + getParentRoute: () => rootRoute, +} as any) + const IndexRoute = IndexImport.update({ id: '/', path: '/', getParentRoute: () => rootRoute, } as any) +const AuthDriveIndexRoute = AuthDriveIndexImport.update({ + id: '/drive/', + path: '/drive/', + getParentRoute: () => AuthRoute, +} as any) + +const AuthDriveFolderPathRoute = AuthDriveFolderPathImport.update({ + id: '/drive/$folderPath', + path: '/drive/$folderPath', + getParentRoute: () => AuthRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -46,6 +66,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } + '/_auth': { + id: '/_auth' + path: '' + fullPath: '' + preLoaderRoute: typeof AuthImport + parentRoute: typeof rootRoute + } '/login': { id: '/login' path: '/login' @@ -60,47 +87,91 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof RegisterImport parentRoute: typeof rootRoute } + '/_auth/drive/$folderPath': { + id: '/_auth/drive/$folderPath' + path: '/drive/$folderPath' + fullPath: '/drive/$folderPath' + preLoaderRoute: typeof AuthDriveFolderPathImport + parentRoute: typeof AuthImport + } + '/_auth/drive/': { + id: '/_auth/drive/' + path: '/drive' + fullPath: '/drive' + preLoaderRoute: typeof AuthDriveIndexImport + parentRoute: typeof AuthImport + } } } // Create and export the route tree +interface AuthRouteChildren { + AuthDriveFolderPathRoute: typeof AuthDriveFolderPathRoute + AuthDriveIndexRoute: typeof AuthDriveIndexRoute +} + +const AuthRouteChildren: AuthRouteChildren = { + AuthDriveFolderPathRoute: AuthDriveFolderPathRoute, + AuthDriveIndexRoute: AuthDriveIndexRoute, +} + +const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren) + export interface FileRoutesByFullPath { '/': typeof IndexRoute + '': typeof AuthRouteWithChildren '/login': typeof LoginRoute '/register': typeof RegisterRoute + '/drive/$folderPath': typeof AuthDriveFolderPathRoute + '/drive': typeof AuthDriveIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '': typeof AuthRouteWithChildren '/login': typeof LoginRoute '/register': typeof RegisterRoute + '/drive/$folderPath': typeof AuthDriveFolderPathRoute + '/drive': typeof AuthDriveIndexRoute } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute + '/_auth': typeof AuthRouteWithChildren '/login': typeof LoginRoute '/register': typeof RegisterRoute + '/_auth/drive/$folderPath': typeof AuthDriveFolderPathRoute + '/_auth/drive/': typeof AuthDriveIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/login' | '/register' + fullPaths: '/' | '' | '/login' | '/register' | '/drive/$folderPath' | '/drive' fileRoutesByTo: FileRoutesByTo - to: '/' | '/login' | '/register' - id: '__root__' | '/' | '/login' | '/register' + to: '/' | '' | '/login' | '/register' | '/drive/$folderPath' | '/drive' + id: + | '__root__' + | '/' + | '/_auth' + | '/login' + | '/register' + | '/_auth/drive/$folderPath' + | '/_auth/drive/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + AuthRoute: typeof AuthRouteWithChildren LoginRoute: typeof LoginRoute RegisterRoute: typeof RegisterRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + AuthRoute: AuthRouteWithChildren, LoginRoute: LoginRoute, RegisterRoute: RegisterRoute, } @@ -116,6 +187,7 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", + "/_auth", "/login", "/register" ] @@ -123,11 +195,26 @@ export const routeTree = rootRoute "/": { "filePath": "index.tsx" }, + "/_auth": { + "filePath": "_auth.tsx", + "children": [ + "/_auth/drive/$folderPath", + "/_auth/drive/" + ] + }, "/login": { "filePath": "login.tsx" }, "/register": { "filePath": "register.tsx" + }, + "/_auth/drive/$folderPath": { + "filePath": "_auth/drive/$folderPath.tsx", + "parent": "/_auth" + }, + "/_auth/drive/": { + "filePath": "_auth/drive/index.tsx", + "parent": "/_auth" } } } diff --git a/app/front/src/routes/__root.tsx b/app/front/src/routes/__root.tsx index 72414ec..745c97c 100644 --- a/app/front/src/routes/__root.tsx +++ b/app/front/src/routes/__root.tsx @@ -1,5 +1,16 @@ -import { createRootRoute, Outlet } from "@tanstack/react-router"; +import { AuthState } from "@/hooks/hooksTypes"; +import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/router-devtools"; -export const Route = createRootRoute({ - component: () => , +interface RouterAuthContext { + auth: AuthState; +} + +export const Route = createRootRouteWithContext()({ + component: () => ( + <> + + + + ), }); diff --git a/app/front/src/routes/_auth.tsx b/app/front/src/routes/_auth.tsx new file mode 100644 index 0000000..87f5e99 --- /dev/null +++ b/app/front/src/routes/_auth.tsx @@ -0,0 +1,17 @@ +import { SiteHeader } from '@/components/header' +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth')({ + beforeLoad: ({ context, location }) => { + if (!context.auth.isAuth) { + throw redirect({ + to: '/login', + search: { + redirect: location.href, + }, + }) + } + }, + + component: SiteHeader, +}) diff --git a/app/front/src/routes/_auth/drive/$folderPath.tsx b/app/front/src/routes/_auth/drive/$folderPath.tsx new file mode 100644 index 0000000..bb08a49 --- /dev/null +++ b/app/front/src/routes/_auth/drive/$folderPath.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/drive/$folderPath')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/_auth/drive/$folderPath"!
+} diff --git a/app/front/src/routes/_auth/drive/index.tsx b/app/front/src/routes/_auth/drive/index.tsx new file mode 100644 index 0000000..1624666 --- /dev/null +++ b/app/front/src/routes/_auth/drive/index.tsx @@ -0,0 +1,6 @@ +import Drive from '@/pages/drive' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/_auth/drive/')({ + component: () => , +}) diff --git a/app/front/src/routes/index.tsx b/app/front/src/routes/index.tsx index 6366ad3..2fc056f 100644 --- a/app/front/src/routes/index.tsx +++ b/app/front/src/routes/index.tsx @@ -1,12 +1,5 @@ -import Index from "@/pages/Index"; -import { createFileRoute } from "@tanstack/react-router"; -import { TanStackRouterDevtools } from "@tanstack/router-devtools"; +import { createFileRoute, Outlet } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ - component: () => ( - <> - - - - ), + component: Outlet, });