diff --git a/.gitignore b/.gitignore index 30cce58..b2de018 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ yarn-error.log* # idea files .idea + +certificates \ No newline at end of file diff --git a/package.json b/package.json index b9da9b0..31eac10 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@better-auth/passkey": "^1.4.17", "@hookform/resolvers": "^3.9.1", - "@polinetwork/backend": "^0.14.0", + "@polinetwork/backend": "file:../backend/package/dist/", "@radix-ui/react-alert-dialog": "^1.1.3", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-collapsible": "^1.1.1", @@ -25,7 +26,7 @@ "@radix-ui/react-progress": "^1.1.3", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.4", "@t3-oss/env-nextjs": "^0.13.10", "@tanstack/react-query": "^5.90.19", @@ -34,8 +35,8 @@ "@trpc/next": "11.5.1", "@trpc/react-query": "11.5.1", "@trpc/tanstack-react-query": "11.5.1", - "better-auth": "^1.4.15", "babel-plugin-react-compiler": "1.0.0", + "better-auth": "^1.4.17", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -47,6 +48,7 @@ "postgres": "^3.4.4", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-error-boundary": "^6.1.0", "react-hook-form": "^7.55.0", "server-only": "^0.0.1", "sonner": "^2.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afcba2c..7f7f662 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,15 @@ importers: .: dependencies: + '@better-auth/passkey': + specifier: ^1.4.17 + version: 1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.17(next@15.5.9(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(better-call@1.1.8(zod@4.3.5))(nanostores@1.1.0) '@hookform/resolvers': specifier: ^3.9.1 version: 3.10.0(react-hook-form@7.55.0(react@18.3.1)) '@polinetwork/backend': - specifier: ^0.14.0 - version: 0.14.0 + specifier: file:../backend/package/dist/ + version: dist@file:../backend/package/dist '@radix-ui/react-alert-dialog': specifier: ^1.1.3 version: 1.1.6(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -45,7 +48,7 @@ importers: specifier: ^1.1.0 version: 1.1.2(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': - specifier: ^1.1.0 + specifier: ^1.1.2 version: 1.1.2(@types/react@18.3.18)(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.1.4 @@ -75,8 +78,8 @@ importers: specifier: 1.0.0 version: 1.0.0 better-auth: - specifier: ^1.4.15 - version: 1.4.15(next@15.5.9(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.4.17 + version: 1.4.17(next@15.5.9(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -110,6 +113,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-error-boundary: + specifier: ^6.1.0 + version: 6.1.0(react@18.3.1) react-hook-form: specifier: ^7.55.0 version: 7.55.0(react@18.3.1) @@ -181,8 +187,8 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} - '@better-auth/core@1.4.15': - resolution: {integrity: sha512-uAvq8YA7SaS7v+TrvH/Kwt7LAJihzUqB3FX8VweDsqu3gn5t51M+Bve+V1vVWR9qBAtC6cN68V6b+scxZxDY4A==} + '@better-auth/core@1.4.17': + resolution: {integrity: sha512-WSaEQDdUO6B1CzAmissN6j0lx9fM9lcslEYzlApB5UzFaBeAOHNUONTdglSyUs6/idiZBoRvt0t/qMXCgIU8ug==} peerDependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -191,10 +197,20 @@ packages: kysely: ^0.28.5 nanostores: ^1.0.1 - '@better-auth/telemetry@1.4.15': - resolution: {integrity: sha512-7NW/2PS4RN85rv+ozpAezP/kSLPZeWkxqcA6RA/CFXqWp2YR2e5q5E6Hym1qBgVBkoAQa3lWFdX3b+jEs+vvrQ==} + '@better-auth/passkey@1.4.17': + resolution: {integrity: sha512-SmS7a3UYi9A0muo4fWx1wEn59vtjoOLBFD5PnRo330BcXro6mHWxG/IXtnOnyeQKNGnEp4X7/QXcRrW/nDH0Iw==} + peerDependencies: + '@better-auth/core': 1.4.17 + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + better-auth: 1.4.17 + better-call: 1.1.8 + nanostores: ^1.0.1 + + '@better-auth/telemetry@1.4.17': + resolution: {integrity: sha512-R1BC4e/bNjQbXu7lG6ubpgmsPj7IMqky5DvMlzAtnAJWJhh99pMh/n6w5gOHa0cqDZgEAuj75IPTxv+q3YiInA==} peerDependencies: - '@better-auth/core': 1.4.15 + '@better-auth/core': 1.4.17 '@better-auth/utils@0.3.0': resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} @@ -273,6 +289,9 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@hexagon/base64@1.1.28': + resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + '@hookform/resolvers@3.10.0': resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} peerDependencies: @@ -415,6 +434,9 @@ packages: cpu: [x64] os: [win32] + '@levischuck/tiny-cbor@0.2.11': + resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + '@next/env@15.5.9': resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==} @@ -474,9 +496,42 @@ packages: resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} - '@polinetwork/backend@0.14.0': - resolution: {integrity: sha512-VSkSLetxprjPu38WFK580uQ3VBtflM4Srz+D32ONtL0u9SUjixSq5lNwPXcQmGqiByrV3RIA3Z2WBJY2A9Lk4w==} - engines: {node: '>=24.8.0', pnpm: '>=10.17.1'} + '@peculiar/asn1-android@2.6.0': + resolution: {integrity: sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ==} + + '@peculiar/asn1-cms@2.6.0': + resolution: {integrity: sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==} + + '@peculiar/asn1-csr@2.6.0': + resolution: {integrity: sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==} + + '@peculiar/asn1-ecc@2.6.0': + resolution: {integrity: sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==} + + '@peculiar/asn1-pfx@2.6.0': + resolution: {integrity: sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==} + + '@peculiar/asn1-pkcs8@2.6.0': + resolution: {integrity: sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==} + + '@peculiar/asn1-pkcs9@2.6.0': + resolution: {integrity: sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==} + + '@peculiar/asn1-rsa@2.6.0': + resolution: {integrity: sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==} + + '@peculiar/asn1-schema@2.6.0': + resolution: {integrity: sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==} + + '@peculiar/asn1-x509-attr@2.6.0': + resolution: {integrity: sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==} + + '@peculiar/asn1-x509@2.6.0': + resolution: {integrity: sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==} + + '@peculiar/x509@1.14.3': + resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} + engines: {node: '>=20.0.0'} '@radix-ui/number@1.1.0': resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} @@ -1017,6 +1072,13 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@simplewebauthn/browser@13.2.2': + resolution: {integrity: sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==} + + '@simplewebauthn/server@13.2.2': + resolution: {integrity: sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==} + engines: {node: '>=20.0.0'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1233,11 +1295,15 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} + asn1js@3.0.7: + resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==} + engines: {node: '>=12.0.0'} + babel-plugin-react-compiler@1.0.0: resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} - better-auth@1.4.15: - resolution: {integrity: sha512-XZr4GnFPbjvf8wip8AAjTrpGNn3Sba600zT+DgsR3NNCMWCt9aD8+nuRah6BHwHWnVP1nfnby07tPmti72SRBw==} + better-auth@1.4.17: + resolution: {integrity: sha512-VmHGQyKsEahkEs37qguROKg/6ypYpNF13D7v/lkbO7w7Aivz0Bv2h+VyUkH4NzrGY0QBKXi1577mGhDCVwp0ew==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -1346,6 +1412,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dist@file:../backend/package/dist: + resolution: {directory: ../backend/package/dist, type: directory} + enhanced-resolve@5.18.1: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} @@ -1520,11 +1589,23 @@ packages: peerDependencies: react: '>=16.0.0' + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.5: + resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} + engines: {node: '>=16.0.0'} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: react: ^18.3.1 + react-error-boundary@6.1.0: + resolution: {integrity: sha512-02k9WQ/mUhdbXir0tC1NiMesGzRPaCsJEWU/4bcFrbY1YMZOtHShtZP6zw0SJrBWA/31H0KT9/FgdL8+sPKgHA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-hook-form@7.55.0: resolution: {integrity: sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==} engines: {node: '>=18.0.0'} @@ -1565,6 +1646,9 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + rou3@0.7.12: resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} @@ -1579,8 +1663,8 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} @@ -1634,9 +1718,16 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsyringe@4.10.0: + resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} + engines: {node: '>= 6.0.0'} + typescript@5.7.3: resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} @@ -1681,7 +1772,7 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@better-auth/core@1.4.15(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)': + '@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)': dependencies: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -1692,9 +1783,21 @@ snapshots: nanostores: 1.1.0 zod: 4.3.5 - '@better-auth/telemetry@1.4.15(@better-auth/core@1.4.15(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0))': + '@better-auth/passkey@1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0))(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-auth@1.4.17(next@15.5.9(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(better-call@1.1.8(zod@4.3.5))(nanostores@1.1.0)': + dependencies: + '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + '@simplewebauthn/browser': 13.2.2 + '@simplewebauthn/server': 13.2.2 + better-auth: 1.4.17(next@15.5.9(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + better-call: 1.1.8(zod@4.3.5) + nanostores: 1.1.0 + zod: 4.3.5 + + '@better-auth/telemetry@1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0))': dependencies: - '@better-auth/core': 1.4.15(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) + '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 @@ -1759,6 +1862,8 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@hexagon/base64@1.1.28': {} + '@hookform/resolvers@3.10.0(react-hook-form@7.55.0(react@18.3.1))': dependencies: react-hook-form: 7.55.0(react@18.3.1) @@ -1860,6 +1965,8 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@levischuck/tiny-cbor@0.2.11': {} + '@next/env@15.5.9': {} '@next/swc-darwin-arm64@15.5.7': @@ -1890,7 +1997,101 @@ snapshots: '@noble/hashes@2.0.1': {} - '@polinetwork/backend@0.14.0': {} + '@peculiar/asn1-android@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-cms@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + '@peculiar/asn1-x509-attr': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-csr@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-ecc@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-pfx@2.6.0': + dependencies: + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-pkcs8': 2.6.0 + '@peculiar/asn1-rsa': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs8@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs9@2.6.0': + dependencies: + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-pfx': 2.6.0 + '@peculiar/asn1-pkcs8': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + '@peculiar/asn1-x509-attr': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-rsa@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-schema@2.6.0': + dependencies: + asn1js: 3.0.7 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/asn1-x509-attr@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + asn1js: 3.0.7 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.6.0': + dependencies: + '@peculiar/asn1-schema': 2.6.0 + asn1js: 3.0.7 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/x509@1.14.3': + dependencies: + '@peculiar/asn1-cms': 2.6.0 + '@peculiar/asn1-csr': 2.6.0 + '@peculiar/asn1-ecc': 2.6.0 + '@peculiar/asn1-pkcs9': 2.6.0 + '@peculiar/asn1-rsa': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + pvtsutils: 1.3.6 + reflect-metadata: 0.2.2 + tslib: 2.8.1 + tsyringe: 4.10.0 '@radix-ui/number@1.1.0': {} @@ -2410,6 +2611,19 @@ snapshots: '@radix-ui/rect@1.1.0': {} + '@simplewebauthn/browser@13.2.2': {} + + '@simplewebauthn/server@13.2.2': + dependencies: + '@hexagon/base64': 1.1.28 + '@levischuck/tiny-cbor': 0.2.11 + '@peculiar/asn1-android': 2.6.0 + '@peculiar/asn1-ecc': 2.6.0 + '@peculiar/asn1-rsa': 2.6.0 + '@peculiar/asn1-schema': 2.6.0 + '@peculiar/asn1-x509': 2.6.0 + '@peculiar/x509': 1.14.3 + '@standard-schema/spec@1.1.0': {} '@swc/helpers@0.5.15': @@ -2569,14 +2783,20 @@ snapshots: dependencies: tslib: 2.8.1 + asn1js@3.0.7: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + babel-plugin-react-compiler@1.0.0: dependencies: '@babel/types': 7.28.2 - better-auth@1.4.15(next@15.5.9(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + better-auth@1.4.17(next@15.5.9(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@better-auth/core': 1.4.15(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) - '@better-auth/telemetry': 1.4.15(@better-auth/core@1.4.15(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)) + '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0) + '@better-auth/telemetry': 1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.5))(jose@6.1.3)(kysely@0.28.10)(nanostores@1.1.0)) '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 @@ -2597,7 +2817,7 @@ snapshots: '@better-auth/utils': 0.3.0 '@better-fetch/fetch': 1.1.21 rou3: 0.7.12 - set-cookie-parser: 2.7.1 + set-cookie-parser: 2.7.2 optionalDependencies: zod: 4.3.5 @@ -2638,6 +2858,8 @@ snapshots: detect-node-es@1.1.0: {} + dist@file:../backend/package/dist: {} + enhanced-resolve@5.18.1: dependencies: graceful-fs: 4.2.11 @@ -2776,12 +2998,22 @@ snapshots: clsx: 2.1.1 react: 18.3.1 + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.5: {} + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 react: 18.3.1 scheduler: 0.23.2 + react-error-boundary@6.1.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-hook-form@7.55.0(react@18.3.1): dependencies: react: 18.3.1 @@ -2817,6 +3049,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + reflect-metadata@0.2.2: {} + rou3@0.7.12: {} scheduler@0.23.2: @@ -2828,7 +3062,7 @@ snapshots: server-only@0.0.1: {} - set-cookie-parser@2.7.1: {} + set-cookie-parser@2.7.2: {} sharp@0.34.5: dependencies: @@ -2895,8 +3129,14 @@ snapshots: tapable@2.2.1: {} + tslib@1.14.1: {} + tslib@2.8.1: {} + tsyringe@4.10.0: + dependencies: + tslib: 1.14.1 + typescript@5.7.3: {} undici-types@6.19.8: {} diff --git a/src/app/dashboard/(active)/account/page.tsx b/src/app/dashboard/(active)/account/page.tsx index 7ee6670..4f71631 100644 --- a/src/app/dashboard/(active)/account/page.tsx +++ b/src/app/dashboard/(active)/account/page.tsx @@ -1,7 +1,11 @@ -import { CircleAlert, UserIcon } from "lucide-react" +import { Calendar, CircleAlert, KeyIcon, UserIcon } from "lucide-react" +import { headers } from "next/headers" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { auth } from "@/lib/auth" import { getInitials } from "@/lib/utils" import { getServerSession } from "@/server/auth" +import { NewPasskeyButton } from "./passkey-button" import { SetName } from "./set-name" import { Telegram } from "./telegram" @@ -9,13 +13,20 @@ export default async function Account() { const { data: session } = await getServerSession() if (!session) return + const { data: passkeys, error } = await auth.passkey.listUserPasskeys({ + fetchOptions: { + headers: await headers(), + }, + }) + + console.log(passkeys, error) + const { user } = session return (

