Init reactors.
parent
60eac45175
commit
0c0e29947a
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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…
Reference in New Issue