diff --git a/examples/realworld/main.wasp b/examples/realworld/main.wasp
index 53e4731bc9..38015ef404 100644
--- a/examples/realworld/main.wasp
+++ b/examples/realworld/main.wasp
@@ -7,7 +7,9 @@ app Conduit {
auth: {
userEntity: User,
- methods: [ UsernameAndPassword ],
+ methods: {
+ usernameAndPassword: {}
+ },
onAuthFailedRedirectTo: "/login"
},
diff --git a/examples/thoughts/main.wasp b/examples/thoughts/main.wasp
index 860306989a..3aff240fb6 100644
--- a/examples/thoughts/main.wasp
+++ b/examples/thoughts/main.wasp
@@ -3,7 +3,9 @@ app Thoughts {
db: { system: PostgreSQL },
auth: {
userEntity: User,
- methods: [ UsernameAndPassword ],
+ methods: {
+ usernameAndPassword: {}
+ },
onAuthFailedRedirectTo: "/login"
},
dependencies: [
diff --git a/examples/tutorials/TodoApp/main.wasp b/examples/tutorials/TodoApp/main.wasp
index 8db857330a..4b38e3f568 100644
--- a/examples/tutorials/TodoApp/main.wasp
+++ b/examples/tutorials/TodoApp/main.wasp
@@ -3,7 +3,9 @@ app TodoApp {
auth: {
userEntity: User,
- methods: [ UsernameAndPassword ],
+ methods: {
+ usernameAndPassword: {}
+ },
onAuthFailedRedirectTo: "/login"
},
diff --git a/examples/waspello/main.wasp b/examples/waspello/main.wasp
index f09802140e..46e82e9077 100644
--- a/examples/waspello/main.wasp
+++ b/examples/waspello/main.wasp
@@ -7,7 +7,9 @@ app trello {
auth: {
userEntity: User,
- methods: [ UsernameAndPassword ],
+ methods: {
+ usernameAndPassword: {}
+ },
onAuthFailedRedirectTo: "/login"
},
diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md
index caba4f8d6f..7dbdb082a6 100644
--- a/waspc/ChangeLog.md
+++ b/waspc/ChangeLog.md
@@ -3,7 +3,7 @@
## v0.6.0.0 (TBD)
### BREAKING CHANGES
-- The `EmailAndPassword` auth method has been renamed `UsernameAndPassword` to better reflect the current usage. Email validation will be addressed in the future.
+- The `EmailAndPassword` auth method has been renamed `usernameAndPassword` to better reflect the current usage. Email validation will be addressed in the future.
- This means the `auth.userEntity` model should now have field called `username` (instead of `email`, as before).
- If you'd like to treat the old `email` field as `username`, you can create a migration file like so:
```bash
@@ -24,8 +24,16 @@
```
- NOTE: If you simply changed `email` to `username` in your .wasp file, Prisma will try to drop the table and recreate it, which is likely not what you want if you have data you want to preserve.
- If you would like to add a new `username` column and keep `email` as is, be sure to add a calculated value in the migration (perhaps a random string, or something based on the `email`). The `username` column should remain `NOT NULL` and `UNIQUE`.
+- `WASP_WEB_CLIENT_URL` is now a required environment variable to improve CORS security. It is set by default in development. In production, this should point to the URL where your frontend app is being hosted.
+- The generated Dockerfile has been updated from `node:14-alpine` to `node:16-alpine`.
- Wasp Jobs callback function arguments have been updated to the following: `async function jobHandler(args, context)`. Jobs can now make use of entities, accessed via `context`, like Operations. Additionally, the data passed into the Job handler function are no longer wrapped in a `data` property, and are now instead accessed exactly as they are supplied via `args`.
+### [NEW FEATURE] Google is now a supported authentication method!
+
+You can now offer your users the ability to sign in with Google! Enabling it is just a few lines and offers a fast, easy, and secure way to get users into your app! We also have a comprehensive setup guide for creating a new app in the Google Developer Console.
+
+Stay tuned, as more external auth methods will be added in the future. Let us know what you'd like to see support for next!
+
---
## v0.5.2.1 (2022/07/14)
diff --git a/waspc/data/Generator/templates/Dockerfile b/waspc/data/Generator/templates/Dockerfile
index 3bc727d1a3..631bb68c0b 100644
--- a/waspc/data/Generator/templates/Dockerfile
+++ b/waspc/data/Generator/templates/Dockerfile
@@ -1,5 +1,5 @@
{{={= =}=}}
-FROM node:14-alpine AS node
+FROM node:{= nodeMajorVersion =}-alpine AS node
FROM node AS base
@@ -11,6 +11,9 @@ FROM base AS server-builder
RUN apk add --no-cache build-base libtool autoconf automake python3
WORKDIR /app
# Install npm packages, resulting in node_modules/.
+{=# usingServerPatches =}
+COPY server/patches ./server/patches
+{=/ usingServerPatches =}
COPY server/package*.json ./server/
RUN cd server && npm install
{=# usingPrisma =}
diff --git a/waspc/data/Generator/templates/react-app/public/images/btn_google_signin_dark_normal_web@2x.png b/waspc/data/Generator/templates/react-app/public/images/btn_google_signin_dark_normal_web@2x.png
new file mode 100644
index 0000000000..f27bb24330
Binary files /dev/null and b/waspc/data/Generator/templates/react-app/public/images/btn_google_signin_dark_normal_web@2x.png differ
diff --git a/waspc/data/Generator/templates/react-app/src/auth/buttons/Google.js b/waspc/data/Generator/templates/react-app/src/auth/buttons/Google.js
new file mode 100644
index 0000000000..d0d907ad97
--- /dev/null
+++ b/waspc/data/Generator/templates/react-app/src/auth/buttons/Google.js
@@ -0,0 +1,11 @@
+import config from '../../config.js'
+
+export const googleSignInUrl = `${config.apiUrl}/auth/external/google/login`
+
+export function GoogleSignInButton(props) {
+ return (
+
+
+
+ )
+}
diff --git a/waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCodeExchange.js b/waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCodeExchange.js
new file mode 100644
index 0000000000..e5137cb027
--- /dev/null
+++ b/waspc/data/Generator/templates/react-app/src/auth/pages/OAuthCodeExchange.js
@@ -0,0 +1,56 @@
+{{={= =}=}}
+import React, { useEffect } from 'react'
+import { useHistory } from 'react-router-dom'
+
+import config from '../../config.js'
+import api, { setAuthToken } from '../../api.js'
+
+// After a user authenticates via an Oauth 2.0 provider, this is the page that
+// the provider should redirect them to, while providing query string parameters
+// that contain information needed for the API server to authenticate with the provider.
+// This page forwards that information to the API server and in response get a JWT,
+// which it stores on the client, therefore completing the OAuth authentication process.
+export default function OAuthCodeExchange({ pathToApiServerRouteHandlingOauthRedirect }) {
+ const history = useHistory()
+
+ useEffect(() => {
+ // NOTE: Different auth methods will have different Wasp API server validation paths.
+ // This helps us reuse one component for various methods (e.g., Google, Facebook, etc.).
+ const apiServerUrlHandlingOauthRedirect = constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRedirect)
+
+ exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect)
+ }, [history, pathToApiServerRouteHandlingOauthRedirect])
+
+ return (
+
Completing login process...
+ )
+}
+
+function constructOauthRedirectApiServerUrl(pathToApiServerRouteHandlingOauthRedirect) {
+ // Take the redirect query params supplied by the external OAuth provider and
+ // send them as-is to our backend, so Passport can finish the process.
+ const queryParams = window.location.search
+ return `${config.apiUrl}${pathToApiServerRouteHandlingOauthRedirect}${queryParams}`
+}
+
+async function exchangeCodeForJwtAndRedirect(history, apiServerUrlHandlingOauthRedirect) {
+ const token = await exchangeCodeForJwt(apiServerUrlHandlingOauthRedirect)
+
+ if (token !== null) {
+ setAuthToken(token)
+ history.push('{= onAuthSucceededRedirectTo =}')
+ } else {
+ console.error('Error obtaining JWT token')
+ history.push('{= onAuthFailedRedirectTo =}')
+ }
+}
+
+async function exchangeCodeForJwt(url) {
+ try {
+ const response = await api.get(url)
+ return response?.data?.token || null
+ } catch (e) {
+ console.error(e)
+ return null
+ }
+}
diff --git a/waspc/data/Generator/templates/react-app/src/auth/useAuth.js b/waspc/data/Generator/templates/react-app/src/auth/useAuth.js
index bdc82ff56d..1def2c5990 100644
--- a/waspc/data/Generator/templates/react-app/src/auth/useAuth.js
+++ b/waspc/data/Generator/templates/react-app/src/auth/useAuth.js
@@ -11,7 +11,7 @@ async function getMe() {
return response.data
} catch (error) {
- if (error.response?.status === 403) {
+ if (error.response?.status === 401) {
return null
} else {
handleApiError(error)
diff --git a/waspc/data/Generator/templates/react-app/src/router.js b/waspc/data/Generator/templates/react-app/src/router.js
index 42ed9664c0..56f9f53d91 100644
--- a/waspc/data/Generator/templates/react-app/src/router.js
+++ b/waspc/data/Generator/templates/react-app/src/router.js
@@ -10,6 +10,9 @@ import createAuthRequiredPage from "./auth/pages/createAuthRequiredPage.js"
import {= importWhat =} from "{= importFrom =}"
{=/ pagesToImport =}
+{=# isExternalAuthEnabled =}
+import OAuthCodeExchange from "./auth/pages/OAuthCodeExchange"
+{=/ isExternalAuthEnabled =}
const router = (
@@ -17,6 +20,16 @@ const router = (
{=# routes =}
{=/ routes =}
+
+ {=# isExternalAuthEnabled =}
+
+ {=# isGoogleAuthEnabled =}
+
+
+
+ {=/ isGoogleAuthEnabled =}
+
+ {=/ isExternalAuthEnabled =}
)
diff --git a/waspc/data/Generator/templates/server/package.json b/waspc/data/Generator/templates/server/package.json
index 4d4754db80..5e92a13bd1 100644
--- a/waspc/data/Generator/templates/server/package.json
+++ b/waspc/data/Generator/templates/server/package.json
@@ -10,7 +10,8 @@
"db-migrate-prod": "prisma migrate deploy --schema=../db/schema.prisma",
"db-migrate-dev": "prisma migrate dev --schema=../db/schema.prisma",
"start-production": "{=& startProductionScript =}",
- "standard": "standard"
+ "standard": "standard",
+ "postinstall": "patch-package"
},
"nodemonConfig": {
"delay": "1000"
diff --git a/waspc/data/Generator/templates/server/patches/oauth+0.9.15.patch b/waspc/data/Generator/templates/server/patches/oauth+0.9.15.patch
new file mode 100644
index 0000000000..a797ef0353
--- /dev/null
+++ b/waspc/data/Generator/templates/server/patches/oauth+0.9.15.patch
@@ -0,0 +1,21 @@
+diff --git a/node_modules/oauth/lib/oauth2.js b/node_modules/oauth/lib/oauth2.js
+index 77241c4..b6c0184 100644
+--- a/node_modules/oauth/lib/oauth2.js
++++ b/node_modules/oauth/lib/oauth2.js
+@@ -158,8 +158,14 @@ exports.OAuth2.prototype._executeRequest= function( http_library, options, post_
+ });
+ });
+ request.on('error', function(e) {
+- callbackCalled= true;
+- callback(e);
++ // Ref: https://github.com/ciaranj/node-oauth/pull/363
++ // `www.googleapis.com` does `ECONNRESET` just after data is received in `passBackControl`
++ // this prevents the callback from being called twice, first in passBackControl and second time in here
++ // see also NodeJS Stream documentation: "The 'error' event may be emitted by a Readable implementation at any time"
++ if(!callbackCalled) {
++ callbackCalled= true;
++ callback(e);
++ }
+ });
+
+ if( (options.method == 'POST' || options.method == 'PUT') && post_body ) {
diff --git a/waspc/data/Generator/templates/server/src/app.js b/waspc/data/Generator/templates/server/src/app.js
index 6c769e0cd1..7ffcd317d9 100644
--- a/waspc/data/Generator/templates/server/src/app.js
+++ b/waspc/data/Generator/templates/server/src/app.js
@@ -6,6 +6,7 @@ import helmet from 'helmet'
import HttpError from './core/HttpError.js'
import indexRouter from './routes/index.js'
+import config from './config.js'
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
// it can be used in unit tests to test each route individually.
@@ -13,7 +14,10 @@ import indexRouter from './routes/index.js'
const app = express()
app.use(helmet())
-app.use(cors()) // TODO: Consider configuring CORS to be more restrictive, right now it allows all CORS requests.
+app.use(cors({
+ // TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
+ origin: config.frontendUrl,
+}))
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
diff --git a/waspc/data/Generator/templates/server/src/config.js b/waspc/data/Generator/templates/server/src/config.js
index 5d3c2ada35..4090e9309d 100644
--- a/waspc/data/Generator/templates/server/src/config.js
+++ b/waspc/data/Generator/templates/server/src/config.js
@@ -13,6 +13,7 @@ const config = {
env,
port: parseInt(process.env.PORT) || 3001,
databaseUrl: process.env.DATABASE_URL,
+ frontendUrl: undefined,
{=# isAuthEnabled =}
auth: {
jwtSecret: undefined
@@ -20,6 +21,7 @@ const config = {
{=/ isAuthEnabled =}
},
development: {
+ frontendUrl: process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000',
{=# isAuthEnabled =}
auth: {
jwtSecret: 'DEVJWTSECRET'
@@ -27,6 +29,7 @@ const config = {
{=/ isAuthEnabled =}
},
production: {
+ frontendUrl: process.env.WASP_WEB_CLIENT_URL,
{=# isAuthEnabled =}
auth: {
jwtSecret: process.env.JWT_SECRET
diff --git a/waspc/data/Generator/templates/server/src/core/auth.js b/waspc/data/Generator/templates/server/src/core/auth.js
index 7cefdb6d21..bb70d47bf1 100644
--- a/waspc/data/Generator/templates/server/src/core/auth.js
+++ b/waspc/data/Generator/templates/server/src/core/auth.js
@@ -2,6 +2,7 @@
import jwt from 'jsonwebtoken'
import SecurePassword from 'secure-password'
import util from 'util'
+import { randomInt } from 'node:crypto'
import prisma from '../dbClient.js'
import { handleRejection } from '../utils.js'
@@ -69,5 +70,52 @@ export const verifyPassword = async (hashedPassword, password) => {
}
}
-export default auth
+// Generates an unused username that looks similar to "quick-purple-sheep-91231".
+// It generates several options and ensures it picks one that is not currently in use.
+export function generateAvailableDictionaryUsername() {
+ const adjectives = ['fuzzy', 'tall', 'short', 'nice', 'happy', 'quick', 'slow', 'good', 'new', 'old', 'first', 'last', 'old', 'young']
+ const colors = ['red', 'green', 'blue', 'white', 'black', 'brown', 'purple', 'orange', 'yellow']
+ const nouns = ['wasp', 'cat', 'dog', 'lion', 'rabbit', 'duck', 'pig', 'bee', 'goat', 'crab', 'fish', 'chicken', 'horse', 'llama', 'camel', 'sheep']
+
+ const potentialUsernames = []
+ for (let i = 0; i < 10; i++) {
+ const potentialUsername = `${adjectives[randomInt(adjectives.length)]}-${colors[randomInt(colors.length)]}-${nouns[randomInt(nouns.length)]}-${randomInt(100_000)}`
+ potentialUsernames.push(potentialUsername)
+ }
+
+ return findAvailableUsername(potentialUsernames)
+}
+
+// Generates an unused username based on an array of username segments and a separator.
+// It generates several options and ensures it picks one that is not currently in use.
+export function generateAvailableUsername(usernameSegments, config) {
+ const separator = config?.separator || '-'
+ const baseUsername = usernameSegments.join(separator)
+
+ const potentialUsernames = []
+ for (let i = 0; i < 10; i++) {
+ const potentialUsername = `${baseUsername}${separator}${randomInt(100_000)}`
+ potentialUsernames.push(potentialUsername)
+ }
+
+ return findAvailableUsername(potentialUsernames)
+}
+
+// Checks the database for an unused username from an array provided and returns first.
+async function findAvailableUsername(potentialUsernames) {
+ const users = await prisma.{= userEntityLower =}.findMany({
+ where: {
+ username: { in: potentialUsernames },
+ }
+ })
+ const takenUsernames = users.map(user => user.username)
+ const availableUsernames = potentialUsernames.filter(username => !takenUsernames.includes(username))
+
+ if (availableUsernames.length === 0) {
+ throw new Error('Unable to generate a unique username. Please contact Wasp.')
+ }
+
+ return availableUsernames[0]
+}
+export default auth
diff --git a/waspc/data/Generator/templates/server/src/routes/auth/index.js b/waspc/data/Generator/templates/server/src/routes/auth/index.js
index 39bf2e0fb4..55436b476f 100644
--- a/waspc/data/Generator/templates/server/src/routes/auth/index.js
+++ b/waspc/data/Generator/templates/server/src/routes/auth/index.js
@@ -1,3 +1,4 @@
+{{={= =}=}}
import express from 'express'
import auth from '../../core/auth.js'
@@ -5,11 +6,18 @@ import login from './login.js'
import signup from './signup.js'
import me from './me.js'
+{=# isExternalAuthEnabled =}
+import passportAuth from './passport/passport.js'
+{=/ isExternalAuthEnabled =}
+
const router = express.Router()
router.post('/login', login)
router.post('/signup', signup)
router.get('/me', auth, me)
-export default router
+{=# isExternalAuthEnabled =}
+router.use('/external', passportAuth)
+{=/ isExternalAuthEnabled =}
+export default router
diff --git a/waspc/data/Generator/templates/server/src/routes/auth/me.js b/waspc/data/Generator/templates/server/src/routes/auth/me.js
index 3bdd2542ba..0e1346c055 100644
--- a/waspc/data/Generator/templates/server/src/routes/auth/me.js
+++ b/waspc/data/Generator/templates/server/src/routes/auth/me.js
@@ -5,6 +5,6 @@ export default handleRejection(async (req, res) => {
if (req.{= userEntityLower =}) {
return res.json(req.{= userEntityLower =})
} else {
- return res.status(403).send()
+ return res.status(401).send()
}
})
diff --git a/waspc/data/Generator/templates/server/src/routes/auth/passport/google/google.js b/waspc/data/Generator/templates/server/src/routes/auth/passport/google/google.js
new file mode 100644
index 0000000000..726fc013b0
--- /dev/null
+++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/google/google.js
@@ -0,0 +1,77 @@
+import express from 'express'
+import passport from 'passport'
+import GoogleStrategy from 'passport-google-oauth20'
+
+import waspServerConfig from '../../../../config.js'
+import { contextWithUserEntity, authConfig, findOrCreateUserByExternalAuthAssociation } from '../../utils.js'
+import { sign } from '../../../../core/auth.js'
+import { configFn, getUserFieldsFn } from './googleConfig.js'
+
+// Validates the provided config function returns all required data.
+const config = ((config) => {
+ if (!config?.clientId) {
+ throw new Error("auth.google.configFn must return an object with a clientId property.")
+ }
+
+ if (!config?.clientSecret) {
+ throw new Error("auth.google.configFn must return an object with a clientSecret property.")
+ }
+
+ if (!config?.scope) {
+ throw new Error("auth.google.configFn must return an object with a scope property.")
+ } else if (!Array.isArray(config.scope) || !config.scope.includes('profile')) {
+ throw new Error("auth.google.configFn returned an object with an invalid scope property. It must be an array including 'profile'.")
+ }
+
+ return config
+})(await configFn())
+
+passport.use('waspGoogleLoginStrategy', new GoogleStrategy({
+ clientID: config.clientId,
+ clientSecret: config.clientSecret,
+ callbackURL: `${waspServerConfig.frontendUrl}/auth/login/google`,
+ scope: config.scope,
+ passReqToCallback: true
+}, addGoogleProfileToRequest))
+
+// This function is invoked after we successfully exchange the one-time-use OAuth code for a real Google API token.
+// This token was used to get the Google profile information supplied as a parameter.
+// We add the Google profile to the request for downstream use.
+async function addGoogleProfileToRequest(req, _accessToken, _refreshToken, googleProfile, done) {
+ req.wasp = { ...req.wasp, googleProfile }
+
+ done(null, {})
+}
+
+const router = express.Router()
+
+// Constructs a Google OAuth URL and redirects browser to start sign in flow.
+router.get('/login', passport.authenticate('waspGoogleLoginStrategy', { session: false }))
+
+// Validates the OAuth code from the frontend, via server-to-server communication
+// with Google. If valid, provides frontend a response containing the JWT.
+// NOTE: `addGoogleProfileToRequest` is invoked as part of the `passport.authenticate`
+// call, before the final route handler callback. This is how we gain access to `req.wasp.googleProfile`.
+router.get('/validateCodeForLogin',
+ passport.authenticate('waspGoogleLoginStrategy', {
+ session: false,
+ failureRedirect: waspServerConfig.frontendUrl + authConfig.failureRedirectPath
+ }),
+ async function (req, res) {
+ const googleProfile = req?.wasp?.googleProfile
+
+ if (!googleProfile) {
+ throw new Error('Missing Google profile on request. This should not happen! Please contact Wasp.')
+ } else if (!googleProfile.id) {
+ throw new Error("Google profile was missing required id property. This should not happen! Please contact Wasp.")
+ }
+
+ // Wrap call to getUserFieldsFn so we can invoke only if needed.
+ const getUserFields = () => getUserFieldsFn(contextWithUserEntity, { profile: googleProfile })
+ const user = await findOrCreateUserByExternalAuthAssociation('google', googleProfile.id, getUserFields)
+
+ const token = await sign(user.id)
+ res.json({ token })
+ })
+
+export default router
diff --git a/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleConfig.js b/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleConfig.js
new file mode 100644
index 0000000000..0b2d2c9c7b
--- /dev/null
+++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleConfig.js
@@ -0,0 +1,17 @@
+{{={= =}=}}
+
+{=# doesConfigFnExist =}
+{=& configFnImportStatement =}
+export { {= configFnIdentifier =} as configFn }
+{=/ doesConfigFnExist =}
+{=^ doesConfigFnExist =}
+export { configFn } from './googleDefaults.js'
+{=/ doesConfigFnExist =}
+
+{=# doesOnSignInFnExist =}
+{=& getUserFieldsFnImportStatement =}
+export { {= getUserFieldsFnIdentifier =} as getUserFieldsFn }
+{=/ doesOnSignInFnExist =}
+{=^ doesOnSignInFnExist =}
+export { getUserFieldsFn } from './googleDefaults.js'
+{=/ doesOnSignInFnExist =}
diff --git a/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleDefaults.js b/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleDefaults.js
new file mode 100644
index 0000000000..09db94b871
--- /dev/null
+++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleDefaults.js
@@ -0,0 +1,23 @@
+import { generateAvailableDictionaryUsername } from '../../../../core/auth.js'
+
+// Default implementation if there is no `auth.methods.google.configFn`.
+export function configFn() {
+ const clientId = process.env['GOOGLE_CLIENT_ID']
+ const clientSecret = process.env['GOOGLE_CLIENT_SECRET']
+
+ if (!clientId) {
+ throw new Error("Missing GOOGLE_CLIENT_ID environment variable.")
+ }
+
+ if (!clientSecret) {
+ throw new Error("Missing GOOGLE_CLIENT_SECRET environment variable.")
+ }
+
+ return { clientId, clientSecret, scope: ['profile'] }
+}
+
+// Default implementation if there is no `auth.methods.google.getUserFieldsFn`.
+export async function getUserFieldsFn(_context, _args) {
+ const username = await generateAvailableDictionaryUsername()
+ return { username }
+}
diff --git a/waspc/data/Generator/templates/server/src/routes/auth/passport/passport.js b/waspc/data/Generator/templates/server/src/routes/auth/passport/passport.js
new file mode 100644
index 0000000000..fe8f820ee0
--- /dev/null
+++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/passport.js
@@ -0,0 +1,14 @@
+{{={= =}=}}
+import express from 'express'
+
+{=# isGoogleAuthEnabled =}
+import googleAuth from './google/google.js'
+{=/ isGoogleAuthEnabled =}
+
+const router = express.Router()
+
+{=# isGoogleAuthEnabled =}
+router.use('/google', googleAuth)
+{=/ isGoogleAuthEnabled =}
+
+export default router
diff --git a/waspc/data/Generator/templates/server/src/routes/auth/utils.js b/waspc/data/Generator/templates/server/src/routes/auth/utils.js
new file mode 100644
index 0000000000..ffa9fe77f2
--- /dev/null
+++ b/waspc/data/Generator/templates/server/src/routes/auth/utils.js
@@ -0,0 +1,42 @@
+{{={= =}=}}
+
+import { v4 as uuidv4 } from 'uuid'
+
+import prisma from '../../dbClient.js'
+
+export const contextWithUserEntity = {
+ entities: {
+ {= userEntityUpper =}: prisma.{= userEntityLower =}
+ }
+}
+
+export const authConfig = {
+ failureRedirectPath: "{= failureRedirectPath =}",
+ successRedirectPath: "{= successRedirectPath =}",
+}
+
+export async function findOrCreateUserByExternalAuthAssociation(provider, providerId, getUserFields) {
+ // Attempt to find a User by an external auth association.
+ const externalAuthAssociation = await prisma.{= externalAuthEntityLower =}.findFirst({
+ where: { provider, providerId },
+ include: { user: true }
+ })
+
+ if (externalAuthAssociation) {
+ return externalAuthAssociation.user
+ }
+
+ // No external auth association linkage found. Create a new User using details from
+ // `getUserFields()`. Additionally, associate the externalAuthAssociations with the new User.
+ // NOTE: For now, we force a random (uuidv4) password string. In the future, we will allow password reset.
+ const userFields = await getUserFields()
+ const userAndExternalAuthAssociation = {
+ ...userFields,
+ password: uuidv4(),
+ externalAuthAssociations: {
+ create: [{ provider, providerId }]
+ }
+ }
+
+ return await prisma.{= userEntityLower =}.create({ data: userAndExternalAuthAssociation })
+}
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums
index b441e4639f..a4a1b48c00 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/.waspchecksums
@@ -11,7 +11,7 @@
"file",
"Dockerfile"
],
- "93bb104c26d9c0e06b947bd9b2be153e579f599590d3fa8876db2ca777ce5183"
+ "faae4a6f87557e624d9c7631ec6f3ed20e31115f2c177bfe19b3f52d163d86e9"
],
[
[
@@ -46,21 +46,21 @@
"file",
"server/package.json"
],
- "9c3186abd79e11706fd56699054b653303dbae26b3b7037a2104377d697b7b22"
+ "f737ca2e276090fdbc46be2279b41d50973c024bb31259a45206a6cce7c469e9"
],
[
[
"file",
"server/src/app.js"
],
- "31bf8e67e2bc95b2558c649fdb51e6c7a6d373cbc8488141ab8652e8d69604ce"
+ "1e802078a0c6738f9dc2dc8f1739120d28fdc3d6fdc8029671ec9aed73c8ed72"
],
[
[
"file",
"server/src/config.js"
],
- "beed84b80d5bbb90c6d02781cf03d7b1f7192b1e1eeda01becfefa57aa69dc50"
+ "d7f23a370f15869e6a3920552993c41d5e1a23d8e12a4a9d3a583d22b730a5d6"
],
[
[
@@ -207,7 +207,7 @@
"file",
"web-app/package.json"
],
- "bf1d166d30b2b04b50180276c8c34d7cae29e31ce11635e0199e2fde537e46ce"
+ "9aaa97208e8b524bf6c15fe9004e99564bfd0e8a3942e2ba7156795634b0f0bb"
],
[
[
@@ -333,7 +333,7 @@
"file",
"web-app/src/router.js"
],
- "959a870fd08b7bc2eabf397750256946e26960e08fb2887ec65ab5fc775aebfc"
+ "dd6f5f5c6981df1935c02437d7a6ae343841300c3f7e99b0ad6ca62e95071421"
],
[
[
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/Dockerfile b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/Dockerfile
index ec86950447..6711e7a7b8 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/Dockerfile
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:14-alpine AS node
+FROM node:16-alpine AS node
FROM node AS base
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/installedFullStackNpmDependencies.json b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/installedFullStackNpmDependencies.json
index ba3f0505a0..435173c51e 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/installedFullStackNpmDependencies.json
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/installedFullStackNpmDependencies.json
@@ -1 +1 @@
-{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.4"},{"name":"cors","version":"^2.8.5"},{"name":"debug","version":"~2.6.9"},{"name":"express","version":"~4.16.1"},{"name":"morgan","version":"~1.9.1"},{"name":"@prisma/client","version":"3.15.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"8.2.0"},{"name":"helmet","version":"^4.6.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.4"},{"name":"standard","version":"^14.3.4"},{"name":"prisma","version":"3.15.2"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.21.1"},{"name":"lodash","version":"^4.17.15"},{"name":"react","version":"^16.12.0"},{"name":"react-dom","version":"^16.12.0"},{"name":"react-query","version":"^3.34.19"},{"name":"react-router-dom","version":"^5.1.2"},{"name":"react-scripts","version":"4.0.3"},{"name":"uuid","version":"^3.4.0"},{"name":"react-error-overlay","version":"6.0.9"}],"devDependencies":[]}}
\ No newline at end of file
+{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.4"},{"name":"cors","version":"^2.8.5"},{"name":"debug","version":"~2.6.9"},{"name":"express","version":"~4.16.1"},{"name":"morgan","version":"~1.9.1"},{"name":"@prisma/client","version":"3.15.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"8.2.0"},{"name":"helmet","version":"^4.6.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^8.3.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.4"},{"name":"standard","version":"^14.3.4"},{"name":"prisma","version":"3.15.2"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.21.1"},{"name":"lodash","version":"^4.17.15"},{"name":"react","version":"^16.12.0"},{"name":"react-dom","version":"^16.12.0"},{"name":"react-query","version":"^3.34.19"},{"name":"react-router-dom","version":"^5.1.2"},{"name":"react-scripts","version":"4.0.3"},{"name":"react-error-overlay","version":"6.0.9"}],"devDependencies":[]}}
\ No newline at end of file
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/package.json b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/package.json
index 7666c12c9e..73fe45996f 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/package.json
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/package.json
@@ -9,7 +9,9 @@
"helmet": "^4.6.0",
"jsonwebtoken": "^8.5.1",
"morgan": "~1.9.1",
- "secure-password": "^4.0.0"
+ "patch-package": "^6.4.7",
+ "secure-password": "^4.0.0",
+ "uuid": "^8.3.2"
},
"devDependencies": {
"nodemon": "^2.0.4",
@@ -30,6 +32,7 @@
"db-migrate-dev": "prisma migrate dev --schema=../db/schema.prisma",
"db-migrate-prod": "prisma migrate deploy --schema=../db/schema.prisma",
"debug": "DEBUG=server:* npm start",
+ "postinstall": "patch-package",
"standard": "standard",
"start": "nodemon -r dotenv/config ./src/server.js",
"start-production": "NODE_ENV=production node ./src/server.js"
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/app.js b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/app.js
index 6c769e0cd1..7ffcd317d9 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/app.js
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/app.js
@@ -6,6 +6,7 @@ import helmet from 'helmet'
import HttpError from './core/HttpError.js'
import indexRouter from './routes/index.js'
+import config from './config.js'
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
// it can be used in unit tests to test each route individually.
@@ -13,7 +14,10 @@ import indexRouter from './routes/index.js'
const app = express()
app.use(helmet())
-app.use(cors()) // TODO: Consider configuring CORS to be more restrictive, right now it allows all CORS requests.
+app.use(cors({
+ // TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
+ origin: config.frontendUrl,
+}))
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/config.js b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/config.js
index a7958f3a3f..2769066bf7 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/config.js
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/server/src/config.js
@@ -12,10 +12,13 @@ const config = {
env,
port: parseInt(process.env.PORT) || 3001,
databaseUrl: process.env.DATABASE_URL,
+ frontendUrl: undefined,
},
development: {
+ frontendUrl: process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000',
},
production: {
+ frontendUrl: process.env.WASP_WEB_CLIENT_URL,
}
}
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/package.json b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/package.json
index 4b45bbb759..de402cc5ce 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/package.json
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/package.json
@@ -19,8 +19,7 @@
"react-error-overlay": "6.0.9",
"react-query": "^3.34.19",
"react-router-dom": "^5.1.2",
- "react-scripts": "4.0.3",
- "uuid": "^3.4.0"
+ "react-scripts": "4.0.3"
},
"devDependencies": {},
"engineStrict": true,
diff --git a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/router.js b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/router.js
index 5516a073dd..c783ed20ac 100644
--- a/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/router.js
+++ b/waspc/e2e-test/test-outputs/waspBuild-golden/waspBuild/.wasp/build/web-app/src/router.js
@@ -9,6 +9,7 @@ const router = (
+
)
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums
index 066f263bc2..135fb35f85 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/.waspchecksums
@@ -11,7 +11,7 @@
"file",
"Dockerfile"
],
- "93bb104c26d9c0e06b947bd9b2be153e579f599590d3fa8876db2ca777ce5183"
+ "faae4a6f87557e624d9c7631ec6f3ed20e31115f2c177bfe19b3f52d163d86e9"
],
[
[
@@ -46,21 +46,21 @@
"file",
"server/package.json"
],
- "9c3186abd79e11706fd56699054b653303dbae26b3b7037a2104377d697b7b22"
+ "f737ca2e276090fdbc46be2279b41d50973c024bb31259a45206a6cce7c469e9"
],
[
[
"file",
"server/src/app.js"
],
- "31bf8e67e2bc95b2558c649fdb51e6c7a6d373cbc8488141ab8652e8d69604ce"
+ "1e802078a0c6738f9dc2dc8f1739120d28fdc3d6fdc8029671ec9aed73c8ed72"
],
[
[
"file",
"server/src/config.js"
],
- "beed84b80d5bbb90c6d02781cf03d7b1f7192b1e1eeda01becfefa57aa69dc50"
+ "d7f23a370f15869e6a3920552993c41d5e1a23d8e12a4a9d3a583d22b730a5d6"
],
[
[
@@ -207,7 +207,7 @@
"file",
"web-app/package.json"
],
- "8d5e3c23f6fdfe422e307d0dee0e5503d1584fa47a4cff5ca4299e653a6b0fb6"
+ "0087b0093eca5a67c4e66948f0658f7d18ab371fee633e913877a468715e7d16"
],
[
[
@@ -333,7 +333,7 @@
"file",
"web-app/src/router.js"
],
- "959a870fd08b7bc2eabf397750256946e26960e08fb2887ec65ab5fc775aebfc"
+ "dd6f5f5c6981df1935c02437d7a6ae343841300c3f7e99b0ad6ca62e95071421"
],
[
[
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/Dockerfile b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/Dockerfile
index ec86950447..6711e7a7b8 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/Dockerfile
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:14-alpine AS node
+FROM node:16-alpine AS node
FROM node AS base
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/installedFullStackNpmDependencies.json b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/installedFullStackNpmDependencies.json
index ba3f0505a0..435173c51e 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/installedFullStackNpmDependencies.json
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/installedFullStackNpmDependencies.json
@@ -1 +1 @@
-{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.4"},{"name":"cors","version":"^2.8.5"},{"name":"debug","version":"~2.6.9"},{"name":"express","version":"~4.16.1"},{"name":"morgan","version":"~1.9.1"},{"name":"@prisma/client","version":"3.15.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"8.2.0"},{"name":"helmet","version":"^4.6.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.4"},{"name":"standard","version":"^14.3.4"},{"name":"prisma","version":"3.15.2"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.21.1"},{"name":"lodash","version":"^4.17.15"},{"name":"react","version":"^16.12.0"},{"name":"react-dom","version":"^16.12.0"},{"name":"react-query","version":"^3.34.19"},{"name":"react-router-dom","version":"^5.1.2"},{"name":"react-scripts","version":"4.0.3"},{"name":"uuid","version":"^3.4.0"},{"name":"react-error-overlay","version":"6.0.9"}],"devDependencies":[]}}
\ No newline at end of file
+{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.4"},{"name":"cors","version":"^2.8.5"},{"name":"debug","version":"~2.6.9"},{"name":"express","version":"~4.16.1"},{"name":"morgan","version":"~1.9.1"},{"name":"@prisma/client","version":"3.15.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"8.2.0"},{"name":"helmet","version":"^4.6.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^8.3.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.4"},{"name":"standard","version":"^14.3.4"},{"name":"prisma","version":"3.15.2"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.21.1"},{"name":"lodash","version":"^4.17.15"},{"name":"react","version":"^16.12.0"},{"name":"react-dom","version":"^16.12.0"},{"name":"react-query","version":"^3.34.19"},{"name":"react-router-dom","version":"^5.1.2"},{"name":"react-scripts","version":"4.0.3"},{"name":"react-error-overlay","version":"6.0.9"}],"devDependencies":[]}}
\ No newline at end of file
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/package.json b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/package.json
index 7666c12c9e..73fe45996f 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/package.json
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/package.json
@@ -9,7 +9,9 @@
"helmet": "^4.6.0",
"jsonwebtoken": "^8.5.1",
"morgan": "~1.9.1",
- "secure-password": "^4.0.0"
+ "patch-package": "^6.4.7",
+ "secure-password": "^4.0.0",
+ "uuid": "^8.3.2"
},
"devDependencies": {
"nodemon": "^2.0.4",
@@ -30,6 +32,7 @@
"db-migrate-dev": "prisma migrate dev --schema=../db/schema.prisma",
"db-migrate-prod": "prisma migrate deploy --schema=../db/schema.prisma",
"debug": "DEBUG=server:* npm start",
+ "postinstall": "patch-package",
"standard": "standard",
"start": "nodemon -r dotenv/config ./src/server.js",
"start-production": "NODE_ENV=production node ./src/server.js"
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/app.js b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/app.js
index 6c769e0cd1..7ffcd317d9 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/app.js
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/app.js
@@ -6,6 +6,7 @@ import helmet from 'helmet'
import HttpError from './core/HttpError.js'
import indexRouter from './routes/index.js'
+import config from './config.js'
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
// it can be used in unit tests to test each route individually.
@@ -13,7 +14,10 @@ import indexRouter from './routes/index.js'
const app = express()
app.use(helmet())
-app.use(cors()) // TODO: Consider configuring CORS to be more restrictive, right now it allows all CORS requests.
+app.use(cors({
+ // TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
+ origin: config.frontendUrl,
+}))
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/config.js b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/config.js
index a7958f3a3f..2769066bf7 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/config.js
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/server/src/config.js
@@ -12,10 +12,13 @@ const config = {
env,
port: parseInt(process.env.PORT) || 3001,
databaseUrl: process.env.DATABASE_URL,
+ frontendUrl: undefined,
},
development: {
+ frontendUrl: process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000',
},
production: {
+ frontendUrl: process.env.WASP_WEB_CLIENT_URL,
}
}
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/package.json b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/package.json
index 68ae733c56..1f141ebe00 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/package.json
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/package.json
@@ -19,8 +19,7 @@
"react-error-overlay": "6.0.9",
"react-query": "^3.34.19",
"react-router-dom": "^5.1.2",
- "react-scripts": "4.0.3",
- "uuid": "^3.4.0"
+ "react-scripts": "4.0.3"
},
"devDependencies": {},
"engineStrict": true,
diff --git a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/router.js b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/router.js
index 5516a073dd..c783ed20ac 100644
--- a/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/router.js
+++ b/waspc/e2e-test/test-outputs/waspCompile-golden/waspCompile/.wasp/out/web-app/src/router.js
@@ -9,6 +9,7 @@ const router = (
+
)
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums
index 910ed85a35..d83c2056ed 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/.waspchecksums
@@ -11,7 +11,7 @@
"file",
"Dockerfile"
],
- "93bb104c26d9c0e06b947bd9b2be153e579f599590d3fa8876db2ca777ce5183"
+ "faae4a6f87557e624d9c7631ec6f3ed20e31115f2c177bfe19b3f52d163d86e9"
],
[
[
@@ -46,21 +46,21 @@
"file",
"server/package.json"
],
- "aed180a5eb3804680a90c96e24aa9ae4f78a3c64f46976fccab5b25f2e9e2992"
+ "fa2e6f47b8fbf4dd35d272992b39138942823a3be759a429373debfd049a153d"
],
[
[
"file",
"server/src/app.js"
],
- "31bf8e67e2bc95b2558c649fdb51e6c7a6d373cbc8488141ab8652e8d69604ce"
+ "1e802078a0c6738f9dc2dc8f1739120d28fdc3d6fdc8029671ec9aed73c8ed72"
],
[
[
"file",
"server/src/config.js"
],
- "beed84b80d5bbb90c6d02781cf03d7b1f7192b1e1eeda01becfefa57aa69dc50"
+ "d7f23a370f15869e6a3920552993c41d5e1a23d8e12a4a9d3a583d22b730a5d6"
],
[
[
@@ -221,7 +221,7 @@
"file",
"web-app/package.json"
],
- "5b5fd1238f4ab4e031b9dde5967358a8d704d9028938071393293b07e906da10"
+ "1dcfe86b9c226cbaa291136e8df6a9598486f6954ee40701f111ded71a09d93c"
],
[
[
@@ -354,7 +354,7 @@
"file",
"web-app/src/router.js"
],
- "959a870fd08b7bc2eabf397750256946e26960e08fb2887ec65ab5fc775aebfc"
+ "dd6f5f5c6981df1935c02437d7a6ae343841300c3f7e99b0ad6ca62e95071421"
],
[
[
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/Dockerfile b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/Dockerfile
index ec86950447..6711e7a7b8 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/Dockerfile
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:14-alpine AS node
+FROM node:16-alpine AS node
FROM node AS base
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/installedFullStackNpmDependencies.json b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/installedFullStackNpmDependencies.json
index 4f4403bf9a..1a03167950 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/installedFullStackNpmDependencies.json
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/installedFullStackNpmDependencies.json
@@ -1 +1 @@
-{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.4"},{"name":"cors","version":"^2.8.5"},{"name":"debug","version":"~2.6.9"},{"name":"express","version":"~4.16.1"},{"name":"morgan","version":"~1.9.1"},{"name":"@prisma/client","version":"3.15.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"8.2.0"},{"name":"helmet","version":"^4.6.0"},{"name":"pg-boss","version":"^7.2.1"}],"devDependencies":[{"name":"nodemon","version":"^2.0.4"},{"name":"standard","version":"^14.3.4"},{"name":"prisma","version":"3.15.2"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.21.1"},{"name":"lodash","version":"^4.17.15"},{"name":"react","version":"^16.12.0"},{"name":"react-dom","version":"^16.12.0"},{"name":"react-query","version":"^3.34.19"},{"name":"react-router-dom","version":"^5.1.2"},{"name":"react-scripts","version":"4.0.3"},{"name":"uuid","version":"^3.4.0"},{"name":"react-error-overlay","version":"6.0.9"}],"devDependencies":[]}}
\ No newline at end of file
+{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.4"},{"name":"cors","version":"^2.8.5"},{"name":"debug","version":"~2.6.9"},{"name":"express","version":"~4.16.1"},{"name":"morgan","version":"~1.9.1"},{"name":"@prisma/client","version":"3.15.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"8.2.0"},{"name":"helmet","version":"^4.6.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^8.3.2"},{"name":"pg-boss","version":"^7.2.1"}],"devDependencies":[{"name":"nodemon","version":"^2.0.4"},{"name":"standard","version":"^14.3.4"},{"name":"prisma","version":"3.15.2"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.21.1"},{"name":"lodash","version":"^4.17.15"},{"name":"react","version":"^16.12.0"},{"name":"react-dom","version":"^16.12.0"},{"name":"react-query","version":"^3.34.19"},{"name":"react-router-dom","version":"^5.1.2"},{"name":"react-scripts","version":"4.0.3"},{"name":"react-error-overlay","version":"6.0.9"}],"devDependencies":[]}}
\ No newline at end of file
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/package.json b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/package.json
index d3cb87c265..ac2ba02c6a 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/package.json
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/package.json
@@ -9,8 +9,10 @@
"helmet": "^4.6.0",
"jsonwebtoken": "^8.5.1",
"morgan": "~1.9.1",
+ "patch-package": "^6.4.7",
"pg-boss": "^7.2.1",
- "secure-password": "^4.0.0"
+ "secure-password": "^4.0.0",
+ "uuid": "^8.3.2"
},
"devDependencies": {
"nodemon": "^2.0.4",
@@ -31,6 +33,7 @@
"db-migrate-dev": "prisma migrate dev --schema=../db/schema.prisma",
"db-migrate-prod": "prisma migrate deploy --schema=../db/schema.prisma",
"debug": "DEBUG=server:* npm start",
+ "postinstall": "patch-package",
"standard": "standard",
"start": "nodemon -r dotenv/config ./src/server.js",
"start-production": "NODE_ENV=production node ./src/server.js"
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/app.js b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/app.js
index 6c769e0cd1..7ffcd317d9 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/app.js
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/app.js
@@ -6,6 +6,7 @@ import helmet from 'helmet'
import HttpError from './core/HttpError.js'
import indexRouter from './routes/index.js'
+import config from './config.js'
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
// it can be used in unit tests to test each route individually.
@@ -13,7 +14,10 @@ import indexRouter from './routes/index.js'
const app = express()
app.use(helmet())
-app.use(cors()) // TODO: Consider configuring CORS to be more restrictive, right now it allows all CORS requests.
+app.use(cors({
+ // TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
+ origin: config.frontendUrl,
+}))
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/config.js b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/config.js
index a7958f3a3f..2769066bf7 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/config.js
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/server/src/config.js
@@ -12,10 +12,13 @@ const config = {
env,
port: parseInt(process.env.PORT) || 3001,
databaseUrl: process.env.DATABASE_URL,
+ frontendUrl: undefined,
},
development: {
+ frontendUrl: process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000',
},
production: {
+ frontendUrl: process.env.WASP_WEB_CLIENT_URL,
}
}
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/package.json b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/package.json
index 9fba8112a2..451b88e760 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/package.json
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/package.json
@@ -19,8 +19,7 @@
"react-error-overlay": "6.0.9",
"react-query": "^3.34.19",
"react-router-dom": "^5.1.2",
- "react-scripts": "4.0.3",
- "uuid": "^3.4.0"
+ "react-scripts": "4.0.3"
},
"devDependencies": {},
"engineStrict": true,
diff --git a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/router.js b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/router.js
index 5516a073dd..c783ed20ac 100644
--- a/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/router.js
+++ b/waspc/e2e-test/test-outputs/waspJob-golden/waspJob/.wasp/out/web-app/src/router.js
@@ -9,6 +9,7 @@ const router = (
+
)
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums
index 17c4c7cad5..444d69f2a8 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/.waspchecksums
@@ -11,7 +11,7 @@
"file",
"Dockerfile"
],
- "a2e26d43bffd368f037c0ed7cb0362331b436c8beb36f952251212564cc2b8df"
+ "f94ec7a7e7db084cdda6a0860d68693252da43d7ddd28ba990222ea0831c5467"
],
[
[
@@ -46,21 +46,21 @@
"file",
"server/package.json"
],
- "c1483b8ee9fb6b7b19575ee3327d88a92a3be99427a4714b28dfa2c2207434fa"
+ "532a3c9ba7889699a4974d717dbdc33dfb2167710a788df3f1c3dbd8641dc064"
],
[
[
"file",
"server/src/app.js"
],
- "31bf8e67e2bc95b2558c649fdb51e6c7a6d373cbc8488141ab8652e8d69604ce"
+ "1e802078a0c6738f9dc2dc8f1739120d28fdc3d6fdc8029671ec9aed73c8ed72"
],
[
[
"file",
"server/src/config.js"
],
- "beed84b80d5bbb90c6d02781cf03d7b1f7192b1e1eeda01becfefa57aa69dc50"
+ "d7f23a370f15869e6a3920552993c41d5e1a23d8e12a4a9d3a583d22b730a5d6"
],
[
[
@@ -207,7 +207,7 @@
"file",
"web-app/package.json"
],
- "b3843b68d443332680fe36af1262ed79a76b1fb600f91153adf1abbe60902ea7"
+ "ced9ff47676f583d95a381dfabaa7a5b17bae875dca080b9cd4f5f6fa6321ec4"
],
[
[
@@ -333,7 +333,7 @@
"file",
"web-app/src/router.js"
],
- "959a870fd08b7bc2eabf397750256946e26960e08fb2887ec65ab5fc775aebfc"
+ "dd6f5f5c6981df1935c02437d7a6ae343841300c3f7e99b0ad6ca62e95071421"
],
[
[
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/Dockerfile b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/Dockerfile
index 9c3e1644cf..864e12bb78 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/Dockerfile
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:14-alpine AS node
+FROM node:16-alpine AS node
FROM node AS base
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/installedFullStackNpmDependencies.json b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/installedFullStackNpmDependencies.json
index ba3f0505a0..435173c51e 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/installedFullStackNpmDependencies.json
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/installedFullStackNpmDependencies.json
@@ -1 +1 @@
-{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.4"},{"name":"cors","version":"^2.8.5"},{"name":"debug","version":"~2.6.9"},{"name":"express","version":"~4.16.1"},{"name":"morgan","version":"~1.9.1"},{"name":"@prisma/client","version":"3.15.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"8.2.0"},{"name":"helmet","version":"^4.6.0"}],"devDependencies":[{"name":"nodemon","version":"^2.0.4"},{"name":"standard","version":"^14.3.4"},{"name":"prisma","version":"3.15.2"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.21.1"},{"name":"lodash","version":"^4.17.15"},{"name":"react","version":"^16.12.0"},{"name":"react-dom","version":"^16.12.0"},{"name":"react-query","version":"^3.34.19"},{"name":"react-router-dom","version":"^5.1.2"},{"name":"react-scripts","version":"4.0.3"},{"name":"uuid","version":"^3.4.0"},{"name":"react-error-overlay","version":"6.0.9"}],"devDependencies":[]}}
\ No newline at end of file
+{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.4"},{"name":"cors","version":"^2.8.5"},{"name":"debug","version":"~2.6.9"},{"name":"express","version":"~4.16.1"},{"name":"morgan","version":"~1.9.1"},{"name":"@prisma/client","version":"3.15.2"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"8.2.0"},{"name":"helmet","version":"^4.6.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^8.3.2"}],"devDependencies":[{"name":"nodemon","version":"^2.0.4"},{"name":"standard","version":"^14.3.4"},{"name":"prisma","version":"3.15.2"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.21.1"},{"name":"lodash","version":"^4.17.15"},{"name":"react","version":"^16.12.0"},{"name":"react-dom","version":"^16.12.0"},{"name":"react-query","version":"^3.34.19"},{"name":"react-router-dom","version":"^5.1.2"},{"name":"react-scripts","version":"4.0.3"},{"name":"react-error-overlay","version":"6.0.9"}],"devDependencies":[]}}
\ No newline at end of file
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/package.json b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/package.json
index 9d1818492a..34e3f356f4 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/package.json
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/package.json
@@ -9,7 +9,9 @@
"helmet": "^4.6.0",
"jsonwebtoken": "^8.5.1",
"morgan": "~1.9.1",
- "secure-password": "^4.0.0"
+ "patch-package": "^6.4.7",
+ "secure-password": "^4.0.0",
+ "uuid": "^8.3.2"
},
"devDependencies": {
"nodemon": "^2.0.4",
@@ -30,6 +32,7 @@
"db-migrate-dev": "prisma migrate dev --schema=../db/schema.prisma",
"db-migrate-prod": "prisma migrate deploy --schema=../db/schema.prisma",
"debug": "DEBUG=server:* npm start",
+ "postinstall": "patch-package",
"standard": "standard",
"start": "nodemon -r dotenv/config ./src/server.js",
"start-production": "npm run db-migrate-prod && NODE_ENV=production node ./src/server.js"
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/app.js b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/app.js
index 6c769e0cd1..7ffcd317d9 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/app.js
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/app.js
@@ -6,6 +6,7 @@ import helmet from 'helmet'
import HttpError from './core/HttpError.js'
import indexRouter from './routes/index.js'
+import config from './config.js'
// TODO: Consider extracting most of this logic into createApp(routes, path) function so that
// it can be used in unit tests to test each route individually.
@@ -13,7 +14,10 @@ import indexRouter from './routes/index.js'
const app = express()
app.use(helmet())
-app.use(cors()) // TODO: Consider configuring CORS to be more restrictive, right now it allows all CORS requests.
+app.use(cors({
+ // TODO: Consider allowing users to provide an ENV variable or function to further configure CORS setup.
+ origin: config.frontendUrl,
+}))
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/config.js b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/config.js
index a7958f3a3f..2769066bf7 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/config.js
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/server/src/config.js
@@ -12,10 +12,13 @@ const config = {
env,
port: parseInt(process.env.PORT) || 3001,
databaseUrl: process.env.DATABASE_URL,
+ frontendUrl: undefined,
},
development: {
+ frontendUrl: process.env.WASP_WEB_CLIENT_URL || 'http://localhost:3000',
},
production: {
+ frontendUrl: process.env.WASP_WEB_CLIENT_URL,
}
}
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/package.json b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/package.json
index 64c6866a7a..4207392fa0 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/package.json
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/package.json
@@ -19,8 +19,7 @@
"react-error-overlay": "6.0.9",
"react-query": "^3.34.19",
"react-router-dom": "^5.1.2",
- "react-scripts": "4.0.3",
- "uuid": "^3.4.0"
+ "react-scripts": "4.0.3"
},
"devDependencies": {},
"engineStrict": true,
diff --git a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/router.js b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/router.js
index 5516a073dd..c783ed20ac 100644
--- a/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/router.js
+++ b/waspc/e2e-test/test-outputs/waspMigrate-golden/waspMigrate/.wasp/out/web-app/src/router.js
@@ -9,6 +9,7 @@ const router = (
+
)
diff --git a/waspc/examples/todoApp/ext/auth/google.js b/waspc/examples/todoApp/ext/auth/google.js
new file mode 100644
index 0000000000..283153617f
--- /dev/null
+++ b/waspc/examples/todoApp/ext/auth/google.js
@@ -0,0 +1,16 @@
+import { generateAvailableUsername } from '@wasp/core/auth.js'
+
+export function config() {
+ console.log("Inside user-supplied Google config")
+ return {
+ clientId: process.env['GOOGLE_CLIENT_ID'],
+ clientSecret: process.env['GOOGLE_CLIENT_SECRET'],
+ scope: ['profile']
+ }
+}
+
+export async function getUserFields(_context, args) {
+ console.log("Inside user-supplied Google getUserFields")
+ const username = await generateAvailableUsername(args.profile.displayName.split(' '), { separator: '.' })
+ return { username }
+}
diff --git a/waspc/examples/todoApp/ext/pages/auth/Login.js b/waspc/examples/todoApp/ext/pages/auth/Login.js
index 54f3fd7fef..9945e8c77c 100644
--- a/waspc/examples/todoApp/ext/pages/auth/Login.js
+++ b/waspc/examples/todoApp/ext/pages/auth/Login.js
@@ -2,6 +2,7 @@ import React from 'react'
import { Link } from 'react-router-dom'
import LoginForm from '@wasp/auth/forms/Login'
+// import { GoogleSignInButton } from '@wasp/auth/buttons/Google'
const Login = () => {
return (
@@ -11,6 +12,10 @@ const Login = () => {
I don't have an account yet (go to signup).
+
+ {/*
+
+
*/}
>
)
}
diff --git a/waspc/examples/todoApp/migrations/20220822154342_add_social_login/migration.sql b/waspc/examples/todoApp/migrations/20220822154342_add_social_login/migration.sql
new file mode 100644
index 0000000000..0ae1754d7d
--- /dev/null
+++ b/waspc/examples/todoApp/migrations/20220822154342_add_social_login/migration.sql
@@ -0,0 +1,16 @@
+-- CreateTable
+CREATE TABLE "SocialLogin" (
+ "id" SERIAL 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")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "SocialLogin_provider_providerId_userId_key" ON "SocialLogin"("provider", "providerId", "userId");
+
+-- AddForeignKey
+ALTER TABLE "SocialLogin" ADD CONSTRAINT "SocialLogin_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/waspc/examples/todoApp/todoApp.wasp b/waspc/examples/todoApp/todoApp.wasp
index d773f46d3d..6bc48af3df 100644
--- a/waspc/examples/todoApp/todoApp.wasp
+++ b/waspc/examples/todoApp/todoApp.wasp
@@ -8,7 +8,14 @@ app todoApp {
],
auth: {
userEntity: User,
- methods: [ UsernameAndPassword ],
+ // externalAuthEntity: SocialLogin,
+ methods: {
+ usernameAndPassword: {},
+ // google: {
+ // configFn: import { config } from "@ext/auth/google.js",
+ // getUserFieldsFn: import { getUserFields } from "@ext/auth/google.js"
+ // }
+ },
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/profile"
},
@@ -24,10 +31,21 @@ app todoApp {
}
entity User {=psl
- id Int @id @default(autoincrement())
- username String @unique
- password String
- tasks Task[]
+ id Int @id @default(autoincrement())
+ username String @unique
+ password String
+ tasks Task[]
+ 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=}
entity Task {=psl
@@ -38,7 +56,6 @@ entity Task {=psl
userId Int
psl=}
-
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import Signup from "@ext/pages/auth/Signup"
@@ -131,7 +148,7 @@ job mySpecialScheduledJob {
fn: import { foo } from "@ext/jobs/bar.js"
},
schedule: {
- cron: "*/2 * * * *",
+ cron: "0 * * * *",
args: {=json { "foo": "bar" } json=},
executorOptions: {
pgBoss: {=json { "retryLimit": 2 } json=}
diff --git a/waspc/src/Wasp/Analyzer/StdTypeDefinitions.hs b/waspc/src/Wasp/Analyzer/StdTypeDefinitions.hs
index 34db44e5a8..030f47366d 100644
--- a/waspc/src/Wasp/Analyzer/StdTypeDefinitions.hs
+++ b/waspc/src/Wasp/Analyzer/StdTypeDefinitions.hs
@@ -12,7 +12,6 @@ import qualified Wasp.Analyzer.TypeDefinitions as TD
import Wasp.Analyzer.TypeDefinitions.TH (makeDeclType, makeEnumType)
import Wasp.AppSpec.Action (Action)
import Wasp.AppSpec.App (App)
-import Wasp.AppSpec.App.Auth (AuthMethod)
import Wasp.AppSpec.App.Db (DbSystem)
import Wasp.AppSpec.Entity (Entity)
import Wasp.AppSpec.Job (Job, JobExecutor)
@@ -20,7 +19,6 @@ import Wasp.AppSpec.Page (Page)
import Wasp.AppSpec.Query (Query)
import Wasp.AppSpec.Route (Route)
-makeEnumType ''AuthMethod
makeEnumType ''DbSystem
makeDeclType ''App
makeDeclType ''Page
@@ -37,7 +35,6 @@ makeDeclType ''Job
stdTypes :: TD.TypeDefinitions
stdTypes =
TD.addDeclType @App $
- TD.addEnumType @AuthMethod $
TD.addEnumType @DbSystem $
TD.addDeclType @Entity $
TD.addDeclType @Page $
diff --git a/waspc/src/Wasp/AppSpec/App/Auth.hs b/waspc/src/Wasp/AppSpec/App/Auth.hs
index 9b59e184e2..648c959a8f 100644
--- a/waspc/src/Wasp/AppSpec/App/Auth.hs
+++ b/waspc/src/Wasp/AppSpec/App/Auth.hs
@@ -1,21 +1,58 @@
{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DuplicateRecordFields #-}
module Wasp.AppSpec.App.Auth
( Auth (..),
- AuthMethod (..),
+ AuthMethods (..),
+ GoogleConfig (..),
+ usernameAndPasswordConfig,
+ isUsernameAndPasswordAuthEnabled,
+ isGoogleAuthEnabled,
+ isExternalAuthEnabled,
)
where
import Data.Data (Data)
+import Data.Maybe (isJust)
import Wasp.AppSpec.Core.Ref (Ref)
import Wasp.AppSpec.Entity (Entity)
+import Wasp.AppSpec.ExtImport (ExtImport)
data Auth = Auth
{ userEntity :: Ref Entity,
- methods :: [AuthMethod],
+ externalAuthEntity :: Maybe (Ref Entity),
+ methods :: AuthMethods,
onAuthFailedRedirectTo :: String,
onAuthSucceededRedirectTo :: Maybe String
}
deriving (Show, Eq, Data)
-data AuthMethod = UsernameAndPassword deriving (Show, Eq, Data)
+data AuthMethods = AuthMethods
+ { usernameAndPassword :: Maybe UsernameAndPasswordConfig,
+ google :: Maybe GoogleConfig
+ }
+ deriving (Show, Eq, Data)
+
+data UsernameAndPasswordConfig = UsernameAndPasswordConfig
+ { -- NOTE: Not used right now, but Analyzer does not support an empty data type.
+ configFn :: Maybe ExtImport
+ }
+ deriving (Show, Eq, Data)
+
+data GoogleConfig = GoogleConfig
+ { configFn :: Maybe ExtImport,
+ getUserFieldsFn :: Maybe ExtImport
+ }
+ deriving (Show, Eq, Data)
+
+usernameAndPasswordConfig :: UsernameAndPasswordConfig
+usernameAndPasswordConfig = UsernameAndPasswordConfig Nothing
+
+isUsernameAndPasswordAuthEnabled :: Auth -> Bool
+isUsernameAndPasswordAuthEnabled = isJust . usernameAndPassword . methods
+
+isGoogleAuthEnabled :: Auth -> Bool
+isGoogleAuthEnabled = isJust . google . methods
+
+isExternalAuthEnabled :: Auth -> Bool
+isExternalAuthEnabled auth = any ($ auth) [isGoogleAuthEnabled]
diff --git a/waspc/src/Wasp/AppSpec/Valid.hs b/waspc/src/Wasp/AppSpec/Valid.hs
index 68adfdf5fb..9d57f52ced 100644
--- a/waspc/src/Wasp/AppSpec/Valid.hs
+++ b/waspc/src/Wasp/AppSpec/Valid.hs
@@ -35,6 +35,7 @@ validateAppSpec spec =
concat
[ validateAppAuthIsSetIfAnyPageRequiresAuth spec,
validateAuthUserEntityHasCorrectFieldsIfUsernameAndPasswordAuthIsUsed spec,
+ validateExternalAuthEntityHasCorrectFieldsIfExternalAuthIsUsed spec,
validateDbIsPostgresIfPgBossUsed spec
]
@@ -50,52 +51,74 @@ validateExactlyOneAppExists spec =
validateAppAuthIsSetIfAnyPageRequiresAuth :: AppSpec -> [ValidationError]
validateAppAuthIsSetIfAnyPageRequiresAuth spec =
- if anyPageRequiresAuth && not (isAuthEnabled spec)
- then
- [ GenericValidationError
- "Expected app.auth to be defined since there are Pages with authRequired set to true."
- ]
- else []
+ [ GenericValidationError
+ "Expected app.auth to be defined since there are Pages with authRequired set to true."
+ | anyPageRequiresAuth && not (isAuthEnabled spec)
+ ]
where
anyPageRequiresAuth = any ((== Just True) . Page.authRequired) (snd <$> AS.getPages spec)
validateDbIsPostgresIfPgBossUsed :: AppSpec -> [ValidationError]
validateDbIsPostgresIfPgBossUsed spec =
- if isPgBossJobExecutorUsed spec && not (isPostgresUsed spec)
- then
- [ GenericValidationError
- "Expected app.db.system to be PostgreSQL since there are jobs with executor set to PgBoss."
- ]
- else []
+ [ GenericValidationError
+ "Expected app.db.system to be PostgreSQL since there are jobs with executor set to PgBoss."
+ | isPgBossJobExecutorUsed spec && not (isPostgresUsed spec)
+ ]
validateAuthUserEntityHasCorrectFieldsIfUsernameAndPasswordAuthIsUsed :: AppSpec -> [ValidationError]
validateAuthUserEntityHasCorrectFieldsIfUsernameAndPasswordAuthIsUsed spec = case App.auth (snd $ getApp spec) of
Nothing -> []
Just auth ->
- if Auth.UsernameAndPassword `notElem` Auth.methods auth
+ if not $ Auth.isUsernameAndPasswordAuthEnabled auth
then []
else
let userEntity = snd $ AS.resolveRef spec (Auth.userEntity auth)
userEntityFields = Entity.getFields userEntity
- maybeUsernameField = find ((== "username") . Entity.Field.fieldName) userEntityFields
- maybePasswordField = find ((== "password") . Entity.Field.fieldName) userEntityFields
- in concat
- [ case maybeUsernameField of
- Just usernameField
- | Entity.Field.fieldType usernameField == Entity.Field.FieldTypeScalar Entity.Field.String -> []
- _ ->
- [ GenericValidationError
- "Expected an Entity referenced by app.auth.userEntity to have field 'username' of type 'string'."
- ],
- case maybePasswordField of
- Just passwordField
- | Entity.Field.fieldType passwordField == Entity.Field.FieldTypeScalar Entity.Field.String -> []
- _ ->
- [ GenericValidationError
- "Expected an Entity referenced by app.auth.userEntity to have field 'password' of type 'string'."
- ]
+ in concatMap
+ (validateEntityHasField "app.auth.userEntity" userEntityFields)
+ [ ("username", Entity.Field.FieldTypeScalar Entity.Field.String, "String"),
+ ("password", Entity.Field.FieldTypeScalar Entity.Field.String, "String")
]
+validateExternalAuthEntityHasCorrectFieldsIfExternalAuthIsUsed :: AppSpec -> [ValidationError]
+validateExternalAuthEntityHasCorrectFieldsIfExternalAuthIsUsed spec = case App.auth (snd $ getApp spec) of
+ Nothing -> []
+ Just auth ->
+ if not $ Auth.isExternalAuthEnabled auth
+ then []
+ else case Auth.externalAuthEntity auth of
+ Nothing -> [GenericValidationError "app.auth.externalAuthEntity must be specified when using a social login method."]
+ Just externalAuthEntityRef ->
+ let (userEntityName, userEntity) = AS.resolveRef spec (Auth.userEntity auth)
+ userEntityFields = Entity.getFields userEntity
+ (externalAuthEntityName, externalAuthEntity) = AS.resolveRef spec externalAuthEntityRef
+ externalAuthEntityFields = Entity.getFields externalAuthEntity
+ externalAuthEntityValidationErrors =
+ concatMap
+ (validateEntityHasField "app.auth.externalAuthEntity" externalAuthEntityFields)
+ [ ("provider", Entity.Field.FieldTypeScalar Entity.Field.String, "String"),
+ ("providerId", Entity.Field.FieldTypeScalar Entity.Field.String, "String"),
+ ("user", Entity.Field.FieldTypeScalar (Entity.Field.UserType userEntityName), userEntityName),
+ ("userId", Entity.Field.FieldTypeScalar Entity.Field.Int, "Int")
+ ]
+ userEntityValidationErrors =
+ concatMap
+ (validateEntityHasField "app.auth.userEntity" userEntityFields)
+ [ ("externalAuthAssociations", Entity.Field.FieldTypeComposite $ Entity.Field.List $ Entity.Field.UserType externalAuthEntityName, externalAuthEntityName ++ "[]")
+ ]
+ in externalAuthEntityValidationErrors ++ userEntityValidationErrors
+
+validateEntityHasField :: String -> [Entity.Field.Field] -> (String, Entity.Field.FieldType, String) -> [ValidationError]
+validateEntityHasField entityName entityFields (fieldName, fieldType, fieldTypeName) =
+ let maybeField = find ((== fieldName) . Entity.Field.fieldName) entityFields
+ in case maybeField of
+ Just providerField
+ | Entity.Field.fieldType providerField == fieldType -> []
+ _ ->
+ [ GenericValidationError $
+ "Expected an Entity referenced by " ++ entityName ++ " to have field '" ++ fieldName ++ "' of type '" ++ fieldTypeName ++ "'."
+ ]
+
-- | This function assumes that @AppSpec@ it operates on was validated beforehand (with @validateAppSpec@ function).
-- TODO: It would be great if we could ensure this at type level, but we decided that was too much work for now.
-- Check https://github.com/wasp-lang/wasp/pull/455 for considerations on this and analysis of different approaches.
diff --git a/waspc/src/Wasp/Generator/Common.hs b/waspc/src/Wasp/Generator/Common.hs
index f65c1e0f57..bbe8901660 100644
--- a/waspc/src/Wasp/Generator/Common.hs
+++ b/waspc/src/Wasp/Generator/Common.hs
@@ -1,5 +1,6 @@
module Wasp.Generator.Common
( ProjectRootDir,
+ latestMajorNodeVersion,
nodeVersionRange,
npmVersionRange,
prismaVersion,
@@ -11,11 +12,19 @@ import qualified Wasp.SemanticVersion as SV
-- | Directory where the whole web app project (client, server, ...) is generated.
data ProjectRootDir
--- | Range of node versions that node packages generated by this generator work correctly with.
+-- | Latest concrete major node version supported by the nodeVersionRange, and
+-- therefore by Wasp.
+-- Here we assume that nodeVersionRange is using latestNodeLTSVersion as its basis.
+-- TODO: instead of making assumptions, extract the latest major node version
+-- directly from the nodeVersionRange.
+latestMajorNodeVersion :: SV.Version
+latestMajorNodeVersion = latestNodeLTSVersion
+
nodeVersionRange :: SV.Range
-nodeVersionRange = SV.Range [SV.backwardsCompatibleWith latestLTSVersion]
- where
- latestLTSVersion = SV.Version 16 0 0
+nodeVersionRange = SV.Range [SV.backwardsCompatibleWith latestNodeLTSVersion]
+
+latestNodeLTSVersion :: SV.Version
+latestNodeLTSVersion = SV.Version 16 0 0
-- | Range of npm versions that Wasp and generated projects work correctly with.
npmVersionRange :: SV.Range
diff --git a/waspc/src/Wasp/Generator/DockerGenerator.hs b/waspc/src/Wasp/Generator/DockerGenerator.hs
index 6548bfc932..76f53393c3 100644
--- a/waspc/src/Wasp/Generator/DockerGenerator.hs
+++ b/waspc/src/Wasp/Generator/DockerGenerator.hs
@@ -10,24 +10,29 @@ import StrongPath (File', Path', Rel, relfile)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.Entity as AS.Entity
-import Wasp.Generator.Common (ProjectRootDir)
+import Wasp.Generator.Common (ProjectRootDir, latestMajorNodeVersion)
import Wasp.Generator.FileDraft (FileDraft, createTemplateFileDraft)
import Wasp.Generator.Monad (Generator)
+import Wasp.Generator.ServerGenerator (areServerPatchesUsed)
import Wasp.Generator.Templates (TemplatesDir)
+import qualified Wasp.SemanticVersion as SV
genDockerFiles :: AppSpec -> Generator [FileDraft]
genDockerFiles spec = sequence [genDockerfile spec, genDockerignore spec]
-- TODO: Inject paths to server and db files/dirs, right now they are hardcoded in the templates.
genDockerfile :: AppSpec -> Generator FileDraft
-genDockerfile spec =
+genDockerfile spec = do
+ usingServerPatches <- areServerPatchesUsed spec
return $
createTemplateFileDraft
([relfile|Dockerfile|] :: Path' (Rel ProjectRootDir) File')
([relfile|Dockerfile|] :: Path' (Rel TemplatesDir) File')
( Just $
object
- [ "usingPrisma" .= not (null $ AS.getDecls @AS.Entity.Entity spec)
+ [ "usingPrisma" .= not (null $ AS.getDecls @AS.Entity.Entity spec),
+ "nodeMajorVersion" .= show (SV.major latestMajorNodeVersion),
+ "usingServerPatches" .= usingServerPatches
]
)
diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs
index ca5d476786..5b804b6740 100644
--- a/waspc/src/Wasp/Generator/ServerGenerator.hs
+++ b/waspc/src/Wasp/Generator/ServerGenerator.hs
@@ -4,6 +4,7 @@ module Wasp.Generator.ServerGenerator
( genServer,
operationsRouteInRootRouter,
npmDepsForWasp,
+ areServerPatchesUsed,
)
where
@@ -30,6 +31,7 @@ import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
+import qualified Wasp.AppSpec.App.Dependency as App.Dependency
import qualified Wasp.AppSpec.App.Server as AS.App.Server
import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.AppSpec.Util (isPgBossJobExecutorUsed)
@@ -63,6 +65,7 @@ genServer spec =
<++> genDotEnv spec
<++> genJobs spec
<++> genJobExecutors
+ <++> genPatches spec
genDotEnv :: AppSpec -> Generator [FileDraft]
genDotEnv spec = return $
@@ -115,8 +118,11 @@ npmDepsForWasp spec =
("jsonwebtoken", "^8.5.1"),
("secure-password", "^4.0.0"),
("dotenv", "8.2.0"),
- ("helmet", "^4.6.0")
+ ("helmet", "^4.6.0"),
+ ("patch-package", "^6.4.7"),
+ ("uuid", "^8.3.2")
]
+ ++ depsRequiredByPassport spec
++ depsRequiredByJobs spec,
N.waspDevDependencies =
AS.Dependency.fromList
@@ -220,3 +226,28 @@ genRoutesDir spec =
operationsRouteInRootRouter :: String
operationsRouteInRootRouter = "operations"
+
+depsRequiredByPassport :: AppSpec -> [App.Dependency.Dependency]
+depsRequiredByPassport spec =
+ AS.Dependency.fromList $
+ concat
+ [ [("passport", "0.6.0") | (AS.App.Auth.isExternalAuthEnabled <$> maybeAuth) == Just True],
+ [("passport-google-oauth20", "2.0.0") | (AS.App.Auth.isGoogleAuthEnabled <$> maybeAuth) == Just True]
+ ]
+ where
+ maybeAuth = AS.App.auth $ snd $ getApp spec
+
+areServerPatchesUsed :: AppSpec -> Generator Bool
+areServerPatchesUsed spec = not . null <$> genPatches spec
+
+genPatches :: AppSpec -> Generator [FileDraft]
+genPatches spec = patchesRequiredByPassport spec
+
+patchesRequiredByPassport :: AppSpec -> Generator [FileDraft]
+patchesRequiredByPassport spec =
+ return $
+ [ C.mkTmplFd (C.asTmplFile [relfile|patches/oauth+0.9.15.patch|])
+ | (AS.App.Auth.isExternalAuthEnabled <$> maybeAuth) == Just True
+ ]
+ where
+ maybeAuth = AS.App.auth $ snd $ getApp spec
diff --git a/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs b/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs
index fcf55ab659..c3a10fb783 100644
--- a/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs
+++ b/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs
@@ -4,15 +4,31 @@ module Wasp.Generator.ServerGenerator.AuthG
where
import Data.Aeson (object, (.=))
-import StrongPath (reldir, relfile, (>))
+import Data.Maybe (fromMaybe, isJust)
+import StrongPath
+ ( Dir,
+ File',
+ Path,
+ Path',
+ Posix,
+ Rel,
+ reldir,
+ reldirP,
+ relfile,
+ (>),
+ )
+import qualified StrongPath as SP
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.AppSpec.Valid (getApp)
+import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
import Wasp.Generator.FileDraft (FileDraft)
+import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport)
import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.ServerGenerator.Common as C
+import Wasp.Util ((<++>))
import qualified Wasp.Util as Util
genAuth :: AppSpec -> Generator [FileDraft]
@@ -22,11 +38,13 @@ genAuth spec = case maybeAuth of
[ genCoreAuth auth,
genAuthMiddleware auth,
-- Auth routes
- genAuthRoutesIndex,
+ genAuthRoutesIndex auth,
genLoginRoute auth,
genSignupRoute auth,
- genMeRoute auth
+ genMeRoute auth,
+ genUtilsJs auth
]
+ <++> genPassportAuth auth
Nothing -> return []
where
maybeAuth = AS.App.auth $ snd $ getApp spec
@@ -65,8 +83,18 @@ genAuthMiddleware auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Jus
[ "userEntityUpper" .= (userEntityName :: String)
]
-genAuthRoutesIndex :: Generator FileDraft
-genAuthRoutesIndex = return $ C.mkSrcTmplFd (C.asTmplSrcFile [relfile|routes/auth/index.js|])
+genAuthRoutesIndex :: AS.Auth.Auth -> Generator FileDraft
+genAuthRoutesIndex auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
+ where
+ tmplFile = C.srcDirInServerTemplatesDir > SP.castRel authIndexFileInSrcDir
+ dstFile = C.serverSrcDirInServerRootDir > authIndexFileInSrcDir
+ tmplData =
+ object
+ [ "isExternalAuthEnabled" .= AS.Auth.isExternalAuthEnabled auth
+ ]
+
+ authIndexFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
+ authIndexFileInSrcDir = [relfile|routes/auth/index.js|]
genLoginRoute :: AS.Auth.Auth -> Generator FileDraft
genLoginRoute auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
@@ -105,3 +133,85 @@ genMeRoute auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplD
object
[ "userEntityLower" .= (Util.toLowerFirst (AS.refName $ AS.Auth.userEntity auth) :: String)
]
+
+genPassportAuth :: AS.Auth.Auth -> Generator [FileDraft]
+genPassportAuth auth
+ | AS.Auth.isExternalAuthEnabled auth = (:) <$> genPassportJs auth <*> genGoogleAuth auth
+ | otherwise = return []
+
+genPassportJs :: AS.Auth.Auth -> Generator FileDraft
+genPassportJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
+ where
+ tmplFile = C.srcDirInServerTemplatesDir > SP.castRel passportFileInSrcDir
+ dstFile = C.serverSrcDirInServerRootDir > passportFileInSrcDir
+ tmplData =
+ object
+ [ "isGoogleAuthEnabled" .= AS.Auth.isGoogleAuthEnabled auth
+ ]
+
+ passportFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
+ passportFileInSrcDir = [relfile|routes/auth/passport/passport.js|]
+
+genUtilsJs :: AS.Auth.Auth -> Generator FileDraft
+genUtilsJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
+ where
+ userEntityName = AS.refName $ AS.Auth.userEntity auth
+ externalAuthEntityName = maybe "undefined" AS.refName (AS.Auth.externalAuthEntity auth)
+ tmplFile = C.srcDirInServerTemplatesDir > SP.castRel utilsFileInSrcDir
+ dstFile = C.serverSrcDirInServerRootDir > utilsFileInSrcDir
+ tmplData =
+ object
+ [ "userEntityUpper" .= (userEntityName :: String),
+ "userEntityLower" .= (Util.toLowerFirst userEntityName :: String),
+ "externalAuthEntityLower" .= (Util.toLowerFirst externalAuthEntityName :: String),
+ "failureRedirectPath" .= AS.Auth.onAuthFailedRedirectTo auth,
+ "successRedirectPath" .= getOnAuthSucceededRedirectToOrDefault auth
+ ]
+
+ utilsFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
+ utilsFileInSrcDir = [relfile|routes/auth/utils.js|]
+
+genGoogleAuth :: AS.Auth.Auth -> Generator [FileDraft]
+genGoogleAuth auth
+ | AS.Auth.isGoogleAuthEnabled auth =
+ sequence
+ [ copyTmplFile [relfile|routes/auth/passport/google/google.js|],
+ copyTmplFile [relfile|routes/auth/passport/google/googleDefaults.js|],
+ genGoogleConfigJs auth
+ ]
+ | otherwise = return []
+ where
+ copyTmplFile = return . C.mkSrcTmplFd
+
+genGoogleConfigJs :: AS.Auth.Auth -> Generator FileDraft
+genGoogleConfigJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
+ where
+ tmplFile = C.srcDirInServerTemplatesDir > SP.castRel googleConfigFileInSrcDir
+ dstFile = C.serverSrcDirInServerRootDir > googleConfigFileInSrcDir
+ tmplData =
+ object
+ [ "doesConfigFnExist" .= isJust maybeConfigFn,
+ "configFnImportStatement" .= fromMaybe "" maybeConfigFnImportStmt,
+ "configFnIdentifier" .= fromMaybe "" maybeConfigFnImportIdentifier,
+ "doesOnSignInFnExist" .= isJust maybeGetUserFieldsFn,
+ "getUserFieldsFnImportStatement" .= fromMaybe "" maybeOnSignInFnImportStmt,
+ "getUserFieldsFnIdentifier" .= fromMaybe "" maybeOnSignInFnImportIdentifier
+ ]
+
+ googleConfigFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
+ googleConfigFileInSrcDir = [relfile|routes/auth/passport/google/googleConfig.js|]
+
+ maybeConfigFn = AS.Auth.configFn =<< AS.Auth.google (AS.Auth.methods auth)
+ maybeConfigFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromGoogleAuthDirToExtSrcDir <$> maybeConfigFn
+ (maybeConfigFnImportIdentifier, maybeConfigFnImportStmt) = (fst <$> maybeConfigFnImportDetails, snd <$> maybeConfigFnImportDetails)
+
+ maybeGetUserFieldsFn = AS.Auth.getUserFieldsFn =<< AS.Auth.google (AS.Auth.methods auth)
+ maybeOnSignInFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromGoogleAuthDirToExtSrcDir <$> maybeGetUserFieldsFn
+ (maybeOnSignInFnImportIdentifier, maybeOnSignInFnImportStmt) = (fst <$> maybeOnSignInFnImportDetails, snd <$> maybeOnSignInFnImportDetails)
+
+-- | TODO: Make this not hardcoded!
+relPosixPathFromGoogleAuthDirToExtSrcDir :: Path Posix (Rel (Dir C.ServerSrcDir)) (Dir GeneratedExternalCodeDir)
+relPosixPathFromGoogleAuthDirToExtSrcDir = [reldirP|../../../../ext-src|]
+
+getOnAuthSucceededRedirectToOrDefault :: AS.Auth.Auth -> String
+getOnAuthSucceededRedirectToOrDefault auth = fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)
diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs
index b2c101af73..7e9982c774 100644
--- a/waspc/src/Wasp/Generator/WebAppGenerator.hs
+++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs
@@ -12,14 +12,13 @@ import StrongPath
Path,
Posix,
Rel,
- reldir,
relfile,
- (>),
)
import StrongPath.TH (reldirP)
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App
+import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
import Wasp.AppSpec.App.Client as AS.App.Client
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import Wasp.AppSpec.Valid (getApp)
@@ -90,7 +89,6 @@ npmDepsForWasp _spec =
("react-query", "^3.34.19"),
("react-router-dom", "^5.1.2"),
("react-scripts", "4.0.3"),
- ("uuid", "^3.4.0"),
-- NOTE: We need to specify this exact version of `react-error-overlay` for use with
-- `react-scripts` v4 due to this issue: https://github.com/facebook/create-react-app/issues/11773
("react-error-overlay", "6.0.9")
@@ -113,14 +111,22 @@ genPublicDir :: AppSpec -> Generator [FileDraft]
genPublicDir spec = do
publicIndexHtmlFd <- genPublicIndexHtml spec
return $
- C.mkTmplFd (C.asTmplFile [relfile|public/favicon.ico|]) :
- publicIndexHtmlFd :
- ( let tmplData = object ["appName" .= (fst (getApp spec) :: String)]
- processPublicTmpl path = C.mkTmplFdWithData (C.asTmplFile $ [reldir|public|] > path) tmplData
- in processPublicTmpl
- <$> [ [relfile|manifest.json|]
- ]
- )
+ [ publicIndexHtmlFd,
+ genFaviconFd,
+ genManifestFd
+ ]
+ ++ genGoogleSigninImage
+ where
+ maybeAuth = AS.App.auth $ snd $ getApp spec
+ genFaviconFd = C.mkTmplFd (C.asTmplFile [relfile|public/favicon.ico|])
+ genGoogleSigninImage =
+ [ C.mkTmplFd (C.asTmplFile [relfile|public/images/btn_google_signin_dark_normal_web@2x.png|])
+ | (AS.App.Auth.isGoogleAuthEnabled <$> maybeAuth) == Just True
+ ]
+ genManifestFd =
+ let tmplData = object ["appName" .= (fst (getApp spec) :: String)]
+ tmplFile = C.asTmplFile [relfile|public/manifest.json|]
+ in C.mkTmplFdWithData tmplFile tmplData
genPublicIndexHtml :: AppSpec -> Generator FileDraft
genPublicIndexHtml spec =
diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs b/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs
index c43d503b81..56f4f73d4e 100644
--- a/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs
+++ b/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs
@@ -9,6 +9,7 @@ import Data.Maybe (fromMaybe)
import StrongPath (File', Path', Rel', reldir, relfile, (>))
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec.App as AS.App
+import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.FileDraft (FileDraft)
@@ -63,20 +64,36 @@ genAuthForms auth =
[ genLoginForm auth,
genSignupForm auth
]
+ <++> genExternalAuth auth
genLoginForm :: AS.Auth.Auth -> Generator FileDraft
genLoginForm auth =
- -- TODO: Logic that says "/" is a default redirect on success is duplicated here and in the function below.
- -- We should remove that duplication.
compileTmplToSamePath
[relfile|auth/forms/Login.js|]
- ["onAuthSucceededRedirectTo" .= fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)]
+ ["onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth]
genSignupForm :: AS.Auth.Auth -> Generator FileDraft
genSignupForm auth =
compileTmplToSamePath
[relfile|auth/forms/Signup.js|]
- ["onAuthSucceededRedirectTo" .= fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)]
+ ["onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth]
+
+genExternalAuth :: AS.Auth.Auth -> Generator [FileDraft]
+genExternalAuth auth
+ | AS.App.Auth.isExternalAuthEnabled auth = (:) <$> genOAuthCodeExchange auth <*> genSocialLoginButtons auth
+ | otherwise = return []
+
+genSocialLoginButtons :: AS.Auth.Auth -> Generator [FileDraft]
+genSocialLoginButtons auth =
+ return [C.mkTmplFd (C.asTmplFile [relfile|src/auth/buttons/Google.js|]) | AS.App.Auth.isGoogleAuthEnabled auth]
+
+genOAuthCodeExchange :: AS.Auth.Auth -> Generator FileDraft
+genOAuthCodeExchange auth =
+ compileTmplToSamePath
+ [relfile|auth/pages/OAuthCodeExchange.js|]
+ [ "onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth,
+ "onAuthFailedRedirectTo" .= AS.Auth.onAuthFailedRedirectTo auth
+ ]
compileTmplToSamePath :: Path' Rel' File' -> [Pair] -> Generator FileDraft
compileTmplToSamePath tmplFileInTmplSrcDir keyValuePairs =
@@ -88,3 +105,6 @@ compileTmplToSamePath tmplFileInTmplSrcDir keyValuePairs =
where
targetPath = C.webAppSrcDirInWebAppRootDir > asWebAppSrcFile tmplFileInTmplSrcDir
templateData = object keyValuePairs
+
+getOnAuthSucceededRedirectToOrDefault :: AS.Auth.Auth -> String
+getOnAuthSucceededRedirectToOrDefault auth = fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)
diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs
index 3c65c31a41..013a659686 100644
--- a/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs
+++ b/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs
@@ -13,10 +13,12 @@ import qualified StrongPath as SP
import qualified System.FilePath as FP
import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec as AS
+import qualified Wasp.AppSpec.App as AS.App
+import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
import qualified Wasp.AppSpec.ExtImport as AS.ExtImport
import qualified Wasp.AppSpec.Page as AS.Page
import qualified Wasp.AppSpec.Route as AS.Route
-import Wasp.AppSpec.Valid (isAuthEnabled)
+import Wasp.AppSpec.Valid (getApp, isAuthEnabled)
import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator)
import Wasp.Generator.WebAppGenerator.Common (asTmplFile, asWebAppSrcFile)
@@ -25,7 +27,9 @@ import qualified Wasp.Generator.WebAppGenerator.Common as C
data RouterTemplateData = RouterTemplateData
{ _routes :: ![RouteTemplateData],
_pagesToImport :: ![PageTemplateData],
- _isAuthEnabled :: Bool
+ _isAuthEnabled :: Bool,
+ _isExternalAuthEnabled :: Bool,
+ _isGoogleAuthEnabled :: Bool
}
instance ToJSON RouterTemplateData where
@@ -33,7 +37,9 @@ instance ToJSON RouterTemplateData where
object
[ "routes" .= _routes routerTD,
"pagesToImport" .= _pagesToImport routerTD,
- "isAuthEnabled" .= _isAuthEnabled routerTD
+ "isAuthEnabled" .= _isAuthEnabled routerTD,
+ "isExternalAuthEnabled" .= _isExternalAuthEnabled routerTD,
+ "isGoogleAuthEnabled" .= _isGoogleAuthEnabled routerTD
]
data RouteTemplateData = RouteTemplateData
@@ -78,11 +84,14 @@ createRouterTemplateData spec =
RouterTemplateData
{ _routes = routes,
_pagesToImport = pages,
- _isAuthEnabled = isAuthEnabled spec
+ _isAuthEnabled = isAuthEnabled spec,
+ _isExternalAuthEnabled = (AS.App.Auth.isExternalAuthEnabled <$> maybeAuth) == Just True,
+ _isGoogleAuthEnabled = (AS.App.Auth.isGoogleAuthEnabled <$> maybeAuth) == Just True
}
where
routes = map (createRouteTemplateData spec) $ AS.getRoutes spec
pages = map createPageTemplateData $ AS.getPages spec
+ maybeAuth = AS.App.auth $ snd $ getApp spec
createRouteTemplateData :: AppSpec -> (String, AS.Route.Route) -> RouteTemplateData
createRouteTemplateData spec namedRoute@(_, route) =
diff --git a/waspc/src/Wasp/Util.hs b/waspc/src/Wasp/Util.hs
index 13187e76cf..bccf8bc9c7 100644
--- a/waspc/src/Wasp/Util.hs
+++ b/waspc/src/Wasp/Util.hs
@@ -31,7 +31,7 @@ module Wasp.Util
)
where
-import Control.Monad (liftM2)
+import Control.Applicative (liftA2)
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Data.Aeson as Aeson
import qualified Data.ByteString as B
@@ -155,13 +155,13 @@ insertAt theInsert idx host =
infixr 5 <++>
-(<++>) :: Monad m => m [a] -> m [a] -> m [a]
-(<++>) = liftM2 (++)
+(<++>) :: Applicative f => f [a] -> f [a] -> f [a]
+(<++>) = liftA2 (++)
infixr 5 <:>
-(<:>) :: Monad m => m a -> m [a] -> m [a]
-(<:>) = liftM2 (:)
+(<:>) :: Applicative f => f a -> f [a] -> f [a]
+(<:>) = liftA2 (:)
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM p x y = p >>= \b -> if b then x else y
diff --git a/waspc/test/AnalyzerTest.hs b/waspc/test/AnalyzerTest.hs
index 8ca33c0d0e..d468d5194b 100644
--- a/waspc/test/AnalyzerTest.hs
+++ b/waspc/test/AnalyzerTest.hs
@@ -42,7 +42,7 @@ spec_Analyzer = do
" head: [\"foo\", \"bar\"],",
" auth: {",
" userEntity: User,",
- " methods: [UsernameAndPassword],",
+ " methods: { usernameAndPassword: {} },",
" onAuthFailedRedirectTo: \"/\",",
" },",
" dependencies: [",
@@ -114,7 +114,12 @@ spec_Analyzer = do
Just
Auth.Auth
{ Auth.userEntity = Ref "User" :: Ref Entity,
- Auth.methods = [Auth.UsernameAndPassword],
+ Auth.externalAuthEntity = Nothing,
+ Auth.methods =
+ Auth.AuthMethods
+ { Auth.usernameAndPassword = Just Auth.usernameAndPasswordConfig,
+ Auth.google = Nothing
+ },
Auth.onAuthFailedRedirectTo = "/",
Auth.onAuthSucceededRedirectTo = Nothing
},
diff --git a/waspc/test/AppSpec/ValidTest.hs b/waspc/test/AppSpec/ValidTest.hs
index e2f3e5314f..0eebf68b1e 100644
--- a/waspc/test/AppSpec/ValidTest.hs
+++ b/waspc/test/AppSpec/ValidTest.hs
@@ -53,7 +53,12 @@ spec_AppSpecValid = do
let validAppAuth =
AS.Auth.Auth
{ AS.Auth.userEntity = AS.Core.Ref.Ref userEntityName,
- AS.Auth.methods = [AS.Auth.UsernameAndPassword],
+ AS.Auth.externalAuthEntity = Nothing,
+ AS.Auth.methods =
+ AS.Auth.AuthMethods
+ { AS.Auth.usernameAndPassword = Just AS.Auth.usernameAndPasswordConfig,
+ AS.Auth.google = Nothing
+ },
AS.Auth.onAuthFailedRedirectTo = "/",
AS.Auth.onAuthSucceededRedirectTo = Nothing
}
@@ -112,11 +117,11 @@ spec_AppSpecValid = do
it "returns an error if app.auth is set and user entity is of invalid shape" $ do
ASV.validateAppSpec (makeSpec (Just validAppAuth) invalidUserEntity)
`shouldBe` [ ASV.GenericValidationError
- "Expected an Entity referenced by app.auth.userEntity to have field 'username' of type 'string'."
+ "Expected an Entity referenced by app.auth.userEntity to have field 'username' of type 'String'."
]
ASV.validateAppSpec (makeSpec (Just validAppAuth) invalidUserEntity2)
`shouldBe` [ ASV.GenericValidationError
- "Expected an Entity referenced by app.auth.userEntity to have field 'password' of type 'string'."
+ "Expected an Entity referenced by app.auth.userEntity to have field 'password' of type 'String'."
]
where
makeBasicPslField name typ =
diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal
index 4d2e8b06a8..f06dfb9b65 100644
--- a/waspc/waspc.cabal
+++ b/waspc/waspc.cabal
@@ -28,6 +28,7 @@ data-files:
Generator/templates/dockerignore
Generator/templates/react-app/gitignore
Generator/templates/react-app/npmrc
+ Generator/templates/server/patches/*.patch
Generator/templates/server/gitignore
Generator/templates/server/npmrc
Generator/templates/**/*.prisma
diff --git a/web/blog/2021-03-02-wasp-alpha.md b/web/blog/2021-03-02-wasp-alpha.md
index e040f239e2..389b3a05cd 100644
--- a/web/blog/2021-03-02-wasp-alpha.md
+++ b/web/blog/2021-03-02-wasp-alpha.md
@@ -64,7 +64,9 @@ page Main {
auth { /* full-stack auth out-of-the-box */
userEntity: User,
- methods: [ UsernameAndPassword ],
+ methods: {
+ usernameAndPassword: {}
+ }
}
entity User {=psl
diff --git a/web/blog/2022-06-24-ML-code-gen-vs-coding-by-hand-future.md b/web/blog/2022-06-24-ML-code-gen-vs-coding-by-hand-future.md
index d16d1588ae..e6c4f5efde 100644
--- a/web/blog/2022-06-24-ML-code-gen-vs-coding-by-hand-future.md
+++ b/web/blog/2022-06-24-ML-code-gen-vs-coding-by-hand-future.md
@@ -169,7 +169,11 @@ If we try to apply the principles from above (less code, less detailed instructi
```css
auth: {
userEntity: User,
- methods: [ UsernameAndPassword, LinkedIn, Google ],
+ externalAuthEntity: SocialLogin,
+ methods: {
+ usernameAndPassword: {},
+ google: {}
+ },
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/dashboard"
}
diff --git a/web/docs/deploying.md b/web/docs/deploying.md
index 760c366c9c..229acf00c6 100644
--- a/web/docs/deploying.md
+++ b/web/docs/deploying.md
@@ -30,9 +30,10 @@ Below we will explain the required env vars and also provide detailed instructio
### Env vars
Server uses following environment variables, so you need to ensure they are set on your hosting provider:
-- `PORT` -> number of port at which it will listen for requests (e.g. `3001`).
-- `DATABASE_URL` -> url to the Postgres database that it should use (e.g. `postgresql://mydbuser:mypass@localhost:5432/nameofmydb`)
-- `JWT_SECRET` -> you need this if you are using Wasp's `auth` feature. Set it to a random string (password), at least 32 characters long.
+- `PORT` -> The port number at which it will listen for requests (e.g. `3001`).
+- `DATABASE_URL` -> The URL of the Postgres database it should use (e.g. `postgresql://mydbuser:mypass@localhost:5432/nameofmydb`).
+- `WASP_WEB_CLIENT_URL` -> The URL of where the frontend app is running (e.g. `https://.netlify.app`), which is necessary for CORS.
+- `JWT_SECRET` -> You need this if you are using Wasp's `auth` feature. Set it to a random string (password), at least 32 characters long.
### Deploying to Heroku
@@ -53,11 +54,14 @@ heroku addons:create --app heroku-postgresql:hobby-dev
```
Heroku will also set `DATABASE_URL` env var for us at this point. If you are using external database, you will have to set it yourself.
-`PORT` env var will also be provided by Heroku, so the only thing left is to set `JWT_SECRET` env var:
+The `PORT` env var will also be provided by Heroku, so the only two left to set are the `JWT_SECRET` and `WASP_WEB_CLIENT_URL` env vars:
```
heroku config:set --app JWT_SECRET=
+heroku config:set --app WASP_WEB_CLIENT_URL=
```
+NOTE: If you do not know what your frontend URL is yet, don't worry. You can set WASP_WEB_CLIENT_URL after you deploy your frontend.
+
#### Deploy to a Heroku app
Position yourself in `.wasp/build/` directory (reminder: which you created by running `wasp build` previously):
```
@@ -137,3 +141,5 @@ netlify deploy
and carefully follow their instructions (i.e. do you want to create a new app or use existing one, team under which your app will reside, ..., final step to run `netlify deploy --prod`).
That is it!
+
+NOTE: Make sure you set this URL as the `WASP_WEB_CLIENT_URL` environment variable in Heroku.
diff --git a/web/docs/integrations/google.md b/web/docs/integrations/google.md
new file mode 100644
index 0000000000..d66db7d8e0
--- /dev/null
+++ b/web/docs/integrations/google.md
@@ -0,0 +1,67 @@
+---
+title: Google Integrations
+---
+
+import useBaseUrl from '@docusaurus/useBaseUrl';
+
+# Google Integrations
+
+## Google Auth
+
+To use Google as an authentication method (covered [here](/docs/language/features#google)), you'll first need to create a Google project and provide Wasp with your client key and secret. Here is how to do so:
+
+1. Create a Google Cloud Platform account if you do not already have one: https://cloud.google.com/
+2. Create and configure a new Google project here: https://console.cloud.google.com/home/dashboard
+
+ ![Google Console Screenshot 1](../../static/img/integrations-google-1.jpg)
+
+ ![Google Console Screenshot 2](../../static/img/integrations-google-2.jpg)
+
+3. Search for `OAuth` in the top bar, click on `OAuth consent screen`
+
+ ![Google Console Screenshot 3](../../static/img/integrations-google-3.jpg)
+
+ - Select what type of app you want, we will go External
+
+ ![Google Console Screenshot 4](../../static/img/integrations-google-4.jpg)
+
+ - Fill out applicable information on Page 1
+
+ ![Google Console Screenshot 5](../../static/img/integrations-google-5.jpg)
+
+ - On Page 2, Scopes, you should select `userinfo.profile`. You can optionally search for other things, like `email`.
+
+ ![Google Console Screenshot 6](../../static/img/integrations-google-6.jpg)
+
+ ![Google Console Screenshot 7](../../static/img/integrations-google-7.jpg)
+
+ ![Google Console Screenshot 8](../../static/img/integrations-google-8.jpg)
+
+ - Add any test users you want on Page 3
+
+ ![Google Console Screenshot 9](../../static/img/integrations-google-9.jpg)
+
+4. Next, click `Credentials`
+
+ ![Google Console Screenshot 10](../../static/img/integrations-google-10.jpg)
+
+ - Select `+ Create Credentials`
+ - Select `OAuth client ID`
+
+ ![Google Console Screenshot 11](../../static/img/integrations-google-11.jpg)
+
+ - Complete the form
+
+ ![Google Console Screenshot 12](../../static/img/integrations-google-12.jpg)
+
+ - Under Authorized redirect URIs, put in: `http://localhost:3000/auth/login/google`
+
+ ![Google Console Screenshot 13](../../static/img/integrations-google-13.jpg)
+
+ - Once you know on which URL(s) your API server will be deployed, also add those URL(s)
+ - For example: `https://someotherhost.com/auth/login/google`
+ - When you save, you can click the Edit icon and your credentials will be shown
+
+ ![Google Console Screenshot 14](../../static/img/integrations-google-14.jpg)
+
+5. Copy your Client ID and Client secret, and expose them as environment variables named `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` wherever your app is running
diff --git a/web/docs/language/features.md b/web/docs/language/features.md
index 69e78f99ff..7b7bff6e2c 100644
--- a/web/docs/language/features.md
+++ b/web/docs/language/features.md
@@ -675,7 +675,9 @@ app MyApp {
// ...
auth: {
userEntity: User,
- methods: [ UsernameAndPassword ],
+ methods: {
+ usernameAndPassword: {}
+ },
onAuthFailedRedirectTo: "/someRoute"
}
}
@@ -686,9 +688,13 @@ app MyApp {
#### `userEntity: entity` (required)
Entity which represents the user (sometimes also referred to as *Principal*).
-#### `methods: [AuthMethod]` (required)
+#### `externalAuthEntity: entity` (optional)
+Entity which associates a user with some external authentication provider. We currently offer support for [Google](#google).
+
+#### `methods: dict` (required)
List of authentication methods that Wasp app supports. Currently supported methods are:
-* `UsernameAndPassword`: Provides support for authentication with a username and password.
+* `usernameAndPassword`: Provides support for authentication with a username and password. See [here](#username-and-password) for more.
+* `google`: Provides support for login via Google accounts. See [here](#google) for more.
#### `onAuthFailedRedirectTo: String` (required)
Path where an unauthenticated user will be redirected to if they try to access a private page (which is declared by setting `authRequired: true` for a specific page).
@@ -700,7 +706,7 @@ Default value is "/".
### Username and Password
-`UsernameAndPassword` authentication method makes it possible to signup/login into the app by using a username and password.
+`usernameAndPassword` authentication method makes it possible to signup/login into the app by using a username and password.
This method requires that `userEntity` specified in `auth` contains `username: string` and `password: string` fields.
We provide basic validations out of the box, which you can customize as shown below. Default validations are:
@@ -937,6 +943,115 @@ import AuthError from '@wasp/core/AuthError.js'
}
```
+### Google
+
+`google` authentication makes it possible to use Google's OAuth 2.0 service to sign Google users into your app. To enable it, add `google: {}` to your `auth.methods` dictionary to use it with default settings. If you require custom configuration setup or user entity field assignment, you can override the defaults.
+
+This method requires that `externalAuthEntity` specified in `auth` [described here](features#externalauthentity).
+#### Default settings
+- Configuration:
+ - By default, Wasp expects you to set two environment variables in order to use Google authentication:
+ - `GOOGLE_CLIENT_ID`
+ - `GOOGLE_CLIENT_SECRET`
+ - These can be obtained in your Google Cloud Console project dashboard. See [here](/docs/integrations/google#google-auth) for more.
+- Sign in:
+ - When a user signs in for the first time, Wasp will create a new User account and link it to their Google account for future logins. The `username` will default to a random dictionary phrase that does not exist in the database, like "nice-blue-horse-27160".
+ - Aside: If you would like to allow the user to select their own username, or some other sign up flow, you could add a boolean property to your User entity which indicates if the account setup is complete. You can then redirect them in your `onAuthSucceededRedirectTo` handler.
+- Here is a link to the default implementations: https://github.com/wasp-lang/wasp/blob/main/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleDefaults.js These can be overriden as explained below.
+
+#### Overrides
+If you require modifications to the above, you can add one or more of the following to your `auth.methods.google` dictionary:
+
+```js
+ auth: {
+ userEntity: User,
+ externalAuthEntity: SocialLogin,
+ methods: {
+ google: {
+ configFn: import { config } from "@ext/auth/google.js",
+ getUserFieldsFn: import { getUserFields } from "@ext/auth/google.js"
+ }
+ },
+ ...
+ }
+```
+
+- `configFn`: This function should return an object with the following shape:
+ ```js
+ export function config() {
+ // ...
+ return {
+ clientId, // look up from env or elsewhere,
+ clientSecret, // look up from env or elsewhere,
+ scope: ['profile'] // must include at least 'profile'
+ }
+ }
+ ```
+- `getUserFieldsFn`: This function should return the user fields to use when creating a new user upon their first Google login. The context contains a User entity for DB access, and the args are what the OAuth provider responds with. Here is how you could generate a username based on the Google display name. In your model, you could choose to add more attributes and set additional information.
+ ```js
+ import { generateAvailableUsername } from '@wasp/core/auth.js'
+
+ export async function getUserFields(_context, args) {
+ const username = await generateAvailableUsername(args.profile.displayName.split(' '), { separator: '.' })
+ return { username }
+ }
+ ```
+ - `generateAvailableUsername` takes an array of Strings and an optional separator and generates a string ending with a random number that is not yet in the database. For example, the above could produce something like "Jim.Smith.3984" for a Google user Jim Smith.
+
+#### UI helpers
+
+To use the Google sign-in button or URL on your login page, do either of the following:
+
+```js
+...
+import { GoogleSignInButton, googleSignInUrl } from '@wasp/auth/buttons/Google'
+
+const Login = () => {
+ return (
+ <>
+ ...
+
+
+ {/* or */}
+ Sign in with Google
+ >
+ )
+}
+
+export default Login
+```
+
+You can set the height of the button by setting a prop (e.g., ``), which defaults to 40px.
+
+
+### `externalAuthEntity`
+Anytime an authentication method is used that relies on an external authorization provider, for example, Google, we require an `externalAuthEntity` specified in `auth` that contains at least the following highlighted fields:
+
+```css {4,11,16-19,21}
+...
+ auth: {
+ userEntity: User,
+ externalAuthEntity: SocialLogin,
+...
+
+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=}
+```
+
## Client configuration
You can configure the client using the `client` field inside the `app`
diff --git a/web/docs/language/syntax.md b/web/docs/language/syntax.md
index 768aeaa666..e78fcbe7f8 100644
--- a/web/docs/language/syntax.md
+++ b/web/docs/language/syntax.md
@@ -70,7 +70,6 @@ While fundamental types are here to be basic building blocks of a language, and
- **query**
- **route**
- Enum types
- - **AuthMethod**
- **DbSystem**
- **JobExecutor**
diff --git a/web/docs/tutorials/todo-app/auth.md b/web/docs/tutorials/todo-app/auth.md
index 6fd41f2acd..35498ed855 100644
--- a/web/docs/tutorials/todo-app/auth.md
+++ b/web/docs/tutorials/todo-app/auth.md
@@ -36,14 +36,16 @@ to propagate the schema change (we added User).
## Defining `app.auth`
Next, we want to tell Wasp that we want full-stack [authentication](language/features.md#authentication--authorization) in our app, and that it should use entity `User` for it:
-```c {4-9} title="main.wasp"
+```c {4-11} title="main.wasp"
app TodoApp {
title: "Todo app",
auth: {
// Expects entity User to have (username:String) and (password:String) fields.
userEntity: User,
- methods: [ UsernameAndPassword ], // More methods coming soon!
+ methods: {
+ usernameAndPassword: {} // We also support Google, with more on the way!
+ }
onAuthFailedRedirectTo: "/login" // We'll see how this is used a bit later
}
}
diff --git a/web/sidebars.js b/web/sidebars.js
index a8edf79e03..d61d5afe9d 100644
--- a/web/sidebars.js
+++ b/web/sidebars.js
@@ -53,6 +53,14 @@ module.exports = {
'cli',
'deploying',
'examples',
+ {
+ type: 'category',
+ label: 'Integrations',
+ collapsed: false,
+ items: [
+ 'integrations/google'
+ ]
+ },
{
type: 'category',
label: 'Other',
diff --git a/web/src/pages/index.js b/web/src/pages/index.js
index 3ecd9af033..7d6775cf04 100644
--- a/web/src/pages/index.js
+++ b/web/src/pages/index.js
@@ -125,7 +125,11 @@ function HeroCodeExample() {
auth: { /* full-stack auth out-of-the-box */
userEntity: User,
- methods: [ UsernameAndPassword ],
+ externalAuthEntity: SocialLogin,
+ methods: {
+ usernameAndPassword: {},
+ google: {}
+ }
}
}
@@ -199,7 +203,9 @@ export default () => Hello World!
/* full-stack auth out-of-the-box */
auth: {
userEntity: User,
- methods: [ UsernameAndPassword ], /* more methods coming soon */
+ methods: {
+ usernameAndPassword: {}
+ }
onAuthFailedRedirectTo: "/login"
}
}
@@ -770,13 +776,14 @@ function Home() {
Alpha
-
full-stack auth (username & password)
+
full-stack auth (username & password, Google)
pages & routing
blurs the line between client & server - define your server actions and queries and call them directly in your client code (RPC)!
smart caching of server actions and queries (automatic cache invalidation)
entity (data model) definition with Prisma.io
ACL on frontend
importing NPM dependencies
+
background and scheduled jobs
@@ -784,7 +791,7 @@ function Home() {
ACL on backend
one-click deployment
-
more auth methods (Google, LinkedIn, ...)
+
more auth methods (Facebook, LinkedIn, ...)
tighter integration of entities with other features
themes and layouts
support for explicitly defined server API
diff --git a/web/static/img/integrations-google-1.jpg b/web/static/img/integrations-google-1.jpg
new file mode 100644
index 0000000000..b2ff40efaf
Binary files /dev/null and b/web/static/img/integrations-google-1.jpg differ
diff --git a/web/static/img/integrations-google-10.jpg b/web/static/img/integrations-google-10.jpg
new file mode 100644
index 0000000000..9bcc23771d
Binary files /dev/null and b/web/static/img/integrations-google-10.jpg differ
diff --git a/web/static/img/integrations-google-11.jpg b/web/static/img/integrations-google-11.jpg
new file mode 100644
index 0000000000..0175aa0b59
Binary files /dev/null and b/web/static/img/integrations-google-11.jpg differ
diff --git a/web/static/img/integrations-google-12.jpg b/web/static/img/integrations-google-12.jpg
new file mode 100644
index 0000000000..95b63fd3f0
Binary files /dev/null and b/web/static/img/integrations-google-12.jpg differ
diff --git a/web/static/img/integrations-google-13.jpg b/web/static/img/integrations-google-13.jpg
new file mode 100644
index 0000000000..2509c820f2
Binary files /dev/null and b/web/static/img/integrations-google-13.jpg differ
diff --git a/web/static/img/integrations-google-14.jpg b/web/static/img/integrations-google-14.jpg
new file mode 100644
index 0000000000..286f07b0c0
Binary files /dev/null and b/web/static/img/integrations-google-14.jpg differ
diff --git a/web/static/img/integrations-google-2.jpg b/web/static/img/integrations-google-2.jpg
new file mode 100644
index 0000000000..c14a4d659c
Binary files /dev/null and b/web/static/img/integrations-google-2.jpg differ
diff --git a/web/static/img/integrations-google-3.jpg b/web/static/img/integrations-google-3.jpg
new file mode 100644
index 0000000000..ff70f8503c
Binary files /dev/null and b/web/static/img/integrations-google-3.jpg differ
diff --git a/web/static/img/integrations-google-4.jpg b/web/static/img/integrations-google-4.jpg
new file mode 100644
index 0000000000..cef73331ad
Binary files /dev/null and b/web/static/img/integrations-google-4.jpg differ
diff --git a/web/static/img/integrations-google-5.jpg b/web/static/img/integrations-google-5.jpg
new file mode 100644
index 0000000000..18cc493842
Binary files /dev/null and b/web/static/img/integrations-google-5.jpg differ
diff --git a/web/static/img/integrations-google-6.jpg b/web/static/img/integrations-google-6.jpg
new file mode 100644
index 0000000000..cec27c38ab
Binary files /dev/null and b/web/static/img/integrations-google-6.jpg differ
diff --git a/web/static/img/integrations-google-7.jpg b/web/static/img/integrations-google-7.jpg
new file mode 100644
index 0000000000..1e29c261ac
Binary files /dev/null and b/web/static/img/integrations-google-7.jpg differ
diff --git a/web/static/img/integrations-google-8.jpg b/web/static/img/integrations-google-8.jpg
new file mode 100644
index 0000000000..9b59e7950a
Binary files /dev/null and b/web/static/img/integrations-google-8.jpg differ
diff --git a/web/static/img/integrations-google-9.jpg b/web/static/img/integrations-google-9.jpg
new file mode 100644
index 0000000000..66b8129cb7
Binary files /dev/null and b/web/static/img/integrations-google-9.jpg differ