mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-09-11 15:26:19 +03:00
Merge branch 'filip-restructuring-prototype' into filip-project-install-step
This commit is contained in:
commit
f85daea1fc
@ -383,6 +383,7 @@ If it happens just once every so it is probably nothing to worry about. If it ha
|
|||||||
- Merge `release` back into `main` (`git merge release` while on the `main` branch), if needed.
|
- Merge `release` back into `main` (`git merge release` while on the `main` branch), if needed.
|
||||||
- Publish new [docs](/web#deployment) from the `release` branch as well.
|
- Publish new [docs](/web#deployment) from the `release` branch as well.
|
||||||
- Publish new [Mage](/mage#deployment) from the `release` branch as well, if needed.
|
- Publish new [Mage](/mage#deployment) from the `release` branch as well, if needed.
|
||||||
|
- Publish new [Wasp VSCode extension](https://github.com/wasp-lang/vscode-wasp), if needed.
|
||||||
- Announce new release in Discord.
|
- Announce new release in Discord.
|
||||||
|
|
||||||
#### Determining next version
|
#### Determining next version
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
"./core/HttpError": "./core/HttpError.js",
|
"./core/HttpError": "./core/HttpError.js",
|
||||||
|
"./core/AuthError": "./core/AuthError.js",
|
||||||
"./core/config": "./core/config.js",
|
"./core/config": "./core/config.js",
|
||||||
"./core/stitches.config": "./core/stitches.config.js",
|
"./core/stitches.config": "./core/stitches.config.js",
|
||||||
"./core/storage": "./core/storage.ts",
|
"./core/storage": "./core/storage.ts",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
|
||||||
import HttpError from './core/HttpError.js'
|
import HttpError from 'wasp/core/HttpError'
|
||||||
import indexRouter from './routes/index.js'
|
import indexRouter from './routes/index.js'
|
||||||
|
|
||||||
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
|
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
import { ensureValidEmail } from "../../validation.js";
|
import { ensureValidEmail } from "../../validation.js";
|
||||||
import type { EmailFromField } from '../../../email/core/types.js';
|
import type { EmailFromField } from '../../../email/core/types.js';
|
||||||
import { GetPasswordResetEmailContentFn } from './types.js';
|
import { GetPasswordResetEmailContentFn } from './types.js';
|
||||||
import HttpError from '../../../core/HttpError.js';
|
import HttpError from 'wasp/core/HttpError'
|
||||||
|
|
||||||
export function getRequestPasswordResetRoute({
|
export function getRequestPasswordResetRoute({
|
||||||
fromField,
|
fromField,
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from "../../utils.js";
|
} from "../../utils.js";
|
||||||
import { ensureTokenIsPresent, ensurePasswordIsPresent, ensureValidPassword } from "../../validation.js";
|
import { ensureTokenIsPresent, ensurePasswordIsPresent, ensureValidPassword } from "../../validation.js";
|
||||||
import { tokenVerificationErrors } from "./types.js";
|
import { tokenVerificationErrors } from "./types.js";
|
||||||
import HttpError from '../../../core/HttpError.js';
|
import HttpError from 'wasp/core/HttpError';
|
||||||
|
|
||||||
export async function resetPassword(
|
export async function resetPassword(
|
||||||
req: Request<{ token: string; password: string; }>,
|
req: Request<{ token: string; password: string; }>,
|
||||||
|
@ -18,7 +18,7 @@ import {
|
|||||||
import { ensureValidEmail, ensureValidPassword, ensurePasswordIsPresent } from "../../validation.js";
|
import { ensureValidEmail, ensureValidPassword, ensurePasswordIsPresent } from "../../validation.js";
|
||||||
import { GetVerificationEmailContentFn } from './types.js';
|
import { GetVerificationEmailContentFn } from './types.js';
|
||||||
import { validateAndGetUserFields } from '../../utils.js'
|
import { validateAndGetUserFields } from '../../utils.js'
|
||||||
import HttpError from '../../../core/HttpError.js';
|
import HttpError from 'wasp/core/HttpError';
|
||||||
import { type UserSignupFields } from '../types.js';
|
import { type UserSignupFields } from '../types.js';
|
||||||
|
|
||||||
export function getSignupRoute({
|
export function getSignupRoute({
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
deserializeAndSanitizeProviderData,
|
deserializeAndSanitizeProviderData,
|
||||||
} from '../../utils.js';
|
} from '../../utils.js';
|
||||||
import { tokenVerificationErrors } from './types.js';
|
import { tokenVerificationErrors } from './types.js';
|
||||||
import HttpError from '../../../core/HttpError.js';
|
import HttpError from 'wasp/core/HttpError';
|
||||||
|
|
||||||
|
|
||||||
export async function verifyEmail(
|
export async function verifyEmail(
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { hashPassword } from './password.js'
|
import { hashPassword } from './password.js'
|
||||||
import { verify } from './jwt.js'
|
import { verify } from './jwt.js'
|
||||||
import AuthError from '../core/AuthError.js'
|
import AuthError from 'wasp/core/AuthError'
|
||||||
import HttpError from '../core/HttpError.js'
|
import HttpError from 'wasp/core/HttpError'
|
||||||
import prisma from '../dbClient.js'
|
import prisma from '../dbClient.js'
|
||||||
import { sleep } from '../utils.js'
|
import { sleep } from '../utils.js'
|
||||||
import {
|
import {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import HttpError from '../core/HttpError.js';
|
import HttpError from 'wasp/core/HttpError'
|
||||||
|
|
||||||
export const PASSWORD_FIELD = 'password';
|
export const PASSWORD_FIELD = 'password';
|
||||||
const USERNAME_FIELD = 'username';
|
const USERNAME_FIELD = 'username';
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
class AuthError extends Error {
|
|
||||||
constructor (message, data, ...params) {
|
|
||||||
super(message, ...params)
|
|
||||||
|
|
||||||
if (Error.captureStackTrace) {
|
|
||||||
Error.captureStackTrace(this, AuthError)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.name = this.constructor.name
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
this.data = data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AuthError
|
|
@ -1,22 +0,0 @@
|
|||||||
class HttpError extends Error {
|
|
||||||
constructor (statusCode, message, data, ...params) {
|
|
||||||
super(message, ...params)
|
|
||||||
|
|
||||||
if (Error.captureStackTrace) {
|
|
||||||
Error.captureStackTrace(this, HttpError)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.name = this.constructor.name
|
|
||||||
|
|
||||||
if (!(Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600)) {
|
|
||||||
throw new Error('statusCode has to be integer in range [400, 600).')
|
|
||||||
}
|
|
||||||
this.statusCode = statusCode
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
this.data = data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HttpError
|
|
@ -221,8 +221,6 @@ genSrcDir :: AppSpec -> Generator [FileDraft]
|
|||||||
genSrcDir spec =
|
genSrcDir spec =
|
||||||
sequence
|
sequence
|
||||||
[ genFileCopy [relfile|app.js|],
|
[ genFileCopy [relfile|app.js|],
|
||||||
genFileCopy [relfile|core/AuthError.js|],
|
|
||||||
genFileCopy [relfile|core/HttpError.js|],
|
|
||||||
genDbClient spec,
|
genDbClient spec,
|
||||||
genConfigFile spec,
|
genConfigFile spec,
|
||||||
genServerJs spec,
|
genServerJs spec,
|
||||||
|
@ -71,7 +71,7 @@ After you defined the API, it should be implemented as a NodeJS function that ta
|
|||||||
```ts title="src/server/apis.js"
|
```ts title="src/server/apis.js"
|
||||||
export const fooBar = (req, res, context) => {
|
export const fooBar = (req, res, context) => {
|
||||||
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
|
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
|
||||||
res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` });
|
res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` });
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ import { FooBar } from "@wasp/apis/types"; // This type is generated by Wasp bas
|
|||||||
|
|
||||||
export const fooBar: FooBar = (req, res, context) => {
|
export const fooBar: FooBar = (req, res, context) => {
|
||||||
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
|
res.set("Access-Control-Allow-Origin", "*"); // Example of modifying headers to override Wasp default CORS middleware.
|
||||||
res.json({ msg: `Hello, ${context.user?.username || "stranger"}!` });
|
res.json({ msg: `Hello, ${context.user ? "registered user" : "stranger"}!` });
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -71,10 +71,11 @@ This is how we can define our `webSocketFn` function:
|
|||||||
|
|
||||||
```ts title=src/server/webSocket.js
|
```ts title=src/server/webSocket.js
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { getFirstProviderUserId } from '@wasp/auth/user.js'
|
||||||
|
|
||||||
export const webSocketFn = (io, context) => {
|
export const webSocketFn = (io, context) => {
|
||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
const username = socket.data.user?.email || socket.data.user?.username || 'unknown'
|
const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
|
||||||
console.log('a user connected: ', username)
|
console.log('a user connected: ', username)
|
||||||
|
|
||||||
socket.on('chatMessage', async (msg) => {
|
socket.on('chatMessage', async (msg) => {
|
||||||
@ -92,10 +93,11 @@ export const webSocketFn = (io, context) => {
|
|||||||
```ts title=src/server/webSocket.ts
|
```ts title=src/server/webSocket.ts
|
||||||
import type { WebSocketDefinition, WaspSocketData } from '@wasp/webSocket'
|
import type { WebSocketDefinition, WaspSocketData } from '@wasp/webSocket'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { getFirstProviderUserId } from '@wasp/auth/user.js'
|
||||||
|
|
||||||
export const webSocketFn: WebSocketFn = (io, context) => {
|
export const webSocketFn: WebSocketFn = (io, context) => {
|
||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
const username = socket.data.user?.email || socket.data.user?.username || 'unknown'
|
const username = getFirstProviderUserId(socket.data.user) ?? 'Unknown'
|
||||||
console.log('a user connected: ', username)
|
console.log('a user connected: ', username)
|
||||||
|
|
||||||
socket.on('chatMessage', async (msg) => {
|
socket.on('chatMessage', async (msg) => {
|
||||||
|
382
web/docs/auth/migrate-from-11.md
Normal file
382
web/docs/auth/migrate-from-11.md
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
---
|
||||||
|
title: Migration from 0.11.X to 0.12.X
|
||||||
|
---
|
||||||
|
|
||||||
|
Wasp made a big change in the way authentication works in version 0.12.0. This guide will help you migrate your app from 0.11.X to 0.12.X.
|
||||||
|
|
||||||
|
## What Changed?
|
||||||
|
|
||||||
|
### 0.11.X Auth
|
||||||
|
|
||||||
|
In 0.11.X, authentication was based on the `User` model which the developer needed to set up properly and take care of the auth fields like `email` or `password`.
|
||||||
|
|
||||||
|
```wasp title="main.wasp"
|
||||||
|
app myApp {
|
||||||
|
wasp: {
|
||||||
|
version: "^0.11.0"
|
||||||
|
},
|
||||||
|
title: "My App",
|
||||||
|
auth: {
|
||||||
|
userEntity: User,
|
||||||
|
externalAuthEntity: SocialLogin,
|
||||||
|
methods: {
|
||||||
|
gitHub: {}
|
||||||
|
},
|
||||||
|
onAuthFailedRedirectTo: "/login"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
entity User {=psl
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
username String @unique
|
||||||
|
password String
|
||||||
|
externalAuthAssociations SocialLogin[]
|
||||||
|
psl=}
|
||||||
|
|
||||||
|
entity SocialLogin {=psl
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
provider String
|
||||||
|
providerId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
@@unique([provider, providerId, userId])
|
||||||
|
psl=}
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Auth
|
||||||
|
|
||||||
|
#### Auth Models
|
||||||
|
|
||||||
|
From 0.12.X onwards, authentication is based on the auth models which are automatically set up by Wasp. You don't need to take care of the auth fields anymore.
|
||||||
|
|
||||||
|
The `User` model is now just a business logic model and you use it for storing the data that is relevant for your app.
|
||||||
|
|
||||||
|
```wasp title="main.wasp"
|
||||||
|
app myApp {
|
||||||
|
wasp: {
|
||||||
|
version: "^0.12.0"
|
||||||
|
},
|
||||||
|
title: "My App",
|
||||||
|
auth: {
|
||||||
|
userEntity: User,
|
||||||
|
methods: {
|
||||||
|
gitHub: {}
|
||||||
|
},
|
||||||
|
onAuthFailedRedirectTo: "/login"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
entity User {=psl
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
psl=}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::caution Regression Note: Multiple Auth Identities per User
|
||||||
|
|
||||||
|
With our old auth implementation, if you were using both Google and email auth methods, your users could sign up with Google first and then, later on, reset their password and therefore also enable logging in with their email and password. This was the only way in which a single user could have multiple login methods at the same time (Google and email).
|
||||||
|
|
||||||
|
This is not possible anymore. **The new auth system doesn't support multiple login methods per user at the moment**. We do plan to add this soon though, with the introduction of the [account merging feature](https://github.com/wasp-lang/wasp/issues/954).
|
||||||
|
|
||||||
|
If you have any users that have both Google and email login credentials at the same time, you will have to pick only one of those for that user to keep when migrating them.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
You can read more about the new auth system in the [Auth Entities](./entities) section.
|
||||||
|
|
||||||
|
## How to Migrate?
|
||||||
|
|
||||||
|
Migrating your existing app to the new auth system is a two-step process:
|
||||||
|
1. Migrate to the new auth system
|
||||||
|
1. Cleanup the old auth system
|
||||||
|
|
||||||
|
:::info Migrating a deployed app
|
||||||
|
|
||||||
|
While going through these steps, we will focus first on doing the changes locally and your local development database.
|
||||||
|
|
||||||
|
Once we confirm that everything works well, we will also apply those same changes to the deployed app.
|
||||||
|
|
||||||
|
**We'll put extra info for migrating a deployed app in a box like this one.**
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 1. Migrate to the New Auth System
|
||||||
|
|
||||||
|
You can follow these steps to migrate to the new auth system:
|
||||||
|
|
||||||
|
1. Upgrade Wasp to the latest 0.12 version.
|
||||||
|
|
||||||
|
These instructions are for migrating the auth from Wasp `0.11.X` to Wasp `0.12.X`, which means that they work for example for both `0.12.0` and `0.12.5` versions. We suggest that you install the latest 0.12 version of Wasp. Find the available Wasp versions in the [Releases](https://github.com/wasp-lang/wasp/releases) section of our GitHub repo.
|
||||||
|
|
||||||
|
Then you can install that version with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sSL https://get.wasp-lang.dev/installer.sh | sh -s -- -v 0.12.0
|
||||||
|
```
|
||||||
|
|
||||||
|
<small>
|
||||||
|
|
||||||
|
In the above command, replace `0.12.0` with the version you want to install.
|
||||||
|
|
||||||
|
</small>
|
||||||
|
|
||||||
|
1. Bump the version to `^0.12.0` in `main.wasp`.
|
||||||
|
1. Create the new auth tables in the database by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wasp db migrate-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see the new `Auth`, `AuthIdentity` and `Session` tables in your database. You can use the `wasp db studio` command to open the database in a GUI and verify that the tables are there.
|
||||||
|
|
||||||
|
1. Write your data migration function(s) in `src/server/migrateToNewAuth.ts`
|
||||||
|
- In the previous step, we migrated the schema, and now we need to prepare logic for migrating the data.
|
||||||
|
- Below you can find [examples of migration functions](#example-migration-scripts) for each of the auth methods. They should be fine to use as-is, meaning you can just copy them, but you can also modify them to your needs. You will want to have one function per each auth method that you use in your app.
|
||||||
|
1. Add the migration function(s) to the `db.seeds` config:
|
||||||
|
```wasp title="main.wasp"
|
||||||
|
app myApp {
|
||||||
|
wasp: {
|
||||||
|
version: "^0.12.0"
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
db: {
|
||||||
|
seeds: [
|
||||||
|
import { migrateEmailAuth } from "@server/migrateToNewAuth.js",
|
||||||
|
import { migrateGoogleAuth } from "@server/migrateToNewAuth.js",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
1. Run the migration function(s) by running:
|
||||||
|
```bash
|
||||||
|
wasp db seed
|
||||||
|
```
|
||||||
|
If you added multiple migration functions, you can pick which one to run by selecting it from the list.
|
||||||
|
|
||||||
|
1. Verify that the auth still works by logging in with each of the auth methods.
|
||||||
|
1. Update your JS code to work correctly with the new auth entities.
|
||||||
|
|
||||||
|
You should use the new auth helper functions to get the `email` or `username` from a user object. Read more about the helpers in the [Auth Entities](./entities#accessing-the-auth-fields) section. The helpers you are most likely to use are the `getEmail` and `getUsername` helpers.
|
||||||
|
1. Finally, check that your app now works as it worked before. If the above steps were done correctly, everything should be working now.
|
||||||
|
|
||||||
|
:::info Migrating a deployed app
|
||||||
|
|
||||||
|
After successfully performing migration locally so far, and verifying the your app works as expected, it is time to also migrate our deployed app.
|
||||||
|
|
||||||
|
Before migrating your production (deployed) app, we advise you to back up your production database in case something goes wrong. Also, besides testing it in development, it's good to test the migration in a staging environment.
|
||||||
|
|
||||||
|
We will perform the production migration in 2 steps:
|
||||||
|
- Deploying the new code to production (client and server).
|
||||||
|
- Migrating the production database.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Between these two steps, so after deploying the new code to production and before migrating the production database, your app will not be working completely: new users will be able to sign up, but existing users won't be able to log in, and already logged in users will be logged out. Once you do the second step, migrating the production database, it will all be back to normal.
|
||||||
|
|
||||||
|
You will likely want to keep the time between the two steps as short as you can. Make sure you know exactly what each step means before doing them for real to eliminate any surprises.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- **First step:** deploy the new code (client and server), either via `wasp deploy` or manually.
|
||||||
|
|
||||||
|
Check our [Deployment docs](../advanced/deployment/overview.md) for more details.
|
||||||
|
|
||||||
|
- **Second step:** run the migration script on the production database with `wasp db seed` command.
|
||||||
|
|
||||||
|
We wrote instructions on how to do it for **Fly.io** deployments here: https://github.com/wasp-lang/wasp/issues/1464 . The instructions should be similar for other deployment providers: setting up some sort of an SSH tunnel from your local machine to the production database and running the migration script locally with `DATABASE_URL` pointing to the production database.
|
||||||
|
|
||||||
|
Your deployed app should be working normally now, with the new auth system.
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
### 2. Cleanup the Old Auth System
|
||||||
|
|
||||||
|
Your app should be working correctly and using new auth, but to finish the migration, we need to clean up the old auth system:
|
||||||
|
|
||||||
|
1. Delete auth-related fields from `User` entity.
|
||||||
|
|
||||||
|
- This means any fields that were used for authentication, like `email`, `password`, `isEmailVerified`, `emailVerificationSentAt`, `passwordResetSentAt`, `username`, etc.
|
||||||
|
|
||||||
|
1. Remove the `externalAuthEntity` from the `auth` config and the `SocialLogin` entity if you used Google or GitHub auth.
|
||||||
|
1. Run `wasp db migrate-dev` again to remove the redundant fields from the database.
|
||||||
|
1. You can now delete the migration script and the `db.seeds` config.
|
||||||
|
|
||||||
|
:::info Migrating a deployed app
|
||||||
|
|
||||||
|
After doing the steps above successfully locally and making sure everything is working, it is time to push these changes to the deployed app again.
|
||||||
|
|
||||||
|
_Deploy the app again_, either via `wasp deploy` or manually. Check our [Deployment docs](../advanced/deployment/overview.md) for more details.
|
||||||
|
|
||||||
|
The database migrations will automatically run on successful deployment of the server and delete the now redundant auth-related `User` columns from the database.
|
||||||
|
|
||||||
|
Your app is now fully migrated to the new auth system.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
## Example Migration Functions
|
||||||
|
|
||||||
|
The migration functions provided below are written with the typical use cases in mind and you can use them as-is. If your setup requires additional logic, you can use them as a good starting point and modify them to your needs.
|
||||||
|
|
||||||
|
### Username & Password
|
||||||
|
|
||||||
|
```ts title="src/server/migrateToNewAuth.ts"
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { ProviderName, UsernameProviderData } from "@wasp/auth/utils";
|
||||||
|
|
||||||
|
export async function migrateUsernameAuth(prismaClient: PrismaClient) {
|
||||||
|
const users = await prismaClient.user.findMany({
|
||||||
|
include: {
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
if (user.auth) {
|
||||||
|
console.log("User was already migrated, skipping", user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.username || !user.password) {
|
||||||
|
console.log("Missing username auth info, skipping user", user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerData: UsernameProviderData = {
|
||||||
|
hashedPassword: user.password,
|
||||||
|
};
|
||||||
|
const providerName: ProviderName = "username";
|
||||||
|
|
||||||
|
await prismaClient.auth.create({
|
||||||
|
data: {
|
||||||
|
identities: {
|
||||||
|
create: {
|
||||||
|
providerName,
|
||||||
|
providerUserId: user.username.toLowerCase(),
|
||||||
|
providerData: JSON.stringify(providerData),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Email
|
||||||
|
|
||||||
|
```ts title="src/server/migrateToNewAuth.ts"
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { EmailProviderData, ProviderName } from "@wasp/auth/utils";
|
||||||
|
|
||||||
|
export async function migrateEmailAuth(prismaClient: PrismaClient) {
|
||||||
|
const users = await prismaClient.user.findMany({
|
||||||
|
include: {
|
||||||
|
auth: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
if (user.auth) {
|
||||||
|
console.log("User was already migrated, skipping", user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.email || !user.password) {
|
||||||
|
console.log("Missing email auth info, skipping user", user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerData: EmailProviderData = {
|
||||||
|
isEmailVerified: user.isEmailVerified,
|
||||||
|
emailVerificationSentAt:
|
||||||
|
user.emailVerificationSentAt?.toISOString() ?? null,
|
||||||
|
passwordResetSentAt: user.passwordResetSentAt?.toISOString() ?? null,
|
||||||
|
hashedPassword: user.password,
|
||||||
|
};
|
||||||
|
const providerName: ProviderName = "email";
|
||||||
|
|
||||||
|
await prismaClient.auth.create({
|
||||||
|
data: {
|
||||||
|
identities: {
|
||||||
|
create: {
|
||||||
|
providerName,
|
||||||
|
providerUserId: user.email,
|
||||||
|
providerData: JSON.stringify(providerData),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Google & GitHub
|
||||||
|
|
||||||
|
```ts title="src/server/migrateToNewAuth.ts"
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import { ProviderName } from "@wasp/auth/utils";
|
||||||
|
|
||||||
|
export async function migrateGoogleAuth(prismaClient: PrismaClient) {
|
||||||
|
return createSocialLoginMigration(prismaClient, "google");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function migrateGitHubAuth(prismaClient: PrismaClient) {
|
||||||
|
return createSocialLoginMigration(prismaClient, "github");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSocialLoginMigration(
|
||||||
|
prismaClient: PrismaClient,
|
||||||
|
providerName: "google" | "github"
|
||||||
|
) {
|
||||||
|
const users = await prismaClient.user.findMany({
|
||||||
|
include: {
|
||||||
|
auth: true,
|
||||||
|
externalAuthAssociations: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
if (user.auth) {
|
||||||
|
console.log("User was already migrated, skipping", user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = user.externalAuthAssociations.find(
|
||||||
|
(provider) => provider.provider === providerName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
console.log(`Missing ${providerName} provider, skipping user`, user);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prismaClient.auth.create({
|
||||||
|
data: {
|
||||||
|
identities: {
|
||||||
|
create: {
|
||||||
|
providerName,
|
||||||
|
providerUserId: provider.providerId,
|
||||||
|
providerData: JSON.stringify({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -476,55 +476,6 @@ Again, annotating the Actions is optional, but greatly improves **full-stack typ
|
|||||||
|
|
||||||
The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud).
|
The object `context.entities.Task` exposes `prisma.task` from [Prisma's CRUD API](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud).
|
||||||
|
|
||||||
### Prisma Error Helpers
|
|
||||||
|
|
||||||
In your Operations, you may wish to handle general Prisma errors with HTTP-friendly responses.
|
|
||||||
|
|
||||||
Wasp exposes two helper functions, `isPrismaError`, and `prismaErrorToHttpError`, for this purpose. As of now, we convert two specific Prisma errors (which we will continue to expand), with the rest being `500`. See the [source here](https://github.com/wasp-lang/wasp/blob/main/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/utils.js).
|
|
||||||
|
|
||||||
Here's how you can import and use them:
|
|
||||||
|
|
||||||
<Tabs groupId="js-ts">
|
|
||||||
<TabItem value="js" label="JavaScript">
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { isPrismaError, prismaErrorToHttpError } from "@wasp/utils.js";
|
|
||||||
|
|
||||||
// ...
|
|
||||||
|
|
||||||
try {
|
|
||||||
await context.entities.Task.create({...})
|
|
||||||
} catch (e) {
|
|
||||||
if (isPrismaError(e)) {
|
|
||||||
throw prismaErrorToHttpError(e)
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="ts" label="TypeScript">
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { isPrismaError, prismaErrorToHttpError } from "@wasp/utils.js";
|
|
||||||
|
|
||||||
// ...
|
|
||||||
|
|
||||||
try {
|
|
||||||
await context.entities.Task.create({...})
|
|
||||||
} catch (e) {
|
|
||||||
if (isPrismaError(e)) {
|
|
||||||
throw prismaErrorToHttpError(e)
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Cache Invalidation
|
## Cache Invalidation
|
||||||
|
|
||||||
One of the trickiest parts of managing a web app's state is making sure the data returned by the Queries is up to date.
|
One of the trickiest parts of managing a web app's state is making sure the data returned by the Queries is up to date.
|
||||||
|
@ -38,6 +38,10 @@ That's it 🎉 You have successfully created and served a new full-stack web app
|
|||||||
Check [More Details](#more-details) section below if anything went wrong with the installation, or if you have additional questions.
|
Check [More Details](#more-details) section below if anything went wrong with the installation, or if you have additional questions.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
:::tip Want an even faster start?
|
||||||
|
Try out [Wasp AI](../wasp-ai/creating-new-app.md) 🤖 to generate a new Wasp app in minutes just from a title and short description!
|
||||||
|
:::
|
||||||
|
|
||||||
:::tip Try Wasp Without Installing 🤔?
|
:::tip Try Wasp Without Installing 🤔?
|
||||||
Give Wasp a spin in the browser without any setup by running our [Wasp Template for Gitpod](https://github.com/wasp-lang/gitpod-template)
|
Give Wasp a spin in the browser without any setup by running our [Wasp Template for Gitpod](https://github.com/wasp-lang/gitpod-template)
|
||||||
:::
|
:::
|
||||||
|
47
web/docs/wasp-ai/creating-new-app.md
Normal file
47
web/docs/wasp-ai/creating-new-app.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
title: Creating New App with AI
|
||||||
|
---
|
||||||
|
|
||||||
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
|
|
||||||
|
import ImgWithCaption from '@site/blog/components/ImgWithCaption'
|
||||||
|
|
||||||
|
Wasp comes with its own AI: Wasp AI, aka Mage (**M**agic web **A**pp **GE**nerator).
|
||||||
|
|
||||||
|
Wasp AI allows you to create a new Wasp app **from only a title and a short description** (using GPT in the background)!
|
||||||
|
|
||||||
|
There are two main ways to create a new Wasp app with Wasp AI:
|
||||||
|
1. Free, open-source online app [usemage.ai](https://usemage.ai).
|
||||||
|
2. Running `wasp new` on your machine and picking AI generation. For this you need to provide your own OpenAI API keys, but it allows for more flexibility (choosing GPT models).
|
||||||
|
|
||||||
|
They both use the same logic in the background, so both approaches are equally "smart", the difference is just in the UI / settings.
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Wasp AI is an experimental feature. Apps that Wasp AI generates can have mistakes (proportional to their complexity), but even then they can often serve as a great starting point (once you fix the mistakes) or an interesting way to explore how to implement stuff in Wasp.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## usemage.ai
|
||||||
|
|
||||||
|
<ImgWithCaption
|
||||||
|
source="img/gpt-wasp/how-it-works.gif"
|
||||||
|
caption="1. Describe your app 2. Pick the color 3. Generate your app 🚀"
|
||||||
|
/>
|
||||||
|
|
||||||
|
[Mage](https://usemage.ai) is an open-source app with which you can create new Wasp apps from just a short title and description.
|
||||||
|
|
||||||
|
It is completely free for you - it uses our OpenAI API keys and we take on the costs.
|
||||||
|
|
||||||
|
Once you provide an app title, app description, and choose some basic settings, your new Wasp app will be created for you in a matter of minutes and you will be able to download it to your machine and keep working on it!
|
||||||
|
|
||||||
|
If you want to know more, check this [blog post](/blog/2023/07/10/gpt-web-app-generator) for more details on how Mage works, or this [blog post](blog/2023/07/17/how-we-built-gpt-web-app-generator) for a high-level overview of how we implemented it.
|
||||||
|
|
||||||
|
## Wasp Cli
|
||||||
|
|
||||||
|
You can create a new Wasp app using Wasp AI by running `wasp new` in your terminal and picking AI generation.
|
||||||
|
|
||||||
|
If you don't have them set yet, `wasp` will ask you to provide (via ENV vars) your OpenAI API keys (which it will use to query GPT).
|
||||||
|
|
||||||
|
Then, after providing a title and description for your Wasp app, the new app will be generated on your disk!
|
||||||
|
|
||||||
|
![wasp-cli-ai-input](./wasp-ai-1.png)
|
||||||
|
![wasp-cli-ai-generation](./wasp-ai-2.png)
|
9
web/docs/wasp-ai/developing-existing-app.md
Normal file
9
web/docs/wasp-ai/developing-existing-app.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: Developing Existing App with AI
|
||||||
|
---
|
||||||
|
|
||||||
|
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||||
|
|
||||||
|
While Wasp AI doesn't at the moment offer any additional help for developing your Wasp app with AI beyond initial generation, this is something we are exploring actively.
|
||||||
|
|
||||||
|
In the meantime, while waiting for Wasp AI to add support for this, we suggest checking out [aider](https://github.com/paul-gauthier/aider), which is an AI pair programming tool in your terminal. This is a third-party tool, not affiliated with Wasp in any way, but we and some of Wasp users have found that it can be helpful when working on Wasp apps.
|
BIN
web/docs/wasp-ai/wasp-ai-1.png
Normal file
BIN
web/docs/wasp-ai/wasp-ai-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 307 KiB |
BIN
web/docs/wasp-ai/wasp-ai-2.png
Normal file
BIN
web/docs/wasp-ai/wasp-ai-2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 298 KiB |
@ -105,7 +105,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
label: 'Todo app tutorial',
|
label: 'Todo app tutorial',
|
||||||
to: 'docs/tutorial/create',
|
to: 'docs/tutorial/create',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -140,7 +140,7 @@ module.exports = {
|
|||||||
// ContextualSearch is useful when you are doing versioning,
|
// ContextualSearch is useful when you are doing versioning,
|
||||||
// it searches only in v1 docs if you are searching from v1 docs.
|
// it searches only in v1 docs if you are searching from v1 docs.
|
||||||
// Therefore we have it enabled, since we have multiple doc versions.
|
// Therefore we have it enabled, since we have multiple doc versions.
|
||||||
contextualSearch: true
|
contextualSearch: true,
|
||||||
},
|
},
|
||||||
image: 'img/wasp_twitter_cover.png',
|
image: 'img/wasp_twitter_cover.png',
|
||||||
metadata: [{ name: 'twitter:card', content: 'summary_large_image' }],
|
metadata: [{ name: 'twitter:card', content: 'summary_large_image' }],
|
||||||
@ -163,6 +163,11 @@ module.exports = {
|
|||||||
|
|
||||||
// "current" docs (under /docs) are in-progress docs, so we show them only in development.
|
// "current" docs (under /docs) are in-progress docs, so we show them only in development.
|
||||||
includeCurrentVersion: process.env.NODE_ENV === 'development',
|
includeCurrentVersion: process.env.NODE_ENV === 'development',
|
||||||
|
// In development, we want "current" docs to be the default docs (served at /docs),
|
||||||
|
// to make it easier for us a bit. Otherwise, by default, the latest versioned docs
|
||||||
|
// will be served under /docs.
|
||||||
|
lastVersion:
|
||||||
|
process.env.NODE_ENV === 'development' ? 'current' : undefined,
|
||||||
|
|
||||||
// Uncomment line below to build and show only current docs, for faster build times
|
// Uncomment line below to build and show only current docs, for faster build times
|
||||||
// during development, if/when needed.
|
// during development, if/when needed.
|
||||||
@ -171,15 +176,18 @@ module.exports = {
|
|||||||
// "versions" option here enables us to customize each version of docs individually,
|
// "versions" option here enables us to customize each version of docs individually,
|
||||||
// and there are also other options if we ever need to customize versioned docs further.
|
// and there are also other options if we ever need to customize versioned docs further.
|
||||||
versions: {
|
versions: {
|
||||||
...(
|
// We provide config for `current` only during development because otherwise
|
||||||
(process.env.NODE_ENV === 'development') ? {
|
// we don't even build them (due to includeCurrentVersion above), so this config
|
||||||
"current": {
|
// would cause error in that case.
|
||||||
path: "next", // Token used in the URL to address this version of docs: {baseUrl}/docs/{path}.
|
...(process.env.NODE_ENV === 'development'
|
||||||
label: "Next", // Label shown in the documentation to address this version of docs.
|
? {
|
||||||
|
current: {
|
||||||
|
label: 'Next', // Label shown in the documentation to address this version of docs.
|
||||||
noIndex: true, // these are un-released docs, we don't want search engines indexing them.
|
noIndex: true, // these are un-released docs, we don't want search engines indexing them.
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} : {}
|
: {}),
|
||||||
),
|
|
||||||
// Configuration example:
|
// Configuration example:
|
||||||
// "0.11.1": {
|
// "0.11.1": {
|
||||||
// path: "0.11.1", // default, but can be anything.
|
// path: "0.11.1", // default, but can be anything.
|
||||||
@ -187,10 +195,9 @@ module.exports = {
|
|||||||
// banner: "unmaintained"
|
// banner: "unmaintained"
|
||||||
// // and more!
|
// // and more!
|
||||||
// },
|
// },
|
||||||
}
|
},
|
||||||
|
|
||||||
// ------------------------------------------------------ //
|
// ------------------------------------------------------ //
|
||||||
|
|
||||||
},
|
},
|
||||||
blog: {
|
blog: {
|
||||||
showReadingTime: true,
|
showReadingTime: true,
|
||||||
|
@ -68,6 +68,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
'auth/entities/entities',
|
'auth/entities/entities',
|
||||||
|
'auth/migrate-from-11',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -88,6 +89,13 @@ module.exports = {
|
|||||||
'project/custom-vite-config',
|
'project/custom-vite-config',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Wasp AI',
|
||||||
|
collapsed: false,
|
||||||
|
collapsible: false,
|
||||||
|
items: ['wasp-ai/creating-new-app', 'wasp-ai/developing-existing-app'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'Advanced Features',
|
label: 'Advanced Features',
|
||||||
|
Loading…
Reference in New Issue
Block a user