restructure blog
Delete .env copy.server update gitignore add guided tour doc
13
.gitignore
vendored
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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
|
@ -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]
|
||||
}
|
113
app/migrations/20231211161119_init/migration.sql
Normal 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;
|
14
app/migrations/20231211163814_username/migration.sql
Normal 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");
|
@ -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>
|
Before Width: | Height: | Size: 493 B After Width: | Height: | Size: 493 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |