Init reactors.

reactors
Thomas Hintz 2 years ago
parent 60eac45175
commit 0c0e29947a

151
package-lock.json generated

@ -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",

@ -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",

@ -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}
<nav>
<ul>
<Link href="/reactors/sign-out">sign out</Link>
</ul>
</nav>
</>
);
};

@ -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 (
<div className="pt-16 pb-12 sm:pb-4 lg:pt-12">
<Container>
<h1 className="text-2xl font-bold leading-7 text-slate-900">
The Reactors
</h1>
<a href="https://buy.stripe.com/test_3cs01j768d65gso289">Level I</a>
{userId && <p>user: {userId}</p>}
</Container>
</div>
);
};

@ -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 <Link href="/contact">Contact</Link> and we will get it figured out!
</>
);
}
return (
<div className="pt-16 pb-12 sm:pb-4 lg:pt-12">
<Container>
<h1 className="text-2xl font-bold leading-7 text-slate-900">
The Reactors - Create Account
</h1>
<div className="divide-y divide-slate-100 sm:mt-4 lg:mt-8 lg:border-t lg:border-slate-100">
<p>
Thank you so much for signing up to become a Reactor! We just need a password now to create an account for you!
</p>
{msg && (
<div className="rounded-md bg-red-50 p-4 mt-8">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">There was an error with your submission</h3>
<div className="mt-2 text-sm text-red-700">
{msg}
</div>
</div>
</div>
</div>
)}
<form className="space-y-8" method="POST" action="/api/create-account">
<input
name="csi"
value={csi}
type="hidden"
/>
<div className="space-y-8">
<div className="pt-8">
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-3">
<label htmlFor="emailprefilled" className="block text-sm font-medium text-gray-700">
Email address
</label>
<div className="mt-1">
<input
id="emailprefilled"
name="emailprefilled"
type="email"
defaultValue={email}
disabled
title="Email Address (required)"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
<input
type="hidden"
name="email"
value={email}
/>
</div>
</div>
</div>
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-3">
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<div className="mt-1">
<input
id="password"
name="password"
type="password"
required
minlength="12"
title="Password (required)"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="passwordagain" className="block text-sm font-medium text-gray-700">
Password (again)
</label>
<div className="mt-1">
<input
id="passwordagain"
name="passwordagain"
type="password"
required
minlength="12"
title="Password (required)"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
</div>
</div>
</div>
</div>
</div>
<div className="pt-5">
<div className="flex justify-end">
<button
type="submit"
className="ml-3 inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Create Account
</button>
</div>
</div>
</form>
</div>
</Container>
</div>
);
};

@ -0,0 +1,14 @@
import { Container } from '@/components/Container';
export default async function Page() {
return (
<div className="pt-16 pb-12 sm:pb-4 lg:pt-12">
<Container>
<h1 className="text-2xl font-bold leading-7 text-slate-900">
The Reactors
</h1>
<a href="https://buy.stripe.com/test_3cs01j768d65gso289">Level I</a>
</Container>
</div>
);
};

@ -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 <Link href="/contact">Contact</Link> and we will get it figured out!
</>
);
}
return (
<div className="pt-16 pb-12 sm:pb-4 lg:pt-12">
<Container>
<h1 className="text-2xl font-bold leading-7 text-slate-900">
The Reactors - Sign In
</h1>
<div className="divide-y divide-slate-100 sm:mt-4 lg:mt-8 lg:border-t lg:border-slate-100">
{msg && (
<div className="rounded-md bg-red-50 p-4 mt-8">
<div className="flex">
<div className="flex-shrink-0">
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">There was an error with your submission</h3>
<div className="mt-2 text-sm text-red-700">
{msg}
</div>
</div>
</div>
</div>
)}
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<img
className="mx-auto h-12 w-auto"
src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600"
alt="Your Company"
/>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">Sign in to your account</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Or{' '}
<Link href='/reactors' className="font-medium text-indigo-600 hover:text-indigo-500">
sign up to become a Reactor!
</Link>
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10 bg-gray-50">
<form className="space-y-6" method="POST" action="/api/sign-in">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email address
</label>
<div className="mt-1">
<input
id="email"
name="email"
type="email"
autoComplete="email"
defaultValue={email}
required
className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
/>
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<div className="mt-1">
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember_me"
name="remember_me"
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
defaultValue="checked"
/>
<label htmlFor="remember_me" className="ml-2 block text-sm text-gray-900">
Remember me
</label>
</div>
<div className="text-sm">
<a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">
Forgot your password?
</a>
</div>
</div>
<div>
<button
type="submit"
className="flex w-full justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Sign in
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</Container>
</div>
);
};

@ -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],
})
);

@ -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;

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

@ -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);
}
};

@ -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
}
}
Loading…
Cancel
Save