diff --git a/.qsync/meta/meta.conf b/.qsync/meta/meta.conf index c68e52a3..473c87fd 100644 --- a/.qsync/meta/meta.conf +++ b/.qsync/meta/meta.conf @@ -1,4 +1,4 @@ [/dms] -max_log = 502986 +max_log = 510381 number = 3 finish = 1 diff --git a/.qsync/meta/qmeta0 b/.qsync/meta/qmeta0 index 172bd15d..e844e6db 100644 --- a/.qsync/meta/qmeta0 +++ b/.qsync/meta/qmeta0 @@ -1,39 +1,39 @@ /share/CACHEDEV1_DATA/Container/dms -.git/:1759638121:0 -mariadb/:1759638121:0 -phpmyadmin/:1759638121:0 -backend/:1759638121:0 -frontend/:1759638121:0 -n8n/:1759638121:0 +.git/:1759640646:0 +mariadb/:1759640620:0 +phpmyadmin/:1759640620:0 +backend/:1759640620:0 +frontend/:1759640620:0 +n8n/:1759640620:0 logs/:1757491706:0 .vscode/:1757985732:0 .syncing_db/:1756445267:0 -scripts/:1759638121:0 -docker-compose.yml:1759638121:11090 +scripts/:1759640620:0 +docker-compose.yml:1759640620:11090 npm/:1758073592:0 -index.html:1759638121:1499 -.gitignore:1759638121:2195 -pgadmin/:1759638121:0 -style.css:1759638121:2208 -b.env:1759638121:2689 -README.md:1759638121:5487 -n8n-postgres/:1759638121:0 +index.html:1759640620:1499 +.gitignore:1759640620:2195 +README.md:1759640620:5487 +pgadmin/:1759640620:0 +style.css:1759640620:2208 +b.env:1759640620:2689 +gitea.yml:1759640620:2228 +n8n-postgres/:1759640620:0 .editorconfig:1758876690:187 -docker-backend-build.yml:1759638121:653 +docker-backend-build.yml:1759640620:653 7.conf:1759482888:6539 -gitea.yml:1759638121:2228 -landing/:1759638121:0 -docker-frontend-build.yml:1759638121:917 +landing/:1759640620:0 +docker-frontend-build.yml:1759640620:917 Documents/:1758647542:0 ng.oonf:1759482888:3220 -generate-shadcn-components.yml:1759638121:951 +generate-shadcn-components.yml:1759640620:942 build.log:1759638408:11225 -lcbp3.code-workspace:1759638121:588 +Bearer-Token.patch.diff:1759640620:19143 package-lock.json:1759208202:82 -Bearer-Token.patch.diff:1759638121:19143 -.github/:1759638121:0 +lcbp3.code-workspace:1759640620:588 +.github/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/.github -copilot-instructions.md:1759638121:5519 +copilot-instructions.md:1759640620:5519 /share/CACHEDEV1_DATA/Container/dms/Documents สรุปขั้นตอนการแก้ไขปัญหา n8n.docx:1757496137:18955 ChatGPT_prompt__v5_1.md:1757098209:1674 @@ -89,24 +89,24 @@ package.json:1757746298:411 Dockerfile:1757756549:2332 เช็คลิสต์กู้ frontend (Next.js + Tailwind + shadcn).md:1758336066:4059 /share/CACHEDEV1_DATA/Container/dms/landing -index.html:1759638121:1263 +index.html:1759640620:1263 /share/CACHEDEV1_DATA/Container/dms/n8n-postgres -pg_wal/:1759638121:0 -base/:1759638121:0 +pg_wal/:1759640620:0 +base/:1759640620:0 pg_commit_ts/:1757494489:0 pg_dynshmem/:1757494489:0 pg_notify/:1757494489:0 pg_serial/:1757494489:0 pg_snapshots/:1757494489:0 -global/:1759638121:0 +global/:1759640620:0 pg_twophase/:1757494489:0 -pg_xact/:1759638121:0 +pg_xact/:1759640620:0 pg_replslot/:1757494489:0 pg_tblspc/:1757494489:0 pg_stat/:1759632264:0 pg_stat_tmp/:1757494489:0 -pg_logical/:1759638121:0 -postmaster.pid:1759638121:94 +pg_logical/:1759640620:0 +postmaster.pid:1759640620:94 /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_logical snapshots/:1757494489:0 mappings/:1757494489:0 @@ -118,53 +118,53 @@ pgstat.stat:1759632264:41880 /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_tblspc /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_replslot /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_xact -0000:1759638121:8192 +0000:1759640620:8192 /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_twophase /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/global -pg_control:1759638121:8192 -pg_internal.init:1759638121:28676 +pg_control:1759640620:8192 +pg_internal.init:1759640620:28676 /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_snapshots /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_serial /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_notify /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_dynshmem /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_commit_ts /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/base -16384/:1759638121:0 +16384/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/base/16384 -1247:1759638121:147456 -1247_vm:1759638121:8192 -1249:1759638121:786432 -1249_fsm:1759638121:24576 -1249_vm:1759638121:8192 -1259:1759638121:172032 -1259_vm:1759638121:8192 -2608:1759638121:188416 -2608_vm:1759638121:8192 -2619:1759638121:237568 -2619_fsm:1759638121:24576 -2658:1759638121:262144 -2658_fsm:1759638121:24576 -2659:1759638121:180224 -2659_fsm:1759638121:24576 -2662:1759638121:40960 -2663:1759638121:65536 -2673:1759638121:196608 -2673_fsm:1759638121:24576 -2674:1759638121:114688 -2696:1759638121:40960 -2703:1759638121:40960 -2704:1759638121:57344 -2840:1759638121:32768 -2841:1759638121:16384 -3455:1759638121:32768 -pg_internal.init:1759638121:159700 +1247_vm:1759640620:8192 +1249:1759640620:786432 +1249_fsm:1759640620:24576 +1249_vm:1759640620:8192 +1259_vm:1759640620:8192 +2608:1759640620:188416 +2619_fsm:1759640620:24576 +2658:1759640620:262144 +2659_fsm:1759640620:24576 +2662:1759640620:40960 +2663:1759640620:65536 +2674:1759640620:114688 +2841:1759640620:16384 +3455:1759640620:32768 +1247:1759640620:147456 +1259:1759640620:172032 +2659:1759640620:180224 +2608_vm:1759640620:8192 +2703:1759640620:40960 +2619:1759640620:237568 +2658_fsm:1759640620:24576 +2673:1759640620:196608 +2673_fsm:1759640620:24576 +2696:1759640620:40960 +2704:1759640620:57344 +2840:1759640620:32768 +pg_internal.init:1759640620:159700 /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_wal archive_status/:1757494489:0 -000000010000000000000004:1759638121:16777216 +000000010000000000000004:1759640620:16777216 000000010000000000000005:1759556297:16777216 /share/CACHEDEV1_DATA/Container/dms/n8n-postgres/pg_wal/archive_status /share/CACHEDEV1_DATA/Container/dms/pgadmin -sessions/:1759595501:0 +sessions/:1759640620:0 storage/:1757868516:0 azurecredentialcache/:1757868516:0 /share/CACHEDEV1_DATA/Container/dms/pgadmin/azurecredentialcache @@ -186,16 +186,16 @@ d2df0450-77b2-4967-bf87-8f38ce0bb4e8:1759485387:0 0df55aa5-d6f8-4fee-a943-8ac114d46671:1759589589:0 e5583fcf-64a9-40c6-be53-07db7d42a3e8:1759595501:0 /share/CACHEDEV1_DATA/Container/dms/npm -data/:1759638121:0 -letsencrypt/:1759638121:0 -custom/:1759638121:0 +data/:1759640620:0 +letsencrypt/:1759640620:0 +custom/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/npm/custom http.conf:1759595375:70 /share/CACHEDEV1_DATA/Container/dms/npm/letsencrypt renewal-hooks/:1758077823:0 -renewal/:1759638121:0 -archive/:1759638121:0 -live/:1759638121:0 +renewal/:1759640620:0 +archive/:1759640620:0 +live/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/npm/letsencrypt/live npm-12/:1759398744:0 npm-13/:1759463000:0 @@ -235,12 +235,12 @@ post/:1758077823:0 /share/CACHEDEV1_DATA/Container/dms/npm/letsencrypt/renewal-hooks/deploy /share/CACHEDEV1_DATA/Container/dms/npm/letsencrypt/renewal-hooks/pre /share/CACHEDEV1_DATA/Container/dms/npm/data -nginx/:1759638121:0 +nginx/:1759640620:0 custom_ssl/:1758077299:0 -logs/:1759638121:0 +logs/:1759640620:0 access/:1758077299:0 letsencrypt-acme-challenge/:1758246982:0 -database.sqlite:1759638121:479232 +database.sqlite:1759640620:479232 landng/:1758100038:0 landing/:1758103032:0 /share/CACHEDEV1_DATA/Container/dms/npm/data/landing @@ -278,7 +278,7 @@ proxy-host-9_error.log:1759463001:0 /share/CACHEDEV1_DATA/Container/dms/npm/data/nginx custom/:1758077294:0 default_www/:1758077299:0 -proxy_host/:1759638121:0 +proxy_host/:1759640620:0 redirection_host/:1758077299:0 stream/:1758077299:0 dead_host/:1758077299:0 @@ -289,7 +289,7 @@ temp/:1759463000:0 /share/CACHEDEV1_DATA/Container/dms/npm/data/nginx/redirection_host /share/CACHEDEV1_DATA/Container/dms/npm/data/nginx/proxy_host 6.conf:1759595375:1789 -5.conf:1759638121:1725 +5.conf:1759640620:1725 7.conf:1759595375:4773 8.conf:1759595375:1295 4.conf.disabled:1759461427:1659 @@ -297,15 +297,15 @@ temp/:1759463000:0 /share/CACHEDEV1_DATA/Container/dms/npm/data/nginx/default_www /share/CACHEDEV1_DATA/Container/dms/npm/data/nginx/custom /share/CACHEDEV1_DATA/Container/dms/scripts -backup-mariadb.sh:1759638121:483 -healthcheck.sh:1759638121:66 -watch-qnap-ssl.sh:1759638121:316 -backup_db.sh:1759638121:1968 +backup-mariadb.sh:1759640620:483 +healthcheck.sh:1759640620:66 +watch-qnap-ssl.sh:1759640620:316 +backup_db.sh:1759640620:1968 /share/CACHEDEV1_DATA/Container/dms/.syncing_db /share/CACHEDEV1_DATA/Container/dms/.vscode settings.json:1757985748:31 /share/CACHEDEV1_DATA/Container/dms/logs -backend/:1759638121:0 +backend/:1759640620:0 frontend/:1757320861:0 nginx/:1757931149:0 phpmyadmin/:1757493691:0 @@ -341,42 +341,42 @@ n8nEventLog-3.log:1759541899:0 /share/CACHEDEV1_DATA/Container/dms/n8n/git /share/CACHEDEV1_DATA/Container/dms/n8n/binaryData /share/CACHEDEV1_DATA/Container/dms/frontend -app/:1759638121:0 +app/:1759640620:0 Dockerfile:1759213359:4313 -package.json:1759638121:1143 -.dockerignore:1759638121:89 -lib/:1759638121:0 +package.json:1759640620:1143 +.dockerignore:1759640620:89 +lib/:1759640620:0 api/:1758331810:0 -page.jsx:1759638121:27573 +page.jsx:1759640620:27573 public/:1756542229:0 -node_modules/:1759631884:0 -components.json:1759635683:466 +node_modules/:1759638752:0 +package-lock.json:1759640727:266527 .next/:1759215523:0 .logs/:1757748438:0 -postcss.config.js:1759638121:81 -styles/:1759638121:0 -tailwind.config.js:1759638121:2330 +postcss.config.js:1759640620:81 +styles/:1759640620:0 +tailwind.config.js:1759640620:2330 components/:1758333756:0 .editorconfig:1759138259:147 .eslintrc.json:1759138259:384 .prettierrc.json:1759138259:207 Dockerfile.bak:1759198743:3846 /share/CACHEDEV1_DATA/Container/dms/frontend/components -ui/:1759638121:0 +ui/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/frontend/components/ui -button.jsx:1759638121:1676 -badge.jsx:1759638121:990 -card.jsx:1759638121:1440 -input.jsx:1759638121:688 -tabs.jsx:1759638121:1543 -progress.jsx:1759638121:667 -dropdown-menu.jsx:1759638121:6359 -tooltip.jsx:1759638121:1141 -switch.jsx:1759638121:1039 +button.jsx:1759640620:1676 +badge.jsx:1759640620:990 +card.jsx:1759640620:1440 +input.jsx:1759640620:688 +tabs.jsx:1759640620:1543 +progress.jsx:1759640620:667 +dropdown-menu.jsx:1759640620:6359 +tooltip.jsx:1759640620:1141 +switch.jsx:1759640620:1039 label.jsx:1759206357:539 alert.jsx:1759208266:1335 /share/CACHEDEV1_DATA/Container/dms/frontend/styles -global.css:1759638121:58 +global.css:1759640620:58 /share/CACHEDEV1_DATA/Container/dms/frontend/.logs /share/CACHEDEV1_DATA/Container/dms/frontend/.next /share/CACHEDEV1_DATA/Container/dms/frontend/node_modules @@ -457,14 +457,14 @@ eastasianwidth/:1759204684:0 is-number/:1759204684:0 strip-bom/:1759204684:0 yaml/:1759204690:0 -@radix-ui/:1759634564:0 +@radix-ui/:1759638753:0 @types/:1759204681:0 @unrs/:1759631884:0 string-width-cjs/:1759586283:0 tinyglobby/:1759204688:0 eslint-module-utils/:1759586283:0 tailwindcss/:1759586283:0 -.package-lock.json:1759635707:279008 +.package-lock.json:1759640728:266657 ecdsa-sig-formatter/:1759223641:0 class-variance-authority/:1759204689:0 clsx/:1759586285:0 @@ -559,6 +559,7 @@ glob/:1759204684:0 eslint-import-resolver-node/:1759204687:0 lodash.isinteger/:1759223641:0 jwa/:1759223641:0 +js-cookie/:1759638753:0 react-dom/:1759204690:0 autoprefixer/:1759204690:0 eslint/:1759204691:0 diff --git a/.qsync/meta/qmeta1 b/.qsync/meta/qmeta1 index 9407170d..a4953dd0 100644 --- a/.qsync/meta/qmeta1 +++ b/.qsync/meta/qmeta1 @@ -3880,6 +3880,17 @@ react-dom-test-utils.production.min.js:1759204690:12616 react-dom.development.js:1759204690:1029622 react-dom.production.min.js:1759204690:131685 react-dom.profiling.min.js:1759204690:141112 +/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/js-cookie +LICENSE:1759638753:1118 +index.js:1759638753:45 +dist/:1759638753:0 +package.json:1759638753:1785 +README.md:1759638753:12387 +/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/js-cookie/dist +js.cookie.js:1759638753:4189 +js.cookie.min.js:1759638753:1731 +js.cookie.min.mjs:1759638753:1428 +js.cookie.mjs:1759638753:3475 /share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/jwa LICENSE:1759223641:1068 index.js:1759223641:6801 diff --git a/.qsync/meta/qmeta2 b/.qsync/meta/qmeta2 index ab983e58..c13dc0c6 100644 --- a/.qsync/meta/qmeta2 +++ b/.qsync/meta/qmeta2 @@ -1079,75 +1079,6 @@ react-primitive/:1759204690:0 react-use-previous/:1759204688:0 react-dropdown-menu/:1759204689:0 react-progress/:1759206304:0 -react-dialog/:1759634564:0 -react-scroll-area/:1759634564:0 -number/:1759634564:0 -react-alert-dialog/:1759634564:0 -react-checkbox/:1759634564:0 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/react-checkbox -dist/:1759634564:0 -package.json:1759634564:1921 -README.md:1759634564:96 -LICENSE:1759634564:1063 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/react-checkbox/dist -index.js:1759634564:10982 -index.js.map:1759634564:19393 -index.mjs.map:1759634564:18638 -index.mjs:1759634564:8751 -index.d.mts:1759634564:3054 -index.d.ts:1759634564:3054 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/react-alert-dialog -dist/:1759634564:0 -package.json:1759634564:1815 -README.md:1759634564:104 -src/:1759634564:0 -LICENSE:1759634564:1063 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/react-alert-dialog/src -index.ts:1759634564:643 -alert-dialog.test.tsx:1759634564:2062 -alert-dialog.tsx:1759634564:12961 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/react-alert-dialog/dist -index.js:1759634564:9558 -index.js.map:1759634564:17746 -index.mjs.map:1759634564:16790 -index.mjs:1759634564:7227 -index.d.mts:1759634564:4131 -index.d.ts:1759634564:4131 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/number -README.md:1759634564:174 -dist/:1759634564:0 -package.json:1759634564:1405 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/number/dist -index.d.mts:1759634564:96 -index.d.ts:1759634564:96 -index.js:1759634564:1115 -index.js.map:1759634564:458 -index.mjs:1759634564:177 -index.mjs.map:1759634564:354 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/react-scroll-area -dist/:1759634564:0 -package.json:1759634564:1958 -README.md:1759634564:102 -LICENSE:1759634565:1063 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/react-scroll-area/dist -index.js:1759634564:33152 -index.js.map:1759634564:59789 -index.mjs.map:1759634564:59284 -index.mjs:1759634564:29784 -index.d.mts:1759634564:6006 -index.d.ts:1759634564:6006 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/react-dialog -dist/:1759634564:0 -package.json:1759634564:2151 -README.md:1759634564:92 -LICENSE:1759634564:1063 -/share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/react-dialog/dist -index.js:1759634564:16209 -index.js.map:1759634564:29625 -index.mjs.map:1759634564:28979 -index.mjs:1759634564:12986 -index.d.mts:1759634564:5203 -index.d.ts:1759634564:5203 /share/CACHEDEV1_DATA/Container/dms/frontend/node_modules/@radix-ui/react-progress dist/:1759586742:0 package.json:1759206304:1658 @@ -6004,92 +5935,92 @@ stringify.d.ts:1759204689:165 warning.d.ts:1759204689:2994 /share/CACHEDEV1_DATA/Container/dms/frontend/public /share/CACHEDEV1_DATA/Container/dms/frontend/api -health/:1759638121:0 +health/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/frontend/api/health -route.js:1759638121:241 +route.js:1759640620:241 /share/CACHEDEV1_DATA/Container/dms/frontend/lib -api.js:1759638121:1344 -auth.js:1759638121:2561 -rbac.js:1759638121:203 -utils.js:1759638121:137 -auth copy.js:1759638121:3157 +api.js:1759640620:1344 +auth.js:1759640620:2561 +rbac.js:1759640620:203 +utils.js:1759640620:137 +auth copy.js:1759640620:3157 /share/CACHEDEV1_DATA/Container/dms/frontend/app -globals.css:1759638121:3271 -layout.jsx:1759638121:5837 -page.jsx:1759638121:1972 +globals.css:1759640620:3271 +layout.jsx:1759640620:5837 +page.jsx:1759640620:1972 _auth/:1759307658:0 .next/:1757403024:0 (auth)/:1759219438:0 -(protected)/:1759638121:0 +(protected)/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected) -layout.jsx:1759638121:3151 -dashboard/:1759638121:0 -correspondences/:1759638121:0 -users/:1759638121:0 -drawings/:1759638121:0 -rfas/:1759638121:0 -transmittals/:1759638121:0 -contracts-volumes/:1759638121:0 -reports/:1759638121:0 -health/:1759638121:0 -admin/:1759569176:0 -workflow/:1759638121:0 -_components/:1759638121:0 -/share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/_components -SideNavigation.jsx:1759638121:3360 -navigation.jsx:1759638121:3251 -/share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/workflow -page.jsx:1759638121:163 +layout.jsx:1759640620:3151 +dashboard/:1759640620:0 +correspondences/:1759640620:0 +users/:1759640620:0 +drawings/:1759640620:0 +rfas/:1759640620:0 +transmittals/:1759640620:0 +contracts-volumes/:1759640620:0 +reports/:1759640620:0 +health/:1759640620:0 +_components/:1759640620:0 +workflow/:1759640620:0 +admin/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/admin -_components/:1759571216:0 -layout.jsx:1759569176:1445 -roles/:1759569176:0 -users/:1759569176:0 +_components/:1759640620:0 +layout.jsx:1759640620:1445 +roles/:1759640620:0 +users/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/admin/users -page.jsx:1759569176:5955 +page.jsx:1759640620:5955 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/admin/roles -page.jsx:1759569176:3465 +page.jsx:1759640620:3465 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/admin/_components -confirm-delete-dialog.jsx:1759569176:1120 -role-form-dialog.jsx:1759569176:5376 -user-form-dialog.jsx:1759571216:11826 +confirm-delete-dialog.jsx:1759640620:1120 +role-form-dialog.jsx:1759640620:5376 +user-form-dialog.jsx:1759640620:11826 +/share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/workflow +page.jsx:1759640620:163 +/share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/_components +SideNavigation.jsx:1759640620:3360 +navigation.jsx:1759640620:3251 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/health -page.jsx:1759638121:168 +page.jsx:1759640620:168 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/reports -page.jsx:1759638121:118 +page.jsx:1759640620:118 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/contracts-volumes -page.jsx:1759638121:174 +page.jsx:1759640620:174 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/transmittals -new/:1759638121:0 -page.jsx:1759638121:4559 +new/:1759640620:0 +page.jsx:1759640620:4559 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/transmittals/new -page.jsx:1759638121:4735 +page.jsx:1759640620:4735 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/rfas -new/:1759638121:0 -page.jsx:1759638121:5939 +new/:1759640620:0 +page.jsx:1759640620:5939 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/rfas/new -page.jsx:1759638121:4733 +page.jsx:1759640620:4733 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/drawings -page.jsx:1759638121:239 -upload/:1759638121:0 +page.jsx:1759640620:239 +upload/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/drawings/upload -page.jsx:1759638121:190 +page.jsx:1759640620:190 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/users -page.jsx:1759638121:154 +page.jsx:1759640620:154 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/correspondences -new/:1759638121:0 -page.jsx:1759638121:122 +new/:1759640620:0 +page.jsx:1759640620:122 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/correspondences/new -page.jsx:1759638121:167 +page.jsx:1759640620:167 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(protected)/dashboard -page.jsx:1759638121:6913 -page copy.jsx:1759569176:37806 +page.jsx:1759640620:6913 +page copy.jsx:1759640620:37806 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(auth) -login/:1759638121:0 +login/:1759640620:0 layout.jsx:1759219438:1064 /share/CACHEDEV1_DATA/Container/dms/frontend/app/(auth)/login -page.jsx:1759638121:8834 -page copy.jsx:1759638121:7864 +page.jsx:1759640620:8834 +page copy.jsx:1759640620:7864 /share/CACHEDEV1_DATA/Container/dms/frontend/app/.next standalone/:1757403024:0 /share/CACHEDEV1_DATA/Container/dms/frontend/app/.next/standalone @@ -6103,25 +6034,25 @@ useAuthGuard.jsx:1759307658:722 bearerDriver.js:1759307658:2264 cookieDriver.js:1759307658:1309 /share/CACHEDEV1_DATA/Container/dms/backend -src/:1759569462:0 -Dockerfile:1759638121:2948 -package.json:1759638121:1075 +src/:1759640620:0 +Dockerfile:1759640620:2948 +package.json:1759640620:1075 app/:1757398694:0 -.dockerignore:1759638121:69 +.dockerignore:1759640620:69 logs/:1757399942:0 README.md:1757503080:6593 -package_v0_5_0..json:1759638121:656 +package_v0_5_0..json:1759640620:656 node_modules/:1758876682:0 -Dockerfile.dev:1759638121:2948 -ed25519:1759638121:411 -ed25519.pub:1759638121:0 -package-lock.json:1759638121:101053 -fix-bearer-index.patch.diff:1759638121:2105 +Dockerfile.dev:1759640620:2948 +ed25519:1759640620:411 +ed25519.pub:1759640620:0 +package-lock.json:1759640620:101053 +fix-bearer-index.patch.diff:1759640620:2105 README2.md:1759292144:6536 -Dockerfile.build:1759638121:1289 -Dockerfile copy:1759638121:2711 +Dockerfile.build:1759640620:1289 +Dockerfile copy:1759640620:2711 start-dev.sh:1757740663:2064 -package_v0_6_0.json:1759638121:1028 +package_v0_6_0.json:1759640620:1028 /share/CACHEDEV1_DATA/Container/dms/backend/node_modules bcryptjs/:1758876682:0 dotenv/:1758876682:0 @@ -6454,76 +6385,76 @@ settings.json:1758355386:42 logs/:1757380974:0 /share/CACHEDEV1_DATA/Container/dms/backend/app/logs /share/CACHEDEV1_DATA/Container/dms/backend/src -db/:1759638121:0 -middleware/:1759638121:0 -routes/:1759638121:0 -utils/:1759638121:0 -index.js:1759569462:5701 +db/:1759640620:0 +middleware/:1759640620:0 +routes/:1759640620:0 +utils/:1759640620:0 +index.js:1759640620:5701 config.js:1759292144:1272 -config/:1759638121:0 +config/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/backend/src/config -permissions.js:1759638121:1306 +permissions.js:1759640620:1306 /share/CACHEDEV1_DATA/Container/dms/backend/src/utils passwords.js:1759292144:758 -rbac.js:1759638121:3795 -scope.js:1759638121:3977 +rbac.js:1759640620:3795 +scope.js:1759640620:3977 cookie.js:1759550383:566 jwt.js:1759550383:1033 /share/CACHEDEV1_DATA/Container/dms/backend/src/routes auth.js:1759551860:9895 -users.js:1759638121:3728 +users.js:1759640620:3728 correspondences.js:1759292144:3755 -rfa.js:1759638121:2326 +rfa.js:1759640620:2326 transmittals.js:1759292144:4109 -technicaldocs.js:1759638121:3894 -view.js:1759638121:2905 -admin.js:1759638121:2775 +technicaldocs.js:1759640620:3894 +view.js:1759640620:2905 +admin.js:1759640620:2775 health.js:1759292144:505 auth_extras.js:1759292144:783 documents.js:1759292144:4374 drawings.js:1759292144:3764 files_extras.js:1759292144:5392 -contracts.js:1759638121:3916 +contracts.js:1759640620:3916 maps.js:1759292144:5214 module_files.js:1759292144:2551 mvp.js:1759292144:2892 ops.js:1759292144:740 organizations.js:1759292144:1571 projects.js:1759292144:3575 -rbac_admin.js:1759638121:3947 +rbac_admin.js:1759640620:3947 rfas.js:1759292144:5395 subcategories.js:1759292144:2729 uploads.js:1759292144:3230 users_extras.js:1759292144:1415 views.js:1759292144:1056 volumes.js:1759292144:1597 -permissions.js:1759638121:537 +permissions.js:1759640620:537 lookup.js:1759292144:3481 -contract_dwg.js:1759638121:4287 -categories.js:1759638121:1710 +contract_dwg.js:1759640620:4287 +categories.js:1759640620:1710 auth พัง.js:1759551860:5262 -dashboard copy.js:1759569176:1780 -dashboard.js:1759638121:2234 -rbac_admin copy.js:1759569176:3899 -users copy.js:1759569176:1601 +dashboard copy.js:1759640620:1780 +dashboard.js:1759640620:2234 +rbac_admin copy.js:1759640620:3899 +users copy.js:1759640620:1601 /share/CACHEDEV1_DATA/Container/dms/backend/src/middleware -auth.js:1759638121:2001 +auth.js:1759640620:2001 rbac.js:1759136311:765 errorHandler.js:1759136311:398 abac.js:1759136311:5276 permGuard.js:1759136311:770 permissions.js:1759136311:1391 -loadPrincipal.js:1759638121:925 -requirePerm.js:1759638121:1590 -authJwt.js:1759638121:1741 -index.js:1759638121:1089 -auth copy.js:1759638121:2001 +loadPrincipal.js:1759640620:925 +requirePerm.js:1759640620:1590 +authJwt.js:1759640620:1741 +auth copy.js:1759640620:2001 +index.js:1759640620:1089 /share/CACHEDEV1_DATA/Container/dms/backend/src/db models/:1757558454:0 -sequelize.js:1759638121:6879 -index.js:1759638121:1226 -index copy.js:1759572555:1226 -sequelize copy.js:1759573613:2665 +sequelize.js:1759640620:6879 +index.js:1759640620:1226 +index copy.js:1759640620:1226 +sequelize copy.js:1759640620:2665 /share/CACHEDEV1_DATA/Container/dms/backend/src/db/models User.js:1757497334:506 Role.js:1757497334:305 @@ -6554,7 +6485,7 @@ vw_FilesByModule.js:1757499720:633 vw_RfaSummary.js:1757499720:550 vw_TransmittalOverview.js:1757499720:520 /share/CACHEDEV1_DATA/Container/dms/phpmyadmin -sessions/:1759638121:0 +sessions/:1759640620:0 tmp/:1757126512:0 /share/CACHEDEV1_DATA/Container/dms/phpmyadmin/tmp /share/CACHEDEV1_DATA/Container/dms/phpmyadmin/sessions @@ -6565,25 +6496,25 @@ sess_7c786cdae7eb5d7cead835a9a479bc62:1759580734:103 sess_54ff5adfd5f57b7951a03668f3a132a5:1759567233:8984 /share/CACHEDEV1_DATA/Container/dms/mariadb init/:1759292144:0 -my.cnf:1759638121:1733 -data/:1759638121:0 -Dockerfile:1759638121:58 -util.sql:1759638121:729 -backup init/:1759638121:0 +my.cnf:1759640620:1733 +data/:1759640620:0 +Dockerfile:1759640620:58 +util.sql:1759640620:729 +backup init/:1759640620:0 /share/CACHEDEV1_DATA/Container/dms/mariadb/backup init -01_dms_v0_5_0_data_v5_1_deploy_table_rbac.sql:1759638121:32958 -02_dms_v0_5_0_data_v5_1_triggers.sql:1759638121:30689 -03_dms_v0_5_0_data_v5_1_procedures_handlers.sql:1759638121:54941 -04_dms_v0_5_0_data_v5_1_views.sql:1759638121:28375 -05 dms_v0_5_0_data_v5_1_seeก_data.sql:1759638121:11434 -06_dms_v0_5_0_data_v5_1_seed_users.sql:1759638121:7319 -07_dms_v0_5_0_data_v5_1_seed_contract_dwg.sql:1759638121:266678 -10 dma_data_v5_1_patch_kpi_dashboard.sql:1759638121:0 -dms_v0_5_0_data_v5_1_sql.zip:1759638121:33591 -20_n8n_db.sh:1759638121:535 +01_dms_v0_5_0_data_v5_1_deploy_table_rbac.sql:1759640620:32958 +02_dms_v0_5_0_data_v5_1_triggers.sql:1759640620:30689 +03_dms_v0_5_0_data_v5_1_procedures_handlers.sql:1759640620:54941 +04_dms_v0_5_0_data_v5_1_views.sql:1759640620:28375 +05 dms_v0_5_0_data_v5_1_seeก_data.sql:1759640620:11434 +06_dms_v0_5_0_data_v5_1_seed_users.sql:1759640620:7319 +07_dms_v0_5_0_data_v5_1_seed_contract_dwg.sql:1759640620:266678 +10 dma_data_v5_1_patch_kpi_dashboard.sql:1759640620:0 +dms_v0_5_0_data_v5_1_sql.zip:1759640620:33591 +20_n8n_db.sh:1759640620:535 /share/CACHEDEV1_DATA/Container/dms/mariadb/data -aria_log.00000001:1759638121:17137664 -aria_log_control:1759638121:52 +aria_log.00000001:1759640620:17137664 +aria_log_control:1759640620:52 ddl_recovery-backup.log:1759220398:20480 /share/CACHEDEV1_DATA/Container/dms/mariadb/init 03_dms_v0_5_0_data_v5_1_procedures_handlers.sql:1758642791:54941 @@ -6601,36 +6532,17 @@ hooks/:1758460941:0 info/:1758461466:0 refs/:1758465366:0 config:1759632143:425 -objects/:1759638121:0 -HEAD:1759638121:41 +objects/:1759640620:0 +HEAD:1759640594:21 packed-refs:1758461355:103 -index:1759638121:29908 -COMMIT_EDITMSG:1759638112:24 +index:1759640646:26334 +COMMIT_EDITMSG:1759640263:4 logs/:1758461355:0 -FETCH_HEAD:1759638120:111 -rebase-merge/:1759638121:0 -ORIG_HEAD:1759638121:41 -MERGE_MSG:1759638121:1327 -AUTO_MERGE:1759638121:41 +FETCH_HEAD:1759640620:262 +ORIG_HEAD:1759640620:41 REBASE_HEAD:1759638121:41 -/share/CACHEDEV1_DATA/Container/dms/.git/rebase-merge -interactive:1759638121:0 -head-name:1759638121:43 -onto:1759638121:41 -orig-head:1759638121:41 -drop_redundant_commits:1759638121:0 -no-reschedule-failed-exec:1759638121:0 -end:1759638121:2 -done:1759638121:59 -git-rebase-todo:1759638121:146 -msgnum:1759638121:2 -stopped-sha:1759638121:41 -git-rebase-todo.backup:1759638121:1689 -message:1759638121:1327 -author-script:1759638121:108 -patch:1759638121:0 /share/CACHEDEV1_DATA/Container/dms/.git/logs -HEAD:1759638121:10063 +HEAD:1759640620:10573 refs/:1758465366:0 /share/CACHEDEV1_DATA/Container/dms/.git/logs/refs heads/:1759565383:0 @@ -6638,13 +6550,13 @@ remotes/:1758465366:0 /share/CACHEDEV1_DATA/Container/dms/.git/logs/refs/remotes origin/:1759587716:0 /share/CACHEDEV1_DATA/Container/dms/.git/logs/refs/remotes/origin -main:1759637114:7582 +main:1759640280:7733 HEAD:1758764057:135 feature/:1759587716:0 /share/CACHEDEV1_DATA/Container/dms/.git/logs/refs/remotes/origin/feature dashboard-update-251004:1759638132:604 /share/CACHEDEV1_DATA/Container/dms/.git/logs/refs/heads -main:1759564681:6544 +main:1759640620:6699 feature/:1759565383:0 /share/CACHEDEV1_DATA/Container/dms/.git/logs/refs/heads/feature dashboard-update-251004:1759638112:1723 @@ -6656,7 +6568,7 @@ b7/:1759636691:0 58/:1759637884:0 e5/:1759632112:0 8b/:1759225086:0 -d8/:1759588054:0 +d8/:1759640263:0 38/:1759596865:0 8f/:1759637884:0 61/:1759301407:0 @@ -6676,7 +6588,7 @@ ba/:1759638106:0 44/:1759454426:0 24/:1759225086:0 dd/:1759571211:0 -22/:1759638112:0 +22/:1759640263:0 b4/:1759637884:0 2b/:1759632113:0 70/:1759569003:0 @@ -6693,7 +6605,7 @@ a8/:1759638121:0 bb/:1759225086:0 0f/:1759637884:0 8c/:1759637884:0 -b5/:1759596866:0 +b5/:1759640263:0 48/:1759638121:0 b3/:1759127594:0 db/:1759225086:0 @@ -6767,7 +6679,7 @@ c9/:1759572555:0 be/:1759313751:0 55/:1759370679:0 a9/:1759637114:0 -01/:1759637114:0 +01/:1759640179:0 80/:1759127627:0 49/:1759569003:0 1d/:1759569003:0 @@ -6780,7 +6692,7 @@ d5/:1759301435:0 c4/:1759637884:0 ce/:1759637884:0 f0/:1759225117:0 -ab/:1759596915:0 +ab/:1759640179:0 54/:1759632112:0 02/:1759634355:0 42/:1759225086:0 @@ -6820,7 +6732,7 @@ f4/:1759569458:0 9b/:1759225086:0 66/:1759454426:0 05/:1759588054:0 -c6/:1759596915:0 +c6/:1759640179:0 72/:1759632112:0 23/:1759596866:0 15/:1759127594:0 @@ -6868,7 +6780,7 @@ ae/:1759638121:0 c1/:1759313721:0 85/:1759637883:0 32/:1759637883:0 -94/:1759225084:0 +94/:1759640179:0 7a/:1759596915:0 20/:1759634355:0 47/:1759596915:0 @@ -6887,14 +6799,14 @@ d2/:1759573613:0 28/:1759551859:0 9c/:1759454426:0 53/:1759632130:0 -67/:1759225086:0 +67/:1759640263:0 03/:1759373409:0 78/:1759632112:0 b6/:1759588054:0 5f/:1759225086:0 -0e/:1759637884:0 +0e/:1759640263:0 04/:1759596866:0 -4d/:1759482887:0 +4d/:1759640179:0 c2/:1759292144:0 56/:1759588053:0 79/:1759632130:0 @@ -6902,13 +6814,13 @@ c5/:1759596866:0 d0/:1759225086:0 a1/:1759588054:0 e8/:1759637884:0 -17/:1759637883:0 +17/:1759640179:0 0a/:1759632131:0 16/:1759569458:0 99/:1759588054:0 11/:1759637114:0 /share/CACHEDEV1_DATA/Container/dms/.git/objects/11 -58c240b99868dcd44c4b6e102a0c783db8e92d:1759637114:447 +58c240b99868dcd44c4b6e102a0c783db8e92d:1759640263:447 /share/CACHEDEV1_DATA/Container/dms/.git/objects/99 1c54224188ecae3aeb33184a2f25e9d702a8ba:1759292144:1243 cfdee90d0997cd424e079df1a185b1be86ade0:1759454488:85 @@ -6930,6 +6842,7 @@ bf5618f76363b70eceb8f64c7f11e1b9dfb229:1759632131:173 6f591f919cd4d918a23772a3a9935262850a66:1759225086:924 8dfe04fe8c752e984607149acad0ca686f34b5:1759307658:2706 84d9c391405830ed9eb5e0d5cf0267940bf490:1759637883:207 +2bd15d7cff50163f2dc44d99473c5e78cf02b3:1759640179:96335 /share/CACHEDEV1_DATA/Container/dms/.git/objects/e8 b587710e6145ba944eba57f825fd69f8683109:1759225086:861 73d625e20ec4f73c7e277cf761802ea93b87b3:1759313722:4391 @@ -6948,7 +6861,7 @@ ddcdef195a5bde9ce5ad0262a4b43e4366b481:1759225086:369 92472040211988e7050b9a7a770e44e4b39adb:1759225084:580 276e458b75bc08a2f3bbd246ae00ecb39b0794:1759301407:63706 8f600afb39b192016fe7d2ac3a4be0b01fb235:1759571211:533 -38c47a187d7b96864321eda821f6289bbc1f42:1759596866:19851 +38c47a187d7b96864321eda821f6289bbc1f42:1759640179:19851 /share/CACHEDEV1_DATA/Container/dms/.git/objects/79 727048095c2d5e16eb5dc39e56e98f331ed9fd:1759225084:871 b4b3231a57cad4029237f2c375c40394c49136:1759225086:1171 @@ -6965,14 +6878,15 @@ e3108bd9d7a0f2fa46a1cdb74fe000c88c6953:1759225084:569 f38749c22a83eabbe929bc7e5a9472e18bb18e:1759225084:1349 041e3839ba332afb6813f496bb87e2b7697b33:1759307658:206 7e69247de348bca3be5d34eb6fcff8b5df7823:1759482887:164 +ab35e4df9351bbd5c874ffc07e3c45625e9500:1759640179:67503 /share/CACHEDEV1_DATA/Container/dms/.git/objects/04 37afdb2d7d642ca134aaf6cf1139f54f744775:1759301407:4756 7bea3cbe0fd239167a2185a94ab84b2172046e:1759218944:187 7b1756307aa9fc63e9afe3c99d6dbce75a2577:1759292144:734 9290fd23d141780f070a3e05823825eed3d276:1759368663:983 -98fc150e7677460cc20426cb1de1bdeffe8848:1759596866:21833 +98fc150e7677460cc20426cb1de1bdeffe8848:1759640179:21833 /share/CACHEDEV1_DATA/Container/dms/.git/objects/0e -19653e48e6ad295a5bed72ed17bae54a758596:1759215825:416 +19653e48e6ad295a5bed72ed17bae54a758596:1759640179:416 22d5d49672a0c2e4ac54df419466604723d8cc:1759225086:530 1bd83031b7eaaa16e79d312ae60805b15777d1:1759225117:1393 2e863d7217c51a3a1e9603b98fc91790c8de0c:1759313723:28680 @@ -6981,6 +6895,7 @@ f38749c22a83eabbe929bc7e5a9472e18bb18e:1759225084:1349 004a8ee49e57505fc7d9d83a1a56c6c2223d95:1759572555:533 b537c160ad2fdfcd4f77579e45f4098c33e4d9:1759638121:132 39ae65da3b4aa021c5c1aeb4a5ed9f7de06891:1759637884:47 +8db4d275c77e58ff224ed6eb734456969ad130:1759640263:132 /share/CACHEDEV1_DATA/Container/dms/.git/objects/5f b661731d9b19f9a46a908bdf467c5a315a796b:1759141563:558 079bea658fde3741dce52bbbe38c6ef36bc9b4:1759225086:262 @@ -7011,6 +6926,7 @@ a8a3b864c8e8f83b9dd268f817ca22f47db82d:1759373409:183 398d527a82eafc32d0d48880c0adbd9463db26:1759138259:2777 e4765b38da44f53ab57b85404c40e2eb35a5d8:1759225083:7539 2a779067bb9de43744afefada2a26d7e11eee7:1759225086:773 +0228b76e11b8de6dc6b707a775b78b2b7f5820:1759640263:155 /share/CACHEDEV1_DATA/Container/dms/.git/objects/53 f75a2d7118926458b439e69dd5e2080ea96f05:1759136315:206 f816282249037848ab16e2bf78183db67223ad:1759225084:376 @@ -7078,7 +6994,7 @@ ae320e4cf5cc419f00e37b6d638bd7bc879b4c:1759141563:600 5f56b2be9dfb4aa3b3589e144f4353f78960a6:1759225084:1666 7b079ea34b7dda48f1850fbc95d2158005e0a9:1759225086:355 1a093084cd5ae79d1206b8efe8debd5a35f4bc:1759301435:78 -5fa7d470616a616f24448a99bfa1498e63fa6d:1759588054:102 +5fa7d470616a616f24448a99bfa1498e63fa6d:1759640179:102 /share/CACHEDEV1_DATA/Container/dms/.git/objects/1c 08ad15ed3319d11e720e849ae43395067129b1:1759127596:984 710015de14a5be21002eaece7e780779c9d0a6:1759141200:176 @@ -7124,12 +7040,13 @@ db83ade3a6eb78c1155aba6a77ade382259f2c:1759127596:735 a7bbe5ceed6b615b71045175d3c6edeae37b0a:1759225084:855 0de3d4b9147a5c722ab44bf5c81ef444345d77:1759225086:349 f77575957d3e03fb92693170ba00bb57c5ff13:1759370679:983 -55b0dec6fbac06c49a4e743d62843cece9faf4:1759596866:402 +55b0dec6fbac06c49a4e743d62843cece9faf4:1759640179:402 5e880575d4251fe38a449d7c1e5771a5880f7f:1759596915:1136 /share/CACHEDEV1_DATA/Container/dms/.git/objects/94 be7e3c3ddf934646db53c92f7e19e9cf319f14:1759127596:924 d2df7f815cd4c9e36333eb8e7e19a6c2494771:1759127627:171 625fd68d5003d20bd3dcb7dbb49d565fd4912e:1759225084:268 +07170dc420cc8d85884107af585f5233836c24:1759640179:101681 /share/CACHEDEV1_DATA/Container/dms/.git/objects/32 41e47032b90b0f009124755c1ca4a025dde6cd:1759127596:802 ca728adc1ebe199d541fde0926c8dc23ad1c8b:1759225117:463 @@ -7140,7 +7057,7 @@ c2e7dfcaca71d46ae579b4d19f2329b6900e62:1759596915:233 /share/CACHEDEV1_DATA/Container/dms/.git/objects/85 2dc9faa7ec2bbe1e30a5f9f883d137ffc9746d:1759127596:615 3ae67a9b8cba7f98386669f3bcfd1cd24f163a:1759225086:1974 -4eb5378fc0283ee4a95dd2eeca8b2764d2106a:1759596866:66 +4eb5378fc0283ee4a95dd2eeca8b2764d2106a:1759640179:66 e8b686d8e5846c35d309e4d7b10751c175ffd2:1759638106:3302885 /share/CACHEDEV1_DATA/Container/dms/.git/objects/c1 fe0e84e9ee48e15197df2dd4dbf56385ea9750:1759127596:874 @@ -7152,10 +7069,10 @@ a5112d9422ce8d1750a131827051ac8fd57f7e:1759313721:3302792 a6701a89adac3cfec9ebbc838dce822fd09b38:1759138763:601 f3d7108ae90181cb7cd5cc3855e7988578847b:1759225086:784 91a80876a737cd3a603a0250965dbdee402797:1759454426:5558 -2bad54cc68de80e16f74e20c7e3ebbc16e213f:1759596867:689 +2bad54cc68de80e16f74e20c7e3ebbc16e213f:1759640179:689 24593c05cd00471568f68d1ea0f8f731a44bf2:1759638111:1391 -c89fa537a973688ed882ef733ff279825eb601:1759638121:233 -2edd75f5e9040d2fe5fda42f4f3fb1983cc967:1759638121:750 +c89fa537a973688ed882ef733ff279825eb601:1759640263:233 +2edd75f5e9040d2fe5fda42f4f3fb1983cc967:1759640263:750 /share/CACHEDEV1_DATA/Container/dms/.git/objects/6d aceed6d89108aa9528f976cd9d423ce2df8470:1759127596:745 92504d0cdf3786f94ed395120b4f37cae81aa0:1759225086:706 @@ -7175,7 +7092,7 @@ db538591c5a1031309af801c78faacf4eeef87:1759127596:1974 392cd11fafa1e9d4db5cf2f896a0dffa536af0:1759292144:768 4ce25b13afd8e425d267cb503b2fef6bb589a2:1759313723:2289 74b4316826436639ff8bb7aaed4423642841cd:1759569003:53 -b60585e59712a7c43fb3962065a8b303671a78:1759596866:5541 +b60585e59712a7c43fb3962065a8b303671a78:1759640179:5541 1aeee191b8ca17e16ba56f4af160664caae154:1759636691:168 872528af72c3f726c5bbcad10987c9681b5cd7:1759637884:232 /share/CACHEDEV1_DATA/Container/dms/.git/objects/d6 @@ -7183,7 +7100,7 @@ b60585e59712a7c43fb3962065a8b303671a78:1759596866:5541 55cf91ff673b2e8bdc381ea51b5689915d5540:1759225086:300 b816b922aed0bf40405b60f47dcb760cbe5288:1759301407:4385 e27acf74597b579e5b19c35debf75480477179:1759454488:1393 -97b55205b888777e66aebeb6252283a4cdc9fa:1759637114:1432 +97b55205b888777e66aebeb6252283a4cdc9fa:1759640179:1432 /share/CACHEDEV1_DATA/Container/dms/.git/objects/c7 9c9fcf48ba6d39e7de5f627ea13782acef83f8:1759127596:718 22f531677a8c4f564c1fba586fc496d02f17ca:1759225084:381 @@ -7204,7 +7121,7 @@ a65ceaccfcb983c756dbb7f0531c8842f052cb:1759225086:643 3cede926cfbdba665b1b849c894e045ed22ed0:1759313751:467 765260e0b0cb9a5390248a18b8a6a2a36555df:1759370106:984 811b3b61e17089cfd859208544da475061ad87:1759454426:566 -18e82db4c40ad95f0ef83727db7c830256a401:1759638121:177 +18e82db4c40ad95f0ef83727db7c830256a401:1759640263:177 /share/CACHEDEV1_DATA/Container/dms/.git/objects/91 67e25f0c6c9822428dd5b6b24ed2bd95c9c70f:1759127596:931 40fb974414eae71ce26d034b23b81781fc1b94:1759127596:1171 @@ -7223,7 +7140,7 @@ c80748c012a81f6a4a9b8511c2022f4c326515:1759127596:797 7e565735a35fba4c276286a7c8717a3e30e994:1759301407:77194 cdd357af4db77cc87ae2bd5ebab555c2ddecfc:1759301408:5006661 5d93ed4d76f1f5d52ec9e88e2e5ccba648f9a2:1759569458:233 -8a94bc56f30cd09235858f93968458e8dc5e92:1759596866:22199 +8a94bc56f30cd09235858f93968458e8dc5e92:1759640179:22199 262ce73dc45fe4df539d9525f930fe831ae0bf:1759638121:497 /share/CACHEDEV1_DATA/Container/dms/.git/objects/f1 71ffd2a72bb2ff7e5f674e34cb955080da2d5c:1759127596:1236 @@ -7251,7 +7168,7 @@ c4a0ee45a93679812236107e82e6a2d485056e:1759225086:617 829044b617265b246f962bb2e42846fb1f4362:1759127596:760651 112d997c60c503e45f227422545bf345c957e6:1759127596:665 70d963b592d56983ab76551294aa10f741e7af:1759127597:363 -60e04cfa9398b5159512f88d997e5a42ab4bb6:1759215825:3068 +60e04cfa9398b5159512f88d997e5a42ab4bb6:1759640179:3068 4f18decf034f32e521f16a229835d1010457e3:1759225084:982 c6f751e0c2417bcb22357f8c883ccac4dbe82d:1759571211:3356 /share/CACHEDEV1_DATA/Container/dms/.git/objects/a4 @@ -7268,7 +7185,7 @@ a5db5b3b0d98da5cfd1670a08da5eee51c01ea:1759569458:533 b440d76cff0f49b67e7d2b8588fcb2a4ce70b5:1759225086:460 ff89f18c1dbddd700481d7b1652e96f7ae7455:1759454426:4391 c71f1dff2fd45c87c97f7674b187f3bcab964b:1759632112:400 -76b0785f5a772fdc291ec09ebefab8459d9b16:1759638121:358 +76b0785f5a772fdc291ec09ebefab8459d9b16:1759640263:358 /share/CACHEDEV1_DATA/Container/dms/.git/objects/18 8273d5fcc1702d93d97c38af2d3dd92132afc9:1759127595:344 214747e25dbf744b0aa4e91eab8ca37aa2c09b:1759127596:573 @@ -7288,7 +7205,7 @@ f1fafb1455595179189467712b5a15efa808af:1759225084:271 57924f843ebc341e927ba49798ff3d57908b20:1759225086:298 4edaf8564160f672c03fc992252bc047f132e2:1759225086:616 ae7bce5723807bd2474823b47c264978c46966:1759313751:214 -798242577434eadbd2ef5ea51f73257f511207:1759638121:51 +798242577434eadbd2ef5ea51f73257f511207:1759640263:51 /share/CACHEDEV1_DATA/Container/dms/.git/objects/cf 3eeba183afeddd19c961c648382bb8fff36b6a:1759127595:205 5b2385015f4fd21034db03d598296673b9f654:1759215888:87 @@ -7306,7 +7223,7 @@ d2cd044539c32451d6a47da941145eb3b4bdf8:1759638106:4999614 da3f6fc31e29a176d677d5bcc9053edef3882c:1759225084:632 dcf260b6821d81e7e79313f1974b9aed657cbf:1759225086:493 a52e6c9c733395eb0e28930b7f4475f7f2115a:1759292552:984 -ef3a82a1fa77728e873cd1435c77c5245f8aa5:1759588054:103 +ef3a82a1fa77728e873cd1435c77c5245f8aa5:1759640179:103 /share/CACHEDEV1_DATA/Container/dms/.git/objects/d3 15049216b7e20cf5bd3fb8bbe79970b2365936:1759127595:2301 cfbaf836e86b45ad4e701fbd9ace1a4a32d4b5:1759215826:321 @@ -7323,7 +7240,7 @@ cfb91ed82e1da4cdcc4f5df91a64d3664cc218:1759127595:262 736bd9ec8ebc8dffd4e0386ad377fea6063910:1759138259:851 35198311ab91dc9bb3167a7ea6646c073078e5:1759225086:287 b8ec1b4257df3c4147b6c9b80b71e11aa1b8d6:1759373409:87 -7942f97b7aa1f3b4410428a6c3f268e05594f0:1759596866:11121 +7942f97b7aa1f3b4410428a6c3f268e05594f0:1759640179:11121 /share/CACHEDEV1_DATA/Container/dms/.git/objects/5d dbc7fca874dfe10cb6bbd649eef2b734a17840:1759127595:278 1edb384dc683e982e680b49a5238102a289580:1759127596:1231 @@ -7362,7 +7279,7 @@ dfa4a01a4ffa73ab44934619e5be1cbc212ed6:1759218944:2150 d005265f497ad8c6b0dcb131b0ed437c6917b0:1759225086:1731 /share/CACHEDEV1_DATA/Container/dms/.git/objects/de 42059da674d4df5c240e8b2a98209345f06e2b:1759127594:886 -94c1593d5bcd6ca64c055c3cef308c921046c6:1759596865:474 +94c1593d5bcd6ca64c055c3cef308c921046c6:1759640179:474 7b9f9b641cedd265b663e669a39fe87ef95f0c:1759632112:6024 /share/CACHEDEV1_DATA/Container/dms/.git/objects/68 772ce0767e907627ea39b1672347da246f8439:1759127594:482 @@ -7377,14 +7294,14 @@ c31c7a62cb87883800124c68be7e40576fdad8:1759632130:122 ac5e4c2900a3d6d3a74f5c499321e131c62bd7:1759225084:222 e08dd755da541d858a979409655bca3d5fa286:1759313722:19888 c8f7e8df8bd891c3ee47ce4464625c455ceb33:1759588054:29015 -1827fb0587239de2c2e0642f40218ef7397f09:1759588054:270 +1827fb0587239de2c2e0642f40218ef7397f09:1759640179:270 7981be135e34293afee34cae75a4703d146805:1759638106:1064 /share/CACHEDEV1_DATA/Container/dms/.git/objects/63 2d64050f5a64768c352aa264201f99ab3a8ea2:1759127594:547 a01fba77448d21e821f3806dc73e22f2b32313:1759127596:510 65eebb87849c732f5fc480fafeb011c8fdec04:1759127596:1159 bc7c20e68964f0303ac0b519f2263a01e034c7:1759225084:235 -1280e191a850fe958ade7b4a28ef73ab6b81ee:1759596865:1272 +1280e191a850fe958ade7b4a28ef73ab6b81ee:1759640179:1272 /share/CACHEDEV1_DATA/Container/dms/.git/objects/e6 fc70098aceb46b7d83017ac59770eab5b5e5ce:1759127594:593 33a91ffeeee1f2460d670af759b83364128cdb:1759225084:311 @@ -7392,13 +7309,13 @@ fc70098aceb46b7d83017ac59770eab5b5e5ce:1759127594:593 6f14ebf50c8fc8b8e7e507d026f38fb1be0714:1759225117:209 5febb03a04fc757887876f1a45827a114cae08:1759292144:1375 78a0cc0be0a065e8b871a9de6e65f602fab972:1759573613:533 -a0f2bbc6a44488f7380f7180cc1c7eec8c095e:1759636691:1155 +a0f2bbc6a44488f7380f7180cc1c7eec8c095e:1759640179:1155 /share/CACHEDEV1_DATA/Container/dms/.git/objects/00 73ca159dd62a797f30e8db7b66cf6b9c2b9549:1759127594:609 bdd6219e5b1fbf93a9084d5e2b96f272117ee5:1759127596:1205 6cc948e4271cfcb7f87677cf0d61452dd9f9cf:1759127596:446 621e429ce4f803483f4b702dc5156971d038bc:1759313751:983 -16e4b632bf5882af14fe3379a6489509206b07:1759596867:77 +16e4b632bf5882af14fe3379a6489509206b07:1759640179:77 /share/CACHEDEV1_DATA/Container/dms/.git/objects/eb c20a19be167f7676d8b849b53ff26fcc0a9b0b:1759127594:560 0c16741e3ff43f899d0572bc1de16aebd7f75a:1759225084:307 @@ -7414,8 +7331,8 @@ ea909902b82372fae820e35a05e8ec247ea250:1759370679:183 34c1e41e23630d87c64e5a1dc4bd0fa9d47332:1759225084:763 54b94c9fc1bfff34dc97a2a79854585063c397:1759225084:761 3d9fc1d0ca11c7f71ae3a289858544a08a3859:1759301435:164 -031b0f07e049822a8e624e32c6faff3e28c69d:1759596866:20149 -1b9159101c957fd131aca34ae0432316f25b19:1759638121:1046 +031b0f07e049822a8e624e32c6faff3e28c69d:1759640179:20149 +1b9159101c957fd131aca34ae0432316f25b19:1759640179:1046 /share/CACHEDEV1_DATA/Container/dms/.git/objects/87 68a55a0a43c7b0b65b867379d54d8f8b669799:1759127594:635 a9a77347d2cbd7d9c3ab571d914fe7a93d403c:1759127595:310 @@ -7437,7 +7354,7 @@ b3aa5fa573508eeff3d19835b62319c7581c8a:1759588109:329 /share/CACHEDEV1_DATA/Container/dms/.git/objects/23 bc87e091e0107b0fd426bc40ac9c7b1fa91623:1759127594:519 cc86b87e12feb4d7c20c9c713298f25296ab03:1759225084:4887 -3634ef6f55238bed7a6d368dea87f0eafb02cc:1759596866:64591 +3634ef6f55238bed7a6d368dea87f0eafb02cc:1759640179:64591 /share/CACHEDEV1_DATA/Container/dms/.git/objects/72 63b8e0cb28253143af6836b5da338cce6419f8:1759127594:1565 2204240d129d52d2cfbadea92b5cabb5f33775:1759127595:303 @@ -7454,6 +7371,7 @@ be048a792fd8f602e4032b4ac5eefbccf9623b:1759292144:121 bdb3d43cb99659e2758cf1f3d1bf79aa372cb3:1759301408:28679 b9b269cb9a7b7a380cc32518a5348abd2e3c77:1759313721:1474 67bee427c9fad7761e1bfdc28777a309d31cba:1759596915:183 +8e52a36644589c262da05986fbe341def4a274:1759640179:59 /share/CACHEDEV1_DATA/Container/dms/.git/objects/05 514a388b601e0361e7c3be111604f1a47ec184:1759127594:981 c8d64ee9d36670384173ba643d597701d65a88:1759127596:752 @@ -7474,7 +7392,7 @@ b3270f9ca20e960ea66476aa6885243ec4b031:1759225086:205 6f39dbcc8ba935e0c2f95bc7d6c53260a4d1a4:1759225086:760661 /share/CACHEDEV1_DATA/Container/dms/.git/objects/41 a596147b0e65c13c14eafdcae946ba4733cb1d:1759127594:324 -19c3f11f6f70501f11841ee78191779507f7fc:1759215825:777 +19c3f11f6f70501f11841ee78191779507f7fc:1759640179:777 dd55000e6e511470ffd2b37f187110208d4c5a:1759225086:615 2f98e3b4606d10dc1dd902ff0da83c672ca566:1759301435:154 d417c03fd91388f601c0154754fab8c68c4a5d:1759454426:28927 @@ -7523,7 +7441,7 @@ c0d3fbeb6daabcfa52b1b04be7dcca061cbfe9:1759225086:310 e9a6c9f4869c3f493901319881c8a683a1e5c2:1759292144:1241 330da8b37034076d6885213c8a9bd0d1d47fd4:1759301435:122 91c3e7625d95cc4a612b42513b38ab9d5e292e:1759313721:1899 -54b780283fd6ff6f849ae0c2885f6ab38b3236:1759638121:46 +54b780283fd6ff6f849ae0c2885f6ab38b3236:1759640263:46 /share/CACHEDEV1_DATA/Container/dms/.git/objects/3c 5a131d314758a443840ed65b2d26c21fad1a9b:1759127594:1107 c457142f9ed0a858ea5de31d56282a923047ad:1759215825:689 @@ -7542,7 +7460,7 @@ d2cead0f26cc2efc3eedd735223f7f7da08375:1759141200:1631 86ffa58f6178ce8f102c33992cba6ea6737767:1759225086:219 83db38c36fd4a4062a314fa8201b7d22c19989:1759454488:90 52afe08a39b5451cec4729ebdf519bc5c9c493:1759596915:358 -a8c8235e8a86d07ca349c8a7a90186076d6771:1759638121:51 +a8c8235e8a86d07ca349c8a7a90186076d6771:1759640263:51 8f467d13f52fca32963c6513c531cec88aaa4a:1759638111:44 6c60376904ef65efc00f1f44e8ac9017b15841:1759638121:868 /share/CACHEDEV1_DATA/Container/dms/.git/objects/d1 @@ -7551,7 +7469,7 @@ d6fe3f2804b1e2e4423749d189741fd12c4ecc:1759127594:518 b164402217b8abc646fb33a2bd5d50e4d24225:1759127595:240 4b16ca8cca34d3c2e62291af111aab9c4756e0:1759127627:111 ba831ef6188aaf46c4e86a48876f40b836363e:1759225084:726 -e354f112767d43d3578d3304ea31120b6ece12:1759596865:1812 +e354f112767d43d3578d3304ea31120b6ece12:1759640179:1812 /share/CACHEDEV1_DATA/Container/dms/.git/objects/40 a48a53038011229c2b93f1253869a2e5303e12:1759127594:997 7d6d8384af09e501702fa0a37e74186b5352e0:1759127594:667 @@ -7584,7 +7502,7 @@ f6719d2dfab2b471245810fbdc99b16460ce04:1759127594:492 45ac036b96df2c3f4e31f3f685ddddd59c8f8a:1759292144:381 5e85c247a87fc51b29b66e901779e542601713:1759454426:5830 0b0180b34931438d7a368fa634db01be810e25:1759572555:233 -9d4c640a783f7781156d83786e8de9ead6dc6b:1759596866:70197 +9d4c640a783f7781156d83786e8de9ead6dc6b:1759640179:70197 5477877497492ffdb2ed90b2e802171b783fa1:1759632112:19808 /share/CACHEDEV1_DATA/Container/dms/.git/objects/ee c5e0f3e4d1d748929dca089f2881c4ff916589:1759127594:296 @@ -7603,14 +7521,14 @@ ce39755e18a362039ae3a9b38691d651472f8c:1759127596:11030 1da2de01a6fff09d445bec9bf7e4c1b6e70c4a:1759292144:620 9bcfb3f39f3fe707d5be9e5f19942268e628fb:1759301435:467 32093b4605b951ef81f439f7b94829a38befef:1759454488:111 -69135096f4cae12fbe182f668fd9b87c48f56d:1759596866:6017 +69135096f4cae12fbe182f668fd9b87c48f56d:1759640179:6017 43289d69cf9f072c1c353715008210098b37d8:1759632112:4810 /share/CACHEDEV1_DATA/Container/dms/.git/objects/b1 8d8b4b94853ef6285d607963600de38db851c1:1759127594:1556 3eceb9bf664b95ad48d42855da6d4a239089be:1759127594:462 7862dcecae4e71105f08431fe08393ec13da17:1759127596:891 16a347afe04cfc2a9472caf97cfc08691cced5:1759301407:114 -bbaf6617392611bb3d6907a402890900a81780:1759596866:76899 +bbaf6617392611bb3d6907a402890900a81780:1759640179:76899 52cbf0e715c2a2469c7b992387f5ba205fb154:1759638106:20225 /share/CACHEDEV1_DATA/Container/dms/.git/objects/1b 168038211e21b268bcc6715b963bcf10325710:1759127594:300 @@ -7630,11 +7548,11 @@ c8c6d7e702a0f4bba0e9d98bdd435e5b36c0fa:1759225117:123 /share/CACHEDEV1_DATA/Container/dms/.git/objects/71 6cb4addd2e1af197c1d962b18f667104369f6a:1759127594:221 536254c54ad86024382e302c6d2396bc4b3c04:1759225084:2262 -9fe64150504e597730bad4435d1109be0c2bff:1759596866:3302879 -c6e7679db7fd7da5a1d4ddeea7249c498e70a7:1759596866:5823 +9fe64150504e597730bad4435d1109be0c2bff:1759640179:3302879 +c6e7679db7fd7da5a1d4ddeea7249c498e70a7:1759640179:5823 fc7eee13ee4eadbf3c714c906f52116f2fae97:1759596915:162 4367cf9648f4ed983e529eab57a7ebdb4edcbc:1759632112:10982 -7f7a6cefcaaf47df551a4ff9eabdcac88a2fed:1759638121:52 +7f7a6cefcaaf47df551a4ff9eabdcac88a2fed:1759640263:52 /share/CACHEDEV1_DATA/Container/dms/.git/objects/10 b2f0e1e2dba80f0d97b406ba2b5aba15d2b894:1759127594:509 2c946df3c5fd52e1914efd4b9e6fd1f6ae6a97:1759127594:591 @@ -7661,7 +7579,7 @@ f89734f8aa0d4a77467f2c3b212aba550474e1:1759588054:5823 1fc2682a323cf004d5b4f767885dc0d1b8706e:1759127596:288 72abc446741036ab951808b46e33577116e5d8:1759301407:44830 fdff2a3ff02574aff32baaeef49af4a425536f:1759313722:35602 -7757c299bfa2f71dcb40333e5fb07764088f41:1759634355:3090 +7757c299bfa2f71dcb40333e5fb07764088f41:1759640179:3090 /share/CACHEDEV1_DATA/Container/dms/.git/objects/46 087086d38c718b947eff7844c93ae67c7c4222:1759127594:4723 ef07a24c1efc5158d3d1491c37d5dff2718ba4:1759127594:463 @@ -7691,7 +7609,7 @@ d2192694939412432b13bea93d31f2f3f1cc5a:1759569003:2090 e8897b7da3a9308e790f16bc9e4d8c6ab92b6b:1759301407:5550 e70757befa957b2f6ddbee2fc29884f2adfeb1:1759454427:4995622 548ec2090bf99b10b278b849a68e3dbe356ed8:1759551859:1008 -14e0844dd1bbc6c99f04f5ff7b2f76c296d328:1759638121:47 +14e0844dd1bbc6c99f04f5ff7b2f76c296d328:1759640263:47 /share/CACHEDEV1_DATA/Container/dms/.git/objects/fd 3a222e5ad3ce2589c71a66c7e706df9b95da86:1759127594:156669 e857faecbd393d83f35aed3bcdf519a348b5a8:1759127594:615 @@ -7719,7 +7637,7 @@ d821b081f46dffb5281f367b35afecf4120375:1759301407:10917 6efc6635551874ed7b9b4758cf0abc0be00a18:1759370106:214 7846cc80dbb30c711c95b5b9272d740193e520:1759569003:1075 58d2a478be74d1bb09bfbdde24844c2e716186:1759588053:58 -0f59a0a7c7eb5581c9c6eea2cbd389a4f4c971:1759596866:35304 +0f59a0a7c7eb5581c9c6eea2cbd389a4f4c971:1759640179:35304 a35fe415df085dbf61c7d6f247b2b028d4bae6:1759636691:215 b5b99a21ecaa9337258ebc11723f28e89062f7:1759637884:96 /share/CACHEDEV1_DATA/Container/dms/.git/objects/2f @@ -7738,7 +7656,7 @@ b19b316c6bfc22f427cac7403e2570bf3f8250:1759225086:1868 b95ce0879cdf51dbfb3dfd26d70b4b75df9462:1759127596:5536 6bc4bb7d2b49b167af2df9bad4d703c4d4e557:1759127597:4948454 377d37b96d3f789933a1bab8001b1f495f99c5:1759225086:1530 -22ae234e39b3da86eddf49dcf97e407bd877b2:1759373409:3286 +22ae234e39b3da86eddf49dcf97e407bd877b2:1759640179:3286 1f8d746dee04dc9637a2c067ce3b6f48d9604f:1759571211:233 e509986ba184030a5ea5a18cd274709e6f7ea4:1759634355:166 /share/CACHEDEV1_DATA/Container/dms/.git/objects/54 @@ -7762,9 +7680,10 @@ c6e844fda625a53ebaaab058a363376ce3617b:1759225086:874 2ef92f24612d2397fb7deba030523c2a5de17c:1759292144:958 7b5621b381aaeb6e372e9dbd2fb8b72b9102c5:1759292144:1520 efdbd637dcbac5db6857affadf3dfb0c96bbb0:1759569003:1990 -e7077b8c9498a6d243da65b2775195ff43b0f3:1759572555:758 +e7077b8c9498a6d243da65b2775195ff43b0f3:1759640179:758 d5a6eb92ca6796aa9233342973772050df525c:1759588054:69819 9f905898f6f72c50374bf99ba744ea6d0f75ab:1759596915:533 +983e58518204e1ff7e2d1f56cc74db31d85e29:1759640179:93743 /share/CACHEDEV1_DATA/Container/dms/.git/objects/f0 b4db72c2ca36a1f669a618a6ea0dc845b1899b:1759127593:126 996b4711c8776f8396942f5413b727fdaf6d29:1759127594:586 @@ -7779,21 +7698,21 @@ ff54442f1734c5915fd586daeef7b5a2313f25:1759127596:278 58851c49860a3b584876de74091d206faadd82:1759215888:209 ca5552dac661ecb4b9be7410f41ac0bbc2d3e0:1759225086:987 f86184fe6dd6acbb3c7aca0b0d3d8ff277b5ec:1759454488:983 -b75a58256ebd481ec7fb60e822a751f32f0747:1759596866:4801 +b75a58256ebd481ec7fb60e822a751f32f0747:1759640179:4801 1730fab2715957c8981a2268dde5da85039f06:1759637884:178 /share/CACHEDEV1_DATA/Container/dms/.git/objects/c4 -b5f9d1bd807059af8ab32b83787514af8be342:1759215825:1479 +b5f9d1bd807059af8ab32b83787514af8be342:1759640179:1479 6f90fab8bb0e69599cbd80985f0179385abc6d:1759127595:362 da71655ea276a1ca237b1050f050e9b29d06f0:1759127627:90 24be2efa5a035ffa203b8205424db6829e4545:1759225086:874 14899a4f0dcca7ddf2b7d1a222e1ede433b1fc:1759571211:176 d81c46aea941631bac16fabfd088213a78e0df:1759637884:868 /share/CACHEDEV1_DATA/Container/dms/.git/objects/d5 -7408f78c3e441871e535aa8cd03b08ad0609e7:1759215825:745 +7408f78c3e441871e535aa8cd03b08ad0609e7:1759640179:745 142af9233683b0bad68702bfe81cd9ebccf316:1759127596:1008 b1fd252cd8c69b2a40c438bb2b15e70f55f8c4:1759301435:1393 /share/CACHEDEV1_DATA/Container/dms/.git/objects/a6 -87ebad024768896157580c5884e8c6c0c0f502:1759215825:456 +87ebad024768896157580c5884e8c6c0c0f502:1759640179:456 9b2970588bff34ca5da457a3c364d2d645f604:1759127594:454 13344c17f995d5c57c38cb382a4b6c77ec93c2:1759127594:383 14a55642a85599495be4506dd20308964a9f44:1759127594:1040 @@ -7809,7 +7728,7 @@ b03ce260022b0d3d8ad6b981bfb26122f670de:1759127596:287 fd3dae0d209a87d960af1385451bc95a13934c:1759127596:304 11fd8d1dfff03e681c8fd4cef6169d5837c634:1759215888:466 79af086457c079948e04256fca384e86102d4f:1759313751:91 -fd524b8ddd9d559221bf26f9de2528c3a33dd6:1759596866:45252 +fd524b8ddd9d559221bf26f9de2528c3a33dd6:1759640179:45252 /share/CACHEDEV1_DATA/Container/dms/.git/objects/75 5a9b737d426902f501c394e084e79945f3a44e:1759127262:209 7648c9c186211813550ac83a18dec6052dbf98:1759127594:791 @@ -7846,7 +7765,8 @@ cf24207389f1de6a664cbf4c92709de7c41305:1759127596:729 17fd29122a20f170e860a55010d5964aa29fb8:1759127627:4494 b841ba5ffc8395818cba04146fb6863b83f82a:1759292144:1641 2d12aca0396825171d2d64efb62e8c5dea1cfd:1759569003:733 -21b655b4c1d7c1e245c7c487e2e1ce543c561e:1759637114:214 +21b655b4c1d7c1e245c7c487e2e1ce543c561e:1759640263:214 +d6dc63522099daeed9e433061893bd9d983ad3:1759640179:549 /share/CACHEDEV1_DATA/Container/dms/.git/objects/a9 33162bbe8859e9402acb805385598ac0052069:1759127262:724 8cdf3081ecc175134bcd92f1696e499d9b0e71:1759127594:727 @@ -7882,7 +7802,7 @@ f550c0fb07fa98af38cc79a44ab747f3a7b1e4:1759127262:1071 92362f0e35de338e0a522ece6d43f9d0cc1006:1759588054:4377 /share/CACHEDEV1_DATA/Container/dms/.git/objects/c9 2cacfcdb70736242e013030df17fa2479f41ad:1759127262:3375 -7570a55bcfd7c369610e9e1d8c735e7747be28:1759215825:538 +7570a55bcfd7c369610e9e1d8c735e7747be28:1759640179:538 375663cb967feefa1298e9b988226d8ee52e68:1759225086:454 8baa94fcc75f57410aee702046eb706afeb52a:1759572555:173 /share/CACHEDEV1_DATA/Container/dms/.git/objects/26 @@ -7953,7 +7873,7 @@ e79121578774af1a4de3963686bb058e756480:1759225086:765 9a67f858fa9ab6146d1268f90404cb1a938226:1759313722:77281 2daf4ac9d57872a7e9a404af1e40bb9f07d7cb:1759636691:777 918acc4593ba81bf9d034b4ad41014250d2dcd:1759638111:646 -9edb00edde1f0788eefd828e71ccc7907b2773:1759638121:47 +9edb00edde1f0788eefd828e71ccc7907b2773:1759640263:47 /share/CACHEDEV1_DATA/Container/dms/.git/objects/aa f542a4e5eb5e6febf6e63f2d55d8d6fe1c1596:1759127262:1427 a53b527b6fb281325903cc50569cbe91bd6488:1759127594:931 @@ -7998,7 +7918,7 @@ af2357bfde4f1a86c4ab45e0f29aea6ddbfd2a:1759127262:657 f0188205496a76c295472c7c4eb7a2cb72ad74:1759127594:311 814243fdde9c5c121c3564f1c7beeebaee4bfe:1759127594:741 54203e539c1fc137e899d147d61037920316cb:1759127596:135 -4a3d7a4a5b031e122e51e3254dfb854ff0be56:1759632112:4389 +4a3d7a4a5b031e122e51e3254dfb854ff0be56:1759640179:4389 /share/CACHEDEV1_DATA/Container/dms/.git/objects/1a 19e66231b9ad6e1edbfc39032c80fdac30ae08:1759127262:488 f2a958cbf2d2b23a6d24d706b5773b6db5e2cb:1759127594:421 @@ -8074,7 +7994,7 @@ d45de4c17099f8ac6db3d4ca01d6b1f37ef946:1759225083:65298 30e20809687e75293c8729b421255b0a2eb366:1759225086:558 33098666314833b4d2baa5d8bf50e780dc7312:1759301408:79 712da852b5752f0a56396ea6610ab5c7aa5f5a:1759370679:3058 -31f22d11ab41fd5fbd7fb813d242f36ab6ea1e:1759596866:592 +31f22d11ab41fd5fbd7fb813d242f36ab6ea1e:1759640179:592 /share/CACHEDEV1_DATA/Container/dms/.git/objects/51 707322c110f70002872c7296d6563c295d1381:1759109967:88 6a8453d1b3ca0386435d6a4ec0c602d23c76dc:1759225086:930 @@ -8122,8 +8042,8 @@ f8c62c9df0a9e09c20168de574a8df507adeb1:1759127594:67 30f43d4c0db08b5f4c4aaa48c959c6d95b6906:1759225084:474 01f26359afa7dd4f8fdb49b53ce6d852494b05:1759225086:905 9680688bba053451618eea2cd48b56708e9176:1759454488:78 -0b0f66cc9a21778272c2dfeb67c76bcb895597:1759596867:87 -7bcab32caaf89eb894ccff5033a5ddc86f46cc:1759636691:1353 +0b0f66cc9a21778272c2dfeb67c76bcb895597:1759640179:87 +7bcab32caaf89eb894ccff5033a5ddc86f46cc:1759640179:1353 edf9847da21794fca7efa7e0a5e644530ea48f:1759638106:572 /share/CACHEDEV1_DATA/Container/dms/.git/objects/27 36ea0059e688e5a71c3ac47888c15be24586ef:1758967105:249 @@ -8153,7 +8073,7 @@ cf3d577c8515f782cebc3661fc54ce60f60731:1759596867:855 15aa4f00605130f4472bd7f285598d20ddbf66:1759127596:304 64649485ce34e82982761300fe4684b5c9ee44:1759225086:278 77586a6f74a9ae6f0e049eaf6baf4c0eaa93ec:1759485142:501 -a238618fb89a5d88d360cd7f1e7c9815abfd0c:1759638121:1136 +a238618fb89a5d88d360cd7f1e7c9815abfd0c:1759640263:1136 /share/CACHEDEV1_DATA/Container/dms/.git/objects/5c f48b6ff944dc226344c115e0568883d05ce541:1758966462:500 e2b68155d123b77b8bde3393c1d4e4ae118ceb:1758967105:195 @@ -8162,10 +8082,10 @@ e2b68155d123b77b8bde3393c1d4e4ae118ceb:1758967105:195 595a7ae9723ac7d4d08c51a321b2e0570b47e2:1759127596:773 7db7a0980d1a5b6b106b38156d295b0400280c:1759215888:122 ac3bdabf3b95234c5b5cdd0ea6f47545e78f6e:1759292552:175 -f920aab4682d5724120138329455f23f301e8f:1759596866:382 +f920aab4682d5724120138329455f23f301e8f:1759640179:382 /share/CACHEDEV1_DATA/Container/dms/.git/objects/82 fc98e9dfd3a9ec2c364342ccb31ec6ecbba2f5:1758966462:182 -7b40d8f32db479baa90dbc202972f3b8002cb1:1759215825:506 +7b40d8f32db479baa90dbc202972f3b8002cb1:1759640179:506 62d225f292fe3fc3df8ec94da56e7266ff8c2d:1759127594:1981 8924aa72947ba3b92956b874df9b7ccaf23c99:1759127627:230 a73b71b5b619cc715d37cf6f14901cc73d408d:1759225086:1124 @@ -8183,10 +8103,10 @@ d3988f369c41ee108ffa6a0c393b30afd599de:1759127594:470 6c0e22b40e3a39e57858823be59bba7bca4522:1759632112:63425 /share/CACHEDEV1_DATA/Container/dms/.git/objects/76 dd805dc0f313aa54381c1479475c9f40111161:1758965251:374 -4b48f35a932da47fcd9ed32efb95cdfd679010:1759215825:400 +4b48f35a932da47fcd9ed32efb95cdfd679010:1759640179:400 8e79a57b3571d75303f89902975294eaa8886d:1759215825:57 725058f72e73b5a72556048b44ddacf335a50e:1759225086:406 -6129b4a87f8621f542368a31bd894cb00d1f6b:1759637114:89 +6129b4a87f8621f542368a31bd894cb00d1f6b:1759640263:89 /share/CACHEDEV1_DATA/Container/dms/.git/objects/6a 26afbf90e97e6620433f3cfa24b234f8abe8df:1758965251:138 4ec58338d115e8f37609a050bb8f2ed649a248:1759127262:846 @@ -8210,7 +8130,7 @@ d5ce80150aef7447bf1b4c4af2012cc045625b:1759127262:195 c2bae48b9ad6aed1566c310d6576d623e6b4da:1759215888:470 295394be4c747ab27a8a58dd39fbd37185eec4:1759370106:87 e0ba865d3a8286e71c449a9395eff050bf3784:1759573613:1697 -c56dae0d92c08e53fb5fa8df2d9134bbca6b30:1759596866:29044 +c56dae0d92c08e53fb5fa8df2d9134bbca6b30:1759640179:29044 /share/CACHEDEV1_DATA/Container/dms/.git/objects/57 8b4b6a08d2a74f355376324a5e83943666f0c2:1758965251:53 3112015bf8629c3bf2ef1f0504a2a52a24370d:1759127262:374 @@ -8236,8 +8156,8 @@ bb469a151e70e14cd12c85b9b1226d94aa5198:1758965251:500 05f54288901b34c5ea8d9e9b084a908729930e:1759127595:295 66810ffb4f20842159b29175d9b111d37dadd9:1759225084:554 6d88dbfa85fcf8e07a969e075672e66d3518b6:1759569003:88 -9d8721145f446bc01dcf525fa40bca969a8b26:1759596866:11572 -32c6d3636c17521bef61c78260bacf2380ce9f:1759638121:91 +9d8721145f446bc01dcf525fa40bca969a8b26:1759640179:11572 +32c6d3636c17521bef61c78260bacf2380ce9f:1759640263:91 /share/CACHEDEV1_DATA/Container/dms/.git/objects/b8 e9bfe870ddeb7fdacd54f01269aedfb7f69449:1758965251:847 de085929ba417a881ea095fcd295d8efb01fd7:1758967105:86 @@ -8316,7 +8236,7 @@ d142b07918200597ddf31ddffa1b1b32771d22:1759225084:588 3ce200ee2a4760b5c021e73350f9598cac7205:1759225084:327 6e72622460199ccd3dd2e7ef10a251fa3e7df6:1759301407:35565 b98602039f48c597fedf7f70aec30bc94e48f9:1759569003:53 -6b705f7293b4a6b4f29ffb94bd14d115c5ef37:1759596866:113 +6b705f7293b4a6b4f29ffb94bd14d115c5ef37:1759640179:113 /share/CACHEDEV1_DATA/Container/dms/.git/objects/3d 54a1cfccc3a39461dfb659de11a1c60beaf303:1758947524:1719 516b0ed0360eb653fe86a9dcb95ae93396d73b:1759127596:701 @@ -8350,12 +8270,12 @@ d5af7186b2a8ec9331f99f4852da6cd17aa327:1758947524:452 876bee1a7ab2c58edd1029d56ee5c31811a654:1758966462:847 35005d2723dd4ea156f2409b64250d1782d735:1759127627:47 0a63c69ee53196fdd47f936bda758f2e2a86ef:1759569003:2106 -2803912642a01c6105b3f8cc78f2ebc1179c4d:1759588054:98 +2803912642a01c6105b3f8cc78f2ebc1179c4d:1759640179:98 633f340601b76d4930fa7b44975da3994d3fc6:1759588054:586 9df755970bd49134a7bdab6ffd85c4c568ace1:1759632112:598 722922f57bd50bef92ea9220fbf2e9389aadc8:1759632113:4999594 e21efe52fb737e855609910ae06aabdbd1f66c:1759638106:245 -97b2237ffd9f447ec77211b641a18b7fa338fc:1759638121:89 +97b2237ffd9f447ec77211b641a18b7fa338fc:1759640263:89 /share/CACHEDEV1_DATA/Container/dms/.git/objects/86 1fd1fb73d9de1202dd98c21610ca021f6c4655:1758947524:848 b84f7ae6d8c326c2237ed7fdd1f3820ba38b8b:1759127596:555 @@ -8380,16 +8300,17 @@ a59e11223d760e43eb2cd20b0b497a45c4fa1d:1759127594:588 36d9374fa0ea6eb2261a3d09f683ca36b99373:1759127597:116 f8af6917431ba2ce18faf7d4c8c0aa763a62a3:1759218944:634 f1d7056c25db59e060d442de57b086f49da5a1:1759569003:1590 -c15dec51e53829e1d51c9cb4b9553e106fb2a8:1759596865:991 -7152e1160c0147a3eb0a0239ccb4fc06f28c05:1759596866:27005 -b7471daeb2365262a9954f69d6b1a8c0c44a79:1759638121:168 +c15dec51e53829e1d51c9cb4b9553e106fb2a8:1759640179:991 +7152e1160c0147a3eb0a0239ccb4fc06f28c05:1759640179:27005 +b7471daeb2365262a9954f69d6b1a8c0c44a79:1759640263:168 /share/CACHEDEV1_DATA/Container/dms/.git/objects/b5 3a7a38198a9cf54861a9597b3533b57341760c:1758880487:1314 d704e8593511d49f2887cefb87c02c01ae301a:1759127594:235 c2bd272f527858881cc5cb2a9af69620e5bf2f:1759127596:906 88d302ae1e4089b823df5e5f909920e4762eb0:1759127596:972735 2c25a303639c81cfa750647c64065aa31bfd1a:1759225086:694 -633416812b91616f8a5aaeb94965c014423506:1759596866:248 +633416812b91616f8a5aaeb94965c014423506:1759640179:248 +ddbf4b8786492aec603af2791e9d39b39006b4:1759640263:502 /share/CACHEDEV1_DATA/Container/dms/.git/objects/8c 6a401f717fc6422d2b8817493403fbdd348650:1758880487:52 08aeb9fbe0cf9ec30e0f6896470ac0e478ad03:1759127595:7491 @@ -8397,7 +8318,7 @@ c2bd272f527858881cc5cb2a9af69620e5bf2f:1759127596:906 f2bc340a3ecc1c96defd2a08ce3ff810b3b4cd:1759292144:364 e9ed43d10879f54488ba33b95d4d4854975f77:1759301408:2288 dcdee24579561d62c12fb2126f412c3ccf15d9:1759482887:1040 -cd9ce4f53bcae06ec9f17a201690d8ab43b08d:1759638121:162 +cd9ce4f53bcae06ec9f17a201690d8ab43b08d:1759640263:162 /share/CACHEDEV1_DATA/Container/dms/.git/objects/0f 0f19e64ef3b9543ccd1034aebd5f9df38dad48:1758880487:187 ce319f94631a1514dd4db24595b4a0505f5798:1759127594:252 @@ -8419,7 +8340,7 @@ eed2a6d29968c0040c8b046a71df5e1a609c46:1759225084:466 1932725ffc75fb8715a166f2e297a0bcb60daf:1759225084:65 762a71c49b950da419a27182f94b9583629e7c:1759225086:679 eadb56981a5f4e1dbf0507733f0eff673d9ba9:1759638111:10021 -4c8351b4ecb18937cd8ac24f9a7f7ccf6f07e0:1759638121:96 +4c8351b4ecb18937cd8ac24f9a7f7ccf6f07e0:1759640263:96 /share/CACHEDEV1_DATA/Container/dms/.git/objects/5b 2a4088151560bdda6f50e62b48f798dc858090:1758880487:90 122fa0a295f6811c4bbcc934bdc90907556f1b:1759127595:279 @@ -8427,7 +8348,7 @@ eadb56981a5f4e1dbf0507733f0eff673d9ba9:1759638111:10021 e0f5407bafe82abeaa01047f99d9af756b3af2:1759225117:161 3796be00071e8656b56e5286391af1386da856:1759307658:83 432819e9546cb0f59d2737fa19bdf01acdd41e:1759454488:104 -614558e05e7429f0cbe380df0954ecd42f6883:1759596867:41730 +614558e05e7429f0cbe380df0954ecd42f6883:1759640179:41730 /share/CACHEDEV1_DATA/Container/dms/.git/objects/0c b06874275704f48a6f4168b656076c78f5d86a:1758880487:847 b43681dd48ca98d9e9a953eef2007e7117f0c3:1758947524:187 @@ -8487,7 +8408,7 @@ f7bb8a2d280fb8e88e786022512db7e79adcf9:1759127596:953 8f6abd55d1d6aba2dd1aaf3435b95dad2739fe:1759454426:252 /share/CACHEDEV1_DATA/Container/dms/.git/objects/29 33bcc02e0b253f5d95300cbf94ecad39fc536c:1758764057:807 -85cca80f03eae3175a3ee94936204141e38b6f:1759215825:429 +85cca80f03eae3175a3ee94936204141e38b6f:1759640179:429 4f4016d05bdd696670c4840f1f36a71f9239de:1759127596:110 df01bd74cf48b97f920f130b4e044cde8b9c33:1759225117:153 c7ff541fcbfbe6f1ab7032d00338dbc8255f64:1759301435:628 @@ -8527,6 +8448,7 @@ c5da24d84fe1141fae620a6ba4a3e879685f7f:1759367539:983 1fa22f37ca4599596b0a14b69e6dae6e1623e4:1759588053:307 9ecd667e78107b4208ea842c34b3b68f7b7590:1759632112:250 3c9a6c6e0b58c0fd5775d1d708d0cf9c1101a6:1759638112:174 +b58b46e61e92e3d20714a38a45a4ea10e0df04:1759640263:869 /share/CACHEDEV1_DATA/Container/dms/.git/objects/dd 120a8400bbacab5a923a7bf31bb92ff0fcc1aa:1758764056:847 0b5719f0dcc299365182ba29f8c1a4a3d38139:1759127596:896 @@ -8582,7 +8504,7 @@ b916e6e2e7b1051035a60ff2f7a23e21c1552d:1759225086:656 492e7b3fcee90aa5f899af1e3611a6503a346f:1758764054:528 a2eac88a233677304c0959f0d18cf5e11cdd9e:1758967105:572 bf3b1aaea6cafff5f7cbea552a604155995af0:1759127593:45454 -b97c91bb57e7d114c175c05b958c031637b11b:1759215825:372 +b97c91bb57e7d114c175c05b958c031637b11b:1759640179:372 c84677fb4fe5e6068d20b97a8bfd9fc3e3b08d:1759140384:176 fa89550feb664559e4921e8e3275977e879189:1759292144:209 1a1f2f735f56d6080fb8505622ecf93b45e66c:1759313722:10925 @@ -8595,9 +8517,9 @@ a59d23d727e084a686682ad7cca74528de2e74:1759127594:761 8c2cae6df121afd35c14ce8067926a74c21e5b:1759225084:1983 bb89e63e182a9ca43cf1ab6c59839a5f181900:1759225117:837 7092e7027fe89d9e60ff47db25ad90c605e620:1759454488:48 -d2179b2d8a21f3ab60bdebad4206b72f34ad22:1759588054:270 +d2179b2d8a21f3ab60bdebad4206b72f34ad22:1759640179:270 /share/CACHEDEV1_DATA/Container/dms/.git/objects/4b -e9dfe20196967a237be7956a3532178096e587:1758764054:287 +e9dfe20196967a237be7956a3532178096e587:1759640179:287 18338b8e59a5b89cf823062a7b1ec3891b4f59:1759127596:952 bc54953c2d531132b67f966342ced700d72849:1759225086:1230 1cf3a1143a38a29ed65058318310c8979e8d12:1759225086:592 @@ -8627,7 +8549,7 @@ d52821e92652a226cf72927835ef0b92cfadaa:1759225084:3302748 /share/CACHEDEV1_DATA/Container/dms/.git/objects/45 2371e95185fade2722d990b1f3b311aec07e0d:1758764053:574 314966b9f6b03266bdec540a6a108de3886ccc:1759109551:88 -dadbed71dc1aa088260d3ccf95273f1453e1ac:1759215825:503 +dadbed71dc1aa088260d3ccf95273f1453e1ac:1759640179:503 d66b78b7c59ce7314d605476283bc0a379310f:1759127594:271 7e4d84ac5c7f697bd0d846674b764652a9eb0c:1759127595:456 781a0d855a0e4615169988b463bdaff43d4589:1759127596:399 @@ -8646,7 +8568,7 @@ f605134d1ef06f6188396ae0ccb82c1ace200b:1759127596:135020 5281f1bf91b1f1a12496d9f0b19a707593191a:1759225086:367 794ed9027f4c3091b3a6d5c588b631a88ee1cc:1759225086:1158 40453222c6d7b865481583b4764d62e09ba153:1759370106:2905 -d8e75ad6b01a467f3532bec223477c2084321b:1759638121:210 +d8e75ad6b01a467f3532bec223477c2084321b:1759640263:210 /share/CACHEDEV1_DATA/Container/dms/.git/objects/ff f45215db248beb3f373e88fa63bff1a58cfcf3:1758764052:1272 65887e4b937954d0cccfac07b09f20058d3d33:1758965251:86 @@ -8679,7 +8601,7 @@ b62af7c8041dfc45eba85a9e6aa56bd903cd72:1759138434:846 c157f1e8c314f4e3d31044b7acafd35e221ead:1759138763:1579 df9feb7d4cd0e4316759e323ba213763690cd1:1759225084:970 077468137424f156c75621fc1bcd7ab295ad5b:1759454426:19858 -ad530361fb0f24d0605f2e6ce0299daf765ed2:1759596867:5010617 +ad530361fb0f24d0605f2e6ce0299daf765ed2:1759640179:5010617 bfd8ce39fe97df93ee129aa8381c50b0173098:1759637884:1078 /share/CACHEDEV1_DATA/Container/dms/.git/objects/38 bc0837eb802ef34bd124fa5560ec249b861d4e:1758764050:848 @@ -8687,7 +8609,7 @@ bc0837eb802ef34bd124fa5560ec249b861d4e:1758764050:848 43dd5f916acca9f73b878321d57f59ae3e3085:1759127594:862 812e68b78bc52a5088cf5b5bea76c1d2e60348:1759138564:404 0d744bfbaddb3cdb460a5c72d20a4e1048d967:1759225086:1933 -f9ae0c4693d0b5feca824cf9b7f061fdcf7167:1759596865:1193 +f9ae0c4693d0b5feca824cf9b7f061fdcf7167:1759640179:1193 /share/CACHEDEV1_DATA/Container/dms/.git/objects/d8 cfce87e49cbbd42049cb0b9c2ec4e0d0c4306f:1758764050:847 c604de1ddcc43059969b21fa1ee9f45c89a9ac:1759109551:165 @@ -8698,6 +8620,7 @@ cb953be6835e5e71e3cb1d874a022bcad9924a:1759225083:3787 e57ace76aea0661ba64ee4749ca2c2ba31d03e:1759308318:214 aeefa76bd279039636cf6ed67a8230eaeddc43:1759313751:153 ac5d53f066e4c2a185c25438aad31ed93129b9:1759588054:41730 +844575bcd9a93c93744c54942df2eb1f064ce8:1759640263:46 /share/CACHEDEV1_DATA/Container/dms/.git/objects/8b 24732b3546e1d5b2d08c4d80a6bbbfd34d0b9d:1758764050:190 f4a45d74d4cc95eeb945bad32bb1d71715ced0:1759109967:86 @@ -8715,7 +8638,7 @@ ab731a8ffa098685f1555b1eddc79a593aac00:1759127596:339 5afae6b5e284d7a99291f0db3607ecd811ab84:1759127596:5329 5e8be4f6014732f97560bafd30fadcda8701ff:1759225086:722 8b8914722abf369fe7fa6396dfa3c38254571e:1759292144:917 -680cba6292d92893d10f45b89e39e05d424071:1759588054:278 +680cba6292d92893d10f45b89e39e05d424071:1759640179:278 7a86e0bd9ba638bb52d6f448862cc13b2cb597:1759588054:5011346 90816fe82a3f9d5645e219904b8292d0f7878f:1759632112:96 /share/CACHEDEV1_DATA/Container/dms/.git/objects/58 @@ -8725,8 +8648,8 @@ a2fc3f5cffb33b7ed1b84f61ab3ad6218e293d:1758764049:181 5b49cf328289696c1270d1835b0dcae041fc88:1759454426:68334 08da1f46f342d3a7d0c89889e4cb554ff648ce:1759454426:20282 d8af65a2b607956785f07d4561c706dba096fa:1759454488:10032 -3f398586956e51557b2662482e82f2a64652cc:1759637114:1399 -cd8d32f07fa65f50e4d6f95075c2a5e085947d:1759638121:518 +3f398586956e51557b2662482e82f2a64652cc:1759640179:1399 +cd8d32f07fa65f50e4d6f95075c2a5e085947d:1759640263:518 /share/CACHEDEV1_DATA/Container/dms/.git/objects/b7 8a95e244aff38534a30447e77bf969c22813ba:1758764047:153 c9690bab3030a40573ee07861a66082810561a:1759127594:580 @@ -8750,24 +8673,24 @@ a970aa459b2885930828c058d871e172dd30ce:1759301407:29120 commit-graph:1758461466:1172 packs:1758461466:54 /share/CACHEDEV1_DATA/Container/dms/.git/objects/pack -pack-b89eab53043a25c5154c2cc603bca7b5f140e69e.pack:1759632130:210055136 +pack-b89eab53043a25c5154c2cc603bca7b5f140e69e.pack:1759640179:210055136 pack-b89eab53043a25c5154c2cc603bca7b5f140e69e.rev:1758461461:141520 pack-b89eab53043a25c5154c2cc603bca7b5f140e69e.idx:1758461461:991348 -pack-42522f9b62e071421cce31c0a1f81a1cb9caed1d.pack:1759596865:492177069 +pack-42522f9b62e071421cce31c0a1f81a1cb9caed1d.pack:1759640179:492177069 pack-42522f9b62e071421cce31c0a1f81a1cb9caed1d.rev:1759550377:4880 pack-42522f9b62e071421cce31c0a1f81a1cb9caed1d.idx:1759550377:34868 -pack-35ac5af8d6730d8394a83a0ef2d2145c1db2c3cd.pack:1759632080:299012 +pack-35ac5af8d6730d8394a83a0ef2d2145c1db2c3cd.pack:1759640263:299012 pack-35ac5af8d6730d8394a83a0ef2d2145c1db2c3cd.rev:1759632080:792 pack-35ac5af8d6730d8394a83a0ef2d2145c1db2c3cd.idx:1759632080:6252 /share/CACHEDEV1_DATA/Container/dms/.git/refs -heads/:1759565383:0 +heads/:1759640620:0 tags/:1758466456:0 remotes/:1758465366:0 /share/CACHEDEV1_DATA/Container/dms/.git/refs/remotes -origin/:1759637114:0 +origin/:1759640280:0 /share/CACHEDEV1_DATA/Container/dms/.git/refs/remotes/origin feature/:1759638132:0 -main:1759637114:41 +main:1759640280:41 HEAD:1758764057:30 /share/CACHEDEV1_DATA/Container/dms/.git/refs/remotes/origin/feature dashboard-update-251004:1759638132:41 @@ -8775,7 +8698,7 @@ dashboard-update-251004:1759638132:41 v0.5.0:1758466456:41 /share/CACHEDEV1_DATA/Container/dms/.git/refs/heads feature/:1759638112:0 -main:1759564681:41 +main:1759640620:41 /share/CACHEDEV1_DATA/Container/dms/.git/refs/heads/feature dashboard-update-251004:1759638112:41 /share/CACHEDEV1_DATA/Container/dms/.git/info diff --git a/7.conf b/7.conf deleted file mode 100644 index 207a4c24..00000000 --- a/7.conf +++ /dev/null @@ -1,257 +0,0 @@ -# ------------------------------------------------------------ -# lcbp3.np-dms.work -# ------------------------------------------------------------ - - - -map $scheme $hsts_header { - https "max-age=63072000; preload"; -} - -server { - set $forward_scheme http; - set $server "dms_frontend"; - set $port 3000; - - listen 80; -listen [::]:80; - -listen 443 ssl; -listen [::]:443 ssl; - - - server_name lcbp3.np-dms.work; - - http2 on; - - - # Let's Encrypt SSL - include conf.d/include/letsencrypt-acme-challenge.conf; - include conf.d/include/ssl-cache.conf; - include conf.d/include/ssl-ciphers.conf; - ssl_certificate /etc/letsencrypt/live/npm-7/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/npm-7/privkey.pem; - - - - - - - - - # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) - add_header Strict-Transport-Security $hsts_header always; - - - - - - # Force SSL - include conf.d/include/force-ssl.conf; - - - - -proxy_set_header Upgrade $http_upgrade; -proxy_set_header Connection $http_connection; -proxy_http_version 1.1; - - - access_log /data/logs/proxy-host-7_access.log proxy; - error_log /data/logs/proxy-host-7_error.log warn; - -# ===== ขนาดไฟล์/timeout ระดับ Host ===== -client_max_body_size 200m; -client_body_timeout 60s; -send_timeout 60s; - -# ===== Proxy headers พื้นฐาน (ส่งให้ backend ทุกตัว) ===== -# ที่นี่จะเป็นที่กำหนด header หลักเพียงแห่งเดียว -proxy_set_header Host $host; -proxy_set_header X-Real-IP $remote_addr; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -proxy_set_header X-Forwarded-Proto $scheme; - -# ===== WebSocket/SSE header ระดับ Host ===== -# พร้อมสำหรับทุก location ที่อาจต้องใช้ WebSocket -proxy_set_header Upgrade $http_upgrade; -proxy_set_header Connection "upgrade"; # ใช้ "upgrade" โดยตรงจะเสถียรกว่า - -# ===== สำคัญสำหรับคุกกี้ HttpOnly (ให้ทุก backend) ===== -proxy_pass_header Set-Cookie; - -# ===== Security headers ระดับ Host (ยอดเยี่ยมมากครับ) ===== -add_header X-Content-Type-Options "nosniff" always; -add_header X-Frame-Options "SAMEORIGIN" always; -add_header Referrer-Policy "no-referrer-when-downgrade" always; -add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - - - location /health { - proxy_set_header Host $host; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -proxy_set_header X-Forwarded-Proto https; -proxy_read_timeout 15s; -proxy_send_timeout 15s; - - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Scheme $scheme; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - - proxy_pass http://dms_frontend:3000; - - - - - - - # Force SSL - include conf.d/include/force-ssl.conf; - - - - - # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) - add_header Strict-Transport-Security $hsts_header always; - - - - - - - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - proxy_http_version 1.1; - - } - - location /_next/static/ { - proxy_set_header Host $host; -add_header Cache-Control "public, max-age=31536000, immutable"; - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Scheme $scheme; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - - proxy_pass http://dms_frontend:3000; - - - - - - - # Force SSL - include conf.d/include/force-ssl.conf; - - - - - # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) - add_header Strict-Transport-Security $hsts_header always; - - - - - - - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - proxy_http_version 1.1; - - } - - location /api/ { - # ===== CORS Configuration ===== -set $cors_allow_origin ""; -if ($http_origin ~* "^https?://(localhost(:\\d+)?|127\\.0\\.0\\.1(:\\d+)?|np-dms\\.work|www\\.np-dms\\.work|lcbp3\\.np-dms\\.work)$") { - set $cors_allow_origin $http_origin; -} - -add_header Vary "Origin" always; -add_header Access-Control-Allow-Credentials "true" always; -add_header Access-Control-Allow-Origin $cors_allow_origin always; -add_header Access-Control-Expose-Headers "Content-Disposition,Content-Length" always; -add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always; -add_header Access-Control-Allow-Headers "Authorization, Content-Type, Accept, Origin, Referer, User-Agent, X-Requested-With, Cache-Control, Pragma" always; - -# ===== OPTIONS preflight ===== -if ($request_method = OPTIONS) { - add_header Content-Length 0; - add_header Content-Type text/plain; - add_header Access-Control-Allow-Origin $cors_allow_origin always; - add_header Access-Control-Allow-Credentials "true" always; - add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always; - add_header Access-Control-Allow-Headers "Authorization, Content-Type, Accept, Origin, Referer, User-Agent, X-Requested-With, Cache-Control, Pragma" always; - return 204; -} - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Scheme $scheme; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - - proxy_pass http://dms_backend:3001; - - - - - - - # Force SSL - include conf.d/include/force-ssl.conf; - - - - - # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) - add_header Strict-Transport-Security $hsts_header always; - - - - - - - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - proxy_http_version 1.1; - - } - - - - - - location / { - - - - - - # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) - add_header Strict-Transport-Security $hsts_header always; - - - - - - - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - proxy_http_version 1.1; - - - # Proxy! - include conf.d/include/proxy.conf; - } - - - # Custom - include /data/nginx/custom/server_proxy[.]conf; -} - diff --git a/Architech.md b/Architech.md new file mode 100755 index 00000000..b38aa1fa --- /dev/null +++ b/Architech.md @@ -0,0 +1,506 @@ +# DMS Architecture Deep Dive (Backend + Frontend) + +**Project:** Document Management System (DMS) — LCBP3 +**Platform:** QNAP TS‑473A (Container Station) +**Last updated:** 2025‑10‑07 (UTC+7) + +--- + +## 0) TL;DR (Executive Summary) + +* Reverse proxy (Nginx/NPM) เผยแพร่ Frontend (Next.js) และ Backend (Node.js/Express) ผ่าน HTTPS (HSTS) +* Backend เชื่อม MariaDB 10.11 (ข้อมูลหลัก DMS) และแยก n8n + Postgres 16 สำหรับ workflow +* RBAC/ABAC ถูกบังคับใช้งานใน middleware + มีชุด SQL (tables → triggers → procedures → views → seed) +* ไฟล์จริง (PDF/DWG) เก็บนอก webroot ที่ **/share/dms‑data** พร้อมมาตรฐานการตั้งชื่อ+โฟลเดอร์ +* Dev/Prod แยกชัดเจนผ่าน Docker multi‑stage + docker‑compose + โฟลเดอร์ persist logs/config/certs + +--- + +## 1) Runtime Topology & Trust Boundaries + +``` +Internet Clients (Browser) + │ HTTPS 443 (HSTS) [QNAP mgmt = 8443] + ▼ +┌─────────────────────────────────────────────────────┐ +│ Reverse Proxy Layer │ +│ ├─ Nginx (Alpine) or Nginx Proxy Manager (NPM) │ +│ ├─ TLS (LE cert; SAN multi‑subdomain) │ +│ └─ Routes: │ +│ • /, /_next/* → Frontend (Next.js :3000) │ +│ • /api/* → Backend (Express :3001) │ +│ • /pma/* → phpMyAdmin │ +│ • /n8n/* → n8n (Workflows) │ +└─────────────────────────────────────────────────────┘ + │ │ + │ └──────────┐ + ▼ │ + Frontend (Next.js) │ + │ Cookie-based Auth (HttpOnly) │ + ▼ ▼ + Backend (Node/Express ESM) ─────────► MariaDB 10.11 + │ │ + └────────────────────────────────────┘ + Project data (.pdf/.dwg) @ /share/dms-data + + n8n (workflows) ──► Postgres 16 (separate DB for automations) +``` + +**Trust Boundaries** + +* Public zone: Internet ↔ Reverse proxy +* App zone: Reverse proxy ↔ FE/BE containers (internal Docker network) +* Data zone: Backend ↔ Databases (MariaDB, Postgres) + `/share/dms-data` + +--- + +## 2) Frontend Architecture (Next.js / React) + +### 2.1 Stack & Key libs + +* **Next.js (App Router)**, **React**, ESM +* **Tailwind CSS**, **PostCSS**, **shadcn/ui** (components.json) +* Fetch API (credentials include) → Cookie Auth (HttpOnly) + +### 2.2 Directory Layout + +``` +/frontend/ +├─ app/ +│ ├─ login/ +│ ├─ dashboard/ +│ ├─ users/ +│ ├─ correspondences/ +│ ├─ health/ +│ └─ layout.tsx / page.tsx (ตาม App Router) +├─ public/ +├─ Dockerfile (multi-stage: dev/prod) +├─ package.json +├─ next.config.js +└─ ... +``` + +### 2.3 Routing & Layouts + +* **Public**: `/login`, `/health` +* **Protected**: `/dashboard`, `/users`, `/correspondences`, ... (client-side guard) +* เก็บ **middleware.ts (ของเดิม)** เพื่อหลีกเลี่ยง regression; ใช้ client‑guard + server action อย่างระมัดระวัง + +### 2.4 Auth Flow (Cookie-based) + +1. ผู้ใช้ submit form `/login` → `POST /api/auth/login` (Backend) +2. Backend set **HttpOnly** cookie (JWT) + `SameSite=Lax/Strict`, `Secure` +3. หน้า protected เรียก `GET /api/auth/me` เพื่อตรวจสอบสถานะ +4. หาก 401 → redirect → `/login` + +> **CORS/Fetch**: เปิด `credentials: 'include'` ทุกครั้ง, ตั้ง `NEXT_PUBLIC_API_BASE` เป็น origin ของ backend ผ่าน proxy (เช่น `https://lcbp3.np-dms.work`) + +### 2.5 UI/UX + +* Sea‑blue palette, sidebar พับได้, card‑based KPI +* ตารางข้อมูลเตรียมรองรับ **server‑side DataTables** +* shadcn/ui: Button, Card, Badge, Tabs, Dropdown, Tooltip, Switch, etc. + +### 2.6 Config & ENV + +* `NEXT_PUBLIC_API_BASE` (ex: `https://lcbp3.np-dms.work`) +* Build output แยก dev/prod; ระวัง EACCES บน QNAP → ใช้ user `node` + ปรับสิทธิ์โวลุ่ม `.next/*` + +### 2.7 Error Handling & Observability (FE) + +* Global error boundary (app router) + toast/alert patterns +* Network layer: แยก handler สำหรับ 401/403/500 + retry/backoff ที่จำเป็น +* Metrics (optional): web‑vitals, UX timing (เก็บฝั่ง n8n หรือ simple logging) + +--- + +## 3) Backend Architecture (Node.js ESM / Express) + +### 3.1 Stack & Structure + +* Node 20.x, **ESM** modules, **Express** +* `mysql2/promise`, `jsonwebtoken`, `cookie-parser`, `cors`, `helmet`, `winston/morgan` + +```tree +/backend/ +├─ src/ +│ ├─ index.js # bootstrap server, CORS, cookies, health +│ ├─ routes/ +│ │ ├─ auth.js # /api/auth/* (login, me, logout) +│ │ ├─ users.js # /api/users/* +│ │ ├─ correspondences.js # /api/correspondences/* +│ │ ├─ drawings.js # /api/drawings/* +│ │ ├─ rfas.js # /api/rfas/* +│ │ └─ transmittals.js # /api/transmittals/* +│ ├─ middleware/ +│ │ ├─ authGuard.js # verify JWT from cookie +│ │ ├─ requirePermission.js# RBAC/ABAC enforcement +│ │ ├─ errorHandler.js +│ │ └─ requestLogger.js +│ ├─ db/ +│ │ ├─ pool.js # createPool, sane defaults +│ │ └─ models/ # query builders (User, Drawing, ...) +│ ├─ utils/ +│ │ ├─ hash.js (bcrypt/argon2) +│ │ ├─ jwt.js +│ │ ├─ pagination.js +│ │ └─ responses.js +│ └─ config/ +│ └─ index.js # env, constants +├─ Dockerfile +└─ package.json +``` + +### 3.2 Request Lifecycle + +1. `helmet` + `cors` (allow specific origin; credentials true) +2. `cookie-parser`, `json limit` (e.g., 2MB) +3. `requestLogger` → trace + response time +4. Route handler → `authGuard` (protected) → `requirePermission` (per‑route) → Controller +5. Error bubbles → `errorHandler` (JSON shape, status map) + +### 3.3 Auth & RBAC/ABAC + +* **JWT** ใน HttpOnly cookie; Claims: `sub` (user_id), `roles`, `exp` +* **authGuard**: ตรวจ token → แนบ `req.user` +* **requirePermission**: เช็ค permission ตามเส้นทาง/วิธี; แผนขยาย ABAC (เช่น project scope, owner, doc state) +* Roles/Permissions ถูก seed ใน SQL; มี **view เมทริกซ์** เพื่อ debug (เช่น `v_role_permission_matrix`) + +**ตัวอย่าง pseudo** `requirePermission(permission)` + +```js +export const requirePermission = (perm) => async (req, res, next) => { + if (!req.user) return res.status(401).json({ error: 'Unauthenticated' }); + const ok = await checkPermission(req.user.user_id, perm, req.context); + if (!ok) return res.status(403).json({ error: 'Forbidden' }); + return next(); +}; +``` + +### 3.4 Database Access & Pooling + +* `createPool({ connectionLimit: 10~25, queueLimit: 0, waitForConnections: true })` +* ใช้ parameterized queries เสมอ; ปรับ `sql_mode` ที่จำเป็นใน `my.cnf` + +### 3.5 File Storage & Secure Download + +* Root: **/share/dms‑data** +* โครงโฟลเดอร์: `{module}/{yyyy}/{mm}/{entityId}/` + ชื่อไฟล์ตามมาตรฐาน (เช่น `DRW--REV-.pdf`) +* Endpoint download: ตรวจสิทธิ์ (RBAC/ABAC) → `res.sendFile()`/stream; ป้องกัน path traversal +* MIME allowlist + size limit + virus scan (optional; ภายหลัง) + +### 3.6 Health & Readiness + +* `GET /api/health` → `{ ok: true }` +* (optional) `/api/ready` ตรวจ DB ping + disk space (dms‑data) + +### 3.7 Config & ENV (BE) + +* `DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME` +* `JWT_SECRET, COOKIE_NAME, COOKIE_SAMESITE, COOKIE_SECURE` +* `CORS_ORIGIN, LOG_LEVEL, APP_BASE_URL` +* `FILE_ROOT=/share/dms-data` + +### 3.8 Logging + +* Access log (morgan) + App log (winston) → `/share/Container/dms/logs/backend/` +* รูปแบบ JSON (timestamp, level, msg, reqId) + daily rotation (logrotate/container‑side) + +--- + +## 4) Database (MariaDB 10.11) + +### 4.1 Schema Overview (ย่อ) + +* **RBAC core**: `users`, `roles`, `permissions`, `user_roles`, `role_permissions` +* **Domain**: `drawings`, `contracts`, `correspondences`, `rfas`, `transmittals`, `organizations`, `projects`, ... +* **Audit**: `audit_logs` (แผนขยาย), `deleted_at` (soft delete, แผนงาน) + +``` +[users]────[roles]────[permissions] + │ + └── activities/audit_logs (future expansion) + +[drawings]────[contracts] +[rfas]────[drawings] +[correspondences] (internal/external flag) +``` + +### 4.2 Init SQL Pipeline + +1. `01_*_deploy_table_rbac.sql` — สร้างตารางหลักทั้งหมด + RBAC +2. `02_*_triggers.sql` — บังคับ data rules, auto‑audit fields +3. `03_*_procedures_handlers.sql` — upsert/bulk handlers (เช่น `sp_bulk_import_contract_dwg`) +4. `04_*_views.sql` — รายงาน/เมทริกซ์สิทธิ์ (`v_role_permission_matrix`, etc.) +5. `05_*_seed_data.sql` — ค่าพื้นฐาน domain (project, categories, statuses) +6. `06_*_seed_users.sql` — บัญชีเริ่มต้น (superadmin, editors, viewers) +7. `07_*_seed_contract_dwg.sql` — ข้อมูลตัวอย่างแบบสัญญา + +### 4.3 Indexing & Performance + +* Composite indexes ตามคอลัมน์ filter/sort (เช่น `(project_id, updated_at DESC)`) +* Full‑text index (optional) สำหรับ advanced search +* Query plan review (EXPLAIN) + เพิ่ม covering index ตามรายงาน + +### 4.4 MySQL/MariaDB Config (my.cnf — แนวทาง) + +``` +[mysqld] +innodb_buffer_pool_size = 4G # ปรับตาม RAM/QNAP +innodb_log_file_size = 512M +innodb_flush_log_at_trx_commit = 1 +max_connections = 200 +sql_mode = STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci +``` + +> ปรับค่าให้เหมาะกับ workload จริง + เฝ้าดู IO/CPU ของ QNAP + +### 4.5 Backup/Restore + +* Logical backup: `mysqldump --routines --triggers --single-transaction` +* Physical (snapshot QNAP) + schedule ผ่าน n8n/cron +* เก็บสำเนา off‑NAS (encrypted) + +--- + +## 5) Reverse Proxy & TLS + +### 5.1 Nginx (Alpine) — ตัวอย่าง server block + +> **สำคัญ:** บนสภาพแวดล้อมนี้ ให้ใช้คนละบรรทัด: +> `listen 443 ssl;` +> `http2 on;` +> หลีกเลี่ยง `listen 443 ssl http2;` + +```nginx +server { + listen 80; + server_name lcbp3.np-dms.work; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + http2 on; + server_name lcbp3.np-dms.work; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + add_header Strict-Transport-Security "max-age=63072000; preload" always; + + # Frontend + location / { + proxy_pass http://frontend:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Next.js static + location /_next/ { + proxy_pass http://frontend:3000; + } + + # Backend API + location /api/ { + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_pass http://backend:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # phpMyAdmin (sub-path) + location /pma/ { + proxy_pass http://phpmyadmin:80/; + } + + # n8n + location /n8n/ { + proxy_pass http://n8n:5678/; + } +} +``` + +### 5.2 Nginx Proxy Manager (NPM) — Tips + +* ระวังอย่าใส่ `proxy_http_version` ซ้ำซ้อน (duplicate directive) ใน Advanced +* ถ้าต้องแก้ไฟล์ด้านใน NPM → ระวังไฟล์ใน `/data/nginx/proxy_host/*.conf` +* จัดการ certificate / SAN หลาย sub‑domain ใน UI แต่ mainten ดีเรื่อง symlink/renew + +### 5.3 TLS & Certificates + +* Let’s Encrypt (HTTP‑01 webroot/standalone) + HSTS +* QNAP mgmt เปลี่ยนเป็น 8443 → พอร์ต 443 public ว่างสำหรับ Nginx/NPM + +--- + +## 6) Docker Compose Topology + +### 6.1 Services (สรุป) + +* `frontend` (Next.js) :3000 +* `backend` (Express) :3001 +* `mariadb` (10.11) :3306 (internal) +* `phpmyadmin` :80 (internal) +* `nginx` or `npm` :80/443 (published) +* `n8n` :5678 (internal) +* `postgres_n8n` (16-alpine) +* `pgadmin4` + +### 6.2 Volumes & Paths + +``` +/share/Container/dms/ +├─ mariadb/data +├─ mariadb/init/*.sql +├─ backend/ (code) +├─ frontend/ (code) +├─ phpmyadmin/{sessions,tmp,config.user.inc.php} +├─ nginx/{nginx.conf,dms.conf,certs/} +├─ n8n, n8n-postgres, n8n-cache +└─ logs/{backend,frontend,nginx,pgadmin,phpmyadmin,postgres_n8n} +/share/dms-data (pdf/dwg storage) +``` + +### 6.3 Healthchecks (suggested) + +* **backend**: curl `http://localhost:3001/api/health` +* **frontend**: curl `/health` (simple JSON) +* **mariadb**: `mysqladmin ping` with credentials +* **nginx**: `nginx -t` at startup + +### 6.4 Security Hardening + +* รัน container ด้วย user non‑root (`user: node` สำหรับ FE/BE) +* จำกัด capabilities; read‑only FS (ยกเว้นโวลุ่มจำเป็น) +* เฉพาะ backend เมานต์ `/share/dms-data` + +--- + +## 7) Observability, Ops, and Troubleshooting + +### 7.1 Logs + +* Frontend → `/logs/frontend/*` +* Backend → `/logs/backend/*` (app/access/error) +* Nginx/NPM → `/logs/nginx/*` +* MariaDB → default datadir log + slow query (เปิดใน my.cnf หากต้องการ) + +### 7.2 Common Issues & Playbooks + +* **401 Unauthenticated**: ตรวจ `authGuard` → JWT cookie มี/หมดอายุ → เวลา server/FE sync → CORS `credentials: true` +* **EACCES Next.js**: สิทธิ์ `.next/*` + run as `node`, โวลุ่ม map ถูก user:group +* **NPM duplicate directive**: ลบซ้ำ `proxy_http_version` ใน Advanced / ตรวจ `proxy_host/*.conf` +* **LE cert path/symlink**: ตรวจ `/etc/letsencrypt/live/npm-*` symlink ชี้ถูก +* **DB field not found**: ตรวจ schema vs code (migration/init SQL) → sync ให้ตรง + +### 7.3 Performance Guides + +* **Backend**: keep‑alive, gzip/deflate at proxy, pool 10–25, paginate, avoid N+1 +* **Frontend**: prefetch critical routes, cache static, image optimization +* **DB**: เพิ่ม index จุด filter, analyze query (EXPLAIN), ปรับ buffer pool + +--- + +## 8) Security & Compliance + +* **HTTPS only** + HSTS (preload) +* **CORS**: allow list เฉพาะ FE origin; `Access-Control-Allow-Credentials: true` +* **Cookie**: HttpOnly, Secure, SameSite=Lax/Strict +* **Input Validation**: celebrate/zod (optional) + sanitize +* **Rate limiting**: per IP/route (optional) +* **AuditLog**: วางแผนเพิ่ม ครอบคลุม CRUD + mapping (actor, action, entity, before/after) +* **Backups**: DB + `/share/dms-data` + config (encrypted off‑NAS) + +--- + +## 9) Backlog → Architecture Mapping + +1. **RBAC Enforcement ครบ** → เติม `requirePermission` ทุก route + test matrix ผ่าน view +2. **AuditLog ครบ CRUD/Mapping** → trigger + table `audit_logs` + BE hook +3. **Upload/Download จริงของ Drawing Revisions** → BE endpoints + virus scan (optional) +4. **Dashboard KPI** → BE summary endpoints + FE cards/charts +5. **Server‑side DataTables** → paging/sort/filter + indexesรองรับ +6. **รายงาน Export CSV/Excel/PDF** → BE export endpoints + FE buttons +7. **Soft delete** (`deleted_at`) → BE filter default scope + restore endpoint +8. **Validation เข้ม** → celebrate/zod schema + consistent error shape +9. **Indexing/Perf** → slow query log + EXPLAIN review +10. **Job/Cron Deadline Alerts** → n8n schedule + SMTP + +--- + +## 10) Port & ENV Matrix (Quick Ref) + +| Component | Ports | Key ENV | +| --------- | --------------- | ------------------------------------------------ | +| Nginx/NPM | 80/443 (public) | SSL paths, HSTS | +| Frontend | 3000 (internal) | `NEXT_PUBLIC_API_BASE` | +| Backend | 3001 (internal) | `DB_*`, `JWT_SECRET`, `CORS_ORIGIN`, `FILE_ROOT` | +| MariaDB | 3306 (internal) | `MY_CNF`, credentials | +| n8n | 5678 (internal) | `N8N_*`, webhook URL under `/n8n/` | +| Postgres | 5432 (internal) | n8n DB | + +**QNAP mgmt**: 8443 (already moved) + +--- + +## 11) Sample Snippets + +### 11.1 Backend CORS (credentials) + +```js +app.use(cors({ + origin: ['https://lcbp3.np-dms.work'], + credentials: true, +})); +``` + +### 11.2 Secure Download (guarded) + +```js +router.get('/files/:module/:id/:filename', authGuard, requirePermission('file.read'), async (req, res) => { + const { module, id, filename } = req.params; + // 1) ABAC: verify user can access this module/entity + const ok = await canReadFile(req.user.user_id, module, id); + if (!ok) return res.status(403).json({ error: 'Forbidden' }); + + const abs = path.join(FILE_ROOT, module, id, filename); + if (!abs.startsWith(FILE_ROOT)) return res.status(400).json({ error: 'Bad path' }); + return res.sendFile(abs); +}); +``` + +### 11.3 Healthcheck + +```js +router.get('/health', (req, res) => res.json({ ok: true })); +``` + +--- + +## 12) Deployment Workflow (Suggested) + +1. Git (Gitea) branch strategy `feature/*` → PR → main +2. Build images (dev/prod) via Dockerfile multi‑stage; pin Node/MariaDB versions +3. `docker compose up -d --build` จาก `/share/Container/dms` +4. Validate: `/health`, `/api/health`, login roundtrip +5. Monitor logs + baseline perf; run SQL smoke tests (views/triggers/procs) + +--- + +## 13) Appendix + +* **Naming conventions**: snake_case DB, camelCase JS +* **Timezones**: store UTC in DB; display in app TZ (+07:00) +* **Character set**: UTF‑8 (`utf8mb4_unicode_ci`) +* **Large file policy**: size limit (e.g., 50–200MB), allowlist extensions +* **Retention**: archive strategy for old revisions (optional) + +--- + +> หากต้องการ เวอร์ชัน **README.md พร้อมโค้ดตัวอย่าง compose/nginx** จัดรูปแบบให้นำไปวางใน repo ได้ทันที แจ้งได้เลยว่าจะให้แตกไฟล์เป็น `/docs/Architecture.md` + `/nginx/dms.conf` + `/docker-compose.yml` template หรือรูปแบบอื่นที่สะดวกต่อการใช้งานของทีม diff --git a/Bearer-Token.patch.diff b/Bearer-Token.patch.diff deleted file mode 100644 index 9e3b929e..00000000 --- a/Bearer-Token.patch.diff +++ /dev/null @@ -1,483 +0,0 @@ -diff --git a/backend/src/middleware/requireBearer.js b/backend/src/middleware/requireBearer.js -new file mode 100644 -index 0000000..1111111 ---- /dev/null -+++ b/backend/src/middleware/requireBearer.js -@@ -0,0 +1,44 @@ -+// backend/src/middleware/requireBearer.js -+import jwt from "jsonwebtoken"; -+import { findUserById } from "../db/models/users.js"; -+ -+export async function requireBearer(req, res, next) { -+ const hdr = req.get("Authorization") || ""; -+ const m = hdr.match(/^Bearer\s+(.+)$/i); -+ if (!m) return res.status(401).json({ error: "Unauthenticated" }); -+ try { -+ const payload = jwt.verify(m[1], process.env.JWT_ACCESS_SECRET, { -+ issuer: "dms-backend", -+ }); -+ const user = await findUserById(payload.user_id); -+ if (!user) return res.status(401).json({ error: "Unauthenticated" }); -+ req.user = { -+ user_id: user.user_id, -+ username: user.username, -+ email: user.email, -+ first_name: user.first_name, -+ last_name: user.last_name, -+ }; -+ next(); -+ } catch { -+ return res.status(401).json({ error: "Unauthenticated" }); -+ } -+} -diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js -index 2222222..3333333 100644 ---- a/backend/src/routes/auth.js -+++ b/backend/src/routes/auth.js -@@ -1,99 +1,109 @@ --// (เดิม) ผูกกับคุกกี้ / ส่ง ok:true ฯลฯ -+// backend/src/routes/auth.js — Bearer Token ล้วน - import { Router } from "express"; - import jwt from "jsonwebtoken"; --import { findUserByUsername } from "../db/models/users.js"; -+import { findUserByUsername, findUserById } from "../db/models/users.js"; - import { verifyPassword } from "../utils/passwords.js"; --// NOTE: ลบการใช้งาน res.cookie(...) ทั้งหมด -+// NOTE: ไม่มีการใช้ res.cookie(...) อีกต่อไป - - const router = Router(); - - function signAccessToken(user) { - return jwt.sign( - { user_id: user.user_id, username: user.username }, - process.env.JWT_ACCESS_SECRET, -- { issuer: "dms-backend", expiresIn: "30m" } // ปรับได้ -+ { issuer: "dms-backend", expiresIn: "30m" } - ); - } - function signRefreshToken(user) { - return jwt.sign( -- { user_id: user.user_id, username: user.username }, -+ { user_id: user.user_id, username: user.username, t: "refresh" }, - process.env.JWT_REFRESH_SECRET, - { issuer: "dms-backend", expiresIn: "30d" } - ); - } - - router.post("/login", async (req, res) => { - const { username, password } = req.body || {}; - const user = await findUserByUsername(username); - if (!user || !(await verifyPassword(password, user.password_hash))) { - return res.status(401).json({ error: "INVALID_CREDENTIALS" }); - } - const token = signAccessToken(user); - const refresh_token = signRefreshToken(user); - return res.json({ - token, - refresh_token, - user: { - user_id: user.user_id, - username: user.username, - email: user.email, - first_name: user.first_name, - last_name: user.last_name, - }, - }); - }); - -+router.post("/refresh", async (req, res) => { -+ const hdr = req.get("Authorization") || ""; -+ const m = hdr.match(/^Bearer\s+(.+)$/i); -+ const r = m?.[1]; -+ if (!r) return res.status(401).json({ error: "NO_REFRESH_TOKEN" }); -+ try { -+ const payload = jwt.verify(r, process.env.JWT_REFRESH_SECRET, { -+ issuer: "dms-backend", -+ }); -+ const user = await findUserById(payload.user_id); -+ if (!user) return res.status(401).json({ error: "USER_NOT_FOUND" }); -+ const token = signAccessToken(user); -+ return res.json({ token }); -+ } catch { -+ return res.status(401).json({ error: "INVALID_REFRESH_TOKEN" }); -+ } -+}); -+ - export default router; -diff --git a/backend/src/index.js b/backend/src/index.js -index 4444444..5555555 100644 ---- a/backend/src/index.js -+++ b/backend/src/index.js -@@ -1,60 +1,69 @@ - import express from "express"; - import cors from "cors"; - import authRouter from "./routes/auth.js"; -+import { requireBearer } from "./middleware/requireBearer.js"; --// import routers อื่น ๆ ตามจริง เช่น rfasRouter, transmittalsRouter - - const app = express(); - --// CORS เดิม (อาจมี credentials) --app.use(cors({ -- origin: true, -- credentials: true, --})); -+// ✅ CORS สำหรับ Bearer: ไม่ต้อง credentials, อนุญาต Authorization header -+app.use(cors({ -+ origin: [ -+ "https://lcbp3.np-dms.work", -+ "http://localhost:3000" -+ ], -+ methods: ["GET","POST","PUT","PATCH","DELETE","OPTIONS"], -+ allowedHeaders: ["Authorization","Content-Type","Accept","Origin","Referer","User-Agent","X-Requested-With","Cache-Control","Pragma"], -+ exposedHeaders: ["Content-Disposition","Content-Length"] -+})); - - app.use(express.json()); - --// routes เดิม --app.use("/api/auth", authRouter); --// app.use("/api/rfas", rfasRouter); --// app.use("/api/transmittals", transmittalsRouter); -+// ✅ เส้นทาง auth (ไม่ต้องมี token) -+app.use("/api/auth", authRouter); -+ -+// ✅ ตั้ง guard สำหรับเส้นทางที่เหลือต้องล็อกอิน -+app.use("/api", requireBearer); -+// แล้วค่อย mount routers protected ใต้ /api -+// app.use("/api/rfas", rfasRouter); -+// app.use("/api/transmittals", transmittalsRouter); - - app.use((err, _req, res, _next) => { - console.error(err); - res.status(500).json({ error: "INTERNAL_SERVER_ERROR" }); - }); - - const port = process.env.PORT || 4000; - app.listen(port, () => console.log(`backend listening on :${port}`)); -diff --git a/frontend/app/(auth)/login/page.jsx b/frontend/app/(auth)/login/page.jsx -index 6666666..7777777 100644 ---- a/frontend/app/(auth)/login/page.jsx -+++ b/frontend/app/(auth)/login/page.jsx -@@ -1,200 +1,236 @@ - // File: frontend/app/(auth)/login/page.jsx - "use client"; - --// เวอร์ชันเดิม -+// ✅ Bearer-only + Debug toggle (NEXT_PUBLIC_DEBUG_AUTH) - import { useState, useMemo, Suspense } from "react"; - import { useSearchParams, useRouter } from "next/navigation"; - import { - Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, - } from "@/components/ui/card"; - import { Label } from "@/components/ui/label"; - import { Input } from "@/components/ui/input"; - import { Button } from "@/components/ui/button"; - import { Alert, AlertDescription } from "@/components/ui/alert"; - --const API_BASE = process.env.NEXT_PUBLIC_API_BASE?.replace(/\/$/, "") || ""; -+const API_BASE = process.env.NEXT_PUBLIC_API_BASE?.replace(/\/$/, "") || ""; -+const DEBUG = -+ String(process.env.NEXT_PUBLIC_DEBUG_AUTH || "").trim() !== "" && -+ process.env.NEXT_PUBLIC_DEBUG_AUTH !== "0" && -+ process.env.NEXT_PUBLIC_DEBUG_AUTH !== "false"; -+function dlog(...args) { -+ if (DEBUG && typeof window !== "undefined") console.debug("[login]", ...args); -+} - - function LoginForm() { - const router = useRouter(); - const searchParams = useSearchParams(); - const nextPath = useMemo( - () => searchParams.get("next") || "/dashboard", - [searchParams] - ); - - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [showPw, setShowPw] = useState(false); - const [remember, setRemember] = useState(false); - const [submitting, setSubmitting] = useState(false); - const [err, setErr] = useState(""); - - async function onSubmit(e) { - e.preventDefault(); - setErr(""); - if (!username.trim() || !password) { - setErr("กรอกชื่อผู้ใช้และรหัสผ่านให้ครบ"); - return; - } - try { - setSubmitting(true); -+ dlog("API_BASE =", API_BASE || "(empty → relative)"); -+ dlog("nextPath =", nextPath, "remember =", remember); - - const res = await fetch(`${API_BASE}/api/auth/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, password }), - cache: "no-store", - }); -- const data = await res.json().catch(() => ({})); -+ dlog("response.status =", res.status); -+ dlog("response.headers.content-type =", res.headers.get("content-type")); -+ let data = {}; -+ try { data = await res.json(); } catch (e) { dlog("response.json() error =", e); } -+ dlog("response.body =", data); - - if (!res.ok) { -- setErr(data?.error || "เข้าสู่ระบบไม่สำเร็จ"); -+ const msg = -+ data?.error === "INVALID_CREDENTIALS" -+ ? "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง" -+ : data?.error || `เข้าสู่ระบบไม่สำเร็จ (HTTP ${res.status})`; -+ dlog("login FAILED →", msg); -+ setErr(msg); - return; - } -+ if (!data?.token) { -+ dlog("login FAILED → data.token not found"); -+ setErr("รูปแบบข้อมูลตอบกลับไม่ถูกต้อง (ไม่มี token)"); -+ return; -+ } - - const storage = remember ? window.localStorage : window.sessionStorage; - storage.setItem("dms.token", data.token); - storage.setItem("dms.refresh_token", data.refresh_token); - storage.setItem("dms.user", JSON.stringify(data.user || {})); -+ dlog("token stored in", remember ? "localStorage" : "sessionStorage"); - - try { - window.dispatchEvent( - new StorageEvent("storage", { key: "dms.auth", newValue: "login" }) - ); - } catch {} -- router.replace(nextPath); -+ dlog("navigating →", nextPath); -+ router.replace(nextPath); - } catch (e) { -+ dlog("exception =", e); - setErr("เชื่อมต่อเซิร์ฟเวอร์ไม่ได้ กรุณาลองใหม่"); - } finally { - setSubmitting(false); -+ dlog("done"); - } - } - - return ( -
- - - เข้าสู่ระบบ - Document Management System • LCBP3 - - - {err ? ( - {err} - ) : null} -
-
- - setUsername(e.target.value)} placeholder="เช่น superadmin" disabled={submitting}/> -
-
- -
- setPassword(e.target.value)} placeholder="••••••••" - disabled={submitting} className="pr-10"/> - -
-
-
- - ลืมรหัสผ่าน? -
- -+ {DEBUG ? ( -+

-+ DEBUG: NEXT_PUBLIC_API_BASE = {API_BASE || "(empty)"} -+

-+ ) : null} -
-
- - © {new Date().getFullYear()} np-dms.work - -
-
- ); - } - - export default function LoginPage() { - return ( - }> - - - ); - } - - function LoginPageSkeleton() { - return ( -
- - - เข้าสู่ระบบ - Document Management System • LCBP3 - - -
-
-
-
-
-
-
-
- ); - } - - function Spinner() { - return ( - - ); - } -diff --git a/frontend/app/(protected)/layout.jsx b/frontend/app/(protected)/layout.jsx -new file mode 100644 -index 0000000..8888888 ---- /dev/null -+++ b/frontend/app/(protected)/layout.jsx -@@ -0,0 +1,38 @@ -+"use client"; -+import { useEffect, useState } from "react"; -+import { usePathname, useRouter } from "next/navigation"; -+ -+export default function ProtectedLayout({ children }) { -+ const router = useRouter(); -+ const pathname = usePathname(); -+ const [ready, setReady] = useState(false); -+ -+ useEffect(() => { -+ try { -+ const token = -+ (typeof window !== "undefined" && -+ (localStorage.getItem("dms.token") || -+ sessionStorage.getItem("dms.token"))) || -+ null; -+ if (!token) { -+ const next = encodeURIComponent(pathname || "/dashboard"); -+ router.replace(`/login?next=${next}`); -+ return; -+ } -+ } finally { -+ setReady(true); -+ } -+ }, [pathname, router]); -+ -+ if (!ready) { -+ return ( -+
-+ กำลังตรวจสิทธิ์… -+
-+ ); -+ } -+ return <>{children}; -+} -diff --git a/frontend/lib/api.js b/frontend/lib/api.js -new file mode 100644 -index 0000000..9999999 ---- /dev/null -+++ b/frontend/lib/api.js -@@ -0,0 +1,45 @@ -+// frontend/lib/api.js -+const API_BASE = process.env.NEXT_PUBLIC_API_BASE?.replace(/\/$/, "") || ""; -+ -+function getToken() { -+ if (typeof window === "undefined") return null; -+ return localStorage.getItem("dms.token") || sessionStorage.getItem("dms.token"); -+} -+ -+export async function apiFetch(path, options = {}) { -+ const token = getToken(); -+ const headers = new Headers(options.headers || {}); -+ headers.set("Accept", "application/json"); -+ if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json"); -+ if (token) headers.set("Authorization", `Bearer ${token}`); -+ -+ const res = await fetch(`${API_BASE}${path}`, { ...options, headers, cache: "no-store" }); -+ -+ if (res.status === 401) { -+ const refresh = -+ localStorage.getItem("dms.refresh_token") || sessionStorage.getItem("dms.refresh_token"); -+ if (refresh) { -+ const r = await fetch(`${API_BASE}/api/auth/refresh`, { -+ method: "POST", -+ headers: { Authorization: `Bearer ${refresh}` }, -+ }); -+ if (r.ok) { -+ const { token: newToken } = await r.json(); -+ const store = localStorage.getItem("dms.refresh_token") ? localStorage : sessionStorage; -+ store.setItem("dms.token", newToken); -+ const headers2 = new Headers(headers); -+ headers2.set("Authorization", `Bearer ${newToken}`); -+ return fetch(`${API_BASE}${path}`, { ...options, headers: headers2, cache: "no-store" }); -+ } -+ } -+ } -+ return res; -+} -diff --git a/frontend/middleware.ts b/frontend/middleware.ts -index aaaaaaa..bbbbbbb 100644 ---- a/frontend/middleware.ts -+++ b/frontend/middleware.ts -@@ -1,15 +1,14 @@ --import { NextResponse } from "next/server"; --import type { NextRequest } from "next/server"; -- --// เดิม: ตรวจคุกกี้แล้ว redirect /dashboard --export function middleware(req: NextRequest) { -- // ... logic เดิมที่ใช้คุกกี้ -- return NextResponse.next(); --} -- --export const config = { -- matcher: ["/(protected/:path*)","/dashboard","/users/:path*","/api/:path*"], --}; -+import { NextResponse } from "next/server"; -+// ✅ ไม่บล็อกเพจอีกต่อไป (Bearer อยู่ใน storage ฝั่ง client) -+export function middleware() { -+ return NextResponse.next(); -+} -+// จำกัดให้ทำงานเฉพาะ /api ถ้าต้องการใช้ในอนาคต (ตอนนี้ผ่านเฉย ๆ) -+export const config = { matcher: ["/api/:path*"] }; -diff --git a/frontend/app/(protected)/dashboard/page.jsx b/frontend/app/(protected)/dashboard/page.jsx -new file mode 100644 -index 0000000..ccccccc ---- /dev/null -+++ b/frontend/app/(protected)/dashboard/page.jsx -@@ -0,0 +1,11 @@ -+"use client"; -+export default function DashboardPage() { -+ return ( -+
-+

