From b6c91e8bd0203e2f30b433834291b8f73abf9cc0 Mon Sep 17 00:00:00 2001 From: Derock Date: Sun, 5 Nov 2023 21:37:59 -0500 Subject: [PATCH] wip: dashboard --- package.json | 4 + pnpm-lock.yaml | 278 +++++++++++++++++- src/app/Login.tsx | 4 +- src/app/dashboard/page.tsx | 11 - src/app/home/ChartGraph.tsx | 0 src/app/{dashboard => home}/RSC.tsx | 0 src/app/home/StatCard.module.css | 9 + src/app/home/StatCard.tsx | 52 ++++ src/app/home/page.tsx | 17 ++ src/app/settings/SettingsHeader.tsx | 21 ++ src/app/settings/account/page.tsx | 5 + src/app/settings/layout.tsx | 65 ++++ src/app/settings/page.tsx | 5 + src/app/settings/sessions/Sessions.tsx | 23 ++ src/app/settings/sessions/page.tsx | 15 + src/components/SidebarNav.tsx | 43 +++ src/components/ui/separator.tsx | 31 ++ .../api/routers/{auth.ts => auth/index.ts} | 5 +- src/server/api/routers/auth/sessions.tsx | 21 ++ src/server/auth/Session.ts | 15 +- 20 files changed, 604 insertions(+), 20 deletions(-) delete mode 100644 src/app/dashboard/page.tsx create mode 100644 src/app/home/ChartGraph.tsx rename src/app/{dashboard => home}/RSC.tsx (100%) create mode 100644 src/app/home/StatCard.module.css create mode 100644 src/app/home/StatCard.tsx create mode 100644 src/app/home/page.tsx create mode 100644 src/app/settings/SettingsHeader.tsx create mode 100644 src/app/settings/account/page.tsx create mode 100644 src/app/settings/layout.tsx create mode 100644 src/app/settings/page.tsx create mode 100644 src/app/settings/sessions/Sessions.tsx create mode 100644 src/app/settings/sessions/page.tsx create mode 100644 src/components/SidebarNav.tsx create mode 100644 src/components/ui/separator.tsx rename src/server/api/routers/{auth.ts => auth/index.ts} (94%) create mode 100644 src/server/api/routers/auth/sessions.tsx diff --git a/package.json b/package.json index cbcb02b..eded653 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@prisma/migrate": "^5.5.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@t3-oss/env-nextjs": "^0.7.1", "@tanstack/react-query": "^4.36.1", @@ -33,11 +34,13 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.11.0", + "recharts": "^2.9.2", "sonner": "^1.2.0", "superjson": "^2.2.0", "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7", "ts-permissions": "^1.0.0", + "ua-parser-js": "^1.0.37", "zod": "^3.22.4" }, "devDependencies": { @@ -48,6 +51,7 @@ "@types/node": "^20.8.10", "@types/react": "^18.2.35", "@types/react-dom": "^18.2.14", + "@types/ua-parser-js": "^0.7.38", "@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/parser": "^6.9.1", "autoprefixer": "^10.4.16", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a887e2..a76654d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.2.14)(@types/react@18.2.35)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.35)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.35)(react@18.2.0) @@ -74,6 +77,9 @@ dependencies: react-icons: specifier: ^4.11.0 version: 4.11.0(react@18.2.0) + recharts: + specifier: ^2.9.2 + version: 2.9.2(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) sonner: specifier: ^1.2.0 version: 1.2.0(react-dom@18.2.0)(react@18.2.0) @@ -89,6 +95,9 @@ dependencies: ts-permissions: specifier: ^1.0.0 version: 1.0.0 + ua-parser-js: + specifier: ^1.0.37 + version: 1.0.37 zod: specifier: ^3.22.4 version: 3.22.4 @@ -115,6 +124,9 @@ devDependencies: '@types/react-dom': specifier: ^18.2.14 version: 18.2.14 + '@types/ua-parser-js': + specifier: ^0.7.38 + version: 0.7.38 '@typescript-eslint/eslint-plugin': specifier: ^6.9.1 version: 6.9.1(@typescript-eslint/parser@6.9.1)(eslint@8.53.0)(typescript@5.2.2) @@ -1042,6 +1054,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.35)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.35)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.35 + '@types/react-dom': 18.2.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.35)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -1214,6 +1247,48 @@ packages: '@types/node': 20.8.10 dev: false + /@types/d3-array@3.2.0: + resolution: {integrity: sha512-tjU8juPSfhMnu6mJZPOCVVGba4rZoE0tjHDPb81PYwA8CzbaFscGjgkUM7juUJu6iWA1cCVWNEVwxZ5HN9Jj8Q==} + dev: false + + /@types/d3-color@3.1.2: + resolution: {integrity: sha512-At+Ski7dL8Bs58E8g8vPcFJc8tGcaC12Z4m07+p41+DRqnZQcAlp3NfYjLrhNYv+zEyQitU1CUxXNjqUyf+c0g==} + dev: false + + /@types/d3-ease@3.0.1: + resolution: {integrity: sha512-VZofjpEt8HWv3nxUAosj5o/+4JflnJ7Bbv07k17VO3T2WRuzGdZeookfaF60iVh5RdhVG49LE5w6LIshVUC6rg==} + dev: false + + /@types/d3-interpolate@3.0.3: + resolution: {integrity: sha512-6OZ2EIB4lLj+8cUY7I/Cgn9Q+hLdA4DjJHYOQDiHL0SzqS1K9DL5xIOVBSIHgF+tiuO9MU1D36qvdIvRDRPh+Q==} + dependencies: + '@types/d3-color': 3.1.2 + dev: false + + /@types/d3-path@3.0.1: + resolution: {integrity: sha512-blRhp7ki7pVznM8k6lk5iUU9paDbVRVq+/xpf0RRgSJn5gr6SE7RcFtxooYGMBOc1RZiGyqRpVdu5AD0z0ooMA==} + dev: false + + /@types/d3-scale@4.0.6: + resolution: {integrity: sha512-lo3oMLSiqsQUovv8j15X4BNEDOsnHuGjeVg7GRbAuB2PUa1prK5BNSOu6xixgNf3nqxPl4I1BqJWrPvFGlQoGQ==} + dependencies: + '@types/d3-time': 3.0.2 + dev: false + + /@types/d3-shape@3.1.4: + resolution: {integrity: sha512-M2/xsWPsjaZc5ifMKp1EBp0gqJG0eO/zlldJNOC85Y/5DGsBQ49gDkRJ2h5GY7ZVD6KUumvZWsylSbvTaJTqKg==} + dependencies: + '@types/d3-path': 3.0.1 + dev: false + + /@types/d3-time@3.0.2: + resolution: {integrity: sha512-kbdRXTmUgNfw5OTE3KZnFQn6XdIc4QGroN5UixgdrXATmYsdlPQS6pEut9tVlIojtzuFD4txs/L+Rq41AHtLpg==} + dev: false + + /@types/d3-timer@3.0.1: + resolution: {integrity: sha512-GGTvzKccVEhxmRfJEB6zhY9ieT4UhGVUIQaBzFpUO9OXy2ycAlnPCSJLzmGGgqt3KVjqN3QCQB4g1rsZnHsWhg==} + dev: false + /@types/debug@4.1.9: resolution: {integrity: sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==} dependencies: @@ -1286,6 +1361,10 @@ packages: resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==} dev: true + /@types/ua-parser-js@0.7.38: + resolution: {integrity: sha512-59CA5oavBEWSNLtS/BChj9xntiWMsIf9IytjxmBo9OuZEYuRzRf3K1ARzFPlXTOz5Zm2wXI38AP9RlLqDYMToQ==} + dev: true + /@types/webidl-conversions@7.0.2: resolution: {integrity: sha512-uNv6b/uGRLlCVmelat2rA8bcVd3k/42mV2EmjhPh6JLkd35T5bgwR/t6xy7a9MWhd9sixIeBUzhBenvk3NO+DQ==} dev: false @@ -1952,6 +2031,10 @@ packages: clsx: 2.0.0 dev: false + /classnames@2.3.2: + resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} + dev: false + /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -2114,6 +2197,77 @@ packages: /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + /d@1.0.1: resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} dependencies: @@ -2147,6 +2301,10 @@ packages: dependencies: ms: 2.1.2 + /decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dev: false + /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -2260,6 +2418,12 @@ packages: esutils: 2.0.3 dev: true + /dom-helpers@3.4.0: + resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==} + dependencies: + '@babel/runtime': 7.23.2 + dev: false + /dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} @@ -2891,6 +3055,10 @@ packages: engines: {node: '>=6'} dev: false + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -2925,6 +3093,11 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + /fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + dev: false + /fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} dev: false @@ -3429,6 +3602,11 @@ packages: hasown: 2.0.0 side-channel: 1.0.4 + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /ip@2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} dev: false @@ -4920,7 +5098,6 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - dev: true /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -4970,7 +5147,35 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true + + /react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false + + /react-resize-detector@8.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-smooth@2.0.5(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==} + peerDependencies: + prop-types: ^15.6.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 2.9.0(react-dom@18.2.0)(react@18.2.0) + dev: false /react-ssr-prepass@1.5.0(react@18.2.0): resolution: {integrity: sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==} @@ -4980,6 +5185,20 @@ packages: react: 18.2.0 dev: false + /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} + peerDependencies: + react: '>=15.0.0' + react-dom: '>=15.0.0' + dependencies: + dom-helpers: 3.4.0 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -5055,6 +5274,34 @@ packages: dependencies: picomatch: 2.3.1 + /recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + dependencies: + decimal.js-light: 2.5.1 + dev: false + + /recharts@2.9.2(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ig0zYgO5nUP/896GW16b9yy2sHIRW1AHB90x48hypFTSjjxQt/J9rPzlLJjgNupzJKEHPCwMi1VnvN/k20K45w==} + engines: {node: '>=12'} + peerDependencies: + prop-types: ^15.6.0 + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + classnames: 2.3.2 + eventemitter3: 4.0.7 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 16.13.1 + react-resize-detector: 8.1.0(react-dom@18.2.0)(react@18.2.0) + react-smooth: 2.0.5(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.1 + victory-vendor: 36.6.12 + dev: false + /reflect.getprototypeof@1.0.4: resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} engines: {node: '>= 0.4'} @@ -5705,6 +5952,10 @@ packages: next-tick: 1.1.0 dev: true + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: false + /tmp@0.2.1: resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} engines: {node: '>=8.17.0'} @@ -5847,6 +6098,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + /ua-parser-js@1.0.37: + resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} + dev: false + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -5915,6 +6170,25 @@ packages: spdx-expression-parse: 3.0.1 dev: false + /victory-vendor@36.6.12: + resolution: {integrity: sha512-pJrTkNHln+D83vDCCSUf0ZfxBvIaVrFHmrBOsnnLAbdqfudRACAj51He2zU94/IWq9464oTADcPVkmWAfNMwgA==} + dependencies: + '@types/d3-array': 3.2.0 + '@types/d3-ease': 3.0.1 + '@types/d3-interpolate': 3.0.3 + '@types/d3-scale': 4.0.6 + '@types/d3-shape': 3.1.4 + '@types/d3-time': 3.0.2 + '@types/d3-timer': 3.0.1 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + dev: false + /watchpack@2.4.0: resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} engines: {node: '>=10.13.0'} diff --git a/src/app/Login.tsx b/src/app/Login.tsx index ac9e3f1..41f6e1d 100644 --- a/src/app/Login.tsx +++ b/src/app/Login.tsx @@ -24,9 +24,9 @@ export default function LoginForm() { >(); const login = api.auth.login.useMutation({ - onSuccess: () => { + onSuccess: (data) => { toast.success("Successfully logged in!", { id: toastLoading }); - router.push("/dashboard"); + router.push("/home"); }, onError: (error) => { diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx deleted file mode 100644 index be11672..0000000 --- a/src/app/dashboard/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { api } from "~/trpc/server"; -import Test from "./RSC"; - -export default async function DashboardHome() { - return ( -
- rip - -
- ); -} diff --git a/src/app/home/ChartGraph.tsx b/src/app/home/ChartGraph.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dashboard/RSC.tsx b/src/app/home/RSC.tsx similarity index 100% rename from src/app/dashboard/RSC.tsx rename to src/app/home/RSC.tsx diff --git a/src/app/home/StatCard.module.css b/src/app/home/StatCard.module.css new file mode 100644 index 0000000..3595072 --- /dev/null +++ b/src/app/home/StatCard.module.css @@ -0,0 +1,9 @@ +/* tailwind does not have text-shadow support, so we do it ourselves */ +.stat-card > p { + /* -webkit-text-stroke: 1px theme("colors.card.DEFAULT"); */ + text-shadow: + 1px 1px 0 theme("colors.card.DEFAULT"), + -1px 1px 0 theme("colors.card.DEFAULT"), + 1px -1px 0 theme("colors.card.DEFAULT"), + -1px -1px 0 theme("colors.card.DEFAULT"); +} diff --git a/src/app/home/StatCard.tsx b/src/app/home/StatCard.tsx new file mode 100644 index 0000000..c0dbfe0 --- /dev/null +++ b/src/app/home/StatCard.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { RiPulseFill } from "react-icons/ri"; +import { LineChart, Line, ResponsiveContainer } from "recharts"; +import styles from "./StatCard.module.css"; + +const TEST_DATA = [ + { cpu: 0.05 }, + { cpu: 0.1 }, + { cpu: 0.08 }, + { cpu: 0.09 }, + { cpu: 0.2 }, + { cpu: 0.1 }, + { cpu: 0.12 }, + { cpu: 0.3 }, +]; + +export function StatCard() { + return ( + + + CPU Usage + + + + + {/* chart background */} +
+ + + + + +
+ +
+

4.5%

+

+ of 8 CPUs +

+
+
+
+ ); +} diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx new file mode 100644 index 0000000..02a1d51 --- /dev/null +++ b/src/app/home/page.tsx @@ -0,0 +1,17 @@ +import { api } from "~/trpc/server"; +import Test from "./RSC"; +import { StatCard } from "./StatCard"; + +export default async function DashboardHome() { + return ( +
+ +
+ + + + +
+
+ ); +} diff --git a/src/app/settings/SettingsHeader.tsx b/src/app/settings/SettingsHeader.tsx new file mode 100644 index 0000000..0c98874 --- /dev/null +++ b/src/app/settings/SettingsHeader.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { usePathname } from "next/navigation"; + +type SidebarNavItem = { + title: string; + description: string; + href: string; +}; + +export function SettingsHeader(props: { items: SidebarNavItem[] }) { + const path = usePathname(); + const active = props.items.find((item) => item.href === path); + + return ( +
+

{active?.title}

+

{active?.description}

+
+ ); +} diff --git a/src/app/settings/account/page.tsx b/src/app/settings/account/page.tsx new file mode 100644 index 0000000..5899447 --- /dev/null +++ b/src/app/settings/account/page.tsx @@ -0,0 +1,5 @@ +"use client"; + +export default function AccountSettings() { + return <>; +} diff --git a/src/app/settings/layout.tsx b/src/app/settings/layout.tsx new file mode 100644 index 0000000..a8dcbc6 --- /dev/null +++ b/src/app/settings/layout.tsx @@ -0,0 +1,65 @@ +import Image from "next/image"; + +import { Separator } from "~/components/ui/separator"; +import { SidebarNav } from "~/components/SidebarNav"; +import { SettingsHeader } from "./SettingsHeader"; + +const sidebarNavItems = [ + { + title: "Account", + description: "Manage your account security and settings.", + href: "/settings/account", + }, + { + title: "Sessions", + description: "Manage your active sessions and logout remotely.", + href: "/settings/sessions", + }, +]; + +interface SettingsLayoutProps { + children: React.ReactNode; +} + +export default function SettingsLayout({ children }: SettingsLayoutProps) { + return ( + <> +
+ Forms + Forms +
+
+
+

Settings

+

+ Manage your account settings as well as the global instance + settings. +

+
+ +
+ +
+ + + {children} +
+
+
+ + ); +} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx new file mode 100644 index 0000000..5838661 --- /dev/null +++ b/src/app/settings/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Settings() { + redirect("/settings/account"); +} diff --git a/src/app/settings/sessions/Sessions.tsx b/src/app/settings/sessions/Sessions.tsx new file mode 100644 index 0000000..8c43ec8 --- /dev/null +++ b/src/app/settings/sessions/Sessions.tsx @@ -0,0 +1,23 @@ +import { Card } from "~/components/ui/card"; +import UAParser from "ua-parser-js"; + +type SessionData = { + lastUA: string | null; + lastIP: string | null; + lastAccessed: Date | null; + createdAt: number; + id: string; +}; + +export default function Session(props: { session: SessionData }) { + const ua = new UAParser(props.session.lastUA ?? ""); + + return ( + +

{ua.getBrowser().name}

+

+ {ua.getOS().name} {ua.getOS().version} +

+
+ ); +} diff --git a/src/app/settings/sessions/page.tsx b/src/app/settings/sessions/page.tsx new file mode 100644 index 0000000..12369ec --- /dev/null +++ b/src/app/settings/sessions/page.tsx @@ -0,0 +1,15 @@ +import { Card, CardContent } from "~/components/ui/card"; +import { api } from "~/trpc/server"; +import Session from "./Sessions"; + +export default async function SessionsPage() { + const sessions = await api.auth.sessions.list.query(); + + return ( + <> + {sessions.map((session, idx) => ( + + ))} + + ); +} diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx new file mode 100644 index 0000000..a21f91f --- /dev/null +++ b/src/components/SidebarNav.tsx @@ -0,0 +1,43 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { buttonVariants } from "~/components/ui/button"; +import { cn } from "~/utils/utils"; + +interface SidebarNavProps extends React.HTMLAttributes { + items: { + href: string; + title: string; + }[]; +} + +export function SidebarNav({ className, items, ...props }: SidebarNavProps) { + const pathname = usePathname(); + + return ( + + ); +} diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..675953f --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "~/utils/utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref, + ) => ( + + ), +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth/index.ts similarity index 94% rename from src/server/api/routers/auth.ts rename to src/server/api/routers/auth/index.ts index c257a1a..fb7c497 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth/index.ts @@ -3,14 +3,17 @@ import { authenticatedProcedure, createTRPCRouter, publicProcedure, -} from "../trpc"; +} from "../../trpc"; import { users } from "~/server/db/schema"; import { eq } from "drizzle-orm"; import { TRPCError } from "@trpc/server"; import argon2 from "argon2"; import { Session } from "~/server/auth/Session"; +import { sessionsRouter } from "./sessions"; export const authRouter = createTRPCRouter({ + sessions: sessionsRouter, + me: authenticatedProcedure.query(async ({ ctx }) => { const user = await ctx.session.getUser(); diff --git a/src/server/api/routers/auth/sessions.tsx b/src/server/api/routers/auth/sessions.tsx new file mode 100644 index 0000000..59e7803 --- /dev/null +++ b/src/server/api/routers/auth/sessions.tsx @@ -0,0 +1,21 @@ +import { sessions } from "~/server/db/schema"; +import { authenticatedProcedure, createTRPCRouter } from "../../trpc"; +import { eq } from "drizzle-orm"; +import { Session } from "~/server/auth/Session"; + +export const sessionsRouter = createTRPCRouter({ + list: authenticatedProcedure.query(async ({ ctx }) => { + const activeSessions = ctx.db + .select({ + lastUA: sessions.lastUA, + lastIP: sessions.lastIP, + lastAccessed: sessions.lastAccessed, + createdAt: sessions.createdAt, + id: sessions.token, + }) + .from(sessions) + .where(eq(sessions.userId, ctx.session.data.userId)); + + return activeSessions; + }), +}); diff --git a/src/server/auth/Session.ts b/src/server/auth/Session.ts index 265c87d..2c4b8a9 100644 --- a/src/server/auth/Session.ts +++ b/src/server/auth/Session.ts @@ -4,6 +4,7 @@ import { users, sessions } from "../db/schema"; import { randomBytes } from "crypto"; import assert from "assert"; import { NextRequest, userAgent } from "next/server"; +import { hash } from "argon2"; export type SessionUpdateData = Partial<{ ua: string; @@ -90,13 +91,13 @@ export class Session { .values({ lastUA: parsedContext.ua, lastIP: parsedContext.ip, - token, + token: await hash(token), userId, }) .returning(); assert(sessionData, "Session should be created"); - return new Session(sessionData); + return new Session(sessionData, token); } /** @@ -112,8 +113,12 @@ export class Session { /** * Create a new session instance from a user's session data. * @param sessionData The user's session data. + * @param unhashedSessionToken The unhashed session token. */ - constructor(public readonly data: typeof sessions.$inferSelect) {} + constructor( + public readonly data: typeof sessions.$inferSelect, + private readonly unhashedSessionToken?: string, + ) {} /** * Get the user associated with this session. @@ -145,9 +150,11 @@ export class Session { * Returns a cookie string for this session. */ getCookieString() { + assert(this.unhashedSessionToken, "Sessions cannot be unhashed"); + const expire = new Date(this.data.createdAt + Session.EXPIRE_TIME); return `sessionToken=${ - this.data.token + this.unhashedSessionToken }; Expires=${expire.toUTCString()}; Path=/; HttpOnly; SameSite=Strict`; } }