From 5101652e729c5ca81eebbde22eddf42f933535de Mon Sep 17 00:00:00 2001 From: vincanger <70215737+vincanger@users.noreply.github.com> Date: Fri, 7 Apr 2023 12:11:10 +0200 Subject: [PATCH] update for rc --- .DS_Store | Bin 0 -> 6148 bytes README.md | 20 ++---- env.server.example | 4 +- main.wasp | 36 +++++----- .../migration.sql | 13 ++++ src/server/actions.ts | 19 ++--- src/server/auth/google.ts | 3 +- src/server/{serverSetup.ts => webhooks.ts} | 66 +++++++----------- src/server/workers/checkAndQueueEmails.ts | 13 ++-- src/server/workers/sendGrid.ts | 47 ------------- 10 files changed, 85 insertions(+), 136 deletions(-) create mode 100644 .DS_Store create mode 100644 migrations/20230407100445_drop_userpass/migration.sql rename src/server/{serverSetup.ts => webhooks.ts} (59%) delete mode 100644 src/server/workers/sendGrid.ts diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..feca8613504bcd7430524202412f605a970dd548 GIT binary patch literal 6148 zcmeH~JqiLr422VS3&Cbf%V|7-HyA`u-~~i21wpZ&qx41vI)z6#m+=T zbn`f`MFtU>!%bynVPcAXCwIBY>3lz3j@RpDteEA>YT$!Ro{xoGkN^pg011!)3H%HJ zJGWu;MJOW)kN^pc1nmD%;HEXTh5D}p!AAgSkF*=sK1)E0C7?C6g(3sfXoW_r`WRw) zZ-`o{NKSp&Hs}YZb^Uy{)~Y3 z+x>QfkIJ+4?ei>u$gHg!9O~r=FP8vp>?mHs-SE8F0$NjBC^9f!1RMhc34E2n6Mwc5 Aa{vGU literal 0 HcmV?d00001 diff --git a/README.md b/README.md index b4948c5..232e3e5 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,17 @@ ## Running it locally -After cloning this repo, you can run it locally by following these steps: +Before you being, install [Wasp](https://wasp-lang.dev) by running `curl -sSL https://get.wasp-lang.dev/installer.sh | sh` in your terminal. +ou have two options to run this template locally: -1. Install [Wasp](https://wasp-lang.dev) by running `curl -sSL https://get.wasp-lang.dev/installer.sh | sh` in your terminal. +1. Run `wasp new -t saas`. This will clone and set up the repo in the background. 2. Create a `.env.server` file in the root of the project 3. Copy the `env.example` file contents to `.env.server` and fill in your API keys 4. Make sure you have a Database connected and running. Here are two quick options: - - Provision a Postgres database on [Railway](https://railway.app), go to settings and copy the `connection url`. Past it as `DATABASE_URL=` into your `env.server` file. - - or you can spin up a Postgres docker container with this command: - ```shell - docker run \ - --rm \ - --publish 5432:5432 \ - -v my-app-data:/var/lib/postgresql/data \ - --env POSTGRES_PASSWORD=devpass1234 \ - postgres - ``` - and then paste `DATABASE_URL=postgresql://postgres:devpass1234@localhost:5432/postgres` into your `env.server` file + - run `wasp start db` if you have Docker installed (if not, on MacOS run `brew install docker-machine docker`). This will start a Postgres database for you. No need to do anything else! 🤯 + - or provision a Postgres database on [Railway](https://railway.app), go to settings and copy the `connection url`. Past it as `DATABASE_URL=` into your `env.server` file. 5. Run `wasp db migrate-dev` -6. Run `wasp start` +6. Run `wasp start`. This will install all dependencies and start the client and server for you :) 7. Go to `localhost:3000` in your browser (your NodeJS server will be running on port `3001`) 8. Install the Wasp extension for VSCode to get the best DX 9. Check the files for comments containing specific instructions diff --git a/env.server.example b/env.server.example index 9e35e3d..7cf39dc 100644 --- a/env.server.example +++ b/env.server.example @@ -8,4 +8,6 @@ GOOGLE_CLIENT_SECRET= OPENAI_API_KEY= -SENDGRID_API_KEY= \ No newline at end of file +SENDGRID_API_KEY= +# if not excplicitly set to true, emails be logged to console but not actually sent +SEND_EMAILS_IN_DEVELOPMENT=true \ No newline at end of file diff --git a/main.wasp b/main.wasp index f14c313..8ced9a0 100644 --- a/main.wasp +++ b/main.wasp @@ -1,6 +1,6 @@ app SaaSTemplate { wasp: { - version: "^0.9.0" + version: "^0.10.0" }, title: "My SaaS App", head: [ @@ -8,7 +8,7 @@ app SaaSTemplate { "", "", "", - // put your google analytics script here, too! + // you can put your google analytics script here, too! ], // 🔐 Auth out of the box! https://wasp-lang.dev/docs/language/features#authentication--authorization auth: { @@ -25,19 +25,23 @@ app SaaSTemplate { db: { system: PostgreSQL }, - server: { - setupFn: import serverSetup from "@server/serverSetup.js" - }, client: { rootComponent: import App from "@client/App", }, + emailSender: { + provider: SendGrid, + defaultFrom: { + name: "MySaaSApp", + // make sure this address is the same you registered your SendGrid account with! + email: "email@mysaasapp.com" + }, + }, dependencies: [ ("@headlessui/react", "1.7.13"), ("@tailwindcss/forms", "^0.5.3"), ("@tailwindcss/typography", "^0.5.7"), ("react-hook-form", "7.43.1"), ("react-icons", "4.8.0"), - ("@sendgrid/mail", "7.7.0"), ("request-ip", "3.3.0"), ("@types/request-ip", "0.0.37"), ("node-fetch", "3.3.0"), @@ -52,9 +56,7 @@ app SaaSTemplate { entity User {=psl id Int @id @default(autoincrement()) - username String @unique email String @unique - password String stripeId String? checkoutSessionId String? hasPaid Boolean @default(false) @@ -155,6 +157,16 @@ query getRelatedObjects { entities: [User, RelatedObject] } +/* + * 📡 These are custom Wasp API Endpoints. Use them for callbacks, webhooks, etc. + */ + +api stripeWebhook { + fn: import { stripeWebhook } from "@server/webhooks.js", + entities: [User], + httpRoute: (POST, "/stripe-webhook") +} + /* 🕵️‍♂️ These are the Wasp Cron Jobs. Use them to set up recurring tasks and/or queues: * https://wasp-lang.dev/docs/language/features#jobs */ @@ -169,11 +181,3 @@ job emailChecker { }, entities: [User] } - -job emailSender { - executor: PgBoss, - perform: { - fn: import { sendGrid } from "@server/workers/sendGrid.js" - }, - entities: [User] -} \ No newline at end of file diff --git a/migrations/20230407100445_drop_userpass/migration.sql b/migrations/20230407100445_drop_userpass/migration.sql new file mode 100644 index 0000000..5ad32a2 --- /dev/null +++ b/migrations/20230407100445_drop_userpass/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - You are about to drop the column `password` on the `User` table. All the data in the column will be lost. + - You are about to drop the column `username` on the `User` table. All the data in the column will be lost. + +*/ +-- DropIndex +DROP INDEX "User_username_key"; + +-- AlterTable +ALTER TABLE "User" DROP COLUMN "password", +DROP COLUMN "username"; diff --git a/src/server/actions.ts b/src/server/actions.ts index 04d51f8..50d26d1 100644 --- a/src/server/actions.ts +++ b/src/server/actions.ts @@ -1,10 +1,7 @@ -import HttpError from '@wasp/core/HttpError.js'; import fetch from 'node-fetch'; -import type { RelatedObject, User } from '@wasp/entities'; -import type { - GenerateGptResponse, - StripePayment, -} from '@wasp/actions/types'; +import HttpError from '@wasp/core/HttpError.js'; +import type { RelatedObject } from '@wasp/entities'; +import type { GenerateGptResponse, StripePayment } from '@wasp/actions/types'; import type { StripePaymentResult, OpenAIResponse } from './types'; import Stripe from 'stripe'; @@ -101,7 +98,6 @@ export const generateGptResponse: GenerateGptResponse temperature: Number(temperature), }; - try { if (!context.user.hasPaid && !context.user.credits) { throw new HttpError(402, 'User has not paid or is out of credits'); @@ -117,7 +113,7 @@ export const generateGptResponse: GenerateGptResponse }); } - console.log('fetching', payload) + console.log('fetching', payload); const response = await fetch('https://api.openai.com/v1/chat/completions', { headers: { 'Content-Type': 'application/json', @@ -127,15 +123,14 @@ export const generateGptResponse: GenerateGptResponse body: JSON.stringify(payload), }); - const json = (await response.json()) as OpenAIResponse - console.log('response json', json) + const json = (await response.json()) as OpenAIResponse; + console.log('response json', json); return context.entities.RelatedObject.create({ data: { content: json?.choices[0].message.content, user: { connect: { id: context.user.id } }, }, }); - } catch (error) { if (!context.user.hasPaid) { await context.entities.User.update({ @@ -153,4 +148,4 @@ export const generateGptResponse: GenerateGptResponse return new Promise((resolve, reject) => { reject(new HttpError(500, 'Something went wrong')); }); -}; \ No newline at end of file +}; diff --git a/src/server/auth/google.ts b/src/server/auth/google.ts index 96407a1..c81bc45 100644 --- a/src/server/auth/google.ts +++ b/src/server/auth/google.ts @@ -2,9 +2,8 @@ export async function getUserFields(_context: unknown, args: any) { console.log('args', args.profile) - const username = args.profile.emails[0].value const email = args.profile.emails[0].value - return { username, email }; + return { email }; } export function config() { diff --git a/src/server/serverSetup.ts b/src/server/webhooks.ts similarity index 59% rename from src/server/serverSetup.ts rename to src/server/webhooks.ts index c02558c..c89d862 100644 --- a/src/server/serverSetup.ts +++ b/src/server/webhooks.ts @@ -1,7 +1,7 @@ -import type { ServerSetupFnContext } from '@wasp/types'; +import { StripeWebhook } from '@wasp/apis/types'; +import { emailSender } from '@wasp/email/index.js'; + import Stripe from 'stripe'; -import { PrismaClient } from '@prisma/client'; -import { emailSender } from '@wasp/jobs/emailSender.js'; import requestIp from 'request-ip'; export const STRIPE_WEBHOOK_IPS = [ @@ -19,43 +19,28 @@ export const STRIPE_WEBHOOK_IPS = [ '54.187.216.72', ]; -export const prisma = new PrismaClient(); - const stripe = new Stripe(process.env.STRIPE_KEY!, { apiVersion: '2022-11-15', }); -/** 🪝 Server Setup - * This is a custom API endpoint that is used to handle Stripe webhooks. - * Wasp will setup all the other endpoints for you automatically - * based on your queries and actions in the main.wasp file 🎉 - */ +export const stripeWebhook: StripeWebhook = async (request, response, context) => { + response.set('Access-Control-Allow-Origin', '*'); // Example of modifying headers to override Wasp default CORS middleware. -export default async function ({ app, server }: ServerSetupFnContext) { - // this just tests that the sendgrid worker is working correctly - // it can be removed here after sendgrid is properly configured + if (process.env.NODE_ENV === 'production') { + const detectedIp = requestIp.getClientIp(request) as string; + const isStripeIP = STRIPE_WEBHOOK_IPS.includes(detectedIp); - // await emailSender.submit({ - // to: 'your@email.com', - // subject: 'Test', - // text: 'Test', - // html: 'Test', - // }); - - app.post('/stripe-webhook', async (request, response) => { - if (process.env.NODE_ENV === 'production') { - const detectedIp = requestIp.getClientIp(request) as string; - const isStripeIP = STRIPE_WEBHOOK_IPS.includes(detectedIp); - - if (!isStripeIP) { - console.log('IP address not from Stripe: ', detectedIp); - return response.status(403).json({ received: false }); - } + if (!isStripeIP) { + console.log('IP address not from Stripe: ', detectedIp); + return response.status(403).json({ received: false }); } + } - let event: Stripe.Event = request.body; - let userStripeId: string | null = null; - // console.log('event', event) + let event: Stripe.Event; + let userStripeId: string | null = null; + + try { + event = request.body; if (event.type === 'invoice.paid') { const charge = event.data.object as Stripe.Invoice; @@ -63,7 +48,7 @@ export default async function ({ app, server }: ServerSetupFnContext) { if (charge.amount_paid === 999) { console.log('Subscription purchased: ', charge.amount_paid); - await prisma.user.updateMany({ + await context.entities.User.updateMany({ where: { stripeId: userStripeId, }, @@ -75,7 +60,7 @@ export default async function ({ app, server }: ServerSetupFnContext) { if (charge.amount_paid === 295) { console.log('Credits purchased: ', charge.amount_paid); - await prisma.user.updateMany({ + await context.entities.User.updateMany({ where: { stripeId: userStripeId, }, @@ -91,7 +76,7 @@ export default async function ({ app, server }: ServerSetupFnContext) { userStripeId = subscription.customer as string; if (subscription.cancel_at_period_end) { - const customerEmail = await prisma.user.findFirst({ + const customerEmail = await context.entities.User.findFirst({ where: { stripeId: userStripeId, }, @@ -101,7 +86,7 @@ export default async function ({ app, server }: ServerSetupFnContext) { }); if (customerEmail) { - await emailSender.submit({ + await emailSender.send({ to: customerEmail.email, subject: 'We hate to see you go :(', text: 'We hate to see you go. Here is a sweet offer...', @@ -114,7 +99,7 @@ export default async function ({ app, server }: ServerSetupFnContext) { userStripeId = subscription.customer as string; console.log('Subscription canceled'); - await prisma.user.updateMany({ + await context.entities.User.updateMany({ where: { stripeId: userStripeId, }, @@ -126,7 +111,8 @@ export default async function ({ app, server }: ServerSetupFnContext) { console.log(`Unhandled event type ${event.type}`); } - // Return a 200 response to acknowledge receipt of the event response.json({ received: true }); - }); -} + } catch (err: any) { + response.status(400).send(`Webhook Error: ${err?.message}`); + } +}; diff --git a/src/server/workers/checkAndQueueEmails.ts b/src/server/workers/checkAndQueueEmails.ts index 4984ac1..c5018d5 100644 --- a/src/server/workers/checkAndQueueEmails.ts +++ b/src/server/workers/checkAndQueueEmails.ts @@ -1,5 +1,7 @@ -import { emailSender } from '@wasp/jobs/emailSender.js'; -import type { Email } from './sendGrid'; +import { emailSender } from '@wasp/email/index.js' + +import type { Email } from '@wasp/email/core/types'; +import type { User } from '@wasp/entities' import type { Context } from '../types'; const emailToSend: Email = { @@ -19,7 +21,10 @@ const emailToSend: Email = { `, }; +// you could use this function to send newsletters, expiration notices, etc. export async function checkAndQueueEmails(_args: unknown, context: Context) { + + // e.g. you could send an offer email 2 weeks before their subscription expires const currentDate = new Date(); const twoWeeksFromNow = new Date(currentDate.getTime() + 14 * 24 * 60 * 60 * 1000); @@ -32,7 +37,7 @@ export async function checkAndQueueEmails(_args: unknown, context: Context) { }, sendEmail: true, }, - }); + }) as User[]; console.log('Sending expiration notices to users: ', users.length); @@ -45,7 +50,7 @@ export async function checkAndQueueEmails(_args: unknown, context: Context) { if (user.email) { try { emailToSend.to = user.email; - await emailSender.submit(emailToSend); + await emailSender.send(emailToSend); } catch (error) { console.error('Error sending expiration notice to user: ', user.id, error); } diff --git a/src/server/workers/sendGrid.ts b/src/server/workers/sendGrid.ts deleted file mode 100644 index c14bc8f..0000000 --- a/src/server/workers/sendGrid.ts +++ /dev/null @@ -1,47 +0,0 @@ -import SendGrid from '@sendgrid/mail'; - -export type Email = { - from?: EmailFromField; - to: string; - subject: string; - text: string; - html: string; -}; - -export type EmailFromField = { - name?: string; - email: string; -}; - -export type SendGridProvider = { - type: 'sendgrid'; - apiKey: string; -}; - -SendGrid.setApiKey(process.env.SENDGRID_API_KEY!); - -const MyAppName: string = 'MyAppName'; -const MyEmail: string = 'email@saasapp.com'; - -export async function sendGrid(email: Email, context: any) { - const fromField = { - name: MyAppName, - email: MyEmail, - }; - - try { - const sentEmail = await SendGrid.send({ - from: { - email: fromField.email, - name: fromField.name, - }, - to: email.to, - subject: email.subject, - text: email.text, - html: email.html, - }); - console.log('Email sent: ', sentEmail); - } catch (error) { - console.error('Error sending email: ', error); - } -}