From 0c0e29947a5ced88653609e0b31a2e8372ec6ccf Mon Sep 17 00:00:00 2001 From: Thomas Hintz Date: Mon, 27 Feb 2023 19:41:05 -0800 Subject: [PATCH] Init reactors. --- package-lock.json | 151 +++++++++++++++++- package.json | 5 +- src/app/(main)/reactors/account/layout.jsx | 36 +++++ src/app/(main)/reactors/account/page.jsx | 33 ++++ .../(main)/reactors/create-account/page.jsx | 142 ++++++++++++++++ src/app/(main)/reactors/page.jsx | 14 ++ src/app/(main)/reactors/sign-in/page.jsx | 142 ++++++++++++++++ src/data/episodes.js | 7 +- src/db.js | 119 ++++++++++++++ src/pages/api/create-account.js | 64 ++++++++ src/pages/api/feed.rss.js | 95 +++++++++++ src/pages/api/sign-in.js | 53 ++++++ 12 files changed, 857 insertions(+), 4 deletions(-) create mode 100644 src/app/(main)/reactors/account/layout.jsx create mode 100644 src/app/(main)/reactors/account/page.jsx create mode 100644 src/app/(main)/reactors/create-account/page.jsx create mode 100644 src/app/(main)/reactors/page.jsx create mode 100644 src/app/(main)/reactors/sign-in/page.jsx create mode 100644 src/db.js create mode 100644 src/pages/api/create-account.js create mode 100644 src/pages/api/feed.rss.js create mode 100644 src/pages/api/sign-in.js diff --git a/package-lock.json b/package-lock.json index c6124bb..33d2bef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,14 @@ "@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/typography": "^0.5.7", "clsx": "^1.2.1", + "cookies-next": "^2.1.1", "eslint": "8.32.0", "eslint-config-next": "13.1.4", "focus-visible": "^5.2.0", "i": "^0.3.7", "next": "^13.1.7-canary.12", "nodemailer": "^6.9.1", + "podcast": "^2.0.1", "postcss-focus-visible": "^6.0.4", "react": "18.2.0", "react-aria": "^3.19.0", @@ -31,7 +33,8 @@ "sanitize-html": "^2.8.1", "sqlite3": "^5.1.4", "srtparsejs": "^1.0.8", - "stripe": "^11.8.0" + "stripe": "^11.8.0", + "uuid": "^9.0.0" }, "devDependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", @@ -2101,6 +2104,11 @@ "@types/estree": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -3160,6 +3168,29 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookies-next": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-2.1.1.tgz", + "integrity": "sha512-AZGZPdL1hU3jCjN2UMJTGhLOYzNUN9Gm+v8BdptYIHUdwz397Et1p+sZRfvAl8pKnnmMdX2Pk9xDRKCGBum6GA==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/node": "^16.10.2", + "cookie": "^0.4.0" + } + }, + "node_modules/cookies-next/node_modules/@types/node": { + "version": "16.18.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.12.tgz", + "integrity": "sha512-vzLe5NaNMjIE3mcddFVGlAXN1LEWueUsMsOJWaT6wWMJGyljHAWHznqfnKUQWGzu7TLPrGvWdNAsvQYW+C0xtw==" + }, "node_modules/cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -7041,6 +7072,14 @@ "node": ">=0.10.0" } }, + "node_modules/podcast": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/podcast/-/podcast-2.0.1.tgz", + "integrity": "sha512-TWXe/zVziwJNksAn7RLkSre+Z6VQgbs/+gC7qQCKdkyw0hv2hdFGOY9rHgKqa4LI+UP+yZBa6Wr+b9a9vrYDYQ==", + "dependencies": { + "rss": "^1.2.2" + } + }, "node_modules/postcss": { "version": "8.4.21", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", @@ -7553,6 +7592,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rss": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz", + "integrity": "sha512-xUhRTgslHeCBeHAqaWSbOYTydN2f0tAzNXvzh3stjz7QDhQMzdgHf3pfgNIngeytQflrFPfy6axHilTETr6gDg==", + "dependencies": { + "mime-types": "2.1.13", + "xml": "1.0.1" + } + }, + "node_modules/rss/node_modules/mime-db": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz", + "integrity": "sha512-5k547tI4Cy+Lddr/hdjNbBEWBwSl8EBc5aSdKvedav8DReADgWJzcYiktaRIw3GtGC1jjwldXtTzvqJZmtvC7w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rss/node_modules/mime-types": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz", + "integrity": "sha512-ryBDp1Z/6X90UvjUK3RksH0IBPM137T7cmg4OgD5wQBojlAiUwuok0QeELkim/72EtcYuNlmbkrcGuxj3Kl0YQ==", + "dependencies": { + "mime-db": "~1.25.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8517,6 +8584,14 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/uvu": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", @@ -8773,6 +8848,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -10389,6 +10469,11 @@ "@types/estree": "*" } }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -11190,6 +11275,28 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "cookies-next": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-2.1.1.tgz", + "integrity": "sha512-AZGZPdL1hU3jCjN2UMJTGhLOYzNUN9Gm+v8BdptYIHUdwz397Et1p+sZRfvAl8pKnnmMdX2Pk9xDRKCGBum6GA==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/node": "^16.10.2", + "cookie": "^0.4.0" + }, + "dependencies": { + "@types/node": { + "version": "16.18.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.12.tgz", + "integrity": "sha512-vzLe5NaNMjIE3mcddFVGlAXN1LEWueUsMsOJWaT6wWMJGyljHAWHznqfnKUQWGzu7TLPrGvWdNAsvQYW+C0xtw==" + } + } + }, "cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -13891,6 +13998,14 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" }, + "podcast": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/podcast/-/podcast-2.0.1.tgz", + "integrity": "sha512-TWXe/zVziwJNksAn7RLkSre+Z6VQgbs/+gC7qQCKdkyw0hv2hdFGOY9rHgKqa4LI+UP+yZBa6Wr+b9a9vrYDYQ==", + "requires": { + "rss": "^1.2.2" + } + }, "postcss": { "version": "8.4.21", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", @@ -14232,6 +14347,30 @@ "glob": "^7.1.3" } }, + "rss": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz", + "integrity": "sha512-xUhRTgslHeCBeHAqaWSbOYTydN2f0tAzNXvzh3stjz7QDhQMzdgHf3pfgNIngeytQflrFPfy6axHilTETr6gDg==", + "requires": { + "mime-types": "2.1.13", + "xml": "1.0.1" + }, + "dependencies": { + "mime-db": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz", + "integrity": "sha512-5k547tI4Cy+Lddr/hdjNbBEWBwSl8EBc5aSdKvedav8DReADgWJzcYiktaRIw3GtGC1jjwldXtTzvqJZmtvC7w==" + }, + "mime-types": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz", + "integrity": "sha512-ryBDp1Z/6X90UvjUK3RksH0IBPM137T7cmg4OgD5wQBojlAiUwuok0QeELkim/72EtcYuNlmbkrcGuxj3Kl0YQ==", + "requires": { + "mime-db": "~1.25.0" + } + } + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -14907,6 +15046,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, "uvu": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", @@ -15099,6 +15243,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 1267c63..b02e529 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,14 @@ "@tailwindcss/line-clamp": "^0.4.2", "@tailwindcss/typography": "^0.5.7", "clsx": "^1.2.1", + "cookies-next": "^2.1.1", "eslint": "8.32.0", "eslint-config-next": "13.1.4", "focus-visible": "^5.2.0", "i": "^0.3.7", "next": "^13.1.7-canary.12", "nodemailer": "^6.9.1", + "podcast": "^2.0.1", "postcss-focus-visible": "^6.0.4", "react": "18.2.0", "react-aria": "^3.19.0", @@ -33,7 +35,8 @@ "sanitize-html": "^2.8.1", "sqlite3": "^5.1.4", "srtparsejs": "^1.0.8", - "stripe": "^11.8.0" + "stripe": "^11.8.0", + "uuid": "^9.0.0" }, "devDependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", diff --git a/src/app/(main)/reactors/account/layout.jsx b/src/app/(main)/reactors/account/layout.jsx new file mode 100644 index 0000000..71b1ff8 --- /dev/null +++ b/src/app/(main)/reactors/account/layout.jsx @@ -0,0 +1,36 @@ +import Link from 'next/link'; +import { redirect } from 'next/navigation'; + +import { cookies } from 'next/headers'; + +import db from '@/db'; + +async function getSession() { + const cookieStore = cookies(); + const sessionId = cookieStore.get('session'); + if (!sessionId) { + return false; + } + const { user_id: userId } = await db.get('select user_id from sessions where session_id=?;', sessionId.value); + if (!userId) { + return false; + } + return userId; +}; + +export default async function Layout({ children }) { + const userId = await getSession(); + if (!userId) { + redirect('/reactors'); + } + return ( + <> + signed in as: {userId} + + + ); +}; diff --git a/src/app/(main)/reactors/account/page.jsx b/src/app/(main)/reactors/account/page.jsx new file mode 100644 index 0000000..1c221cb --- /dev/null +++ b/src/app/(main)/reactors/account/page.jsx @@ -0,0 +1,33 @@ +import { cookies } from 'next/headers'; + +import db from '@/db'; + +import { Container } from '@/components/Container'; + +async function getSession() { + const cookieStore = cookies(); + const sessionId = cookieStore.get('session'); + if (!sessionId) { + return false; + } + const { user_id: userId } = await db.get('select user_id from sessions where session_id=?;', sessionId.value); + if (!userId) { + return false; + } + return userId; +}; + +export default async function Page() { + const userId = await getSession(); + return ( +
+ +

+ The Reactors +

+ Level I + {userId &&

user: {userId}

} +
+
+ ); +}; diff --git a/src/app/(main)/reactors/create-account/page.jsx b/src/app/(main)/reactors/create-account/page.jsx new file mode 100644 index 0000000..2e9ec5a --- /dev/null +++ b/src/app/(main)/reactors/create-account/page.jsx @@ -0,0 +1,142 @@ +export const dynamic = 'force-dynamic'; + +import Link from 'next/link' +import Stripe from 'stripe'; +const stripe = new Stripe('sk_test_51MVz87Ke2JFOuDSNa2PVPrs3BBq9vJQwwDITC3sOB521weM4oklKtQFbJ03MNsJwsxtjHO5NScqOHC9MABREVjU900yYz3lWgL'); +import { dbRun } from '@/db'; + +import { XCircleIcon } from '@heroicons/react/20/solid' + +import { Container } from '@/components/Container'; + + +// /reactors/create-account?csi=cs_test_a1pBB0FI8GUKnWYlCVn0RKUYXV8FRroacXjI5WVhWPlFJilm46lZwdjgac +export default async function Page({ searchParams }) { + const unexpectedError = searchParams['unexpected_error']; + const msg = searchParams['msg']; + const csi = searchParams['csi']; + const session = csi && await stripe.checkout.sessions.retrieve(csi); + const email = (csi && session && session.customer_details.email) || searchParams['email']; + const message = searchParams['message']; + const submitted = email || message; + const valid = submitted && email && message; + let emailSentSuccessfully = false; + if (unexpectedError) { + return ( + <> + Unexpected Error sorry about that! Please contact us via Contact and we will get it figured out! + + ); + } + return ( +
+ +

