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 (
+
+ );
+};
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}
+
+
+
+
+ )}
+
+
+

+
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
+ }
+}