Account

- -
+
{user.image && } @@ -40,6 +51,25 @@ export default async function Account() {
+
+

Passkeys

+ {passkeys?.map((p) => ( +
+
+ +
+
+

{p.name}

+

+ + Created on {p.createdAt.toLocaleDateString()} +

+
+ +
+ ))} + +
) } diff --git a/src/app/dashboard/(active)/account/passkey-button.tsx b/src/app/dashboard/(active)/account/passkey-button.tsx new file mode 100644 index 0000000..0cd4836 --- /dev/null +++ b/src/app/dashboard/(active)/account/passkey-button.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useRouter } from "next/navigation" +import { useState } from "react" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { auth } from "@/lib/auth" + +export function NewPasskeyButton() { + const router = useRouter() + const [isLoading, setIsLoading] = useState(false) + + return ( + + ) +} diff --git a/src/app/dashboard/(active)/assoc/_components/set-assoc-number-dialog.tsx b/src/app/dashboard/(active)/assoc/_components/set-assoc-number-dialog.tsx new file mode 100644 index 0000000..843d688 --- /dev/null +++ b/src/app/dashboard/(active)/assoc/_components/set-assoc-number-dialog.tsx @@ -0,0 +1,90 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { useRouter } from "next/navigation" +import { useState } from "react" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useTRPC } from "@/lib/trpc/client" + +export function SetAssocNumberDialog({ userId, children }: { userId: string; children?: React.ReactNode }) { + const [value, setValue] = useState("") + const [open, setOpen] = useState(false) + const trpc = useTRPC() + const _router = useRouter() + + const qc = useQueryClient() + const { mutateAsync, isPending } = useMutation(trpc.azure.members.setAssocNumber.mutationOptions()) + + function handleOpenChange(v: boolean): void { + setOpen(v) + setValue("") + } + + async function handleSubmit(e: React.FormEvent) { + console.log("submit") + e.preventDefault() + if (isPending || !value || Number.isNaN(parseInt(value, 10))) return + + const res = await mutateAsync({ userId, assocNumber: parseInt(value, 10) }) + handleOpenChange(false) + if (res.error !== null) { + toast.error("There was an error") + console.error(res.error) + return + } + + console.log("Updated user assocNumber", userId, value) + await qc.invalidateQueries({ queryKey: trpc.azure.members.getAll.queryKey() }) + toast.success(`Updated successfully!`) + } + + return ( + + {children ?? } + + + Set Assoc Number + This changes the `employeeId` field in the Azure User properties. + +
+
+ + setValue(e.target.value)} + /> +
+ + + + + + +
+
+
+ ) +} diff --git a/src/app/dashboard/(active)/assoc/columns.tsx b/src/app/dashboard/(active)/assoc/columns.tsx new file mode 100644 index 0000000..9de908f --- /dev/null +++ b/src/app/dashboard/(active)/assoc/columns.tsx @@ -0,0 +1,60 @@ +"use client" +import { createColumnHelper, type Row } from "@tanstack/react-table" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import type { ApiOutput } from "@/lib/trpc/types" +import { SetAssocNumberDialog } from "./_components/set-assoc-number-dialog" + +type ParsedUser = ApiOutput["azure"]["members"]["getAll"][0] +const ch = createColumnHelper() + +export const columns = [ + ch.accessor("employeeId", { + id: "number", + header: "#", + footer: (props) => props.column.id, + cell: ({ getValue, row }) => { + const value = getValue() + return value ? ( + {value} + ) : ( + + + + ) + }, + }), + ch.accessor("displayName", { + id: "displayName", + header: "Full Name", + cell: ({ getValue, row }) => ( + <> + {row.original.isMember && Socio} + {getValue()} + + ), + }), + ch.accessor("mail", { id: "mail", header: "Email" }), + ch.accessor("assignedLicensesIds", { + id: "licenses", + header: "Licenses", + cell: ({ getValue }) => { + const licenses = getValue().sort((a, b) => a.localeCompare(b)) + return licenses.map((l) => ( + + {l} + + )) + }, + }), + ch.group({ + id: "actions", + cell: (props) => , + }), +] + +function RowActions({ row }: { row: Row }) { + return
+} diff --git a/src/app/dashboard/(active)/assoc/page.tsx b/src/app/dashboard/(active)/assoc/page.tsx new file mode 100644 index 0000000..477c03b --- /dev/null +++ b/src/app/dashboard/(active)/assoc/page.tsx @@ -0,0 +1,19 @@ +import { Suspense } from "react" +import { ErrorBoundary } from "react-error-boundary" +import { Spinner } from "@/components/spinner" +import { getQueryClient, trpc } from "@/lib/trpc/server" +import { AssocTable } from "./table" + +export default async function AssocIndex() { + const qc = getQueryClient() + void qc.prefetchQuery(trpc.azure.members.getAll.queryOptions()) + return ( +
+ Something went wrong
}> + }> + + + + + ) +} diff --git a/src/app/dashboard/(active)/assoc/table.tsx b/src/app/dashboard/(active)/assoc/table.tsx new file mode 100644 index 0000000..765da54 --- /dev/null +++ b/src/app/dashboard/(active)/assoc/table.tsx @@ -0,0 +1,54 @@ +"use client" + +import { useSuspenseQuery } from "@tanstack/react-query" +import { CreateAssocUser } from "@/components/create-assoc-member" +import { DataTable } from "@/components/data-table" +import { Badge } from "@/components/ui/badge" +import { useTRPC } from "@/lib/trpc/client" +import type { ApiOutput } from "@/lib/trpc/types" +import { columns } from "./columns" + +type ParsedUser = ApiOutput["azure"]["members"]["getAll"][0] +function sortByAssocNumber(users: ParsedUser[]) { + if (!users || users.length === 0) return users + if (users.every((u) => !u.employeeId)) + return users.sort((a, b) => (a.displayName ?? "").localeCompare(b.displayName ?? "")) + + return users.sort((a, b) => { + if (a.employeeId && b.employeeId) { + const aInt = parseInt(a.employeeId, 10) + const bInt = parseInt(b.employeeId, 10) + if (Number.isNaN(aInt) && Number.isNaN(bInt)) return 0 + if (Number.isNaN(aInt)) return 1 + if (Number.isNaN(bInt)) return -1 + return aInt - bInt + } + if (a.employeeId) return -1 + if (b.employeeId) return 1 + return 0 + }) +} + +export function AssocTable() { + const trpc = useTRPC() + const { data: users } = useSuspenseQuery(trpc.azure.members.getAll.queryOptions()) + + return ( +
+
+

Utenti MS @polinetwork.org

+ {users.length} Soci + {users.length} fuori + {users.filter((u) => u.assignedLicensesIds.includes("OFFICE_365")).length} licenze Office +
+ +
+ + +
+ ) +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index f5e9888..c0316b3 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,3 +1,4 @@ +import { cookies } from "next/headers" import { redirect } from "next/navigation" import { SidebarProvider } from "@/components/ui/sidebar" import { getQueryClient, trpc } from "@/lib/trpc/server" @@ -18,5 +19,8 @@ export default async function AdminLayout({ children }: { children: React.ReactN if (!roles || roles.length === 0) redirect("/onboarding/no-role") if (roles.includes("creator")) redirect("/onboarding/unauthorized") - return {children} + const cookieStore = await cookies() + const defaultOpen = cookieStore.get("sidebar:state")?.value === "true" + + return {children} } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 985c474..ef158bb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,11 +1,12 @@ -import { GeistSans } from "geist/font/sans" import type { Metadata } from "next" +import { Poppins } from "next/font/google" import "@/index.css" import { HEADER_HEIGHT, Header } from "@/components/header" import { ThemeProvider } from "@/components/theme-provider" import { Toaster } from "@/components/ui/sonner" import { TooltipProvider } from "@/components/ui/tooltip" import { TRPCReactProvider } from "@/lib/trpc/client" +import { HydrateClient } from "@/lib/trpc/server" const desc = "PoliNetwork Admin Dashboard" @@ -18,9 +19,11 @@ export const metadata: Metadata = { icons: [{ rel: "icon", url: "/favicon.ico" }], } +const poppins = Poppins({ weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], subsets: ["latin"] }) + export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( - + -
-
- {children} -
+ +
+
+ {children} +
+
diff --git a/src/app/login/login-form.tsx b/src/app/login/login-form.tsx index aae38a4..feae630 100644 --- a/src/app/login/login-form.tsx +++ b/src/app/login/login-form.tsx @@ -1,11 +1,10 @@ "use client" -import { Loader2 } from "lucide-react" +import { ArrowRight, Loader2 } from "lucide-react" import { useRouter } from "next/navigation" import { useState } from "react" import { toast } from "sonner" import { Button } from "@/components/ui/button" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp" import { Label } from "@/components/ui/label" @@ -14,16 +13,36 @@ import { auth } from "@/lib/auth" export default function LoginForm() { const [email, setEmail] = useState("") const [sent, setSent] = useState(false) + const _router = useRouter() + + // useEffect(() => { + // if (!PublicKeyCredential.isConditionalMediationAvailable || + // !PublicKeyCredential.isConditionalMediationAvailable()) { + // return; + // } + // + // void auth.signIn.passkey({ autoFill: true }).then(({ data, error }) => { + // if (error) { + // if ("code" in error && error.code === "AUTH_CANCELLED") return + // console.error("ERROR PASSKEY LOGIN", error) + // toast.error("Error passkey") + // return + // } + // + // console.log({ data }) + // toast.success("Logged in with passkey!") + // router.refresh() + // }) + // }, []) return ( -
- {!sent ? ( - setEmail(v)} onSend={() => setSent(true)} /> - ) : ( - - )} +
+ setEmail(v)} onSend={() => setSent(true)} /> + {sent && }
) + // Or continue with + // } function EmailCard({ @@ -36,8 +55,19 @@ function EmailCard({ onSend: () => void }) { const [loading, setLoading] = useState(false) + const router = useRouter() async function sendOtp() { + const { data: passkeyData, error: passkeyError } = await auth.signIn.passkey({ + autoFill: true, + }) + + if (passkeyData && !passkeyError) { + toast.success("Logged in with passkey!") + router.push("/dashboard") + return + } + const { data, error } = await auth.emailOtp.sendVerificationOtp({ type: "sign-in", email, @@ -57,45 +87,48 @@ function EmailCard({ return } + if (error?.code === "INVALID_EMAIL") { + toast.error("Invalid email") + return + } + toast.error("There was an unexpected error") console.error({ error }) } return ( - - - Sign In - - Enter your email below to login to your account - - - -
{ - e.preventDefault() - await sendOtp() - }} - > -
- +
+ { + e.preventDefault() + setLoading(true) + await sendOtp() + setLoading(false) + }} + > +
+ +
{ onChange(e.target.value) }} value={email} + autoComplete="email webauthn" /> +
- - - - +
+ +
) } @@ -130,47 +163,39 @@ function OTPCard({ email }: { email: string }) { } } return ( - - - Sign In - Enter the OTP we sent to your email - - -
{ - e.preventDefault() - await verifyOtp() - }} + { + e.preventDefault() + await verifyOtp() + }} + > +
+ + setOtp(v)} + autoComplete="off" + data-1p-ignore + data-lpignore="true" + data-protonpass-ignore="true" + type="text" > -
- - setOtp(v)} - autoComplete="off" - data-1p-ignore - data-lpignore="true" - data-protonpass-ignore="true" - type="text" - > - - - - - - - - - -
- - - - + + + + + + + + +
+
+ + ) } diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index e701bc7..c4c10db 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -7,7 +7,11 @@ export default async function Page() { if (session) return redirect("/dashboard") return ( -
+
+

Login

+

+ Enter your email below to recieve an OTP to login or use a registered passkey. +

) diff --git a/src/app/onboarding/link/telegram.tsx b/src/app/onboarding/link/telegram.tsx index 2d97f5a..7f33a13 100644 --- a/src/app/onboarding/link/telegram.tsx +++ b/src/app/onboarding/link/telegram.tsx @@ -3,6 +3,7 @@ import { APIError } from "better-auth/api" import { CircleCheckBig, ClockAlertIcon } from "lucide-react" import { useRouter } from "next/navigation" import { useCallback, useEffect, useState } from "react" +import { toast } from "sonner" import { Code } from "@/components/code" import { InputWithPrefix } from "@/components/input-prefix" import { Button } from "@/components/ui/button" @@ -120,11 +121,25 @@ export function TelegramLink({ botUsername }: { botUsername: string }) { return savedLink && timeLeft ? (
-

- {savedLink.code.split("").map((c, i) => ( - {c} - ))} -

+ setExpired(true)} />
diff --git a/src/app/page.tsx b/src/app/page.tsx index eda2643..a2711ff 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,6 @@ import { redirect } from "next/navigation" import { getServerSession } from "@/server/auth" import { CanIAccess } from "./login/can-i-access" -import { LoginButton } from "./login/login-button" import { WhatIs } from "./login/what-is" export default async function IndexPage() { @@ -9,8 +8,7 @@ export default async function IndexPage() { if (session.data?.user) redirect("/dashboard") return ( -
- +
diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..d064c30 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..1b0d8fc --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/create-assoc-member.tsx b/src/components/create-assoc-member.tsx new file mode 100644 index 0000000..856482a --- /dev/null +++ b/src/components/create-assoc-member.tsx @@ -0,0 +1,130 @@ +"use client" +import { useMutation } from "@tanstack/react-query" +import { useState } from "react" +import { toast } from "sonner" +import { + Sheet, + SheetClose, + SheetContent, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet" +import { useTRPC } from "@/lib/trpc/client" +import { Button } from "./ui/button" +import { Input } from "./ui/input" +import { Label } from "./ui/label" + +type Props = { + trigger?: React.ReactNode +} + +export function CreateAssocUser({ trigger }: Props) { + const [open, setOpen] = useState(false) + const [firstName, setFirstName] = useState("") + const [lastName, setLastName] = useState("") + const [assocNumber, setAssocNumber] = useState("") + const [sendTo, setSendTo] = useState("") + const trpc = useTRPC() + + const { mutateAsync, isPending } = useMutation(trpc.azure.members.create.mutationOptions()) + + function handleOpenChange(v: boolean): void { + setOpen(v) + setFirstName("") + setLastName("") + setAssocNumber("") + setSendTo("") + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (!assocNumber || Number.isNaN(parseInt(assocNumber, 10))) return + + const res = await mutateAsync({ assocNumber: parseInt(assocNumber, 10), firstName, lastName, sendEmailTo: sendTo }) + if (res.error !== null) { + toast.error(res.error) + return + } + + console.log("Created user", res) + toast.success(`User created, email: ${res.email}`, { duration: 10_000 }) + } + + return ( + + {trigger ?? } + + + New Assoc Member + +
+
+
+ + setFirstName(e.target.value)} + /> +
+
+ + setLastName(e.target.value)} + /> +
+ +
+ + setAssocNumber(e.target.value)} + /> +
+ +
+ + setSendTo(e.target.value)} + /> +
+
+ + + + + + +
+
+
+ ) +} diff --git a/src/components/data-table.tsx b/src/components/data-table.tsx index bdd6798..eba2000 100644 --- a/src/components/data-table.tsx +++ b/src/components/data-table.tsx @@ -1,15 +1,22 @@ "use client" -import { type ColumnDef, flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table" +import { + type ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + type RowData, + useReactTable, +} from "@tanstack/react-table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -interface DataTableProps { +interface DataTableProps { columns: ColumnDef[] data: TData[] } -export function DataTable({ columns, data }: DataTableProps) { +export function DataTable({ columns, data }: DataTableProps) { const table = useReactTable({ data, columns, diff --git a/src/components/header-login-button.tsx b/src/components/header-login-button.tsx new file mode 100644 index 0000000..61b9d90 --- /dev/null +++ b/src/components/header-login-button.tsx @@ -0,0 +1,44 @@ +"use client" +import { LogIn, LogOut } from "lucide-react" +import Link from "next/link" +import { usePathname, useRouter } from "next/navigation" +import { useIsMobile } from "@/hooks/use-mobile" +import { auth, useSession } from "@/lib/auth" +import { Button } from "./ui/button" +import { Skeleton } from "./ui/skeleton" + +export function HeaderLoginButton() { + const { data, isPending } = useSession() + const pathname = usePathname() + const isMobile = useIsMobile() + const router = useRouter() + + if (isPending) return + if (data) + return ( + + ) + + return ( + + + + ) +} diff --git a/src/components/header.tsx b/src/components/header.tsx index 47c5bb0..55c341e 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,36 +1,31 @@ import { GlobeIcon } from "lucide-react" -import Image from "next/image" import Link from "next/link" -import logo from "@/assets/svg/logo.svg" import { ThemeButton } from "@/components/theme-button" +import { HeaderLoginButton } from "./header-login-button" +import { Logo } from "./logo" import { Button } from "./ui/button" export const HEADER_HEIGHT = "3.3rem" export async function Header() { return ( -
-
- -
- PoliNetwork Logo -

PoliNetwork Admin

-
- - - -
+
+ +
+ +

+ PoliNetwork Admin +

+
+ + +
) } diff --git a/src/components/logo.tsx b/src/components/logo.tsx new file mode 100644 index 0000000..f51fb95 --- /dev/null +++ b/src/components/logo.tsx @@ -0,0 +1,6 @@ +import Image, { type ImageProps } from "next/image" +import logo from "@/assets/logo.svg" + +export function Logo({ size, ...props }: { size?: number } & Omit) { + return PoliNetwork Logo +} diff --git a/src/components/sidebar/admin-sidebar.tsx b/src/components/sidebar/admin-sidebar.tsx index ef5fb96..12bbaab 100644 --- a/src/components/sidebar/admin-sidebar.tsx +++ b/src/components/sidebar/admin-sidebar.tsx @@ -28,6 +28,12 @@ const data = { }, ], }, + { + title: "Assoc", + url: "/dashboard/assoc", + icon: Users, + // items: [], + }, { title: "Management", //url: "/dashboard/management", diff --git a/src/components/theme-button.tsx b/src/components/theme-button.tsx index fbebe64..cbaa0ad 100644 --- a/src/components/theme-button.tsx +++ b/src/components/theme-button.tsx @@ -5,8 +5,10 @@ import { Button } from "./ui/button" export function ThemeButton() { const { resolvedTheme, setTheme } = useTheme() + + // TODO: remove disabled when light theme is ready return ( - diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..b47f184 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps { } + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index abfc91b..a841b07 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-30 [&_svg]:pointer-events-none [&_svg]:shrink-0", + "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-default disabled:opacity-30 [&_svg]:pointer-events-none [&_svg]:shrink-0", { variants: { variant: { @@ -36,7 +36,7 @@ const buttonVariants = cva( export interface ButtonProps extends React.ButtonHTMLAttributes, - VariantProps { + VariantProps { asChild?: boolean; } diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 050e3d5..7032ed5 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -84,7 +84,7 @@ const SidebarProvider = React.forwardRef< } // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE} SameSite=Lax`; }, [setOpenProp, open], ); @@ -623,7 +623,7 @@ const SidebarMenuAction = React.forwardRef< "peer-data-[size=lg]/menu-button:top-2.5", "group-data-[collapsible=icon]:hidden", showOnHover && - "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", + "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0", className, )} {...props} @@ -661,7 +661,7 @@ const SidebarMenuSkeleton = React.forwardRef< >(({ className, showIcon = false, ...props }, ref) => { // Random width between 50 to 90%. const width = React.useMemo(() => { - return `${Math.floor(Math.random() * 40) + 50}%`; + return `${Math.floor(Math.random() * 40) + 50}% `; }, []); return ( diff --git a/src/index.css b/src/index.css index 8784f7b..dc1d8d4 100644 --- a/src/index.css +++ b/src/index.css @@ -65,7 +65,7 @@ } .dark { - --background: oklch(0.22 0.03 249.42); + --background: oklch(0.19 0.03 256.45); --foreground: oklch(0.88 0.03 277.2); --card: oklch(0.24 0.03 259.05); --card-foreground: oklch(0.9 0.03 255.59); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index bac714a..f00cd8e 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,3 +1,4 @@ +import { passkeyClient } from "@better-auth/passkey/client" import { AUTH_PATH, type TelegramPlugin } from "@polinetwork/backend" import type { BetterAuthClientPlugin } from "better-auth" import { emailOTPClient } from "better-auth/client/plugins" @@ -15,7 +16,7 @@ const telegramPlugin = () => { export const auth = createAuthClient({ baseURL: getBaseUrl(), basePath: AUTH_PATH, - plugins: [telegramPlugin(), emailOTPClient(), nextCookies()], + plugins: [telegramPlugin(), emailOTPClient(), nextCookies(), passkeyClient()], }) export const { signIn, signOut, getSession, useSession } = auth diff --git a/src/lib/trpc/server.tsx b/src/lib/trpc/server.tsx index 05b288b..93d09af 100644 --- a/src/lib/trpc/server.tsx +++ b/src/lib/trpc/server.tsx @@ -1,8 +1,9 @@ import "server-only" // <-- ensure this file cannot be imported from the client import { type AppRouter, TRPC_PATH } from "@polinetwork/backend" +import { dehydrate, HydrationBoundary } from "@tanstack/react-query" import { createTRPCClient, httpLink } from "@trpc/client" -import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query" +import { createTRPCOptionsProxy, type TRPCQueryOptions } from "@trpc/tanstack-react-query" import { cache } from "react" import SuperJSON from "superjson" import { getBaseUrl } from "../utils" @@ -16,3 +17,17 @@ export const trpc = createTRPCOptionsProxy({ }), queryClient: getQueryClient, }) +export function HydrateClient(props: { children: React.ReactNode }) { + const queryClient = getQueryClient() + return {props.children} +} +// biome-ignore lint/suspicious/noExplicitAny: tRPC docs +export function prefetch>>(queryOptions: T) { + const queryClient = getQueryClient() + if (queryOptions.queryKey[1]?.type === "infinite") { + // biome-ignore lint/suspicious/noExplicitAny: tRPC docs + void queryClient.prefetchInfiniteQuery(queryOptions as any) + } else { + void queryClient.prefetchQuery(queryOptions) + } +} diff --git a/src/lib/trpc/types.tsx b/src/lib/trpc/types.tsx new file mode 100644 index 0000000..ca397f6 --- /dev/null +++ b/src/lib/trpc/types.tsx @@ -0,0 +1,6 @@ +import type { AppRouter } from "@polinetwork/backend" +import type { inferRouterError, inferRouterInputs, inferRouterOutputs } from "@trpc/server" + +export type ApiOutput = inferRouterOutputs +export type ApiInput = inferRouterInputs +export type ApiError = inferRouterError