Merge branch 'release' into vince-add-todo-ts-example

This commit is contained in:
vincanger 2023-01-16 16:20:24 +01:00
commit f8468c9f18
68 changed files with 1060 additions and 284 deletions

View File

@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Cancel actions in progress of same workflow and same branch - name: Cancel actions in progress of same workflow and same branch
uses: styfle/cancel-workflow-action@0.9.0 uses: styfle/cancel-workflow-action@0.11.0
with: with:
access_token: ${{ github.token }} access_token: ${{ github.token }}
@ -40,7 +40,14 @@ jobs:
cabal: cabal:
- "3.6.2.0" - "3.6.2.0"
os: os:
- ubuntu-latest # Using Ubuntu 20.04 due to recent GLIBC_2.34 version mismatch errors for
# users and other services. The strategy here is to pick a slightly older LTS
# version of Ubuntu to build on so glibc is not too new. glibc has great backward
# compatibility though, so older builds will run on new OSs (but not vice versa).
# We can return to `ubuntu-latest` when enough time has elapsed.
# Still looking into musl static binaries.
# Ref: https://github.com/wasp-lang/wasp/issues/650
- ubuntu-20.04
- macos-latest - macos-latest
- windows-latest - windows-latest
@ -55,11 +62,11 @@ jobs:
git config --global core.eol lf git config --global core.eol lf
- name: Checkout the repo - name: Checkout the repo
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up Haskell - name: Set up Haskell
id: setup-haskell-cabal id: setup-haskell-cabal
uses: haskell/actions/setup@v1 uses: haskell/actions/setup@v2
with: with:
ghc-version: ${{ matrix.ghc }} ghc-version: ${{ matrix.ghc }}
cabal-version: ${{ matrix.cabal }} cabal-version: ${{ matrix.cabal }}
@ -70,7 +77,7 @@ jobs:
cabal --version cabal --version
- name: Cache - name: Cache
uses: actions/cache@v2 uses: actions/cache@v3
with: with:
path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }} path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }}
# TODO: Right now, actions/cache updates cache only if cache was not fe # TODO: Right now, actions/cache updates cache only if cache was not fe
@ -86,11 +93,11 @@ jobs:
# fails multiple times while others don't, its cache will likely get evicted, # fails multiple times while others don't, its cache will likely get evicted,
# making it even slower to test and fix (uffff). # making it even slower to test and fix (uffff).
# When they fix this, we should remove ${{ github.run_id }} from the end of the key. # When they fix this, we should remove ${{ github.run_id }} from the end of the key.
key: wasp-build-${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('waspc/waspc.cabal') }}-${{ hashFiles('waspc/cabal.project') }}-${{ github.run_id }} key: wasp-build-${{ matrix.os }}-${{ matrix.ghc }}-${{ hashFiles('waspc/waspc.cabal') }}-${{ hashFiles('waspc/cabal.project') }}-${{ github.run_id }}
restore-keys: wasp-build-${{ runner.os }}-${{ matrix.ghc }}- restore-keys: wasp-build-${{ matrix.os }}-${{ matrix.ghc }}-
- name: Check Haskell code formatting - name: Check Haskell code formatting
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-20.04'
run: ./run ormolu:check run: ./run ormolu:check
- name: Build external dependencies - name: Build external dependencies
@ -100,7 +107,7 @@ jobs:
run: cabal build all run: cabal build all
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v2 uses: actions/setup-node@v3
with: with:
node-version: '18' node-version: '18'
@ -112,7 +119,7 @@ jobs:
run: cabal test run: cabal test
- name: Create binary package (Unix) - name: Create binary package (Unix)
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest') if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest')
run: | run: |
OS_NAME=`case "${{ runner.os }}" in Linux) echo "linux";; macOS) echo "macos";; *) exit 1;; esac` OS_NAME=`case "${{ runner.os }}" in Linux) echo "linux";; macOS) echo "macos";; *) exit 1;; esac`
mkdir artifacts mkdir artifacts
@ -120,7 +127,7 @@ jobs:
- name: Create Github release - name: Create Github release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest') if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-latest')
with: with:
draft: true draft: true
allowUpdates: true allowUpdates: true

View File

