All in dirs (#154)
* Split the project into template and opensaas-sh (demo app (diff) + docs). * fix
16
.github/workflows/e2e-tests.yml
vendored
@@ -32,11 +32,13 @@ jobs:
|
|||||||
- name: Docker setup
|
- name: Docker setup
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# In order for the app to run in the dev mode we need to set the required env vars even if they aren't actually used by the app.
|
# In order for the app to run in the dev mode we need to set the required env vars even if
|
||||||
# This step sets mock env vars in order to pass the validation steps so the app can run
|
# they aren't actually used by the app. This step sets mock env vars in order to pass the
|
||||||
# in the CI environment. For env vars that are actually used in tests and therefore need real values, we set them in
|
# validation steps so the app can run in the CI environment. For env vars that are actually
|
||||||
# the GitHub secrets settings and access them in a step below.
|
# used in tests and therefore need real values, we set them in the GitHub secrets settings and
|
||||||
|
# access them in a step below.
|
||||||
- name: Set required wasp app env vars to mock values
|
- name: Set required wasp app env vars to mock values
|
||||||
|
working-directory: ./template
|
||||||
run: |
|
run: |
|
||||||
cd app
|
cd app
|
||||||
cp .env.server.example .env.server && cp .env.client.example .env.client
|
cp .env.server.example .env.server && cp .env.client.example .env.client
|
||||||
@@ -45,17 +47,19 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: node-modules-${{ runner.os }}-${{ hashFiles('app/package-lock.json') }}-${{ hashFiles('e2e-tests/package-lock.json') }}-wasp${{ env.WASP_VERSION }}-node${{ steps.setup-node.outputs.node-version }}
|
key: node-modules-${{ runner.os }}-${{ hashFiles('template/app/package-lock.json') }}-${{ hashFiles('template/e2e-tests/package-lock.json') }}-wasp${{ env.WASP_VERSION }}-node${{ steps.setup-node.outputs.node-version }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
node-modules-${{ runner.os }}-
|
node-modules-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Install Node.js dependencies for Playwright tests
|
- name: Install Node.js dependencies for Playwright tests
|
||||||
if: steps.cache-e2e-tests.outputs.cache-hit != 'true'
|
if: steps.cache-e2e-tests.outputs.cache-hit != 'true'
|
||||||
|
working-directory: ./template
|
||||||
run: |
|
run: |
|
||||||
cd e2e-tests
|
cd e2e-tests
|
||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
- name: Store Playwright's Version
|
- name: Store Playwright's Version
|
||||||
|
working-directory: ./template
|
||||||
run: |
|
run: |
|
||||||
cd e2e-tests
|
cd e2e-tests
|
||||||
PLAYWRIGHT_VERSION=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//')
|
PLAYWRIGHT_VERSION=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//')
|
||||||
@@ -71,6 +75,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Playwright
|
- name: Set up Playwright
|
||||||
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
|
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
|
||||||
|
working-directory: ./template
|
||||||
run: |
|
run: |
|
||||||
cd e2e-tests
|
cd e2e-tests
|
||||||
npx playwright install --with-deps
|
npx playwright install --with-deps
|
||||||
@@ -102,6 +107,7 @@ jobs:
|
|||||||
PRO_SUBSCRIPTION_PRICE_ID: ${{ secrets.PRO_SUBSCRIPTION_PRICE_ID }}
|
PRO_SUBSCRIPTION_PRICE_ID: ${{ secrets.PRO_SUBSCRIPTION_PRICE_ID }}
|
||||||
CREDITS_PRICE_ID: ${{ secrets.CREDITS_PRICE_ID }}
|
CREDITS_PRICE_ID: ${{ secrets.CREDITS_PRICE_ID }}
|
||||||
SKIP_EMAIL_VERIFICATION_IN_DEV: true
|
SKIP_EMAIL_VERIFICATION_IN_DEV: true
|
||||||
|
working-directory: ./template
|
||||||
run: |
|
run: |
|
||||||
cd e2e-tests
|
cd e2e-tests
|
||||||
npm run e2e:playwright
|
npm run e2e:playwright
|
||||||
|
|||||||
30
.github/workflows/retag-commit.yml
vendored
@@ -1,30 +0,0 @@
|
|||||||
name: Retag Commit
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
retag:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
TAG_NAME: wasp-v0.13-template
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Configure Git
|
|
||||||
run: |
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
|
|
||||||
- name: Delete Old Tag
|
|
||||||
run: |
|
|
||||||
git tag -d ${{ env.TAG_NAME }} || true
|
|
||||||
git push origin :refs/tags/${{ env.TAG_NAME }} || true
|
|
||||||
|
|
||||||
- name: Add New Tag
|
|
||||||
run: |
|
|
||||||
git tag ${{ env.TAG_NAME }}
|
|
||||||
git push origin ${{ env.TAG_NAME }}
|
|
||||||
9
.gitignore
vendored
@@ -1,7 +1,2 @@
|
|||||||
*/.wasp/
|
# There is no need to pollute the template with the migrations.
|
||||||
*/.env.server
|
template/app/migrations/
|
||||||
*/.env.client
|
|
||||||
*/node_modules
|
|
||||||
*/migrations
|
|
||||||
*/.DS_Store
|
|
||||||
.DS_Store
|
|
||||||
|
|||||||
@@ -6,15 +6,24 @@ Check if there is a GitHub issue already for the thing you would like to work on
|
|||||||
Let us know, in the issue, that you would like to work on it and how you plan to approach it.
|
Let us know, in the issue, that you would like to work on it and how you plan to approach it.
|
||||||
This helps, especially with the more complex issues, as it allows us to discuss the solution upfront and make sure it is well planned and fits with the rest of the project.
|
This helps, especially with the more complex issues, as it allows us to discuss the solution upfront and make sure it is well planned and fits with the rest of the project.
|
||||||
|
|
||||||
|
## Repo organization
|
||||||
|
|
||||||
|
Repo is divided into two main parts: [template](/template) dir and [opensaas-sh](/opensaas-sh) dir.
|
||||||
|
|
||||||
|
`template` contains the actual open saas template that will be used by Wasp to create your new open-saas-based app when you run `wasp new -t saas`.
|
||||||
|
|
||||||
|
`opensaas-sh` is the app deployed to https://opensaas.sh , and is actually made with open saas! It contains a demo app and open saas docs. We keep it updated as we work on the template.
|
||||||
|
|
||||||
## How to Contribute
|
## How to Contribute
|
||||||
1. Make sure you understand the basics of how open-saas works (check out [docs](https://docs.opensaas.sh)).
|
1. Make sure you understand the basics of how open-saas works (check out [docs](https://docs.opensaas.sh)).
|
||||||
2. Check out this repo (`main` branch) and make sure you are able to get the app in `app/` running (to set it up, follow the same steps as for running a new open-saas app, as explained in the open-saas docs).
|
2. Check out this repo (`main` branch) and make sure you are able to get the app in [template/app/](/template/app) running (to set it up, follow the same steps as for running a new open-saas app, as explained in the open-saas docs).
|
||||||
3. Create a new git branch for your work (aka feature branch) and do your changes on it.
|
3. Create a new git branch for your work (aka feature branch) and do your changes on it.
|
||||||
4. Update e2e tests in [e2e-tests](/e2e-tests/) if needed and make sure they are passing.
|
4. Update e2e tests in [template/e2e-tests](/template/e2e-tests/) if needed and make sure they are passing.
|
||||||
5. Create a pull request (towards `main` as a base branch).
|
5. Update docs in [opensaas-sh/blog/src/content/docs](/opensaas-sh/blog/src/content/docs/) if needed. Check [opensaas-sh/README.md](/opensaas-sh/README.md) for more details.
|
||||||
6. If docs (also) need updating, check out the `deployed-version` branch, make your own feature branch from it, make changes in [blog/src/content/docs](/blog/src/content/docs/), and submit another PR with those changes (towards `deployed-version` as a base branch).
|
6. Update demo app in [opensaas-sh/app_diff](/opensaas-sh/app_diff) if needed. Check [opensaas-sh/README.md](/opensaas-sh/README.md) for more details.
|
||||||
7. Make a "Da Boi" meme while you wait for us to review your PR(s).
|
7. Create a pull request (towards `main` as a base branch).
|
||||||
8. If you don't know who "Da Boi" is, head back to the [Wasp Discord](https://discord.gg/aCamt5wCpS) and find out :)
|
8. Make a "Da Boi" meme while you wait for us to review your PR(s).
|
||||||
|
9. If you don't know who "Da Boi" is, head back to the [Wasp Discord](https://discord.gg/aCamt5wCpS) and find out :)
|
||||||
|
|
||||||
## Additional Info
|
## Additional Info
|
||||||
|
|
||||||
@@ -24,20 +33,9 @@ Whenever a user starts a new Wasp project with `wasp new -t <template_name>`, Wa
|
|||||||
|
|
||||||
In the case of Open SaaS, which is a Wasp template, the tag is `wasp-v{{version}}-template`, where `{{version}}` is the current version of Wasp, e.g. `wasp-v0.13-template`.
|
In the case of Open SaaS, which is a Wasp template, the tag is `wasp-v{{version}}-template`, where `{{version}}` is the current version of Wasp, e.g. `wasp-v0.13-template`.
|
||||||
|
|
||||||
For simplicity, in Open SaaS, we automatically re-apply the tag to the most recent commit on the `main` branch via the `.github/workflows/retag-commit.yml` workflow. This way, users always get the latest version of the template (as on `main` branch) when they start a new project via `wasp new -t saas`.
|
We manually (re)assign the appropriate tag when we are ready to release a new version of Open Saas.
|
||||||
|
|
||||||
**This means, that whenever a user pulls the template via `wasp new -t saas`, they are getting the version present in the most recent commit on `main`.**
|
### Releasing
|
||||||
|
|
||||||
NOTE: When Wasp releases a new major version, we should also make sure to update Open SaaS to work with this new version. In PR that will bring this update, we should also make sure to update the `TAG_NAME` in the GitHub Workflow that does the tagging, to be the template tag used by the newest version of Wasp.
|
|
||||||
|
|
||||||
### The Default Template vs. the Deployed Site / Docs
|
|
||||||
|
|
||||||
There are two main branches for development:
|
|
||||||
- `main`
|
|
||||||
- `deployed-version`
|
|
||||||
|
|
||||||
The default, clean template that users get when cloning the starter lives on `main`, while `deployed-version` is somewhat modified version of that same template which you see when you go to [OpenSaaS.sh](https://opensaas.sh) and the [docs](https://docs.opensaas.sh).
|
|
||||||
|
|
||||||
If you want to make changes to the default starter template, base feature branches and Pull Requests off of `main`.
|
|
||||||
If you want to make changes to the OpenSaaS.sh site or it's Documentation, base feature branches and Pull Requests off of `deployed-version`.
|
|
||||||
|
|
||||||
|
1. Assign appropriate tag to the commit we want to release (usually current `main`) so `wasp new -t saas` can pick up code from the [/template](/template).
|
||||||
|
2. Deploy the demo app + landing page and blog + docs: [/opensaas-sh/README.md#Deployment](/opensaas-sh/README.md#Deployment).
|
||||||
|
|||||||
30
opensaas-sh/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# OpenSaas.sh
|
||||||
|
|
||||||
|
This is the https://opensaas.sh page and demo app, built with the Open Saas template!
|
||||||
|
|
||||||
|
It consists of a Wasp app for showcasing the Open Saas template (+ landing page), while the Astro blog is blog and docs for the Open Saas template, found at https://docs.opensaas.sh.
|
||||||
|
|
||||||
|
Inception :)!
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Demo app (app_diff/)
|
||||||
|
|
||||||
|
Since the demo app is just the open saas template with some small tweaks, and we want to be able to easily keep it up to date as the template changes, we don't version (in git) the actual demo app code, instead we version the diffs between it and the template: `app_diff/`.
|
||||||
|
|
||||||
|
So because we don't version the actual demo app (`app/`) but its diffs instead (`app_diff`), the typical workflow is as follows:
|
||||||
|
1. Run `./tools/patch.sh` to generate `app/` from `../template/` and `app_diff/`.
|
||||||
|
2. If there are any conflicts (normally due to updates to the template), modify `app_diff/` till you resolve them.
|
||||||
|
3. Make any changes in the `app/` if you wish, and then generate new `app_diff/` by running `./tools/diff.sh`.
|
||||||
|
|
||||||
|
Make sure not to commit `app/` to git. It is currently (until we resolve this) not added to .gitignore because that messes up diffing for us.
|
||||||
|
|
||||||
|
### Blog (blog/)
|
||||||
|
|
||||||
|
Blog (and docs in it) is currently tracked in whole, as it has quite some content, so updating it to the latest version of Open Saas is done manually, but it might be interesting to also move it to the `diff` approach, as we use for the demo app, if it turns out to be a good match.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
App: check its README.md (after you generate it with `.tools/patch.sh`) .
|
||||||
|
|
||||||
|
Blog (docs): hosted on Netlify.
|
||||||
7
opensaas-sh/app_diff/.env.client.diff
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
--- template/app/.env.client
|
||||||
|
+++ opensaas-sh/app/.env.client
|
||||||
|
@@ -0,0 +1,3 @@
|
||||||
|
+REACT_APP_STRIPE_CUSTOMER_PORTAL=https://billing.stripe.com/p/login/test_8wM8x17JN7DT4zC000
|
||||||
|
+
|
||||||
|
+REACT_APP_GOOGLE_ANALYTICS_ID=G-H3LSJCK95H
|
||||||
|
\ No newline at end of file
|
||||||
28
opensaas-sh/app_diff/.env.vault.diff
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
--- template/app/.env.vault
|
||||||
|
+++ opensaas-sh/app/.env.vault
|
||||||
|
@@ -0,0 +1,25 @@
|
||||||
|
+#/-------------------.env.vault---------------------/
|
||||||
|
+#/ cloud-agnostic vaulting standard /
|
||||||
|
+#/ [how it works](https://dotenv.org/env-vault) /
|
||||||
|
+#/--------------------------------------------------/
|
||||||
|
+
|
||||||
|
+# development
|
||||||
|
+DOTENV_VAULT_DEVELOPMENT="jN5uAkXf9ILhD+HD38Xii0otUrM7yysT2lc+/xVdihLN1EelZ3Faj40Y+KLewE+b8UIv+WXnqXRy1AMe30EZ6r4PQ8/nAczP3riRhC8mrqbI8USjSBUNd7Fp3Zf/UfOrIiWLve+Sta4H4kRZAUfS4wfiGUWZ2p8rEciV0QTUkryy73+cxVoZk0vz/62E/mOy2uauQzP3qYZ5q3QMsJPG8wTnCrhE2JaFQI2XyOeUPcBBO9r41PBbU//rQr+tj8t12jnCh4uM61kieEU5Xa78LZjmHnA6aJx1bBCesmmntCXOJ4lNsRprkIzyEfS/1kwpOJPYewXJ8wYOmflweN1EG/86uPBWutF2LIi3zKXe53KxiMwlDAwEoakRSNJw7otVUZK1SqnqdHxj91gJI8913xwRepcx6JYdLSxOCYEmrO4MCvwZjN/uF+vb3Vx9i9MNX8itOUNvQHFWWky6BP2PbO+aNC3oRQ5KZQ0dhnSTwy/pn3L0d5L18CIZoscyPgzviDjuL3LAZqUmrObUGx7DlZLGJ/VGaH9SUtf6PRo9w98PwnChl65874rlAKwDhcZGDJXlLAOeAydvsMcEQz+ooP7WkF+vTvPvzEFWoerstrfzqG8pHUsjsaW+JXXXGOKyBmnI9SSi68O/ZMKEIsR59/0DAxNaXTXeycrLMzkqSoSaztR/k3Rg+u5u/xYJcHqSSc8w3+iQNc/CBzX0y79zH9ZidBKpsZ2Xpfg+8wwcZqqv/d5WBQ1AGSfEJuy8pj8hBEaL2V+SoblAoO3TRcsTicTXnM8OMiR3mOKrvA+QKBk0jPJRefhseVhi6XYTLzTgJ2hjM1MP08YWkHgWpKQIh/eejLOl3CYafZjdJdFOD4EIkiFxOVtsmdBz4O35IkLzcUR8YQY0NUHEfCZ+1zBRgM/3qNZZa9fH91X6csrvUlAXwEa+roaaOf3w5k95eENu2NrkoNOm1+7SHb80uZQAC8/y9MzL19vCiEEBvIC8lg4KViLs/DmXCuyKXU0eeZnOlHyrCX4ZKHUWmaaYsAdJBy8KLnaG7DuZ/+A4TLq8xBNOMJ9R889U+jFXDCs51k2oD7LvRTw3OwlXeTX8no2VxWQp49BBwdDanxa6/v/f+ZG2afeeyRl0C/kyLaK3K8kIzq1YLLSsX+8p5QGInscZqK2qr6G7jFP/Pds/LB/Y6WXkUNWdQ3iEzBtl3Hv6uckwQlzsXIQX0BWxhJwRTh+miJvAdVxLaJnDUMZ7Aes2a72rgXBDYm5ZiZf8nJUgUmTgEf1HJztdu31gGFfS1oFhp8dJGMO8bDgUqGpJ8Dq6KJnjXXUFUNTGv+CxJ+QKfc/YrkxDAW2QLVhKf3v4A6JFqiH637SfrTvAfJUtDiTZSPYgB06amCAWi38kIfknQ61vNt5hHyLJXGiGew58TwdqpqfMA/R24yoIQX65lAstXiOL7oSeiEBTqUzx0ZvHteKwn6UNBOaiZvNQ207dpmzAv7bW4lZbzMQW6Z3eCo1vdLunr/s/5cazoQmOsm5OJ7ThyZ3lYOO7wCIB8xOTsE+YXApyJT1sfPOOQuw602RB1OoSqyRey/KpXEmwKeAoWxXCZ5dU6kjQGmMiieVpEjczFiXYdnMA8k32T3ki5PHPTxxEaCUv28Hqc1DqQnxOxf/aSdDtbUjBHqEoNcarOlGLwjzl2CTtcP7H1WBSDB7kfMc3Umk46RhIE4QmYkAFyEbs7Ob6eKQZRoFHnPrgJlceNc6vgradMBjZFIoDGWKVOy5L1nzQ/XRpJ/g53IS58snqcgbmsuxu6nXpgWyChboOoinTB2ToMHZfAdSkWqwVT3v77w5mWbPG352APTTGYsqrt01x8kgGMvC24P9XYMdh5oXeXPvURKnBji0G2/+l23v/zKXjgePvT7roMOHMXzxQyc+5M5U8PcVySYPdYs+nynlGAN9A3aRmMZmj6mj3TIzETrtPUVotN0ztOr+8nkR/tEnMG6tAnmRhZBJUV7EXgtJRTL9YvmHIzqVI405/G+8ikbe9oHN0BE+lHdC7fyJ9gR3u1ySEmlPnkaN8UhL8rqCP8IkLJUZszs1XELMYQW2L6s/di/cZR8XfazgbyILEEUJx9g=="
|
||||||
|
+DOTENV_VAULT_DEVELOPMENT_VERSION=4
|
||||||
|
+
|
||||||
|
+# ci
|
||||||
|
+DOTENV_VAULT_CI="BzXtisw7TpmxCnpJlocNzsueSrO+0NfaJoRWBj7IkQXc5z4WIgnF9f9F98qyYoMWJRj3ij5Ytbt4jMC/LIJTWqUdY8xSnzPA3Wt21OXZre511kiGOMbzdTsJXUN7jDX9XQxNC6aTZ/t7BwNxHi35g3acjn0qxXZDcnzMA+TRtYefuDcLjqMugVKnoL3NKtEuZnepDWgXickRk1yI/QlLYn7z2UKEDu+RTn+LcBfjKwYcSjsGxMphDCdN1zphecd7apVX57PY3zFXXVSTQLOE6EdmJk/bnvhq5meqCcjlXKe3FSAUc+TsvzGG/8P/clGX1lPMD7tIyIJACtvUmtaGinYRwCQaPQiUdYC3BmFSNs8ctH1nElSvGIwdncY3XKh1ryvpZX3pLpFhI9/n+EVt8ncBz7KP883oLzjP+yopRjOj8/T2Isrt+rXlCj4TJtnzs/VNzcokY62xfN9tx8Q+wEvZEeIY7fuVNO5wUyAUlpt2hyfuEf3tSHnT7viLt5fXZhwWc4gntT6HEUsysA49kp31VC48KjVkNsdXv960s8ZmbG/TBaShAD27zrO4G3s20lEpDnzuX+rLVAiRAvxmnAtsB5Odz+E6npoIHN+Wi/W7TXtazQ5IirCwVRLg+zPHaLmovy2ROc0qbDMZXn2M6mrHFjklO3br91u5WBZXhtt7ioP06xSe/FmKnwNFGu17NW5knWXGW/GkLhj+u5juY09ODSpPrbF0Saxp/tlL1ov3pGraXK/83Y88TuBYMtix5dqg75E8OshDrxUnQQEiVmexo9Jrtxbf50J72v3N1YA2ulCYHrcMTitgmjFVPZD0BSQESkcUIP0MHVap39XSg+uAaIHvffnNJZPY"
|
||||||
|
+DOTENV_VAULT_CI_VERSION=3
|
||||||
|
+
|
||||||
|
+# staging
|
||||||
|
+DOTENV_VAULT_STAGING="R8o5+wTbFwi3IpN48pSaw4RwxqThoeUsnBuG3oGF9UTx0HfhpHdaZ4TFlk1KoVJs/XOQl55LmMsiZX07ULeVvJogbKrK/XEyLgWXolbkpL2OEerNaaFwZ+HdPTd6Z9ZgQGtPn3/y+CDG9dVp/uYTF+73SigMhhHNUO701MNirD1A/maDFmuUKZM8RwgmgZsdd8B9pPP9J+55tc5zGd3tI2hz5svdtodirXwBW+q5GGiAYxymu6kzXKY8OtcUzJQO1QDxIgERlybnJj1rBz4/WYYuR2WBRjeUCI47FbjhG0G5P9IM3Vv/p9MN6PDV7l6rnIrT393y3yXb9WFxUqUZdOAeSzC5CVMuW+EJuCmw6+MG7BRczYpiasnYHhgFRWriU4HlE56vtt/L/5wlWvQOZpmiwwBpI8R14sJT8ZJHl0aIr3XX7kjNhXuy1s+nCuKBcnA4Scl428dw70y0crWCQ6ooWY4PbNRISDtErzxXJuLOf9301j8OdGrrBwPfRMY/uK5TzfkIuc7ODQbNXmlX+cczX07SNlF0px3/sXwfeeBHE3nj6rcOnqbu6iVhZ5m68cjluaf4fUFJGDrw5qEGj1iZLw3BmfrBAeLbGkEkgOTdouzzrLBPMgi8W7Vh7zQkb5UkhO3i//i2aTLC2KulAyB3jL4kO+XzjEToO2kWdamr9UwHPP/Ii/XFB7DNWnFio/L8oFe4ZjqztE7jgQlOCtZQhFzu92A5D7h3oQgdhIu3FLxdWzON673zmSFTdJHwP/6UHGGDBFTuh3/cycYOhytibw8PfI4GdYqh4FsJeYb1mrnQV4TFnvwaI6MrsyDyJaK82/OEySccdllsHBHuHi7m29upjCe9cD49"
|
||||||
|
+DOTENV_VAULT_STAGING_VERSION=3
|
||||||
|
+
|
||||||
|
+# production
|
||||||
|
+DOTENV_VAULT_PRODUCTION="WJOcgnA2hoxbckPeYKs0YbaTX+f6wB5c0eEzBvft4xAV4lq45I3Zd6vGksiswVXrDakCQfNYQgIZgCvNLFmCtS4ibZ1S1QUXyFtrH1uqLj3cP8TE0JrVQfzvayWaFZytTgXn2pwDJHKIFGSGq22csUd9TrTuMpeZXbaPgPGy1dFczJ/MDPXRz1ls7bsV/Lie+lzozhmLZZzbVKgw0Gymgtk+oqFuqoO0WFlJeTGYrnZgdg5DUW3LpIsMj+7fpmha8s2MrzL8Npo/W0XkR6ifH3JW9yOUUHVD9iJcRCky+HRFzARD74I3BVWxhJ/uHJrrTU2ArdTDbubnJZKXJiIJixKUquI+44FjiueayGkwEha+JdIO6D7mYFNw4mF2cqRxLiU6gHQXFIFPH65l4vyLmoo77d9//1NEhBXBqjxypqDq9CG02tvm0zskp0Qf62UoA/rdgarFsGsJG1/neqjhgYRPBE6zub4CQZogdGG3X7JG8Xt5jtYqzxln8S79i1zGY2VTVcy8/Qmva95XBqD6ljQLalBXG2qZK1UtAENubZ8OZ4zhI6kAP7Utp+mt3U3bVQceF/qpKCdJER+4SN8QrV5kx8o12D6f0ma2UnuCgpqajXOPVnTKvNmp6So4iXYug4Y+LVirL2m8jpaupDutPPa3CeDAkH4y9yCz8TGLX7pG2+Zp04quZVZN76yecacwsigIfYCsJg3tx/Yv36cJcoON/KlJIW2cZPK6/E8e7yrljk+gRbE6ICSjOwbfcdp0xFmQb1dLARPeILtjsjJ93RCPZV3TeNrMWcNl2Hj/ASCtgHpL48o90UnwEKTHBmu+hogtzd8NRyvUXdXayXT3Tn3rIj1p/Qg052zN"
|
||||||
|
+DOTENV_VAULT_PRODUCTION_VERSION=3
|
||||||
|
+
|
||||||
|
+#/----------------settings/metadata-----------------/
|
||||||
|
+DOTENV_VAULT="vlt_47e3eeb0730e831e688049600e59f8975260a1f00302ae08684ed87ba67872d0"
|
||||||
|
+DOTENV_API_URL="https://vault.dotenv.org"
|
||||||
|
+DOTENV_CLI="npx dotenv-vault@latest"
|
||||||
31
opensaas-sh/app_diff/.gitignore.diff
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
--- template/app/.gitignore
|
||||||
|
+++ opensaas-sh/app/.gitignore
|
||||||
|
@@ -2,16 +2,24 @@
|
||||||
|
node_modules/
|
||||||
|
**/.DS_Store
|
||||||
|
|
||||||
|
-# We ignore env files recognized and used by Wasp.
|
||||||
|
+# We ignore sensitive env files recognized and used by Wasp.
|
||||||
|
.env.server
|
||||||
|
-.env.client
|
||||||
|
-
|
||||||
|
# To be extra safe, we by default ignore any files with `.env` extension in them.
|
||||||
|
# If this is too agressive for you, consider allowing specific files with `!` operator,
|
||||||
|
# or modify/delete these two lines.
|
||||||
|
*.env
|
||||||
|
*.env.*
|
||||||
|
-
|
||||||
|
+# These two we added only because dotenv-vault keeps adding them if it doesn't find them,
|
||||||
|
+# even though we don't need them. Remove them once dotenv-vault stops doing that.
|
||||||
|
+.env*
|
||||||
|
+.flaskenv*
|
||||||
|
# We don't want to ignore .env example files.
|
||||||
|
!*.env.*.example
|
||||||
|
!*.env.example
|
||||||
|
+# We don't want to ignore .env.client as it doesn't have any secrets.
|
||||||
|
+!.env.client
|
||||||
|
+# These are config files for dotenv-vault, so we don't want to ignore them.
|
||||||
|
+!.env.project
|
||||||
|
+!.env.vault
|
||||||
|
+# .env.me is used by dotenv-vault as a personal credential, so we explicitly ignore it.
|
||||||
|
+.env.me
|
||||||
30
opensaas-sh/app_diff/README.md.diff
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
--- template/app/README.md
|
||||||
|
+++ opensaas-sh/app/README.md
|
||||||
|
@@ -1,12 +1,25 @@
|
||||||
|
-# <YOUR_APP_NAME>
|
||||||
|
+# opensaas.sh (demo) app
|
||||||
|
|
||||||
|
-Built with [Wasp](https://wasp-lang.dev), based on the [Open Saas](https://opensaas.sh) template.
|
||||||
|
+This is a Wasp app based on Open Saas template with minimal modifications that make it into a demo app that showcases Open Saas's abilities.
|
||||||
|
+
|
||||||
|
+It is deployed to https://opensaas.sh and serves both as a landing page for Open Saas and as a demo app.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
+### .env files
|
||||||
|
+`.env.client` file is versioned, but `.env.server` file you have to obtain by running `npm run env:pull`, since it has secrets in it.
|
||||||
|
+This will generate `.env.server` based on the `.env.vault`.
|
||||||
|
+We are using https://vault.dotenv.org to power this and have an account/organization up there.
|
||||||
|
+If you modify .env.server and want to persist the changes (for yourself and for the other team members), do `npm run env:push`.
|
||||||
|
+
|
||||||
|
### Running locally
|
||||||
|
- Make sure you have the `.env.client` and `.env.server` files with correct dev values in the root of the project.
|
||||||
|
- Run the database with `wasp start db` and leave it running.
|
||||||
|
- Run `wasp start` and leave it running.
|
||||||
|
- [OPTIONAL]: If this is the first time starting the app, or you've just made changes to your entities/prisma schema, also run `wasp db migrate-dev`.
|
||||||
|
|
||||||
|
+## Deployment
|
||||||
|
+
|
||||||
|
+This app is deployed to fly.io, Wasp org, via `wasp deploy`.
|
||||||
|
+
|
||||||
|
+You can run `npm run deploy` to deploy it via `wasp deploy` with required client side env vars correctly set.
|
||||||
3
opensaas-sh/app_diff/deletions
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
public/public-banner.png
|
||||||
|
src/client/static/avatar-placeholder.png
|
||||||
|
src/client/static/open-saas-banner.png
|
||||||
26
opensaas-sh/app_diff/fly-client.toml.diff
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
--- template/app/fly-client.toml
|
||||||
|
+++ opensaas-sh/app/fly-client.toml
|
||||||
|
@@ -0,0 +1,22 @@
|
||||||
|
+# fly.toml app configuration file generated for open-saas-wasp-sh-client on 2023-12-04T12:34:07+01:00
|
||||||
|
+#
|
||||||
|
+# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||||
|
+#
|
||||||
|
+
|
||||||
|
+app = "open-saas-wasp-sh-client"
|
||||||
|
+primary_region = "ams"
|
||||||
|
+
|
||||||
|
+[build]
|
||||||
|
+
|
||||||
|
+[http_service]
|
||||||
|
+ internal_port = 8043
|
||||||
|
+ force_https = true
|
||||||
|
+ auto_stop_machines = true
|
||||||
|
+ auto_start_machines = true
|
||||||
|
+ min_machines_running = 0
|
||||||
|
+ processes = ["app"]
|
||||||
|
+
|
||||||
|
+[[vm]]
|
||||||
|
+ cpu_kind = "shared"
|
||||||
|
+ cpus = 1
|
||||||
|
+ memory_mb = 1024
|
||||||
|
\ No newline at end of file
|
||||||
26
opensaas-sh/app_diff/fly-server.toml.diff
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
--- template/app/fly-server.toml
|
||||||
|
+++ opensaas-sh/app/fly-server.toml
|
||||||
|
@@ -0,0 +1,22 @@
|
||||||
|
+# fly.toml app configuration file generated for open-saas-wasp-sh-server on 2023-12-04T12:33:59+01:00
|
||||||
|
+#
|
||||||
|
+# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||||
|
+#
|
||||||
|
+
|
||||||
|
+app = "open-saas-wasp-sh-server"
|
||||||
|
+primary_region = "ams"
|
||||||
|
+
|
||||||
|
+[build]
|
||||||
|
+
|
||||||
|
+[http_service]
|
||||||
|
+ internal_port = 8080
|
||||||
|
+ force_https = true
|
||||||
|
+ auto_stop_machines = true
|
||||||
|
+ auto_start_machines = true
|
||||||
|
+ min_machines_running = 1
|
||||||
|
+ processes = ["app"]
|
||||||
|
+
|
||||||
|
+[[vm]]
|
||||||
|
+ cpu_kind = "shared"
|
||||||
|
+ cpus = 1
|
||||||
|
+ memory_mb = 1024
|
||||||
|
\ No newline at end of file
|
||||||
99
opensaas-sh/app_diff/main.wasp.diff
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
--- template/app/main.wasp
|
||||||
|
+++ opensaas-sh/app/main.wasp
|
||||||
|
@@ -3,24 +3,27 @@
|
||||||
|
version: "^0.13.2"
|
||||||
|
},
|
||||||
|
|
||||||
|
- title: "My Open SaaS App",
|
||||||
|
+ title: "Open SaaS",
|
||||||
|
|
||||||
|
head: [
|
||||||
|
"<meta property='og:type' content='website' />",
|
||||||
|
- "<meta property='og:title' content='My Open SaaS App' />",
|
||||||
|
+ "<meta property='og:title' content='Open SaaS' />",
|
||||||
|
"<meta property='og:url' content='https://opensaas.sh' />",
|
||||||
|
- "<meta property='og:description' content='I made a SaaS App. Buy my stuff.' />",
|
||||||
|
- "<meta property='og:image' content='https://opensaas.sh/public-banner.png' />",
|
||||||
|
- "<meta name='twitter:image' content='https://opensaas.sh/public-banner.png' />",
|
||||||
|
+ "<meta property='og:description' content='Free, open-source SaaS boilerplate starter for React & NodeJS.' />",
|
||||||
|
+ "<meta property='og:image' content='https://opensaas.sh/banner.png' />",
|
||||||
|
+
|
||||||
|
+ "<meta name=\"twitter:title\" content=\"Open SaaS\" />",
|
||||||
|
+ "<meta name=\"twitter:text:title\" content=\"Open SaaS\" />",
|
||||||
|
+ "<meta name='twitter:image' content='https://opensaas.sh/banner.png' />",
|
||||||
|
+ "<meta name=\"twitter:image:alt\" content=\"Open SaaS\" />",
|
||||||
|
"<meta name='twitter:image:width' content='800' />",
|
||||||
|
"<meta name='twitter:image:height' content='400' />",
|
||||||
|
"<meta name='twitter:card' content='summary_large_image' />",
|
||||||
|
- // TODO: You can put your analytics scripts below (https://docs.opensaas.sh/guides/analytics/):
|
||||||
|
- // If you are going with Plausible:
|
||||||
|
- "<script defer data-domain='<your-site-id>' src='https://plausible.io/js/script.js'></script>", // for production
|
||||||
|
- "<script defer data-domain='<your-site-id>' src='https://plausible.io/js/script.local.js'></script>", // for development
|
||||||
|
- // If you are going with Google Analytics:
|
||||||
|
- "<!-- Google tag (gtag.js) --><script>...</script>" // for both production and development
|
||||||
|
+
|
||||||
|
+ "<script defer data-domain='opensaas.sh' src='https://plausible.apps.twoducks.dev/js/script.js'></script>",
|
||||||
|
+ "<script defer data-domain='opensaas.sh' src='https://plausible.apps.twoducks.dev/js/script.local.js'></script>",
|
||||||
|
+
|
||||||
|
+ "<!-- Google tag (gtag.js) --><script async src='https://www.googletagmanager.com/gtag/js?id=G-H3LSJCK95H'></script><script>window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', 'G-H3LSJCK95H');</script>"
|
||||||
|
],
|
||||||
|
|
||||||
|
// 🔐 Auth out of the box! https://wasp-lang.dev/docs/auth/overview
|
||||||
|
@@ -32,7 +35,7 @@
|
||||||
|
email: {
|
||||||
|
fromField: {
|
||||||
|
name: "Open SaaS App",
|
||||||
|
- email: "me@example.com"
|
||||||
|
+ email: "vince@wasp-lang.dev"
|
||||||
|
},
|
||||||
|
emailVerification: {
|
||||||
|
clientRoute: EmailVerificationRoute,
|
||||||
|
@@ -44,16 +47,14 @@
|
||||||
|
},
|
||||||
|
userSignupFields: import { getEmailUserFields } from "@src/server/auth/setUsername.js",
|
||||||
|
},
|
||||||
|
- // Uncomment to enable Google Auth (check https://wasp-lang.dev/docs/auth/social-auth/google for setup instructions):
|
||||||
|
- // google: { // Guide for setting up Auth via Google
|
||||||
|
- // userSignupFields: import { getGoogleUserFields } from "@src/server/auth/setUsername.js",
|
||||||
|
- // configFn: import { getGoogleAuthConfig } from "@src/server/auth/setUsername.js",
|
||||||
|
- // },
|
||||||
|
- // Uncomment to enable GitHub Auth (check https://wasp-lang.dev/docs/auth/social-auth/github for setup instructions):
|
||||||
|
- // gitHub: {
|
||||||
|
- // userSignupFields: import { getGitHubUserFields } from "@src/server/auth/setUsername.js",
|
||||||
|
- // configFn: import { getGitHubAuthConfig } from "@src/server/auth/setUsername.js",
|
||||||
|
- // },
|
||||||
|
+ google: {
|
||||||
|
+ userSignupFields: import { getGoogleUserFields } from "@src/server/auth/setUsername",
|
||||||
|
+ configFn: import { getGoogleAuthConfig } from "@src/server/auth/setUsername",
|
||||||
|
+ },
|
||||||
|
+ gitHub: {
|
||||||
|
+ userSignupFields: import { getGitHubUserFields } from "@src/server/auth/setUsername",
|
||||||
|
+ configFn: import { getGitHubAuthConfig } from "@src/server/auth/setUsername",
|
||||||
|
+ },
|
||||||
|
},
|
||||||
|
onAuthFailedRedirectTo: "/login",
|
||||||
|
onAuthSucceededRedirectTo: "/demo-app",
|
||||||
|
@@ -76,11 +77,11 @@
|
||||||
|
// NOTE: "Dummy" provider is just for local development purposes.
|
||||||
|
// Make sure to check the server logs for the email confirmation url (it will not be sent to an address)!
|
||||||
|
// Once you are ready for production, switch to e.g. "SendGrid" or "MailGun" providers. Check out https://docs.opensaas.sh/guides/email-sending/ .
|
||||||
|
- provider: Dummy,
|
||||||
|
+ provider: SendGrid,
|
||||||
|
defaultFrom: {
|
||||||
|
name: "Open SaaS App",
|
||||||
|
// When using a real provider, e.g. SendGrid, you must use the same email address that you configured your account to send out emails with!
|
||||||
|
- email: "me@example.com"
|
||||||
|
+ email: "vince@wasp-lang.dev"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@@ -97,6 +98,9 @@
|
||||||
|
username String? @unique
|
||||||
|
lastActiveTimestamp DateTime @default(now())
|
||||||
|
isAdmin Boolean @default(false)
|
||||||
|
+ // isMockUser is an extra property for the demo app ensuring that all users can access
|
||||||
|
+ // the admin dashboard but won't be able to see the other users' data, only mock user data.
|
||||||
|
+ isMockUser Boolean @default(false)
|
||||||
|
|
||||||
|
stripeId String?
|
||||||
|
checkoutSessionId String?
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
--- template/app/migrations/20231213174854_init/migration.sql
|
||||||
|
+++ opensaas-sh/app/migrations/20231213174854_init/migration.sql
|
||||||
|
@@ -0,0 +1,118 @@
|
||||||
|
+-- CreateTable
|
||||||
|
+CREATE TABLE "User" (
|
||||||
|
+ "id" SERIAL NOT NULL,
|
||||||
|
+ "email" TEXT,
|
||||||
|
+ "username" TEXT,
|
||||||
|
+ "password" TEXT,
|
||||||
|
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
+ "lastActiveTimestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
+ "isEmailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
+ "isMockUser" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
+ "isAdmin" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
+ "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 "User_username_key" ON "User"("username");
|
||||||
|
+
|
||||||
|
+-- 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;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
--- template/app/migrations/20240105224550_tasks/migration.sql
|
||||||
|
+++ opensaas-sh/app/migrations/20240105224550_tasks/migration.sql
|
||||||
|
@@ -0,0 +1,14 @@
|
||||||
|
+-- CreateTable
|
||||||
|
+CREATE TABLE "Task" (
|
||||||
|
+ "id" TEXT NOT NULL,
|
||||||
|
+ "description" TEXT NOT NULL,
|
||||||
|
+ "time" TEXT NOT NULL DEFAULT '1',
|
||||||
|
+ "isDone" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
+ "userId" INTEGER NOT NULL,
|
||||||
|
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
+
|
||||||
|
+ CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
|
||||||
|
+);
|
||||||
|
+
|
||||||
|
+-- AddForeignKey
|
||||||
|
+ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
--- template/app/migrations/20240207164719_files/migration.sql
|
||||||
|
+++ opensaas-sh/app/migrations/20240207164719_files/migration.sql
|
||||||
|
@@ -0,0 +1,15 @@
|
||||||
|
+-- CreateTable
|
||||||
|
+CREATE TABLE "File" (
|
||||||
|
+ "id" TEXT NOT NULL,
|
||||||
|
+ "name" TEXT NOT NULL,
|
||||||
|
+ "type" TEXT NOT NULL,
|
||||||
|
+ "key" TEXT NOT NULL,
|
||||||
|
+ "uploadUrl" TEXT NOT NULL,
|
||||||
|
+ "userId" INTEGER NOT NULL,
|
||||||
|
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
+
|
||||||
|
+ CONSTRAINT "File_pkey" PRIMARY KEY ("id")
|
||||||
|
+);
|
||||||
|
+
|
||||||
|
+-- AddForeignKey
|
||||||
|
+ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
--- template/app/migrations/20240226123357_new_auth_structure/migration.sql
|
||||||
|
+++ opensaas-sh/app/migrations/20240226123357_new_auth_structure/migration.sql
|
||||||
|
@@ -0,0 +1,44 @@
|
||||||
|
+-- CreateTable
|
||||||
|
+CREATE TABLE "Auth" (
|
||||||
|
+ "id" TEXT NOT NULL,
|
||||||
|
+ "userId" INTEGER,
|
||||||
|
+
|
||||||
|
+ CONSTRAINT "Auth_pkey" PRIMARY KEY ("id")
|
||||||
|
+);
|
||||||
|
+
|
||||||
|
+-- CreateTable
|
||||||
|
+CREATE TABLE "AuthIdentity" (
|
||||||
|
+ "providerName" TEXT NOT NULL,
|
||||||
|
+ "providerUserId" TEXT NOT NULL,
|
||||||
|
+ "providerData" TEXT NOT NULL DEFAULT '{}',
|
||||||
|
+ "authId" TEXT NOT NULL,
|
||||||
|
+
|
||||||
|
+ CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("providerName","providerUserId")
|
||||||
|
+);
|
||||||
|
+
|
||||||
|
+-- CreateTable
|
||||||
|
+CREATE TABLE "Session" (
|
||||||
|
+ "id" TEXT NOT NULL,
|
||||||
|
+ "expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
+ "userId" TEXT NOT NULL,
|
||||||
|
+
|
||||||
|
+ CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||||
|
+);
|
||||||
|
+
|
||||||
|
+-- CreateIndex
|
||||||
|
+CREATE UNIQUE INDEX "Auth_userId_key" ON "Auth"("userId");
|
||||||
|
+
|
||||||
|
+-- CreateIndex
|
||||||
|
+CREATE UNIQUE INDEX "Session_id_key" ON "Session"("id");
|
||||||
|
+
|
||||||
|
+-- CreateIndex
|
||||||
|
+CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
||||||
|
+
|
||||||
|
+-- AddForeignKey
|
||||||
|
+ALTER TABLE "Auth" ADD CONSTRAINT "Auth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
+
|
||||||
|
+-- AddForeignKey
|
||||||
|
+ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_authId_fkey" FOREIGN KEY ("authId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
+
|
||||||
|
+-- AddForeignKey
|
||||||
|
+ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Auth"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
--- template/app/migrations/20240226130234_remove_old_auth_structure/migration.sql
|
||||||
|
+++ opensaas-sh/app/migrations/20240226130234_remove_old_auth_structure/migration.sql
|
||||||
|
@@ -0,0 +1,21 @@
|
||||||
|
+/*
|
||||||
|
+ Warnings:
|
||||||
|
+
|
||||||
|
+ - You are about to drop the column `emailVerificationSentAt` on the `User` table. All the data in the column will be lost.
|
||||||
|
+ - You are about to drop the column `isEmailVerified` on the `User` table. All the data in the column will be lost.
|
||||||
|
+ - 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 `passwordResetSentAt` on the `User` table. All the data in the column will be lost.
|
||||||
|
+ - You are about to drop the `SocialLogin` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
+
|
||||||
|
+*/
|
||||||
|
+-- DropForeignKey
|
||||||
|
+ALTER TABLE "SocialLogin" DROP CONSTRAINT "SocialLogin_userId_fkey";
|
||||||
|
+
|
||||||
|
+-- AlterTable
|
||||||
|
+ALTER TABLE "User" DROP COLUMN "emailVerificationSentAt",
|
||||||
|
+DROP COLUMN "isEmailVerified",
|
||||||
|
+DROP COLUMN "password",
|
||||||
|
+DROP COLUMN "passwordResetSentAt";
|
||||||
|
+
|
||||||
|
+-- DropTable
|
||||||
|
+DROP TABLE "SocialLogin";
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
--- template/app/migrations/20240226145340_remove_unique_email_username/migration.sql
|
||||||
|
+++ opensaas-sh/app/migrations/20240226145340_remove_unique_email_username/migration.sql
|
||||||
|
@@ -0,0 +1,5 @@
|
||||||
|
+-- DropIndex
|
||||||
|
+DROP INDEX "User_email_key";
|
||||||
|
+
|
||||||
|
+-- DropIndex
|
||||||
|
+DROP INDEX "User_username_key";
|
||||||
7
opensaas-sh/app_diff/migrations/migration_lock.toml.diff
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
--- template/app/migrations/migration_lock.toml
|
||||||
|
+++ opensaas-sh/app/migrations/migration_lock.toml
|
||||||
|
@@ -0,0 +1,3 @@
|
||||||
|
+# Please do not edit this file manually
|
||||||
|
+# It should be added in your version-control system (i.e. Git)
|
||||||
|
+provider = "postgresql"
|
||||||
|
\ No newline at end of file
|
||||||
13
opensaas-sh/app_diff/package.json.diff
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
--- template/app/package.json
|
||||||
|
+++ opensaas-sh/app/package.json
|
||||||
|
@@ -1,5 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "opensaas",
|
||||||
|
+ "scripts": {
|
||||||
|
+ "env:pull": "npx dotenv-vault@latest pull development .env.server",
|
||||||
|
+ "env:push": "npx dotenv-vault@latest push development .env.server",
|
||||||
|
+ "deploy": "REACT_APP_STRIPE_CUSTOMER_PORTAL=https://billing.stripe.com/p/login/test_8wM8x17JN7DT4zC000 REACT_APP_GOOGLE_ANALYTICS_ID=G-H3LSJCK95H wasp deploy"
|
||||||
|
+ },
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.523.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.523.0",
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
--- template/app/src/client/admin/components/UsersTable.tsx
|
||||||
|
+++ opensaas-sh/app/src/client/admin/components/UsersTable.tsx
|
||||||
|
@@ -11,6 +11,7 @@
|
||||||
|
const [email, setEmail] = useState<string | undefined>(undefined);
|
||||||
|
const [isAdminFilter, setIsAdminFilter] = useState<boolean | undefined>(undefined);
|
||||||
|
const [statusOptions, setStatusOptions] = useState<SubscriptionStatusOptions[]>([]);
|
||||||
|
+ const [isDemoInfoVisible, setIsDemoInfoVisible] = useState(false);
|
||||||
|
const { data, isLoading, error } = useQuery(getPaginatedUsers, {
|
||||||
|
skip,
|
||||||
|
emailContains: email,
|
||||||
|
@@ -26,8 +27,43 @@
|
||||||
|
setskip((page - 1) * 10);
|
||||||
|
}, [page]);
|
||||||
|
|
||||||
|
+
|
||||||
|
+ useEffect(() => {
|
||||||
|
+ try {
|
||||||
|
+ if (localStorage.getItem('isDemoInfoVisible') === 'false') {
|
||||||
|
+ // do nothing
|
||||||
|
+ } else {
|
||||||
|
+ setIsDemoInfoVisible(true);
|
||||||
|
+ }
|
||||||
|
+ } catch (error) {
|
||||||
|
+ console.error(error);
|
||||||
|
+ }
|
||||||
|
+ }, []);
|
||||||
|
+
|
||||||
|
+ const handleDemoInfoClose = () => {
|
||||||
|
+ try {
|
||||||
|
+ localStorage.setItem('isDemoInfoVisible', 'false');
|
||||||
|
+ setIsDemoInfoVisible(false);
|
||||||
|
+ } catch (error) {
|
||||||
|
+ console.error(error);
|
||||||
|
+ }
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
+ {/* Floating Demo Announcement */}
|
||||||
|
+ {isDemoInfoVisible && (
|
||||||
|
+ <div className='fixed z-999 bottom-0 mb-2 left-1/2 -translate-x-1/2 lg:mb-4 bg-gray-700 rounded-full px-3.5 py-2 text-sm text-white duration-300 ease-in-out hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-600'>
|
||||||
|
+ <div className='px-4 flex flex-row gap-2 items-center my-1'>
|
||||||
|
+ <span className='text-gray-100'>
|
||||||
|
+ You are viewing mock user data only ;)
|
||||||
|
+ </span>
|
||||||
|
+ <button className=' pl-2.5 text-gray-400 text-xl font-bold' onClick={() => handleDemoInfoClose()}>
|
||||||
|
+ X
|
||||||
|
+ </button>
|
||||||
|
+ </div>
|
||||||
|
+ </div>
|
||||||
|
+ )}
|
||||||
|
<div className='rounded-sm border border-stroke bg-white shadow-default dark:border-strokedark dark:bg-boxdark'>
|
||||||
|
<div className='flex-col flex items-start justify-between p-6 gap-3 w-full bg-gray-100/40 dark:bg-gray-700/50'>
|
||||||
|
<span className='text-sm font-medium'>Filters:</span>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
--- template/app/src/client/admin/pages/DashboardPage.tsx
|
||||||
|
+++ opensaas-sh/app/src/client/admin/pages/DashboardPage.tsx
|
||||||
|
@@ -1,5 +1,7 @@
|
||||||
|
import { type User } from 'wasp/entities';
|
||||||
|
import { useQuery, getDailyStats } from 'wasp/client/operations';
|
||||||
|
+import { Link } from "wasp/client/router";
|
||||||
|
+import { useState, useEffect } from 'react';
|
||||||
|
import TotalSignupsCard from '../components/TotalSignupsCard';
|
||||||
|
import TotalPageViewsCard from '../components/TotalPaidViewsCard';
|
||||||
|
import TotalPayingUsersCard from '../components/TotalPayingUsersCard';
|
||||||
|
@@ -10,6 +12,8 @@
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
const Dashboard = ({ user }: { user: User }) => {
|
||||||
|
+ const [isDemoInfoVisible, setIsDemoInfoVisible] = useState(false);
|
||||||
|
+
|
||||||
|
const history = useHistory();
|
||||||
|
if (!user.isAdmin) {
|
||||||
|
history.push('/');
|
||||||
|
@@ -17,8 +21,41 @@
|
||||||
|
|
||||||
|
const { data: stats, isLoading, error } = useQuery(getDailyStats);
|
||||||
|
|
||||||
|
+
|
||||||
|
+ useEffect(() => {
|
||||||
|
+ try {
|
||||||
|
+ if (localStorage.getItem('isStripeDemoInfoVisible') === 'false') {
|
||||||
|
+ // do nothing
|
||||||
|
+ } else {
|
||||||
|
+ setIsDemoInfoVisible(true);
|
||||||
|
+ }
|
||||||
|
+ } catch (error) {
|
||||||
|
+ console.error(error);
|
||||||
|
+ }
|
||||||
|
+ }, []);
|
||||||
|
+
|
||||||
|
+ const handleDemoInfoClose = () => {
|
||||||
|
+ try {
|
||||||
|
+ localStorage.setItem('isStripeDemoInfoVisible', 'false');
|
||||||
|
+ setIsDemoInfoVisible(false);
|
||||||
|
+ } catch (error) {
|
||||||
|
+ console.error(error);
|
||||||
|
+ }
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
return (
|
||||||
|
<DefaultLayout>
|
||||||
|
+ {/* Floating Demo Announcement */}
|
||||||
|
+ {isDemoInfoVisible && (
|
||||||
|
+ <div className='fixed z-999 bottom-0 mb-2 left-1/2 -translate-x-1/2 lg:mb-4 bg-gray-700 rounded-full px-3.5 py-2 text-sm text-white duration-300 ease-in-out hover:bg-gray-800 focus-visible:outline focus-visible:outline-2 focus-visible:outline-indigo-600'>
|
||||||
|
+ <div className='px-4 flex flex-row gap-2 items-center my-1'>
|
||||||
|
+ <span className='text-gray-100 text-center'>This is actual data from Stripe test purchases. <br/> Try out purchasing a <Link to='/pricing' className="underline text-yellow-400">test product</Link>!</span>
|
||||||
|
+ <button className=' pl-2.5 text-gray-400 text-xl font-bold' onClick={() => handleDemoInfoClose()}>
|
||||||
|
+ X
|
||||||
|
+ </button>
|
||||||
|
+ </div>
|
||||||
|
+ </div>
|
||||||
|
+ )}
|
||||||
|
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 xl:grid-cols-4 2xl:gap-7.5'>
|
||||||
|
<TotalPageViewsCard
|
||||||
|
totalPageViews={stats?.dailyStats.totalViews}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
--- template/app/src/client/landing-page/Announcement.tsx
|
||||||
|
+++ opensaas-sh/app/src/client/landing-page/Announcement.tsx
|
||||||
|
@@ -0,0 +1,57 @@
|
||||||
|
+import { useState, useEffect } from 'react';
|
||||||
|
+import { AiFillGithub } from 'react-icons/ai';
|
||||||
|
+
|
||||||
|
+type TimeLeft = { hours: string; minutes: string; seconds: string };
|
||||||
|
+
|
||||||
|
+export default function Announcement() {
|
||||||
|
+ const [timeLeft, setTimeLeft] = useState<TimeLeft | undefined>(
|
||||||
|
+ calculateTimeLeft()
|
||||||
|
+ );
|
||||||
|
+
|
||||||
|
+ function calculateTimeLeft() {
|
||||||
|
+ const targetDate = '2024-01-30T08:01:00Z';
|
||||||
|
+ let diff = new Date(targetDate).getTime() - new Date().getTime();
|
||||||
|
+ let timeLeft: TimeLeft | undefined;
|
||||||
|
+
|
||||||
|
+ if (diff > 0) {
|
||||||
|
+ timeLeft = {
|
||||||
|
+ hours: Math.floor((diff / (1000 * 60 * 60)) % 24).toString(),
|
||||||
|
+ minutes: Math.floor((diff / 1000 / 60) % 60).toString(),
|
||||||
|
+ // make sure seconds are always displayed as two digits, e.g. '02'
|
||||||
|
+ seconds: Math.floor((diff / 1000) % 60).toString(),
|
||||||
|
+ };
|
||||||
|
+ }
|
||||||
|
+ if (!!timeLeft) {
|
||||||
|
+ if (timeLeft.seconds.length === 1) {
|
||||||
|
+ timeLeft.seconds = '0' + timeLeft.seconds;
|
||||||
|
+ }
|
||||||
|
+ if (timeLeft.minutes.length === 1) {
|
||||||
|
+ timeLeft.minutes = '0' + timeLeft.minutes;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ return timeLeft;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ useEffect(() => {
|
||||||
|
+ const timer = setTimeout(() => {
|
||||||
|
+ setTimeLeft(calculateTimeLeft());
|
||||||
|
+ }, 1000);
|
||||||
|
+ return () => clearTimeout(timer);
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ return (
|
||||||
|
+ <div className='flex items-center justify-center gap-3 border-b border-gray-300 border-dashed text-center py-6 text-sm'>
|
||||||
|
+ Open SaaS trending on{' '}
|
||||||
|
+ <a
|
||||||
|
+ href='https://github.com/trending#:~:text=wasp%2Dlang%20/%20open%2Dsaas'
|
||||||
|
+ target='_blank'
|
||||||
|
+ rel='noopener noreferrer'
|
||||||
|
+ className='flex items-center justify-center gap-2 bg-purple-200 hover:bg-purple-300 text-gray-900 border-b border-1 border-purple-300 hover:border-purple-400 py-1 px-3 -my-1 rounded-full shadow-lg hover:shadow-md duration-200 ease-in-out tracking-wider'
|
||||||
|
+ >
|
||||||
|
+ <span>GitHub</span>
|
||||||
|
+ <AiFillGithub />
|
||||||
|
+ </a>
|
||||||
|
+ 📈
|
||||||
|
+ </div>
|
||||||
|
+ );
|
||||||
|
+}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
--- template/app/src/client/landing-page/contentSections.ts
|
||||||
|
+++ opensaas-sh/app/src/client/landing-page/contentSections.ts
|
||||||
|
@@ -1,74 +1,150 @@
|
||||||
|
-import { DOCS_URL, BLOG_URL } from '../../shared/constants';
|
||||||
|
-import daBoiAvatar from '../static/da-boi.png';
|
||||||
|
-import avatarPlaceholder from '../static/avatar-placeholder.png';
|
||||||
|
import { routes } from 'wasp/client/router';
|
||||||
|
+import { DOCS_URL, BLOG_URL, GITHUB_URL } from '../../shared/constants';
|
||||||
|
+import daBoiAavatar from '../static/da-boi.png';
|
||||||
|
|
||||||
|
export const navigation = [
|
||||||
|
{ name: 'Features', href: '#features' },
|
||||||
|
- { name: 'Pricing', href: routes.PricingPageRoute.build() },
|
||||||
|
{ name: 'Documentation', href: DOCS_URL },
|
||||||
|
{ name: 'Blog', href: BLOG_URL },
|
||||||
|
];
|
||||||
|
export const features = [
|
||||||
|
{
|
||||||
|
- name: 'Cool Feature #1',
|
||||||
|
- description: 'Describe your cool feature here.',
|
||||||
|
+ name: 'Open-Source Philosophy',
|
||||||
|
+ description:
|
||||||
|
+ 'The repo and framework are 100% open-source, and so are the services wherever possible. Still missing something? Contribute!',
|
||||||
|
icon: '🤝',
|
||||||
|
href: DOCS_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
- name: 'Cool Feature #2',
|
||||||
|
- description: 'Describe your cool feature here.',
|
||||||
|
+ name: 'DIY Auth, Done For You',
|
||||||
|
+ description: 'Pre-configured full-stack Auth that you own. No 3rd-party services or hidden fees.',
|
||||||
|
icon: '🔐',
|
||||||
|
- href: DOCS_URL,
|
||||||
|
+ href: DOCS_URL + '/guides/authentication/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
- name: 'Cool Feature #3',
|
||||||
|
- description: 'Describe your cool feature here.',
|
||||||
|
+ name: 'Full-stack Type Safety',
|
||||||
|
+ description:
|
||||||
|
+ 'Full support for TypeScript with auto-generated types that span the whole stack. Nothing to configure!',
|
||||||
|
icon: '🥞',
|
||||||
|
href: DOCS_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
- name: 'Cool Feature #4',
|
||||||
|
- description: 'Describe your cool feature here.',
|
||||||
|
+ name: 'Stripe Integration',
|
||||||
|
+ description:
|
||||||
|
+ "No SaaS is complete without payments. That's why payments and the necessary webhooks are built-in.",
|
||||||
|
icon: '💸',
|
||||||
|
+ href: DOCS_URL + '/guides/stripe-integration/',
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ name: 'Admin Dashboard',
|
||||||
|
+ description: 'Graphs! Tables! Analytics w/ Plausible or Google! All in one place. Ooooooooooh.',
|
||||||
|
+ icon: '📈',
|
||||||
|
+ href: DOCS_URL + '/general/admin-dashboard/',
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ name: 'Email Sending',
|
||||||
|
+ description:
|
||||||
|
+ 'Email sending built-in. Combine it with the cron jobs feature to easily send emails to your customers.',
|
||||||
|
+ icon: '📧',
|
||||||
|
+ href: DOCS_URL + '/guides/email-sending/',
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ name: 'OpenAI API Implemented',
|
||||||
|
+ description: 'Have a sweet AI-powered app concept? Get your idea shipped to potential customers in days!',
|
||||||
|
+ icon: '🤖',
|
||||||
|
+ href: DOCS_URL,
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ name: 'File Uploads with AWS',
|
||||||
|
+ description: 'File upload examples with AWS S3 presigned URLs are included and fully documented!',
|
||||||
|
+ icon: '📁',
|
||||||
|
+ href: DOCS_URL + '/guides/file-uploading/',
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ name: 'Deploy Anywhere. Easily.',
|
||||||
|
+ description:
|
||||||
|
+ 'No vendor lock-in because you own all your code. Deploy yourself, or let Wasp deploy it for you with a single command.',
|
||||||
|
+ icon: '🚀 ',
|
||||||
|
+ href: DOCS_URL + '/guides/deploying/',
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ name: 'Blog w/ Astro',
|
||||||
|
+ description:
|
||||||
|
+ 'Built-in blog with the Astro framework. Write your posts in Markdown, and watch your SEO performance take off.',
|
||||||
|
+ icon: '📝',
|
||||||
|
+ href: DOCS_URL + '/start/guided-tour/',
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ name: 'Complete Documentation & Support',
|
||||||
|
+ description: "We don't leave you hanging. We have detailed docs and a Discord community to help!",
|
||||||
|
+ icon: '🫂',
|
||||||
|
href: DOCS_URL,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const testimonials = [
|
||||||
|
- {
|
||||||
|
- name: 'Da Boi',
|
||||||
|
- role: 'Wasp Mascot',
|
||||||
|
- avatarSrc: daBoiAvatar,
|
||||||
|
- socialUrl: 'https://twitter.com/wasplang',
|
||||||
|
- quote: "I don't even know how to code. I'm just a plushie.",
|
||||||
|
- },
|
||||||
|
- {
|
||||||
|
- name: 'Mr. Foobar',
|
||||||
|
- role: 'Founder @ Cool Startup',
|
||||||
|
- avatarSrc: avatarPlaceholder,
|
||||||
|
- socialUrl: '',
|
||||||
|
- quote: 'This product makes me cooler than I already am.',
|
||||||
|
- },
|
||||||
|
- {
|
||||||
|
- name: 'Jamie',
|
||||||
|
- role: 'Happy Customer',
|
||||||
|
- avatarSrc: avatarPlaceholder,
|
||||||
|
- socialUrl: '#',
|
||||||
|
- quote: 'My cats love it!',
|
||||||
|
+ // {
|
||||||
|
+ // name: 'Jason Warner',
|
||||||
|
+ // role: 'former CTO @ GitHub',
|
||||||
|
+ // avatarSrc: 'https://pbs.twimg.com/profile_images/1538765024021258240/qXJBzw6U_400x400.jpg',
|
||||||
|
+ // socialUrl: 'https://twitter.com/jasoncwarner',
|
||||||
|
+ // quote:
|
||||||
|
+ // "I've actually had a bunch of fun with [Wasp]... I loved Batman.js back in the day and getting some of those vibes.",
|
||||||
|
+ // },
|
||||||
|
+ {
|
||||||
|
+ name: 'Max Khamrovskyi',
|
||||||
|
+ role: 'Senior Eng @ Red Hat',
|
||||||
|
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1719397191205179392/V_QrGPSO_400x400.jpg',
|
||||||
|
+ socialUrl: 'https://twitter.com/maksim36ua',
|
||||||
|
+ quote: 'I used Wasp to build and sell my AI-augmented SaaS app for marketplace vendors within two months!',
|
||||||
|
+ },
|
||||||
|
+ // {
|
||||||
|
+ // name: 'Da Boi',
|
||||||
|
+ // role: 'Wasp Mascot',
|
||||||
|
+ // avatarSrc: daBoiAavatar,
|
||||||
|
+ // socialUrl: 'https://twitter.com/wasplang',
|
||||||
|
+ // quote: "I don't even know how to code. I'm just a plushie.",
|
||||||
|
+ // },
|
||||||
|
+ {
|
||||||
|
+ name: 'Tim Skaggs',
|
||||||
|
+ role: 'Founder @ Antler US',
|
||||||
|
+ avatarSrc: 'https://pbs.twimg.com/profile_images/1486119226771480577/VptdEo6A_400x400.png',
|
||||||
|
+ socialUrl: 'https://twitter.com/tskaggs',
|
||||||
|
+ quote: 'Nearly done with a MVP in 3 days of part-time work... and deployed on Fly.io in 10 minutes.',
|
||||||
|
+ },
|
||||||
|
+ // {
|
||||||
|
+ // name: 'Fecony',
|
||||||
|
+ // role: 'Wasp Expert',
|
||||||
|
+ // avatarSrc: 'https://pbs.twimg.com/profile_images/1560677466749943810/QIFuQMqU_400x400.jpg',
|
||||||
|
+ // socialUrl: 'https://twitter.com/webrickony',
|
||||||
|
+ // quote: 'My cats love it!',
|
||||||
|
+ // },
|
||||||
|
+ {
|
||||||
|
+ name: 'Jonathan Cocharan',
|
||||||
|
+ role: 'Entrepreneur',
|
||||||
|
+ avatarSrc: 'https://pbs.twimg.com/profile_images/926142421653753857/o6Hmcbr7_400x400.jpg',
|
||||||
|
+ socialUrl: 'https://twitter.com/jonathancocharan',
|
||||||
|
+ quote:
|
||||||
|
+ 'In just 6 nights... my SaaS app is live 🎉! Huge thanks to the amazing @wasplang community 🙌 for their guidance along the way. These tools are incredibly efficient 🤯!',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const faqs = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
- question: 'Whats the meaning of life?',
|
||||||
|
- answer: '42.',
|
||||||
|
- href: 'https://en.wikipedia.org/wiki/42_(number)',
|
||||||
|
+ question: 'Why is this SaaS Template free and open-source?',
|
||||||
|
+ answer:
|
||||||
|
+ 'We believe the best product is made when the community puts their heads together. We also believe a quality starting point for a web app should be free and available to everyone. Our hope is that together we can create the best SaaS template out there and bring our ideas to customers quickly.',
|
||||||
|
+ },
|
||||||
|
+ {
|
||||||
|
+ id: 2,
|
||||||
|
+ question: "What's Wasp?",
|
||||||
|
+ href: 'https://wasp-lang.dev',
|
||||||
|
+ answer: "It's the fastest way to develop full-stack React + NodeJS + Prisma apps and it's what gives this template superpowers. Wasp relies on React, NodeJS, and Prisma to define web components and server queries and actions. Wasp's secret sauce is its compiler which takes the client, server code, and config file and outputs the client app, server app and deployment code, supercharging the development experience. Combined with this template, you can build a SaaS app in record time.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const footerNavigation = {
|
||||||
|
app: [
|
||||||
|
+ { name: 'Github', href: GITHUB_URL },
|
||||||
|
{ name: 'Documentation', href: DOCS_URL },
|
||||||
|
{ name: 'Blog', href: BLOG_URL },
|
||||||
|
],
|
||||||
21
opensaas-sh/app_diff/src/server/actions.ts.diff
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
--- template/app/src/server/actions.ts
|
||||||
|
+++ opensaas-sh/app/src/server/actions.ts
|
||||||
|
@@ -318,6 +318,18 @@
|
||||||
|
throw new HttpError(401);
|
||||||
|
}
|
||||||
|
|
||||||
|
+ const numberOfFilesByUser = await context.entities.File.count({
|
||||||
|
+ where: {
|
||||||
|
+ user: {
|
||||||
|
+ id: context.user.id,
|
||||||
|
+ },
|
||||||
|
+ },
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
+ if (numberOfFilesByUser >= 2) {
|
||||||
|
+ throw new HttpError(403, 'Thanks for trying Open SaaS. This demo only allows 2 file uploads per user.');
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
const userInfo = context.user.id.toString();
|
||||||
|
|
||||||
|
const { uploadUrl, key } = await getUploadFileSignedURLFromS3({ fileType, userInfo });
|
||||||
18
opensaas-sh/app_diff/src/server/queries.ts.diff
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
--- template/app/src/server/queries.ts
|
||||||
|
+++ opensaas-sh/app/src/server/queries.ts
|
||||||
|
@@ -136,6 +136,7 @@
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
isAdmin: args.isAdmin,
|
||||||
|
+ isMockUser: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
@@ -176,6 +177,7 @@
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
isAdmin: args.isAdmin,
|
||||||
|
+ isMockUser: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
10
opensaas-sh/app_diff/src/server/scripts/usersSeed.ts.diff
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
--- template/app/src/server/scripts/usersSeed.ts
|
||||||
|
+++ opensaas-sh/app/src/server/scripts/usersSeed.ts
|
||||||
|
@@ -26,6 +26,7 @@
|
||||||
|
credits: faker.number.int({ min: 0, max: 3 }),
|
||||||
|
checkoutSessionId: null,
|
||||||
|
subscriptionTier: faker.helpers.arrayElement([TierIds.HOBBY, TierIds.PRO]),
|
||||||
|
+ isMockUser: true,
|
||||||
|
};
|
||||||
|
return user;
|
||||||
|
}
|
||||||
7
opensaas-sh/app_diff/src/shared/constants.ts.diff
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
--- template/app/src/shared/constants.ts
|
||||||
|
+++ opensaas-sh/app/src/shared/constants.ts
|
||||||
|
@@ -6,3 +6,4 @@
|
||||||
|
|
||||||
|
export const DOCS_URL = 'https://docs.opensaas.sh';
|
||||||
|
export const BLOG_URL = 'https://docs.opensaas.sh/blog';
|
||||||
|
+export const GITHUB_URL = 'https://github.com/wasp-lang/open-saas';
|
||||||
20
opensaas-sh/app_diff/tailwind.config.cjs.diff
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
--- template/app/tailwind.config.cjs
|
||||||
|
+++ opensaas-sh/app/tailwind.config.cjs
|
||||||
|
@@ -8,6 +8,7 @@
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
+ sans: ['ui-monospace', 'Liberation Mono', 'Menlo', 'monospace'],
|
||||||
|
satoshi: ['Satoshi', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
@@ -246,6 +247,9 @@
|
||||||
|
'spin-2': 'spin 2s linear infinite',
|
||||||
|
'spin-3': 'spin 3s linear infinite',
|
||||||
|
},
|
||||||
|
+ aspectRatio: {
|
||||||
|
+ '4/3': '4 / 3',
|
||||||
|
+ },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography'), require('@tailwindcss/forms')],
|
||||||
21
opensaas-sh/blog/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
98
opensaas-sh/blog/astro.config.mjs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import starlight from '@astrojs/starlight';
|
||||||
|
import starlightBlog from 'starlight-blog';
|
||||||
|
|
||||||
|
import tailwind from '@astrojs/tailwind';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://opensaas.sh',
|
||||||
|
trailingSlash: 'always',
|
||||||
|
integrations: [
|
||||||
|
starlight({
|
||||||
|
title: 'OpenSaaS.sh',
|
||||||
|
description: 'Open SaaS is a free, open-source, full-stack SaaS starter kit for React + NodeJS.',
|
||||||
|
customCss: ['./src/styles/tailwind.css'],
|
||||||
|
logo: {
|
||||||
|
src: '/src/assets/logo.png',
|
||||||
|
alt: 'Open SaaS',
|
||||||
|
},
|
||||||
|
head: [
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
attrs: {
|
||||||
|
src: 'https://www.googletagmanager.com/gtag/js?id=G-8QGM76GR3Q',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
content: `
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'G-8QGM76GR3Q');
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
editLink: {
|
||||||
|
baseUrl: 'https://github.com/wasp-lang/open-saas/edit/deployed-version/blog',
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
SiteTitle: './src/components/MyHeader.astro',
|
||||||
|
ThemeSelect: './src/components/MyThemeSelect.astro',
|
||||||
|
},
|
||||||
|
social: {
|
||||||
|
github: 'https://github.com/wasp-lang/open-saas',
|
||||||
|
twitter: 'https://twitter.com/wasp_lang',
|
||||||
|
discord: 'https://discord.gg/aCamt5wCpS',
|
||||||
|
},
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
label: 'Start Here',
|
||||||
|
items: [
|
||||||
|
{ label: 'Introduction', link: '/' },
|
||||||
|
{ label: 'Getting Started', link: '/start/getting-started/' },
|
||||||
|
{ label: 'Guided Tour', link: '/start/guided-tour/' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Guides',
|
||||||
|
items: [
|
||||||
|
{ label: 'Authentication', link: '/guides/authentication/' },
|
||||||
|
{ label: 'Authorization', link: '/guides/authorization/' },
|
||||||
|
{ label: 'Stripe Integration', link: '/guides/stripe-integration/' },
|
||||||
|
{ label: 'Stripe Testing', link: '/guides/stripe-testing/' },
|
||||||
|
{ label: 'Analytics', link: '/guides/analytics/' },
|
||||||
|
{ label: 'SEO', link: '/guides/seo/' },
|
||||||
|
{ label: 'Email Sending', link: '/guides/email-sending/' },
|
||||||
|
{ label: 'File Uploading', link: '/guides/file-uploading/' },
|
||||||
|
{ label: 'Deploying', link: '/guides/deploying/' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'General',
|
||||||
|
items: [
|
||||||
|
{ label: 'Admin Dashboard', link: '/general/admin-dashboard/' },
|
||||||
|
{ label: 'User Overview', link: '/general/user-overview/' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
starlightBlog({
|
||||||
|
title: 'Blog',
|
||||||
|
customCss: ['./src/styles/tailwind.css'],
|
||||||
|
authors: {
|
||||||
|
vince: {
|
||||||
|
name: 'Vince',
|
||||||
|
title: 'Dev Rel @ Wasp',
|
||||||
|
picture: '/CRAIG_ROCK.png', // Images in the `public` directory are supported.
|
||||||
|
url: 'https://wasp-lang.dev',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
tailwind({ applyBaseStyles: false }),
|
||||||
|
],
|
||||||
|
});
|
||||||
18211
opensaas-sh/blog/package-lock.json
generated
Normal file
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
BIN
opensaas-sh/blog/public/file-uploads/cors.png
Normal file
|
After Width: | Height: | Size: 728 KiB |
BIN
opensaas-sh/blog/public/file-uploads/create-bucket.png
Normal file
|
After Width: | Height: | Size: 772 KiB |
BIN
opensaas-sh/blog/public/file-uploads/default-settings.png
Normal file
|
After Width: | Height: | Size: 974 KiB |
BIN
opensaas-sh/blog/public/file-uploads/find-s3.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
opensaas-sh/blog/public/file-uploads/keys.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
opensaas-sh/blog/public/file-uploads/new-bucket.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
opensaas-sh/blog/public/file-uploads/permissions.png
Normal file
|
After Width: | Height: | Size: 550 KiB |
BIN
opensaas-sh/blog/public/file-uploads/username.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
opensaas-sh/blog/public/seo/open-saas-google.png
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
opensaas-sh/blog/public/stripe/api-keys.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
opensaas-sh/blog/public/stripe/db-studio.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
opensaas-sh/blog/public/stripe/listen-to-stripe-events.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
opensaas-sh/blog/public/stripe/price-ids.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
opensaas-sh/blog/public/stripe/stripe-webhook-signing-secret.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
opensaas-sh/blog/public/stripe/test-product.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
98
opensaas-sh/blog/src/components/MyHeader.astro
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
import { logos } from 'virtual:starlight/user-images';
|
||||||
|
import config from 'virtual:starlight/user-config';
|
||||||
|
import blogConfig from 'virtual:starlight-blog-config'
|
||||||
|
import type { Props } from '@astrojs/starlight/props';
|
||||||
|
|
||||||
|
const href = Astro.site;
|
||||||
|
const { siteTitle } = Astro.props;
|
||||||
|
---
|
||||||
|
<a {href} class="site-title sl-flex">
|
||||||
|
{
|
||||||
|
config.logo && logos.dark && (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
class:list={{ 'light:sl-hidden': !('src' in config.logo) }}
|
||||||
|
alt={config.logo.alt}
|
||||||
|
src={logos.dark.src}
|
||||||
|
width={logos.dark.width}
|
||||||
|
height={logos.dark.height}
|
||||||
|
/>
|
||||||
|
{/* Show light alternate if a user configure both light and dark logos. */}
|
||||||
|
{!('src' in config.logo) && (
|
||||||
|
<img
|
||||||
|
class="dark:sl-hidden"
|
||||||
|
alt={config.logo.alt}
|
||||||
|
src={logos.light?.src}
|
||||||
|
width={logos.light?.width}
|
||||||
|
height={logos.light?.height}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<span class:list={{ 'sr-only': config.logo?.replacesTitle }} class="title-text dark:text-white hover:text-yellow-500">
|
||||||
|
{siteTitle}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<a href="/" class="text-gray-900 hover:text-yellow-500 dark:text-white">Docs</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/blog/" class="text-gray-900 hover:text-yellow-500 dark:text-white">{blogConfig.title}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.site-title {
|
||||||
|
justify-self: flex-start;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--sl-color-gray-9);
|
||||||
|
gap: var(--sl-nav-gap);
|
||||||
|
font-size: var(--sl-text-h4);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-inline-end: 1rem;
|
||||||
|
border-inline-end: 1px solid var(--sl-color-gray-5);
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
height: calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y));
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: 0 50%;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
border-inline-end: 1px solid var(--sl-color-gray-5);
|
||||||
|
margin-left: 1rem;
|
||||||
|
align-self: center;
|
||||||
|
gap: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
padding-inline-end: 1rem;
|
||||||
|
}
|
||||||
|
.title-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 50rem) {
|
||||||
|
div {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 28rem) {
|
||||||
|
.title-text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--sl-color-text-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
opacity: 0.66;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
80
opensaas-sh/blog/src/content/docs/general/admin-dashboard.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: Admin Dashboard
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
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
|
||||||
|
username 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
|
||||||
|
|
||||||
|
// or add many admins with a comma-separated list
|
||||||
|
|
||||||
|
ADMIN_EMAILS=me@example.com,you@example.com,them@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
if you've already logged in with an email address that you want to give admin priveledges to, you can run the following command in a separate terminal window to update the user's `isAdmin` field:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wasp db studio
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
:::tip[Star our Repo on GitHub! 🌟]
|
||||||
|
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!
|
||||||
|
|
||||||
|
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||||
|
:::
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
<!-- TODO: add photo -->
|
||||||
|
|
||||||
|
- [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.
|
||||||
|
|
||||||
|
:::note[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](https://github.com/wasp-lang/open-saas/issues) or [submitting a pull request](https://github.com/wasp-lang/open-saas/pulls)
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 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/payment status
|
||||||
|
|
||||||
133
opensaas-sh/blog/src/content/docs/general/user-overview.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
title: User Overview
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
|
||||||
|
This reference will help you understand how the User entity works in this template.
|
||||||
|
This includes the user roles, subscription tiers and statuses, and how to authorize access to certain pages and components.
|
||||||
|
|
||||||
|
## User Entity
|
||||||
|
|
||||||
|
The `User` entity within your app is defined in the `main.wasp` file:
|
||||||
|
|
||||||
|
```tsx title="main.wasp" ins="User: {}"
|
||||||
|
entity User {=psl
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
email String? @unique
|
||||||
|
username String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
lastActiveTimestamp DateTime @default(now())
|
||||||
|
isAdmin Boolean @default(false)
|
||||||
|
stripeId String?
|
||||||
|
checkoutSessionId String?
|
||||||
|
subscriptionTier String?
|
||||||
|
subscriptionStatus String?
|
||||||
|
sendEmail Boolean @default(false)
|
||||||
|
datePaid DateTime?
|
||||||
|
credits Int @default(3)
|
||||||
|
relatedObject RelatedObject[]
|
||||||
|
externalAuthAssociations SocialLogin[]
|
||||||
|
contactFormMessages ContactFormMessage[]
|
||||||
|
psl=}
|
||||||
|
```
|
||||||
|
|
||||||
|
We store all pertinent information to the user, including identification, subscription, and Stripe information. Meanwhile, Wasp abstracts away all the Auth related entities dealing with `passwords`, `sessions`, and `socialLogins`, so you don't have to worry about these at all in your Prisma schema (if you want to learn more about this process, check out the [Wasp Auth Docs](https://wasp-lang.dev/docs/auth/overview)).
|
||||||
|
|
||||||
|
## Stripe and Subscriptions
|
||||||
|
|
||||||
|
We use Stripe to handle all of our subscription payments. The `User` entity has a number of fields that are related to Stripe and their ability to access features behind the paywall:
|
||||||
|
|
||||||
|
```tsx title="main.wasp" {4-10}
|
||||||
|
entity User {=psl
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
//...
|
||||||
|
stripeId String?
|
||||||
|
checkoutSessionId String?
|
||||||
|
subscriptionTier String?
|
||||||
|
subscriptionStatus String?
|
||||||
|
datePaid DateTime?
|
||||||
|
credits Int @default(3)
|
||||||
|
//...
|
||||||
|
psl=}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `stripeId`: The Stripe customer ID. This is created by Stripe on checkout and used to identify the customer.
|
||||||
|
- `checkoutSessionId`: The Stripe checkout session ID. This is created by Stripe on checkout and used to identify the checkout session.
|
||||||
|
- `subscriptionTier`: The subscription tier the user is on. This is set by the app and is used to determine what features the user has access to. By default, we have two tiers: `hobby-tier` and `pro-tier`.
|
||||||
|
- `subscriptionStatus`: The subscription status of the user. This is set by Stripe and is used to determine whether the user has access to the app or not. By default, we have four statuses: `active`, `past_due`, `canceled`, and `deleted`.
|
||||||
|
- `credits` (optional): By default, a user is given 3 credits to trial your product before they have to pay. You can create a one-time purchase product in Stripe to allow users to purchase more credits if they run out.
|
||||||
|
|
||||||
|
### Subscription Statuses
|
||||||
|
|
||||||
|
In general, we determine if a user has paid for an initial subscription by checking if the `subscriptionStatus` field is set. This field is set by Stripe within your webhook handler and is used to signify more detailed information on the user's current status. By default, the template handles four statuses: `active`, `past_due`, `canceled`, and `deleted`.
|
||||||
|
|
||||||
|
- When `active` the user has paid for a subscription and has full access to the app.
|
||||||
|
|
||||||
|
- When `canceled`, the user has canceled their subscription and has access to the app until the end of their billing period.
|
||||||
|
|
||||||
|
- When `deleted`, the user has reached the end of their subscription period after canceling and no longer has access to the app.
|
||||||
|
|
||||||
|
- When `past_due`, the user's automatic subscription renewal payment was declined (e.g. their credit card expired). You can choose how to handle this status within your app. For example, you can send the user an email to update their payment information:
|
||||||
|
```tsx title="src/server/webhooks/stripe.ts"
|
||||||
|
import { emailSender } from "wasp/server/email";
|
||||||
|
//...
|
||||||
|
|
||||||
|
if (subscription.status === 'past_due') {
|
||||||
|
const updatedCustomer = await context.entities.User.update({
|
||||||
|
where: {
|
||||||
|
id: customer.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
subscriptionStatus: 'past_due',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updatedCustomer.email) {
|
||||||
|
await emailSender.send({
|
||||||
|
to: updatedCustomer.email,
|
||||||
|
subject: 'Your Payment is Past Due',
|
||||||
|
text: 'Please update your payment information to continue using our service.',
|
||||||
|
html: '...',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See the client-side [authorization section](/guides/authorization) below for more info on how to handle these statuses within your app.
|
||||||
|
|
||||||
|
### Subscription Tiers
|
||||||
|
|
||||||
|
The `subscriptionTier` field is used to determine what features the user has access to.
|
||||||
|
|
||||||
|
By default, we have two tiers: `hobby-tier` and `pro-tier`.
|
||||||
|
|
||||||
|
You can add more tiers by adding more products and price IDs to your Stripe product and updating environment variables in your `.env.server` file as well as the relevant code in your app.
|
||||||
|
|
||||||
|
See the [Stripe Integration Guide](/guides/stripe-integration) for more info on how to do this.
|
||||||
|
|
||||||
|
## User Roles
|
||||||
|
|
||||||
|
At the moment, we have two user roles: `admin` and `user`. This is defined within the `isAdmin` field in the `User` entity:
|
||||||
|
|
||||||
|
```tsx title="main.wasp" {7}
|
||||||
|
entity User {=psl
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
email String? @unique
|
||||||
|
username String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
lastActiveTimestamp DateTime @default(now())
|
||||||
|
isAdmin Boolean @default(false)
|
||||||
|
//...
|
||||||
|
psl=}
|
||||||
|
```
|
||||||
|
|
||||||
|
As an Admin, a user has access to the Admin dashboard, along with the user table where they can view and search for users, and edit and update information manually if necessary.
|
||||||
|
|
||||||
|
:::tip[Admin Priveleges]
|
||||||
|
If you'd like to give yourself and/or certain users admin priveleges, follow the instructions in the [Admin Dashboard](/general/admin-dashboard/#permissions) section.
|
||||||
|
:::
|
||||||
|
|
||||||
|
As a general User, a user has access to the user-facing app that sits behind the login, but not the Admin dashboard. You can further restrict access to certain features within the app by following the [authorization guide](/guides/authorization).
|
||||||
135
opensaas-sh/blog/src/content/docs/guides/analytics.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
title: Analytics
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
This guide will show you how to integrate analytics for your app. You can choose between [Google Analytics](#google-analytics) and [Plausible](#plausible).
|
||||||
|
|
||||||
|
Google Analytics is free, but tends to be more cumbersome to use.
|
||||||
|
|
||||||
|
Plausible is an open-source, privacy-friendly alternative to Google Analytics. It's also easier to use than Google if you use their hosted service, which is a paid feature. But, it is completely free if you want to self-host it, although this comes with some additional setup steps.
|
||||||
|
|
||||||
|
If you're looking to add analytics to your blog, you can follow the [Adding Analytics to your Blog](#adding-analytics-to-your-blog) section at the end of this guide.
|
||||||
|
|
||||||
|
## Plausible
|
||||||
|
|
||||||
|
### Hosted Plausible
|
||||||
|
Sign up for a hosted Plausible account [here](https://plausible.io/).
|
||||||
|
|
||||||
|
Once you've signed up, you'll be taken to your dashboard. Create your site by adding your domain. Your domain is also your `PLAUSIBLE_SITE_ID` in your `.env.server` file. Make sure to add it.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PLAUSIBLE_SITE_ID=<your domain without www>
|
||||||
|
```
|
||||||
|
|
||||||
|
After adding your domain, you'll be taken to a page with your Plausible script tag. Copy and paste this script tag into the `main.wasp` file's head section.
|
||||||
|
|
||||||
|
```js {7}
|
||||||
|
app OpenSaaS {
|
||||||
|
wasp: {
|
||||||
|
version: "^0.13.0"
|
||||||
|
},
|
||||||
|
title: "My SaaS App",
|
||||||
|
head: [
|
||||||
|
"<your plausible script tag here>",
|
||||||
|
],
|
||||||
|
//...
|
||||||
|
```
|
||||||
|
|
||||||
|
Go back to your Plausible dashboard, click on your username in the top right, and click on the `Settings` tab. Scroll down, find your API key and paste it into your `.env.server` file under the `PLAUSIBLE_API_KEY` variable.
|
||||||
|
|
||||||
|
|
||||||
|
### Self-hosted Plausible
|
||||||
|
|
||||||
|
Plausible, being an open-source project, allows you to self-host your analytics. This is a great option if you want to keep your data private and not pay for the hosted service.
|
||||||
|
|
||||||
|
*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
|
||||||
|
|
||||||
|
After you sign up for [Google analytics](https://analytics.google.com/), go to your `Admin` panel in the bottom of the left sidebar and then create a "Property" for your app.
|
||||||
|
|
||||||
|
Once you've completed the steps to create a new Property, some Installation Instructions will pop up. Select `install manually` and copy and paste the Google script tag into the `main.wasp` file's head section.
|
||||||
|
|
||||||
|
```js {7}
|
||||||
|
app OpenSaaS {
|
||||||
|
wasp: {
|
||||||
|
version: "^0.13.0"
|
||||||
|
},
|
||||||
|
title: "My SaaS App",
|
||||||
|
head: [
|
||||||
|
"<your google analytics script tag here>",
|
||||||
|
],
|
||||||
|
//...
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip[noscript]
|
||||||
|
In the Installation Instructions, Google Tag Manager might also instruct you to paste the `noscript` code snippet immediately after the opening `<body>` tag.
|
||||||
|
You should skip this step because this snippet is activated only if users try to browse your app without JavaScript enabled, which is very rare and Wasp needs JS anyway.
|
||||||
|
:::
|
||||||
|
|
||||||
|
Then, set up the Google Analytics API access by following these steps:
|
||||||
|
|
||||||
|
1. **Set up a Google Cloud project:** If you haven't already, start by setting up a project in the [Google Cloud Console](https://console.cloud.google.com/).
|
||||||
|
|
||||||
|
2. **Enable the Google Analytics API for your project:** Navigate to the "Library" in the Google Cloud Console and search for the "Google Analytics Data API" (for Google Analytics 4 properties) and enable it.
|
||||||
|
|
||||||
|
3. **Create credentials:** Now go to the "Credentials" tab within your Google Cloud project, click on `+ credentials`, and create a new service account key. First, give it a name. Then, under "Grant this service account access to project", choose `viewer`.
|
||||||
|
|
||||||
|
4. **Create Credentials:** When you go back to `Credentials` page, you should see a new service account listed under "Service Accounts". It will be a long email address to ends with `@your-project-id.iam.gserviceaccount.com`. Click on the service account name to go to the service account details page.
|
||||||
|
|
||||||
|
- Under “Keys” in the service account details page, click “Add Key” and choose `Create new key`.
|
||||||
|
|
||||||
|
- Select "JSON", then click “Create” to download your new service account’s JSON key file. Keep this file secure and don't add it to your git repo – it grants access to your Google Analytics data.
|
||||||
|
5. **Update your Google Anayltics Settings:** Go back to your Google Analytics dashboard, and click on the `Admin` section in the left sidebar. Under `Property Settings > Property > Property Access Management` Add the service account email address (the one that ends with `@your-project-id.iam.gserviceaccount.com`) and give it `Viewer` permissions.
|
||||||
|
|
||||||
|
6. **Encode and add the Credentials:** Add the `client_email` and the `private_key` from your JSON Key file into your `.env.server` file. But be careful! Because Google uses a special PEM private key, you need to first convert the key to base64, otherwise you will run into errors parsing the key. To do this, in a terminal window, run the command below and paste the output into your `.env.server` file under the `GOOGLE_ANALYTICS_PRIVATE_KEY` variable:
|
||||||
|
```sh
|
||||||
|
echo -n "PRIVATE_KEY" | base64
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Add your Google Analytics Property ID:** You will find the Property ID in your Google Analytics dashboard in the `Admin > Property > Property Settings > Property Details` section of your Google Analytics property (**not** your Google Cloud console). Add this 9-digit number to your `.env.server` file under the `GOOGLE_ANALYTICS_PROPERTY_ID` variable.
|
||||||
|
|
||||||
|
## Adding Analytics to your Blog
|
||||||
|
|
||||||
|
To add your analytics script to your Astro Starlight blog, all you need to do is modify the `head` property in your `blog/astro.config.mjs` file.
|
||||||
|
|
||||||
|
Below is an example of how to add Google Analytics to your blog:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://opensaas.sh',
|
||||||
|
integrations: [
|
||||||
|
starlightBlog({
|
||||||
|
// ...
|
||||||
|
}),
|
||||||
|
starlight({
|
||||||
|
//...
|
||||||
|
head: [
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
attrs: {
|
||||||
|
src: 'https://www.googletagmanager.com/gtag/js?id=<YOUR-GOOGLE-ANALYTICS-ID>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
content: `
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', '<YOUR-GOOGLE-ANALYTICS-ID>');
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
```
|
||||||
92
opensaas-sh/blog/src/content/docs/guides/authentication.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
title: Authentication
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
|
||||||
|
Setting up your app's authentication is easy with Wasp. In fact, it's already set up for you in the `main.wasp` file:
|
||||||
|
|
||||||
|
```tsx title="main.wasp" "
|
||||||
|
auth: {
|
||||||
|
userEntity: User,
|
||||||
|
methods: {
|
||||||
|
email: {},
|
||||||
|
google: {},
|
||||||
|
gitHub: {}
|
||||||
|
},
|
||||||
|
onAuthFailedRedirectTo: "/",
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
The great part is, by defining your auth config in the `main.wasp` file, Wasp manages most of the Auth process for you, including the auth-related database entities for user credentials and sessions, as well as auto-generated client components for your app on the fly (aka AuthUI -- you can see them in the `src/client/auth` folder).
|
||||||
|
|
||||||
|
## Email Verified Auth
|
||||||
|
|
||||||
|
`email` method is the default auth method in Open Saas.
|
||||||
|
|
||||||
|
Since it needs to send emails to verify users and reset passwords, it requires an [email sender](https://wasp-lang.dev/docs/advanced/email) provider: a service it can use to send emails.
|
||||||
|
"email sender" provider is configured via `app.emailSender` field in the `main.wasp` file.
|
||||||
|
|
||||||
|
:::caution[Dummy Email Provider]
|
||||||
|
To make it easy for you to get started, Open SaaS initially comes with the `Dummy` "email sender" provider, which does not send any emails, but instead logs all email verification links/tokens to the server's console!
|
||||||
|
You can then follow these links to verify the user and continue with the sign-up process.
|
||||||
|
|
||||||
|
```tsx title="main.wasp"
|
||||||
|
emailSender: {
|
||||||
|
provider: Dummy, // logs all email verification links/tokens to the server's console
|
||||||
|
defaultFrom: {
|
||||||
|
name: "Open SaaS App",
|
||||||
|
email: "me@example.com"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
You **can not use the Dummy provider in production** and your app **will not build** until you move to a production-ready provider, such as SendGrid. We outline the process of migrating to SendGrid below.
|
||||||
|
:::
|
||||||
|
|
||||||
|
In order to use the `email` auth method in production, you'll need to switch from the `Dummy` "email sender" provider to a production-ready provider like SendGrid:
|
||||||
|
|
||||||
|
1. First, set up your app's `emailSender` in the `main.wasp` file by following [this guide](/guides/email-sending/#integrate-your-email-sender).
|
||||||
|
2. Add your `SENDGRID_API_KEY` to the `.env.server` file.
|
||||||
|
3. Make sure the email address you use in the `fromField` object is the same email address that you configured your SendGrid account to send out emails with. In the end, your `main.wasp` file should look something like this:
|
||||||
|
```ts title="main.wasp" {6,7} del={15} ins={16}
|
||||||
|
auth: {
|
||||||
|
methods: {
|
||||||
|
email: {
|
||||||
|
fromField: {
|
||||||
|
name: "Open SaaS App",
|
||||||
|
// When using SendGrid, you must use the same email address that you configured your account to send out emails with!
|
||||||
|
email: "me@example.com"
|
||||||
|
},
|
||||||
|
//...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//...
|
||||||
|
emailSender: {
|
||||||
|
provider: Dummy,
|
||||||
|
provider: SendGrid,
|
||||||
|
defaultFrom: {
|
||||||
|
name: "Open SaaS App",
|
||||||
|
// When using SendGrid, you must use the same email address that you configured your account to send out emails with!
|
||||||
|
email: "me@example.com"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
And that's it. Wasp will take care of the rest and update your AuthUI components accordingly.
|
||||||
|
|
||||||
|
Check out the [Wasp Auth docs](https://wasp-lang.dev/docs/auth/overview) for more info.
|
||||||
|
|
||||||
|
## Google & GitHub Auth
|
||||||
|
|
||||||
|
We've also customized and pre-built the Google and GitHub auth flow for you. To start using them, you just need to uncomment out the methods you want in your `main.wasp` file and obtain the proper API keys to add to your `.env.server` file.
|
||||||
|
|
||||||
|
To create a Google OAuth app and get your Google API keys, follow the instructions in [Wasp's Google Auth docs](https://wasp-lang.dev/docs/auth/social-auth/google#3-creating-a-google-oauth-app).
|
||||||
|
|
||||||
|
To create a GitHub OAuth app and get your GitHub API keys, follow the instructions in [Wasp's GitHub Auth docs](https://wasp-lang.dev/docs/auth/social-auth/github#3-creating-a-github-oauth-app).
|
||||||
|
|
||||||
|
Again, Wasp will take care of the rest and update your AuthUI components accordingly.
|
||||||
95
opensaas-sh/blog/src/content/docs/guides/authorization.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
title: Authorization
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide will help you get started with authorization in your SaaS app.
|
||||||
|
|
||||||
|
Authorization refers to what users can access in your app. This is useful for differentiating between users who have paid for different subscription tiers (e.g. "hobby" vs "pro"), or between users who have admin privileges and those who do not.
|
||||||
|
|
||||||
|
Authorization differs from [authentication](/guides/authentication) in that authentication refers to the process of verifying that a user is who they say they are (e.g. logging in with a username and password).
|
||||||
|
|
||||||
|
To learn more about the different types of user permissions built into this SaaS template, including Stripe subscription tiers and statuses, check out the [User Permissions Reference](/general/user-permissions).
|
||||||
|
|
||||||
|
Also, check out our [blog post](https://wasp-lang.dev/blog/2022/11/29/permissions-in-web-apps) to learn more about authorization (access control) in web apps.
|
||||||
|
|
||||||
|
### Client-side Authorization
|
||||||
|
|
||||||
|
Open Saas starts with all users having access to the landing page (`/`), but only authenticated users having access to the rest of the app (e.g. to the `/demo-app`, or to the `/account`).
|
||||||
|
|
||||||
|
To control which pages require users to be authenticated to access them, you can set the `authRequired` property of the corresponding `page` definition in your `main.wasp` file:
|
||||||
|
|
||||||
|
```tsx title="main.wasp" {3}
|
||||||
|
route AccountRoute { path: "/account", to: AccountPage }
|
||||||
|
page AccountPage {
|
||||||
|
authRequired: true,
|
||||||
|
component: import Account from "@src/client/app/AccountPage"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically redirect users to the login page if they are not logged in while trying to access that page.
|
||||||
|
|
||||||
|
:::caution[Client-side authorization is just for the looks]
|
||||||
|
Users can manipulate the client code as they wish, meaning that client-side access control (authorization) serves the purpose of ergonomics/user experience, not the purpose of restricting access to sensitive data.
|
||||||
|
This means that authorization in the client code is a nice-to-have: it is here to make sure users don't get lost in the part of the app they can't work with because data is missing due to them not having access, not to actually restrict them from doing something.
|
||||||
|
Actually ensuring they don't have access to the data, that is on the server to ensure, via server-side logic that you will implement for authorization (access control).
|
||||||
|
:::
|
||||||
|
|
||||||
|
If you want more fine-grained control over what users can access, there are two Wasp-specific options:
|
||||||
|
1. When you define the `authRequired: true` property on the `page` definition, Wasp automatically passes the User object to the page component. Here you can check for certain user properties before authorizing access:
|
||||||
|
|
||||||
|
```tsx title="ExamplePage.tsx" "{ user }: { user: User }"
|
||||||
|
import { type User } from "wasp/entities";
|
||||||
|
|
||||||
|
export default function Example({ user }: { user: User }) {
|
||||||
|
|
||||||
|
if (user.subscriptionStatus === 'past_due') {
|
||||||
|
return (<span>Your subscription is past due. Please update your payment information.</span>)
|
||||||
|
}
|
||||||
|
if (user.subscriptionStatus === 'canceled') {
|
||||||
|
return (<span>Your will susbscription end on 01.01.2024</span>)
|
||||||
|
}
|
||||||
|
if (user.subscriptionStatus === 'active') {
|
||||||
|
return (<span>Thanks so much for your support!</span>)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Or you can take advantage of the `useAuth` hook and check for certain user properties before authorizing access to certain pages or components:
|
||||||
|
|
||||||
|
```tsx title="ExamplePage.tsx" {1, 4}
|
||||||
|
import { useAuth } from "wasp/client/auth";
|
||||||
|
|
||||||
|
export default function ExampleHomePage() {
|
||||||
|
const { data: user } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h1> Hi {user.email || 'there'} 👋 </h1>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-side Authorization
|
||||||
|
|
||||||
|
Authorization on the server-side is the core of your access control logic, and determines what users actually can or can't do (unlike client-side authorization logic which is there merely for UX).
|
||||||
|
|
||||||
|
You can authorize access to server-side operations by adding a check for a logged-in user on the `context.user` object which is passed to all operations in Wasp:
|
||||||
|
|
||||||
|
```tsx title="src/server/actions.ts"
|
||||||
|
export const updateCurrentUser: UpdateCurrentUser<...> = async (args, context) => {
|
||||||
|
if (!context.user) {
|
||||||
|
throw new HttpError(401); // throw an error if user is not logged in
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.user.subscriptionStatus === 'past_due') {
|
||||||
|
throw new HttpError(403, 'Your subscription is past due. Please update your payment information.');
|
||||||
|
}
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
160
opensaas-sh/blog/src/content/docs/guides/deploying.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
---
|
||||||
|
title: Deploying
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
|
||||||
|
Because this SaaS app is a React/NodeJS/Postgres app built on top of [Wasp](https://wasp-lang.dev), we will direct you to the [Wasp Deployment Guide](https://wasp-lang.dev/docs/advanced/deployment/overview/) for more detailed instructions, except for where the instructions are specific to this template.
|
||||||
|
|
||||||
|
The simplest and quickest option is to take advantage of Wasp's one-command deploy to [Fly.io](#deploying-to-flyio) (`wasp deploy`).
|
||||||
|
|
||||||
|
Or if you prefer to deploy to a different provider, or your frontend and backend separately, you can follow the [Deploying Manually](#deploying-manually--to-other-providers) section below.
|
||||||
|
|
||||||
|
If you're looking to deploy your Astro Blog, you can follow the [Deploying your Blog](#deploying-your-blog) section at the end of this guide.
|
||||||
|
|
||||||
|
## Deploying your App
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Make sure you've got all your API keys and environment variables set up before you deploy.
|
||||||
|
|
||||||
|
#### Env Vars
|
||||||
|
##### Stripe Vars
|
||||||
|
In the [Stripe integration guide](/guides/stripe-integration), you set up your Stripe API keys using test keys and product ids. You'll need to get the live/production versions of those keys at [https://dashboard.stripe.com](https://dashboard.stripe.com). To get these, repeat the instructions in the [Stripe Integration Guide](/guides/stripe-integration) without being in test mode.
|
||||||
|
- [ ] `STRIPE_KEY`
|
||||||
|
- [ ] `STRIPE_WEBHOOK_SECRET`
|
||||||
|
- [ ] all `PRICE_ID` variables
|
||||||
|
- [ ] `REACT_APP_STRIPE_CUSTOMER_PORTAL` (for the client-side)
|
||||||
|
|
||||||
|
##### Other Vars
|
||||||
|
Many of your other environment variables will probably be the same as in development, but you should double-check that they are set correctly for production.
|
||||||
|
|
||||||
|
Here are a list of all of them (some of which you may not be using, e.g. Analytics, Social Auth) in case you need to check:
|
||||||
|
###### General Vars
|
||||||
|
- [ ] `DATABASE_URL`
|
||||||
|
- [ ] `JWT_SECRET`
|
||||||
|
- [ ] `WASP_WEB_CLIENT_URL`
|
||||||
|
- [ ] `WASP_SERVER_URL`
|
||||||
|
|
||||||
|
###### Open AI API Key
|
||||||
|
- [ ] `OPENAI_API_KEY`
|
||||||
|
|
||||||
|
###### Sendgrid API Key
|
||||||
|
- [ ] `SENDGRID_API_KEY`
|
||||||
|
|
||||||
|
###### Social Auth Vars
|
||||||
|
- [ ] `GOOGLE_CLIENT_ID`
|
||||||
|
- [ ] `GOOGLE_CLIENT_SECRET`
|
||||||
|
- [ ] `GITHUB_CLIENT_ID`
|
||||||
|
- [ ] `GITHUB_CLIENT_SECRET`
|
||||||
|
|
||||||
|
###### Analytics Vars
|
||||||
|
- [ ] `REACT_APP_PLAUSIBLE_ANALYTICS_ID` (for client-side)
|
||||||
|
- [ ] `PLAUSIBLE_API_KEY`
|
||||||
|
- [ ] `PLAUSIBLE_SITE_ID`
|
||||||
|
- [ ] `PLAUSIBLE_BASE_URL`
|
||||||
|
- [ ] `REACT_APP_GOOGLE_ANALYTICS_ID` (for client-side)
|
||||||
|
- [ ] `GOOGLE_ANALYTICS_CLIENT_EMAIL`
|
||||||
|
- [ ] `GOOGLE_ANALYTICS_PROPERTY_ID`
|
||||||
|
- [ ] `GOOGLE_ANALYTICS_PRIVATE_KEY`
|
||||||
|
(Make sure you convert the private key within the JSON file to base64 first with `echo -n "PRIVATE_KEY" | base64`. See the [Analytics docs](/guides/analytics/#google-analytics) for more info)
|
||||||
|
|
||||||
|
###### AWS S3 Vars
|
||||||
|
- [ ] `AWS_S3_IAM_ACCESS_KEY`
|
||||||
|
- [ ] `AWS_S3_IAM_SECRET_KEY`
|
||||||
|
- [ ] `AWS_S3_FILES_BUCKET`
|
||||||
|
- [ ] `AWS_S3_REGION`
|
||||||
|
|
||||||
|
### Deploying to Fly.io
|
||||||
|
|
||||||
|
[Fly.io](https://fly.io) is a platform for running your apps globally. It's a great choice for deploying your SaaS app because it's free to get started, can host your entire full-stack app in one place, scales well, and has one-command deploy integration with Wasp.
|
||||||
|
|
||||||
|
**Wasp provides the handy `wasp deploy` command to deploy your entire full-stack app (DB, server, and client) in one command.**
|
||||||
|
|
||||||
|
To learn how, please follow the detailed guide for [deploying to Fly via the Wasp CLI](https://wasp-lang.dev/docs/advanced/deployment/cli) from the Wasp documentation. We suggest you follow this guide carefully to get your app deployed.
|
||||||
|
|
||||||
|
:::caution[Setting Environment Variables]
|
||||||
|
Remember, because we've set certain client-side env variables, make sure to pass them to the `wasp deploy` commands so that they can be included in the build:
|
||||||
|
```sh
|
||||||
|
REACT_APP_CLIENT_ENV_VAR_1=<...> REACT_APP_CLIENT_ENV_VAR_2=<...> wasp deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
The `wasp deploy` command will also take care of setting the following server-side environment variables for you so you don't have to:
|
||||||
|
- `DATABASE_URL`
|
||||||
|
- `PORT`
|
||||||
|
- `JWT_SECRET`
|
||||||
|
- `WASP_WEB_CLIENT_URL`
|
||||||
|
- `WASP_SERVER_URL`
|
||||||
|
|
||||||
|
For setting the remaining server-side environment variables, please refer to the [Deploying with the Wasp CLI Guide](https://wasp-lang.dev/docs/advanced/deployment/cli#launch).
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Deploying Manually / to Other Providers
|
||||||
|
|
||||||
|
If you prefer to deploy manually, your frontend and backend separately, or just prefer using your favorite provider you can follow [Wasp's Manual Deployment Guide](https://wasp-lang.dev/docs/advanced/deployment/manually).
|
||||||
|
|
||||||
|
:::caution[Client-side Environment Variables]
|
||||||
|
Remember to always set additional client-side environment variables, such as `REACT_APP_STRIPE_CUSTOMER_PORTAL` by appending them to the build command, e.g.
|
||||||
|
```sh
|
||||||
|
REACT_APP_CLIENT_ENV_VAR_1=<...> npm run build
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Adding Server Redirect URL's to Social Auth
|
||||||
|
|
||||||
|
After deploying your server, you need to add the correct redirect URIs to the credential settings. For this, refer to the following guides from the Wasp Docs:
|
||||||
|
|
||||||
|
- [Google Auth](https://wasp-lang.dev/docs/auth/social-auth/google#3-creating-a-google-oauth-app:~:text=Under%20Authorized%20redirect%20URIs)
|
||||||
|
- [Github Auth](https://wasp-lang.dev/docs/auth/social-auth/github#3-creating-a-github-oauth-app:~:text=Authorization%20callback%20URL)
|
||||||
|
|
||||||
|
### Setting up your Stripe Webhook
|
||||||
|
|
||||||
|
Now you need to set up your stripe webhook for production use.
|
||||||
|
|
||||||
|
1. go to [https://dashboard.stripe.com/webhooks](https://dashboard.stripe.com/webhooks)
|
||||||
|
2. click on `+ add endpoint`
|
||||||
|
3. enter your endpoint url, which will be the url of your deployed server + `/stripe-webhook`, e.g. `https://open-saas-wasp-sh-server.fly.dev/stripe-webhook`
|
||||||
|

|
||||||
|
4. select the events you want to listen to. These should be the same events you're consuming in your webhook. For example, if you haven't added any additional events to the webhook and are using the defaults that came with this template, then you'll need to add:
|
||||||
|
<br/>- `account.updated`
|
||||||
|
<br/>- `checkout.session.completed`
|
||||||
|
<br/>- `customer.subscription.deleted`
|
||||||
|
<br/>- `customer.subscription.updated`
|
||||||
|
<br/>- `invoice.paid`
|
||||||
|

|
||||||
|
5. after that, go to the webhook you just created and `reveal` the new signing secret.
|
||||||
|
6. add this secret to your deployed server's `STRIPE_WEBHOOK_SECRET=` environment variable. <br/>If you've deployed to Fly.io, you can do that easily with the following command:
|
||||||
|
```sh
|
||||||
|
wasp deploy fly cmd --context server secrets set STRIPE_WEBHOOK_SECRET=whsec_...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploying your Blog
|
||||||
|
|
||||||
|
Deploying your Astro Starlight blog is a bit different than deploying your SaaS app. As an example, we will show you how to deploy your blog for free to Netlify. You will need a Netlify account and [Netlify CLI](https://docs.netlify.com/cli/get-started/) installed to follow these instructions.
|
||||||
|
|
||||||
|
Make sure you are logged in with Netlify CLI.
|
||||||
|
- You can check if you are logged in with `netlify status`,
|
||||||
|
- you can log in with `netlify login`.
|
||||||
|
|
||||||
|
Position yourself in the `blog` directory and run the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will build your blog into the `blog/dist` directory. Now you can deploy your blog to Netlify with the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
netlify deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Select the `dist` directory as the deploy path.
|
||||||
|
|
||||||
|
Finally, if the deployment looks good, you can deploy your blog to production with the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
netlify deploy --prod
|
||||||
|
```
|
||||||
|
|
||||||
97
opensaas-sh/blog/src/content/docs/guides/email-sending.mdx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
title: Email Sending
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
### The `Dummy` Email Provider (for Local Dev Only)
|
||||||
|
By default we've set up the email sender to use the `Dummy` provider. This is **for local development only** and no emails will actually be sent out!
|
||||||
|
To obtain an email verification token/link, you must check the server logs on initial sign up. You can click this link to verify your email and continue with the sign up process.
|
||||||
|
```tsx title="main.wasp"
|
||||||
|
app SaaSTemplate {
|
||||||
|
// ...
|
||||||
|
emailSender: {
|
||||||
|
provider: Dummy,
|
||||||
|
defaultFrom: {
|
||||||
|
name: "Open SaaS App",
|
||||||
|
email: "me@example.com"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that your app will not build if using the `Dummy` provider and you must switch to a production-ready provider in order to do so.
|
||||||
|
|
||||||
|
### Using a Production-Ready Email Provider (e.g. SendGrid)
|
||||||
|
To change your email provider to a production-ready one, such as SendGrid, you'll want to configure your `emailSender` like so:
|
||||||
|
|
||||||
|
```tsx title="main.wasp"
|
||||||
|
app SaaSTemplate {
|
||||||
|
// ...
|
||||||
|
emailSender: {
|
||||||
|
provider: SendGrid,
|
||||||
|
defaultFrom: {
|
||||||
|
name: "Open SaaS App",
|
||||||
|
// When using SendGrid, you must use the same email address that you configured your account to send out emails with!
|
||||||
|
email: "me@example.com"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
This means that you can send emails from your app using the `send` function from the `email` modul provided by Wasp:
|
||||||
|
|
||||||
|
```tsx title="src/server/webhooks.ts"
|
||||||
|
import { emailSender } from "wasp/server/email";
|
||||||
|
|
||||||
|
//...
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Integrate your email sender
|
||||||
|
|
||||||
|
To set up your email sender, you first need an account with one of the supported email providers.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="SendGrid">
|
||||||
|
- 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.
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="MailGun">
|
||||||
|
- 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=`.
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
Make sure to change the `defaultFrom` email address in the `main.wasp` file to use the same email address that you configured your account to send out emails with!
|
||||||
|
|
||||||
|
```tsx title="main.wasp" {5}
|
||||||
|
emailSender: {
|
||||||
|
provider: SendGrid,
|
||||||
|
defaultFrom: {
|
||||||
|
name: "Open SaaS App",
|
||||||
|
email: "me@example.com" // <--- same email address you configured your SendGrid account to send emails with!
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want more detailed info, or would like to use SMTP, check out the [Wasp docs](https://wasp-lang.dev/docs/advanced/email).
|
||||||
151
opensaas-sh/blog/src/content/docs/guides/file-uploading.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
---
|
||||||
|
title: File Uploading
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide will show you how to set up file uploading in your SaaS app.
|
||||||
|
|
||||||
|
There are two options we recommend:
|
||||||
|
1. Using [AWS S3](https://aws.amazon.com/s3/) with presigned URLS for secure file storage
|
||||||
|
2. Using Multer middleware to upload files to your own server
|
||||||
|
|
||||||
|
**We recommend using AWS S3 as it's a scalable, secure option, that can handle a large amount of storage.**
|
||||||
|
|
||||||
|
If you're just looking to upload small files and don't expect your app to grow to a large scale, you can use Multer to upload files to your app's server.
|
||||||
|
|
||||||
|
:::tip[Star our Repo on GitHub! 🌟]
|
||||||
|
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!
|
||||||
|
|
||||||
|
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Using AWS S3
|
||||||
|
|
||||||
|
### How presigned URLs work
|
||||||
|
|
||||||
|
Presigned URLs are URLs that have been signed with your AWS credentials and can be used to upload files to your S3 bucket. They are time-limited and can be generated on the server and sent to the client to upload files directly to S3.
|
||||||
|
|
||||||
|
The process of generating a presigned URL is as follows:
|
||||||
|
1. The client sends a request to the server to upload a file
|
||||||
|
2. The server generates a presigned URL using its AWS credentials
|
||||||
|
3. The server sends the presigned URL to the client
|
||||||
|
4. The client uses the presigned URL to upload the file directly to S3 before the URL expires
|
||||||
|
|
||||||
|
We use this method to upload files to S3 because it is more secure than uploading files directly from the client to S3. It also allows us to keep our AWS credentials private and not expose them to the client.
|
||||||
|
|
||||||
|
To use presigned URLs, we'll need to set up an S3 bucket and get our AWS credentials.
|
||||||
|
|
||||||
|
### Create an AWS Account
|
||||||
|
|
||||||
|
Before you begin, you'll need to create an AWS account. AWS accounts are free to create and are split up into:
|
||||||
|
1. Root account
|
||||||
|
2. IAM users
|
||||||
|
|
||||||
|
You'll need to first create a root account, and then an IAM user for your SaaS app before you can start uploading files to S3.
|
||||||
|
|
||||||
|
To do so, follow the steps in this external guide: [Creating IAM users and S3 buckets in AWS](https://medium.com/@emmanuelnwright/create-iam-users-and-s3-buckets-in-aws-264e78281f7f)
|
||||||
|
|
||||||
|
### Create an AWS S3 Bucket
|
||||||
|
|
||||||
|
Once you are logged in with your IAM user, you'll need to create an S3 bucket to store your files.
|
||||||
|
|
||||||
|
1. Navigate to the S3 service in the AWS console
|
||||||
|

|
||||||
|
2. Click on the `Create bucket` button
|
||||||
|

|
||||||
|
3. Fill in the bucket name and region
|
||||||
|
4. **Leave all the settings as default** and click `Create bucket`
|
||||||
|

|
||||||
|
|
||||||
|
### Change the CORS settings
|
||||||
|
|
||||||
|
Now we need to change some permissions on the bucket to allow for file uploads from your app.
|
||||||
|
|
||||||
|
1. Click on the bucket you just created
|
||||||
|

|
||||||
|
2. Click on the `Permissions` tab
|
||||||
|

|
||||||
|
3. Scroll down to the `Cross-origin resource sharing (CORS)` section and click `Edit`
|
||||||
|

|
||||||
|
5. Paste the following CORS configuration and click `Save changes`:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"AllowedHeaders": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"AllowedMethods": [
|
||||||
|
"PUT",
|
||||||
|
"GET"
|
||||||
|
],
|
||||||
|
"AllowedOrigins": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"ExposeHeaders": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get your AWS S3 credentials
|
||||||
|
|
||||||
|
Now that you have your S3 bucket set up, you'll need to get your S3 credentials to use in your app.
|
||||||
|
|
||||||
|
1. Click on your username in the top right corner of the AWS console and select `Security Credentials`
|
||||||
|

|
||||||
|
2. Scroll down to the `Access keys` section
|
||||||
|
3. Click on `Create Access Key`
|
||||||
|
4. Select the `Application running on an AWS service` option and create the access key
|
||||||
|

|
||||||
|
5. Copy the `Access key ID` and `Secret access key` and paste them in your `src/app/.env.server` file:
|
||||||
|
```sh
|
||||||
|
AWS_S3_IAM_ACCESS_KEY=ACK...
|
||||||
|
AWS_S3_IAM_SECRET_KEY=t+33a...
|
||||||
|
AWS_S3_FILES_BUCKET=your-bucket-name
|
||||||
|
AWS_S3_REGION=your-region // (e.g. us-west-2)
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip[Star our Repo on GitHub! 🌟]
|
||||||
|
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!
|
||||||
|
|
||||||
|
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Using and Customizing File Uploads with S3 in your App
|
||||||
|
|
||||||
|
With your S3 bucket set up and your AWS credentials in place, you can now start uploading files in your app using presigned URLs by navigating to `localhost:3000/file-upload` and uploading a file.
|
||||||
|
|
||||||
|
To begin customizing file uploads, is important to know where everything lives in your app. Here's a quick overview:
|
||||||
|
- `main.wasp`:
|
||||||
|
- The `File entity` can be found here. Here you can modify the fields to suit your needs:
|
||||||
|
```c
|
||||||
|
entity File {=psl
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
type String
|
||||||
|
key String
|
||||||
|
uploadUrl String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
userId Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
psl=}
|
||||||
|
```
|
||||||
|
- `src/server/actions.ts`:
|
||||||
|
- The `createFile` action lives here and calls the `getUploadFileSignedURLFromS3` within it using your AWS credentials before passing it to the client. This function stores the files in the S3 bucket within folders named after the user's ID, so that each user's files are stored separately.
|
||||||
|
- `src/server/queries.ts`:
|
||||||
|
- The `getAllFilesByUser` fetches all File information uploaded by the user. Note that the files do not exist in the app database, but rather the file data, name its `key`, which is used to fetch the file from S3
|
||||||
|
- The `getDownloadFileSignedURL` query fetches the presigned URL for a file to be downloaded from S3 using the file's `key` stored in the app's database
|
||||||
|
- `src/client/app/FileUploadPage.tsx`:
|
||||||
|
- The `FileUploadPage` component is where the file upload form lives. It also allows you to download the file from S3 by calling the `getDownloadFileSignedURL` based on that files `key` in the app DB.
|
||||||
|
|
||||||
|
## Using Multer to upload files to your server
|
||||||
|
|
||||||
|
If you're looking to upload files to the app server, you can use the Multer middleware to handle file uploads. This will allow you to store files on your server and is a good option if you need a quick and dirty, free solution for simple file uploads.
|
||||||
|
|
||||||
|
Below are GitHub Gists that show you how to set up file uploads using Multer in your app:
|
||||||
|
|
||||||
|
### Wasp version 0.12 & higher
|
||||||
|
|
||||||
|
<script src="https://gist.github.com/infomiho/ec379df4e33f3ae3410a251ba3aa81af.js"></script>
|
||||||
68
opensaas-sh/blog/src/content/docs/guides/seo.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: SEO
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
|
||||||
|
This guides explains how to improve SEO for of your app
|
||||||
|
|
||||||
|
## Landing Page Meta Tags
|
||||||
|
|
||||||
|
Wasp gives you the ability to add meta tags to your landing page HTML via the `main.wasp` file's `head` property:
|
||||||
|
|
||||||
|
```js {8-11}
|
||||||
|
app SaaSTemplate {
|
||||||
|
wasp: {
|
||||||
|
version: "^0.13.0"
|
||||||
|
},
|
||||||
|
title: "Open SaaS",
|
||||||
|
head: [
|
||||||
|
"<meta property='og:type' content='website' />",
|
||||||
|
"<meta property='og:url' content='https://opensaas.sh' />",
|
||||||
|
"<meta property='og:title' content='Open SaaS' />",
|
||||||
|
"<meta property='og:description' content='Free, open-source SaaS boilerplate starter for React & NodeJS.' />",
|
||||||
|
"<meta property='og:image' content='https://opensaas.sh/public-banner.png' />",
|
||||||
|
//...
|
||||||
|
],
|
||||||
|
//...
|
||||||
|
```
|
||||||
|
|
||||||
|
Change the above highlighted meta tags to match your app. Wasp will inject these tags into the HTML of your `index.html` file, which is the Landing Page (`app/src/client/landing-page/LandingPage.tsx`), in this case.
|
||||||
|
|
||||||
|
This means you **do not** need to rely on a seperate app or framework to serve your landing page for SEO purposes.
|
||||||
|
|
||||||
|
:::tip[Star our Repo on GitHub! 🌟]
|
||||||
|
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!
|
||||||
|
|
||||||
|
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Docs & Blog Meta Tags
|
||||||
|
|
||||||
|
Astro, being a static-site generator, will automatically inject relevant information provided in the `blog/astro.config.mjs` file, as well as in the frontmatter of `.md` files into the pages HTML:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: 'My First Blog Post'
|
||||||
|
pubDate: 2022-07-01
|
||||||
|
description: 'This is the first post of my new Astro blog.'
|
||||||
|
author: 'Astro Learner'
|
||||||
|
image:
|
||||||
|
url: 'https://docs.astro.build/assets/full-logo-light.png'
|
||||||
|
alt: 'The full Astro logo.'
|
||||||
|
tags: ["astro", "blogging", "learning in public"]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Improving your SEO is as simple as adding these properties to your docs and blog content!
|
||||||
|
|
||||||
|
## A Word on SSR & SEO
|
||||||
|
|
||||||
|
Open SaaS and Wasp do not currently have a SSR option (although it is coming soon!), but that does not mean that Open SaaS apps are at a disadvantage with regards to SEO.
|
||||||
|
|
||||||
|
That's because the meta tags for the landing page (described above), plus the Astro docs/blog provided with Open SaaS are more than enough! Not to mention, Google is also able to crawl websites with JavaScript activated, making SSR unnecessary.
|
||||||
|
|
||||||
|
For example, try searching "Open SaaS" on Google and you'll see this App, which was built with this template, as the first result!
|
||||||
|

|
||||||
108
opensaas-sh/blog/src/content/docs/guides/stripe-integration.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
title: Stripe Integration
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide will show you how to set up your Stripe account for testing and local development.
|
||||||
|
|
||||||
|
Once you deploy your app, you can follow the same steps, just make sure you're using your live Stripe API keys and product IDs and you are no longer in test mode within the Stripe Dashboard.
|
||||||
|
|
||||||
|
To get started, you'll need to create a Stripe account. You can do that [here](https://dashboard.stripe.com/register).
|
||||||
|
|
||||||
|
:::tip[Star our Repo on GitHub! 🌟]
|
||||||
|
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!
|
||||||
|
|
||||||
|
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Get your test Stripe API Keys
|
||||||
|
|
||||||
|
Once you've created your account, you'll need to get your test API keys. You can do that by navigating to [https://dashboard.stripe.com/test/apikeys](https://dashboard.stripe.com/test/apikeys) or by going to the [Stripe Dashboard](https://dashboard.stripe.com/test/dashboard) and clicking on the `Developers`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Click on the `Reveal test key token` button and copy the `Secret key`.
|
||||||
|
- Paste it in your `.env.server` file under `STRIPE_KEY=`
|
||||||
|
|
||||||
|
## Create Test Products
|
||||||
|
|
||||||
|
To create a test product, go to the test products url [https://dashboard.stripe.com/test/products](https://dashboard.stripe.com/test/products), or after navigating to your dashboard, click the `test mode` toggle.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Click on the `Add a product` button and fill in the relevant information for your product.
|
||||||
|
- Make sure you select `Software as a service (SaaS)` as the product type.
|
||||||
|
- For Subscription products, make sure you select `Recurring` as the billing type.
|
||||||
|
- For One-time payment products, make sure you select `One-time` as the billing type.
|
||||||
|
- If you want to add different price tiers for the same product, click the `Add another price` button at the buttom.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- After you save the product, you'll be directed to the product page.
|
||||||
|
- Copy the price IDs and paste them in the `.env.server` file
|
||||||
|
- We've set you up with two example subscription product environment variables, `HOBBY_SUBSCRIPTION_PRICE_ID=` and `PRO_SUBSCRIPTION_PRICE_ID=`.
|
||||||
|
- As well as a one-time payment product/credits-based environment variable, `CREDITS_PRICE_ID=`.
|
||||||
|
- Note that if you change the names of the price IDs, you'll need to update your server code to match these names as well
|
||||||
|
|
||||||
|
## Create a Test Customer
|
||||||
|
|
||||||
|
To create a test customer, go to the test customers url [https://dashboard.stripe.com/test/customers](https://dashboard.stripe.com/test/customers).
|
||||||
|
|
||||||
|
- Click on the `Add a customer` button and fill in the relevant information for your test customer.
|
||||||
|
:::note
|
||||||
|
When filling in the test customer email address, use an address you have access to and will use when logging into your SaaS app. This is important because the email address is used to identify the customer when creating a subscription and allows you to manage your test user's payments/subscriptions via the test customer portal
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Get your Customer Portal Link
|
||||||
|
|
||||||
|
Go to https://dashboard.stripe.com/test/settings/billing/portal in the Stripe Dashboard and activate and copy the `Customer portal link`. Paste it in your `.env.client` file:
|
||||||
|
|
||||||
|
```ts title=".env.client"
|
||||||
|
REACT_APP_STRIPE_CUSTOMER_PORTAL=<your-test-customer-portal-link>
|
||||||
|
```
|
||||||
|
|
||||||
|
Your Stripe customer portal link is imported into `src/client/app/AccountPage.tsx` and used to redirect users to the Stripe customer portal when they click the `Manage Subscription` button.
|
||||||
|
|
||||||
|
```tsx title="src/client/app/AccountPage.tsx" {5} "import.meta.env.REACT_APP_STRIPE_CUSTOMER_PORTAL"
|
||||||
|
function CustomerPortalButton() {
|
||||||
|
const handleClick = () => {
|
||||||
|
try {
|
||||||
|
const schema = z.string().url();
|
||||||
|
const customerPortalUrl = schema.parse(import.meta.env.REACT_APP_STRIPE_CUSTOMER_PORTAL);
|
||||||
|
window.open(customerPortalUrl, '_blank');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
:::danger[Client-side Environment Variables]
|
||||||
|
Client-side environment variables, unlike server-side environment variables, should never be used to store sensitive information as they are injected at build time and are exposed to the client.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Install the Stripe CLI
|
||||||
|
|
||||||
|
To install the Stripe CLI with homebrew, run the following command in your terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew install stripe/stripe-cli/stripe
|
||||||
|
```
|
||||||
|
|
||||||
|
or for other install scripts or OSes, follow the instructions [here](https://stripe.com/docs/stripe-cli#install).
|
||||||
|
|
||||||
|
Now, let's start the webhook server and get our webhook signing secret.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
stripe listen --forward-to localhost:3001/stripe-webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see a message like this:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
> Ready! You are using Stripe API Version [2023-08-16]. Your webhook signing secret is whsec_8a... (^C to quit)
|
||||||
|
```
|
||||||
|
|
||||||
|
copy this secret to your `.env.server` file under `STRIPE_WEBHOOK_SECRET=`.
|
||||||
78
opensaas-sh/blog/src/content/docs/guides/stripe-testing.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
title: Stripe Testing
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
This guide will show you how to test and try out your checkout, payments, and webhooks locally.
|
||||||
|
|
||||||
|
First, make sure you've set up your Stripe account for local development. You can find the guide [here](/guides/stripe-integration).
|
||||||
|
|
||||||
|
## Testing Webhooks via the Stripe CLI
|
||||||
|
|
||||||
|
- In a new terminal window, run the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
stripe login
|
||||||
|
```
|
||||||
|
|
||||||
|
- start the Stripe CLI webhook forwarding on port 3001 where your Node server is running.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
stripe listen --forward-to localhost:3001/stripe-webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
remember to copy and paste the outputted webhook signing secret (`whsec_...`) into your `.env.server` file under `STRIPE_WEBHOOK_SECRET=` if you haven't already.
|
||||||
|
|
||||||
|
- In another terminal window, trigger a test event:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
stripe trigger payment_intent.succeeded
|
||||||
|
```
|
||||||
|
|
||||||
|
The results of the event firing will be visible in the initial terminal window. You should see messages like this:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
...
|
||||||
|
2023-11-21 09:31:09 --> invoice.paid [evt_1OEpMPILOQf67J5TjrUgRpk4]
|
||||||
|
2023-11-21 09:31:09 <-- [200] POST http://localhost:3001/stripe-webhook [evt_1OEpMPILOQf67J5TjrUgRpk4]
|
||||||
|
2023-11-21 09:31:10 --> invoice.payment_succeeded [evt_1OEpMPILOQf67J5T3MFBr1bq]
|
||||||
|
2023-11-21 09:31:10 <-- [200] POST http://localhost:3001/stripe-webhook [evt_1OEpMPILOQf67J5T3MFBr1bq]
|
||||||
|
2023-11-21 09:31:10 --> checkout.session.completed [evt_1OEpMQILOQf67J5ThTZ0999r]
|
||||||
|
2023-11-21 09:31:11 <-- [200] POST http://localhost:3001/stripe-webhook [evt_1OEpMQILOQf67J5ThTZ0999r]
|
||||||
|
```
|
||||||
|
|
||||||
|
For more info on testing webhooks, check out https://stripe.com/docs/webhooks#test-webhook
|
||||||
|
|
||||||
|
:::tip[Star our Repo on GitHub! 🌟]
|
||||||
|
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!
|
||||||
|
|
||||||
|
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Testing Checkout and Payments via the Client
|
||||||
|
|
||||||
|
Make sure the **Stripe CLI is running** by following the steps above.
|
||||||
|
You can then test the payment flow via the client by doing the following:
|
||||||
|
|
||||||
|
- Click on a Buy button on the for any of the products on the homepage. You should be redirected to the checkout page.
|
||||||
|
- Fill in the form with the following test credit card number `4242 4242 4242 4242` and any future date for the expiration date and any 3 digits for the CVC.
|
||||||
|
|
||||||
|
- Click on the "Pay" button. You should be redirected to the success page.
|
||||||
|
|
||||||
|
- Check your terminal window for status messages and logs
|
||||||
|
|
||||||
|
- You can also check your Database via the DB Studio to see if the user entity has been updated by running:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wasp db studio
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Navigate to `localhost:5555` and click on the `users` table. You should see the `subscriptionStatus` is `active` for the user that just made the purchase.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
If you want to learn more about how a user's payment status, subscription status, and subscription tier affect a user's priveledges within the app, check out the [User Overview](/general/user-overview) reference.
|
||||||
|
:::
|
||||||
60
opensaas-sh/blog/src/content/docs/index.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: Introduction
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
|
||||||
|
## Welcome to your new SaaS App!
|
||||||
|
|
||||||
|
<!-- {/* TODO: add a screenshot of the app */} -->
|
||||||
|
|
||||||
|
You've decided to build a SaaS app with this template. Great choice! 🎉
|
||||||
|
|
||||||
|
This template is:
|
||||||
|
|
||||||
|
1. fully open-source
|
||||||
|
2. completely free to use and distribute
|
||||||
|
3. comes with a ton of features out of the box!
|
||||||
|
|
||||||
|
Check it out in action here: [OpenSaaS.sh](https://opensaas.sh)
|
||||||
|
Check out the Code: [Open SaaS GitHub Repo](https://github.com/wasp-lang/open-saas)
|
||||||
|
|
||||||
|
:::tip[FREE & OPEN-SOURCE!? 🌟]
|
||||||
|
That's right. Use this template however you like. No strings attached.
|
||||||
|
|
||||||
|
If you find this template useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp). It helps us to keep bringing you open-source software just like this!
|
||||||
|
:::
|
||||||
|
|
||||||
|
## What's inside?
|
||||||
|
|
||||||
|
The template itself is built on top of some very powerful tools and frameworks, including:
|
||||||
|
|
||||||
|
- 🐝 [Wasp](https://wasp-lang.dev) - a full-stack React, NodeJS, Prisma framework with superpowers
|
||||||
|
- 🚀 [Astro](https://starlight.astro.build/) - Astro's lightweight "Starlight" template for documentation and blog
|
||||||
|
- 💸 [Stripe](https://stripe.com) - for products and payments
|
||||||
|
- 📈 [Plausible](https://plausible.io) or [Google](https://analytics.google.com/) Analytics
|
||||||
|
- 🤖 [OpenAI](https://openai.com) - OpenAI API integrated into the app or [Replicate](https://replicate.com/) (coming soon 👀)
|
||||||
|
- 📦 [AWS S3](https://aws.amazon.com/s3/) - for file uploads
|
||||||
|
- 📧 [SendGrid](https://sendgrid.com), [MailGun](https://mailgun.com), or SMTP - for email sending
|
||||||
|
- 💅 [TailwindCSS](https://tailwindcss.com) - for styling
|
||||||
|
- 🧑💼 [TailAdmin](https://tailadmin.com/) - admin dashboard & components for TailwindCSS
|
||||||
|
|
||||||
|
Because we're using Wasp as the full-stack framework, we can leverage a lot of its features to build our SaaS in record time, including:
|
||||||
|
|
||||||
|
- 🔐 [Full-stack Authentication](https://wasp-lang.dev/docs/auth/overview) - Email verified + social Auth in a few lines of code.
|
||||||
|
- ⛑ [End-to-end Type Safety](https://wasp-lang.dev/docs/data-model/operations/overview) - Type your backend functions and get inferred types on the front-end automatically, without the need to install or configure any third-party libraries. Oh, and type-safe Links, too!
|
||||||
|
- 🤖 [Jobs](https://wasp-lang.dev/docs/advanced/jobs) - Run cron jobs in the background or set up queues simply by defining a function in the config file.
|
||||||
|
- 🚀 [One-command Deploy](https://wasp-lang.dev/docs/advanced/deployment/overview) - Easily deploy via the CLI to [Fly.io](https://fly.io), or to other providers like [Railway](https://railway.app) and [Netlify](https://netlify.com).
|
||||||
|
|
||||||
|
You also get access to Wasp's diverse, helpful community if you get stuck or need help.
|
||||||
|
- 🤝 [Wasp Discord](https://discord.gg/rzdnErX)
|
||||||
|
|
||||||
|
:::caution["Work In Progress"]
|
||||||
|
We've tried to get as many of the core features of a SaaS app into this template as possible, but there still might be some missing features or functionality.
|
||||||
|
|
||||||
|
We could always use some help tying up loose ends, so consider [contributing](https://github.com/wasp-lang/open-saas/blob/main/CONTRIBUTING.md)!
|
||||||
|
:::
|
||||||
|
|
||||||
|
In the next sections, we'll get our SaaS app started and tour its features. Let's get started!
|
||||||
227
opensaas-sh/blog/src/content/docs/start/getting-started.md
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
---
|
||||||
|
title: Getting Started
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide will help you get your new SaaS app up and running.
|
||||||
|
|
||||||
|
## Install Wasp
|
||||||
|
|
||||||
|
### Pre-requisites
|
||||||
|
|
||||||
|
You must have Node.js (and NPM) installed on your machine and available in `PATH` to use Wasp.
|
||||||
|
Your version of Node.js must be >= 18.
|
||||||
|
|
||||||
|
To switch easily between Node.js versions, we recommend using [nvm](https://github.com/nvm-sh/nvm).
|
||||||
|
|
||||||
|
:::note[Installing and using nvm]
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
Need help with nvm?
|
||||||
|
</summary>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
Install nvm via your OS package manager (`apt`, `pacman`, `homebrew`, ...) or via the [nvm](https://github.com/nvm-sh/nvm#install--update-script) install script.
|
||||||
|
|
||||||
|
Then, install a version of Node.js that you need:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nvm install 20
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, whenever you need to ensure a specific version of Node.js is used, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
nvm use 20
|
||||||
|
```
|
||||||
|
|
||||||
|
to set the Node.js version for the current shell session.
|
||||||
|
|
||||||
|
You can run
|
||||||
|
|
||||||
|
```shell
|
||||||
|
node -v
|
||||||
|
```
|
||||||
|
|
||||||
|
to check the version of Node.js currently being used in this shell session.
|
||||||
|
|
||||||
|
Check NVM repo for more details: https://github.com/nvm-sh/nvm.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
### Linux and macOS
|
||||||
|
|
||||||
|
Open your terminal and run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution[Bad CPU type in executable]
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
Are you getting this error on a Mac (Apple Silicon)?
|
||||||
|
</summary>
|
||||||
|
Given that the wasp binary is built for x86 and not for arm64 (Apple Silicon), you'll need to install <a href='https://support.apple.com/en-us/HT211861'>Rosetta on your Mac</a> if you are using a Mac with Mx (M1, M2, ...). Rosetta is a translation process that enables users to run applications designed for x86 on arm64 (Apple Silicon). To install Rosetta, run the following command in your terminal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
softwareupdate --install-rosetta
|
||||||
|
```
|
||||||
|
Once Rosetta is installed, you should be able to run Wasp without any issues.
|
||||||
|
:::
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
In order to use Wasp on Windows, you need to install WSL2 (Windows Subsystem for Linux) and a Linux distribution of your choice. We recommend using Ubuntu.
|
||||||
|
|
||||||
|
**You can refer to this [article](https://wasp-lang.dev/blog/2023/11/21/guide-windows-development-wasp-wsl) for a step by step guide to using Wasp in the WSL environment.** If you need further help, reach out to us on [Discord](https://discord.gg/rzdnErX).
|
||||||
|
|
||||||
|
Once in WSL2, run the following command in your **WSL2 environment**:
|
||||||
|
```sh
|
||||||
|
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution[WSL2 and file system issues]
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
Are you getting file system issues using WSL2?
|
||||||
|
</summary>
|
||||||
|
If you are using WSL2, make sure that your Wasp project is not on the Windows file system, <b>but instead on the Linux file system</b>. Otherwise, Wasp won't be able to detect file changes, due to this <a href='https://github.com/microsoft/WSL/issues/4739'>issue in WSL2</a>.
|
||||||
|
</details>
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Finalize Installation
|
||||||
|
|
||||||
|
Run the following command to verify that Wasp was installed correctly:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
wasp version
|
||||||
|
```
|
||||||
|
|
||||||
|
Also be sure to install the Wasp VSCode extension to get the best DX, e.g. syntax highlighting, code scaffolding, autocomplete, etc.
|
||||||
|
|
||||||
|
:::tip[Installing the Wasp VSCode Extension]
|
||||||
|
You can install the Wasp VSCode extension by searching for "Wasp" in the Extensions tab in VSCode, or by visiting the 🐝 [Wasp VSCode Extension](https://marketplace.visualstudio.com/items?itemName=wasp-lang.wasp) 🧑💻 homepage
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Setting up your SaaS app
|
||||||
|
|
||||||
|
### Cloning the OpenSaaS template
|
||||||
|
|
||||||
|
From the directory where you'd like to create your new project run:
|
||||||
|
```sh
|
||||||
|
wasp new
|
||||||
|
```
|
||||||
|
|
||||||
|
Then select option `[3] saas` from the list of templates after entering the name of your project.
|
||||||
|
|
||||||
|
This will clone a **clean copy of the Open SaaS template** into a new directory! 🎉
|
||||||
|
|
||||||
|
### Start your DB
|
||||||
|
|
||||||
|
Before you start your app, you need to have a Postgres Database connected and running. With Wasp, that's super easy!
|
||||||
|
|
||||||
|
First, make sure you have **Docker installed and running**. If not, download and install it [here](https://www.docker.com/products/docker-desktop/)
|
||||||
|
|
||||||
|
With Docker running, open a new terminal window/tab and position yourself in the `app` directory:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd app
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wasp start db
|
||||||
|
```
|
||||||
|
|
||||||
|
This will start and connect your app to a Postgres database for you. No need to do anything else! 🤯 Just make sure to leave this terminal window open in the background while developing. Once you terminate the process, your DB will no longer be available to your app.
|
||||||
|
|
||||||
|
Now let's create our very first database migration, to ensure the database has a correct schema. Open a new terminal tab/window and run the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wasp db migrate-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This might take a bit since this is the first time you are running it and it needs to install all the
|
||||||
|
dependencies for your Wasp project.
|
||||||
|
|
||||||
|
In the future, you will also want to run `wasp db migrate-dev` whenever you make changes to your Prisma schema (Entities),
|
||||||
|
to apply those schema changes to the database.
|
||||||
|
|
||||||
|
Additionally, if you want to see or manage your DB via Prisma's DB Studio GUI, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wasp db studio
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start your app
|
||||||
|
|
||||||
|
At this point, you should be positioned in the `app/` directory and have the database running in another terminal session.
|
||||||
|
|
||||||
|
Next, copy the `.env.server.example` file to `.env.server`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp .env.server.example .env.server
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env.server` is where API keys for services like Stripe, email sender, and similar go, and this is where you will want to put them in later.
|
||||||
|
For now, you can leave it as it is (dummy API keys), this will be enough to run the app.
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wasp start
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install all the dependencies and start the app (client and server) for you :)!
|
||||||
|
|
||||||
|
If the app doesn't open automatically in your browser, you can open it manually by visiting `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
|
At this point, you should have:
|
||||||
|
- your database running in one terminal session, likely on port `5432`.
|
||||||
|
- your app running in another terminal session, the client likely on port `3000`, and the server likely on port `3001`.
|
||||||
|
|
||||||
|
#### Run Blog and Docs
|
||||||
|
|
||||||
|
This SaaS app comes with a docs and blog section built with the [Starlight template on top of the Astro](https://starlight.astro.build) framework. You can use this as a starting point for your own blog and documentation, if necessary.
|
||||||
|
|
||||||
|
If you do not need this, you can simply delete the `blog` folder from the root of the project.
|
||||||
|
|
||||||
|
If you want to run the Starlight docs and blog, first navigate to the `blog` folder:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd ../blog
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Then start the development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the instructions in the terminal for the link to open the blog, it will typically be `https://localhost:4321/`.
|
||||||
|
|
||||||
|
## What's next?
|
||||||
|
|
||||||
|
Awesome! We have our new app ready and we know how to run both it and the blog/docs! Now, in the next section, we'll give you a quick "guided tour" of the different parts of the app we created and understand how it works.
|
||||||
|
|
||||||
|
:::tip[Star our Repo on GitHub! 🌟]
|
||||||
|
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!
|
||||||
|
|
||||||
|
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||||
|
:::
|
||||||
265
opensaas-sh/blog/src/content/docs/start/guided-tour.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
---
|
||||||
|
title: Guided Tour
|
||||||
|
banner:
|
||||||
|
content: |
|
||||||
|
⚠️ Open SaaS is now running on <a href='https://wasp-lang.dev'>Wasp v0.13</a>! If you're running an older version of Open SaaS, please follow the
|
||||||
|
<a href="https://wasp-lang.dev/docs/migrate-from-0-12-to-0-13">migration instructions here</a> ⚠️
|
||||||
|
---
|
||||||
|
|
||||||
|
Let's get to know our new SaaS app.
|
||||||
|
|
||||||
|
First, we'll take a look at the project's file structure, then dive into its main features and how you can get started customizing them.
|
||||||
|
|
||||||
|
:::caution[HOLD UP! ✋]
|
||||||
|
|
||||||
|
If you haven't already, now would be the right time to [explore our demo app](https://opensaas.sh) in your browser:
|
||||||
|
- [ ] explore the landing page
|
||||||
|
- [ ] log in to the demo app
|
||||||
|
- [ ] make a test purchase
|
||||||
|
- [ ] check out the admin dashboard
|
||||||
|
- [ ] check out your account settings
|
||||||
|
- [ ] check out the blog
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Getting acquainted with the codebase
|
||||||
|
Now that you've gotten a feel for the app and how it works, let's dive into the codebase.
|
||||||
|
|
||||||
|
At the root of our project, you will see two folders:
|
||||||
|
```sh
|
||||||
|
.
|
||||||
|
├── app
|
||||||
|
└── blog
|
||||||
|
```
|
||||||
|
|
||||||
|
`app` contains the Wasp project files, which is your full-stack React + NodeJS + Prisma app along with a Wasp config file, `main.wasp`, which will be explained in more detail below.
|
||||||
|
|
||||||
|
`blog` contains the [Astro Starlight template](https://starlight.astro.build/) for the blog and documentation section.
|
||||||
|
|
||||||
|
Let's check out what's in the `app` folder in more detail:
|
||||||
|
|
||||||
|
:::caution[v0.11 and below]
|
||||||
|
If you are using a version of the OpenSaaS template with Wasp `v0.11.x` or below, you may see a slightly different file structure. But don't worry, the vast majority of the code and features are the same! 😅
|
||||||
|
:::
|
||||||
|
|
||||||
|
```sh
|
||||||
|
.
|
||||||
|
├── main.wasp # Wasp Config file. You define your app structure here.
|
||||||
|
├── .wasp/ # Output dir for Wasp. DON'T MODIFY THESE FILES!
|
||||||
|
├── public/ # Public assets dir, e.g. www.yourdomain.com/banner.png
|
||||||
|
├── src/ # Your code goes here.
|
||||||
|
│ ├── client/ # Your client code (React) goes here.
|
||||||
|
│ ├── server/ # Your server code (NodeJS) goes here.
|
||||||
|
│ ├── shared/ # Your shared (runtime independent) code goes here.
|
||||||
|
│ └── .waspignore
|
||||||
|
├── .env.server # Dev environment variables for your server code.
|
||||||
|
├── .env.client # Dev environment variables for your client code.
|
||||||
|
├── .prettierrc # Prettier configuration.
|
||||||
|
├── tailwind.config.js # TailwindCSS configuration.
|
||||||
|
├── package.json
|
||||||
|
├── package-lock.json
|
||||||
|
└── .wasproot
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip[File Structure]
|
||||||
|
Note that since Wasp v0.12, the `src` folder does not need to be organized between `client` and `server` code. You can organize your code however you like, e.g. by feature, but we've chosen to keep the traditional structure for this template.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### The Wasp Config file
|
||||||
|
|
||||||
|
This template at its core is a Wasp project, where [Wasp](https://wasp-lang.dev) is a full-stack web app framework that let’s you write your app in React, NodeJS, and Prisma and will manage the "boilerplatey" work for you, allowing you to just take care of the fun stuff!
|
||||||
|
|
||||||
|
[Wasp's secret sauce](https://wasp-lang.dev/docs) is its use of a config file (`main.wasp`) and compiler which takes your code and outputs the client app, server app and deployment code for you.
|
||||||
|
|
||||||
|
In this template, we've already defined a number of things in the `main.wasp` config file, including:
|
||||||
|
|
||||||
|
- Auth
|
||||||
|
- Routes and Pages
|
||||||
|
- Prisma Database Models
|
||||||
|
- Operations (data read and write functions)
|
||||||
|
- Background Jobs
|
||||||
|
- Email Sending
|
||||||
|
|
||||||
|
By defining these things in the config file, Wasp continuously handles the boilerplate necessary with putting all these features together. You just need to focus on the business logic of your app.
|
||||||
|
|
||||||
|
Wasp abstracts away some things that you would normally be used to doing during development, so don't be surprised if you don't see some of the things you're used to seeing.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
It's possible to learn Wasp's feature set simply through using this template, but if you find yourself unsure how to implement a Wasp-specific feature and/or just want to learn more, a great starting point is the intro tutorial in the [Wasp docs](https://wasp-lang.dev/docs) which takes ~20 minutes.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Client
|
||||||
|
|
||||||
|
The `src/client` folder contains all the code that runs in the browser. It's a standard React app, with a few Wasp-specific things sprinkled in.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
.
|
||||||
|
└── client
|
||||||
|
├── admin # Admin dashboard pages and components
|
||||||
|
├── app # Your user-facing app that sits behind the paywall/login.
|
||||||
|
├── auth # All auth-related pages and components.
|
||||||
|
├── components # Your shared React components.
|
||||||
|
├── hooks # Your shared React hooks.
|
||||||
|
├── landing-page # Landing page related code
|
||||||
|
├── static # Assets that you need access to in your code, e.g. import logo from 'static/logo.png'
|
||||||
|
├── App.tsx # Main app component to wrap all child components. Useful for global state, navbars, etc.
|
||||||
|
└── Main.css
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
The `src/server` folder contains all the code that runs on the server. Wasp compiles everything into a NodeJS server for you.
|
||||||
|
|
||||||
|
All you have to do is define your server-side functions in the `main.wasp` file, write the logic in a function within `src/server` and Wasp will generate the boilerplate code for you.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
└── server
|
||||||
|
├── auth # Some small auth-related functions to customize the auth flow.
|
||||||
|
├── file-upload # File upload utility functions.
|
||||||
|
├── payments # Payments utility functions.
|
||||||
|
├── scripts # Scripts to run via Wasp, e.g. database seeding.
|
||||||
|
├── webhooks # The webhook handler for Stripe.
|
||||||
|
├── workers # Functions that run in the background as Wasp Jobs, e.g. daily stats calculation.
|
||||||
|
├── actions.ts # Your server-side write/mutation functions.
|
||||||
|
├── queries.ts # Your server-side read functions.
|
||||||
|
└── types.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main Features
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
This template comes with a fully functional auth flow out of the box. It takes advantages of Wasp's built-in [Auth features](https://wasp-lang.dev/docs/auth/overview), which do the dirty work of rolling your own full-stack auth for you!
|
||||||
|
|
||||||
|
```js title="main.wasp"
|
||||||
|
auth: {
|
||||||
|
userEntity: User,
|
||||||
|
methods: {
|
||||||
|
email: {
|
||||||
|
//...
|
||||||
|
},
|
||||||
|
google: {},
|
||||||
|
github: {},
|
||||||
|
},
|
||||||
|
onAuthFailedRedirectTo: "/",
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
By defining the auth structure in your `main.wasp` file, Wasp manages all the necessary code for you, including:
|
||||||
|
- Email verified login with reset password
|
||||||
|
- Social login with Google and/or GitHub
|
||||||
|
- Auth-related databse entities for user credentials, sessions, and social logins
|
||||||
|
- Custom-generated AuthUI components for login, signup, and reset password
|
||||||
|
- Auth hooks for fetching user data
|
||||||
|
|
||||||
|
<!-- TODO: add pic of AuthUI components -->
|
||||||
|
|
||||||
|
We've set the template up with Wasp's `email`, `google`, and `gitHub` methods, which are all battle-tested and suitable for production.
|
||||||
|
|
||||||
|
You can get started developing your app with the `email` method right away!
|
||||||
|
|
||||||
|
:::caution[Dummy Email Provider]
|
||||||
|
Note that the `email` method relies on an `emailSender` (configured at `app.emailSender` in the `main.wasp` file), a service which sends emails to verify users and reset passwords.
|
||||||
|
|
||||||
|
For development purposes, Wasp provides a `Dummy` email sender which Open SaaS comes with as the default. This provider *does not* actually send any confirmation emails to the specified email address, but instead logs all email verification links/tokens to the console! You can then follow these links to verify the user and continue with the sign-up process.
|
||||||
|
|
||||||
|
```tsx title="main.wasp"
|
||||||
|
emailSender: {
|
||||||
|
provider: Dummy, // logs all email verification links/tokens to the server's console
|
||||||
|
defaultFrom: {
|
||||||
|
name: "Open SaaS App",
|
||||||
|
email: "me@example.com"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
We will explain more about these auth methods, and how to properly integrate them into your app, in the [Authentication Guide](/guides/authentication).
|
||||||
|
|
||||||
|
### Subscription Payments with Stripe
|
||||||
|
|
||||||
|
No SaaS is complete without payments, specifically subscription payments. That's why this template comes with a fully functional Stripe integration.
|
||||||
|
|
||||||
|
Let's take a quick look at how payments are handled in this template.
|
||||||
|
|
||||||
|
1. a user clicks the `BUY` button and a **Stripe Checkout session** is created on the server
|
||||||
|
2. the user is redirected to the Stripe Checkout page where they enter their payment info
|
||||||
|
3. the user is redirected back to the app and the Stripe Checkout session is completed
|
||||||
|
4. Stripe sends a webhook event to the server with the payment info
|
||||||
|
5. The app server's **webhook handler** handles the event and updates the user's subscription status
|
||||||
|
|
||||||
|
The logic for creating the Stripe Checkout session is defined in the `src/server/actions.ts` file. [Actions](https://wasp-lang.dev/docs/data-model/operations/actions) are your server-side functions that are used to write or update data to the database. Once they're defined in the `main.wasp` file, you can easily call them on the client-side:
|
||||||
|
|
||||||
|
a) define the action in the `main.wasp` file
|
||||||
|
```js title="main.wasp"
|
||||||
|
action stripePayment {
|
||||||
|
fn: import { stripePayment } from "@src/server/actions.js",
|
||||||
|
entities: [User]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
b) implement the action in the `src/server/actions.ts` file
|
||||||
|
```js title="src/server/actions.ts"
|
||||||
|
export const stripePayment = async (tier, context) => {
|
||||||
|
//...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
c) call the action on the client-side
|
||||||
|
```js title="src/client/app/SubscriptionPage.tsx"
|
||||||
|
import { stripePayment } from "wasp/client/operations";
|
||||||
|
|
||||||
|
const handleBuyClick = async (tierId) => {
|
||||||
|
const stripeResults = await stripePayment(tierId);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The webhook handler is defined in the `src/server/webhooks/stripe.ts` file. Unlike Actions and Queries in Wasp which are only to be used internally, we define the webhook handler in the `main.wasp` file as an API endpoint in order to expose it externally to Stripe
|
||||||
|
|
||||||
|
```js title="main.wasp"
|
||||||
|
api stripeWebhook {
|
||||||
|
fn: import { stripeWebhook } from "@src/server/webhooks/stripe.js",
|
||||||
|
httpRoute: (POST, "/stripe-webhook")
|
||||||
|
entities: [User],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Within the webhook handler, we look for specific events that Stripe sends us to let us know which payment was completed and for which user. Then we update the user's subscription status in the database.
|
||||||
|
|
||||||
|
To learn more about configuring the app to handle your products and payments, check out the [Stripe Integration guide](/guides/stripe-integration).
|
||||||
|
|
||||||
|
:::tip[Star our Repo on GitHub! 🌟]
|
||||||
|
We've packed in a ton of features and love into this SaaS starter, and offer it all to you for free!
|
||||||
|
|
||||||
|
If you're finding this template and its guides useful, consider giving us [a star on GitHub](https://github.com/wasp-lang/wasp)
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
### Analytics and Admin Dashboard
|
||||||
|
|
||||||
|
Keeping an eye on your metrics is crucial for any SaaS. That's why we've built an administrator's dashboard where you can view your app's stats, user data, and Stripe revenue all in one place.
|
||||||
|
|
||||||
|
<!-- TODO: add pic of admin dash -->
|
||||||
|
|
||||||
|
To do that, we've leveraged Wasp's [Jobs feature](https://wasp-lang.dev/docs/advanced/jobs) to run a cron job that calculates your daily stats. The app stats, such as page views and sources, can be pulled from either Plausible or Google Analytics. All you have to do is create a project with the analytics provider of your choice and import the respective pre-built helper functions!
|
||||||
|
|
||||||
|
```js title="main.wasp"
|
||||||
|
job dailyStatsJob {
|
||||||
|
executor: PgBoss,
|
||||||
|
perform: {
|
||||||
|
fn: import { calculateDailyStats } from "@src/server/workers/calculateDailyStats.js"
|
||||||
|
},
|
||||||
|
schedule: {
|
||||||
|
cron: "0 * * * *" // runs every hour
|
||||||
|
},
|
||||||
|
entities: [User, DailyStats, Logs, PageViewSource]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For more info on integrating Plausible or Google Analytics, check out the [Analytics guide](/guides/analytics).
|
||||||
|
|
||||||
|
## What's next?
|
||||||
|
|
||||||
|
And that concludes our guided tour! For next steps, we recommend ...
|
||||||
7
opensaas-sh/tools/diff.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
TOOLS_DIR=$(dirname "$(realpath "$0")") # Assumes this script is in `tools/`.
|
||||||
|
cd "${TOOLS_DIR}" && cd ../..
|
||||||
|
|
||||||
|
rm -rf opensaas-sh/app_diff
|
||||||
|
"${TOOLS_DIR}/dope.sh" template/app opensaas-sh/app diff
|
||||||
116
opensaas-sh/tools/dope.sh
Executable file
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Diff Or Patch Executor
|
||||||
|
#
|
||||||
|
# Allows you to easily create a diff between the two projects (base and derived), or to patch those diffs onto the base project to get the derived one.
|
||||||
|
# Useful when derived project has only small changes on top of base project and you want to keep it in a dir in the same repo as main project.
|
||||||
|
|
||||||
|
list_source_files() {
|
||||||
|
local dir=$1
|
||||||
|
(cd "${dir}" && git ls-files --cached --others --exclude-standard | sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the required arguments are provided
|
||||||
|
if [ "$#" -ne 3 ]; then
|
||||||
|
echo "Usage: $0 <BASE_DIR> <DERIVED_DIR> <ACTION>"
|
||||||
|
echo "<ACTION> should be either 'diff' to get the diff between the specified dirs or 'patch' to apply such existing diff onto base dir."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BASE_DIR=$1
|
||||||
|
DERIVED_DIR=$2
|
||||||
|
ACTION=$3
|
||||||
|
|
||||||
|
DIFF_DIR="${DERIVED_DIR}_diff"
|
||||||
|
DIFF_DIR_DELETIONS="${DIFF_DIR}/deletions"
|
||||||
|
|
||||||
|
recreate_diff_dir() {
|
||||||
|
mkdir -p "${DIFF_DIR}"
|
||||||
|
|
||||||
|
local BASE_FILES
|
||||||
|
BASE_FILES=$(list_source_files "${BASE_DIR}")
|
||||||
|
local DERIVED_FILES
|
||||||
|
DERIVED_FILES=$(list_source_files "${DERIVED_DIR}")
|
||||||
|
|
||||||
|
while IFS= read -r filepath; do
|
||||||
|
mkdir -p "${DIFF_DIR}/$(dirname "${filepath}")"
|
||||||
|
local DIFF_OUTPUT
|
||||||
|
local baseFilepath="${BASE_DIR}/${filepath}"
|
||||||
|
local derivedFilepath="${DERIVED_DIR}/${filepath}"
|
||||||
|
DIFF_OUTPUT=$(diff -Nu --label "${baseFilepath}" --label "${derivedFilepath}" "${baseFilepath}" "${derivedFilepath}")
|
||||||
|
if [ $? -eq 1 ]; then
|
||||||
|
echo "${DIFF_OUTPUT}" > "${DIFF_DIR}/${filepath}.diff"
|
||||||
|
fi
|
||||||
|
done <<< "${DERIVED_FILES}"
|
||||||
|
|
||||||
|
local FILES_ONLY_IN_BASE
|
||||||
|
FILES_ONLY_IN_BASE=$(comm -23 <(echo "${BASE_FILES}") <(echo "${DERIVED_FILES}"))
|
||||||
|
echo "${FILES_ONLY_IN_BASE}" > "${DIFF_DIR_DELETIONS}"
|
||||||
|
|
||||||
|
echo "DONE: generated ${DIFF_DIR}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
RED_COLOR='\033[0;31m'
|
||||||
|
GREEN_COLOR='\033[0;32m'
|
||||||
|
RESET_COLOR='\033[0m'
|
||||||
|
|
||||||
|
recreate_derived_dir() {
|
||||||
|
mkdir -p "${DERIVED_DIR}"
|
||||||
|
|
||||||
|
local BASE_FILES
|
||||||
|
BASE_FILES=$(list_source_files "${BASE_DIR}")
|
||||||
|
|
||||||
|
while IFS= read -r filepath; do
|
||||||
|
mkdir -p "${DERIVED_DIR}/$(dirname ${filepath})"
|
||||||
|
cp "${BASE_DIR}/${filepath}" "${DERIVED_DIR}/${filepath}"
|
||||||
|
done <<< "${BASE_FILES}"
|
||||||
|
|
||||||
|
find "${DIFF_DIR}" -name "*.diff" | while IFS= read -r diff_filepath; do
|
||||||
|
local derived_filepath
|
||||||
|
derived_filepath="${diff_filepath#${DIFF_DIR}/}"
|
||||||
|
derived_filepath="${derived_filepath%.diff}"
|
||||||
|
|
||||||
|
local patch_output
|
||||||
|
local patch_exit_code
|
||||||
|
patch_output=$(patch --no-backup-if-mismatch --merge "${DERIVED_DIR}/${derived_filepath}" < "${diff_filepath}")
|
||||||
|
patch_exit_code=$?
|
||||||
|
if [ ${patch_exit_code} -eq 0 ]; then
|
||||||
|
echo "${patch_output}"
|
||||||
|
echo -e "${GREEN_COLOR}[OK]${RESET_COLOR}"
|
||||||
|
else
|
||||||
|
echo "${patch_output}"
|
||||||
|
echo -e "${RED_COLOR}[Failed with exit code ${patch_exit_code}]${RESET_COLOR}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# TODO: also allow deletion of dirs.
|
||||||
|
if [ -f "${DIFF_DIR_DELETIONS}" ]; then
|
||||||
|
while IFS= read -r filepath; do
|
||||||
|
local derived_dir_filepath
|
||||||
|
local rm_exit_code
|
||||||
|
derived_dir_filepath="${DERIVED_DIR}/${filepath}"
|
||||||
|
rm "${derived_dir_filepath}"
|
||||||
|
rm_exit_code=$?
|
||||||
|
if [ ${rm_exit_code} -eq 0 ]; then
|
||||||
|
echo "Deleted ${derived_dir_filepath}"
|
||||||
|
echo -e "${GREEN_COLOR}[OK]${RESET_COLOR}"
|
||||||
|
else
|
||||||
|
echo "Failed to delete ${derived_dir_filepath}"
|
||||||
|
echo -e "${RED_COLOR}[Failed with exit code ${rm_exit_code}]${RESET_COLOR}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
done < "${DIFF_DIR_DELETIONS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "DONE: generated ${DERIVED_DIR}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$ACTION" == "diff" ]; then
|
||||||
|
recreate_diff_dir
|
||||||
|
elif [ "$ACTION" == "patch" ]; then
|
||||||
|
recreate_derived_dir
|
||||||
|
else
|
||||||
|
echo "Invalid action specified. Use 'diff' to get a diff between specified dirs or 'patch' to patch the existing diff onto base dir."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
9
opensaas-sh/tools/patch.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
TOOLS_DIR=$(dirname "$(realpath "$0")") # Assumes this script is in `tools/`.
|
||||||
|
cd "${TOOLS_DIR}" && cd ../..
|
||||||
|
|
||||||
|
# Removes all files except for some gitignored files that we don't want to bother regenerating each time,
|
||||||
|
# like node_modules and certain .env files.
|
||||||
|
find opensaas-sh/app -mindepth 1 \( -path node_modules -o -name .env.server -o -name .env.me \) -prune -o -exec rm -rf {} +
|
||||||
|
"${TOOLS_DIR}/dope.sh" template/app opensaas-sh/app patch
|
||||||
8
template/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# <YOUR_APP_NAME>
|
||||||
|
|
||||||
|
This project is based on [OpenSaas](https://opensaas.sh) template and consists of three main dirs:
|
||||||
|
1. `app` - Your web app, built with [Wasp](https://wasp-lang.dev).
|
||||||
|
2. `e2e-tests` - [Playwright](https://playwright.dev/) tests for your Wasp web app.
|
||||||
|
3. `blog` - Your blog / docs, built with [Astro](https://docs.astro.build) based on [Starlight](https://starlight.astro.build/) template.
|
||||||
|
|
||||||
|
For more details, check READMEs of each respective directory!
|
||||||
17
template/app/.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.wasp/
|
||||||
|
node_modules/
|
||||||
|
**/.DS_Store
|
||||||
|
|
||||||
|
# We ignore env files recognized and used by Wasp.
|
||||||
|
.env.server
|
||||||
|
.env.client
|
||||||
|
|
||||||
|
# To be extra safe, we by default ignore any files with `.env` extension in them.
|
||||||
|
# If this is too agressive for you, consider allowing specific files with `!` operator,
|
||||||
|
# or modify/delete these two lines.
|
||||||
|
*.env
|
||||||
|
*.env.*
|
||||||
|
|
||||||
|
# We don't want to ignore .env example files.
|
||||||
|
!*.env.*.example
|
||||||
|
!*.env.example
|
||||||
12
template/app/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# <YOUR_APP_NAME>
|
||||||
|
|
||||||
|
Built with [Wasp](https://wasp-lang.dev), based on the [Open Saas](https://opensaas.sh) template.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running locally
|
||||||
|
- Make sure you have the `.env.client` and `.env.server` files with correct dev values in the root of the project.
|
||||||
|
- Run the database with `wasp start db` and leave it running.
|
||||||
|
- Run `wasp start` and leave it running.
|
||||||
|
- [OPTIONAL]: If this is the first time starting the app, or you've just made changes to your entities/prisma schema, also run `wasp db migrate-dev`.
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 840 KiB After Width: | Height: | Size: 840 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 246 KiB |