+ The Reactors - Create Account +

+
+

+ Thank you so much for signing up to become a Reactor! We just need a password now to create an account for you! +

+ {msg && ( +
+
+
+
+
+

There was an error with your submission

+
+ {msg} +
+
+
+
+ )} +
+ +
+
+
+
+ +
+ + +
+
+
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+ ); +}; diff --git a/src/app/(main)/reactors/page.jsx b/src/app/(main)/reactors/page.jsx new file mode 100644 index 0000000..6a8da9a --- /dev/null +++ b/src/app/(main)/reactors/page.jsx @@ -0,0 +1,14 @@ +import { Container } from '@/components/Container'; + +export default async function Page() { + return ( +
+ +

+ The Reactors +

+ Level I +
+
+ ); +}; diff --git a/src/app/(main)/reactors/sign-in/page.jsx b/src/app/(main)/reactors/sign-in/page.jsx new file mode 100644 index 0000000..960de8b --- /dev/null +++ b/src/app/(main)/reactors/sign-in/page.jsx @@ -0,0 +1,142 @@ +export const dynamic = 'force-dynamic'; + +import Link from 'next/link' +import Stripe from 'stripe'; +const stripe = new Stripe('sk_test_51MVz87Ke2JFOuDSNa2PVPrs3BBq9vJQwwDITC3sOB521weM4oklKtQFbJ03MNsJwsxtjHO5NScqOHC9MABREVjU900yYz3lWgL'); +import { dbRun } from '@/db'; + +import { XCircleIcon } from '@heroicons/react/20/solid' + +import { Container } from '@/components/Container'; + + +// /reactors/create-account?csi=cs_test_a1pBB0FI8GUKnWYlCVn0RKUYXV8FRroacXjI5WVhWPlFJilm46lZwdjgac +export default async function Page({ searchParams }) { + const unexpectedError = searchParams['unexpected_error']; + const msg = searchParams['msg']; + const csi = searchParams['csi']; + const session = csi && await stripe.checkout.sessions.retrieve(csi); + const email = (csi && session && session.customer_details.email) || searchParams['email']; + const message = searchParams['message']; + const submitted = email || message; + const valid = submitted && email && message; + let emailSentSuccessfully = false; + if (unexpectedError) { + return ( + <> + Unexpected Error sorry about that! Please contact us via Contact and we will get it figured out! + + ); + } + return ( +
+ +

+ The Reactors - Sign In +

+
+ {msg && ( +
+
+
+
+
+

There was an error with your submission

+
+ {msg} +
+
+
+
+ )} +
+
+ Your Company +

Sign in to your account

+

+ Or{' '} + + sign up to become a Reactor! + +

+
+ +
+
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ + +
+ + +
+ +
+ +
+
+
+
+
+
+
+
+ ); +}; diff --git a/src/data/episodes.js b/src/data/episodes.js index 076dbbb..4a091ab 100644 --- a/src/data/episodes.js +++ b/src/data/episodes.js @@ -228,14 +228,15 @@ export async function getEpisodes() { length: enclosure['@_length'] }, content: feedEntry['content:encoded'], - chapters: feedEntry['podcast:chapters'] && feedEntry['podcast:chapters']['@_url'] + chapters: feedEntry['podcast:chapters'] && feedEntry['podcast:chapters']['@_url'], + duration: feedEntry['itunes:duration'] } } }) const numEpisodes = feed.entries.length; const feedEntries = feed.entries.map( - ({ id, title, description, enclosure , published, content, chapters }, i) => ({ + ({ id, title, description, enclosure , published, content, chapters, duration }, i) => ({ num: numEpisodes - i, id, title, @@ -249,6 +250,8 @@ export async function getEpisodes() { audio: [enclosure].map((enclosure) => ({ src: enclosure.url, type: enclosure.type, + length: enclosure.length, + duration }))[0], }) ); diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..1a17013 --- /dev/null +++ b/src/db.js @@ -0,0 +1,119 @@ +const sqlite3 = require('sqlite3'); +const util = require('util'); + +const migrations = [ + { + key: 1, + name: 'create table users', + sql: [`create table users ( +id integer primary key autoincrement, +email text not null, +salt text not null, +password_hash text not null + )`] + }, + { + key: 3, + name: 'create table sessions', + sql: [`create table sessions ( +id integer primary key autoincrement, +user_id integer not null, +session_id text not null, +expires integer not null, +foreign key (user_id) references users (id) + )`] + }, + { + key: 4, + name: 'create table episodes', + sql: [`create table episodes ( +id integer primary key autoincrement, +number integer, +content text, +summary text, +slug text, +season integer, +episode integer, +duration integer, +filename text, +title text, +episode_type text, +buzzsprout_id text, +buzzsprout_url text, +pub_date text, +youtube_url text, +transcript_filename text + )`] + } +]; + +const checkForMigrationsSql = `select key from migrations where run='True' order by key`; + +async function runMigrations(db) { + console.log('turn on foreign keys'); + await db.exec('PRAGMA foreign_keys = ON;'); + console.log('running migrations'); + const rows = await db.all(checkForMigrationsSql) + const runMigrations = rows.map(({ key }) => key); + console.log(runMigrations); + let toRun = []; + migrations.forEach(({ key, name, sql }) => { + if (!runMigrations.includes(key)) { + toRun.push({ key, name, sql }); + } + }); + console.log('Migrations to run:', toRun.map(({ name }) => name)); + await db.exec(toRun.reduce((prev, { sql, key }) => `${prev} ${sql.join(';')} ; insert into migrations (key, run) values (${key}, 'True') ;`, '')); + console.log('migrations run'); + /* db.all(checkForMigrationsSql, (err, rows) => { + * console.log('xx') + * const runMigrations = rows.map(({ key }) => key); + * console.log(runMigrations); + * let toRun = []; + * migrations.forEach(({ key, name, sql }) => { + * if (!runMigrations.includes(key)) { + * toRun.push({ key, name, sql }); + * } + * }); + * console.log('Migrations to run:', toRun.map(({ name }) => name)); + * db.exec(toRun.reduce((prev, { sql, key }) => `${prev} ${sql.join(';')} ; insert into migrations (key, run) values (${key}, 'True') ;`, ''), () => { + * console.log('migrations run'); + * }); + * }); */ +}; + +const createMigrationTable = `create table migrations ( + id integer primary key autoincrement, + key integer not null, + run boolean not null +)`; + +let db = new sqlite3.Database('./db.sqlite3', sqlite3.OPEN_READWRITE, async (err) => { + if (err && err.code == "SQLITE_CANTOPEN") { + db = new sqlite3.Database('./db.sqlite3', async (err) => { + if (err) { + console.log("Getting error " + err); + } + console.log('database created'); + console.log('creating migration table') + db.exec(createMigrationTable, async () => { + await runMigrations(db); + }); + }); + if (err) { + console.error(err.message); + } + } else if (err) { + console.error(err.message); + } else { + console.log('Connected to the database.'); + await runMigrations(db); + } +}); + +db.run = util.promisify(db.run); +db.get = util.promisify(db.get); +db.all = util.promisify(db.all); +db.exec = util.promisify(db.exec); + +export default db; diff --git a/src/pages/api/create-account.js b/src/pages/api/create-account.js new file mode 100644 index 0000000..034a021 --- /dev/null +++ b/src/pages/api/create-account.js @@ -0,0 +1,64 @@ +import Stripe from 'stripe'; +const stripe = new Stripe('sk_test_51MVz87Ke2JFOuDSNa2PVPrs3BBq9vJQwwDITC3sOB521weM4oklKtQFbJ03MNsJwsxtjHO5NScqOHC9MABREVjU900yYz3lWgL'); + +import db from '@/db'; + +import { scrypt, randomBytes, timingSafeEqual } from 'crypto'; +import { promisify } from 'util'; + +const scryptPromise = promisify(scrypt); + +function genSalt(bytes = 16) { + return randomBytes(bytes).toString('hex'); +}; + +async function hash(salt, password, rounds = 64) { + const derivedKey = await scryptPromise(password, salt, rounds); + return derivedKey.toString('hex') +} + +async function verify(password, hash, salt, rounds = 64) { + const keyBuffer = Buffer.from(hash, 'hex'); + const derivedKey = await scryptPromise(password, salt, rounds); + return timingSafeEqual(keyBuffer, derivedKey); +} + + +function makeMsg(csi, email, text) { + return `/reactors/create-account?csi=${csi}&msg=${encodeURIComponent(text)}&email=${encodeURIComponent(email)}` +}; + +export default async function handler(req, res) { + if (req.method === 'POST') { + const { email, password, passwordagain, csi } = req.body; + if (email && password && password === passwordagain && csi) { + const session = csi && await stripe.checkout.sessions.retrieve(csi); + const emailFromSession = session && session.customer_details.email; + if (!session || !emailFromSession || email !== emailFromSession) { + res.redirect('/reactors/create-account?unexpected_error=true'); + } + const existingUser = await db.get('select id from users where email=?', email); + if (existingUser) { + res.redirect('/reactors/create-account?unexpected_error=true'); + } + console.log('inserting user'); + const salt = genSalt(); + const hashRes = await hash(salt, password); + await db.run('insert into users (email, salt, password_hash) values (?, ?, ?);', email, salt, hashRes); + console.log('done inserting user'); + res.redirect('/reactors') + } else { + if (!email || !csi) { + res.redirect('/reactors/create-account?unexpected_error=true'); + } + if (!password) { + res.redirect(makeMsg(csi, email, 'Please enter a password')); + } + if (password !== passwordagain) { + res.redirect(makeMsg(csi, email, 'Passwords did not match. Please try again.')); + } + } + } else { + // Handle any other HTTP method + } +} diff --git a/src/pages/api/feed.rss.js b/src/pages/api/feed.rss.js new file mode 100644 index 0000000..11b0fcc --- /dev/null +++ b/src/pages/api/feed.rss.js @@ -0,0 +1,95 @@ +import path from 'path'; +import fs from 'fs'; + +import db from '@/db'; + +import { Podcast } from 'podcast'; +/* import mp3Duration from 'mp3-duration'; */ + +import { getEpisodes } from '@/data/episodes'; + +export default async function handler(req, res) { + if (req.method === 'GET') { + const feed = new Podcast({ + title: 'The React Show (Reactors)', + description: "Discussions about React, JavaScript, and web development by React experts with a focus on diving deep into learning React and discussing what it's like to work within the React industry.", + feedUrl: 'https://www.thereactshow.com/api/feed.rss', + siteUrl: 'https://www.thereactshow.com', + imageUrl: 'https://storage.buzzsprout.com/variants/d1tds1rufs5340fyq9mpyzo491qp/5cfec01b44f3e29fae1fb88ade93fc4aecd05b192fbfbc2c2f1daa412b7c1921.jpg', + author: 'Owl Creek Studio', + copyright: '© 2023 Owl Creek Studio', + language: 'en', + categories: ['Technology','Education','Business'], + pubDate: 'May 20, 2012 04:00:00 GMT', + ttl: 60, + itunesAuthor: 'Owl Creek Studio', + itunesOwner: { name: 'Owl Creek Studio' }, + itunesExplicit: false, + itunesCategory: [{ + text: 'Technology' + }, + { + text: 'Education' + }, + { + text: 'Business' + }], + itunesImage: 'https://storage.buzzsprout.com/variants/d1tds1rufs5340fyq9mpyzo491qp/5cfec01b44f3e29fae1fb88ade93fc4aecd05b192fbfbc2c2f1daa412b7c1921.jpg' + }); + + const episodes = await getEpisodes(); + episodes.forEach(({ title, published, description, content, slug, audio: { src, length }, num, id, youtube }) => { + const filename = `0${num}-mixed.mp3`; // TODO auto-add the 0 prefix + const filepath = path.join(process.cwd(), 'public', 'files', 'episodes', filename); + if (fs.existsSync(filepath)) { + feed.addItem({ + title, + description: content, + content, + url: `https://www.thereactshow.com/podcast/${slug}`, + date: published, + itunesExplicit: false, + itunesSummary: description, + /* itunesDuration: await mp3Duration(filepath), TODO */ + itunesDuration: 1234, + enclosure : { + url: `https://www.thereactshow.com/files/episodes/${filename}`, + file: filepath + }, + }); + } + }) + + const dbUpdates = episodes.map(async ({ title, published, description, content, slug, audio: { src, length }, num, id, youtube }) => { + const filename = `0${num}-mixed.mp3`; // TODO auto-add the 0 prefix + const filepath = path.join(process.cwd(), 'public', 'files', 'episodes', filename); + if (fs.existsSync(filepath)) { + const existsInDb = await db.get('select id from episodes where number=?', num); + if (!existsInDb) { + console.log('adding to db'); + await db.run('insert into episodes (number, content, summary, slug, season, episode, filename, title, episode_type, buzzsprout_id, buzzsprout_url, pub_date, youtube_url) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', + num, + content, + description, + slug, + 1, + num, + filename, + title, + 'episodic', + id, + src, + published, + youtube); + console.log('added to db', num); + } + } + }) + await Promise.all(dbUpdates); + + const xml = feed.buildXml(); + + res.setHeader('Content-Type', 'text/xml; charset=utf-8'); + res.send(xml); + } +}; diff --git a/src/pages/api/sign-in.js b/src/pages/api/sign-in.js new file mode 100644 index 0000000..1b8f71d --- /dev/null +++ b/src/pages/api/sign-in.js @@ -0,0 +1,53 @@ +import Stripe from 'stripe'; +const stripe = new Stripe('sk_test_51MVz87Ke2JFOuDSNa2PVPrs3BBq9vJQwwDITC3sOB521weM4oklKtQFbJ03MNsJwsxtjHO5NScqOHC9MABREVjU900yYz3lWgL'); + +import { setCookie } from 'cookies-next'; +import { v4 as uuidv4 } from 'uuid'; + +import db from '@/db'; + +import { scrypt, randomBytes, timingSafeEqual } from 'crypto'; +import { promisify } from 'util'; + +const scryptPromise = promisify(scrypt); + +async function verify(password, hash, salt, rounds = 64) { + const keyBuffer = Buffer.from(hash, 'hex'); + const derivedKey = await scryptPromise(password, salt, rounds); + return timingSafeEqual(keyBuffer, derivedKey); +} + +function makeMsg(email, text) { + return `/reactors/sign-in?msg=${encodeURIComponent(text)}&email=${encodeURIComponent(email)}` +}; + +export default async function handler(req, res) { + if (req.method === 'POST') { + const { email, password, remember_me: rememberMe } = req.body; + if (email && password) { + const queryRes = await db.get('select id, salt, password_hash from users where email=?;', email); + const { password_hash, salt, id: userId } = queryRes || { password_hash: '', salt: '', id: '' }; + const verifyRes = await verify(password, password_hash, salt); + if (verifyRes) { + const sessionId = uuidv4(); + const maxAge = 60 * 60 * 24 * 365; + const today = new Date(); + const expiresDate = new Date(today.getTime() + (1000 * maxAge)); + await db.run('insert into sessions (user_id, session_id, expires) values (?, ?, ?);', userId, sessionId, expiresDate.toISOString()); + setCookie('session', sessionId, { req, res, maxAge: rememberMe ? maxAge : undefined, httpOnly: true, sameSite: true, secure: process.env.NODE_ENV === 'production' }); + res.redirect('/reactors/account') + } else { + res.redirect(makeMsg(email, 'Invalid password or account does not exist.')); + } + } else { + if (!email) { + res.redirect(makeMsg(email, 'Please enter an email address.')); + } + if (!password) { + res.redirect(makeMsg(email, 'Please enter a password.')); + } + } + } else { + // Handle any other HTTP method + } +}