From 1b65d89fa8745852950b4344070c4e16e6d2b14d Mon Sep 17 00:00:00 2001 From: "Jason R. Stevens, CFA" Date: Sun, 30 Jan 2022 13:44:10 -0600 Subject: [PATCH 1/9] :heavy_plus_sign: add prisma deps [sc-540] --- package.json | 2 ++ yarn.lock | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/package.json b/package.json index d20c99b..d7e5686 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "test": "jest --coverage" }, "dependencies": { + "@prisma/client": "^3.8.1", "next": "^12.0.9", "react": "^17.0.2", "react-dom": "^17.0.2" @@ -19,6 +20,7 @@ "eslint-config-next": "12.0.9", "jest": "^27.4.7", "postcss": "^8.4.5", + "prisma": "^3.8.1", "tailwindcss": "^3.0.18" } } diff --git a/yarn.lock b/yarn.lock index 91aad27..c6e1f0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -603,6 +603,23 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@prisma/client@^3.8.1": + version "3.8.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.8.1.tgz#c11eda8e84760867552ffde4de7b48fb2cf1e1c0" + integrity sha512-NxD1Xbkx1eT1mxSwo1RwZe665mqBETs0VxohuwNfFIxMqcp0g6d4TgugPxwZ4Jb4e5wCu8mQ9quMedhNWIWcZQ== + dependencies: + "@prisma/engines-version" "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" + +"@prisma/engines-version@3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f": + version "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f.tgz#4c8d9744b5e54650a8ba5fde0a711399d6adba24" + integrity sha512-G2JH6yWt6ixGKmsRmVgaQYahfwMopim0u/XLIZUo2o/mZ5jdu7+BL+2V5lZr7XiG1axhyrpvlyqE/c0OgYSl3g== + +"@prisma/engines@3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f": + version "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f.tgz#4479099b99f6a082ce5843ee7208943ccedd127f" + integrity sha512-bHYubuItSN/DGYo36aDu7xJiJmK52JOSHs4MK+KbceAtwS20BCWadRgtpQ3iZ2EXfN/B1T0iCXlNraaNwnpU2w== + "@rushstack/eslint-patch@^1.0.8": version "1.1.0" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323" @@ -3368,6 +3385,13 @@ pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.4.6: ansi-styles "^5.0.0" react-is "^17.0.1" +prisma@^3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.8.1.tgz#44395cef7cbb1ea86216cb84ee02f856c08a7873" + integrity sha512-Q8zHwS9m70TaD7qI8u+8hTAmiTpK+IpvRYF3Rgb/OeWGQJOMgZCFFvNCiSfoLEQ95wilK7ctW3KOpc9AuYnRUA== + dependencies: + "@prisma/engines" "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" From 2b9f3f00dcc462d7047762a71beea6c0989d0c70 Mon Sep 17 00:00:00 2001 From: "Jason R. Stevens, CFA" Date: Sun, 30 Jan 2022 13:44:59 -0600 Subject: [PATCH 2/9] :sparkles: add initial prisma items [sc-540] --- prisma/schema.prisma | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 prisma/schema.prisma diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..d205f42 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,11 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} From 2ab9019589101f106371579011dbd284cb6c6436 Mon Sep 17 00:00:00 2001 From: "Jason R. Stevens, CFA" Date: Mon, 31 Jan 2022 08:03:15 -0600 Subject: [PATCH 3/9] :notebook: add prisma + auth overview documentation [sc-540] --- README.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8301140..e0ada52 100644 --- a/README.md +++ b/README.md @@ -1 +1,92 @@ -# funded \ No newline at end of file +# funded + +A simple **S**ingle **P**age **A**pp for getting funded. Built on +[Next.js](https://nextjs.org), styled with [Tailwindcss](https://tailwindcss.com), and deployed on [Vercel](https://vercel.com). + +Development and support provided by [Tincre](https://tincre.com). + +## Development Setup + +Clone this repo. Then, + +```bash +yarn install +yarn run dev +``` + +:rocket: The dev site will be running locally at `localhost:3000`. :rocket: +### Tests +Tests leverage `jest` and Kent Dodd's `react testing library`. These can +be found under the `__tests__` directory and run with `yarn run test`. + +### Database Infrastructure + +We generally use PostgreSQL databases via ORMs at Tincre. + +In particular, this site uses [Prisma](https://prisma.io) for its database +ORM. + +#### Updating tables + +To update tables we use Prisma's tools. + +> _Prisma has fantastic documentation https://www.prisma.io/docs/!_ + +Simply add a table via the schema classes in the `prisma/schema.prisma` file. + +Once finished run `npx prisma format && npx migrate dev` to format/lint the +prisma schema and migrate it. Your client will update locally. + +Commit the changes from `prisma/schema.prisma` and the migration file. :boom: + +> _:warning: Production sites will need to be built twice on CI/CD infra, depending on your setup :warning:._ + +#### CRUD Operations + +Database functionality lives in the `/lib` directory. In particular, a global +import `prisma` is in `prisma.js`. + +This contains a `prisma` object you should use via a default export. + +##### `db.js` or `db.ts` +CRUD operations are in the `lib/db*{.js|.ts}`. In particular, examples +of creating, reading, updating, and deleting table objects are extant within +this file. + +> :notebook: Inline documentation is critical and provided using @jsdoc! + +### Authentication Infrastructure +Auth is provided out of the box by [next-auth](https://next-auth.js.org). In particular, +we use the [Prisma Adapter](https://next-auth.js.org/adapters/prisma) +throughout this project. + +#### Client-side Page Authentication + +We add authentication to individual pages via the client, which checks for the auth session, validating it via the /api/auth/session backend api function via the `useSession` hook. This hook is populated by the `SessionProvider` Higher Order Component within the custom `_app.jsx` Next App overload. + +After successful authentication, the redirected user will have a state session stored +in their browser. + +#### Server-side Page Authentication + +For server-side rendering and other protected api endpoints, such as /api/, we check for the session to confirm that the header provides the appropriate authentication. + +This is handled seamlessly within via the next-auth library. + + +#### Providers + +Right now we offer the following authentication providers: + +- direct-to-email link, +- Google Accounts, +- Github, +- Gitlab, +- Microsoft, +- and Twitter. + +We can and will add more at a later date. + +## Contributions + +We :heart: community contributions. From 4d1b2ade4e99d199b700de4d459cd790dfb85840 Mon Sep 17 00:00:00 2001 From: "Jason R. Stevens, CFA" Date: Mon, 31 Jan 2022 09:00:09 -0600 Subject: [PATCH 4/9] :heavy_plus_sign: add nanoid, localforage, jsonwebtoken, jwt-decode, swr, and validator [sc-540] --- package.json | 8 +++- yarn.lock | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d7e5686..88d5528 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,14 @@ "eslint": "8.8.0", "eslint-config-next": "12.0.9", "jest": "^27.4.7", + "jsonwebtoken": "^8.5.1", + "jwt-decode": "^3.1.2", + "localforage": "^1.10.0", + "nanoid": "^3.2.0", "postcss": "^8.4.5", "prisma": "^3.8.1", - "tailwindcss": "^3.0.18" + "swr": "^1.2.0", + "tailwindcss": "^3.0.18", + "validator": "^13.7.0" } } diff --git a/yarn.lock b/yarn.lock index c6e1f0d..d6bd953 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1149,6 +1149,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -1504,6 +1509,13 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + electron-to-chromium@^1.4.17: version "1.4.57" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.57.tgz#2b2766df76ac8dbc0a1d41249bc5684a31849892" @@ -2141,6 +2153,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -2861,6 +2878,22 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b" @@ -2869,6 +2902,28 @@ json5@^2.1.2: array-includes "^3.1.3" object.assign "^4.1.2" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +jwt-decode@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" + integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -2907,6 +2962,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4= + dependencies: + immediate "~3.0.5" + lilconfig@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" @@ -2917,6 +2979,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +localforage@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -2932,11 +3001,46 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash@^4.17.15, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -3042,7 +3146,7 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoid@^3.1.30: +nanoid@^3.1.30, nanoid@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== @@ -3552,6 +3656,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -3577,6 +3686,11 @@ scheduler@^0.20.2: loose-envify "^1.1.0" object-assign "^4.1.1" +semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -3793,6 +3907,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swr@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/swr/-/swr-1.2.0.tgz#8649f6e9131ce94bbcf7ffd65c21334da3d1ec20" + integrity sha512-C3IXeKOREn0jQ1ewXRENE7ED7jjGbFTakwB64eLACkCqkF/A0N2ckvpCTftcaSYi5yV36PzoehgVCOVRmtECcA== + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -3990,6 +4109,11 @@ v8-to-istanbul@^8.1.0: convert-source-map "^1.6.0" source-map "^0.7.3" +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" From 8aefe57b361d5a193a40f7b6739ead9ce6619457 Mon Sep 17 00:00:00 2001 From: "Jason R. Stevens, CFA" Date: Mon, 31 Jan 2022 09:01:33 -0600 Subject: [PATCH 5/9] :sparkles: :wrench: add internal library files --- lib/db.js | 68 +++++++++++++++++++++++++++++++++++++++++++++++ lib/node-utils.js | 53 ++++++++++++++++++++++++++++++++++++ lib/prisma.js | 17 ++++++++++++ lib/utils.js | 38 ++++++++++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 lib/db.js create mode 100644 lib/node-utils.js create mode 100644 lib/prisma.js create mode 100644 lib/utils.js diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 0000000..58674a1 --- /dev/null +++ b/lib/db.js @@ -0,0 +1,68 @@ +import { Prisma } from "@prisma/client"; +import prisma from "./prisma"; + +/** + * Create a new user session in the database + * + * @param sessionId the sessionId of the to-be created userSession; must be + unique + * @param email the email of the to-be created user; may be undefined + * + * @returns boolean true if the user session was created, false otherwise + */ +export async function createUserSession(sessionId, email) { + try { + await prisma.userSession.create({ + data: { email: email || null, sessionId: sessionId }, + }); + return true; + } catch (error) { + console.log(`Error in createUserSession ${error?.message}`); + // https://www.prisma.io/docs/concepts/components/prisma-client/handling-exceptions-and-errors/ + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === "P2002") { + console.log( + `There is a unique constraint violation for ${error.message} and the user cannot be created.` + ); + } + return false; + } else { + throw error; + } + } +} + +/** + * Query a user session via a `sessionId` + * @param sessionId A sessionId of a user session in the database. + * @returns User session or null if the sessionId is not found. + */ +export async function queryUserBySessionId(sessionId) { + console.log(`Running queryUserBySessionId for ${sessionId}`); + const res = await prisma.userSession.findUnique({ + where: { sessionId: sessionId }, + }); + // TODO Remove logs + console.log(JSON.stringify(res)); + return res; +} + +/** + * Update a user session via its sessionId with the given data to update. + * + * @param sessionId the string sessionId of the user session + * @param data an object with keys present in the `UserSession` schema + * @returns boolean true if updated successfully, false otherwise. + */ +export async function updateUserSession(sessionId, data) { + try { + await prisma.userSession.update({ + where: { sessionId: sessionId }, + data: data, + }); + return true; + } catch (error) { + console.error(`Error in updateUserSession ${error.message}`); + return false; + } +} diff --git a/lib/node-utils.js b/lib/node-utils.js new file mode 100644 index 0000000..f4d0e81 --- /dev/null +++ b/lib/node-utils.js @@ -0,0 +1,53 @@ +import jwt from "jsonwebtoken"; +/** + * Encode (and sign) a given object as JWT. + * + * @param data object to encode in base64 and sign via HMAC and the given + * passphrase + * @param passphrase string secret passphrase for producing the HMAC hash of the + * data + * @returns string JWT token + */ +function encodeJwt(data, passphrase) { + const token = jwt.sign(data, passphrase); + return token; +} +/** + * Log an api func error given the route string and error object. + * + * @param route string route e.g. /api/docs + * @param error Error error object, hopefully including a message to be printed + * + * @returns string error message appended with route information + */ +function logApiError(route, error) { + const msg = `Error in route ${route}, not raising! ${error?.message}`; + console.error(msg); + return msg; +} +/* + * Return a string content security policy for this site given a nonce. + * + * @param isProd boolean set to true if production + * + * @returns string Content Security Policy + */ +function getCsp(isProd) { + let imgSrc = "res.cloudinary.com *.tincre.com cdn.sanity.io"; + let csp = ``; + csp += `base-uri 'self';`; + csp += `form-action 'self' https://discord.gg https://discord.com/channels/865659205443518534`; + csp += `default-src 'self';`; + csp += `script-src 'self' ${isProd ? "" : "'unsafe-eval'"} *.b00st.com;`; // NextJS requires 'unsafe-eval' + // in dev (faster source maps) + csp += `style-src 'self' https://fonts.googleapis.com 'unsafe-inline' data:;`; // NextJS requires 'unsafe-inline' + csp += `img-src 'self' ${imgSrc} data: 'unsafe-inline' blob:;`; + csp += `font-src 'self' https://fonts.gstatic.com;`; // TODO + csp += `frame-src *;`; // TODO + csp += `media-src *;`; // TODO + csp += `object-src 'self';`; + csp += `connect-src 'self' https://vitals.vercel-insights.com`; + return csp; +} + +export { getCsp, logApiError, encodeJwt }; diff --git a/lib/prisma.js b/lib/prisma.js new file mode 100644 index 0000000..6c3f68c --- /dev/null +++ b/lib/prisma.js @@ -0,0 +1,17 @@ +// https://github.com/prisma/deployment-example-vercel/blob/903ad88ffef160b5ffce18ba1344df41cc39cfa0/lib/prisma.js +import { PrismaClient } from "@prisma/client"; + +// Avoid instantiating too many instances of Prisma in development +// https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices#problem +let prisma; + +if (process.env.NODE_ENV === "production") { + prisma = new PrismaClient(); +} else { + if (!global.prisma) { + global.prisma = new PrismaClient(); + } + prisma = global.prisma; +} + +export default prisma; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..256c5fc --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,38 @@ +/* eslint-disable no-unused-vars */ + +// import { useRef, useState, useEffect, MutableRefObject } from 'react'; +import jwtDecode from "jwt-decode"; + +export function classNames(...classes) { + return classes.filter(Boolean).join(" "); +} +/** + * A fetcher for useSwr + * + * @param uri string web endpoint + * @param options object `fetch` options. See fetch documentation. + * @returns JSON response + */ +const fetcher = async (uri, options) => { + let fetchOptions = options; + if (typeof options !== "object") fetchOptions = {}; + const response = await fetch(uri, fetchOptions); // TODO can we just send an undefined + // param and avoid the options nonsense? + return response.json(); +}; + +/** + * Decode a JWT token client side + * + * @param token string jwt (base64) encoded token + * @returns decoded string token + */ +function clientJwtDecode(token) { + if (!token) + throw new Error( + "Token must be defined or not null. Do better next time, thanks." + ); + return jwtDecode(token); // Returns with the JwtPayload type +} +// Fall back to default handling +export { clientJwtDecode, fetcher }; From 719502e444811e5bc051efd2340fa4ea13f734bd Mon Sep 17 00:00:00 2001 From: "Jason R. Stevens, CFA" Date: Mon, 31 Jan 2022 09:01:38 -0600 Subject: [PATCH 6/9] :sparkles: add session functionality for anonymous sessions [sc-540] --- pages/api/session.js | 89 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 pages/api/session.js diff --git a/pages/api/session.js b/pages/api/session.js new file mode 100644 index 0000000..071b264 --- /dev/null +++ b/pages/api/session.js @@ -0,0 +1,89 @@ +import { nanoid } from "nanoid/async"; +import isEmail from "validator/lib/isEmail"; + +import { + createUserSession, + queryUserBySessionId, + updateUserSession, +} from "../../lib/db"; +import { encodeJwt, logApiError } from "../../lib/node-utils"; +import prisma from "../../lib/prisma"; + +async function createId() { + const id = await nanoid(); + return id; +} + +export default async function handler(req, res) { + try { + const jwtKey = process.env.SIGNING_PASSPHRASE; + if (req.method === "GET") { + console.log("Running GET session request"); + const data = req?.body; + const { sessionId } = data; + if (typeof sessionId !== "undefined") { + const sessionData = await queryUserBySessionId(sessionId); + console.log(`Session data ${JSON.stringify(sessionData)}`); + if (typeof sessionData !== "undefined") { + console.log(`Successfully retrieved ${sessionData.sessionId}`); + const encoded = encodeJwt( + { + sessionId: sessionData.sessionId, + }, + jwtKey + ); + res.status(200).json({ token: encoded }); + } + res.status(200).json(); + } else { + res.status(200).json(); + } + } + if (req.method === "POST") { + const data = req?.body; + console.log(`POST: Data are ${JSON.stringify(data)}`); + const email = isEmail(data?.email || "") ? data.email : null; + const inputData = { + sessionId: await createId(), + email: email, + }; + const isCreated = await createUserSession( + inputData.sessionId, + inputData.email + ); + if (!isCreated) + throw new Error( + `DB User Session was not created for ${inputData.sessionId}` + ); + const encoded = encodeJwt({ sessionId: inputData.sessionId }, jwtKey); + res.status(200).json({ token: encoded }); + } + if (req.method === "PUT") { + const data = req?.body; + console.log(`PUT: Data are ${JSON.stringify(data)}`); + const email = isEmail(data?.email || "") ? data.email : null; + console.log(`Data are sessionId: ${data?.sessionId} email: ${email}`); + if (typeof data?.sessionId === "undefined" || !email) { + console.log(`Not found ${data?.sessionId} ${data?.email}`); + res.status(404).json(); + } + const inputData = { + sessionId: data?.sessionId, + email: email, + }; + const isUpdated = await updateUserSession(data?.sessionId, inputData); + if (!isUpdated) + throw new Error( + `DB User Session was not updated for ${data?.sessionId}` + ); + console.log(`Encoded for update\n${JSON.stringify(inputData)}`); + const encoded = encodeJwt({ sessionId: data?.sessionId }, jwtKey); + res.status(200).json({ token: encoded }); + } + } catch (error) { + logApiError("/api/session", error); + res.status(400).json(error); + } finally { + prisma.$disconnect; + } +} From e818c25df2ed96489b189cf9d99676c7a8e8eb0b Mon Sep 17 00:00:00 2001 From: "Jason R. Stevens, CFA" Date: Mon, 31 Jan 2022 09:01:43 -0600 Subject: [PATCH 7/9] :sparkles: :wrench: add initial database schema + migrations [sc-540] --- .../migration.sql | 16 ++++++++++++++++ prisma/migrations/migration_lock.toml | 3 +++ prisma/schema.prisma | 10 ++++++++++ 3 files changed, 29 insertions(+) create mode 100644 prisma/migrations/20220131145922_initial_migration/migration.sql create mode 100644 prisma/migrations/migration_lock.toml diff --git a/prisma/migrations/20220131145922_initial_migration/migration.sql b/prisma/migrations/20220131145922_initial_migration/migration.sql new file mode 100644 index 0000000..5f7fb30 --- /dev/null +++ b/prisma/migrations/20220131145922_initial_migration/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "UserSession" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "sessionId" TEXT NOT NULL, + "email" TEXT, + "data" JSONB, + + CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserSession_sessionId_key" ON "UserSession"("sessionId"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserSession_email_sessionId_key" ON "UserSession"("email", "sessionId"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d205f42..098c314 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,4 +8,14 @@ generator client { datasource db { provider = "postgresql" url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") +} + +model UserSession { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) @db.Timestamptz(6) + sessionId String @unique + email String? + data Json? + @@unique([email, sessionId]) } From 2c3722c902b6b7db986fd503ff7fb2d0df2e1e63 Mon Sep 17 00:00:00 2001 From: "Jason R. Stevens, CFA" Date: Mon, 31 Jan 2022 09:02:56 -0600 Subject: [PATCH 8/9] :sparkles: :wrench: add app setup for anonymous sessions w/prisma [sc-540] --- pages/index.jsx | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/pages/index.jsx b/pages/index.jsx index dc0e068..5417963 100644 --- a/pages/index.jsx +++ b/pages/index.jsx @@ -1,3 +1,7 @@ +import { useState, useRef, useEffect } from "react"; +import { fetcher, clientJwtDecode } from "../lib/utils"; +import localforage from "localforage"; +import useSwr, { mutate } from "swr"; import Footer from "../components/Footer"; import NavigationHero from "../components/Sections/NavigationHero"; import WhyInvest from "../components/Sections/WhyInvest"; @@ -9,6 +13,106 @@ import Team from "../components/Sections/Team"; import FAQ from "../components/Sections/FAQ"; export default function Funded() { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [state, setState] = useState(null); + const [decoded, setDecoded] = useState(null); + + const inputEl = useRef(null); + const [inputError, setInputError] = useState(false); + const [message, setMessage] = useState(""); + const [subscribed, setSubscribed] = useState(false); + const hostname = "investor.tincre.com"; + const subscribe = async (e) => { + e.preventDefault(); + + const res = await fetch(`/api/convertkit`, { + body: JSON.stringify({ + email: inputEl.current.value, + }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + + const { error } = await res.json(); + if (error) { + setInputError(true); + setMessage( + "Your e-mail address is invalid or you are already subscribed!" + ); + return; + } + + inputEl.current.value = ""; + setInputError(false); + setSubscribed(true); + setMessage("Successfully! 🎉 You are now subscribed."); + }; + + const { data, error } = useSwr( + "/api/session", + { data: { sessionId: state?.sessionId } }, + fetcher + ); + useEffect(() => { + if (typeof state?.sessionId !== "undefined") { + mutate("/api/session", { sessionId: state?.sessionId }, true); + } + }, [state]); + + // Create a new session + useEffect(() => { + // lookup local storage first + localforage.getItem(hostname).then((stateObj) => { + if (!stateObj) { + // create initial state in db using session identifier + fetch("/api/session", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }) + .then((res) => res.json()) + .then((data) => { + const token = data?.token; + if (typeof token !== "undefined") { + const decoded = clientJwtDecode(token); + setDecoded(decoded); + setState({ hostname: decoded }); + } + }) + .catch((error) => console.error(error.message)); + } else { + setState(stateObj); + } + }); + }, []); + // Update outer state data + useEffect(() => { + if (decoded) { + setState(decoded); + } + }, [decoded]); + // Set local state on state object changes + useEffect(() => { + if (state) { + localforage + .setItem(hostname, state) + .then(() => {}) + .catch((error) => console.error(error.message)); + } + }, [state]); + // Update the local state for decoded tokens on change of data from swr + useEffect(() => { + if (typeof data !== "undefined") { + const token = data?.token; + if (typeof token !== "undefined") { + setDecoded(clientJwtDecode(token)); + } + } + }, [data, error]); + const entityTitle = "Tincre"; const title = `Funded, by ${entityTitle}`; const description = From 1372de2e333867ba6e354def770e0cd5f511e5de Mon Sep 17 00:00:00 2001 From: "Jason R. Stevens, CFA" Date: Mon, 31 Jan 2022 09:16:29 -0600 Subject: [PATCH 9/9] :fire: :recycle: update b00st.com to tincre.com --- lib/node-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/node-utils.js b/lib/node-utils.js index f4d0e81..78cf7d4 100644 --- a/lib/node-utils.js +++ b/lib/node-utils.js @@ -38,7 +38,7 @@ function getCsp(isProd) { csp += `base-uri 'self';`; csp += `form-action 'self' https://discord.gg https://discord.com/channels/865659205443518534`; csp += `default-src 'self';`; - csp += `script-src 'self' ${isProd ? "" : "'unsafe-eval'"} *.b00st.com;`; // NextJS requires 'unsafe-eval' + csp += `script-src 'self' ${isProd ? "" : "'unsafe-eval'"} *.tincre.com;`; // NextJS requires 'unsafe-eval' // in dev (faster source maps) csp += `style-src 'self' https://fonts.googleapis.com 'unsafe-inline' data:;`; // NextJS requires 'unsafe-inline' csp += `img-src 'self' ${imgSrc} data: 'unsafe-inline' blob:;`;