Dashboard

-+

-+ ยินดีต้อนรับสู่ DMS -+

-+
-+ ); -+} diff --git a/README.md b/README.md index a5df9b2e..0c8ebec5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,103 @@ -# Document Management Sysytem (dms) -## บทบาท; คุณคือ Programmer และ Document Engineer ที่เชี่ยวชาญ +# 📝 Project Title: ocument Management System (DMS) Web Application + +## Des + +### 📌 1. Project Overview / Description + +- ระบบ Document Management System (DMS) +- เป็นเว็บแอปพลิเคชันที่ออกแบบมาเพื่อจัดการเอกสารภายในองค์กรอย่างมีประสิทธิภาพ +- โดยมีฟังก์ชันหลักในการอัปโหลด จัดเก็บ ค้นหา แชร์ และควบคุมสิทธิ์การเข้าถึงเอกสาร +- ระบบนี้จะช่วยลดการใช้เอกสารกระดาษ เพิ่มความปลอดภัยในการจัดเก็บข้อมูล +- และเพิ่มความสะดวกในการทำงานร่วมกันระหว่างทีม + +### 🎯 2. Objectives + +- พัฒนาระบบที่สามารถจัดการเอกสารได้อย่างเป็นระบบ +- ลดความซ้ำซ้อนในการจัดเก็บเอกสาร +- เพิ่มความปลอดภัยในการเข้าถึงและจัดการเอกสาร +- รองรับการทำงานร่วมกันแบบออนไลน์ + +### 📦 3. Scope of Work + +ระบบจะครอบคลุมฟีเจอร์หลักดังนี้: + +- การลงทะเบียนและเข้าสู่ระบบผู้ใช้งาน +- การอัปโหลดและจัดเก็บเอกสารในรูปแบบต่าง ๆ (PDF, DOCX, XLSX ฯลฯ) +- การจัดหมวดหมู่และแท็กเอกสาร +- การค้นหาเอกสารด้วยคำสำคัญหรือฟิลเตอร์ +- การกำหนดสิทธิ์การเข้าถึงเอกสาร (เช่น อ่านอย่างเดียว, แก้ไข, ลบ) +- การบันทึกประวัติการใช้งานเอกสาร (Audit Trail) +- การแจ้งเตือนเมื่อมีการเปลี่ยนแปลงเอกสาร + +### 🛠️ 4. DMS Architecture Deep Dive (Backend + Frontend) + +#### 4.0)Executive Summary + +- Reverse proxy (Nginx/NPM) เผยแพร่ Frontend (Next.js) และ Backend (Node.js/Express) ผ่าน HTTPS (HSTS) +- Backend เชื่อม MariaDB 10.11 (ข้อมูลหลัก DMS) และแยก n8n + Postgres 16 สำหรับ workflow +- RBAC/ABAC ถูกบังคับใช้งานใน middleware + มีชุด SQL (tables → triggers → procedures → views → seed) +- ไฟล์จริง (PDF/DWG) เก็บนอก webroot ที่ **/share/dms‑data** พร้อมมาตรฐานการตั้งชื่อ+โฟลเดอร์ +- Dev/Prod แยกชัดเจนผ่าน Docker multi‑stage + docker‑compose + โฟลเดอร์ persist logs/config/certs + +#### 4.1) Runtime Topology & Trust Boundaries + +``` +Internet Clients (Browser) + │ HTTPS 443 (HSTS) [QNAP mgmt = 8443] + ▼ +┌─────────────────────────────────────────────────────┐ +│ Reverse Proxy Layer │ +│ ├─ Nginx (Alpine) or Nginx Proxy Manager (NPM) │ +│ ├─ TLS (LE cert; SAN multi‑subdomain) │ +│ └─ Routes: │ +│ • /, /_next/* → Frontend (Next.js :3000) │ +│ • /api/* → Backend (Express :3001) │ +│ • /pma/* → phpMyAdmin │ +│ • /n8n/* → n8n (Workflows) │ +└─────────────────────────────────────────────────────┘ + │ │ + │ └──────────┐ + ▼ │ + Frontend (Next.js) │ + │ Cookie-based Auth (HttpOnly) │ + ▼ ▼ + Backend (Node/Express ESM) ─────────► MariaDB 10.11 + │ │ + └────────────────────────────────────┘ + Project data (.pdf/.dwg) @ /share/dms-data + + n8n (workflows) ──► Postgres 16 (separate DB for automations) +``` + +**Trust Boundaries** + +* Public zone: Internet ↔ Reverse proxy +* App zone: Reverse proxy ↔ FE/BE containers (internal Docker network) +* Data zone: Backend ↔ Databases (MariaDB, Postgres) + `/share/dms-data` + +--- +- Frontend: Next.js (ESM) / React.js +- Backend: Node.js / Laravel +- Database: Mariadb / PostgreSQL +- Authentication: JWT +- xx +- Cloud Storage: QNAP + +### 👥 Target Users + +- พนักงานภายในองค์กร +- ผู้จัดการฝ่ายเอกสาร +- ผู้ดูแลระบบ IT + +### 📈 Expected Outcomes + +- ลดเวลาในการค้นหาเอกสารลงอย่างน้อย 50% +- ลดการใช้เอกสารกระดาษในองค์กร +- เพิ่มความปลอดภัยในการจัดเก็บข้อมูล +- รองรับการทำงานแบบ Remote Work + +## บทบาท: คุณคือ Programmer และ Document Engineer ที่เชี่ยวชาญ 1. การพัฒนาเว็บแอป (Web Application Development) 2. Configuration of Container Station on QNAP @@ -19,7 +116,7 @@ 15. ภาษา SQL 16. RBAC -## 2. ระบบที่ใช้; +## 2. ระบบที่ใช้ ## Server @@ -134,3 +231,136 @@ git checkout -b feature/dashboard-update-251004 git add frontend/app/dashboard git commit -m "feat(dashboard): เพิ่มส่วนจัดการ user" git push -u origin feature/dashboard-update-251004 + +📘 Use Case: Upload Document + +Actor: ผู้ใช้งานทั่วไป (Employee) +Description: ผู้ใช้งานสามารถอัปโหลดเอกสารเข้าสู่ระบบเพื่อจัดเก็บและใช้งานในภายหลัง +Preconditions: ผู้ใช้งานต้องเข้าสู่ระบบก่อน +Main Flow: + +ผู้ใช้งานเลือกเมนู “อัปโหลดเอกสาร” +เลือกไฟล์จากเครื่องคอมพิวเตอร์ +กรอกข้อมูลประกอบ เช่น ชื่อเอกสาร หมวดหมู่ แท็ก +กดปุ่ม “อัปโหลด” +ระบบบันทึกเอกสารและแสดงผลการอัปโหลดสำเร็จ + + +Postconditions: เอกสารถูกจัดเก็บในระบบและสามารถค้นหาได้ + +## 📘 Use Case: + +### Search Document + +Actor: ผู้ใช้งานทั่วไป +Description: ผู้ใช้งานสามารถค้นหาเอกสารจากระบบด้วยคำสำคัญหรือฟิลเตอร์ +Preconditions: ผู้ใช้งานต้องเข้าสู่ระบบ +Main Flow: + +ผู้ใช้งานกรอกคำค้นหรือเลือกฟิลเตอร์ (หมวดหมู่, วันที่, ผู้สร้าง) +กดปุ่ม “ค้นหา” +ระบบแสดงรายการเอกสารที่ตรงกับเงื่อนไข + + +Postconditions: ผู้ใช้งานสามารถเปิดดูหรือดาวน์โหลดเอกสารที่ค้นพบได้ + +### Share Document + +Actor: ผู้ใช้งานทั่วไป +Description: ผู้ใช้งานสามารถแชร์เอกสารให้กับผู้ใช้งานอื่นในระบบ +Preconditions: ผู้ใช้งานต้องมีสิทธิ์ในการแชร์เอกสาร +Main Flow: + +ผู้ใช้งานเลือกเอกสารที่ต้องการแชร์ +กดปุ่ม “แชร์” +ระบุผู้รับและสิทธิ์การเข้าถึง (อ่าน/แก้ไข) +กด “ยืนยัน” +ระบบส่งการแจ้งเตือนไปยังผู้รับ + + +Postconditions: ผู้รับสามารถเข้าถึงเอกสารตามสิทธิ์ที่กำหนด + +### Manage Access Rights + +Actor: ผู้ดูแลระบบ (Admin) +Description: ผู้ดูแลระบบสามารถกำหนดสิทธิ์การเข้าถึงเอกสารให้กับผู้ใช้งาน +Preconditions: ผู้ดูแลระบบต้องเข้าสู่ระบบ +Main Flow: + +ผู้ดูแลระบบเลือกเอกสาร +กด “จัดการสิทธิ์” +เลือกผู้ใช้งานและกำหนดสิทธิ์ (อ่าน, แก้ไข, ลบ) +กด “บันทึก” + + +Postconditions: สิทธิ์การเข้าถึงเอกสารถูกปรับตามที่กำหนด + +### View Document History + +Actor: ผู้ใช้งานทั่วไป / ผู้ดูแลระบบ +Description: ผู้ใช้งานสามารถดูประวัติการใช้งานเอกสาร เช่น การแก้ไข การดาวน์โหลด +Preconditions: ผู้ใช้งานต้องมีสิทธิ์เข้าถึงเอกสาร +Main Flow: + +ผู้ใช้งานเปิดเอกสาร +เลือก “ดูประวัติ” +ระบบแสดงรายการกิจกรรมที่เกี่ยวข้องกับเอกสาร + + +Postconditions: ผู้ใช้งานสามารถตรวจสอบการเปลี่ยนแปลงย้อนหลังได้ + +## 🔄Workflow อัตโนมัติในระบบ DMS + +### ✅ ประโยชน์ของ Workflow อัตโนมัติใน DMS + +ลดภาระงานซ้ำ ๆ ของผู้ใช้งาน +เพิ่มความปลอดภัยและการควบคุมเอกสาร +เพิ่มความเร็วในการดำเนินงาน +ลดข้อผิดพลาดจากการทำงานด้วยมือ + +### 🧩 1.Document Approval Workflow + +กรณี: เมื่อมีการอัปโหลดเอกสารที่ต้องได้รับการอนุมัติจากหัวหน้า +ขั้นตอนอัตโนมัติ: + +1. ผู้ใช้งานอัปโหลดเอกสารและเลือก “ต้องการอนุมัติ” +2. ระบบส่งแจ้งเตือนไปยังผู้อนุมัติ (เช่น หัวหน้าแผนก) +3. ผู้อนุมัติสามารถตรวจสอบและกด “อนุมัติ” หรือ “ปฏิเสธ” +4. ระบบบันทึกสถานะเอกสารและแจ้งผลกลับไปยังผู้ส่ง + +### 📥 2. Auto Tagging & Categorization + +กรณี: เอกสารที่อัปโหลดมีชื่อหรือเนื้อหาที่ตรงกับหมวดหมู่ที่กำหนดไว้ +ขั้นตอนอัตโนมัติ: + +เมื่ออัปโหลดเอกสาร ระบบวิเคราะห์ชื่อไฟล์หรือเนื้อหา +ระบบกำหนดหมวดหมู่และแท็กให้โดยอัตโนมัติ เช่น “ใบเสนอราคา” → หมวด “การเงิน” +ผู้ใช้งานสามารถแก้ไขได้หากต้องการ + +### 🔐 3. Access Control Workflow + +กรณี: เอกสารที่มีความลับสูงต้องจำกัดการเข้าถึง +ขั้นตอนอัตโนมัติ: + +เมื่ออัปโหลดเอกสารที่มีคำว่า “ลับ” หรือ “Confidential” +ระบบกำหนดสิทธิ์เริ่มต้นให้เฉพาะผู้ใช้งานระดับผู้จัดการขึ้นไป +ระบบแจ้งเตือนผู้ดูแลระบบให้ตรวจสอบสิทธิ์เพิ่มเติม + +### 📤 4. Expiry & Archiving Workflow + +กรณี: เอกสารที่มีอายุการใช้งาน เช่น สัญญา หรือใบอนุญาต +ขั้นตอนอัตโนมัติ: + +เมื่ออัปโหลดเอกสาร ผู้ใช้งานระบุวันหมดอายุ +ระบบแจ้งเตือนก่อนหมดอายุล่วงหน้า เช่น 30 วัน +เมื่อถึงวันหมดอายุ ระบบย้ายเอกสารไปยังหมวด “Archive” โดยอัตโนมัติ + + +### 📊 5. Audit Trail & Notification Workflow + +กรณี: มีการแก้ไขหรือดาวน์โหลดเอกสารสำคัญ +ขั้นตอนอัตโนมัติ: + +ทุกการกระทำกับเอกสาร (เปิด, แก้ไข, ลบ) จะถูกบันทึกใน Audit Log +หากเอกสารถูกแก้ไขโดยผู้ใช้งานที่ไม่ใช่เจ้าของ ระบบแจ้งเตือนเจ้าของเอกสารทันที + diff --git a/b.env b/b.env deleted file mode 100644 index c77474dd..00000000 --- a/b.env +++ /dev/null @@ -1,96 +0,0 @@ -TZ=Asia/Bangkok -GENERIC_TIMEZONE=Asia/Bangkok -PUBLIC_DOMAIN=np-dms.work - -PUBLIC_FRONTEND_URL=https://lcbp3.np-dms.work -PUBLIC_BACKEND_URL=https://lcbp3.np-dms.work/api -PUBLIC_N8N_URL=https://lcbp3.np-dms.work/n8n - -MARIADB_HOST=mariadb -MARIADB_PORT=3306 -MARIADB_ROOT_PASSWORD=Center#2025 -MARIADB_DATABASE=dms -MARIADB_USER=center -MARIADB_PASSWORD=Center#2025 - -# MARIADB_HOST_PORT=7307 -# BACKEND_HOST_PORT=7001 -# FRONTEND_HOST_PORT=7000 -# PHPMYADMIN_HOST_PORT=7070 -NGINX_HTTP_HOST_PORT=80 -NGINX_HTTPS_HOST_PORT=443 -N# 8N_HOST_PORT=7081 - -NODE_ENV=production -JWT_SECRET=8b0df02e4aee9f9f79a4f2d8ba77b0b82c1ee3446b68cb0bae94ab54d60f8d9e -JWT_EXPIRES_IN=12h -PASSWORD_SALT_ROUNDS=10 -RATE_LIMIT_WINDOW_MS=900000 -RATE_LIMIT_MAX=200 -CORS_ORIGINS=https://lcbp3.np-dms.work,http://localhost:7000,http://192.168.20.248:7000 - - -NEXT_TELEMETRY_DISABLED=1 - -PMA_HOST=mariadb -PMA_PORT=3306 -PMA_ABSOLUTE_URI=https://lcbp3.np-dms.work.com/pma/ - -UPLOAD_LIMIT=256M -MEMORY_LIMIT=512M - -NGINX_SERVER_NAME=np-dms.work.com -NGINX_PROXY_READ_TIMEOUT=300 - -# QNAP_SSL_CERT_HOST=/etc/qnap-ssl/combine -# QNAP_SSL_KEY_HOST=/etc/qnap-ssl/key -# NGINX_SSL_CERT=/etc/nginx/certs/fullchain.pem -# NGINX_SSL_KEY=/etc/nginx/certs/privkey.pem -# NGINX_SSL_KEY=/etc/nginx/certs -QNAP_SSL_CERT=/etc/config/QcloudSSLCertificate/cert -NGINX_SSL_CERT=/etc/qnap-ssl - -N8N_BASIC_AUTH_ACTIVE=true -N8N_BASIC_AUTH_USER=n8n -N8N_BASIC_AUTH_PASSWORD=Center#2025 -N8N_PATH=/n8n/ -N8N_PROTOCOL=https -N8N_PROXY_HOPS=1 -N8N_SECURE_COOKIE=true -N8N_HOST=dcs.mycloudnas.com -N8N_PORT=5678 -N8N_EDITOR_BASE_URL=https://lcbp3.np-dms.work/n8n/ -WEBHOOK_URL=https://lcbp3.np-dms.work/n8n/ -N8N_ENCRYPTION_KEY=9AAIB7Da9DW1qAhJE5/Bz4SnbQjeAngI -# --- n8n → MariaDB --- -# DB_TYPE=mysqldb -# DB_MYSQLDB_HOST=mariadb -# DB_MYSQLDB_PORT=3306 -# DB_MYSQLDB_DATABASE=n8n -# DB_MYSQLDB_USER=n8n_user -# DB_MYSQLDB_PASSWORD=Center#2025 # เปลี่ยนเป็นรหัสแข็งแรงของคุณ - -# ==== n8n → PostgreSQL (แทน MariaDB/MySQL) ==== -DB_TYPE=postgresdb -DB_POSTGRESDB_HOST=postgres_n8n -DB_POSTGRESDB_PORT=5432 -DB_POSTGRESDB_DATABASE=n8n -DB_POSTGRESDB_USER=n8n -DB_POSTGRESDB_PASSWORD=Center#2025 -# path โฟลเดอร์ n8n เดิม (มี database.sqlite) -# HOST_N8N=/share/Container/dms/n8n - - -HOST_BASE=/share/Container/dms -HOST_MARIADB=${HOST_BASE}/mariadb -HOST_BACKEND=${HOST_BASE}/backend -HOST_FRONTEND=${HOST_BASE}/frontend -HOST_PHPMYADMIN=${HOST_BASE}/phpmyadmin -HOST_NGINX=${HOST_BASE}/nginx -HOST_LOGS=${HOST_BASE}/logs -HOST_SCRIPTS=${HOST_BASE}/scripts -HOST_N8N=/share/Container/dms/n8n -HOST_N8N_CACHE=${HOST_BASE}/n8n-cache -HOST_DATA=/share/dms-data -# BACKEND_LOG_DIR=${HOST_LOGS}/backend -BACKEND_LOG_DIR=/app/logs diff --git a/backend/backend_tree.txt b/backend/backend_tree.txt new file mode 100755 index 00000000..a38a59ab Binary files /dev/null and b/backend/backend_tree.txt differ diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100755 index 00000000..0165ccdd --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,77 @@ +# File: backend/docker-compose.yml +# DMS Container v0_8_0 แยก service/ lcbp3-backend +x-restart: &restart_policy + restart: unless-stopped + +x-logging: &default_logging + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "5" +volumes: + backend_node_modules: +services: + backend: + <<: [*restart_policy, *default_logging] + image: dms-backend:dev + # pull_policy: never # <-- FINAL FIX ADDED HERE + container_name: dms_backend + stdin_open: true + tty: true + #user: "node" + user: "1000:1000" + working_dir: /app + deploy: + resources: + limits: + cpus: "2.0" + memory: 1G + reservations: + cpus: "0.25" + memory: 256M + environment: + TZ: "Asia/Bangkok" + CHOKIDAR_USEPOLLING: "1" + CHOKIDAR_INTERVAL: "300" + WATCHPACK_POLLING: "true" + # NODE_ENV: "production" + NODE_ENV: "development" + PORT: "3001" + DB_HOST: "mariadb" + DB_PORT: "3306" + DB_USER: "center" + DB_PASSWORD: "Center#2025" + DB_NAME: "dms" + JWT_SECRET: "9a6d8705a6695ab9bae4ca1cd46c72a6379aa72404b96e2c5b59af881bb55c639dd583afdce5a885c68e188da55ce6dbc1fb4aa9cd4055ceb51507e56204e4ca" + JWT_ACCESS_SECRET: "9a6d8705a6695ab9bae4ca1cd46c72a6379aa72404b96e2c5b59af881bb55c639dd583afdce5a885c68e188da55ce6dbc1fb4aa9cd4055ceb51507e56204e4ca" + JWT_REFRESH_SECRET: "743e798bb10d6aba168bf68fc3cf8eff103c18bd34f1957a3906dc87987c0df139ab72498f2fe20d6c4c580f044ccba7d7bfa4393ee6035b73ba038f28d7480c" + ACCESS_TTL_MS: "900000" + REFRESH_TTL_MS: "604800000" + JWT_EXPIRES_IN: "12h" + PASSWORD_SALT_ROUNDS: "10" + FRONTEND_ORIGIN: "https://lcbp3.np-dms.work" + CORS_ORIGINS: "https://lcbp3.np-dms.work,http://localhost:3000,http://127.0.0.1:3000" + COOKIE_DOMAIN: ".np-dms.work" + RATE_LIMIT_WINDOW_MS: "900000" + RATE_LIMIT_MAX: "200" + BACKEND_LOG_DIR: "/app/logs" + networks: + lcbp3: {} + volumes: + - "/share/Container/dms/backend/src:/app/src:rw" + # - "/share/Container/dms/backend/package.json:/app/package.json" + # - "/share/Container/dms/backend/package-lock.json:/app/package-lock.json" + - "/share/dms-data:/share/dms-data:rw" + - "/share/Container/dms/logs/backend:/app/logs:rw" + # - "/share/Container/dms/backend/node_modules:/app/node_modules" + - "backend_node_modules:/app/node_modules" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3001/health"] + interval: 10s + timeout: 5s + retries: 30 + +networks: + lcbp3: + external: true diff --git a/backend/src/db/sequelize.js b/backend/src/db/sequelize.js index d1e354f1..39e16066 100755 --- a/backend/src/db/sequelize.js +++ b/backend/src/db/sequelize.js @@ -5,7 +5,11 @@ import { Sequelize } from "sequelize"; import { config } from "../config.js"; -export const sequelize = new Sequelize(config.DB.NAME, config.DB.USER, config.DB.PASS, { +export const sequelize = new Sequelize( + config.DB.NAME, + config.DB.USER, + config.DB.PASS, + { host: config.DB.HOST, port: config.DB.PORT, dialect: "mariadb", @@ -13,91 +17,55 @@ export const sequelize = new Sequelize(config.DB.NAME, config.DB.USER, config.DB dialectOptions: { timezone: "Z" }, define: { freezeTableName: true, underscored: false, timestamps: false }, pool: { max: 10, min: 0, idle: 10000 }, -}); + } +); -// --- 1. ประกาศตัวแปรสำหรับ Export Model ทั้งหมด --- -export let User, Role, Permission, Organization, Project, UserRole, RolePermission, - UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap, - Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap, - Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory; +export let User = null; +export let Role = null; +export let Permission = null; +export let UserRole = null; +export let RolePermission = null; if (process.env.ENABLE_SEQUELIZE === "1") { - // --- 2. สร้าง Object ของ Models ทั้งหมดที่จะโหลด --- - const modelsToLoad = { - User: await import("./models/User.js").catch(() => null), - Role: await import("./models/Role.js").catch(() => null), - Permission: await import("./models/Permission.js").catch(() => null), - Organization: await import("./models/Organization.js").catch(() => null), - Project: await import("./models/Project.js").catch(() => null), - UserRole: await import("./models/UserRole.js").catch(() => null), - RolePermission: await import("./models/RolePermission.js").catch(() => null), - UserProjectRole: await import("./models/UserProjectRole.js").catch(() => null), - Correspondence: await import("./models/Correspondence.js").catch(() => null), - CorrespondenceVersion: await import("./models/CorrespondenceVersion.js").catch(() => null), - Document: await import("./models/Document.js").catch(() => null), - CorrDocumentMap: await import("./models/CorrDocumentMap.js").catch(() => null), - Drawing: await import("./models/Drawing.js").catch(() => null), - DrawingRevision: await import("./models/DrawingRevision.js").catch(() => null), - FileObject: await import("./models/FileObject.js").catch(() => null), - RFA: await import("./models/RFA.js").catch(() => null), - RFARevision: await import("./models/RFARevision.js").catch(() => null), - RfaDrawingMap: await import("./models/RfaDrawingMap.js").catch(() => null), - Transmittal: await import("./models/Transmittal.js").catch(() => null), - TransmittalItem: await import("./models/TransmittalItem.js").catch(() => null), - Volume: await import("./models/Volume.js").catch(() => null), - ContractDwg: await import("./models/ContractDwg.js").catch(() => null), - SubCategory: await import("./models/SubCategory.js").catch(() => null), - }; + // โหลดโมเดลแบบ on-demand เพื่อลดความเสี่ยง runtime หากไฟล์โมเดลไม่มี + const mdlUser = await import("./models/User.js").catch(() => null); + const mdlRole = await import("./models/Role.js").catch(() => null); + const mdlPerm = await import("./models/Permission.js").catch(() => null); + const mdlUR = await import("./models/UserRole.js").catch(() => null); + const mdlRP = await import("./models/RolePermission.js").catch(() => null); - // --- 3. Initialize Model ทั้งหมด --- - User = modelsToLoad.User?.default ? modelsToLoad.User.default(sequelize) : null; - Role = modelsToLoad.Role?.default ? modelsToLoad.Role.default(sequelize) : null; - Permission = modelsToLoad.Permission?.default ? modelsToLoad.Permission.default(sequelize) : null; - Organization = modelsToLoad.Organization?.default ? modelsToLoad.Organization.default(sequelize) : null; - Project = modelsToLoad.Project?.default ? modelsToLoad.Project.default(sequelize) : null; - UserRole = modelsToLoad.UserRole?.default ? modelsToLoad.UserRole.default(sequelize) : null; - RolePermission = modelsToLoad.RolePermission?.default ? modelsToLoad.RolePermission.default(sequelize) : null; - UserProjectRole = modelsToLoad.UserProjectRole?.default ? modelsToLoad.UserProjectRole.default(sequelize) : null; - Correspondence = modelsToLoad.Correspondence?.default ? modelsToLoad.Correspondence.default(sequelize) : null; - CorrespondenceVersion = modelsToLoad.CorrespondenceVersion?.default ? modelsToLoad.CorrespondenceVersion.default(sequelize) : null; - Document = modelsToLoad.Document?.default ? modelsToLoad.Document.default(sequelize) : null; - CorrDocumentMap = modelsToLoad.CorrDocumentMap?.default ? modelsToLoad.CorrDocumentMap.default(sequelize) : null; - Drawing = modelsToLoad.Drawing?.default ? modelsToLoad.Drawing.default(sequelize) : null; - DrawingRevision = modelsToLoad.DrawingRevision?.default ? modelsToLoad.DrawingRevision.default(sequelize) : null; - FileObject = modelsToLoad.FileObject?.default ? modelsToLoad.FileObject.default(sequelize) : null; - RFA = modelsToLoad.RFA?.default ? modelsToLoad.RFA.default(sequelize) : null; - RFARevision = modelsToLoad.RFARevision?.default ? modelsToLoad.RFARevision.default(sequelize) : null; - RfaDrawingMap = modelsToLoad.RfaDrawingMap?.default ? modelsToLoad.RfaDrawingMap.default(sequelize) : null; - Transmittal = modelsToLoad.Transmittal?.default ? modelsToLoad.Transmittal.default(sequelize) : null; - TransmittalItem = modelsToLoad.TransmittalItem?.default ? modelsToLoad.TransmittalItem.default(sequelize) : null; - Volume = modelsToLoad.Volume?.default ? modelsToLoad.Volume.default(sequelize) : null; - ContractDwg = modelsToLoad.ContractDwg?.default ? modelsToLoad.ContractDwg.default(sequelize) : null; - SubCategory = modelsToLoad.SubCategory?.default ? modelsToLoad.SubCategory.default(sequelize) : null; + if (mdlUser?.default) User = mdlUser.default(sequelize); + if (mdlRole?.default) Role = mdlRole.default(sequelize); + if (mdlPerm?.default) Permission = mdlPerm.default(sequelize); + if (mdlUR?.default) UserRole = mdlUR.default(sequelize); + if (mdlRP?.default) RolePermission = mdlRP.default(sequelize); + if (User && Role && Permission && UserRole && RolePermission) { + User.belongsToMany(Role, { + through: UserRole, + foreignKey: "user_id", + otherKey: "role_id", + }); + Role.belongsToMany(User, { + through: UserRole, + foreignKey: "role_id", + otherKey: "user_id", + }); - // --- 4. สร้างความสัมพันธ์ (Associations) --- - const loadedModels = { User, Role, Permission, Organization, Project, UserRole, RolePermission, - UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap, - Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap, - Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory }; - - for (const modelName in loadedModels) { - if (loadedModels[modelName] && loadedModels[modelName].associate) { - loadedModels[modelName].associate(loadedModels); - } - } + Role.belongsToMany(Permission, { + through: RolePermission, + foreignKey: "role_id", + otherKey: "permission_id", + }); + Permission.belongsToMany(Role, { + through: RolePermission, + foreignKey: "permission_id", + otherKey: "role_id", + }); + } } export async function dbReady() { - if (process.env.ENABLE_SEQUELIZE !== "1") { - console.log("Sequelize is disabled."); - return Promise.resolve(); - } - try { - await sequelize.authenticate(); - console.log("Sequelize connection has been established successfully."); - } catch (error) { - console.error("Unable to connect to the database via Sequelize:", error); - return Promise.reject(error); - } -} \ No newline at end of file + // โหมดเบา ๆ: แค่ทดสอบเชื่อมต่อ + await sequelize.authenticate(); +} diff --git a/backend/src/index.js b/backend/src/index.js index 16d8030e..ed8f7662 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -1,8 +1,8 @@ // FILE: backend/src/index.js (ESM) ไฟล์ฉบับ “Bearer-only” +// FILE: src/index.js (ESM) import fs from "node:fs"; import express from "express"; import cors from "cors"; -import cookieParser from "cookie-parser"; // added import sql from "./db/index.js"; import healthRouter from "./routes/health.js"; @@ -10,9 +10,6 @@ import { authJwt } from "./middleware/authJwt.js"; import { loadPrincipalMw } from "./middleware/loadPrincipal.js"; // ROUTES -import usersRoutes from "./routes/users.js"; -import rbacAdminRoutes from "./routes/rbac_admin.js"; -import dashboardRoutes from "./routes/dashboard.js"; import authRoutes from "./routes/auth.js"; import lookupRoutes from "./routes/lookup.js"; import organizationsRoutes from "./routes/organizations.js"; @@ -26,6 +23,7 @@ import contractDwgRoutes from "./routes/contract_dwg.js"; import categoriesRoutes from "./routes/categories.js"; import volumesRoutes from "./routes/volumes.js"; import uploadsRoutes from "./routes/uploads.js"; +import usersRoutes from "./routes/users.js"; import permissionsRoutes from "./routes/permissions.js"; const PORT = Number(process.env.PORT || 3001); @@ -39,9 +37,7 @@ const ALLOW_ORIGINS = [ "http://127.0.0.1:3000", FRONTEND_ORIGIN, ...(process.env.CORS_ALLOWLIST - ? process.env.CORS_ALLOWLIST.split(",") - .map((x) => x.trim()) - .filter(Boolean) + ? process.env.CORS_ALLOWLIST.split(",").map((x) => x.trim()).filter(Boolean) : []), ].filter(Boolean); @@ -78,10 +74,6 @@ app.use( exposedHeaders: ["Content-Disposition", "Content-Length"], }) ); - -// parse cookies สำหรับ access_token / refresh_token -app.use(cookieParser()); // added - app.options( "*", cors({ @@ -113,12 +105,8 @@ app.get("/health", async (_req, res) => { }); app.get("/livez", (_req, res) => res.send("ok")); app.get("/readyz", async (_req, res) => { - try { - await sql.query("SELECT 1"); - res.send("ready"); - } catch { - res.status(500).send("not-ready"); - } + try { await sql.query("SELECT 1"); res.send("ready"); } + catch { res.status(500).send("not-ready"); } }); app.get("/info", (_req, res) => res.json({ @@ -150,8 +138,6 @@ app.use("/api/volumes", volumesRoutes); app.use("/api/uploads", uploadsRoutes); app.use("/api/users", usersRoutes); app.use("/api/permissions", permissionsRoutes); -app.use("/api/rbac", rbacAdminRoutes); -app.use("/api/dashboard", dashboardRoutes); // 404 / error app.use((req, res) => @@ -173,9 +159,7 @@ async function shutdown(signal) { try { console.log(`[SHUTDOWN] ${signal} received`); await new Promise((resolve) => server.close(resolve)); - try { - await sql.end(); - } catch {} + try { await sql.end(); } catch {} console.log("[SHUTDOWN] complete"); process.exit(0); } catch (e) { diff --git a/backend/src/middleware/abac.js b/backend/src/middleware/abac.js index 977b476b..5b02914e 100644 --- a/backend/src/middleware/abac.js +++ b/backend/src/middleware/abac.js @@ -3,118 +3,41 @@ // - Project-scoped access control base on user_project_roles + permissions // - Requires req.user.roles and req.user.permissions to be populated (e.g. via auth.js with enrichment) // - Uses UserProjectRole model to check project membership +// Helper ABAC เสริมบางเคส (ถ้าต้องการฟิลเตอร์/บังคับ project_id ตรง ๆ) +// หมายเหตุ: โดยหลักแล้วคุณควรใช้ requirePerm() ที่บังคับ ABAC อัตโนมัติจาก permissions.scope_level -import { sequelize } from "../db/sequelize.js"; -import UPRModel from "../db/models/UserProjectRole.js"; - -/** - * ดึง project_id ที่ผู้ใช้เข้าถึงได้ (จาก user_project_roles) - */ -export async function getUserProjectIds(user_id) { - const UPR = UPRModel(sequelize); - const rows = await UPR.findAll({ where: { user_id } }); - return [...new Set(rows.map((r) => r.project_id))]; -} - -/** - * projectScopedView(moduleName) -> middleware - * - ต้องมี permission ':view' หรือ - * - เป็นสมาชิกของโปรเจ็กต์ (ผ่าน user_project_roles) - * Behavior: - * - ถ้า query ไม่มี project_id และผู้ใช้ไม่ใช่ Admin: - * จำกัดผลลัพธ์ให้เฉพาะโปรเจ็กต์ที่ผู้ใช้เป็นสมาชิก - * - ถ้ามี project_id: บังคับตรวจสิทธิ์การเป็นสมาชิกของโปรเจ็กต์นั้น (เว้นแต่เป็น Admin) - */ -export function projectScopedView(moduleName) { +export function projectScopedViewFallback(moduleName) { + // ใช้ในเคส legacy เท่านั้น return async (req, res, next) => { - const roles = req.user?.roles || []; - const isAdmin = roles.includes("Admin"); - const permName = `${moduleName}:view`; - const hasViewPerm = (req.user?.permissions || []).includes(permName); + const p = req.principal; + if (!p) return res.status(401).json({ error: "Unauthenticated" }); - // Admin ผ่านได้เสมอ - if (isAdmin) return next(); + const hasViewPerm = p.can?.(`${moduleName}.view`) || p.permissions?.has?.(`${moduleName}.view`); + if (p.is_superadmin) return next(); - const qProjectId = req.query?.project_id - ? Number(req.query.project_id) - : null; - const memberProjects = await getUserProjectIds(req.user?.user_id); + const qProjectId = req.query?.project_id ? Number(req.query.project_id) : null; if (qProjectId) { - // ต้องเป็นสมาชิกโปรเจ็กต์นั้น หรือมี perm view - if (hasViewPerm || memberProjects.includes(qProjectId)) return next(); - return res - .status(403) - .json({ error: "Forbidden: not a member of project" }); + if (hasViewPerm || p.inProject(qProjectId)) return next(); + return res.status(403).json({ error: "FORBIDDEN_PROJECT" }); } else { - // ไม่มี project_id: ถ้ามี perm view → อนุญาตทั้งหมด - // ถ้าไม่มี perm view → จำกัดด้วยรายการโปรเจ็กต์ที่เป็นสมาชิก (บันทึกไว้ใน req.abac.filterProjectIds) if (hasViewPerm) return next(); - if (!memberProjects.length) - return res - .status(403) - .json({ error: "Forbidden: no accessible projects" }); + if (!p.project_ids?.length) return res.status(403).json({ error: "FORBIDDEN_PROJECT" }); req.abac = req.abac || {}; - req.abac.filterProjectIds = memberProjects; + req.abac.filterProjectIds = p.project_ids; return next(); } }; } -/** - * บังคับเป็นสมาชิกโปรเจ็กต์จากค่า project_id ใน body - * ใช้กับ create endpoints - */ -export function requireProjectMembershipFromBody() { - return async (req, res, next) => { - const roles = req.user?.roles || []; - const isAdmin = roles.includes("Admin"); - if (isAdmin) return next(); - const pid = Number(req.body?.project_id); - if (!pid) return res.status(400).json({ error: "project_id required" }); - const memberProjects = await getUserProjectIds(req.user?.user_id); - if (!memberProjects.includes(pid)) - return res.status(403).json({ error: "Forbidden: not a project member" }); - next(); - }; -} - -/** - * บังคับเป็นสมาชิกโปรเจ็กต์โดยอ้างอิงจากเรคคอร์ด (ใช้กับ update/delete) - * opts: { modelLoader: (sequelize)=>Model, idParam: 'id', projectField: 'project_id' } - */ -export function requireProjectMembershipByRecord(opts) { - const { modelLoader, idParam = "id", projectField = "project_id" } = opts; - return async (req, res, next) => { - const roles = req.user?.roles || []; - const isAdmin = roles.includes("Admin"); - if (isAdmin) return next(); - const id = Number(req.params[idParam]); - if (!id) return res.status(400).json({ error: "Invalid id" }); - const Model = modelLoader(sequelize); - const row = await Model.findByPk(id); - if (!row) return res.status(404).json({ error: "Not found" }); - const pid = Number(row[projectField]); - const memberProjects = await getUserProjectIds(req.user?.user_id); - if (!memberProjects.includes(pid)) - return res.status(403).json({ error: "Forbidden: not a project member" }); - next(); - }; -} - -/** - * บังคับให้ view ทุกอันต้องส่ง project_id (ยกเว้น Admin) - */ export function requireProjectIdQuery() { - return async (req, res, next) => { - const roles = req.user?.roles || []; - const isAdmin = roles.includes("Admin"); - if (isAdmin) return next(); - const qProjectId = req.query?.project_id - ? Number(req.query.project_id) - : null; - if (!qProjectId) - return res.status(400).json({ error: "project_id query required" }); + return (req, res, next) => { + const p = req.principal; + if (!p) return res.status(401).json({ error: "Unauthenticated" }); + if (p.is_superadmin) return next(); + const qProjectId = req.query?.project_id ? Number(req.query.project_id) : null; + if (!qProjectId) return res.status(400).json({ error: "project_id query required" }); next(); }; } + diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js index 947fb31b..55c69355 100755 --- a/backend/src/middleware/auth.js +++ b/backend/src/middleware/auth.js @@ -1,61 +1,30 @@ // FILE: backend/src/middleware/auth.js - +// (ถ้ายังใช้อยู่) ปรับให้สอดคล้อง Bearer + principal import jwt from "jsonwebtoken"; -import { config } from "../config.js"; -import { User, Role, UserRole } from "../db/sequelize.js"; export function signAccessToken(payload) { - return jwt.sign(payload, config.JWT.SECRET, { - expiresIn: config.JWT.EXPIRES_IN, - }); + const { JWT_SECRET = "dev-secret", JWT_EXPIRES_IN = "30m" } = process.env; + return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN, issuer: "dms-backend" }); } export function signRefreshToken(payload) { - return jwt.sign(payload, config.JWT.REFRESH_SECRET, { - expiresIn: config.JWT.REFRESH_EXPIRES_IN, - }); -} - -export function extractToken(req) { - // ให้คุกกี้มาก่อน แล้วค่อย Bearer (รองรับทั้งสองทาง) - const cookieTok = req.cookies?.access_token || null; - if (cookieTok) return cookieTok; - const hdr = req.headers.authorization || ""; - return hdr.startsWith("Bearer ") ? hdr.slice(7) : null; + const { JWT_REFRESH_SECRET = "dev-refresh", JWT_REFRESH_EXPIRES_IN = "30d" } = process.env; + return jwt.sign({ ...payload, t: "refresh" }, JWT_REFRESH_SECRET, { expiresIn: JWT_REFRESH_EXPIRES_IN, issuer: "dms-backend" }); } +// ถ้าจะใช้ standalone (ไม่แนะนำถ้ามี authJwt แล้ว) export function requireAuth(req, res, next) { - if (req.path === "/health") return next(); // อนุญาต health เสมอ - const token = extractToken(req); - if (!token) return res.status(401).json({ error: "Missing token" }); - + const h = req.headers.authorization || ""; + const m = /^Bearer\s+(.+)$/i.exec(h || ""); + if (!m) return res.status(401).json({ error: "Missing token" }); try { - req.user = jwt.verify(token, config.JWT.SECRET); + const { JWT_SECRET = "dev-secret" } = process.env; + const payload = jwt.verify(m[1], JWT_SECRET, { issuer: "dms-backend" }); + req.auth = { user_id: payload.user_id, username: payload.username }; + req.user = req.user || {}; + req.user.user_id = payload.user_id; + req.user.username = payload.username; next(); } catch { return res.status(401).json({ error: "Invalid/Expired token" }); } -} -// ใช้กับเส้นทางที่ login แล้วจะ enrich ต่อได้ แต่ไม่บังคับ -export function optionalAuth(req, _res, next) { - const token = extractToken(req); - if (!token) return next(); - try { - req.user = jwt.verify(token, config.JWT.SECRET); - } catch {} - next(); -} - -export async function enrichRoles(req, _res, next) { - if (!req.user?.user_id) return next(); - const rows = await UserRole.findAll({ - where: { user_id: req.user.user_id }, - include: [{ model: Role }], - }).catch(() => []); - req.user.roles = rows.map((r) => r.role?.role_name).filter(Boolean); - next(); -} - -export function hasPerm(req, perm) { - const set = new Set(req?.user?.permissions || []); - return set.has(perm); -} +} \ No newline at end of file diff --git a/backend/src/middleware/authJwt.js b/backend/src/middleware/authJwt.js index f0974075..01ffc403 100644 --- a/backend/src/middleware/authJwt.js +++ b/backend/src/middleware/authJwt.js @@ -25,6 +25,10 @@ export function authJwt() { // แนบข้อมูลขั้นต่ำให้ middleware ถัดไป req.auth = { user_id: payload.user_id, username: payload.username }; //req.user = { user_id: payload.user_id, username: payload.username }; + // เผื่อโค้ดเก่าอ้างอิง req.user + req.user = req.user || {}; + req.user.user_id = payload.user_id; + req.user.username = payload.username; next(); } catch (e) { return res.status(401).json({ error: "Unauthenticated" }); diff --git a/backend/src/middleware/loadPrincipal.js b/backend/src/middleware/loadPrincipal.js index 1cfcd19d..dfec1601 100755 --- a/backend/src/middleware/loadPrincipal.js +++ b/backend/src/middleware/loadPrincipal.js @@ -5,15 +5,90 @@ // - Uses rbac.js utility to load principal info // - Attaches to req.principal // - Requires req.user.user_id to be populated (e.g. via auth.js or authJwt.js) +// โหลด principal จาก DB แล้วแนบไว้ใน req.principal +// NOTE: ตรงนี้สมมุติว่าคุณมี service/query ฝั่ง DB อยู่แล้ว (เช่น sql/Sequelize) +// ถ้าคุณมีฟังก์ชันโหลด principal อยู่ที่อื่น ให้แทน logic DB ตรง FIXME ด้านล่าง +// ใช้ req.auth.user_id และตั้ง req.principal ให้ครบ (RBAC + ABAC) -import { loadPrincipal } from "../utils/rbac.js"; +import sql from "../db/index.js"; export function loadPrincipalMw() { return async (req, res, next) => { try { - if (!req.user?.user_id) - return res.status(401).json({ error: "Unauthenticated" }); - req.principal = await loadPrincipal(req.user.user_id); + const uid = req?.auth?.user_id || req?.user?.user_id; + if (!uid) return res.status(401).json({ error: "Unauthenticated" }); + + // --- 1) users (รวม org_id) + const [[u]] = await sql.query( + `SELECT user_id, username, email, first_name, last_name, org_id, is_active + FROM users WHERE user_id=? LIMIT 1`, + [uid] + ); + if (!u || u.is_active === 0) return res.status(401).json({ error: "Unauthenticated" }); + + // --- 2) roles (global) + const [roleRows] = await sql.query( + `SELECT r.role_id, r.role_code, r.role_name + FROM user_roles ur + JOIN roles r ON r.role_id = ur.role_id + WHERE ur.user_id=?`, + [uid] + ); + const roleCodes = new Set(roleRows.map(r => r.role_code)); + const is_superadmin = roleCodes.has("SUPER_ADMIN"); + + // --- 3) permissions (ผ่าน role_permissions) + const [permRows] = await sql.query( + `SELECT DISTINCT p.perm_code + FROM user_roles ur + JOIN role_permissions rp ON rp.role_id = ur.role_id + JOIN permissions p ON p.permission_id = rp.permission_id AND p.is_active=1 + WHERE ur.user_id=?`, + [uid] + ); + const permSet = new Set(permRows.map(x => x.perm_code)); + + // --- 4) project scope (user_project_roles) + const [projRows] = await sql.query( + `SELECT DISTINCT project_id FROM user_project_roles WHERE user_id=?`, + [uid] + ); + const project_ids = projRows.map(r => r.project_id); + + // --- 5) org scope: users.org_id + orgs จาก project_parties ของโปรเจ็คที่เข้าถึง + const baseOrgIds = u.org_id ? [u.org_id] : []; + let projOrgIds = []; + if (project_ids.length) { + const [rows] = await sql.query( + `SELECT DISTINCT org_id FROM project_parties WHERE project_id IN (?)`, + [project_ids] + ); + projOrgIds = rows.map(r => r.org_id); + } + const org_ids = Array.from(new Set([...baseOrgIds, ...projOrgIds])); + + req.principal = { + user_id: u.user_id, + username: u.username, + email: u.email, + first_name: u.first_name, + last_name: u.last_name, + org_id: u.org_id || null, + + roles: roleRows.map(r => ({ role_id: r.role_id, role_code: r.role_code, role_name: r.role_name })), + permissions: permSet, // Set ของ perm_code + project_ids, + org_ids, + is_superadmin, + + // helpers + can: (code) => is_superadmin || permSet.has(code), + canAny: (codes=[]) => is_superadmin || codes.some(c => permSet.has(c)), + canAll: (codes=[]) => is_superadmin || codes.every(c => permSet.has(c)), + inProject: (pid) => is_superadmin || project_ids.includes(Number(pid)), + inOrg: (oid) => is_superadmin || org_ids.includes(Number(oid)), + }; + next(); } catch (err) { console.error("loadPrincipal error", err); diff --git a/backend/src/middleware/permGuard.js b/backend/src/middleware/permGuard.js index 1a19e662..a18c8b24 100644 --- a/backend/src/middleware/permGuard.js +++ b/backend/src/middleware/permGuard.js @@ -2,16 +2,14 @@ // Permission guard middleware // - Checks if user has required permissions // - Requires req.user.permissions to be populated (e.g. via auth.js or authJwt.js with enrichment) +// เปลี่ยนให้เป็น wrapper ที่เรียก req.principal (ทางเก่ายังใช้ได้)** -/** - * requirePerm('rfa:create') => ตรวจว่ามี permission นี้ใน req.user.permissions - * ต้องแน่ใจว่าเรียก enrichPermissions() มาก่อน หรือคำนวณที่จุดเข้าใช้งาน - */ export function requirePerm(...allowedPerms) { return (req, res, next) => { - const perms = req.user?.permissions || []; - const ok = perms.some((p) => allowedPerms.includes(p)); - if (!ok) return res.status(403).json({ error: "Forbidden" }); + const p = req.principal; + if (!p) return res.status(401).json({ error: "Unauthenticated" }); + const ok = p.is_superadmin || allowedPerms.some((code) => p.permissions?.has?.(code)); + if (!ok) return res.status(403).json({ error: "FORBIDDEN", need_any_of: allowedPerms }); next(); }; -} +} \ No newline at end of file diff --git a/backend/src/middleware/permissions.js b/backend/src/middleware/permissions.js index e9af2357..cacd5c63 100644 --- a/backend/src/middleware/permissions.js +++ b/backend/src/middleware/permissions.js @@ -2,39 +2,40 @@ // Permission calculation and enrichment middleware // - Computes effective permissions for a user based on their roles // - Attaches permissions to req.user.permissions +// ใช้เฉพาะกรณีที่คุณยังมี stack Sequelize เดิมอยู่ และอยาก enrich จาก Role/Permission model +// โดยทั่วไป ถ้าคุณใช้ loadPrincipalMw() อยู่แล้ว สามารถไม่ใช้ไฟล์นี้ได้ -import { Role, Permission, UserRole, RolePermission } from "../db/sequelize.js"; +import { Permission, UserRole, RolePermission } from "../db/sequelize.js"; -/** - * คืนชุด permission (string[]) ของ user_id - */ export async function computeEffectivePermissions(user_id) { - // ดึง roles ของผู้ใช้ const userRoles = await UserRole.findAll({ where: { user_id } }); const roleIds = userRoles.map((r) => r.role_id); if (!roleIds.length) return []; - // ดึง permission ผ่าน role_permissions const rp = await RolePermission.findAll({ where: { role_id: roleIds } }); const permIds = [...new Set(rp.map((x) => x.permission_id))]; if (!permIds.length) return []; const perms = await Permission.findAll({ where: { permission_id: permIds } }); - return [...new Set(perms.map((p) => p.permission_name))]; + // ใช้ perm_code ให้สอดคล้อง seed + return [...new Set(perms.map((p) => p.perm_code))]; } -/** - * middleware: เติม permissions ลง req.user.permissions - */ export function enrichPermissions() { return async (req, _res, next) => { - if (!req.user?.user_id) return next(); + const uid = req?.auth?.user_id || req?.user?.user_id; + if (!uid) return next(); try { - const perms = await computeEffectivePermissions(req.user.user_id); + const perms = await computeEffectivePermissions(uid); + // อัปเดตทั้ง req.principal และ req.user (เผื่อโค้ดเก่า) + req.principal = req.principal || {}; + req.principal.permissions = new Set(perms); + req.user = req.user || {}; req.user.permissions = perms; - } catch (e) { - req.user.permissions = []; + } catch { + if (req.principal) req.principal.permissions = new Set(); + if (req.user) req.user.permissions = []; } next(); }; -} +} \ No newline at end of file diff --git a/backend/src/middleware/rbac.js b/backend/src/middleware/rbac.js index 57311201..5e65e580 100644 --- a/backend/src/middleware/rbac.js +++ b/backend/src/middleware/rbac.js @@ -5,18 +5,19 @@ export function requireRole(...allowed) { return (req, res, next) => { - const roles = req.user?.roles || []; - const ok = roles.some((r) => allowed.includes(r)); - if (!ok) return res.status(403).json({ error: "Forbidden" }); + const roles = (req.principal?.roles || []).map(r => r.role_code); + const ok = roles.some((r) => allowed.includes(r)) || req.principal?.is_superadmin; + if (!ok) return res.status(403).json({ error: "FORBIDDEN_ROLE", need_any_of: allowed }); next(); }; } -export function requirePermission(...allowedPerms) { +export function requirePermissionCode(...codes) { return (req, res, next) => { - const perms = req.user?.permissions || []; - const ok = perms.some((p) => allowedPerms.includes(p)); - if (!ok) return res.status(403).json({ error: "Forbidden" }); + const p = req.principal; + if (!p) return res.status(401).json({ error: "Unauthenticated" }); + const ok = p.is_superadmin || codes.some((c) => p.permissions?.has?.(c)); + if (!ok) return res.status(403).json({ error: "FORBIDDEN", need_any_of: codes }); next(); }; } diff --git a/backend/src/middleware/requireBearer.js b/backend/src/middleware/requireBearer.js new file mode 100755 index 00000000..92fe5354 --- /dev/null +++ b/backend/src/middleware/requireBearer.js @@ -0,0 +1,18 @@ +// FILE: src/middleware/requireBearer.js +import jwt from "jsonwebtoken"; +import { findUserById } from "../db/models/users.js"; + +export async function requireBearer(req, res, next) { + const hdr = req.get("Authorization") || ""; + const m = hdr.match(/^Bearer\s+(.+)$/i); + if (!m) return res.status(401).json({ error: "Unauthenticated" }); + try { + const payload = jwt.verify(m[1], process.env.JWT_ACCESS_SECRET, { issuer: "dms-backend" }); + const user = await findUserById(payload.user_id); + if (!user) return res.status(401).json({ error: "Unauthenticated" }); + req.user = { user_id: user.user_id, username: user.username, email: user.email, first_name: user.first_name, last_name: user.last_name }; + next(); + } catch { + return res.status(401).json({ error: "Unauthenticated" }); + } +} diff --git a/backend/src/middleware/requirePerm.js b/backend/src/middleware/requirePerm.js index 84f44049..a224100a 100644 --- a/backend/src/middleware/requirePerm.js +++ b/backend/src/middleware/requirePerm.js @@ -6,32 +6,59 @@ // - Uses canPerform() utility from rbac.js // - Supports global, org, and project scopes // - Requires req.principal to be populated (e.g. via loadPrincipal middleware) +// เช็คตาม perm_code + ABAC อัตโนมัติจาก permissions.scope_level +import sql from "../db/index.js"; -import { canPerform } from "../utils/rbac.js"; +let _permMap = null; +let _loadedAt = 0; +const TTL_MS = 60_000; + +async function getPermRegistry() { + const now = Date.now(); + if (_permMap && now - _loadedAt < TTL_MS) return _permMap; + const [rows] = await sql.query( + `SELECT perm_code, scope_level FROM permissions WHERE is_active=1` + ); + _permMap = new Map(rows.map(r => [r.perm_code, r.scope_level])); // GLOBAL | ORG | PROJECT + _loadedAt = now; + return _permMap; +} /** - * requirePerm('correspondence.create', { scope: 'org', getOrgId: req => ... }) - * scope: 'global' | 'org' | 'project' + * requirePerm('rfas.view', { projectParam: 'project_id', orgParam: 'org_id' }) + * - GLOBAL: แค่มี perm ก็ผ่าน + * - ORG: ต้องมี perm + อยู่ใน org scope (อ่าน org_id จาก param หากระบุ; ไม่ระบุจะใช้ req.principal.org_id) + * - PROJECT:ต้องมี perm + อยู่ใน project scope (อ่าน project_id จาก param) */ -export function requirePerm( - permCode, - { scope = "global", getOrgId = null, getProjectId = null } = {} -) { +export function requirePerm(permCode, { projectParam, orgParam } = {}) { return async (req, res, next) => { - try { - const orgId = getOrgId ? await getOrgId(req) : null; - const projectId = getProjectId ? await getProjectId(req) : null; + const p = req.principal; + if (!p) return res.status(401).json({ error: "Unauthenticated" }); - if (canPerform(req.principal, permCode, { scope, orgId, projectId })) - return next(); - - return res.status(403).json({ - error: "FORBIDDEN", - message: `Require ${permCode} (${scope}-scoped)`, - }); - } catch (e) { - console.error("requirePerm error", e); - res.status(500).json({ error: "Permission check error" }); + if (!(p.is_superadmin || p.permissions?.has?.(permCode))) { + return res.status(403).json({ error: "FORBIDDEN", need: permCode }); } + + const registry = await getPermRegistry(); + const scope = registry.get(permCode) || "GLOBAL"; + + const readParam = (name) => req.params?.[name] ?? req.query?.[name] ?? req.body?.[name]; + + if (scope === "PROJECT") { + const pid = Number(projectParam ? readParam(projectParam) : undefined); + if (!p.is_superadmin) { + if (!pid || !p.inProject(pid)) { + return res.status(403).json({ error: "FORBIDDEN_PROJECT", project_id: pid || null }); + } + } + } else if (scope === "ORG") { + const oid = Number(orgParam ? readParam(orgParam) : p.org_id); + if (!p.is_superadmin) { + if (!oid || !p.inOrg(oid)) { + return res.status(403).json({ error: "FORBIDDEN_ORG", org_id: oid || null }); + } + } + } + next(); }; -} +} \ No newline at end of file diff --git a/backend/src/routes/categories.js b/backend/src/routes/categories.js index 84bb33dd..22f00d4f 100644 --- a/backend/src/routes/categories.js +++ b/backend/src/routes/categories.js @@ -1,4 +1,6 @@ // FILE: src/routes/categories.js +// อ่าน: ใช้ organizations.view (GLOBAL) +// สร้าง/แก้/ลบ: ใช้ settings.manage (GLOBAL) import { Router } from "express"; import sql from "../db/index.js"; import { requirePerm } from "../middleware/requirePerm.js"; diff --git a/backend/src/routes/contract_dwg.js b/backend/src/routes/contract_dwg.js index 4d34e0ea..3f0d6671 100644 --- a/backend/src/routes/contract_dwg.js +++ b/backend/src/routes/contract_dwg.js @@ -1,4 +1,6 @@ // FILE: src/routes/contract_dwg.js +// ใน seed ยังไม่มี contract_dwg.* → ผูกชั่วคราวกับสิทธิ์กลุ่ม drawings: +// read → drawings.view, create/update/delete → drawings.upload/delete (PROJECT scope) import { Router } from "express"; import sql from "../db/index.js"; import { requirePerm } from "../middleware/requirePerm.js"; diff --git a/backend/src/routes/contracts.js b/backend/src/routes/contracts.js index da27cfed..172e2ccd 100644 --- a/backend/src/routes/contracts.js +++ b/backend/src/routes/contracts.js @@ -1,4 +1,7 @@ // FILE: src/routes/contracts.js +// ไม่มี contract.* ใน seed → map เป็นงานดูแลองค์กร/โปรเจ็กต์: +// list/get → projects.view (ORG) +// create/update/delete → projects.manage (ORG) import { Router } from "express"; import sql from "../db/index.js"; import { requirePerm } from "../middleware/requirePerm.js"; diff --git a/backend/src/routes/mvp.js b/backend/src/routes/mvp.js index c20689bd..ee56408b 100644 --- a/backend/src/routes/mvp.js +++ b/backend/src/routes/mvp.js @@ -1,4 +1,5 @@ // FILE: backend/src/routes/mvp.js +// (generic entity maps — ใช้ ‘projects.view’ อ่าน และ ‘projects.manage’ เขียน/ลบ) import { Router } from "express"; import sql from "../db/index.js"; import { requirePerm } from "../middleware/requirePerm.js"; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js index 38f9ae0c..9f96432c 100755 --- a/backend/src/routes/users.js +++ b/backend/src/routes/users.js @@ -1,99 +1,55 @@ -// File: backend/src/routes/users.js -import { Router } from 'express'; -import { User, Role } from '../db/sequelize.js'; -import { authJwt } from "../middleware/authJwt.js"; -import { loadPrincipalMw } from "../middleware/loadPrincipal.js"; // แก้ไข: import ให้ถูกต้อง -import { requirePerm } from '../middleware/requirePerm.js'; -import { hashPassword } from '../utils/passwords.js'; +// FILE: backend/src/routes/users.js +import { Router } from "express"; +import sql from "../db/index.js"; +import { requirePerm } from "../middleware/requirePerm.js"; -const router = Router(); +const r = Router(); -// Middleware Chain ที่ถูกต้อง 100% -router.use(authJwt(), loadPrincipalMw()); - -// GET /api/users -router.get('/', requirePerm('users.view'), async (req, res, next) => { - try { - const users = await User.findAll({ - attributes: { exclude: ['password_hash'] }, - include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }], - order: [['username', 'ASC']] - }); - res.json(users); - } catch (error) { next(error); } +// ME (ทุกคน) +r.get("/me", async (req, res) => { + const p = req.principal; + const [[u]] = await sql.query( + `SELECT user_id, username, email, first_name, last_name, org_id FROM users WHERE user_id=?`, + [p.user_id] + ); + if (!u) return res.status(404).json({ error: "User not found" }); + const [roles] = await sql.query( + `SELECT r.role_code, r.role_name, ur.org_id, ur.project_id + FROM user_roles ur JOIN roles r ON r.role_id = ur.role_id + WHERE ur.user_id=?`, + [p.user_id] + ); + res.json({ + ...u, + roles, + role_codes: roles.map((r) => r.role_code), + permissions: [...(p.permissions || [])], + project_ids: p.project_ids, + org_ids: p.org_ids, + is_superadmin: p.is_superadmin, + }); }); -// POST /api/users -router.post('/', requirePerm('users.manage'), async (req, res, next) => { - const { username, email, password, first_name, last_name, is_active, roles } = req.body; - if (!username || !email || !password) { - return res.status(400).json({ message: 'Username, email, and password are required' }); +// USERS LIST (ORG scope) — admin.access +r.get( + "/", + requirePerm("admin.access", { orgParam: "org_id" }), + async (req, res) => { + const P = req.principal; + let rows = []; + if (P.is_superadmin) { + [rows] = await sql.query( + "SELECT user_id, username, email, org_id FROM users ORDER BY user_id DESC LIMIT 500" + ); + } else if (P.org_ids?.length) { + const inSql = P.org_ids.map(() => "?").join(","); + [rows] = await sql.query( + `SELECT user_id, username, email, org_id FROM users WHERE org_id IN (${inSql}) ORDER BY user_id DESC LIMIT 500`, + P.org_ids + ); } - try { - const password_hash = await hashPassword(password); - const newUser = await User.create({ - username, email, password_hash, first_name, last_name, is_active: is_active !== false, - created_by: req.principal.user_id, - updated_by: req.principal.user_id, - org_id: req.principal.org_ids[0] || null, - }); + res.json(rows); + } +); - if (roles && roles.length > 0) { - await newUser.setRoles(roles); - } - - const userWithRoles = await User.findByPk(newUser.id, { - attributes: { exclude: ['password_hash'] }, - include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }] - }); - res.status(201).json(userWithRoles); - } catch (error) { - if (error.name === 'SequelizeUniqueConstraintError') { - return res.status(409).json({ message: 'Username or email already exists.' }); - } - next(error); - } -}); - -// PUT /api/users/:id -router.put('/:id', requirePerm('users.manage'), async (req, res, next) => { - const { id } = req.params; - const { email, first_name, last_name, is_active, roles } = req.body; - try { - const user = await User.findByPk(id); - if (!user) { - return res.status(404).json({ message: 'User not found' }); - } - user.email = email ?? user.email; - user.first_name = first_name ?? user.first_name; - user.last_name = last_name ?? user.last_name; - user.is_active = is_active ?? user.is_active; - user.updated_by = req.principal.user_id; - await user.save(); - - if (roles) { - await user.setRoles(roles); - } - const updatedUser = await User.findByPk(id, { - attributes: { exclude: ['password_hash'] }, - include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }] - }); - res.json(updatedUser); - } catch (error) { next(error); } -}); - -// DELETE /api/users/:id -router.delete('/:id', requirePerm('users.manage'), async (req, res, next) => { - try { - const user = await User.findByPk(req.params.id); - if (!user) { - return res.status(404).json({ message: 'User not found' }); - } - user.is_active = false; - user.updated_by = req.principal.user_id; - await user.save(); - res.status(204).send(); - } catch (error) { next(error); } -}); - -export default router; \ No newline at end of file +export default r; diff --git a/docker-frontend-build.yml b/docker-frontend-build.yml index 0e19653e..564a2845 100644 --- a/docker-frontend-build.yml +++ b/docker-frontend-build.yml @@ -24,6 +24,7 @@ services: command: ["true"] # docker compose -f docker-frontend-build.yml build --no-cache +# docker compose -f docker-frontend-build.yml build --no-cache 2>&1 | tee frontend_build.log # สร้าง package-lock.json # cd frontend diff --git a/frontend/.dockerignore b/frontend/.dockerignore old mode 100644 new mode 100755 diff --git a/frontend/.editorconfig b/frontend/.editorconfig old mode 100644 new mode 100755 diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json old mode 100644 new mode 100755 diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json old mode 100644 new mode 100755 diff --git a/frontend/Dockerfile b/frontend/Dockerfile old mode 100644 new mode 100755 index f448e9be..d48e9922 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.6 ############ Base ############ -FROM node:24-alpine AS base +FROM node:20-alpine AS base WORKDIR /app RUN apk add --no-cache bash curl tzdata \ && ln -snf /usr/share/zoneinfo/Asia/Bangkok /etc/localtime \ @@ -66,6 +66,8 @@ RUN echo "=== Checking components ===" && \ echo "=== Checking .next permissions ===" && \ ls -lad /app/.next +RUN npm ci --no-audit --no-fund --include=dev + RUN npm run build ############ Prod runtime (optimized) ############ diff --git a/frontend/api/health/route.js b/frontend/api/health/route.js old mode 100644 new mode 100755 diff --git a/frontend/app/(auth)/layout.jsx b/frontend/app/(auth)/layout.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(auth)/login/page copy.jsx b/frontend/app/(auth)/login/page copy.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(auth)/login/page.jsx b/frontend/app/(auth)/login/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/_components/SideNavigation.jsx b/frontend/app/(protected)/_components/SideNavigation.jsx deleted file mode 100644 index 583f3985..00000000 --- a/frontend/app/(protected)/_components/SideNavigation.jsx +++ /dev/null @@ -1,84 +0,0 @@ -// File: frontend/app/(protected)/_components/SideNavigation.jsx -'use client'; // <-- 1. กำหนดให้ไฟล์นี้เป็น Client Component - -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { Home, FileText, Settings, Package2 } from 'lucide-react'; -import { can } from "@/lib/rbac"; -import { cn } from "@/lib/utils"; -import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; - -export function SideNavigation({ user }) { // 2. รับข้อมูล user มาจาก props - const pathname = usePathname(); // 3. ใช้งาน usePathname ได้แล้ว - - const navLinks = [ - { href: '/dashboard', label: 'Dashboard', icon: Home }, - { href: '/correspondences', label: 'Correspondences', icon: FileText }, - { href: '/drawings', label: 'Drawings', icon: FileText }, - // ... เพิ่มเมนูอื่นๆ ตามต้องการ - ]; - - const adminLink = { - href: '/admin/users', - label: 'Admin', - icon: Settings, - requiredPermission: 'manage_users' - }; - - return ( -
-
- - - LCB P3 DMS - -
-
- -
-
- - - Need Help? - Contact support for any issues. - - - - - -
-
- ); -} \ No newline at end of file diff --git a/frontend/app/(protected)/admin/_components/confirm-delete-dialog.jsx b/frontend/app/(protected)/admin/_components/confirm-delete-dialog.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/admin/_components/role-form-dialog.jsx b/frontend/app/(protected)/admin/_components/role-form-dialog.jsx old mode 100644 new mode 100755 index abefdbd6..e67038c0 --- a/frontend/app/(protected)/admin/_components/role-form-dialog.jsx +++ b/frontend/app/(protected)/admin/_components/role-form-dialog.jsx @@ -2,7 +2,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import api from '@/lib/api'; +import { api } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -26,17 +26,20 @@ export function RoleFormDialog({ role, allPermissions, isOpen, setIsOpen, onSucc const isEditMode = !!role; useEffect(() => { + // Reset state ทุกครั้งที่ dialog เปิดขึ้นมาใหม่ if (isOpen) { if (isEditMode) { + // โหมดแก้ไข: ตั้งค่าฟอร์มด้วยข้อมูล Role ที่มีอยู่ setFormData({ name: role.name, description: role.description || '' }); setSelectedPermissions(new Set(role.Permissions?.map(p => p.id) || [])); } else { + // โหมดสร้างใหม่: เคลียร์ฟอร์ม setFormData({ name: '', description: '' }); setSelectedPermissions(new Set()); } setError(''); } - }, [role, isOpen]); + }, [role, isOpen]); // ให้ re-run effect นี้เมื่อ role หรือ isOpen เปลี่ยน const handleInputChange = (e) => { const { id, value } = e.target; @@ -62,15 +65,14 @@ export function RoleFormDialog({ role, allPermissions, isOpen, setIsOpen, onSucc try { if (isEditMode) { - // ในโหมดแก้ไข เราจะอัปเดตสิทธิ์เสมอ + // โหมดแก้ไข: อัปเดต Permissions ของ Role ที่มีอยู่ await api.put(`/rbac/roles/${role.id}/permissions`, { permissionIds: Array.from(selectedPermissions) }); - // (Optional) อาจจะเพิ่มการแก้ไขชื่อ/description ของ role ที่นี่ด้วยก็ได้ - // await api.put(`/rbac/roles/${role.id}`, { name: formData.name, description: formData.description }); } else { - // ในโหมดสร้างใหม่ + // โหมดสร้างใหม่: สร้าง Role ใหม่ก่อน const newRoleRes = await api.post('/rbac/roles', formData); + // ถ้าสร้าง Role สำเร็จ และมีการเลือก Permission ไว้ ให้ทำการผูกสิทธิ์ทันที if (newRoleRes.data && selectedPermissions.size > 0) { await api.put(`/rbac/roles/${newRoleRes.data.id}/permissions`, { @@ -78,8 +80,8 @@ export function RoleFormDialog({ role, allPermissions, isOpen, setIsOpen, onSucc }); } } - onSuccess(); - setIsOpen(false); + onSuccess(); // บอกให้หน้าแม่ (roles/page.jsx) โหลดข้อมูลใหม่ + setIsOpen(false); // ปิด Dialog } catch (err) { setError(err.response?.data?.message || 'An unexpected error occurred.'); } finally { @@ -92,13 +94,14 @@ export function RoleFormDialog({ role, allPermissions, isOpen, setIsOpen, onSucc
- {isEditMode ? `Edit Permissions for ${role.name}` : 'Create New Role'} + {isEditMode ? `Edit Permissions for: ${role.name}` : 'Create New Role'} - Select the permissions for this role. + {isEditMode ? 'Select the permissions for this role.' : 'Define a new role and its initial permissions.'}
+ {/* แสดงฟอร์มสำหรับชื่อและคำอธิบายเฉพาะตอนสร้างใหม่ */} {!isEditMode && ( <>
@@ -123,7 +126,7 @@ export function RoleFormDialog({ role, allPermissions, isOpen, setIsOpen, onSucc checked={selectedPermissions.has(perm.id)} onCheckedChange={() => handlePermissionChange(perm.id)} /> -
@@ -135,6 +138,9 @@ export function RoleFormDialog({ role, allPermissions, isOpen, setIsOpen, onSucc {error &&

{error}

} + diff --git a/frontend/app/(protected)/admin/_components/user-form-dialog.jsx b/frontend/app/(protected)/admin/_components/user-form-dialog.jsx old mode 100644 new mode 100755 index 08c6f751..d298e611 --- a/frontend/app/(protected)/admin/_components/user-form-dialog.jsx +++ b/frontend/app/(protected)/admin/_components/user-form-dialog.jsx @@ -2,9 +2,9 @@ 'use client'; import { useState, useEffect } from 'react'; -import api from '@/lib/api'; +import { api } from '@/lib/api'; import { Button } from '@/components/ui/button'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; @@ -13,20 +13,27 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Trash2 } from 'lucide-react'; import { ScrollArea } from '@/components/ui/scroll-area'; - export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { + // State for form fields const [formData, setFormData] = useState({}); - const [allRoles, setAllRoles] = useState([]); const [selectedSystemRoles, setSelectedSystemRoles] = useState(new Set()); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [allProjects, setAllProjects] = useState([]); + + // State for project role assignments const [projectRoles, setProjectRoles] = useState([]); const [selectedProjectId, setSelectedProjectId] = useState(''); const [selectedRoleId, setSelectedRoleId] = useState(''); + // State for prerequisite data (fetched once) + const [allRoles, setAllRoles] = useState([]); + const [allProjects, setAllProjects] = useState([]); + + // UI State + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const isEditMode = !!user; + // Effect to fetch prerequisite data (all roles and projects) when dialog opens useEffect(() => { const fetchPrerequisites = async () => { try { @@ -38,6 +45,7 @@ export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { setAllProjects(projectsRes.data); } catch (err) { console.error('Failed to fetch prerequisites', err); + setError('Could not load required data (roles, projects).'); } }; if (isOpen) { @@ -45,9 +53,11 @@ export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { } }, [isOpen]); + // Effect to set up the form when the user prop changes (for editing) or when opening for creation useEffect(() => { - const fetchUserData = async () => { + const setupForm = async () => { if (isEditMode) { + // Edit mode: populate form with user data setFormData({ username: user.username, email: user.email, @@ -57,6 +67,7 @@ export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { }); setSelectedSystemRoles(new Set(user.Roles?.map(role => role.id) || [])); + // Fetch this user's specific project roles try { const res = await api.get(`/rbac/user-project-roles?userId=${user.id}`); setProjectRoles(res.data); @@ -64,19 +75,20 @@ export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { console.error("Failed to fetch user's project roles", err); setProjectRoles([]); } - } else { + // Create mode: reset all fields setFormData({ username: '', email: '', password: '', first_name: '', last_name: '', is_active: true }); setSelectedSystemRoles(new Set()); setProjectRoles([]); } + // Reset local state setError(''); setSelectedProjectId(''); setSelectedRoleId(''); }; if (isOpen) { - fetchUserData(); + setupForm(); } }, [user, isOpen]); @@ -107,6 +119,7 @@ export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { projectId: selectedProjectId, roleId: selectedRoleId }); + // Refresh the list after adding const res = await api.get(`/rbac/user-project-roles?userId=${user.id}`); setProjectRoles(res.data); setSelectedProjectId(''); @@ -123,12 +136,9 @@ export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { setError(''); try { await api.delete('/rbac/user-project-roles', { - data: { - userId: user.id, - projectId: assignment.project_id, - roleId: assignment.role_id - } + data: { userId: user.id, projectId: assignment.project_id, roleId: assignment.role_id } }); + // Refresh list visually without another API call setProjectRoles(prev => prev.filter(p => p.id !== assignment.id)); } catch(err) { setError(err.response?.data?.message || 'Failed to remove project role.'); @@ -137,7 +147,8 @@ export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { } }; - const handleSaveUserDetails = async () => { + const handleSaveUserDetails = async (e) => { + e.preventDefault(); setIsLoading(true); setError(''); const payload = { ...formData, roles: Array.from(selectedSystemRoles) }; @@ -148,8 +159,8 @@ export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { } else { await api.post('/users', payload); } - onSuccess(); - setIsOpen(false); + onSuccess(); // Tell the parent page to refresh its data + setIsOpen(false); // Close the dialog } catch (err) { setError(err.response?.data?.message || 'An unexpected error occurred.'); } finally { @@ -160,107 +171,113 @@ export function UserFormDialog({ user, isOpen, setIsOpen, onSuccess }) { return ( - - {isEditMode ? `Edit User: ${user.username}` : 'Create New User'} - - -
- - {/* Section 1: User Details & System Roles */} -
-

User Details & System Roles

-
- - -
-
- - -
- {!isEditMode && ( -
- - -
- )} -
-
- - -
-
- - -
-
-
- -
- {allRoles.map(role => ( -
- handleSystemRoleChange(role.id)} /> - -
- ))} -
-
-
- setFormData(prev => ({...prev, is_active: checked}))} /> - -
-
- - {/* Section 2: Project Role Assignments */} -
-

Project Role Assignments

- {isEditMode ? ( - <> -
-

Assign New Project Role

-
- - -
- -
- + + + {isEditMode ? `Edit User: ${user.username}` : 'Create New User'} + + +
+ + {/* Section 1: User Details & System Roles */} +
+

User Details & System Roles

-

Current Assignments

-
- {projectRoles.length > 0 ? projectRoles.map(pr => ( -
-
- {pr.Project.name} - as - {pr.Role.name} -
- -
- )) :

No project assignments.

} -
+ +
- - ) :

Save the user first to assign project roles.

} +
+ + +
+ {!isEditMode && ( +
+ + +
+ )} +
+
+ + +
+
+ + +
+
+
+ + + {allRoles.map(role => ( +
+ handleSystemRoleChange(role.id)} /> + +
+ ))} +
+
+
+ setFormData(prev => ({...prev, is_active: checked}))} /> + +
+
+ + {/* Section 2: Project Role Assignments */} +
+

Project Role Assignments

+ {isEditMode ? ( + <> +
+

Assign New Project Role

+
+ + +
+ +
+ +
+

Current Assignments

+ +
+ {projectRoles.length > 0 ? projectRoles.map(pr => ( +
+
+ {pr.Project.name} + as + {pr.Role.name} +
+ +
+ )) :

No project assignments.

} +
+
+
+ + ) :

Save the user first to assign project roles.

} +
-
- - {error &&

{error}

} - - - - + + {error &&

{error}

} + + + + +
); diff --git a/frontend/app/(protected)/admin/layout.jsx b/frontend/app/(protected)/admin/layout.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/admin/roles/page.jsx b/frontend/app/(protected)/admin/roles/page.jsx old mode 100644 new mode 100755 index 2a63a454..d0899b22 --- a/frontend/app/(protected)/admin/roles/page.jsx +++ b/frontend/app/(protected)/admin/roles/page.jsx @@ -2,7 +2,7 @@ 'use client'; import { useState, useEffect } from 'react'; -import api from '@/lib/api'; +import { api } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -55,16 +55,16 @@ export default function RolesPage() { return ( <>
-
+

Roles & Permissions

{roles.map(role => ( -
+
@@ -78,7 +78,7 @@ export default function RolesPage() {
-

Assigned Permissions:

+

Assigned Permissions:

{role.Permissions.length > 0 ? ( role.Permissions.map(perm => ( diff --git a/frontend/app/(protected)/admin/users/page.jsx b/frontend/app/(protected)/admin/users/page.jsx old mode 100644 new mode 100755 index fcd21926..38e1be36 --- a/frontend/app/(protected)/admin/users/page.jsx +++ b/frontend/app/(protected)/admin/users/page.jsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import { PlusCircle, MoreHorizontal } from 'lucide-react'; -import api from '@/lib/api'; +import { api } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; diff --git a/frontend/app/(protected)/contracts-volumes/page.jsx b/frontend/app/(protected)/contracts-volumes/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/correspondences/new/page.jsx b/frontend/app/(protected)/correspondences/new/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/correspondences/page.jsx b/frontend/app/(protected)/correspondences/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/dashboard/page copy.jsx b/frontend/app/(protected)/dashboard/page copy.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/dashboard/page.jsx b/frontend/app/(protected)/dashboard/page.jsx old mode 100644 new mode 100755 index cf30961c..15fdcbc9 --- a/frontend/app/(protected)/dashboard/page.jsx +++ b/frontend/app/(protected)/dashboard/page.jsx @@ -7,7 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Activity, File, FilePlus, ArrowRight, BellDot, Settings } from 'lucide-react'; -import api from '@/lib/api'; +import { api } from '@/lib/api'; import { useAuth } from '@/lib/auth'; import { can } from '@/lib/rbac'; diff --git a/frontend/app/(protected)/drawings/page.jsx b/frontend/app/(protected)/drawings/page.jsx old mode 100644 new mode 100755 index 04afd949..b16510be --- a/frontend/app/(protected)/drawings/page.jsx +++ b/frontend/app/(protected)/drawings/page.jsx @@ -1,5 +1,5 @@ -import { getSession } from "@/lib/auth"; -export default async function Page(){ - const { user } = await getSession(); - return
Drawings — list/table (ต่อเชื่อม backend)
; +import { requireSession } from '@/lib/auth-server'; +export default async function Page() { + const { user } = await requireSession(); + return
Drawings — list/table (ต่อเชื่อม backend)
; } \ No newline at end of file diff --git a/frontend/app/(protected)/drawings/upload/page.jsx b/frontend/app/(protected)/drawings/upload/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/health/page.jsx b/frontend/app/(protected)/health/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/layout.jsx b/frontend/app/(protected)/layout.jsx old mode 100644 new mode 100755 index d697b552..5733ba11 --- a/frontend/app/(protected)/layout.jsx +++ b/frontend/app/(protected)/layout.jsx @@ -1,54 +1,71 @@ // File: frontend/app/(protected)/layout.jsx +'use client'; -import { cookies } from "next/headers"; // 1. ยังคงใช้ฟังก์ชันฝั่ง Server -import { redirect } from "next/navigation"; -import { Users } from 'lucide-react'; +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuth } from '@/lib/auth'; + +import { Bell, LogOut, Users } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; -// 2. Import SideNavigation Component ที่เราสร้างขึ้นมาใหม่ -import { SideNavigation } from "./_components/SideNavigation"; +// NOTE: ให้ชี้ไปยังไฟล์จริงของคุณ +// เดิมบางโปรเจ็กต์ใช้ "../_components/SideNavigation" +// ที่นี่อ้าง absolute import ตาม tsconfig/baseUrl +import { SideNavigation } from '@/app/_components/SideNavigation'; -// (ฟังก์ชัน fetchSession และตัวแปรอื่นๆ เหมือนเดิม) -const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; +export default function ProtectedLayout({ children }) { + const { user, isAuthenticated, loading, logout } = useAuth(); + const router = useRouter(); -async function fetchSession() { - const cookieStore = cookies(); - const token = cookieStore.get("access_token")?.value; - if (!token) return null; - try { - const res = await fetch(`${API_BASE}/api/auth/me`, { - headers: { Authorization: `Bearer ${token}` }, - cache: "no-store", - }); - if (!res.ok) return null; - return await res.json(); - } catch (error) { - console.error("Failed to fetch session:", error); - return null; + // Guard ฝั่ง client: ถ้าไม่ได้ล็อกอิน ให้เด้งไป /login + useEffect(() => { + if (!loading && !isAuthenticated) { + router.push('/login'); + } + }, [loading, isAuthenticated, router]); + + // ระหว่างรอเช็คสถานะ หรือยังไม่ authenticated -> แสดง loading + if (loading || !isAuthenticated) { + return ( +
+
Loading session…
+
+ ); } -} -export default async function ProtectedLayout({ children }) { - // 3. ดึงข้อมูล Session บน Server - const session = await fetchSession(); - - if (!session?.user) { - redirect("/login"); - } + const handleLogout = async () => { + try { + await logout(); + } finally { + router.replace('/login'); + } + }; return (
+ {/* Sidebar */} - + + {/* Main */}
-
- {/* Optional: Add a search bar */} -
+
+ + + - {session.user.username || 'My Account'} + {user?.username || 'My Account'} - Settings + Profile Settings - - {/* ปุ่ม Logout จริงๆ ควรอยู่ใน Client Component ที่เรียกใช้ useAuth() hook */} - Logout + + + Logout
+
{children}
); -} \ No newline at end of file +} diff --git a/frontend/app/(protected)/reports/page.jsx b/frontend/app/(protected)/reports/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/rfas/new/page.jsx b/frontend/app/(protected)/rfas/new/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/rfas/page.jsx b/frontend/app/(protected)/rfas/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/transmittals/new/page.jsx b/frontend/app/(protected)/transmittals/new/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/transmittals/page.jsx b/frontend/app/(protected)/transmittals/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/users/page.jsx b/frontend/app/(protected)/users/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/(protected)/workflow/page.jsx b/frontend/app/(protected)/workflow/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/_auth/AuthDriver.js b/frontend/app/_auth/AuthDriver.js old mode 100644 new mode 100755 diff --git a/frontend/app/_auth/drivers/bearerDriver.js b/frontend/app/_auth/drivers/bearerDriver.js old mode 100644 new mode 100755 diff --git a/frontend/app/_auth/drivers/cookieDriver.js b/frontend/app/_auth/drivers/cookieDriver.js old mode 100644 new mode 100755 diff --git a/frontend/app/_auth/useAuthGuard.jsx b/frontend/app/_auth/useAuthGuard.jsx old mode 100644 new mode 100755 diff --git a/frontend/app/_components/SideNavigation.jsx b/frontend/app/_components/SideNavigation.jsx new file mode 100755 index 00000000..78fa9d2e --- /dev/null +++ b/frontend/app/_components/SideNavigation.jsx @@ -0,0 +1,84 @@ +// File: frontend/app/_components/SideNavigation.jsx +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { Home, FileText, Users, Settings, Package2, Upload, PlusCircle, Workflow, BarChart } from 'lucide-react'; +import { can } from "@/lib/rbac"; +import { cn } from "@/lib/utils"; + +// Component นี้จะรับ user object ที่มี roles และ permissions มาจาก Server Component Parent +export function SideNavigation({ user }) { + const pathname = usePathname(); + + // สร้าง Array ของเมนูหลักตามโครงสร้างเดิมของคุณ + const mainNavLinks = [ + { href: '/dashboard', label: 'Dashboard', icon: Home, perm: null }, // หน้าแรกเข้าได้ทุกคน + { href: '/correspondences', label: 'Correspondences', icon: FileText, perm: 'correspondence:view' }, + { href: '/drawings', label: 'Drawings', icon: FileText, perm: 'drawing:view' }, + { href: '/rfas', label: 'RFAs', icon: FileText, perm: 'rfa:view' }, + { href: '/transmittals', label: 'Transmittals', icon: FileText, perm: 'transmittal:view' }, + { href: '/reports', label: 'Reports', icon: BarChart, perm: 'report:view' }, + ]; + + // สร้าง Array ของเมนู Admin ตามโครงสร้างเดิมของคุณ + const adminNavLinks = [ + { href: '/admin', label: 'Admin', icon: Settings, perm: 'admin:view' }, + { href: '/users', label: 'ผู้ใช้/บทบาท', icon: Users, perm: 'users:manage' }, + { href: '/workflow', label: 'Workflow', icon: Workflow, perm: 'workflow:view' }, + ]; + + return ( +
+
+ + + LCB P3 DMS + +
+
+ +
+ {/* ส่วน Card ด้านล่างสามารถคงไว้ หรือเอาออกได้ตามต้องการ */} +
+ ); +} \ No newline at end of file diff --git a/frontend/app/_components/TopBar.jsx b/frontend/app/_components/TopBar.jsx new file mode 100755 index 00000000..374bb6e7 --- /dev/null +++ b/frontend/app/_components/TopBar.jsx @@ -0,0 +1,44 @@ +// File: frontend/app/_components/TopBar.jsx <<'JSX' +'use client'; + +import { Button } from "@/components/ui/button"; +import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; +import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; +import { Bell } from "lucide-react"; +import { useAuth } from "@/lib/auth"; +import Link from "next/link"; + +export default function TopBar() { + const { user, loading, logout } = useAuth(); + + return ( +
+
+ + + + + + Notifications + + + + + + + + + Profile + Settings + + Logout + + +
+ ); +} \ No newline at end of file diff --git a/frontend/app/(protected)/_components/navigation.jsx b/frontend/app/_components/navigation.jsx old mode 100644 new mode 100755 similarity index 98% rename from frontend/app/(protected)/_components/navigation.jsx rename to frontend/app/_components/navigation.jsx index 4a7bcab3..d44077cd --- a/frontend/app/(protected)/_components/navigation.jsx +++ b/frontend/app/_components/navigation.jsx @@ -1,4 +1,4 @@ -//File: frontend/app/(protected)/_components/navigation.jsx +//File: frontend/app/_components/navigation.jsx 'use client'; // <-- 1. กำหนดให้ไฟล์นี้เป็น Client Component import Link from 'next/link'; diff --git a/frontend/app/globals.bak.css b/frontend/app/globals.bak.css new file mode 100755 index 00000000..fee8fb2e --- /dev/null +++ b/frontend/app/globals.bak.css @@ -0,0 +1,146 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* ====== shadcn/ui theme (light + dark) ====== */ +:root { + + /* โทน “น้ำทะเล” ตามธีมของคุณ */ + --primary: 199 90% 40%; + --primary-foreground: 0 0% 100%; + + --secondary: 199 60% 92%; + --secondary-foreground: 220 15% 20%; + + --muted: 210 20% 96%; + --muted-foreground: 220 10% 35%; + + --accent: 199 95% 48%; + --accent-foreground: 0 0% 100%; + + --destructive: 0 84% 60%; + --destructive-foreground: 0 0% 100%; + + --card: 0 0% 100%; + --card-foreground: 220 15% 15%; + + --popover: 0 0% 100%; + --popover-foreground: 220 15% 15%; + + --border: 214 32% 91%; + --input: 214 32% 91%; + --ring: 199 90% 40%; + + --radius: 0.8rem; /* โค้งมนตามแนวทาง UI ของโปรเจ็ค */ +} + +.dark { + --background: 220 18% 10%; + --foreground: 0 0% 100%; + + --primary: 199 95% 58%; + --primary-foreground: 220 18% 10%; + + --secondary: 218 14% 20%; + --secondary-foreground: 0 0% 100%; + + --muted: 220 14% 18%; + --muted-foreground: 220 10% 70%; + + --accent: 199 95% 62%; + --accent-foreground: 220 18% 10%; + + --destructive: 0 62% 46%; + --destructive-foreground: 0 0% 100%; + + --card: 220 18% 12%; + --card-foreground: 0 0% 100%; + + --popover: 220 18% 12%; + --popover-foreground: 0 0% 100%; + + --border: 220 14% 28%; + --input: 220 14% 28%; + --ring: 199 95% 62%; +} + +/* Base styling */ +@layer base { + * { + @apply border-border; + } + html, + body { + @apply h-full; + } + body { + @apply bg-background text-foreground antialiased; + } + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +/* Utility: container max width (ช่วยเรื่อง layout) */ +.container { + @apply mx-auto px-4; +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css index fee8fb2e..08cb2354 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1,144 +1,185 @@ +/* File: frontend/app/globals.css */ +@import "tailwindcss"; +@plugin "tailwindcss-animate"; + +@custom-variant dark (&:is(.dark *)); @tailwind base; @tailwind components; @tailwind utilities; -/* ====== shadcn/ui theme (light + dark) ====== */ +/* === Base & Theme (shadcn style) === */ +@layer base { + :root { + /* Sea palette — light */ + --background: 0 0% 100%; + --foreground: 220 15% 15%; + --card: 0 0% 100%; + --card-foreground: 220 15% 15%; + --popover: 0 0% 100%; + --popover-foreground: 220 15% 15%; + + /* sea tones */ + --primary: 199 90% 40%; + --primary-foreground: 0 0% 100%; + --secondary: 199 60% 92%; + --secondary-foreground: 220 15% 20%; + --muted: 210 20% 96%; + --muted-foreground: 220 10% 35%; + --accent: 199 95% 48%; + --accent-foreground: 0 0% 100%; + + --destructive: 0 84% 60%; + --destructive-foreground: 0 0% 100%; + --border: 214 32% 91%; + --input: 214 32% 91%; + --ring: 199 90% 40%; + + --radius: 0.8rem; + } + + .dark { + /* Sea palette — dark */ + --background: 220 18% 10%; + --foreground: 0 0% 100%; + --card: 220 18% 12%; + --card-foreground: 0 0% 100%; + --popover: 220 18% 12%; + --popover-foreground: 0 0% 100%; + + --primary: 199 95% 58%; + --primary-foreground: 220 18% 10%; + --secondary: 218 14% 20%; + --secondary-foreground: 0 0% 100%; + --muted: 220 14% 18%; + --muted-foreground: 220 10% 70%; + --accent: 199 95% 62%; + --accent-foreground: 220 18% 10%; + + --destructive: 0 62% 46%; + --destructive-foreground: 0 0% 100%; + --border: 220 14% 28%; + --input: 220 14% 28%; + --ring: 199 95% 62%; + } + + * { @apply border-border; } + html, body { @apply h-full; } + body { @apply antialiased bg-background text-foreground; } +} + +/* Utilities */ +.container { @apply px-4 mx-auto; } + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + :root { - - /* โทน “น้ำทะเล” ตามธีมของคุณ */ - --primary: 199 90% 40%; - --primary-foreground: 0 0% 100%; - - --secondary: 199 60% 92%; - --secondary-foreground: 220 15% 20%; - - --muted: 210 20% 96%; - --muted-foreground: 220 10% 35%; - - --accent: 199 95% 48%; - --accent-foreground: 0 0% 100%; - - --destructive: 0 84% 60%; - --destructive-foreground: 0 0% 100%; - - --card: 0 0% 100%; - --card-foreground: 220 15% 15%; - - --popover: 0 0% 100%; - --popover-foreground: 220 15% 15%; - - --border: 214 32% 91%; - --input: 214 32% 91%; - --ring: 199 90% 40%; - - --radius: 0.8rem; /* โค้งมนตามแนวทาง UI ของโปรเจ็ค */ + --radius: 0.625rem; + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); } .dark { - --background: 220 18% 10%; - --foreground: 0 0% 100%; - - --primary: 199 95% 58%; - --primary-foreground: 220 18% 10%; - - --secondary: 218 14% 20%; - --secondary-foreground: 0 0% 100%; - - --muted: 220 14% 18%; - --muted-foreground: 220 10% 70%; - - --accent: 199 95% 62%; - --accent-foreground: 220 18% 10%; - - --destructive: 0 62% 46%; - --destructive-foreground: 0 0% 100%; - - --card: 220 18% 12%; - --card-foreground: 0 0% 100%; - - --popover: 220 18% 12%; - --popover-foreground: 0 0% 100%; - - --border: 220 14% 28%; - --input: 220 14% 28%; - --ring: 199 95% 62%; -} - -/* Base styling */ -@layer base { - * { - @apply border-border; - } - html, - body { - @apply h-full; - } - body { - @apply bg-background text-foreground antialiased; - } - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - } - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } -} - -/* Utility: container max width (ช่วยเรื่อง layout) */ -.container { - @apply mx-auto px-4; + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } @layer base { * { - @apply border-border; + @apply border-border outline-ring/50; } body { @apply bg-background text-foreground; diff --git a/frontend/app/layout.jsx b/frontend/app/layout.jsx old mode 100644 new mode 100755 index 21722c6a..302a2ae7 --- a/frontend/app/layout.jsx +++ b/frontend/app/layout.jsx @@ -1,157 +1,20 @@ -// frontend/app/layout.jsx -'use client'; +// File: frontend/app/layout.jsx +import './globals.css'; +import { Inter } from 'next/font/google'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { - Bell, - Home, - Users, - Settings, - Package2, - FileText, // Added for example - LineChart, // Added for example -} from 'lucide-react'; +export const metadata = { + title: 'DMS', + description: 'Document Management System', +}; -import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { cn } from '@/lib/utils'; - -// **1. Import `useAuth` และ `can` จากไฟล์จริงของคุณ** -import { useAuth } from '@/lib/auth'; -import { can } from '@/lib/rbac'; - -export default function ProtectedLayout({ children }) { - const pathname = usePathname(); - - // **2. เรียกใช้งาน useAuth hook เพื่อดึงข้อมูล user** - const { user, logout } = useAuth(); - - const navLinks = [ - { href: '/dashboard', label: 'Dashboard', icon: Home }, - { href: '/correspondences', label: 'Correspondences', icon: FileText }, - { href: '/drawings', label: 'Drawings', icon: FileText }, - { href: '/rfas', label: 'RFAs', icon: FileText }, - { href: '/transmittals', label: 'Transmittals', icon: FileText }, - { href: '/reports', label: 'Reports', icon: LineChart }, - ]; - - // **3. สร้าง object สำหรับเมนู Admin โดยเฉพาะ** - const adminLink = { - href: '/admin/users', - label: 'Admin', - icon: Settings, - requiredPermission: 'manage_users' - }; +const inter = Inter({ subsets: ['latin'] }); +export default function RootLayout({ children }) { return ( -
-
-
-
- - - LCB P3 DMS - - -
-
- -
-
- - - Need Help? - - Contact support for any issues or questions. - - - - - - -
-
-
-
-
- {/* Mobile navigation can be added here */} -
- {/* Optional: Add a search bar */} -
- - - - - - {user ? user.username : 'My Account'} - - Settings - Support - - Logout - - -
-
- {children} -
-
-
+ + + {children} + + ); -} \ No newline at end of file +} diff --git a/frontend/app/page.jsx b/frontend/app/page.jsx old mode 100644 new mode 100755 index 896a99fb..b2c72ffe --- a/frontend/app/page.jsx +++ b/frontend/app/page.jsx @@ -17,7 +17,7 @@ export default function HomePage() { -
+
📑 RFAs diff --git a/frontend/components.json b/frontend/components.json new file mode 100755 index 00000000..9a5b1d4c --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": false, + "tailwind": { + "config": "tailwind.config.js", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + }, + "registries": {} +} diff --git a/frontend/components/ui/alert-dialog.jsx b/frontend/components/ui/alert-dialog.jsx new file mode 100755 index 00000000..e12c4b2c --- /dev/null +++ b/frontend/components/ui/alert-dialog.jsx @@ -0,0 +1,99 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/frontend/components/ui/alert.jsx b/frontend/components/ui/alert.jsx old mode 100644 new mode 100755 diff --git a/frontend/components/ui/badge.jsx b/frontend/components/ui/badge.jsx old mode 100644 new mode 100755 diff --git a/frontend/components/ui/button.jsx b/frontend/components/ui/button.jsx old mode 100644 new mode 100755 diff --git a/frontend/components/ui/card.jsx b/frontend/components/ui/card.jsx old mode 100644 new mode 100755 diff --git a/frontend/components/ui/checkbox.jsx b/frontend/components/ui/checkbox.jsx new file mode 100755 index 00000000..baa4cebb --- /dev/null +++ b/frontend/components/ui/checkbox.jsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/frontend/components/ui/dialog.jsx b/frontend/components/ui/dialog.jsx new file mode 100755 index 00000000..0d7981be --- /dev/null +++ b/frontend/components/ui/dialog.jsx @@ -0,0 +1,96 @@ +"use client" + +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(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/frontend/components/ui/dropdown-menu.jsx b/frontend/components/ui/dropdown-menu.jsx old mode 100644 new mode 100755 diff --git a/frontend/components/ui/input.jsx b/frontend/components/ui/input.jsx old mode 100644 new mode 100755 diff --git a/frontend/components/ui/label.jsx b/frontend/components/ui/label.jsx old mode 100644 new mode 100755 diff --git a/frontend/components/ui/progress.jsx b/frontend/components/ui/progress.jsx old mode 100644 new mode 100755 diff --git a/frontend/components/ui/scroll-area.jsx b/frontend/components/ui/scroll-area.jsx new file mode 100755 index 00000000..9563747b --- /dev/null +++ b/frontend/components/ui/scroll-area.jsx @@ -0,0 +1,40 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/frontend/components/ui/select.jsx b/frontend/components/ui/select.jsx new file mode 100755 index 00000000..28c363cb --- /dev/null +++ b/frontend/components/ui/select.jsx @@ -0,0 +1,121 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props}> + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/frontend/components/ui/switch.jsx b/frontend/components/ui/switch.jsx old mode 100644 new mode 100755 diff --git a/frontend/components/ui/table.jsx b/frontend/components/ui/table.jsx new file mode 100755 index 00000000..07443a1a --- /dev/null +++ b/frontend/components/ui/table.jsx @@ -0,0 +1,86 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef(({ className, ...props }, ref) => ( + tr]:last:border-b-0", className)} + {...props} /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/frontend/components/ui/tabs.jsx b/frontend/components/ui/tabs.jsx old mode 100644 new mode 100755 diff --git a/frontend/components/ui/tooltip.jsx b/frontend/components/ui/tooltip.jsx old mode 100644 new mode 100755 diff --git a/frontend/docker shadcn.yml b/frontend/docker shadcn.yml new file mode 100755 index 00000000..34828e78 --- /dev/null +++ b/frontend/docker shadcn.yml @@ -0,0 +1,96 @@ +docker run --rm -it \ + -v /share/Container/dms/frontend:/app \ + -w /app \ + -e CI=1 \ + node:20-alpine sh -lc ' +set -eu + +echo "1) ตรวจและแก้ package.json → next/react/react-dom ต้องอยู่ใน dependencies + scripts ครบ" +test -f package.json || { echo "❌ ไม่พบ package.json ที่ /app"; exit 1; } +node -e " +const fs=require(\"fs\"); +const p=JSON.parse(fs.readFileSync(\"package.json\",\"utf8\")); +p.dependencies=p.dependencies||{}; +p.devDependencies=p.devDependencies||{}; +p.scripts=p.scripts||{}; +let changed=false; + +for(const k of [\"next\",\"react\",\"react-dom\"]){ + if(p.devDependencies[k]){ p.dependencies[k]=p.devDependencies[k]; delete p.devDependencies[k]; changed=true; } + if(!p.dependencies[k]){ p.dependencies[k]=\"latest\"; changed=true; } +} +if(!p.scripts.dev){ p.scripts.dev=\"next dev\"; changed=true; } +if(!p.scripts.build){ p.scripts.build=\"next build\"; changed=true; } +if(!p.scripts.start){ p.scripts.start=\"next start\"; changed=true; } + +if(changed){ fs.writeFileSync(\"package.json\",JSON.stringify(p,null,2)); console.log(\"package.json patched\"); } +" +npm i + +echo "2) โครง Next.js: app/ หรือ src/app/ + layout.jsx + globals.css" +APPDIR="app"; [ -d src/app ] && APPDIR="src/app" +mkdir -p "$APPDIR" +[ -f "$APPDIR/globals.css" ] || printf "%s\n%s\n%s\n" "@tailwind base;" "@tailwind components;" "@tailwind utilities;" > "$APPDIR/globals.css" +if [ ! -f "$APPDIR/layout.jsx" ] && [ ! -f "$APPDIR/layout.tsx" ]; then + cat > "$APPDIR/layout.jsx" <{children}); +} +EOF +fi +grep -q "import \"./globals.css\"" "$APPDIR/layout.jsx" 2>/dev/null || sed -i "1i import \"./globals.css\";" "$APPDIR/layout.jsx" 2>/dev/null || true +[ -f "$APPDIR/page.jsx" ] || [ -f "$APPDIR/page.tsx" ] || echo '\''export default function Page(){return
OK
}'\'' > "$APPDIR/page.jsx" + +echo "3) สร้าง/อัปเดตไฟล์ config ที่ CLI ต้องมี: jsconfig, postcss, tailwind, next.config" +[ -f jsconfig.json ] || cat > jsconfig.json < postcss.config.cjs + +[ -f tailwind.config.js ] || npx tailwindcss init -p +grep -q "content:" tailwind.config.js || \ + sed -i "s|module.exports = {|module.exports = {\n content: [\"./$APPDIR/**/*.{js,jsx,ts,tsx,mdx}\", \"./components/**/*.{js,jsx,ts,tsx,mdx}\", \"./pages/**/*.{js,jsx,ts,tsx,mdx}\", \"./src/**/*.{js,jsx,ts,tsx,mdx}\"],|g" tailwind.config.js +grep -q "tailwindcss-animate" tailwind.config.js || \ + sed -i '\''s|plugins: \[|plugins: [require("tailwindcss-animate"), |; s|plugins: \[\]|plugins: [require("tailwindcss-animate")]|'\'' tailwind.config.js + +# next.config (บางเวอร์ชัน CLI เช็คไฟล์นี้ด้วย) +if [ ! -f next.config.js ] && [ ! -f next.config.mjs ]; then + cat > next.config.js < components.json </dev/null 2>&1 || true + +echo "6) init (force) — ตอนนี้ควรผ่านการตรวจจับแล้ว" +npx shadcn@latest init -y -f --no-src-dir + +echo "✅ เสร็จ — ถ้าต้องการเพิ่มคอมโพเนนต์ต่อ:" +echo "npx shadcn@latest add -y dialog alert-dialog dropdown-menu checkbox scroll-area tabs tooltip switch button label input card badge progress tabs" +' diff --git a/frontend/docker-compose.yml b/frontend/docker-compose.yml new file mode 100755 index 00000000..8b8ddc6c --- /dev/null +++ b/frontend/docker-compose.yml @@ -0,0 +1,70 @@ +# File: frontend/docker-compose.yml +# DMS Container v0_8_0 แยก service/ lcbp3-frontend +x-restart: &restart_policy + restart: unless-stopped + +x-logging: &default_logging + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "5" +volumes: + frontend_node_modules: + frontend_next: + frontend_next_cache: +services: + frontend: + <<: [*restart_policy, *default_logging] + image: dms-frontend:dev + # pull_policy: never # <-- FINAL FIX ADDED HERE + container_name: dms_frontend + stdin_open: true + tty: true + # user: "node" + # user: "1000:1000" + user: "0:0" + # user: "${PUID:-1000}:${PGID:-1000}" + working_dir: /app + deploy: + resources: + limits: + cpus: "2.0" + memory: 2G + environment: + TZ: "Asia/Bangkok" + NODE_ENV: "development" + # NEXT_PUBLIC_API_BASE: "/api" + CHOKIDAR_USEPOLLING: "1" + CHOKIDAR_INTERVAL: "300" + WATCHPACK_POLLING: "true" + NEXT_TELEMETRY_DISABLED: "1" + NEXT_PUBLIC_API_BASE: "https://lcbp3.np-dms.work" + NEXT_PUBLIC_AUTH_MODE: "cookie" + NEXT_PUBLIC_DEBUG_AUTH: "1" + INTERNAL_API_BASE: "http://backend:3001" + JWT_ACCESS_SECRET: "9a6d8705a6695ab9bae4ca1cd46c72a6379aa72404b96e2c5b59af881bb55c639dd583afdce5a885c68e188da55ce6dbc1fb4aa9cd4055ceb51507e56204e4ca" + JWT_REFRESH_SECRET: "743e798bb10d6aba168bf68fc3cf8eff103c18bd34f1957a3906dc87987c0df139ab72498f2fe20d6c4c580f044ccba7d7bfa4393ee6035b73ba038f28d7480c" + expose: + - "3000" + networks: + lcbp3: {} + volumes: + - "/share/Container/dms/frontend:/app:rw" + - "frontend_node_modules:/app/node_modules" + - "frontend_next_cache:/app/.next/cache" + #- "/share/Container/dms/frontend/node_modules:/app/node_modules" + - "frontend_next:/app/.next" + - "/share/Container/dms/logs/frontend:/app/.logs" + healthcheck: + test: + [ + "CMD-SHELL", + 'wget -qO- http://127.0.0.1:3000/health | grep -q ''"ok":true''', + ] + interval: 15s + timeout: 5s + retries: 30 +networks: + lcbp3: + external: true diff --git a/frontend/frontend_tree.txt b/frontend/frontend_tree.txt new file mode 100755 index 00000000..c43e459f Binary files /dev/null and b/frontend/frontend_tree.txt differ diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100755 index 00000000..9c333833 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + } +} diff --git a/frontend/lib/AuthContext.jsx b/frontend/lib/AuthContext.jsx new file mode 100755 index 00000000..029770a1 --- /dev/null +++ b/frontend/lib/AuthContext.jsx @@ -0,0 +1,58 @@ +// frontend/context/AuthContext.jsx +'use client'; + +import { createContext, useState, useContext, useEffect } from 'react'; +import { api } from '@/lib/api'; +import { cookieDriver } from '@/app/_auth/drivers/cookieDriver'; + +const AuthContext = createContext(null); +const COOKIE_NAME = "access_token"; + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const initializeAuth = async () => { + const token = cookieDriver.get(COOKIE_NAME); + if (token) { + try { + api.defaults.headers.Authorization = `Bearer ${token}`; + const response = await api.get('/auth/me'); + setUser(response.data.user || response.data); + } catch (error) { + cookieDriver.remove(COOKIE_NAME); + delete api.defaults.headers.Authorization; + } + } + setLoading(false); + }; + initializeAuth(); + }, []); + + const login = async (credentials) => { + const response = await api.post('/auth/login', credentials); + const { token, user } = response.data; + cookieDriver.set(COOKIE_NAME, token, { expires: 7 }); + api.defaults.headers.Authorization = `Bearer ${token}`; + setUser(user); + return user; + }; + + const logout = () => { + cookieDriver.remove(COOKIE_NAME); + delete api.defaults.headers.Authorization; + setUser(null); + window.location.href = '/login'; + }; + + return ( + + {!loading && children} + + ); +} + +export const useAuth = () => { + return useContext(AuthContext); +}; \ No newline at end of file diff --git a/frontend/lib/api.js b/frontend/lib/api.js old mode 100644 new mode 100755 diff --git a/frontend/lib/auth copy.js b/frontend/lib/auth copy.js old mode 100644 new mode 100755 index bf1b9159..6409674c --- a/frontend/lib/auth copy.js +++ b/frontend/lib/auth copy.js @@ -1,79 +1,34 @@ -<<<<<<< HEAD // frontend/lib/auth.js import { cookies } from "next/headers"; const COOKIE_NAME = "access_token"; +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; /** - * Server-side session fetcher (ใช้ใน Server Components/Layouts) - * - อ่านคุกกี้แบบ async: await cookies() - * - ถ้าไม่มี token → return null - * - ถ้ามี → เรียก /api/auth/me ที่ backend เพื่อตรวจสอบ + * Server-side session fetcher */ export async function getSession() { - // ✅ ต้อง await - const cookieStore = await cookies(); + const cookieStore = cookies(); const token = cookieStore.get(COOKIE_NAME)?.value; if (!token) return null; - // เรียก backend ตรวจ session (ปรับ endpoint ให้ตรงของคุณ) - const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/me`, { - // ส่งต่อคุกกี้ไป backend (เลือกอย่างใดอย่างหนึ่ง) - // วิธี A: ส่ง header Cookie โดยตรง - headers: { Cookie: `${COOKIE_NAME}=${token}` }, - // วิธี B: ถ้า proxy ผ่าน nginx ในโดเมนเดียวกัน ใช้ credentials รวมคุกกี้อัตโนมัติได้ - // credentials: "include", - cache: "no-store", - }); + try { + const res = await fetch(`${API_BASE}/api/auth/me`, { + headers: { Authorization: `Bearer ${token}` }, + cache: "no-store", + }); - if (!res.ok) return null; - - const data = await res.json(); - // คาดหวังโครงสร้าง { user, permissions } จาก backend - return { - user: data.user, - permissions: data.permissions || [], - token, - }; -} -======= -// frontend/lib/auth.js -import { cookies } from "next/headers"; - -const COOKIE_NAME = "access_token"; - -/** - * Server-side session fetcher (ใช้ใน Server Components/Layouts) - * - อ่านคุกกี้แบบ async: await cookies() - * - ถ้าไม่มี token → return null - * - ถ้ามี → เรียก /api/auth/me ที่ backend เพื่อตรวจสอบ - */ -export async function getSession() { - // ✅ ต้อง await - const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; - - if (!token) return null; - - // เรียก backend ตรวจ session (ปรับ endpoint ให้ตรงของคุณ) - const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/me`, { - // ส่งต่อคุกกี้ไป backend (เลือกอย่างใดอย่างหนึ่ง) - // วิธี A: ส่ง header Cookie โดยตรง - headers: { Cookie: `${COOKIE_NAME}=${token}` }, - // วิธี B: ถ้า proxy ผ่าน nginx ในโดเมนเดียวกัน ใช้ credentials รวมคุกกี้อัตโนมัติได้ - // credentials: "include", - cache: "no-store", - }); - - if (!res.ok) return null; - - const data = await res.json(); - // คาดหวังโครงสร้าง { user, permissions } จาก backend - return { - user: data.user, - permissions: data.permissions || [], - token, - }; -} ->>>>>>> 71fc7eee (backend: Mod) + if (!res.ok) return null; + + const data = await res.json(); + return { + user: data.user, + permissions: data.permissions || data.perms || [], + token, + }; + } catch (error) { + console.error("Error fetching session:", error); + return null; + } +} \ No newline at end of file diff --git a/frontend/lib/auth-server.js b/frontend/lib/auth-server.js new file mode 100755 index 00000000..6d232447 --- /dev/null +++ b/frontend/lib/auth-server.js @@ -0,0 +1,42 @@ +// File: frontend/lib/auth-server.js +// frontend/lib/auth-server.js +import 'server-only'; +import { cookies } from 'next/headers'; + +export function getAccessToken() { + const cookieStore = cookies(); + return cookieStore.get('access_token')?.value ?? null; +} + +function buildCookieHeader() { + const store = cookies(); + return store.getAll().map(c => `${c.name}=${c.value}`).join('; '); +} + +export async function getSession() { + const token = getAccessToken(); + if (!token) return null; + + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/me`, { + method: 'GET', + headers: { cookie: buildCookieHeader(), accept: 'application/json' }, + cache: 'no-store', + }); + if (!res.ok) return null; + const data = await res.json(); + const user = data?.user ?? data; // รองรับทั้ง {user:{...}} หรือส่งตรง + return { user, token }; + } catch { + return null; + } +} + +export async function requireSession() { + const session = await getSession(); + if (!session) { + const { redirect } = await import('next/navigation'); + redirect('/login'); + } + return session; +} diff --git a/frontend/lib/auth.js b/frontend/lib/auth.js old mode 100644 new mode 100755 index e6a0f2bb..f0e57e0e --- a/frontend/lib/auth.js +++ b/frontend/lib/auth.js @@ -1,82 +1,41 @@ // frontend/lib/auth.js -// frontend/lib/auth.js - 'use client'; -import { createContext, useState, useContext, useEffect } from 'react'; -import api from './api'; -// 1. Import cookieDriver ที่คุณมีอยู่แล้ว ซึ่งเป็นวิธีที่ถูกต้อง -import { cookieDriver } from '@/app/_auth/drivers/cookieDriver'; +import { createContext, useContext, useEffect, useState, useCallback } from "react"; -const AuthContext = createContext(null); - -const COOKIE_NAME = "access_token"; +const AuthContext = createContext({ + user: null, + isAuthenticated: false, + loading: true, + logout: () => {}, +}); export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { - const initializeAuth = async () => { - // 2. อ่าน token จาก cookie ด้วย cookieDriver.get() - const token = cookieDriver.get(COOKIE_NAME); - - if (token) { - try { - api.defaults.headers.Authorization = `Bearer ${token}`; - // สมมติว่ามี endpoint /auth/me สำหรับดึงข้อมูลผู้ใช้ - const response = await api.get('/auth/me'); - setUser(response.data.user || response.data); // รองรับทั้งสองรูปแบบ - } catch (error) { - console.error("Failed to initialize auth from cookie:", error); - cookieDriver.remove(COOKIE_NAME); - delete api.defaults.headers.Authorization; - } - } - setLoading(false); - }; - - initializeAuth(); + fetch("/api/auth/me", { credentials: "include" }) + .then((res) => (res.ok ? res.json() : null)) + .then((data) => setUser(data?.user ?? null)) + .finally(() => setLoading(false)); }, []); - const login = async (credentials) => { - const response = await api.post('/auth/login', credentials); - const { token, user } = response.data; - - // 3. ตั้งค่า token ใน cookie ด้วย cookieDriver.set() - cookieDriver.set(COOKIE_NAME, token, { expires: 7, secure: true, sameSite: 'strict' }); - api.defaults.headers.Authorization = `Bearer ${token}`; - setUser(user); - return user; - }; - - const logout = () => { - // 4. ลบ token ออกจาก cookie ด้วย cookieDriver.remove() - cookieDriver.remove(COOKIE_NAME); - delete api.defaults.headers.Authorization; - setUser(null); - window.location.href = '/login'; - }; - - const value = { - user, - isAuthenticated: !!user, - loading, - login, - logout - }; + const logout = useCallback(async () => { + try { + await fetch("/api/auth/logout", { method: "POST", credentials: "include" }); + } finally { + window.location.href = "/login"; + } + }, []); return ( - - {!loading && children} + + {children} ); } -export const useAuth = () => { - const context = useContext(AuthContext); - if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; -}; \ No newline at end of file +export function useAuth() { + return useContext(AuthContext); +} \ No newline at end of file diff --git a/frontend/lib/rbac.js b/frontend/lib/rbac.js old mode 100644 new mode 100755 diff --git a/frontend/lib/session.js b/frontend/lib/session.js new file mode 100755 index 00000000..8fa061d2 --- /dev/null +++ b/frontend/lib/session.js @@ -0,0 +1,31 @@ +// frontend/lib/session.js +import { cookies } from "next/headers"; + +const COOKIE_NAME = "access_token"; +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; + +/** + * Server-side function to get the current session from the request cookies. + * This can only be used in Server Components, Server Actions, or Route Handlers. + */ +export async function getSession() { + const cookieStore = cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; + + if (!token) return null; + + try { + const res = await fetch(`${API_BASE}/api/auth/me`, { + headers: { Authorization: `Bearer ${token}` }, + cache: "no-store", + }); + + if (!res.ok) return null; + + const data = await res.json(); + return data; // Expects { user, permissions, ... } + } catch (error) { + console.error("Error fetching session:", error); + return null; + } +} \ No newline at end of file diff --git a/frontend/lib/utils.js b/frontend/lib/utils.js old mode 100644 new mode 100755 diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100755 index 00000000..b39a9d55 --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,3 @@ +/** @type {import(next).NextConfig} */ +const nextConfig = { reactStrictMode: true }; +module.exports = nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json old mode 100644 new mode 100755 index 4dab35e4..d8e2e692 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,14 +8,18 @@ "name": "dms-frontend", "version": "0.7.0", "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "autoprefixer": "10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^11.2.10", @@ -23,17 +27,19 @@ "jsonwebtoken": "^9.0.2", "lucide-react": "^0.451.0", "next": "15.0.3", - "postcss": "8.4.47", "react": "18.3.1", "react-dom": "18.3.1", - "tailwind-merge": "^2.6.0", - "tailwindcss": "3.4.14", - "tailwindcss-animate": "^1.0.7" + "tailwind-merge": "^2.6.0" }, "devDependencies": { + "@tailwindcss/postcss": "^4.1.14", + "autoprefixer": "^10.4.21", "eslint": "9.13.0", "eslint-config-next": "15.0.3", - "prettier": "3.3.3" + "postcss": "^8.5.6", + "prettier": "3.3.3", + "tailwindcss": "^4.1.14", + "tailwindcss-animate": "^1.0.7" }, "engines": { "node": ">=20.0.0" @@ -43,6 +49,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -687,37 +694,46 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, "license": "ISC", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "minipass": "^7.0.4" }, "engines": { - "node": ">=12" + "node": ">=18.0.0" } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -727,12 +743,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -900,6 +918,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -913,6 +932,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -922,6 +942,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -941,15 +962,11 @@ "node": ">=12.4.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", @@ -957,6 +974,34 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.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-arrow": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", @@ -980,6 +1025,36 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -1036,6 +1111,42 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "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-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -1386,6 +1497,80 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "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-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1691,6 +1876,282 @@ "tslib": "^2.4.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.5", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz", + "integrity": "sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "postcss": "^8.4.41", + "tailwindcss": "4.1.14" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2320,22 +2781,11 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2347,31 +2797,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2589,9 +3014,10 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -2608,11 +3034,11 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -2665,29 +3091,19 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/baseline-browser-mapping": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz", "integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2703,6 +3119,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -2715,6 +3132,7 @@ "version": "4.26.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -2821,15 +3239,6 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001745", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", @@ -2867,40 +3276,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "node": ">=18" } }, "node_modules/class-variance-authority": { @@ -2948,6 +3331,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2960,6 +3344,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true, "license": "MIT" }, "node_modules/color-string": { @@ -2973,15 +3358,6 @@ "simple-swizzle": "^0.2.2" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2993,6 +3369,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3003,18 +3380,6 @@ "node": ">= 8" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -3141,8 +3506,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -3153,18 +3518,6 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3193,12 +3546,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3212,14 +3559,30 @@ "version": "1.5.227", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", + "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -3401,6 +3764,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3856,6 +4220,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -3872,6 +4237,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -3898,6 +4264,7 @@ "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -3920,6 +4287,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3982,26 +4350,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -4038,24 +4391,11 @@ } } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4184,30 +4524,11 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -4216,30 +4537,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4283,6 +4580,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -4375,6 +4679,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4496,18 +4801,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -4552,6 +4845,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -4602,6 +4896,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4623,15 +4918,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", @@ -4655,6 +4941,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -4693,6 +4980,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4871,6 +5159,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -4891,28 +5180,14 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-cookie": { @@ -5080,20 +5355,244 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "license": "MIT", + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { - "node": ">=10" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, "node_modules/locate-path": { "version": "6.0.0", @@ -5172,12 +5671,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/lucide-react": { "version": "0.451.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.451.0.tgz", @@ -5187,6 +5680,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5201,6 +5704,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -5210,6 +5714,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5246,11 +5751,25 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/motion-dom": { "version": "11.18.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", @@ -5272,17 +5791,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -5410,21 +5918,14 @@ "version": "2.0.21", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "dev": true, "license": "MIT" }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5434,20 +5935,12 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5629,12 +6122,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5662,6 +6149,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5671,24 +6159,9 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5699,6 +6172,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5707,24 +6181,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5736,9 +6192,10 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5755,98 +6212,19 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -5901,6 +6279,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -6018,27 +6397,6 @@ } } }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6087,6 +6445,7 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -6127,6 +6486,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -6137,6 +6497,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -6345,6 +6706,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6357,6 +6719,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6438,18 +6801,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/simple-swizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", @@ -6498,65 +6849,6 @@ "node": ">=10.0.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -6670,43 +6962,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6753,28 +7008,6 @@ } } }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6792,6 +7025,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6811,96 +7045,51 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.14", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", - "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.0", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "dev": true, + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "dev": true, "license": "MIT", "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, - "node_modules/tailwindcss/node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=14" + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/text-table": { @@ -6910,27 +7099,6 @@ "dev": true, "license": "MIT" }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6983,6 +7151,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -7004,12 +7173,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "license": "Apache-2.0" - }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -7193,6 +7356,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -7272,16 +7436,11 @@ } } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7392,104 +7551,14 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" + "node": ">=18" } }, "node_modules/yocto-queue": { diff --git a/frontend/package.json b/frontend/package.json old mode 100644 new mode 100755 index 98ddf54f..5636997e --- a/frontend/package.json +++ b/frontend/package.json @@ -10,32 +10,38 @@ "format": "prettier --write ." }, "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "autoprefixer": "10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^11.2.10", - "jsonwebtoken": "^9.0.2", "js-cookie": "^3.0.5", + "jsonwebtoken": "^9.0.2", "lucide-react": "^0.451.0", "next": "15.0.3", - "postcss": "8.4.47", "react": "18.3.1", "react-dom": "18.3.1", - "tailwind-merge": "^2.6.0", - "tailwindcss": "3.4.14", - "tailwindcss-animate": "^1.0.7" + "tailwind-merge": "^2.6.0" }, "devDependencies": { + "@tailwindcss/postcss": "^4.1.14", + "autoprefixer": "^10.4.21", "eslint": "9.13.0", "eslint-config-next": "15.0.3", - "prettier": "3.3.3" + "postcss": "^8.5.6", + "prettier": "3.3.3", + "tailwindcss": "^4.1.14", + "tailwindcss-animate": "^1.0.7" }, "engines": { "node": ">=20.0.0" diff --git a/frontend/page.jsx b/frontend/page.jsx old mode 100644 new mode 100755 diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js old mode 100644 new mode 100755 index 5cbc2c7d..48cfe240 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,6 @@ module.exports = { plugins: { - tailwindcss: {}, - autoprefixer: {} + '@tailwindcss/postcss': {}, + autoprefixer: {}, } }; diff --git a/frontend/styles/global.css b/frontend/styles/global.css old mode 100644 new mode 100755 diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js old mode 100644 new mode 100755 diff --git a/generate-shadcn-components.yml b/generate-shadcn-components.yml index 01d6dc63..f7a374f2 100644 --- a/generate-shadcn-components.yml +++ b/generate-shadcn-components.yml @@ -1,6 +1,6 @@ services: setup-shadcn: - image: node:24-alpine + image: node:20-alpine working_dir: /app volumes: - /share/Container/dms/frontend:/app @@ -10,9 +10,9 @@ services: echo '...installing dependencies...' && npm install && echo '...initiallizing shadcn/ui...' && - npx shadcn@latest init -y -d && + npx shadcn@latest init -y -f && echo '...adding components...' && - npx shadcn@latest add -y alัert dialog checkbox scroll-area button label input card badge tabs progress dropdown-menu tooltip switch && + npx shadcn@latest add -y alert dialog alert-dialog dropdown-menu checkbox scroll-area table tooltip switch button label input card badge progress tabs select && echo '✅ Done! Check components/ui/ directory' " diff --git a/gitea.yml b/gitea.yml deleted file mode 100644 index 1b1f7be2..00000000 --- a/gitea.yml +++ /dev/null @@ -1,100 +0,0 @@ -APP_NAME = Gitea: Git with a cup of tea -RUN_USER = git -RUN_MODE = prod -WORK_PATH = /var/lib/gitea - -[repository] -ROOT = /var/lib/gitea/git/repositories - -[repository.local] -LOCAL_COPY_PATH = /tmp/gitea/local-repo - -[repository.upload] -TEMP_PATH = /tmp/gitea/uploads - -[server] -APP_DATA_PATH = /var/lib/gitea -SSH_DOMAIN = git.np-dms.work -HTTP_PORT = 3000 -ROOT_URL = https://git.np-dms.work/ -DISABLE_SSH = false -; In rootless gitea container only internal ssh server is supported -START_SSH_SERVER = true -SSH_PORT = 2222 -SSH_LISTEN_PORT = 2222 -BUILTIN_SSH_SERVER_USER = git -LFS_START_SERVER = true -HTTP_ADDR = 0.0.0.0 -TRUSTED_PROXIES = 127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 -DOMAIN = git.np-dms.work -LFS_JWT_SECRET = vZwOPm04XQALJOFpEDT3QkSL0xBTWDD1d-MVCk4CLLA -OFFLINE_MODE = true - -[database] -PATH = /var/lib/gitea/data/gitea.db -DB_TYPE = postgres -HOST = gitea_db:5432 -NAME = gitea -USER = gitea -PASSWD = gitea -SCHEMA = -SSL_MODE = disable -LOG_SQL = false - -[session] -PROVIDER_CONFIG = /var/lib/gitea/data/sessions -PROVIDER = file - -[picture] -AVATAR_UPLOAD_PATH = /var/lib/gitea/data/avatars -REPOSITORY_AVATAR_UPLOAD_PATH = /var/lib/gitea/data/repo-avatars - -[attachment] -PATH = /var/lib/gitea/data/attachments - -[log] -ROOT_PATH = /var/lib/gitea/data/log -MODE = console -LEVEL = info - -[security] -INSTALL_LOCK = false -SECRET_KEY = -REVERSE_PROXY_LIMIT = 1 -REVERSE_PROXY_TRUSTED_PROXIES = * -INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3NTgyNjA3Nzl9.dPR2iO3dy95JDbDoO4_xbbnlUVtUm9d7hUL18AkpdOQ -PASSWORD_HASH_ALGO = pbkdf2 - -[service] -DISABLE_REGISTRATION = false -REQUIRE_SIGNIN_VIEW = false -REGISTER_EMAIL_CONFIRM = false -ENABLE_NOTIFY_MAIL = false -ALLOW_ONLY_EXTERNAL_REGISTRATION = false -ENABLE_CAPTCHA = false -DEFAULT_KEEP_EMAIL_PRIVATE = false -DEFAULT_ALLOW_CREATE_ORGANIZATION = true -DEFAULT_ENABLE_TIMETRACKING = true -NO_REPLY_ADDRESS = noreply.git.np-dms.work - -[lfs] -PATH = /var/lib/gitea/git/lfs - -[mailer] -ENABLED = false - -[openid] -ENABLE_OPENID_SIGNIN = true -ENABLE_OPENID_SIGNUP = true - -[cron.update_checker] -ENABLED = false - -[repository.pull-request] -DEFAULT_MERGE_STYLE = merge - -[repository.signing] -DEFAULT_TRUST_MODEL = committer - -[oauth2] -JWT_SECRET = KPqbqCdEM_1gT-_fM8XKnrP6hdhh4Hf1l9jU40n7Jj0 diff --git a/index.html b/index.html deleted file mode 100644 index 21c2e323..00000000 --- a/index.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - ระบบจัดการเอกสารสำหรับโครงการ - - - - - - - - - -
-
-

ระบบจัดการเอกสารสำหรับโครงการ

-

ศูนย์กลางเอกสารสำหรับทุกโครงการของคุณ เข้าถึงง่าย ปลอดภัย

- เริ่มต้นใช้งาน -
-
- - - - - \ No newline at end of file diff --git a/mariadb/data/aria_log.00000001 b/mariadb/data/aria_log.00000001 index 719fe641..0a9fa23d 100644 Binary files a/mariadb/data/aria_log.00000001 and b/mariadb/data/aria_log.00000001 differ diff --git a/mariadb/data/aria_log_control b/mariadb/data/aria_log_control index 854eb537..cebd8b10 100644 Binary files a/mariadb/data/aria_log_control and b/mariadb/data/aria_log_control differ diff --git a/mariadb/docker-compose.yml b/mariadb/docker-compose.yml new file mode 100755 index 00000000..5a6e8097 --- /dev/null +++ b/mariadb/docker-compose.yml @@ -0,0 +1,87 @@ +# File: mariadb/docker-compose.yml +# DMS Container v0_8_0 แยก service/ /lcbp3-db +x-restart: &restart_policy + restart: unless-stopped + +x-logging: &default_logging + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "5" +services: + mariadb: + <<: [*restart_policy, *default_logging] + image: mariadb:10.11 + container_name: dms_mariadb + stdin_open: true + tty: true + deploy: + resources: + limits: + cpus: "2.0" + memory: 4G + reservations: + cpus: "0.5" + memory: 1G + environment: + MYSQL_ROOT_PASSWORD: "Center#2025" + MYSQL_DATABASE: "dms" + MYSQL_USER: "center" + MYSQL_PASSWORD: "Center#2025" + TZ: "Asia/Bangkok" + expose: + - "80" + volumes: + - "/share/Container/dms/mariadb/data:/var/lib/mysql" + - "/share/Container/dms/mariadb/my.cnf:/etc/mysql/conf.d/my.cnf:ro" + - "/share/Container/dms/mariadb/init:/docker-entrypoint-initdb.d:ro" + - "/share/dms-data/mariadb/backup:/backup" + healthcheck: + test: + ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -pCenter#2025 || exit 1"] + interval: 10s + timeout: 5s + retries: 15 + networks: + lcbp3: {} + + phpmyadmin: + <<: [*restart_policy, *default_logging] + image: phpmyadmin:5-apache + container_name: dms_phpmyadmin + stdin_open: true + tty: true + deploy: + resources: + limits: + cpus: "0.25" + memory: 256M + environment: + TZ: "Asia/Bangkok" + PMA_HOST: "mariadb" + PMA_PORT: "3306" + PMA_ABSOLUTE_URI: "https://pma.np-dms.work/" + UPLOAD_LIMIT: "256M" + MEMORY_LIMIT: "512M" + expose: + - "80" + volumes: + - "/share/Container/dms/phpmyadmin/config.user.inc.php:/etc/phpmyadmin/config.user.inc.php:ro" + - "/share/Container/dms/phpmyadmin/zzz-custom.ini:/usr/local/etc/php/conf.d/zzz-custom.ini:ro" + - "/share/Container/dms/phpmyadmin/sessions:/sessions:rw" + - "/share/Container/dms/phpmyadmin/tmp:/var/lib/phpmyadmin/tmp:rw" + - "/share/Container/dms/logs/phpmyadmin:/var/log/apache2" + depends_on: + mariadb: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"] + interval: 15s + timeout: 5s + retries: 20 + networks: + lcbp3: {} +networks: + lcbp3: + external: true diff --git a/npm/data/database.sqlite b/npm/data/database.sqlite index 5b614558..e54d34c5 100644 Binary files a/npm/data/database.sqlite and b/npm/data/database.sqlite differ diff --git a/npm/data/nginx/proxy_host/5.conf b/npm/data/nginx/proxy_host/5.conf index ae2bad54..87614f4b 100644 --- a/npm/data/nginx/proxy_host/5.conf +++ b/npm/data/nginx/proxy_host/5.conf @@ -29,8 +29,8 @@ listen [::]:443 ssl; include conf.d/include/letsencrypt-acme-challenge.conf; include conf.d/include/ssl-cache.conf; include conf.d/include/ssl-ciphers.conf; - ssl_certificate /etc/letsencrypt/live/npm-10/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/npm-10/privkey.pem; + ssl_certificate /etc/letsencrypt/live/npm-17/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/npm-17/privkey.pem; diff --git a/style.css b/style.css deleted file mode 100644 index 48ddca6c..00000000 --- a/style.css +++ /dev/null @@ -1,120 +0,0 @@ -/* --- CSS Reset & Basic Setup --- */ -:root { - --sea-blue: #005f73; - --teal: #0a9396; - --light-blue: #94d2bd; - --background-color: #e9f5f5; - --white: #ffffff; - --text-color: #001219; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Sarabun', sans-serif; - background-color: var(--background-color); - color: var(--text-color); - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.container { - width: 90%; - max-width: 1200px; - margin: 0 auto; -} - -/* --- Header & Footer --- */ -.site-header, .site-footer { - background-color: var(--sea-blue); - color: var(--white); - padding: 1rem 0; -} - -.site-header .container, .site-footer .container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.logo { - font-weight: 700; - font-size: 1.2rem; -} - -.login-button-nav { - color: var(--sea-blue); - background-color: var(--white); - padding: 0.5rem 1.5rem; - text-decoration: none; - border-radius: 5px; - font-weight: 700; - transition: background-color 0.3s, color 0.3s; -} - -.login-button-nav:hover { - background-color: var(--light-blue); -} - -.site-footer a { - color: var(--white); - text-decoration: none; - margin: 0 1rem; -} - -.site-footer a:hover { - text-decoration: underline; -} - -/* --- Hero Section (Main Content) --- */ -.hero-section { - flex-grow: 1; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - padding: 2rem 0; -} - -.hero-content h1 { - font-size: 3rem; - font-weight: 700; - margin-bottom: 1rem; -} - -.hero-content p { - font-size: 1.2rem; - margin-bottom: 2rem; - color: #333; -} - -.cta-button { - display: inline-block; - background-color: var(--teal); - color: var(--white); - font-size: 1.2rem; - font-weight: 700; - padding: 1rem 3rem; - text-decoration: none; - border-radius: 8px; - transition: background-color 0.3s; -} - -.cta-button:hover { - background-color: var(--sea-blue); -} - -/* --- Responsive for Mobile --- */ -@media (max-width: 768px) { - .hero-content h1 { - font-size: 2rem; - } - .hero-content p { - font-size: 1rem; - } -} \ No newline at end of file