diff --git a/.env.server.example b/.env.server.example index 65731df..9f46ba4 100644 --- a/.env.server.example +++ b/.env.server.example @@ -11,6 +11,9 @@ PRO_SUBSCRIPTION_PRICE_ID=price_... # after downloading starting the stripe cli (https://stripe.com/docs/stripe-cli) with `stripe listen --forward-to localhost:3001/stripe-webhook` it will output your signing secret STRIPE_WEBHOOK_SECRET=whsec_... +# set this as a comma-separated list of emails you want to give admin privileges to upon registeration +ADMIN_EMAILS=me@example.com,you@example.com,them@example.com + # this needs to be a string at least 32 characters long JWT_SECRET= diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 6ad77e8..9074627 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -60,7 +60,8 @@ export default defineConfig({ { label: 'General', items: [ - { label: 'User Permissions', link: 'general/user-permissions/' }, + { label: 'Admin Dashboard', link: '/general/admin-dashboard/' }, + { label: 'User Permissions', link: '/general/user-permissions/' }, ], }, ], diff --git a/docs/src/content/docs/general/admin-dashboard.md b/docs/src/content/docs/general/admin-dashboard.md new file mode 100644 index 0000000..4678c74 --- /dev/null +++ b/docs/src/content/docs/general/admin-dashboard.md @@ -0,0 +1,62 @@ +--- +title: Admin Dashboard +--- +This is a reference on how the Admin dashboard is set up and works. + +## Permissions + +The Admin dashboard is only accessible to users with the `isAdmin` field set to true. + +```tsx title="main.wasp" {5} +entity User {=psl + id Int @id @default(autoincrement()) + email String? @unique + password String? + isAdmin Boolean @default(false) + //... +``` + +To give yourself administrator priveledges, make sure you add your email adderesses to the `ADMIN_EMAILS` environment variable in `.env.server` file before registering/logging in with that email address. + +```sh title=".env.server" +ADMIN_EMAILS=me@example.com +``` + +if you want to give administrator priveledges to other users, you can do so by adding them to `ADMIN_EMAILS` as a comma-separated list. + +```sh title=".env.server" +ADMIN_EMAILS=me@example.com,you@example.com,them@example.com +``` + +## Admin Dashboard Pages + +### Dashboard +The Admin dashboard is a single place for you to view your most important metrics and perform some admin tasks. At the moment, it pulls data from: + + + +- [Stripe](/guides/stripe-integration): + - total revenue + - revenue for each day of the past week +- [Google or Plausible](/guides/analytics): + - total number of page views (non-unique) + - percentage change in page views from the previous day + - top sources/referrers with unique visitor count (i.e. how many people came from that source to your app) +- Database: + - total number of registered users + - daily change in number of registered users + - total number of paying users + - daily change in number of paying users + +For a guide on how to integrate these services, check out the [Stripe](/guides/stripe-integration) and [Analytics guide](/guides/analytics) of the docs. + + +:::tip[Help us improve] +We're always looking to improve the Admin dashboard. If you feel something is missing or could be improved, consider [opening an issue]() or [submitting a pull request]() +::: + +### Users +The Users page is where you can view all your users and their most important details. You can also search and filter users by: +- email address +- subscription status + diff --git a/docs/src/content/docs/guides/analytics.md b/docs/src/content/docs/guides/analytics.md index 13ea9f1..3039485 100644 --- a/docs/src/content/docs/guides/analytics.md +++ b/docs/src/content/docs/guides/analytics.md @@ -9,7 +9,15 @@ Plausible is an open-source, privacy-friendly alternative to Google Analytics. I ## Plausible -*coming soon... until then, check out the [official documentation](https://plausible.io/docs)* + +*coming soon...* +*until then, check out the [official documentation](https://plausible.io/docs)* + +:::tip[Contribute!] +If you'd like to help us write this guide, click the "Edit page" button at the bottom of this page + +As a completely free, open-source project, we appreciate any help 🙏 +::: ## Google Analytics diff --git a/docs/src/content/docs/guides/email-sending.md b/docs/src/content/docs/guides/email-sending.md deleted file mode 100644 index 6e6f2bd..0000000 --- a/docs/src/content/docs/guides/email-sending.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Email Sending ---- - -This reference explains when emails \ No newline at end of file diff --git a/docs/src/content/docs/guides/email-sending.mdx b/docs/src/content/docs/guides/email-sending.mdx new file mode 100644 index 0000000..5198bfd --- /dev/null +++ b/docs/src/content/docs/guides/email-sending.mdx @@ -0,0 +1,81 @@ +--- +title: Email Sending +--- +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +This guide explains how to use the integrated email sender and how you can integrate your own account in this template. + +## Sending Emails + +In the `main.wasp` config file, you'll see we've got the email sender set up for you: + +```tsx title="main.wasp" +app SaaSTemplate { + // ... + emailSender: { + provider: SendGrid, + defaultFrom: { + name: "Open SaaS App", + // make sure this address is the same you registered your SendGrid or MailGun account with! + email: "my@email.com" + }, + }, +``` + +This means that you can send emails from your app using the `send` function from the `email` module: + +```tsx title="src/server/webhooks.ts" +import { emailSender } from "@wasp/email/index.js"; + +//... + + if (subscription.cancel_at_period_end) { + await emailSender.send({ + to: customer.email, + subject: 'We hate to see you go :(', + text: 'We hate to see you go. Here is a sweet offer...', + html: 'We hate to see you go. Here is a sweet offer...', + }); + } +``` + +In the example above, you can see that we're sending an email to the customer when we receive a cancel subscription event within the Stripe webhook. + +This is a powerful feature and super simple to use. + +:::tip[Sending Emails in Development] +In the `.env.server` file, we've set the `SEND_EMAILS_IN_DEVELOPMENT` env variable to true. This means that emails will be sent in development mode. + +This is useful for testing, but you can turn it off by setting it to false, and the emails will be logged to the console instead. +::: + +## Integrate your email sender + +To set up your email sender, you first need an account with one of the supported email providers. + + + + - Register at SendGrid.com and then get your [API KEYS](https://app.sendgrid.com/settings/api_keys). + - Copy yours to the `.env.server` file under the `SENDGRID_API_KEY` variable. + + + - Go to [Mailgun](https://mailgun.com) and create an account. + - Go to [API Keys](https://app.mailgun.com/app/account/security/api_keys) and create a new API key. + - Copy the API key and add it to your .env.server file under the `MAILGUN_API_KEY=` variable. + - Go to [Domains](https://app.mailgun.com/app/domains) and create a new domain. + - Copy the domain and add it to your .env.server file as `MAILGUN_DOMAIN=`. + + + +Make sure to change the `defaultFrom` email address in the `main.wasp` file to the same email address you used to register your account with. + +```tsx title="main.wasp" {5} +emailSender: { + provider: SendGrid, + defaultFrom: { + name: "Open SaaS App", + email: "my@email.com" + }, +``` + +If you want more detailed info, or would like to use SMTP, check out the [Wasp docs](https://wasp-lang.dev/docs/advanced/email). \ No newline at end of file diff --git a/main.wasp b/main.wasp index b03cc1b..40c595f 100644 --- a/main.wasp +++ b/main.wasp @@ -2,7 +2,7 @@ app SaaSTemplate { wasp: { version: "^0.11.6" }, - title: "My SaaS App", + title: "My Open SaaS App", head: [ "", "", // TODO change url @@ -26,7 +26,7 @@ app SaaSTemplate { methods: { email: { fromField: { - name: "SaaS App", + name: "Open SaaS App", // make sure this address is the same you registered your SendGrid or MailGun account with! email: "vince@wasp-lang.dev" }, @@ -44,13 +44,15 @@ app SaaSTemplate { configFn: import { config } from "@server/auth/google.js", }, }, + signup: { + additionalFields: import setAdminUsers from "@server/auth/setAdminUsers.js", + }, onAuthFailedRedirectTo: "/", }, db: { system: PostgreSQL, seeds: [ import { devSeedUsers } from "@server/scripts/usersSeed.js", - ] }, client: { @@ -64,6 +66,7 @@ app SaaSTemplate { email: "vince@wasp-lang.dev" // TODO change to generic email before pushing to github }, }, + // add your dependencies here. the quickest way to find the latest version is `npm view version` dependencies: [ ("@headlessui/react", "1.7.13"), ("@tailwindcss/forms", "^0.5.3"), @@ -104,7 +107,7 @@ entity User {=psl sendEmail Boolean @default(false) datePaid DateTime? credits Int @default(3) - relatedObject RelatedObject[] + gptResponses GptResponse[] externalAuthAssociations SocialLogin[] contactFormMessages ContactFormMessage[] psl=} @@ -120,7 +123,7 @@ entity SocialLogin {=psl psl=} // This can be anything. In most cases, this will be your product -entity RelatedObject {=psl +entity GptResponse {=psl id String @id @default(uuid()) content String user User @relation(fields: [userId], references: [id]) @@ -205,6 +208,7 @@ page EmailVerificationPage { route GptRoute { path: "/gpt", to: GptPage } page GptPage { + authRequired: true, component: import GptPage from "@client/app/GptPage" } @@ -287,7 +291,7 @@ page AdminUIButtonsPage { action generateGptResponse { fn: import { generateGptResponse } from "@server/actions.js", - entities: [User, RelatedObject] + entities: [User, GptResponse] } action stripePayment { @@ -307,9 +311,9 @@ action updateUserById { // 📚 Queries -query getRelatedObjects { - fn: import { getRelatedObjects } from "@server/queries.js", - entities: [User, RelatedObject] +query getGptResponses { + fn: import { getGptResponses } from "@server/queries.js", + entities: [User, GptResponse] } query getDailyStats { diff --git a/migrations/20231130143812_gpt_response/migration.sql b/migrations/20231130143812_gpt_response/migration.sql new file mode 100644 index 0000000..7d783ad --- /dev/null +++ b/migrations/20231130143812_gpt_response/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - You are about to drop the `RelatedObject` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "RelatedObject" DROP CONSTRAINT "RelatedObject_userId_fkey"; + +-- DropTable +DROP TABLE "RelatedObject"; + +-- CreateTable +CREATE TABLE "GptResponse" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "GptResponse_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "GptResponse" ADD CONSTRAINT "GptResponse_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/src/client/app/AccountPage.tsx b/src/client/app/AccountPage.tsx index 0d5fde2..540c226 100644 --- a/src/client/app/AccountPage.tsx +++ b/src/client/app/AccountPage.tsx @@ -1,6 +1,6 @@ import { User } from '@wasp/entities'; import { useQuery } from '@wasp/queries' -import getRelatedObjects from '@wasp/queries/getRelatedObjects' +import getGptResponses from '@wasp/queries/getGptResponses' import logout from '@wasp/auth/logout'; import { useState, Dispatch, SetStateAction } from 'react'; import { Link } from '@wasp/router' @@ -10,7 +10,7 @@ import { TierIds } from '@wasp/shared/const'; export default function AccountPage({ user }: { user: User }) { const [isLoading, setIsLoading] = useState(false); - const { data: relatedObjects, isLoading: isLoadingRelatedObjects } = useQuery(getRelatedObjects); + const { data: gptResponses, isLoading: isLoadingGptResponses } = useQuery(getGptResponses); return (
@@ -45,10 +45,10 @@ export default function AccountPage({ user }: { user: User }) {
I'm a cool customer.
-
Most Recent User RelatedObject
+
Most Recent GPT Response
- {!!relatedObjects && relatedObjects.length > 0 - ? relatedObjects[relatedObjects.length - 1].content + {!!gptResponses && gptResponses.length > 0 + ? gptResponses[gptResponses.length - 1].content : "You don't have any at this time."}
diff --git a/src/client/components/AppNavBar.tsx b/src/client/components/AppNavBar.tsx index 85bee19..309edbf 100644 --- a/src/client/components/AppNavBar.tsx +++ b/src/client/components/AppNavBar.tsx @@ -7,9 +7,9 @@ import DropdownUser from './DropdownUser'; const navigation = [ { name: 'GPT Wrapper', href: '/gpt' }, - { name: 'Documentation', href: 'https://saas-template.gitbook.io/test' }, - { name: 'Blog', href: 'https://saas-template.gitbook.io/posts/' }, -]; + { name: 'Documentation', href: '#' }, // TODO: add link to docs + { name: 'Blog', href: '#' }, // TODO: add link to blog +]; export default function AppNavBar() { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); diff --git a/src/client/landing-page/LandingPage.tsx b/src/client/landing-page/LandingPage.tsx index 076efd0..b50b5d1 100644 --- a/src/client/landing-page/LandingPage.tsx +++ b/src/client/landing-page/LandingPage.tsx @@ -395,7 +395,7 @@ export default function LandingPage() { - {/* FAQs */} + {/* FAQ */}

Frequently asked questions

@@ -409,123 +409,38 @@ export default function LandingPage() { ))}
- - {/* CTA section */} -
-