restructure blog

Delete .env copy.server

update gitignore

add guided tour doc
This commit is contained in:
vincanger 2023-12-11 12:49:46 -05:00
parent a193df5c21
commit 7423313364
218 changed files with 587 additions and 659 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

13
.gitignore vendored
View File

@ -1,11 +1,8 @@
/.wasp/
/.env.server
/.env.client
.DS_Store
fly config/fly-client.toml
fly config/fly-server.toml
fly-client.toml
fly-server.toml
*/.wasp/
*/.env.server
*/.env.client
*/.DS_Store
# TODO: create a clean version for the user to fill in
# replace with your own Google Analytics service account json file
saastemplate-381911-6dc3caae2204.json

View File

@ -9,7 +9,7 @@ This template is:
3. comes with a ton of features out of the box!
4. focused on free, open-source services, where possible
Try it out here: [OpenSaaS.sh](https://opensaas.sh)
Check it out in action here: [OpenSaaS.sh](https://opensaas.sh)
Check out the Docs here: [Open SaaS Docs](https://docs.opensaas.sh)
## What's inside?

View File

@ -23,21 +23,21 @@ GOOGLE_CLIENT_SECRET=
# get your sendgrid api key at https://app.sendgrid.com/settings/api_keys
SENDGRID_API_KEY=
# if not explicitly set to true, emails be logged to console but not actually sent
SEND_EMAILS_IN_DEVELOPMENT=true
# if not explicitly set to true, emails will be logged to console but not actually sent during development
SEND_EMAILS_IN_DEVELOPMENT=false
# (OPTIONAL) get your openai api key at https://platform.openai.com/account
OPENAI_API_KEY=
# (OPTIONAL) get your plausible api key at https://plausible.io/login or https://your-plausible-instance.com/login
PLAUSIBLE_API_KEY=
PLAUSIBLE_API_KEY=gUTgtB...
# You will find your site id in the Plausible dashboard. It will look like 'opensaas.sh'
PLAUSIBLE_SITE_ID=
PLAUSIBLE_SITE_ID=yoursite.com
PLAUSIBLE_BASE_URL=https://plausible.io/api/v1 # if you are self-hosting plausible, change this to your plausible instance's base url
# (OPTIONAL) get your google service account key at https://console.cloud.google.com/iam-admin/serviceaccounts
GOOGLE_ANALYTICS_CLIENT_EMAIL=
GOOGLE_ANALYTICS_CLIENT_EMAIL=email@example.gserviceaccount.com
# Make sure you convert the private key within the JSON file to base64 first with `echo -n "PRIVATE_KEY" | base64`. see the docs for more info.
GOOGLE_ANALYTICS_PRIVATE_KEY=
GOOGLE_ANALYTICS_PRIVATE_KEY=LS02...
# You will find your Property ID in the Google Analytics dashboard. It will look like '987654321'
GOOGLE_ANALYTICS_PROPERTY_ID=
GOOGLE_ANALYTICS_PROPERTY_ID=123456789

View File

@ -24,25 +24,26 @@ app SaaSTemplate {
userEntity: User,
externalAuthEntity: SocialLogin,
methods: {
email: {
fromField: {
name: "Open SaaS App",
// make sure this address is the same you registered your SendGrid or MailGun account with!
email: "vince@wasp-lang.dev"
},
emailVerification: {
clientRoute: EmailVerificationRoute,
getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
},
passwordReset: {
clientRoute: PasswordResetRoute,
getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
},
},
google: { // Guide for setting up Auth via Google https://wasp-lang.dev/docs/auth/social-auth/overview
getUserFieldsFn: import { getUserFields } from "@server/auth/google.js",
configFn: import { config } from "@server/auth/google.js",
},
usernameAndPassword: {},
// google: { // Guide for setting up Auth via Google https://wasp-lang.dev/docs/auth/social-auth/overview
// getUserFieldsFn: import { getUserFields } from "@server/auth/google.js",
// configFn: import { config } from "@server/auth/google.js",
// },
// email: {
// fromField: {
// name: "Open SaaS App",
// // make sure this address is the same you registered your SendGrid or MailGun account with!
// email: "vince@wasp-lang.dev"
// },
// emailVerification: {
// clientRoute: EmailVerificationRoute,
// getEmailContentFn: import { getVerificationEmailContent } from "@server/auth/email.js",
// },
// passwordReset: {
// clientRoute: PasswordResetRoute,
// getEmailContentFn: import { getPasswordResetEmailContent } from "@server/auth/email.js",
// },
// },
},
signup: {
additionalFields: import setAdminUsers from "@server/auth/setAdminUsers.js",
@ -92,7 +93,8 @@ app SaaSTemplate {
entity User {=psl
id Int @id @default(autoincrement())
email String? @unique
password String?
username String @unique
password String
createdAt DateTime @default(now())
lastActiveTimestamp DateTime @default(now())
isAdmin Boolean @default(false)
@ -192,20 +194,20 @@ page SignupPage {
component: import { Signup } from "@client/auth/SignupPage"
}
route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
page RequestPasswordResetPage {
component: import { RequestPasswordReset } from "@client/auth/RequestPasswordReset",
}
// route RequestPasswordResetRoute { path: "/request-password-reset", to: RequestPasswordResetPage }
// page RequestPasswordResetPage {
// component: import { RequestPasswordReset } from "@client/auth/RequestPasswordReset",
// }
route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
page PasswordResetPage {
component: import { PasswordReset } from "@client/auth/PasswordReset",
}
// route PasswordResetRoute { path: "/password-reset", to: PasswordResetPage }
// page PasswordResetPage {
// component: import { PasswordReset } from "@client/auth/PasswordReset",
// }
route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
page EmailVerificationPage {
component: import { EmailVerification } from "@client/auth/EmailVerification",
}
// route EmailVerificationRoute { path: "/email-verification", to: EmailVerificationPage }
// page EmailVerificationPage {
// component: import { EmailVerification } from "@client/auth/EmailVerification",
// }
route GptRoute { path: "/gpt", to: GptPage }
page GptPage {
@ -340,7 +342,7 @@ api stripeWebhook {
httpRoute: (POST, "/stripe-webhook")
}
/* 🕵️‍♂️ These are the Wasp Cron Jobs. Use them to set up recurring tasks and/or queues:
/* 🕵️‍♂️ These are the Wasp Jobs. Use them to set up recurring tasks and/or queues:
* https://wasp-lang.dev/docs/advanced/jobs
*/
@ -361,8 +363,8 @@ job dailyStatsJob {
fn: import { calculateDailyStats } from "@server/workers/calculateDailyStats.js"
},
schedule: {
// cron: "0 * * * *" // every hour. use in production
cron: "* * * * *" // every minute. useful for debugging
cron: "0 * * * *" // every hour. useful in production
// cron: "* * * * *" // every minute. useful for debugging
},
entities: [User, DailyStats, Logs, PageViewSource]
}

View File

@ -0,0 +1,113 @@
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"email" TEXT,
"password" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastActiveTimestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isAdmin" BOOLEAN NOT NULL DEFAULT false,
"isEmailVerified" BOOLEAN NOT NULL DEFAULT false,
"emailVerificationSentAt" TIMESTAMP(3),
"passwordResetSentAt" TIMESTAMP(3),
"stripeId" TEXT,
"checkoutSessionId" TEXT,
"hasPaid" BOOLEAN NOT NULL DEFAULT false,
"subscriptionTier" TEXT,
"subscriptionStatus" TEXT,
"sendEmail" BOOLEAN NOT NULL DEFAULT false,
"datePaid" TIMESTAMP(3),
"credits" INTEGER NOT NULL DEFAULT 3,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SocialLogin" (
"id" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerId" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SocialLogin_pkey" PRIMARY KEY ("id")
);
-- 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")
);
-- CreateTable
CREATE TABLE "ContactFormMessage" (
"id" TEXT NOT NULL,
"content" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"isRead" BOOLEAN NOT NULL DEFAULT false,
"repliedAt" TIMESTAMP(3),
CONSTRAINT "ContactFormMessage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DailyStats" (
"id" SERIAL NOT NULL,
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"totalViews" INTEGER NOT NULL DEFAULT 0,
"prevDayViewsChangePercent" TEXT NOT NULL DEFAULT '0',
"userCount" INTEGER NOT NULL DEFAULT 0,
"paidUserCount" INTEGER NOT NULL DEFAULT 0,
"userDelta" INTEGER NOT NULL DEFAULT 0,
"paidUserDelta" INTEGER NOT NULL DEFAULT 0,
"totalRevenue" DOUBLE PRECISION NOT NULL DEFAULT 0,
"totalProfit" DOUBLE PRECISION NOT NULL DEFAULT 0,
CONSTRAINT "DailyStats_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PageViewSource" (
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" TEXT NOT NULL,
"visitors" INTEGER NOT NULL,
"dailyStatsId" INTEGER,
CONSTRAINT "PageViewSource_pkey" PRIMARY KEY ("date","name")
);
-- CreateTable
CREATE TABLE "Logs" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"message" TEXT NOT NULL,
"level" TEXT NOT NULL,
CONSTRAINT "Logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "SocialLogin_provider_providerId_userId_key" ON "SocialLogin"("provider", "providerId", "userId");
-- CreateIndex
CREATE UNIQUE INDEX "DailyStats_date_key" ON "DailyStats"("date");
-- AddForeignKey
ALTER TABLE "SocialLogin" ADD CONSTRAINT "SocialLogin_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "GptResponse" ADD CONSTRAINT "GptResponse_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ContactFormMessage" ADD CONSTRAINT "ContactFormMessage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PageViewSource" ADD CONSTRAINT "PageViewSource_dailyStatsId_fkey" FOREIGN KEY ("dailyStatsId") REFERENCES "DailyStats"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,14 @@
/*
Warnings:
- A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail.
- Added the required column `username` to the `User` table without a default value. This is not possible if the table is not empty.
- Made the column `password` on table `User` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "username" TEXT NOT NULL,
ALTER COLUMN "password" SET NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");

View File

@ -1,12 +1,12 @@
import { useState, useEffect } from 'react';
import SwitcherOne from './SwitcherOne';
import DropdownEditDelete from './DropdownEditDelete';
import { useQuery } from '@wasp/queries';
import getPaginatedUsers from '@wasp/queries/getPaginatedUsers';
import updateUserById from '@wasp/actions/updateUserById';
import Loader from '../common/Loader';
import DropdownEditDelete from './DropdownEditDelete';
type StatusOptions = 'past_due' | 'canceled' | 'active';
type StatusOptions = 'past_due' | 'canceled' | 'active' | 'deleted';
const UsersTable = () => {
const [skip, setskip] = useState(0);
@ -170,7 +170,7 @@ const UsersTable = () => {
<div className='grid grid-cols-12 border-t-4 border-stroke py-4.5 px-4 dark:border-strokedark md:px-6 '>
<div className='col-span-3 flex items-center'>
<p className='font-medium'>Email</p>
<p className='font-medium'>Email / Username</p>
</div>
<div className='col-span-3 hidden items-center sm:flex'>
<p className='font-medium'>Last Active</p>
@ -185,7 +185,7 @@ const UsersTable = () => {
<p className='font-medium'>Has Paid</p>
</div>
<div className='col-span-1 flex items-center'>
<p className='font-medium'>Delete User</p>
<p className='font-medium'></p>
</div>
</div>
{isLoading && (
@ -201,12 +201,14 @@ const UsersTable = () => {
className='grid grid-cols-12 gap-4 border-t border-stroke py-4.5 px-4 dark:border-strokedark md:px-6 '
>
<div className='col-span-3 flex items-center'>
<div className='flex flex-col gap-4 sm:flex-row sm:items-center'>
<div className='flex flex-col gap-1 '>
<p className='text-sm text-black dark:text-white'>{user.email}</p>
<p className='text-sm text-black dark:text-white'>{user.username}</p>
</div>
</div>
<div className='col-span-3 hidden items-center sm:flex'>
<p className='text-sm text-black dark:text-white'>{user.lastActiveTimestamp.toISOString()}</p>
<p className='text-sm text-black dark:text-white'>{user.lastActiveTimestamp.toLocaleDateString() + ' ' + user.lastActiveTimestamp.toLocaleTimeString()}</p>
</div>
<div className='col-span-2 flex items-center'>
<p className='text-sm text-black dark:text-white'>{user.subscriptionStatus}</p>

View File

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 493 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Some files were not shown because too many files have changed in this diff Show More