update for rc

This commit is contained in:
vincanger 2023-04-07 12:11:10 +02:00
parent 70f7ce062b
commit 5101652e72
10 changed files with 85 additions and 136 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -3,25 +3,17 @@
<img src='src/client/static/gptsaastemplate.png' width='700px'/>
## 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 <project-name> -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=<your-postgres-connection-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=<your-postgres-connection-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

View File

@ -8,4 +8,6 @@ GOOGLE_CLIENT_SECRET=
OPENAI_API_KEY=
SENDGRID_API_KEY=
SENDGRID_API_KEY=
# if not excplicitly set to true, emails be logged to console but not actually sent
SEND_EMAILS_IN_DEVELOPMENT=true

View File

@ -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 {
"<meta property='og:url' content='https://mySaaSapp.com' />",
"<meta property='og:description' content='I made a SaaS App. Buy my stuff.' />",
"<meta property='og:image' content='src/client/static/image.png' />",
// 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]
}

View File

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

View File

@ -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<GptPayload, RelatedObject>
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<GptPayload, RelatedObject>
});
}
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<GptPayload, RelatedObject>
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<GptPayload, RelatedObject>
return new Promise((resolve, reject) => {
reject(new HttpError(500, 'Something went wrong'));
});
};
};

View File

@ -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() {

View File

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

View File

@ -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 = {
</html>`,
};
// 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);
}

View File

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