From 2182b9088ddc45d9c7197f09b8913f8eb9dce54e Mon Sep 17 00:00:00 2001 From: Shayne Czyzewski <523636+shayneczyzewski@users.noreply.github.com> Date: Tue, 20 Dec 2022 09:12:08 -0500 Subject: [PATCH 01/16] Adds GitHub login and refactors Passport.js (#872) --- waspc/ChangeLog.md | 12 +- .../btn_google_signin_dark_normal_web@2x.png | Bin 8001 -> 0 bytes .../public/images/github-logo-icon.png | Bin 0 -> 6393 bytes .../public/images/google-logo-icon.png | Bin 0 -> 16590 bytes .../react-app/src/auth/buttons/Google.js | 15 -- .../react-app/src/auth/helpers/Generic.js | 39 +++++ .../templates/react-app/src/router.js | 12 +- .../configMapping.js} | 12 +- .../routes/auth/passport/generic/provider.js | 62 +++++++ .../src/routes/auth/passport/github/config.js | 19 ++ .../routes/auth/passport/github/defaults.js | 23 +++ .../src/routes/auth/passport/google/config.js | 21 +++ .../google/{googleDefaults.js => defaults.js} | 6 +- .../src/routes/auth/passport/google/google.js | 77 --------- .../src/routes/auth/passport/passport.js | 22 ++- .../todoApp/src/client/pages/auth/Login.js | 4 +- .../todoApp/src/server/auth/github.js | 16 ++ .../todoApp/src/server/auth/google.js | 2 +- waspc/examples/todoApp/todoApp.wasp | 4 + waspc/src/Wasp/AppSpec/App/Auth.hs | 17 +- waspc/src/Wasp/Generator/ServerGenerator.hs | 12 +- .../Wasp/Generator/ServerGenerator/AuthG.hs | 71 +------- .../ServerGenerator/ExternalAuthG.hs | 163 ++++++++++++++++++ waspc/src/Wasp/Generator/WebAppGenerator.hs | 29 +++- .../Wasp/Generator/WebAppGenerator/AuthG.hs | 32 +++- .../WebAppGenerator/ExternalAuthG.hs | 53 ++++++ .../WebAppGenerator/RouterGenerator.hs | 39 ++++- waspc/test/AnalyzerTest.hs | 3 +- waspc/test/AppSpec/ValidTest.hs | 3 +- waspc/waspc.cabal | 2 + .../2022-11-15-auth-feature-announcement.md | 2 +- web/docs/integrations/github.md | 26 +++ web/docs/language/features.md | 58 +++++-- web/docs/tutorials/todo-app/06-auth.md | 2 +- web/static/img/integrations-github-1.png | Bin 0 -> 67440 bytes 35 files changed, 626 insertions(+), 232 deletions(-) delete mode 100644 waspc/data/Generator/templates/react-app/public/images/btn_google_signin_dark_normal_web@2x.png create mode 100644 waspc/data/Generator/templates/react-app/public/images/github-logo-icon.png create mode 100644 waspc/data/Generator/templates/react-app/public/images/google-logo-icon.png delete mode 100644 waspc/data/Generator/templates/react-app/src/auth/buttons/Google.js create mode 100644 waspc/data/Generator/templates/react-app/src/auth/helpers/Generic.js rename waspc/data/Generator/templates/server/src/routes/auth/passport/{google/googleConfig.js => generic/configMapping.js} (57%) create mode 100644 waspc/data/Generator/templates/server/src/routes/auth/passport/generic/provider.js create mode 100644 waspc/data/Generator/templates/server/src/routes/auth/passport/github/config.js create mode 100644 waspc/data/Generator/templates/server/src/routes/auth/passport/github/defaults.js create mode 100644 waspc/data/Generator/templates/server/src/routes/auth/passport/google/config.js rename waspc/data/Generator/templates/server/src/routes/auth/passport/google/{googleDefaults.js => defaults.js} (83%) delete mode 100644 waspc/data/Generator/templates/server/src/routes/auth/passport/google/google.js create mode 100644 waspc/examples/todoApp/src/server/auth/github.js create mode 100644 waspc/src/Wasp/Generator/ServerGenerator/ExternalAuthG.hs create mode 100644 waspc/src/Wasp/Generator/WebAppGenerator/ExternalAuthG.hs create mode 100644 web/docs/integrations/github.md create mode 100644 web/static/img/integrations-github-1.png diff --git a/waspc/ChangeLog.md b/waspc/ChangeLog.md index 03d1b19c5..e87fc6965 100644 --- a/waspc/ChangeLog.md +++ b/waspc/ChangeLog.md @@ -1,9 +1,17 @@ # Changelog -## v0.7.3 +## v0.8.0 -### MINOR CLI BREAKING CHANGE +### BREAKING CHANGES - The CLI command for applying a migration with a name has changed from `wasp db migrate-dev foo` to `wasp db migrate-dev --name foo`. This allowed us to add more flags, like `--create-only`. +- Social auth had several breaking changes as we added a new provider (GitHub). + - Buttons and sign in URLs now have a different, standardized import name for each provider. + - Google exe: `import { SignInButton as GoogleSignInButton, signInUrl, logoUrl } from '@wasp/auth/buttons/Google'` + - Buttons themselves have been restyled to make them more uniform, and no longer take an optional `height` parameter. + - Social config object now use a `clientID` property instead of `clientId`. + +### GitHub added as a social login +We have added GitHub as another social login option. It is as easy to use as Google, and only requires adding `gitHub` to your `app.auth.methods` plus two environment variables (`GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`)! Check out the docs for more. ### Bug fixes - Again fixed Dockerfile generated with `wasp build` (after fixing it only half-way last time :facepalm) -> Prisma would break due to unsupported version of openssl. 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 deleted file mode 100644 index f27bb2433042aea5fc34e19fcf90944430ec331b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8001 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 diff --git a/waspc/data/Generator/templates/react-app/public/images/github-logo-icon.png b/waspc/data/Generator/templates/react-app/public/images/github-logo-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6cb3b705d018006a2bd4200ea94c9d5fb98b6f76 GIT binary patch literal 6393 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 Hu0mjfZ$b4` literal 0 HcmV?d00001 diff --git a/waspc/data/Generator/templates/react-app/public/images/google-logo-icon.png b/waspc/data/Generator/templates/react-app/public/images/google-logo-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a4a9918dad10832bc8aaa5574d71fe15f089c8cb GIT binary patch literal 16590 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 literal 0 HcmV?d00001 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 deleted file mode 100644 index d9c62e28f..000000000 --- a/waspc/data/Generator/templates/react-app/src/auth/buttons/Google.js +++ /dev/null @@ -1,15 +0,0 @@ -import config from '../../config.js' - -export const googleSignInUrl = `${config.apiUrl}/auth/external/google/login` - -// Note: Using `style` instead of `height` to work with Tailwind, -// which sets `height: auto` for `img`. -export function GoogleSignInButton(props) { - return ( - - Sign in with Google - - ) -} diff --git a/waspc/data/Generator/templates/react-app/src/auth/helpers/Generic.js b/waspc/data/Generator/templates/react-app/src/auth/helpers/Generic.js new file mode 100644 index 000000000..536314015 --- /dev/null +++ b/waspc/data/Generator/templates/react-app/src/auth/helpers/Generic.js @@ -0,0 +1,39 @@ +{{={= =}=}} + +import config from '../../config.js' + +export const signInUrl = `${config.apiUrl}{= signInPath =}` +export const logoUrl = '/images/{= iconName =}' + +const containerStyle = { + height: 40, + width: 225, + border: '1px solid darkgray', + borderRadius: 5, + padding: 5, + margin: '5px 0px', + backgroundColor: 'white' +} + +const linkStyle = { + textDecoration: 'none', + color: 'black' +} + +const logoStyle = { + maxWidth: '100%', + maxHeight: '100%', + display: 'inline-block', + marginRight: 10 +} + +export function SignInButton() { + return ( + + ) +} diff --git a/waspc/data/Generator/templates/react-app/src/router.js b/waspc/data/Generator/templates/react-app/src/router.js index 56f9f53d9..53dd29738 100644 --- a/waspc/data/Generator/templates/react-app/src/router.js +++ b/waspc/data/Generator/templates/react-app/src/router.js @@ -22,13 +22,13 @@ const router = ( {=/ routes =} {=# isExternalAuthEnabled =} - - {=# isGoogleAuthEnabled =} - - + {=# externalAuthProviders =} + {=# authProviderEnabled =} + + - {=/ isGoogleAuthEnabled =} - + {=/ authProviderEnabled =} + {=/ externalAuthProviders =} {=/ isExternalAuthEnabled =} 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/generic/configMapping.js similarity index 57% rename from waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleConfig.js rename to waspc/data/Generator/templates/server/src/routes/auth/passport/generic/configMapping.js index 0b2d2c9c7..68d5cb9fe 100644 --- a/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleConfig.js +++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/generic/configMapping.js @@ -5,13 +5,13 @@ export { {= configFnIdentifier =} as configFn } {=/ doesConfigFnExist =} {=^ doesConfigFnExist =} -export { configFn } from './googleDefaults.js' +export { configFn } from './defaults.js' {=/ doesConfigFnExist =} -{=# doesOnSignInFnExist =} +{=# doesGetUserFieldsFnExist =} {=& getUserFieldsFnImportStatement =} export { {= getUserFieldsFnIdentifier =} as getUserFieldsFn } -{=/ doesOnSignInFnExist =} -{=^ doesOnSignInFnExist =} -export { getUserFieldsFn } from './googleDefaults.js' -{=/ doesOnSignInFnExist =} +{=/ doesGetUserFieldsFnExist =} +{=^ doesGetUserFieldsFnExist =} +export { getUserFieldsFn } from './defaults.js' +{=/ doesGetUserFieldsFnExist =} diff --git a/waspc/data/Generator/templates/server/src/routes/auth/passport/generic/provider.js b/waspc/data/Generator/templates/server/src/routes/auth/passport/generic/provider.js new file mode 100644 index 000000000..694a83351 --- /dev/null +++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/generic/provider.js @@ -0,0 +1,62 @@ +import express from 'express' +import passport from 'passport' + +import waspServerConfig from '../../../../config.js' +import { contextWithUserEntity, authConfig, findOrCreateUserByExternalAuthAssociation } from '../../utils.js' +import { sign } from '../../../../core/auth.js' + +// This function is invoked after we successfully exchange the one-time-use OAuth code for a real provider API token. +// This token was used to get the provider profile information supplied as a parameter. +// We add the provider profile to the request for downstream use. +async function addProviderProfileToRequest(req, _accessToken, _refreshToken, providerProfile, done) { + req.wasp = { ...req.wasp, providerProfile } + done(null, {}) +} + +export function initRouter(providerName, ProviderStrategy, config, getUserFieldsFn) { + // Configure and use Passport.js strategy. + const passportStrategyName = `wasp${providerName}LoginStrategy` + const requiredConfig = { + callbackURL: `${waspServerConfig.frontendUrl}/auth/login/${providerName}`, + passReqToCallback: true + } + const passportStrategy = new ProviderStrategy({ ...config, ...requiredConfig }, addProviderProfileToRequest) + + passport.use(passportStrategyName, passportStrategy) + + // Create a new router to use the Passport.js strategy. + const router = express.Router() + + // Constructs a provider OAuth URL and redirects browser to start sign in flow. + router.get('/login', passport.authenticate(passportStrategyName, { session: false })) + + // Validates the OAuth code from the frontend, via server-to-server communication + // with provider. If valid, provides frontend a response containing the JWT. + // NOTE: `addProviderProfileToRequest` is invoked as part of the `passport.authenticate` + // call, before the final route handler callback. This is how we gain access to `req.wasp.providerProfile`. + router.get('/validateCodeForLogin', + passport.authenticate(passportStrategyName, { + session: false, + failureRedirect: waspServerConfig.frontendUrl + authConfig.failureRedirectPath + }), + async function (req, res) { + const providerProfile = req?.wasp?.providerProfile + + if (!providerProfile) { + throw new Error(`Missing ${providerName} provider profile on request. This should not happen! Please contact Wasp.`) + } else if (!providerProfile.id) { + throw new Error(`${providerName} provider 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: providerProfile }) + // TODO: In the future we could make this configurable, possibly associating an external account + // with the currently logged in account, or by some DB lookup. + const user = await findOrCreateUserByExternalAuthAssociation(providerName, providerProfile.id, getUserFields) + + const token = await sign(user.id) + res.json({ token }) + }) + + return router +} diff --git a/waspc/data/Generator/templates/server/src/routes/auth/passport/github/config.js b/waspc/data/Generator/templates/server/src/routes/auth/passport/github/config.js new file mode 100644 index 000000000..3daa220d4 --- /dev/null +++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/github/config.js @@ -0,0 +1,19 @@ +import { configFn } from './configMapping.js' +export { getUserFieldsFn } from './configMapping.js' + +// Validates the provided config function returns all required data. +export const config = ((config) => { + if (!config?.clientID) { + throw new Error("The GitHub configFn must return an object with a clientID property.") + } + + if (!config?.clientSecret) { + throw new Error("The GitHub configFn must return an object with a clientSecret property.") + } + + if (!config?.scope || !Array.isArray(config.scope)) { + throw new Error("The GitHub configFn must return an object with a scope property.") + } + + return config +})(await configFn()) diff --git a/waspc/data/Generator/templates/server/src/routes/auth/passport/github/defaults.js b/waspc/data/Generator/templates/server/src/routes/auth/passport/github/defaults.js new file mode 100644 index 000000000..23bea83e7 --- /dev/null +++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/github/defaults.js @@ -0,0 +1,23 @@ +import { generateAvailableDictionaryUsername } from '../../../../core/auth.js' + +// Default implementation if there is no `auth.methods.gitHub.configFn`. +export function configFn() { + const clientID = process.env['GITHUB_CLIENT_ID'] + const clientSecret = process.env['GITHUB_CLIENT_SECRET'] + + if (!clientID) { + throw new Error("Missing GITHUB_CLIENT_ID environment variable.") + } + + if (!clientSecret) { + throw new Error("Missing GITHUB_CLIENT_SECRET environment variable.") + } + + return { clientID, clientSecret, scope: [] } +} + +// Default implementation if there is no `auth.methods.gitHub.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/google/config.js b/waspc/data/Generator/templates/server/src/routes/auth/passport/google/config.js new file mode 100644 index 000000000..c03b2488d --- /dev/null +++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/google/config.js @@ -0,0 +1,21 @@ +import { configFn } from './configMapping.js' +export { getUserFieldsFn } from './configMapping.js' + +// Validates the provided config function returns all required data. +export const config = ((config) => { + if (!config?.clientID) { + throw new Error("The Google configFn must return an object with a clientID property.") + } + + if (!config?.clientSecret) { + throw new Error("The Google configFn must return an object with a clientSecret property.") + } + + if (!config?.scope) { + throw new Error("The Google configFn must return an object with a scope property.") + } else if (!Array.isArray(config.scope) || !config.scope.includes('profile')) { + throw new Error("The Google configFn returned an object with an invalid scope property. It must be an array including 'profile'.") + } + + return config +})(await configFn()) 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/defaults.js similarity index 83% rename from waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleDefaults.js rename to waspc/data/Generator/templates/server/src/routes/auth/passport/google/defaults.js index 09db94b87..46706a509 100644 --- a/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleDefaults.js +++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/google/defaults.js @@ -2,10 +2,10 @@ 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 clientID = process.env['GOOGLE_CLIENT_ID'] const clientSecret = process.env['GOOGLE_CLIENT_SECRET'] - if (!clientId) { + if (!clientID) { throw new Error("Missing GOOGLE_CLIENT_ID environment variable.") } @@ -13,7 +13,7 @@ export function configFn() { throw new Error("Missing GOOGLE_CLIENT_SECRET environment variable.") } - return { clientId, clientSecret, scope: ['profile'] } + return { clientID, clientSecret, scope: ['profile'] } } // Default implementation if there is no `auth.methods.google.getUserFieldsFn`. 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 deleted file mode 100644 index 726fc013b..000000000 --- a/waspc/data/Generator/templates/server/src/routes/auth/passport/google/google.js +++ /dev/null @@ -1,77 +0,0 @@ -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/passport.js b/waspc/data/Generator/templates/server/src/routes/auth/passport/passport.js index fe8f820ee..09c9965ab 100644 --- a/waspc/data/Generator/templates/server/src/routes/auth/passport/passport.js +++ b/waspc/data/Generator/templates/server/src/routes/auth/passport/passport.js @@ -1,14 +1,24 @@ {{={= =}=}} import express from 'express' +import { initRouter } from './generic/provider.js' -{=# isGoogleAuthEnabled =} -import googleAuth from './google/google.js' -{=/ isGoogleAuthEnabled =} +const providerMap = new Map(); +{=# providers =} +{=# isEnabled =} +providerMap.set('{= slug =}', { npmPackage: '{= npmPackage =}', passportImportPath: '{= passportImportPath =}' }) +{=/ isEnabled =} +{=/ providers =} const router = express.Router() -{=# isGoogleAuthEnabled =} -router.use('/google', googleAuth) -{=/ isGoogleAuthEnabled =} +async function initProviders(providers) { + for (let [providerSlug, { npmPackage, passportImportPath }] of providers) { + const { config, getUserFieldsFn } = await import(passportImportPath) + const ProviderStrategy = await import(npmPackage) + router.use(`/${providerSlug}`, initRouter(providerSlug, ProviderStrategy.default, config, getUserFieldsFn)) + } +} + +await initProviders(providerMap) export default router diff --git a/waspc/examples/todoApp/src/client/pages/auth/Login.js b/waspc/examples/todoApp/src/client/pages/auth/Login.js index 9945e8c77..9e3e2369d 100644 --- a/waspc/examples/todoApp/src/client/pages/auth/Login.js +++ b/waspc/examples/todoApp/src/client/pages/auth/Login.js @@ -2,7 +2,8 @@ import React from 'react' import { Link } from 'react-router-dom' import LoginForm from '@wasp/auth/forms/Login' -// import { GoogleSignInButton } from '@wasp/auth/buttons/Google' +// import { SignInButton as GoogleSignInButton } from '@wasp/auth/helpers/Google' +// import { SignInButton as GitHubSignInButton } from '@wasp/auth/helpers/GitHub' const Login = () => { return ( @@ -15,6 +16,7 @@ const Login = () => { {/*
+
*/} ) diff --git a/waspc/examples/todoApp/src/server/auth/github.js b/waspc/examples/todoApp/src/server/auth/github.js new file mode 100644 index 000000000..930bbc569 --- /dev/null +++ b/waspc/examples/todoApp/src/server/auth/github.js @@ -0,0 +1,16 @@ +import { generateAvailableUsername } from '@wasp/core/auth.js' + +export function config() { + console.log("Inside user-supplied GitHub config") + return { + clientID: process.env['GITHUB_CLIENT_ID'], + clientSecret: process.env['GITHUB_CLIENT_SECRET'], + scope: [] + } +} + +export async function getUserFields(_context, args) { + console.log("Inside user-supplied GitHub getUserFields") + const username = await generateAvailableUsername([args.profile.username], { separator: '-' }) + return { username } +} diff --git a/waspc/examples/todoApp/src/server/auth/google.js b/waspc/examples/todoApp/src/server/auth/google.js index 283153617..5af11e548 100644 --- a/waspc/examples/todoApp/src/server/auth/google.js +++ b/waspc/examples/todoApp/src/server/auth/google.js @@ -3,7 +3,7 @@ import { generateAvailableUsername } from '@wasp/core/auth.js' export function config() { console.log("Inside user-supplied Google config") return { - clientId: process.env['GOOGLE_CLIENT_ID'], + clientID: process.env['GOOGLE_CLIENT_ID'], clientSecret: process.env['GOOGLE_CLIENT_SECRET'], scope: ['profile'] } diff --git a/waspc/examples/todoApp/todoApp.wasp b/waspc/examples/todoApp/todoApp.wasp index e64575a9a..9a378e383 100644 --- a/waspc/examples/todoApp/todoApp.wasp +++ b/waspc/examples/todoApp/todoApp.wasp @@ -16,6 +16,10 @@ app todoApp { // google: { // configFn: import { config } from "@server/auth/google.js", // getUserFieldsFn: import { getUserFields } from "@server/auth/google.js" + // }, + // gitHub: { + // configFn: import { config } from "@server/auth/github.js", + // getUserFieldsFn: import { getUserFields } from "@server/auth/github.js" // } }, onAuthFailedRedirectTo: "/login", diff --git a/waspc/src/Wasp/AppSpec/App/Auth.hs b/waspc/src/Wasp/AppSpec/App/Auth.hs index 648c959a8..f17e3fc26 100644 --- a/waspc/src/Wasp/AppSpec/App/Auth.hs +++ b/waspc/src/Wasp/AppSpec/App/Auth.hs @@ -4,11 +4,12 @@ module Wasp.AppSpec.App.Auth ( Auth (..), AuthMethods (..), - GoogleConfig (..), + ExternalAuthConfig (..), usernameAndPasswordConfig, isUsernameAndPasswordAuthEnabled, - isGoogleAuthEnabled, isExternalAuthEnabled, + isGoogleAuthEnabled, + isGitHubAuthEnabled, ) where @@ -29,7 +30,8 @@ data Auth = Auth data AuthMethods = AuthMethods { usernameAndPassword :: Maybe UsernameAndPasswordConfig, - google :: Maybe GoogleConfig + google :: Maybe ExternalAuthConfig, + gitHub :: Maybe ExternalAuthConfig } deriving (Show, Eq, Data) @@ -39,7 +41,7 @@ data UsernameAndPasswordConfig = UsernameAndPasswordConfig } deriving (Show, Eq, Data) -data GoogleConfig = GoogleConfig +data ExternalAuthConfig = ExternalAuthConfig { configFn :: Maybe ExtImport, getUserFieldsFn :: Maybe ExtImport } @@ -51,8 +53,11 @@ usernameAndPasswordConfig = UsernameAndPasswordConfig Nothing isUsernameAndPasswordAuthEnabled :: Auth -> Bool isUsernameAndPasswordAuthEnabled = isJust . usernameAndPassword . methods +isExternalAuthEnabled :: Auth -> Bool +isExternalAuthEnabled auth = any ($ auth) [isGoogleAuthEnabled, isGitHubAuthEnabled] + isGoogleAuthEnabled :: Auth -> Bool isGoogleAuthEnabled = isJust . google . methods -isExternalAuthEnabled :: Auth -> Bool -isExternalAuthEnabled auth = any ($ auth) [isGoogleAuthEnabled] +isGitHubAuthEnabled :: Auth -> Bool +isGitHubAuthEnabled = isJust . gitHub . methods diff --git a/waspc/src/Wasp/Generator/ServerGenerator.hs b/waspc/src/Wasp/Generator/ServerGenerator.hs index 848b1b1ba..d8b6c177c 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator.hs @@ -32,7 +32,6 @@ 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) @@ -47,6 +46,7 @@ import qualified Wasp.Generator.NpmDependencies as N import Wasp.Generator.ServerGenerator.AuthG (genAuth) import qualified Wasp.Generator.ServerGenerator.Common as C import Wasp.Generator.ServerGenerator.ConfigG (genConfigFile) +import Wasp.Generator.ServerGenerator.ExternalAuthG (depsRequiredByPassport) import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir, extServerCodeGeneratorStrategy, extSharedCodeGeneratorStrategy) import Wasp.Generator.ServerGenerator.JobGenerator (depsRequiredByJobs, genJobExecutors, genJobs) import Wasp.Generator.ServerGenerator.OperationsG (genOperations) @@ -239,16 +239,6 @@ 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 diff --git a/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs b/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs index e83040820..b08a18d8a 100644 --- a/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs +++ b/waspc/src/Wasp/Generator/ServerGenerator/AuthG.hs @@ -4,16 +4,12 @@ module Wasp.Generator.ServerGenerator.AuthG where import Data.Aeson (object, (.=)) -import Data.Maybe (fromJust, fromMaybe, isJust) +import Data.Maybe (fromMaybe) import StrongPath - ( Dir, - File', - Path, + ( File', Path', - Posix, Rel, reldir, - reldirP, relfile, (), ) @@ -23,12 +19,10 @@ 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.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir) +import Wasp.Generator.ServerGenerator.ExternalAuthG (genPassportAuth) import Wasp.Util ((<++>)) import qualified Wasp.Util as Util @@ -135,24 +129,6 @@ genMeRoute auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplD [ "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 @@ -172,46 +148,5 @@ genUtilsJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplD 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) - -relPosixPathFromGoogleAuthDirToExtSrcDir :: Path Posix (Rel (Dir C.ServerSrcDir)) (Dir GeneratedExternalCodeDir) -relPosixPathFromGoogleAuthDirToExtSrcDir = [reldirP|../../../../|] fromJust (SP.relDirToPosix extServerCodeDirInServerSrcDir) - getOnAuthSucceededRedirectToOrDefault :: AS.Auth.Auth -> String getOnAuthSucceededRedirectToOrDefault auth = fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth) diff --git a/waspc/src/Wasp/Generator/ServerGenerator/ExternalAuthG.hs b/waspc/src/Wasp/Generator/ServerGenerator/ExternalAuthG.hs new file mode 100644 index 000000000..a07b0ae78 --- /dev/null +++ b/waspc/src/Wasp/Generator/ServerGenerator/ExternalAuthG.hs @@ -0,0 +1,163 @@ +module Wasp.Generator.ServerGenerator.ExternalAuthG + ( genPassportAuth, + depsRequiredByPassport, + ) +where + +import Data.Aeson (object, (.=)) +import qualified Data.Aeson as Aeson +import Data.Maybe (fromJust, fromMaybe, isJust) +import StrongPath + ( Dir, + File', + Path, + Path', + Posix, + Rel, + Rel', + reldirP, + relfile, + (), + ) +import qualified StrongPath as SP +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 qualified Wasp.AppSpec.App.Dependency as App.Dependency +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.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir) +import Wasp.Generator.WebAppGenerator.ExternalAuthG (ExternalAuthInfo (..), gitHubAuthInfo, googleAuthInfo, templateFilePathInPassportDir) +import Wasp.Util ((<++>)) + +genPassportAuth :: AS.Auth.Auth -> Generator [FileDraft] +genPassportAuth auth + | AS.Auth.isExternalAuthEnabled auth = + sequence + [ genPassportJs auth, + copyTmplFile [relfile|routes/auth/passport/generic/provider.js|] + ] + <++> genGoogleAuth auth + <++> genGitHubAuth auth + | otherwise = return [] + where + copyTmplFile = return . C.mkSrcTmplFd + +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 + [ "providers" + .= [ buildProviderData + (_slug googleAuthInfo) + (App.Dependency.name googlePassportDependency) + (AS.Auth.isGoogleAuthEnabled auth) + (templateFilePathInPassportDir googleAuthInfo), + buildProviderData + (_slug gitHubAuthInfo) + (App.Dependency.name gitHubPassportDependency) + (AS.Auth.isGitHubAuthEnabled auth) + (templateFilePathInPassportDir gitHubAuthInfo) + ] + ] + + buildProviderData :: String -> String -> Bool -> Path' Rel' File' -> Aeson.Value + buildProviderData slug npmPackage isEnabled passportTemplateFP = + object + [ "slug" .= slug, + "npmPackage" .= npmPackage, + "isEnabled" .= isEnabled, + "passportImportPath" .= ("./" ++ SP.toFilePath passportTemplateFP) + ] + + passportFileInSrcDir :: Path' (Rel C.ServerSrcDir) File' + passportFileInSrcDir = [relfile|routes/auth/passport/passport.js|] + +genGoogleAuth :: AS.Auth.Auth -> Generator [FileDraft] +genGoogleAuth auth + | AS.Auth.isGoogleAuthEnabled auth = + sequence + [ return $ C.mkSrcTmplFd $ _passportTemplateFilePath googleAuthInfo, + return $ C.mkSrcTmplFd [relfile|routes/auth/passport/google/defaults.js|], + return $ + mkAuthConfigFd + [relfile|routes/auth/passport/generic/configMapping.js|] + [relfile|routes/auth/passport/google/configMapping.js|] + (Just configTmplData) + ] + | otherwise = return [] + where + configTmplData = getTmplDataForAuthMethodConfig auth AS.Auth.google + +genGitHubAuth :: AS.Auth.Auth -> Generator [FileDraft] +genGitHubAuth auth + | AS.Auth.isGitHubAuthEnabled auth = + sequence + [ return $ C.mkSrcTmplFd $ _passportTemplateFilePath gitHubAuthInfo, + return $ C.mkSrcTmplFd [relfile|routes/auth/passport/github/defaults.js|], + return $ + mkAuthConfigFd + [relfile|routes/auth/passport/generic/configMapping.js|] + [relfile|routes/auth/passport/github/configMapping.js|] + (Just configTmplData) + ] + | otherwise = return [] + where + configTmplData = getTmplDataForAuthMethodConfig auth AS.Auth.gitHub + +mkAuthConfigFd :: + Path' (Rel C.ServerTemplatesSrcDir) File' -> + Path' (Rel C.ServerSrcDir) File' -> + Maybe Aeson.Value -> + FileDraft +mkAuthConfigFd pathInTemplatesSrcDir pathInGenProjectSrcDir tmplData = + C.mkTmplFdWithDstAndData srcPath dstPath tmplData + where + srcPath = C.srcDirInServerTemplatesDir pathInTemplatesSrcDir + dstPath = C.serverSrcDirInServerRootDir pathInGenProjectSrcDir + +getTmplDataForAuthMethodConfig :: AS.Auth.Auth -> (AS.Auth.AuthMethods -> Maybe AS.Auth.ExternalAuthConfig) -> Aeson.Value +getTmplDataForAuthMethodConfig auth authMethod = + object + [ "doesConfigFnExist" .= isJust maybeConfigFn, + "configFnImportStatement" .= fromMaybe "" maybeConfigFnImportStmt, + "configFnIdentifier" .= fromMaybe "" maybeConfigFnImportIdentifier, + "doesGetUserFieldsFnExist" .= isJust maybeGetUserFieldsFn, + "getUserFieldsFnImportStatement" .= fromMaybe "" maybeOnSignInFnImportStmt, + "getUserFieldsFnIdentifier" .= fromMaybe "" maybeOnSignInFnImportIdentifier + ] + where + maybeConfigFn = AS.Auth.configFn =<< authMethod (AS.Auth.methods auth) + maybeConfigFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromAuthMethodDirToExtSrcDir <$> maybeConfigFn + (maybeConfigFnImportIdentifier, maybeConfigFnImportStmt) = (fst <$> maybeConfigFnImportDetails, snd <$> maybeConfigFnImportDetails) + + maybeGetUserFieldsFn = AS.Auth.getUserFieldsFn =<< authMethod (AS.Auth.methods auth) + maybeOnSignInFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromAuthMethodDirToExtSrcDir <$> maybeGetUserFieldsFn + (maybeOnSignInFnImportIdentifier, maybeOnSignInFnImportStmt) = (fst <$> maybeOnSignInFnImportDetails, snd <$> maybeOnSignInFnImportDetails) + + relPosixPathFromAuthMethodDirToExtSrcDir :: Path Posix (Rel (Dir C.ServerSrcDir)) (Dir GeneratedExternalCodeDir) + relPosixPathFromAuthMethodDirToExtSrcDir = [reldirP|../../../../|] fromJust (SP.relDirToPosix extServerCodeDirInServerSrcDir) + +depsRequiredByPassport :: AppSpec -> [App.Dependency.Dependency] +depsRequiredByPassport spec = + concat + [ [App.Dependency.make ("passport", "0.6.0") | (AS.App.Auth.isExternalAuthEnabled <$> maybeAuth) == Just True], + [googlePassportDependency | (AS.App.Auth.isGoogleAuthEnabled <$> maybeAuth) == Just True], + [gitHubPassportDependency | (AS.App.Auth.isGitHubAuthEnabled <$> maybeAuth) == Just True] + ] + where + maybeAuth = AS.App.auth $ snd $ getApp spec + +googlePassportDependency :: App.Dependency.Dependency +googlePassportDependency = App.Dependency.make ("passport-google-oauth20", "2.0.0") + +gitHubPassportDependency :: App.Dependency.Dependency +gitHubPassportDependency = App.Dependency.make ("passport-github2", "0.1.12") diff --git a/waspc/src/Wasp/Generator/WebAppGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator.hs index 6cc875a0c..6dfecea82 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator.hs @@ -15,6 +15,7 @@ import StrongPath Posix, Rel, relDirToPosix, + reldir, relfile, (), ) @@ -35,7 +36,12 @@ import Wasp.Generator.Monad (Generator) import qualified Wasp.Generator.NpmDependencies as N import Wasp.Generator.WebAppGenerator.AuthG (genAuth) import qualified Wasp.Generator.WebAppGenerator.Common as C -import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator (extClientCodeDirInWebAppSrcDir, extClientCodeGeneratorStrategy, extSharedCodeGeneratorStrategy) +import Wasp.Generator.WebAppGenerator.ExternalAuthG (ExternalAuthInfo (..), gitHubAuthInfo, googleAuthInfo) +import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator + ( extClientCodeDirInWebAppSrcDir, + extClientCodeGeneratorStrategy, + extSharedCodeGeneratorStrategy, + ) import Wasp.Generator.WebAppGenerator.OperationsGenerator (genOperations) import Wasp.Generator.WebAppGenerator.RouterGenerator (genRouter) import Wasp.Util ((<++>)) @@ -147,24 +153,33 @@ genGitignore = genPublicDir :: AppSpec -> Generator [FileDraft] genPublicDir spec = do publicIndexHtmlFd <- genPublicIndexHtml spec - return $ + return [ publicIndexHtmlFd, genFaviconFd, genManifestFd ] - ++ genGoogleSigninImage + <++> genSocialLoginIcons maybeAuth 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 +genSocialLoginIcons :: Maybe AS.App.Auth.Auth -> Generator [FileDraft] +genSocialLoginIcons maybeAuth = + return $ + [ C.mkTmplFd (C.asTmplFile fp) + | (isEnabled, fp) <- socialIcons, + (isEnabled <$> maybeAuth) == Just True + ] + where + socialIcons = + [ (AS.App.Auth.isGoogleAuthEnabled, [reldir|public/images|] _logoFileName googleAuthInfo), + (AS.App.Auth.isGitHubAuthEnabled, [reldir|public/images|] _logoFileName gitHubAuthInfo) + ] + genPublicIndexHtml :: AppSpec -> Generator FileDraft genPublicIndexHtml spec = return $ diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs b/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs index 56f4f73d4..f38959105 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/AuthG.hs @@ -7,6 +7,7 @@ import Data.Aeson (object, (.=)) import Data.Aeson.Types (Pair) import Data.Maybe (fromMaybe) import StrongPath (File', Path', Rel', reldir, relfile, ()) +import qualified StrongPath as SP import Wasp.AppSpec (AppSpec) import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App.Auth as AS.App.Auth @@ -15,6 +16,8 @@ import Wasp.AppSpec.Valid (getApp) import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.Monad (Generator) import Wasp.Generator.WebAppGenerator.Common as C +import Wasp.Generator.WebAppGenerator.ExternalAuthG (ExternalAuthInfo, gitHubAuthInfo, googleAuthInfo) +import qualified Wasp.Generator.WebAppGenerator.ExternalAuthG as ExternalAuthG import Wasp.Util ((<++>)) genAuth :: AppSpec -> Generator [FileDraft] @@ -80,12 +83,33 @@ genSignupForm auth = genExternalAuth :: AS.Auth.Auth -> Generator [FileDraft] genExternalAuth auth - | AS.App.Auth.isExternalAuthEnabled auth = (:) <$> genOAuthCodeExchange auth <*> genSocialLoginButtons auth + | AS.App.Auth.isExternalAuthEnabled auth = (:) <$> genOAuthCodeExchange auth <*> genSocialLoginHelpers 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] +genSocialLoginHelpers :: AS.Auth.Auth -> Generator [FileDraft] +genSocialLoginHelpers auth = + return $ + concat + [ [gitHubHelpers | AS.App.Auth.isGitHubAuthEnabled auth], + [googleHelpers | AS.App.Auth.isGoogleAuthEnabled auth] + ] + where + gitHubHelpers = mkHelpersFd gitHubAuthInfo [relfile|GitHub.js|] + googleHelpers = mkHelpersFd googleAuthInfo [relfile|Google.js|] + + mkHelpersFd :: ExternalAuthInfo -> Path' Rel' File' -> FileDraft + mkHelpersFd externalAuthInfo helpersFp = + mkTmplFdWithDstAndData + [relfile|src/auth/helpers/Generic.js|] + (SP.castRel $ [reldir|src/auth/helpers|] SP. helpersFp) + (Just tmplData) + where + tmplData = + object + [ "signInPath" .= ExternalAuthG.serverLoginUrl externalAuthInfo, + "iconName" .= SP.toFilePath (ExternalAuthG._logoFileName externalAuthInfo), + "displayName" .= ExternalAuthG._displayName externalAuthInfo + ] genOAuthCodeExchange :: AS.Auth.Auth -> Generator FileDraft genOAuthCodeExchange auth = diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/ExternalAuthG.hs b/waspc/src/Wasp/Generator/WebAppGenerator/ExternalAuthG.hs new file mode 100644 index 000000000..e6ad8a5fe --- /dev/null +++ b/waspc/src/Wasp/Generator/WebAppGenerator/ExternalAuthG.hs @@ -0,0 +1,53 @@ +module Wasp.Generator.WebAppGenerator.ExternalAuthG + ( googleAuthInfo, + gitHubAuthInfo, + frontendLoginUrl, + serverLoginUrl, + serverOauthRedirectHandlerUrl, + templateFilePathInPassportDir, + ExternalAuthInfo (..), + ) +where + +import StrongPath (File', Path', Rel, Rel', relfile, ()) +import qualified StrongPath as SP +import Wasp.Generator.ServerGenerator.Common (ServerTemplatesSrcDir) + +data ExternalAuthInfo = ExternalAuthInfo + { _passportTemplateFilePath :: Path' (Rel ServerTemplatesSrcDir) File', + _logoFileName :: Path' Rel' File', + _displayName :: String, + _slug :: String + } + +googleAuthInfo :: ExternalAuthInfo +googleAuthInfo = + ExternalAuthInfo + { _passportTemplateFilePath = [relfile|routes/auth/passport/google/config.js|], + _logoFileName = [relfile|google-logo-icon.png|], + _displayName = "Google", + _slug = "google" + } + +gitHubAuthInfo :: ExternalAuthInfo +gitHubAuthInfo = + ExternalAuthInfo + { _passportTemplateFilePath = [relfile|routes/auth/passport/github/config.js|], + _logoFileName = [relfile|github-logo-icon.png|], + _displayName = "GitHub", + _slug = "github" + } + +frontendLoginUrl :: ExternalAuthInfo -> String +frontendLoginUrl eai = "/auth/login/" ++ _slug eai + +serverLoginUrl :: ExternalAuthInfo -> String +serverLoginUrl eai = "/auth/external/" ++ _slug eai ++ "/login" + +serverOauthRedirectHandlerUrl :: ExternalAuthInfo -> String +serverOauthRedirectHandlerUrl eai = "/auth/external/" ++ _slug eai ++ "/validateCodeForLogin" + +templateFilePathInPassportDir :: ExternalAuthInfo -> Path' Rel' File' +templateFilePathInPassportDir eai = + (SP.basename . SP.parent $ _passportTemplateFilePath eai) + SP.basename (_passportTemplateFilePath eai) diff --git a/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs b/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs index f70a4185c..2eb776d5b 100644 --- a/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs +++ b/waspc/src/Wasp/Generator/WebAppGenerator/RouterGenerator.hs @@ -23,6 +23,7 @@ import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.Monad (Generator) import Wasp.Generator.WebAppGenerator.Common (asTmplFile, asWebAppSrcFile) import qualified Wasp.Generator.WebAppGenerator.Common as C +import Wasp.Generator.WebAppGenerator.ExternalAuthG (ExternalAuthInfo (..), frontendLoginUrl, gitHubAuthInfo, googleAuthInfo, serverOauthRedirectHandlerUrl) import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator (extClientCodeDirInWebAppSrcDir) data RouterTemplateData = RouterTemplateData @@ -30,7 +31,7 @@ data RouterTemplateData = RouterTemplateData _pagesToImport :: ![PageTemplateData], _isAuthEnabled :: Bool, _isExternalAuthEnabled :: Bool, - _isGoogleAuthEnabled :: Bool + _externalAuthProviders :: ![ExternalAuthProviderTemplateData] } instance ToJSON RouterTemplateData where @@ -40,7 +41,7 @@ instance ToJSON RouterTemplateData where "pagesToImport" .= _pagesToImport routerTD, "isAuthEnabled" .= _isAuthEnabled routerTD, "isExternalAuthEnabled" .= _isExternalAuthEnabled routerTD, - "isGoogleAuthEnabled" .= _isGoogleAuthEnabled routerTD + "externalAuthProviders" .= _externalAuthProviders routerTD ] data RouteTemplateData = RouteTemplateData @@ -68,6 +69,21 @@ instance ToJSON PageTemplateData where "importFrom" .= _importFrom pageTD ] +data ExternalAuthProviderTemplateData = ExternalAuthProviderTemplateData + { _authFrontendUrl :: !String, + _authServerOauthRedirectUrl :: !String, + _authProviderEnabled :: Bool + } + deriving (Show, Eq) + +instance ToJSON ExternalAuthProviderTemplateData where + toJSON externalProviderTD = + object + [ "authFrontendUrl" .= _authFrontendUrl externalProviderTD, + "authServerOauthRedirectUrl" .= _authServerOauthRedirectUrl externalProviderTD, + "authProviderEnabled" .= _authProviderEnabled externalProviderTD + ] + genRouter :: AppSpec -> Generator FileDraft genRouter spec = do return $ @@ -87,13 +103,30 @@ createRouterTemplateData spec = _pagesToImport = pages, _isAuthEnabled = isAuthEnabled spec, _isExternalAuthEnabled = (AS.App.Auth.isExternalAuthEnabled <$> maybeAuth) == Just True, - _isGoogleAuthEnabled = (AS.App.Auth.isGoogleAuthEnabled <$> maybeAuth) == Just True + _externalAuthProviders = externalAuthProviders } where routes = map (createRouteTemplateData spec) $ AS.getRoutes spec pages = map createPageTemplateData $ AS.getPages spec + externalAuthProviders = + map + (createExternalAuthProviderTemplateData maybeAuth) + [ (AS.App.Auth.isGoogleAuthEnabled, googleAuthInfo), + (AS.App.Auth.isGitHubAuthEnabled, gitHubAuthInfo) + ] maybeAuth = AS.App.auth $ snd $ getApp spec +createExternalAuthProviderTemplateData :: + Maybe AS.App.Auth.Auth -> + (AS.App.Auth.Auth -> Bool, ExternalAuthInfo) -> + ExternalAuthProviderTemplateData +createExternalAuthProviderTemplateData maybeAuth (method, externalAuthInfo) = + ExternalAuthProviderTemplateData + { _authFrontendUrl = frontendLoginUrl externalAuthInfo, + _authServerOauthRedirectUrl = serverOauthRedirectHandlerUrl externalAuthInfo, + _authProviderEnabled = (method <$> maybeAuth) == Just True + } + createRouteTemplateData :: AppSpec -> (String, AS.Route.Route) -> RouteTemplateData createRouteTemplateData spec namedRoute@(_, route) = RouteTemplateData diff --git a/waspc/test/AnalyzerTest.hs b/waspc/test/AnalyzerTest.hs index 9b960b009..b6c238bf2 100644 --- a/waspc/test/AnalyzerTest.hs +++ b/waspc/test/AnalyzerTest.hs @@ -124,7 +124,8 @@ spec_Analyzer = do Auth.methods = Auth.AuthMethods { Auth.usernameAndPassword = Just Auth.usernameAndPasswordConfig, - Auth.google = Nothing + Auth.google = Nothing, + Auth.gitHub = Nothing }, Auth.onAuthFailedRedirectTo = "/", Auth.onAuthSucceededRedirectTo = Nothing diff --git a/waspc/test/AppSpec/ValidTest.hs b/waspc/test/AppSpec/ValidTest.hs index f6d5c58c6..3c019e67e 100644 --- a/waspc/test/AppSpec/ValidTest.hs +++ b/waspc/test/AppSpec/ValidTest.hs @@ -97,7 +97,8 @@ spec_AppSpecValid = do AS.Auth.methods = AS.Auth.AuthMethods { AS.Auth.usernameAndPassword = Just AS.Auth.usernameAndPasswordConfig, - AS.Auth.google = Nothing + AS.Auth.google = Nothing, + AS.Auth.gitHub = Nothing }, AS.Auth.onAuthFailedRedirectTo = "/", AS.Auth.onAuthSucceededRedirectTo = Nothing diff --git a/waspc/waspc.cabal b/waspc/waspc.cabal index d058d04e8..a5d407921 100644 --- a/waspc/waspc.cabal +++ b/waspc/waspc.cabal @@ -237,6 +237,7 @@ library Wasp.Generator.ServerGenerator.AuthG Wasp.Generator.ServerGenerator.Common Wasp.Generator.ServerGenerator.ConfigG + Wasp.Generator.ServerGenerator.ExternalAuthG Wasp.Generator.ServerGenerator.ExternalCodeGenerator Wasp.Generator.ServerGenerator.JobGenerator Wasp.Generator.ServerGenerator.OperationsG @@ -249,6 +250,7 @@ library Wasp.Generator.WebAppGenerator Wasp.Generator.WebAppGenerator.AuthG Wasp.Generator.WebAppGenerator.Common + Wasp.Generator.WebAppGenerator.ExternalAuthG Wasp.Generator.WebAppGenerator.ExternalCodeGenerator Wasp.Generator.WebAppGenerator.OperationsGenerator Wasp.Generator.WebAppGenerator.OperationsGenerator.ResourcesG diff --git a/web/blog/2022-11-15-auth-feature-announcement.md b/web/blog/2022-11-15-auth-feature-announcement.md index e98d12851..3af827c19 100644 --- a/web/blog/2022-11-15-auth-feature-announcement.md +++ b/web/blog/2022-11-15-auth-feature-announcement.md @@ -87,7 +87,7 @@ Follow the Google setup guide [here](https://wasp-lang.dev/docs/integrations/goo import React from 'react' import { Link } from 'react-router-dom' -import { GoogleSignInButton } from '@wasp/auth/buttons/Google' +import { SignInButton as GoogleSignInButton } from '@wasp/auth/helpers/Google' import LoginForm from '@wasp/auth/forms/Login' const Login = () => { diff --git a/web/docs/integrations/github.md b/web/docs/integrations/github.md new file mode 100644 index 000000000..c9523881d --- /dev/null +++ b/web/docs/integrations/github.md @@ -0,0 +1,26 @@ +--- +title: GitHub Integrations +--- + +import useBaseUrl from '@docusaurus/useBaseUrl'; + +# GitHub Integrations + +## GitHub Auth + +To use GitHub as an authentication method (covered [here](/docs/language/features#github)), you'll first need to create a GitHub OAuth App and provide Wasp with your client key and secret. Here is how to do so: + +1. Log into your GitHub account and navigate to: https://github.com/settings/developers +2. Select "New OAuth App" +3. Supply required information + + GitHub Applications Screenshot + + - For "Authorization callback URL", if you just want to test your local app, put in: `http://localhost:3000/auth/login/github` + - Once you know on which URL your API server will be deployed, you can create a new app with that URL instead. + - For example: `https://someotherhost.com/auth/login/github` +4. Hit "Register application" +5. Copy your Client ID and Client secret, and expose them as environment variables named `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` wherever your app is running diff --git a/web/docs/language/features.md b/web/docs/language/features.md index 2f28c4e77..6d0efab12 100644 --- a/web/docs/language/features.md +++ b/web/docs/language/features.md @@ -831,12 +831,13 @@ app MyApp { Entity which represents the user (sometimes also referred to as *Principal*). #### `externalAuthEntity: entity` (optional) -Entity which associates a user with some external authentication provider. We currently offer support for [Google](#google). +Entity which associates a user with some external authentication provider. We currently offer support for [Google](#google) and [GitHub](#github). #### `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. See [here](#username-and-password) for more. * `google`: Provides support for login via Google accounts. See [here](#google) for more. +* `gitHub`: Provides support for login via GitHub accounts. See [here](#github) 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). @@ -1115,11 +1116,12 @@ import AuthError from '@wasp/core/AuthError.js' //... } ``` -This method requires also requires that `externalAuthEntity` be specified in `auth` as [described here](features#externalauthentity). -If you require custom configuration setup or user entity field assignment, you can [override the defaults](#overrides). +This method also requires that `externalAuthEntity` be specified in `auth` as [described here](features#externalauthentity). NOTE: The same `externalAuthEntity` can be used across different social login providers (e.g., both GitHub and Google can use the same entity). -#### Default settings +If you require custom configuration setup or user entity field assignment, you can [override the defaults](features#google-overrides). + +#### Google Default settings - Configuration: - By default, Wasp expects you to set two environment variables in order to use Google authentication: - `GOOGLE_CLIENT_ID` @@ -1132,9 +1134,9 @@ If you require custom configuration setup or user entity field assignment, you c Alternatively, you could add a `displayName` property to your User entity and assign it using the details of their Google account, as described in **Overrides** below ::: -- Here is a link to the default implementations: https://github.com/wasp-lang/wasp/blob/release/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleDefaults.js . These can be overriden as explained below. +- Here is a link to the default implementations: https://github.com/wasp-lang/wasp/blob/release/waspc/data/Generator/templates/server/src/routes/auth/passport/google/defaults.js . These can be overriden as explained below. -#### Overrides +#### Google Overrides If you require modifications to the above, you can add one or more of the following to your `auth.methods.google` dictionary: ```js @@ -1156,9 +1158,9 @@ If you require modifications to the above, you can add one or more of the follow export function config() { // ... return { - clientId, // look up from env or elsewhere, + clientID, // look up from env or elsewhere, clientSecret, // look up from env or elsewhere, - scope: ['profile'] // must include at least 'profile' + scope: ['profile'] // must include at least 'profile' for Google } } @@ -1177,13 +1179,13 @@ If you require modifications to the above, you can add one or more of the follow ``` - `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 +#### Google UI helpers -To use the Google sign-in button or URL on your login page, do either of the following: +To use the Google sign-in button, logo or URL on your login page, do either of the following: ```js ... -import { GoogleSignInButton, googleSignInUrl } from '@wasp/auth/buttons/Google' +import { SignInButton as GoogleSignInButton, signInUrl as googleSignInUrl, logoUrl as googleLogoUrl } from '@wasp/auth/helpers/Google' const Login = () => { return ( @@ -1200,7 +1202,39 @@ const Login = () => { export default Login ``` -You can adjust the height of the button by setting a `height` prop (e.g., ``), which defaults to 40px. NOTE: Under the covers it uses `img.style` instead of `img.height` to be compatible with Tailwind, which sets `height: auto` for `img` in the Tailwind `base` directive. If you need more customization, you can create your own custom component using the `googleSignInUrl`. +If you need more customization than what the buttons provide, you can create your own custom component using the `googleSignInUrl`. + +### GitHub + +`gitHub` authentication makes it possible to use GitHub's OAuth 2.0 service to sign GitHub users into your app. To enable it, add `gitHub: {}` to your `auth.methods` dictionary to use it with default settings: + +```js + //... + + auth: { + userEntity: User, + externalAuthEntity: SocialLogin, + methods: { + gitHub: {} + }, + //... + } +``` + +This method requires also requires that `externalAuthEntity` be specified in `auth` as [described here](features#externalauthentity). NOTE: The same `externalAuthEntity` can be used across different social login providers (e.g., both GitHub and Google can use the same entity). + +If you require custom configuration setup or user entity field assignment, you can override the defaults. Please check out that section for [Google overrides](features#google-overrides), as the information is the same. + +#### GitHub Default settings +- Configuration: + - By default, Wasp expects you to set two environment variables in order to use GitHub authentication: + - `GITHUB_CLIENT_ID` + - `GITHUB_CLIENT_SECRET` + - These can be obtained in your GitHub project dashboard. See [here](/docs/integrations/github#github-auth) for more. +- The same sign-in logic applies as for Google. Please see [that section](features#google-default-settings) for more. +- Here is a link to the default implementations: https://github.com/wasp-lang/wasp/blob/release/waspc/data/Generator/templates/server/src/routes/auth/passport/github/defaults.js + +NOTE: The same UI helpers apply as for Google. Please [see here](features#google-ui-helpers) for more. ### `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: diff --git a/web/docs/tutorials/todo-app/06-auth.md b/web/docs/tutorials/todo-app/06-auth.md index 4d3bb863b..762e10b65 100644 --- a/web/docs/tutorials/todo-app/06-auth.md +++ b/web/docs/tutorials/todo-app/06-auth.md @@ -48,7 +48,7 @@ app TodoApp { // Expects entity User to have (username:String) and (password:String) fields. userEntity: User, methods: { - // We also support Google, with more on the way! + // We also support Google and GitHub, with more on the way! usernameAndPassword: {} }, // We'll see how this is used a bit later diff --git a/web/static/img/integrations-github-1.png b/web/static/img/integrations-github-1.png new file mode 100644 index 0000000000000000000000000000000000000000..687e8f707e24c3522c51c6a8afd71b145816a19e GIT binary patch literal 67440 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�= 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 z%0A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

=EXsSlJ(^qb2LY|+o}pJ!y|e8w+*o#VfoxA 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 Date: Tue, 20 Dec 2022 16:03:25 +0100 Subject: [PATCH 02/16] lp: removed annoucement bar. --- web/docusaurus.config.js | 2 ++ web/src/components/Nav/index.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web/docusaurus.config.js b/web/docusaurus.config.js index faf6ce1b1..3c75f409f 100644 --- a/web/docusaurus.config.js +++ b/web/docusaurus.config.js @@ -19,6 +19,7 @@ module.exports = { 'https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap' ], themeConfig: { + /* announcementBar: { id: 'Beta_is_here', content: 'Wasp Hackathon #1 is underway! 🚀 Join now', @@ -26,6 +27,7 @@ module.exports = { textColor: '#fff', isCloseable: false, }, + */ navbar: { title: '.wasp (beta)', logo: { diff --git a/web/src/components/Nav/index.js b/web/src/components/Nav/index.js index 15b0669b0..4ecc7fa0b 100644 --- a/web/src/components/Nav/index.js +++ b/web/src/components/Nav/index.js @@ -119,7 +119,7 @@ const Nav = () => { return ( <> - + {/* */}