@ -1,6 +1,6 @@
app hackathonBetaSubmissions { app hackathonBetaSubmissions {
wasp: { wasp: {
version: "^0.7.0" version: "^0.8.0"
}, },
db: { db: {
system: PostgreSQL system: PostgreSQL

View File

@ -1,6 +1,6 @@
app Conduit { app Conduit {
wasp: { wasp: {
version: "^0.7.0" version: "^0.8.0"
}, },
title: "Conduit", title: "Conduit",

View File

@ -1,6 +1,6 @@
app Thoughts { app Thoughts {
wasp: { wasp: {
version: "^0.7.0" version: "^0.8.0"
}, },
title: "Thoughts", title: "Thoughts",
db: { system: PostgreSQL }, db: { system: PostgreSQL },

View File

@ -1,6 +1,6 @@
app ItWaspsOnMyMachine { app ItWaspsOnMyMachine {
wasp: { wasp: {
version: "^0.7.0" version: "^0.8.0"
}, },
title: "It Wasps On My Machine", title: "It Wasps On My Machine",

View File

@ -1,6 +1,6 @@
app TodoApp { app TodoApp {
wasp: { wasp: {
version: "^0.7.0" version: "^0.8.0"
}, },
title: "Todo app", title: "Todo app",

View File

@ -1,6 +1,6 @@
app trello { app trello {
wasp: { wasp: {
version: "^0.7.0" version: "^0.8.0"
}, },
title: "trello", title: "trello",

View File

@ -1,6 +1,6 @@
app waspleau { app waspleau {
wasp: { wasp: {
version: "^0.7.0" version: "^0.8.0"
}, },
title: "Waspleau", title: "Waspleau",

View File

@ -1,5 +1,17 @@
# Changelog # Changelog
## v0.8.0
### BREAKING CHANGES
- 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.
## v0.7.3 ## v0.7.3
### MINOR CLI BREAKING CHANGE ### MINOR CLI BREAKING CHANGE

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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 (
<a href={googleSignInUrl}>
<img alt="Sign in with Google"
style={{ height: props.height || 40 }}
src="/images/btn_google_signin_dark_normal_web@2x.png" />
</a>
)
}

View File

@ -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 (
<div style={containerStyle}>
<a href={signInUrl} style={linkStyle}>
<img alt="{= displayName =} Icon" src={logoUrl} style={logoStyle} />
<span>Log in with {= displayName =}</span>
</a>
</div>
)
}

View File

@ -22,13 +22,13 @@ const router = (
{=/ routes =} {=/ routes =}
{=# isExternalAuthEnabled =} {=# isExternalAuthEnabled =}
{=# externalAuthProviders =}
{=# isGoogleAuthEnabled =} {=# authProviderEnabled =}
<Route exact path="/auth/login/google"> <Route exact path="{= authFrontendUrl =}">
<OAuthCodeExchange pathToApiServerRouteHandlingOauthRedirect="/auth/external/google/validateCodeForLogin" /> <OAuthCodeExchange pathToApiServerRouteHandlingOauthRedirect="{= authServerOauthRedirectUrl =}" />
</Route> </Route>
{=/ isGoogleAuthEnabled =} {=/ authProviderEnabled =}
{=/ externalAuthProviders =}
{=/ isExternalAuthEnabled =} {=/ isExternalAuthEnabled =}
</div> </div>
</Router> </Router>

View File

@ -5,13 +5,13 @@
export { {= configFnIdentifier =} as configFn } export { {= configFnIdentifier =} as configFn }
{=/ doesConfigFnExist =} {=/ doesConfigFnExist =}
{=^ doesConfigFnExist =} {=^ doesConfigFnExist =}
export { configFn } from './googleDefaults.js' export { configFn } from './defaults.js'
{=/ doesConfigFnExist =} {=/ doesConfigFnExist =}
{=# doesOnSignInFnExist =} {=# doesGetUserFieldsFnExist =}
{=& getUserFieldsFnImportStatement =} {=& getUserFieldsFnImportStatement =}
export { {= getUserFieldsFnIdentifier =} as getUserFieldsFn } export { {= getUserFieldsFnIdentifier =} as getUserFieldsFn }
{=/ doesOnSignInFnExist =} {=/ doesGetUserFieldsFnExist =}
{=^ doesOnSignInFnExist =} {=^ doesGetUserFieldsFnExist =}
export { getUserFieldsFn } from './googleDefaults.js' export { getUserFieldsFn } from './defaults.js'
{=/ doesOnSignInFnExist =} {=/ doesGetUserFieldsFnExist =}

View File

@ -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
}

View File

@ -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())

View File

@ -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 }
}

View File

@ -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())

View File

@ -2,10 +2,10 @@ import { generateAvailableDictionaryUsername } from '../../../../core/auth.js'
// Default implementation if there is no `auth.methods.google.configFn`. // Default implementation if there is no `auth.methods.google.configFn`.
export function 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'] const clientSecret = process.env['GOOGLE_CLIENT_SECRET']
if (!clientId) { if (!clientID) {
throw new Error("Missing GOOGLE_CLIENT_ID environment variable.") 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.") 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`. // Default implementation if there is no `auth.methods.google.getUserFieldsFn`.

View File

@ -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

View File

@ -1,14 +1,24 @@
{{={= =}=}} {{={= =}=}}
import express from 'express' import express from 'express'
import { initRouter } from './generic/provider.js'
{=# isGoogleAuthEnabled =} const providerMap = new Map();
import googleAuth from './google/google.js' {=# providers =}
{=/ isGoogleAuthEnabled =} {=# isEnabled =}
providerMap.set('{= slug =}', { npmPackage: '{= npmPackage =}', passportImportPath: '{= passportImportPath =}' })
{=/ isEnabled =}
{=/ providers =}
const router = express.Router() const router = express.Router()
{=# isGoogleAuthEnabled =} async function initProviders(providers) {
router.use('/google', googleAuth) for (let [providerSlug, { npmPackage, passportImportPath }] of providers) {
{=/ isGoogleAuthEnabled =} 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 export default router

View File

@ -1,7 +1,7 @@
app waspBuild { app waspBuild {
db: { system: PostgreSQL }, db: { system: PostgreSQL },
wasp: { wasp: {
version: "^0.7.3" version: "^0.8.0"
}, },
title: "waspBuild" title: "waspBuild"
} }

View File

@ -1,6 +1,6 @@
app waspCompile { app waspCompile {
wasp: { wasp: {
version: "^0.7.3" version: "^0.8.0"
}, },
title: "waspCompile" title: "waspCompile"
} }

View File

@ -1,7 +1,7 @@
app waspJob { app waspJob {
db: { system: PostgreSQL }, db: { system: PostgreSQL },
wasp: { wasp: {
version: "^0.7.3" version: "^0.8.0"
}, },
title: "waspJob" title: "waspJob"
} }

View File

@ -1,6 +1,6 @@
app waspMigrate { app waspMigrate {
wasp: { wasp: {
version: "^0.7.3" version: "^0.8.0"
}, },
title: "waspMigrate" title: "waspMigrate"
} }

View File

@ -1,6 +1,6 @@
app waspNew { app waspNew {
wasp: { wasp: {
version: "^0.7.3" version: "^0.8.0"
}, },
title: "waspNew" title: "waspNew"
} }

View File

@ -2,7 +2,8 @@ import React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import LoginForm from '@wasp/auth/forms/Login' 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 = () => { const Login = () => {
return ( return (
@ -15,6 +16,7 @@ const Login = () => {
{/* <div> {/* <div>
<GoogleSignInButton/> <GoogleSignInButton/>
<GitHubSignInButton/>
</div> */} </div> */}
</> </>
) )

View File

@ -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 }
}

View File

@ -3,7 +3,7 @@ import { generateAvailableUsername } from '@wasp/core/auth.js'
export function config() { export function config() {
console.log("Inside user-supplied Google config") console.log("Inside user-supplied Google config")
return { return {
clientId: process.env['GOOGLE_CLIENT_ID'], clientID: process.env['GOOGLE_CLIENT_ID'],
clientSecret: process.env['GOOGLE_CLIENT_SECRET'], clientSecret: process.env['GOOGLE_CLIENT_SECRET'],
scope: ['profile'] scope: ['profile']
} }

View File

@ -1,6 +1,6 @@
app todoApp { app todoApp {
wasp: { wasp: {
version: "^0.7.0" version: "^0.8.0"
}, },
title: "ToDo App", title: "ToDo App",
// head: [], // head: [],
@ -16,6 +16,10 @@ app todoApp {
// google: { // google: {
// configFn: import { config } from "@server/auth/google.js", // configFn: import { config } from "@server/auth/google.js",
// getUserFieldsFn: import { getUserFields } 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", onAuthFailedRedirectTo: "/login",

View File

@ -4,11 +4,12 @@
module Wasp.AppSpec.App.Auth module Wasp.AppSpec.App.Auth
( Auth (..), ( Auth (..),
AuthMethods (..), AuthMethods (..),
GoogleConfig (..), ExternalAuthConfig (..),
usernameAndPasswordConfig, usernameAndPasswordConfig,
isUsernameAndPasswordAuthEnabled, isUsernameAndPasswordAuthEnabled,
isGoogleAuthEnabled,
isExternalAuthEnabled, isExternalAuthEnabled,
isGoogleAuthEnabled,
isGitHubAuthEnabled,
) )
where where
@ -29,7 +30,8 @@ data Auth = Auth
data AuthMethods = AuthMethods data AuthMethods = AuthMethods
{ usernameAndPassword :: Maybe UsernameAndPasswordConfig, { usernameAndPassword :: Maybe UsernameAndPasswordConfig,
google :: Maybe GoogleConfig google :: Maybe ExternalAuthConfig,
gitHub :: Maybe ExternalAuthConfig
} }
deriving (Show, Eq, Data) deriving (Show, Eq, Data)
@ -39,7 +41,7 @@ data UsernameAndPasswordConfig = UsernameAndPasswordConfig
} }
deriving (Show, Eq, Data) deriving (Show, Eq, Data)
data GoogleConfig = GoogleConfig data ExternalAuthConfig = ExternalAuthConfig
{ configFn :: Maybe ExtImport, { configFn :: Maybe ExtImport,
getUserFieldsFn :: Maybe ExtImport getUserFieldsFn :: Maybe ExtImport
} }
@ -51,8 +53,11 @@ usernameAndPasswordConfig = UsernameAndPasswordConfig Nothing
isUsernameAndPasswordAuthEnabled :: Auth -> Bool isUsernameAndPasswordAuthEnabled :: Auth -> Bool
isUsernameAndPasswordAuthEnabled = isJust . usernameAndPassword . methods isUsernameAndPasswordAuthEnabled = isJust . usernameAndPassword . methods
isExternalAuthEnabled :: Auth -> Bool
isExternalAuthEnabled auth = any ($ auth) [isGoogleAuthEnabled, isGitHubAuthEnabled]
isGoogleAuthEnabled :: Auth -> Bool isGoogleAuthEnabled :: Auth -> Bool
isGoogleAuthEnabled = isJust . google . methods isGoogleAuthEnabled = isJust . google . methods
isExternalAuthEnabled :: Auth -> Bool isGitHubAuthEnabled :: Auth -> Bool
isExternalAuthEnabled auth = any ($ auth) [isGoogleAuthEnabled] isGitHubAuthEnabled = isJust . gitHub . methods

View File

@ -32,7 +32,6 @@ import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App 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.App.Auth
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency 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.App.Server as AS.App.Server
import qualified Wasp.AppSpec.Entity as AS.Entity import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.AppSpec.Util (isPgBossJobExecutorUsed) import Wasp.AppSpec.Util (isPgBossJobExecutorUsed)
@ -47,6 +46,7 @@ import qualified Wasp.Generator.NpmDependencies as N
import Wasp.Generator.ServerGenerator.AuthG (genAuth) import Wasp.Generator.ServerGenerator.AuthG (genAuth)
import qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.Generator.ServerGenerator.Common as C
import Wasp.Generator.ServerGenerator.ConfigG (genConfigFile) import Wasp.Generator.ServerGenerator.ConfigG (genConfigFile)
import Wasp.Generator.ServerGenerator.ExternalAuthG (depsRequiredByPassport)
import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir, extServerCodeGeneratorStrategy, extSharedCodeGeneratorStrategy) import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir, extServerCodeGeneratorStrategy, extSharedCodeGeneratorStrategy)
import Wasp.Generator.ServerGenerator.JobGenerator (depsRequiredByJobs, genJobExecutors, genJobs) import Wasp.Generator.ServerGenerator.JobGenerator (depsRequiredByJobs, genJobExecutors, genJobs)
import Wasp.Generator.ServerGenerator.OperationsG (genOperations) import Wasp.Generator.ServerGenerator.OperationsG (genOperations)
@ -239,16 +239,6 @@ genRoutesDir spec =
operationsRouteInRootRouter :: String operationsRouteInRootRouter :: String
operationsRouteInRootRouter = "operations" 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 :: AppSpec -> Generator Bool
areServerPatchesUsed spec = not . null <$> genPatches spec areServerPatchesUsed spec = not . null <$> genPatches spec

View File

@ -4,16 +4,12 @@ module Wasp.Generator.ServerGenerator.AuthG
where where
import Data.Aeson (object, (.=)) import Data.Aeson (object, (.=))
import Data.Maybe (fromJust, fromMaybe, isJust) import Data.Maybe (fromMaybe)
import StrongPath import StrongPath
( Dir, ( File',
File',
Path,
Path', Path',
Posix,
Rel, Rel,
reldir, reldir,
reldirP,
relfile, relfile,
(</>), (</>),
) )
@ -23,12 +19,10 @@ import qualified Wasp.AppSpec as AS
import qualified Wasp.AppSpec.App as AS.App import qualified Wasp.AppSpec.App as AS.App
import qualified Wasp.AppSpec.App.Auth as AS.Auth import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.AppSpec.Valid (getApp) import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport)
import Wasp.Generator.Monad (Generator) import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.ServerGenerator.Common as C import qualified Wasp.Generator.ServerGenerator.Common as C
import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir) import Wasp.Generator.ServerGenerator.ExternalAuthG (genPassportAuth)
import Wasp.Util ((<++>)) import Wasp.Util ((<++>))
import qualified Wasp.Util as 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) [ "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 :: AS.Auth.Auth -> Generator FileDraft
genUtilsJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData) genUtilsJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
where where
@ -172,46 +148,5 @@ genUtilsJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplD
utilsFileInSrcDir :: Path' (Rel C.ServerSrcDir) File' utilsFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
utilsFileInSrcDir = [relfile|routes/auth/utils.js|] 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 :: AS.Auth.Auth -> String
getOnAuthSucceededRedirectToOrDefault auth = fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth) getOnAuthSucceededRedirectToOrDefault auth = fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)

View File

@ -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")

View File

@ -15,6 +15,7 @@ import StrongPath
Posix, Posix,
Rel, Rel,
relDirToPosix, relDirToPosix,
reldir,
relfile, relfile,
(</>), (</>),
) )
@ -35,7 +36,12 @@ import Wasp.Generator.Monad (Generator)
import qualified Wasp.Generator.NpmDependencies as N import qualified Wasp.Generator.NpmDependencies as N
import Wasp.Generator.WebAppGenerator.AuthG (genAuth) import Wasp.Generator.WebAppGenerator.AuthG (genAuth)
import qualified Wasp.Generator.WebAppGenerator.Common as C 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.OperationsGenerator (genOperations)
import Wasp.Generator.WebAppGenerator.RouterGenerator (genRouter) import Wasp.Generator.WebAppGenerator.RouterGenerator (genRouter)
import Wasp.Util ((<++>)) import Wasp.Util ((<++>))
@ -147,24 +153,33 @@ genGitignore =
genPublicDir :: AppSpec -> Generator [FileDraft] genPublicDir :: AppSpec -> Generator [FileDraft]
genPublicDir spec = do genPublicDir spec = do
publicIndexHtmlFd <- genPublicIndexHtml spec publicIndexHtmlFd <- genPublicIndexHtml spec
return $ return
[ publicIndexHtmlFd, [ publicIndexHtmlFd,
genFaviconFd, genFaviconFd,
genManifestFd genManifestFd
] ]
++ genGoogleSigninImage <++> genSocialLoginIcons maybeAuth
where where
maybeAuth = AS.App.auth $ snd $ getApp spec maybeAuth = AS.App.auth $ snd $ getApp spec
genFaviconFd = C.mkTmplFd (C.asTmplFile [relfile|public/favicon.ico|]) 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 = genManifestFd =
let tmplData = object ["appName" .= (fst (getApp spec) :: String)] let tmplData = object ["appName" .= (fst (getApp spec) :: String)]
tmplFile = C.asTmplFile [relfile|public/manifest.json|] tmplFile = C.asTmplFile [relfile|public/manifest.json|]
in C.mkTmplFdWithData tmplFile tmplData 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 :: AppSpec -> Generator FileDraft
genPublicIndexHtml spec = genPublicIndexHtml spec =
return $ return $

View File

@ -7,6 +7,7 @@ import Data.Aeson (object, (.=))
import Data.Aeson.Types (Pair) import Data.Aeson.Types (Pair)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import StrongPath (File', Path', Rel', reldir, relfile, (</>)) import StrongPath (File', Path', Rel', reldir, relfile, (</>))
import qualified StrongPath as SP
import Wasp.AppSpec (AppSpec) import Wasp.AppSpec (AppSpec)
import qualified Wasp.AppSpec.App as AS.App 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.App.Auth
@ -15,6 +16,8 @@ import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator) import Wasp.Generator.Monad (Generator)
import Wasp.Generator.WebAppGenerator.Common as C 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 ((<++>)) import Wasp.Util ((<++>))
genAuth :: AppSpec -> Generator [FileDraft] genAuth :: AppSpec -> Generator [FileDraft]
@ -80,12 +83,33 @@ genSignupForm auth =
genExternalAuth :: AS.Auth.Auth -> Generator [FileDraft] genExternalAuth :: AS.Auth.Auth -> Generator [FileDraft]
genExternalAuth auth genExternalAuth auth
| AS.App.Auth.isExternalAuthEnabled auth = (:) <$> genOAuthCodeExchange auth <*> genSocialLoginButtons auth | AS.App.Auth.isExternalAuthEnabled auth = (:) <$> genOAuthCodeExchange auth <*> genSocialLoginHelpers auth
| otherwise = return [] | otherwise = return []
genSocialLoginButtons :: AS.Auth.Auth -> Generator [FileDraft] genSocialLoginHelpers :: AS.Auth.Auth -> Generator [FileDraft]
genSocialLoginButtons auth = genSocialLoginHelpers auth =
return [C.mkTmplFd (C.asTmplFile [relfile|src/auth/buttons/Google.js|]) | AS.App.Auth.isGoogleAuthEnabled 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 :: AS.Auth.Auth -> Generator FileDraft
genOAuthCodeExchange auth = genOAuthCodeExchange auth =

View File

@ -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)

View File

@ -23,6 +23,7 @@ import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator) import Wasp.Generator.Monad (Generator)
import Wasp.Generator.WebAppGenerator.Common (asTmplFile, asWebAppSrcFile) import Wasp.Generator.WebAppGenerator.Common (asTmplFile, asWebAppSrcFile)
import qualified Wasp.Generator.WebAppGenerator.Common as C import qualified Wasp.Generator.WebAppGenerator.Common as C
import Wasp.Generator.WebAppGenerator.ExternalAuthG (ExternalAuthInfo (..), frontendLoginUrl, gitHubAuthInfo, googleAuthInfo, serverOauthRedirectHandlerUrl)
import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator (extClientCodeDirInWebAppSrcDir) import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator (extClientCodeDirInWebAppSrcDir)
data RouterTemplateData = RouterTemplateData data RouterTemplateData = RouterTemplateData
@ -30,7 +31,7 @@ data RouterTemplateData = RouterTemplateData
_pagesToImport :: ![PageTemplateData], _pagesToImport :: ![PageTemplateData],
_isAuthEnabled :: Bool, _isAuthEnabled :: Bool,
_isExternalAuthEnabled :: Bool, _isExternalAuthEnabled :: Bool,
_isGoogleAuthEnabled :: Bool _externalAuthProviders :: ![ExternalAuthProviderTemplateData]
} }
instance ToJSON RouterTemplateData where instance ToJSON RouterTemplateData where
@ -40,7 +41,7 @@ instance ToJSON RouterTemplateData where
"pagesToImport" .= _pagesToImport routerTD, "pagesToImport" .= _pagesToImport routerTD,
"isAuthEnabled" .= _isAuthEnabled routerTD, "isAuthEnabled" .= _isAuthEnabled routerTD,
"isExternalAuthEnabled" .= _isExternalAuthEnabled routerTD, "isExternalAuthEnabled" .= _isExternalAuthEnabled routerTD,
"isGoogleAuthEnabled" .= _isGoogleAuthEnabled routerTD "externalAuthProviders" .= _externalAuthProviders routerTD
] ]
data RouteTemplateData = RouteTemplateData data RouteTemplateData = RouteTemplateData
@ -68,6 +69,21 @@ instance ToJSON PageTemplateData where
"importFrom" .= _importFrom pageTD "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 :: AppSpec -> Generator FileDraft
genRouter spec = do genRouter spec = do
return $ return $
@ -87,13 +103,30 @@ createRouterTemplateData spec =
_pagesToImport = pages, _pagesToImport = pages,
_isAuthEnabled = isAuthEnabled spec, _isAuthEnabled = isAuthEnabled spec,
_isExternalAuthEnabled = (AS.App.Auth.isExternalAuthEnabled <$> maybeAuth) == Just True, _isExternalAuthEnabled = (AS.App.Auth.isExternalAuthEnabled <$> maybeAuth) == Just True,
_isGoogleAuthEnabled = (AS.App.Auth.isGoogleAuthEnabled <$> maybeAuth) == Just True _externalAuthProviders = externalAuthProviders
} }
where where
routes = map (createRouteTemplateData spec) $ AS.getRoutes spec routes = map (createRouteTemplateData spec) $ AS.getRoutes spec
pages = map createPageTemplateData $ AS.getPages 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 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 :: AppSpec -> (String, AS.Route.Route) -> RouteTemplateData
createRouteTemplateData spec namedRoute@(_, route) = createRouteTemplateData spec namedRoute@(_, route) =
RouteTemplateData RouteTemplateData

View File

@ -124,7 +124,8 @@ spec_Analyzer = do
Auth.methods = Auth.methods =
Auth.AuthMethods Auth.AuthMethods
{ Auth.usernameAndPassword = Just Auth.usernameAndPasswordConfig, { Auth.usernameAndPassword = Just Auth.usernameAndPasswordConfig,
Auth.google = Nothing Auth.google = Nothing,
Auth.gitHub = Nothing
}, },
Auth.onAuthFailedRedirectTo = "/", Auth.onAuthFailedRedirectTo = "/",
Auth.onAuthSucceededRedirectTo = Nothing Auth.onAuthSucceededRedirectTo = Nothing

View File

@ -97,7 +97,8 @@ spec_AppSpecValid = do
AS.Auth.methods = AS.Auth.methods =
AS.Auth.AuthMethods AS.Auth.AuthMethods
{ AS.Auth.usernameAndPassword = Just AS.Auth.usernameAndPasswordConfig, { AS.Auth.usernameAndPassword = Just AS.Auth.usernameAndPasswordConfig,
AS.Auth.google = Nothing AS.Auth.google = Nothing,
AS.Auth.gitHub = Nothing
}, },
AS.Auth.onAuthFailedRedirectTo = "/", AS.Auth.onAuthFailedRedirectTo = "/",
AS.Auth.onAuthSucceededRedirectTo = Nothing AS.Auth.onAuthSucceededRedirectTo = Nothing

View File

@ -6,7 +6,7 @@ cabal-version: 2.4
-- Consider using hpack, or maybe even hpack-dhall. -- Consider using hpack, or maybe even hpack-dhall.
name: waspc name: waspc
version: 0.7.3 version: 0.8.0
description: Please see the README on GitHub at <https://github.com/wasp-lang/wasp/waspc#readme> description: Please see the README on GitHub at <https://github.com/wasp-lang/wasp/waspc#readme>
homepage: https://github.com/wasp-lang/wasp/waspc#readme homepage: https://github.com/wasp-lang/wasp/waspc#readme
bug-reports: https://github.com/wasp-lang/wasp/issues bug-reports: https://github.com/wasp-lang/wasp/issues
@ -237,6 +237,7 @@ library
Wasp.Generator.ServerGenerator.AuthG Wasp.Generator.ServerGenerator.AuthG
Wasp.Generator.ServerGenerator.Common Wasp.Generator.ServerGenerator.Common
Wasp.Generator.ServerGenerator.ConfigG Wasp.Generator.ServerGenerator.ConfigG
Wasp.Generator.ServerGenerator.ExternalAuthG
Wasp.Generator.ServerGenerator.ExternalCodeGenerator Wasp.Generator.ServerGenerator.ExternalCodeGenerator
Wasp.Generator.ServerGenerator.JobGenerator Wasp.Generator.ServerGenerator.JobGenerator
Wasp.Generator.ServerGenerator.OperationsG Wasp.Generator.ServerGenerator.OperationsG
@ -249,6 +250,7 @@ library
Wasp.Generator.WebAppGenerator Wasp.Generator.WebAppGenerator
Wasp.Generator.WebAppGenerator.AuthG Wasp.Generator.WebAppGenerator.AuthG
Wasp.Generator.WebAppGenerator.Common Wasp.Generator.WebAppGenerator.Common
Wasp.Generator.WebAppGenerator.ExternalAuthG
Wasp.Generator.WebAppGenerator.ExternalCodeGenerator Wasp.Generator.WebAppGenerator.ExternalCodeGenerator
Wasp.Generator.WebAppGenerator.OperationsGenerator Wasp.Generator.WebAppGenerator.OperationsGenerator
Wasp.Generator.WebAppGenerator.OperationsGenerator.ResourcesG Wasp.Generator.WebAppGenerator.OperationsGenerator.ResourcesG

View File

@ -87,7 +87,7 @@ Follow the Google setup guide [here](https://wasp-lang.dev/docs/integrations/goo
import React from 'react' import React from 'react'
import { Link } from 'react-router-dom' 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' import LoginForm from '@wasp/auth/forms/Login'
const Login = () => { const Login = () => {

View File

@ -16,9 +16,9 @@ import ImgWithCaption from './components/ImgWithCaption'
![amicus hero shot](../static/img/amicus-usecase/amicus-hero-shot.png) ![amicus hero shot](../static/img/amicus-usecase/amicus-hero-shot.png)
[Erlis Kllogjri](https://github.com/ErlisK) is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how Amicus started out. [Erlis Kllogjri](https://github.com/ErlisK) is an engineer based in San Francisco with broad experience ranging from mechanical engineering and C/C++ microcontroller programming to Python and web app development. In his free time, Erlis enjoys working on side projects, which is also how [Amicus](https://www.amicus.work/) started out.
Amicus is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law. [Amicus](https://www.amicus.work/) is a SaaS for legal teams - think about it as "Asana for lawyers", but with features and workflows tailored to the domain of law.
Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next! Read on to learn how long it took Erlis to develop the first version of his SaaS with Wasp, how he got his first paying customers, and what features he plans to add next!

View File

@ -0,0 +1,132 @@
---
title: 'Hosting Our First Hackathon: Results & Review'
authors: [vinny]
tags: [fullstack, webdev, hackathon, startups]
---
import ImgWithCaption from './components/ImgWithCaption'
To finalize the Wasp Beta launch week, we held a Beta Hackathon, which we dubbed the “Betathon”. The idea was to hold a simple, open, and fun hackathon to encourage users to build with Wasp, and thats exactly what they did!
As Wasp is still in its early days, we werent sure what the response would be, or if thered be any response at all. Considering that we didnt do much promotion of the Hackathon outside of our own channels, we were surprised by the results.
In this post, Ill give you a quick run-down of:
- the hackathon results 🏆
- how the hackathon was organized
- how we promoted it
- the community response
## …and the Winners Are:
Whats a hackathon without the participants!? Lets get this post off to a proper start by congratulating our winners and showcasing their work. 🔍
### 🥇 Tims Job Board
<ImgWithCaption
alt="Tim's Job Board"
source="img/betathon/tim.png"
/>
Tim really went for it and created a feature-rich Job Board:
- View the [App](https://client-production-54e7.up.railway.app/) & [GitHub Repo](https://github.com/tskaggs/wasp-jobs)
- Follow [Tim on Twitter](https://twitter.com/tskaggs)
- 🎉 Prizes: Wasp-colored Mechanical Keyboard, Wasp swag, $200 [Railway.app](http://Railway.app) credits
> “***Wasp is very awesome!*** *Easy setup and start-up especially if you're familiar with the Prisma ORM and Tailwind CSS. The stack is small but powerful... I'm going to use Wasp on a few MVP projects this year.”* - Tim
>
### 🥈Chriss “Cook Wherever” Recipes App
<ImgWithCaption
alt="Chris's Cook Wherever Recipes App"
source="img/betathon/chris.png"
/>
Chris created an extensive database of recipes in a slick app:
- View the [App](https://cookwherever.com) & [GitHub Repo](https://github.com/cookwherever/cookwherever)
- Follow [Chris on Twitter](https://twitter.com/breadchris)
- 🎉 Prizes: Wasp swag, $125 [Railway.app](http://Railway.app) credits
> “***This was the best app dev experience I ever had!*** *…Walking through the docs, I immediately figured out how to use Wasp and was able to make a prototype in a couple of days.”* - Chris
>
### 🥉 Richards Roadmap & Feature Voting App
<ImgWithCaption
alt="Richards Roadmap & Feature Voting App"
source="img/betathon/richard.png"
/>
- View the [App](https://droad.netlify.app/) & [GitHub Repo](https://github.com/Fecony/droad)
- Follow [Richard on Twitter](https://twitter.com/webrickony)
- 🎉 Prizes: Wasp Shirt, $75 [Railway.app](http://Railway.app) credits
> “***I liked how Wasp simplified writing query/actions*** *that are used to interact with the backend and frontend. How everything is defined and configured in wasp file and just works. Also […] login/signup was really easy to do since Wasp provides these two methods for use.”* -
>
### 🥉 Emmanuels Notes App
<ImgWithCaption
alt="Emmanuels Notes App"
source="img/betathon/emmanuel.png"
/>
- View the [GitHub Repo](https://github.com/EmmanuelTheCoder/noteapp-with-wasp)
- Follow [Emmanuel on Twitter](https://twitter.com/EmmanuelCoder)
- 🎉 Prizes: Wasp Shirt, $75 [Railway.app](http://Railway.app) credits
> *I joined the hackathon less than 48 hours before the submission deadline.* ***Wasp made it look easy because it handled the hard parts for me.*** *For example, username/password authentication took less than 7 lines of code to implement. -* excerpt from [Emmanuels Betathon Blog Post](https://dev.to/emmanuelthecoder/making-something-waspy-a-review-of-wasp-571j)
>
## Hackathon How-to
Personally, Ive never organized a hackathon before, and this was Wasps first hackathon as well, so when youre a complete newbie at something, you often look towards others for inspiration. Being admirers of the work and style of Supabase, we drew a lot of inspiration from their “[launch week](https://supabase.com/blog/launch-week-5-hackathon)” approach when preparing for our own Beta launch and hacakthon.
<ImgWithCaption
alt="Wasp Betathon Homepage"
source="img/betathon/betathonpage.png"
caption="Our dedicated hackathon landing page w/ intro video & submission form"
/>
With some good inspiration in hand, we set off to create a simple, easy-going Hackathon experience. We werent certain wed get many participants, so we decided to make the process as open as possible: *two weeks to work on any project using Wasp, alone or in a team of up to 4 people, submitted on our [Betathon Homepage](https://betathon.wasp-lang.dev/) before the deadline*. That was it.
When youre an early-stage startup, you cant offer big cash prizes, so we asked Railway if theyd be interested in sponsoring some prizes, as were big fans of their deployment and hosting platform. Luckily, they agreed (thanks, Railway 🙏🚂). It was also a great match, since we already had the documentation for deploying Wasp apps to Railway on our website, making it an obvious choice for the participants to deploy their Hackathon apps with.
<ImgWithCaption
alt="Keyboard"
source="img/betathon/keyboard.png"
caption="Disclaimer: actual prize keyboard will be cooler and waspier 😎🐝"
/>
On top of that, we decided that a cool grand prize could be a Wasp-colored mechanical keyboard. Nothing fancy, but keyboards are an item a lot of programmers love. We also threw in some Wasp beanies and shirts, and stated that wed spotlight the winners on our platforms and social media accounts.
## Promotion
For the Wasp Beta Launch Week, we were active and publicising Wasp on many platforms. We didnt outright promote the hackathon on those platforms, but we were getting a lot of incoming interest to our Website and Discord, so we made noise about it there. We posted banners on the homepage, and made announcements on Discord and Twitter that directed people to a [Beta Hacakthon homepage](https://betathon.wasp-lang.dev) we created.
The homepage was nice to have as a central spot for all the rules and relevant info. We also added a fun intro video to give the hackathon a more personal touch. I also think the effort put into making an intro video gives participants the feeling that theyre entering into a serious contest and committing to something of substance.
<ImgWithCaption
alt="Hackathon Wasp app repo"
source="img/betathon/github.png"
caption="Wanna host your own Hackathon? Use our template app!"
/>
As an extra bonus, we wrote the Betathon Homepage with Wasp, and put the [source code up on our GitHub](https://github.com/wasp-lang/wasp/tree/main/examples/hackathon). We thought it might inspire people to build with Wasp, using it as a guide while creating their own projects for the hackathon, plus it could be used by others in the future if they want to host their own hackathon. 💻
### The Response
The response overall was small but significant, considering Wasps age. We were also extremely happy with the quality of the engagement. We had thirteen participants register overall, a nice number considering we only started promoting the hackathon on the day that we announced it (this is probably something wed do differently next time)!
We also asked participants for their feedback on participating in the Hackathon, and they were all pleased with the open, straight-forward approach we took, so well most likely be repeating this for future versions. Other good signs were the many comments that participants were eager to take part in our next hackathon, as well as some dedicated new community members, which makes it all the more motivating for us. 💪
---
**A big THANK YOU again to all the participants for their hard work and feedback. Heres to the next one! 🍻**

View File

@ -4,10 +4,12 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
const ImgWithCaption = (props) => { const ImgWithCaption = (props) => {
return ( return (
<div> <div>
<p align="center"> <p align='center'>
<figure> <figure>
<img style={{'width': props.width}} alt={props.alt} src={useBaseUrl(props.source)} /> <img style={{ width: props.width }} alt={props.alt} src={useBaseUrl(props.source)} />
<figcaption class="image-caption">{props.caption}</figcaption> <figcaption class='image-caption' style={{ fontStyle: 'italic', opacity: 0.6, fontSize: '0.9rem' }}>
{props.caption}
</figcaption>
</figure> </figure>
</p> </p>
</div> </div>

View File

@ -0,0 +1,5 @@
:::tip Using an external auth method?
if your app is using an external authentication method(s) supported by Wasp (such as [Google](/docs/language/features#google) or [GitHub](/docs/language/features#github)), make sure to set the necessary environment variables.
:::

View File

@ -3,6 +3,8 @@ title: Deploying
--- ---
import useBaseUrl from '@docusaurus/useBaseUrl'; import useBaseUrl from '@docusaurus/useBaseUrl';
import AddExternalAuthEnvVarsReminder from './_addExternalAuthEnvVarsReminder.md'
:::info :::info
Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly a bit bigger changes in the future. Wasp is in beta, so keep in mind there might be some kinks / bugs, and possibly a bit bigger changes in the future.
If you encounter any issues, reach out to us on [Discord](https://discord.gg/rzdnErX) and we will make sure to help you out! If you encounter any issues, reach out to us on [Discord](https://discord.gg/rzdnErX) and we will make sure to help you out!
@ -43,6 +45,8 @@ Server uses following environment variables, so you need to ensure they are set
- `WASP_WEB_CLIENT_URL` -> The URL of where the frontend app is running (e.g. `https://<app-name>.netlify.app`), which is necessary for CORS. - `WASP_WEB_CLIENT_URL` -> The URL of where the frontend app is running (e.g. `https://<app-name>.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. - `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.
<AddExternalAuthEnvVarsReminder />
### Deploying to Fly.io (free, recommended) ### Deploying to Fly.io (free, recommended)
Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the [`flyctl` CLI](https://fly.io/docs/hands-on/install-flyctl/). Fly.io offers a variety of free services that are perfect for deploying your first Wasp app! You will need a Fly.io account and the [`flyctl` CLI](https://fly.io/docs/hands-on/install-flyctl/).
@ -99,8 +103,14 @@ Next, let's add a few more environment variables:
flyctl secrets set PORT=8080 flyctl secrets set PORT=8080
flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long> flyctl secrets set JWT_SECRET=<random_string_at_least_32_characters_long>
flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed> flyctl secrets set WASP_WEB_CLIENT_URL=<url_of_where_frontend_will_be_deployed>
# If you are using an external auth method (Google or GitHub), make sure to add their vars too!
# flyctl secrets set GOOGLE_CLIENT_ID=<google_client_id>
# flyctl secrets set GOOGLE_CLIENT_SECRET=<google_client_secret>
``` ```
<AddExternalAuthEnvVarsReminder />
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. 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.
If you want to make sure you've added your secrets correctly, run `flyctl secrets list` in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data. If you want to make sure you've added your secrets correctly, run `flyctl secrets list` in the terminal. Note that you will see hashed versions of your secrets to protect your sensitive data.
@ -156,8 +166,13 @@ heroku create <app-name>
Unless you have external Postgres database that you want to use, let's create new database on Heroku and attach it to our app: Unless you have external Postgres database that you want to use, let's create new database on Heroku and attach it to our app:
``` ```
heroku addons:create --app <app-name> heroku-postgresql:hobby-dev heroku addons:create --app <app-name> heroku-postgresql:mini
``` ```
:::caution
Heroku does not offer a free plan anymore and `mini` is their cheapest database instance - it costs $5/mo.
:::
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. 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.
@ -375,6 +390,8 @@ Go to the server instance's `Settings` tab, and click `Generate Domain`. Do the
The Postgres database is already initialized with a domain, so click on the Postgres instance, go to the **Connect** tab and copy the `Postgres Connection URL`. The Postgres database is already initialized with a domain, so click on the Postgres instance, go to the **Connect** tab and copy the `Postgres Connection URL`.
Go back to your `server` instance and navigate to its `Variables` tab. Now add the copied Postgres URL as `DATABASE_URL`, as well as the client's domain as `WASP_WEB_CLIENT_URL`. Go back to your `server` instance and navigate to its `Variables` tab. Now add the copied Postgres URL as `DATABASE_URL`, as well as the client's domain as `WASP_WEB_CLIENT_URL`.
<AddExternalAuthEnvVarsReminder />
Next, copy the server's domain, move over to the client's `Variables` tab and add the generated server domain as a new variable called `REACT_APP_API_URL`. Next, copy the server's domain, move over to the client's `Variables` tab and add the generated server domain as a new variable called `REACT_APP_API_URL`.

View File

@ -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
<img alt="GitHub Applications Screenshot"
src={useBaseUrl('img/integrations-github-1.png')}
width="400px"
/>
- 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

View File

@ -831,12 +831,13 @@ app MyApp {
Entity which represents the user (sometimes also referred to as *Principal*). Entity which represents the user (sometimes also referred to as *Principal*).
#### `externalAuthEntity: entity` (optional) #### `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) #### `methods: dict` (required)
List of authentication methods that Wasp app supports. Currently supported methods are: 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. * `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. * `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) #### `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). 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: - Configuration:
- By default, Wasp expects you to set two environment variables in order to use Google authentication: - By default, Wasp expects you to set two environment variables in order to use Google authentication:
- `GOOGLE_CLIENT_ID` - `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 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: If you require modifications to the above, you can add one or more of the following to your `auth.methods.google` dictionary:
```js ```js
@ -1156,9 +1158,9 @@ If you require modifications to the above, you can add one or more of the follow
export function config() { export function config() {
// ... // ...
return { return {
clientId, // look up from env or elsewhere, clientID, // look up from env or elsewhere,
clientSecret, // 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. - `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 ```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 = () => { const Login = () => {
return ( return (
@ -1200,7 +1202,39 @@ const Login = () => {
export default Login export default Login
``` ```
You can adjust the height of the button by setting a `height` prop (e.g., `<GoogleSignInButton height={25}/>`), 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` ### `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: 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:

View File

@ -48,7 +48,7 @@ app TodoApp {
// Expects entity User to have (username:String) and (password:String) fields. // Expects entity User to have (username:String) and (password:String) fields.
userEntity: User, userEntity: User,
methods: { methods: {
// We also support Google, with more on the way! // We also support Google and GitHub, with more on the way!
usernameAndPassword: {} usernameAndPassword: {}
}, },
// We'll see how this is used a bit later // We'll see how this is used a bit later

View File

@ -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' 'https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'
], ],
themeConfig: { themeConfig: {
/*
announcementBar: { announcementBar: {
id: 'Beta_is_here', id: 'Beta_is_here',
content: 'Wasp Hackathon #1 is underway! 🚀 <a href="https://betathon.wasp-lang.dev/">Join now</a>', content: 'Wasp Hackathon #1 is underway! 🚀 <a href="https://betathon.wasp-lang.dev/">Join now</a>',
@ -26,6 +27,7 @@ module.exports = {
textColor: '#fff', textColor: '#fff',
isCloseable: false, isCloseable: false,
}, },
*/
navbar: { navbar: {
title: '.wasp (beta)', title: '.wasp (beta)',
logo: { logo: {
@ -125,7 +127,7 @@ module.exports = {
'@docusaurus/preset-classic', '@docusaurus/preset-classic',
{ {
gtag: { gtag: {
trackingID: 'G-3ZEDH3BVGE', trackingID: 'GTM-WJX89HZ',
anonymizeIP: true, anonymizeIP: true,
}, },
docs: { docs: {

View File

@ -58,6 +58,7 @@ module.exports = {
label: 'Integrations', label: 'Integrations',
collapsed: false, collapsed: false,
items: [ items: [
'integrations/github',
'integrations/google', 'integrations/google',
'integrations/css-frameworks' 'integrations/css-frameworks'
] ]

View File

@ -0,0 +1,181 @@
import React from 'react'
import Link from '@docusaurus/Link'
import SectionContainer from './Layouts/SectionContainer'
import { GitHub, ArrowUpRight, Monitor } from 'react-feather'
const examples = [
{
title: "Waspello 📝",
description: 'A Trello clone made with Wasp.',
tags: ["Optimistic UI Updates"],
level: "Beginner",
authorName: "wasp",
authorImg: 'https://avatars.githubusercontent.com/u/55102317',
repoName: "waspello-example-app",
repoUrl: "https://github.com/wasp-lang/wasp/tree/main/examples/waspello",
demoUrl: "https://waspello-demo.netlify.app/",
// todo: try in GitPod/Replit url
},
{
title: "Real World App 🐑",
description: 'A Medium clone made with Wasp and Material UI.',
authorName: "wasp",
authorImg: 'https://avatars.githubusercontent.com/u/55102317',
repoName: "real-world-app",
repoUrl: "https://github.com/wasp-lang/wasp/tree/main/examples/realworld",
demoUrl: "https://wasp-rwa.netlify.app/",
},
{
title: "Waspleau 📊",
description: 'A simple data dashboard that makes use of Wasp async jobs feature.',
authorName: "wasp",
authorImg: 'https://avatars.githubusercontent.com/u/55102317',
repoName: "waspleau-jobs-example",
repoUrl: "https://github.com/wasp-lang/wasp/tree/main/examples/waspleau",
demoUrl: "https://waspleau.netlify.app/",
},
]
const SeeTheCodeButton = ({ repoUrl }) => (
<Link to={repoUrl}>
<button
className={`
flex items-center
text-xs
px-2.5 py-1 rounded
bg-transparent border border-yellow-500 text-neutral-500
hover:text-neutral-400
transition ease-out duration-200
`}
>
<span>See the code</span>
<ArrowUpRight className='ml-2' size={14} />
</button>
</Link>
)
const DemoButton = ({ demoUrl }) => (
<Link to={demoUrl}>
<button
className={`
flex items-center
text-xs
px-2.5 py-1 rounded
bg-yellow-500 text-white
hover:bg-yellow-400
transition ease-out duration-200
`}
>
<span>Demo</span>
<Monitor className='ml-2' size={14} />
</button>
</Link>
)
const ExampleCard = (props) => (
<>
{/* Top half */}
<div
className={`
bg-yellow-500/5
border-t border-l border-r border-yellow-500/25
rounded rounded-b-none
flex flex-col
h-40
p-5
`}
>
<div className='mb-4'>
<h4 className='mb-4 text-neutral-700'>{props.title}</h4>
<p className='text-sm mb-4 text-neutral-500'>{props.description}</p>
<div>
<img
className='inline w-6 rounded-full'
src={props.authorImg}
alt={props.authorName + ' GitHub profile picture'}
/>
<span className='ml-2 text-sm text-neutral-700'>{props.authorName}</span>
</div>
</div>
</div>
{/* Bottom half */}
<div
className={`
bg-yellow-500/20
border-b border-l border-r border-yellow-500/25
rounded rounded-t-none
flex flex-col
p-5
`}
>
<Link to={props.repoUrl}>
<span className='flex items-center text-sm text-neutral-500 hover:text-neutral-400'>
<span className=''>{props.repoName}</span>
<span className='ml-1 inline-block'>
<GitHub className='' size={14} />
</span>
</span>
</Link>
{/* Action buttons */}
<div className='mt-3 flex items-center gap-2'>
<SeeTheCodeButton repoUrl={props.repoUrl} />
{/* Demo apps are not mobile-friendly yet so hiding them on mobile for now. */}
<span className='hidden md:block'>
{props.demoUrl && (
<DemoButton demoUrl={props.demoUrl} />
)}
</span>
</div>
</div>
</>
)
const ExampleWaspApps = () => {
return (
<SectionContainer className='space-y-16' id='examples'>
<div className='grid grid-cols-12'>
<div className='col-span-12 text-center'>
<h2 className='text-xl lg:text-2xl text-neutral-700 mb-4'>
Show, don't tell.
</h2>
<p className='text-neutral-500'>
Take a look at examples - see how things work and get inspired for your next project.
</p>
</div>
</div>
<div className='mt-16 grid grid-cols-12 gap-5'>
{examples.slice(0, 6).map((e, idx) => (
<div className='col-span-12 lg:col-span-6 xl:col-span-4'>
<ExampleCard {...e} />
</div>
))}
</div>
<div className='flex justify-center'>
<Link to='https://github.com/wasp-lang/wasp/tree/main/examples'>
<span
className={`
text-neutral-500
underline decoration-2 decoration-yellow-500 font-medium
hover:text-neutral-400
transition ease-out duration-200
flex items-center
`}
>
<span>See all examples</span>
<ArrowUpRight className='ml-1' size={14} />
</span>
</Link>
</div>
</SectionContainer>
)
}
export default ExampleWaspApps

View File

@ -2,7 +2,7 @@ import React from 'react'
import Link from '@docusaurus/Link' import Link from '@docusaurus/Link'
import SyntaxHighlighter from 'react-syntax-highlighter' import SyntaxHighlighter from 'react-syntax-highlighter'
import { qtcreatorLight, atomOneLight, atomOneDark, a11ylight } from 'react-syntax-highlighter/dist/cjs/styles/hljs' import { qtcreatorLight, atomOneLight, atomOneDark, a11ylight } from 'react-syntax-highlighter/dist/cjs/styles/hljs'
import { Terminal } from 'react-feather' import { Terminal, ArrowUpRight } from 'react-feather'
import InstallCmd from './InstallCmd' import InstallCmd from './InstallCmd'
import SectionContainer from './Layouts/SectionContainer' import SectionContainer from './Layouts/SectionContainer'
@ -90,7 +90,7 @@ page MainPage {
/* Only logged in users can access this. */ /* Only logged in users can access this. */
authRequired: true, authRequired: true,
/* import your React code */ /* import your React code */
component: import Main from "@ext/Main.js" component: import Main from "@client/Main.js"
}` }`
return ( return (
@ -145,16 +145,27 @@ page MainPage {
<div className='lg:col-span-6 lg:mt-0 mt-16'> <div className='lg:col-span-6 lg:mt-0 mt-16'>
<div className='relative flex flex-col items-center justify-center'> <div className='relative flex flex-col items-center justify-center'>
{/* Editor header bar */}
<div className='bg-yellow-500/10 flex h-6 w-full items-center justify-between rounded-t-md px-2'> <div className='bg-yellow-500/10 flex h-6 w-full items-center justify-between rounded-t-md px-2'>
<span className='text-sm text-neutral-500'>todoApp.wasp</span> <Link to='https://github.com/wasp-lang/wasp/blob/main/examples/tutorials/TodoApp/main.wasp'>
<span
className={`
text-sm text-neutral-500 flex items-center space-x-1 hover:text-neutral-400
transition ease-out duration-200
`}
>
<span>todoApp.wasp</span>
<ArrowUpRight size={14} />
</span>
</Link>
<div className='flex space-x-2'> <div className='flex space-x-2'>
<div className='bg-yellow-500 h-2 w-2 rounded-full' /> <div className='bg-yellow-500 h-2 w-2 rounded-full' />
<div className='bg-yellow-500 h-2 w-2 rounded-full' /> <div className='bg-yellow-500 h-2 w-2 rounded-full' />
<div className='bg-yellow-500 h-2 w-2 rounded-full' /> <div className='bg-yellow-500 h-2 w-2 rounded-full' />
</div> </div>
</div> </div>
{/* Editor body */}
<div className='w-full text-sm shadow-2xl rounded-b-md'> <div className='w-full text-sm shadow-2xl rounded-b-md'>
<SyntaxHighlighter <SyntaxHighlighter
language="javascript" language="javascript"

View File

@ -119,7 +119,7 @@ const Nav = () => {
return ( return (
<> <>
<Announcement /> {/* <Announcement /> */}
<div className='sticky top-0 z-50'> <div className='sticky top-0 z-50'>
<div className='bg-[#f5f4f0] absolute top-0 h-full w-full opacity-80'></div> <div className='bg-[#f5f4f0] absolute top-0 h-full w-full opacity-80'></div>
<nav className='border-b backdrop-blur-sm'> <nav className='border-b backdrop-blur-sm'>

View File

@ -25,6 +25,14 @@ const testimonials = [
img: 'img/lp/tm/reyero.jpg', img: 'img/lp/tm/reyero.jpg',
source: PH source: PH
}, },
{
text: "The simplification of the main.wasp file is 👍. And it feels like a very light weight version of a few larger frameworks.",
url: 'https://twitter.com/tskaggs/status/1602513968207101954',
name: 'Tim ✌️',
handle: '@tskaggs',
img: 'img/lp/tm/tskaggs.png',
source: TWITTER
},
{ {
text: "@WaspLang has been in the back of my mind for months now. It left an impression, and Im really not easy to impress. Thats gotta mean something… #programming #webdevelopment #FullStack", text: "@WaspLang has been in the back of my mind for months now. It left an impression, and Im really not easy to impress. Thats gotta mean something… #programming #webdevelopment #FullStack",
url: 'https://twitter.com/AttilaTheDev/status/1583530646047117317', url: 'https://twitter.com/AttilaTheDev/status/1583530646047117317',
@ -37,7 +45,7 @@ const testimonials = [
text: "If it weren't for Wasp, my app Amicus would probably have never been finished. I estimate it saved me 100+ hours from the start and I'm still amazed that I did all this work as a team-of-one. Being able to quickly change existing features and add the new ones is the biggest advantage of Wasp for me. Wasp is my favorite producthunt find!", text: "If it weren't for Wasp, my app Amicus would probably have never been finished. I estimate it saved me 100+ hours from the start and I'm still amazed that I did all this work as a team-of-one. Being able to quickly change existing features and add the new ones is the biggest advantage of Wasp for me. Wasp is my favorite producthunt find!",
url: 'https://www.producthunt.com/posts/wasp-lang-beta?comment=2048472', url: 'https://www.producthunt.com/posts/wasp-lang-beta?comment=2048472',
name: 'Erlis Kllogjri', name: 'Erlis Kllogjri',
handle: 'erlis_kllogjri', handle: '@erlis_kllogjri',
img: 'img/lp/tm/erlis.jpg', img: 'img/lp/tm/erlis.jpg',
source: PH source: PH
}, },

View File

@ -1,62 +1,74 @@
import React from 'react' import React from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import Head from '@docusaurus/Head'
import Nav from '../components/Nav/index' import Nav from '../components/Nav/index'
import Hero from '../components/Hero' import Hero from '../components/Hero'
import Benefits from '../components/Benefits' import Benefits from '../components/Benefits'
import Testimonials from '../components/Testimonials' import Testimonials from '../components/Testimonials'
import ExampleWaspApps from '../components/ExampleWaspApps'
import HowItWorks from '../components/HowItWorks' import HowItWorks from '../components/HowItWorks'
import ShowcaseGallery from '../components/ShowcaseGallery' import ShowcaseGallery from '../components/ShowcaseGallery'
import Newsletter from '../components/Newsletter' import Newsletter from '../components/Newsletter'
import Faq from '../components/Faq' import Faq from '../components/Faq'
import Footer from '../components/Footer' import Footer from '../components/Footer'
import waspLogo from '../../static/img/wasp-logo.png'
import styles from './styles.module.css' import styles from './styles.module.css'
import './index.css' import './index.css'
import './preflight.css' import './preflight.css'
const Background = () => { const Background = () => {
return ( return (
<div className='absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none'> <div className='absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none'>
<span className={classNames(styles.leftLights, "opacity-100")} /> <span className={classNames(styles.leftLights, 'opacity-100')} />
</div> </div>
) );
} };
const LightsTwo = () => ( const LightsTwo = () => (
<div className='absolute top-[1800px] lg:top-[1000px] left-0 w-full h-full overflow-hidden pointer-events-none'> <div className='absolute top-[1800px] lg:top-[1000px] left-0 w-full h-full overflow-hidden pointer-events-none'>
<span className={classNames(styles.lightsTwo, "opacity-100")} /> <span className={classNames(styles.lightsTwo, 'opacity-100')} />
</div> </div>
) );
const Index = () => { const Index = () => {
return ( return (
<div className='twLandingPage'> <div className='twLandingPage'>
<Head>
{/* opengraph / facebook */}
<meta property='og:type' content='website' />
<meta property='og:url' content='https://wasp-lang.dev/' />
<meta property='og:description' content='Develop full-stack web apps without boilerplate.' />
<meta property='og:image' content={waspLogo} />
{/* twitter */}
<meta property='twitter:card' content='summary_large_image' />
<meta property='twitter:url' content='https://wasp-lang.dev/' />
<meta property='twitter:title' content='Develop full-stack web apps without boilerplate.' />
<meta property='twitter:image' content={waspLogo} />
</Head>
<Nav /> <Nav />
<div className='min-h-screen'> <div className='min-h-screen'>
<main> <main>
<Background /> <Background />
<div> {/* container */} <div>
{/* container */}
<Hero /> <Hero />
<Benefits /> <Benefits />
<Testimonials /> <Testimonials />
<ExampleWaspApps />
<LightsTwo /> <LightsTwo />
<HowItWorks /> <HowItWorks />
<ShowcaseGallery /> <ShowcaseGallery />
<Newsletter /> <Newsletter />
<Faq /> <Faq />
</div>
</div> {/* eof container */} {/* eof container */}
</main> </main>
</div> </div>
<Footer /> <Footer />
</div> </div>
);
};
) export default Index;
}
export default Index

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB