Merge branch 'release' into vince-add-todo-ts-example
29
.github/workflows/ci.yaml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
@ -40,7 +40,14 @@ jobs:
|
||||
cabal:
|
||||
- "3.6.2.0"
|
||||
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
|
||||
- windows-latest
|
||||
|
||||
@ -55,11 +62,11 @@ jobs:
|
||||
git config --global core.eol lf
|
||||
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Haskell
|
||||
id: setup-haskell-cabal
|
||||
uses: haskell/actions/setup@v1
|
||||
uses: haskell/actions/setup@v2
|
||||
with:
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
cabal-version: ${{ matrix.cabal }}
|
||||
@ -70,7 +77,7 @@ jobs:
|
||||
cabal --version
|
||||
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }}
|
||||
# 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,
|
||||
# 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.
|
||||
key: wasp-build-${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('waspc/waspc.cabal') }}-${{ hashFiles('waspc/cabal.project') }}-${{ github.run_id }}
|
||||
restore-keys: wasp-build-${{ runner.os }}-${{ matrix.ghc }}-
|
||||
key: wasp-build-${{ matrix.os }}-${{ matrix.ghc }}-${{ hashFiles('waspc/waspc.cabal') }}-${{ hashFiles('waspc/cabal.project') }}-${{ github.run_id }}
|
||||
restore-keys: wasp-build-${{ matrix.os }}-${{ matrix.ghc }}-
|
||||
|
||||
- name: Check Haskell code formatting
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
if: matrix.os == 'ubuntu-20.04'
|
||||
run: ./run ormolu:check
|
||||
|
||||
- name: Build external dependencies
|
||||
@ -100,7 +107,7 @@ jobs:
|
||||
run: cabal build all
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
@ -112,7 +119,7 @@ jobs:
|
||||
run: cabal test
|
||||
|
||||
- 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: |
|
||||
OS_NAME=`case "${{ runner.os }}" in Linux) echo "linux";; macOS) echo "macos";; *) exit 1;; esac`
|
||||
mkdir artifacts
|
||||
@ -120,7 +127,7 @@ jobs:
|
||||
|
||||
- name: Create Github release
|
||||
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:
|
||||
draft: true
|
||||
allowUpdates: true
|
||||
|
@ -1,6 +1,6 @@
|
||||
app hackathonBetaSubmissions {
|
||||
wasp: {
|
||||
version: "^0.7.0"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
db: {
|
||||
system: PostgreSQL
|
||||
|
@ -1,6 +1,6 @@
|
||||
app Conduit {
|
||||
wasp: {
|
||||
version: "^0.7.0"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
|
||||
title: "Conduit",
|
||||
|
@ -1,6 +1,6 @@
|
||||
app Thoughts {
|
||||
wasp: {
|
||||
version: "^0.7.0"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
title: "Thoughts",
|
||||
db: { system: PostgreSQL },
|
||||
|
@ -1,6 +1,6 @@
|
||||
app ItWaspsOnMyMachine {
|
||||
wasp: {
|
||||
version: "^0.7.0"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
|
||||
title: "It Wasps On My Machine",
|
||||
|
@ -1,6 +1,6 @@
|
||||
app TodoApp {
|
||||
wasp: {
|
||||
version: "^0.7.0"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
|
||||
title: "Todo app",
|
||||
|
@ -1,6 +1,6 @@
|
||||
app trello {
|
||||
wasp: {
|
||||
version: "^0.7.0"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
|
||||
title: "trello",
|
||||
|
@ -1,6 +1,6 @@
|
||||
app waspleau {
|
||||
wasp: {
|
||||
version: "^0.7.0"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
|
||||
title: "Waspleau",
|
||||
|
@ -1,5 +1,17 @@
|
||||
# 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
|
||||
|
||||
### MINOR CLI BREAKING CHANGE
|
||||
|
Before Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 16 KiB |
@ -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>
|
||||
)
|
||||
}
|
39
waspc/data/Generator/templates/react-app/src/auth/helpers/Generic.js
vendored
Normal 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>
|
||||
)
|
||||
}
|
@ -22,13 +22,13 @@ const router = (
|
||||
{=/ routes =}
|
||||
|
||||
{=# isExternalAuthEnabled =}
|
||||
|
||||
{=# isGoogleAuthEnabled =}
|
||||
<Route exact path="/auth/login/google">
|
||||
<OAuthCodeExchange pathToApiServerRouteHandlingOauthRedirect="/auth/external/google/validateCodeForLogin" />
|
||||
{=# externalAuthProviders =}
|
||||
{=# authProviderEnabled =}
|
||||
<Route exact path="{= authFrontendUrl =}">
|
||||
<OAuthCodeExchange pathToApiServerRouteHandlingOauthRedirect="{= authServerOauthRedirectUrl =}" />
|
||||
</Route>
|
||||
{=/ isGoogleAuthEnabled =}
|
||||
|
||||
{=/ authProviderEnabled =}
|
||||
{=/ externalAuthProviders =}
|
||||
{=/ isExternalAuthEnabled =}
|
||||
</div>
|
||||
</Router>
|
||||
|
@ -5,13 +5,13 @@
|
||||
export { {= configFnIdentifier =} as configFn }
|
||||
{=/ doesConfigFnExist =}
|
||||
{=^ doesConfigFnExist =}
|
||||
export { configFn } from './googleDefaults.js'
|
||||
export { configFn } from './defaults.js'
|
||||
{=/ doesConfigFnExist =}
|
||||
|
||||
{=# doesOnSignInFnExist =}
|
||||
{=# doesGetUserFieldsFnExist =}
|
||||
{=& getUserFieldsFnImportStatement =}
|
||||
export { {= getUserFieldsFnIdentifier =} as getUserFieldsFn }
|
||||
{=/ doesOnSignInFnExist =}
|
||||
{=^ doesOnSignInFnExist =}
|
||||
export { getUserFieldsFn } from './googleDefaults.js'
|
||||
{=/ doesOnSignInFnExist =}
|
||||
{=/ doesGetUserFieldsFnExist =}
|
||||
{=^ doesGetUserFieldsFnExist =}
|
||||
export { getUserFieldsFn } from './defaults.js'
|
||||
{=/ doesGetUserFieldsFnExist =}
|
@ -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
|
||||
}
|
@ -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())
|
@ -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 }
|
||||
}
|
@ -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())
|
@ -2,10 +2,10 @@ import { generateAvailableDictionaryUsername } from '../../../../core/auth.js'
|
||||
|
||||
// Default implementation if there is no `auth.methods.google.configFn`.
|
||||
export function configFn() {
|
||||
const clientId = process.env['GOOGLE_CLIENT_ID']
|
||||
const clientID = process.env['GOOGLE_CLIENT_ID']
|
||||
const clientSecret = process.env['GOOGLE_CLIENT_SECRET']
|
||||
|
||||
if (!clientId) {
|
||||
if (!clientID) {
|
||||
throw new Error("Missing GOOGLE_CLIENT_ID environment variable.")
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ export function configFn() {
|
||||
throw new Error("Missing GOOGLE_CLIENT_SECRET environment variable.")
|
||||
}
|
||||
|
||||
return { clientId, clientSecret, scope: ['profile'] }
|
||||
return { clientID, clientSecret, scope: ['profile'] }
|
||||
}
|
||||
|
||||
// Default implementation if there is no `auth.methods.google.getUserFieldsFn`.
|
@ -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
|
@ -1,14 +1,24 @@
|
||||
{{={= =}=}}
|
||||
import express from 'express'
|
||||
import { initRouter } from './generic/provider.js'
|
||||
|
||||
{=# isGoogleAuthEnabled =}
|
||||
import googleAuth from './google/google.js'
|
||||
{=/ isGoogleAuthEnabled =}
|
||||
const providerMap = new Map();
|
||||
{=# providers =}
|
||||
{=# isEnabled =}
|
||||
providerMap.set('{= slug =}', { npmPackage: '{= npmPackage =}', passportImportPath: '{= passportImportPath =}' })
|
||||
{=/ isEnabled =}
|
||||
{=/ providers =}
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
{=# isGoogleAuthEnabled =}
|
||||
router.use('/google', googleAuth)
|
||||
{=/ isGoogleAuthEnabled =}
|
||||
async function initProviders(providers) {
|
||||
for (let [providerSlug, { npmPackage, passportImportPath }] of providers) {
|
||||
const { config, getUserFieldsFn } = await import(passportImportPath)
|
||||
const ProviderStrategy = await import(npmPackage)
|
||||
router.use(`/${providerSlug}`, initRouter(providerSlug, ProviderStrategy.default, config, getUserFieldsFn))
|
||||
}
|
||||
}
|
||||
|
||||
await initProviders(providerMap)
|
||||
|
||||
export default router
|
||||
|
@ -1,7 +1,7 @@
|
||||
app waspBuild {
|
||||
db: { system: PostgreSQL },
|
||||
wasp: {
|
||||
version: "^0.7.3"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
title: "waspBuild"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
app waspCompile {
|
||||
wasp: {
|
||||
version: "^0.7.3"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
title: "waspCompile"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
app waspJob {
|
||||
db: { system: PostgreSQL },
|
||||
wasp: {
|
||||
version: "^0.7.3"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
title: "waspJob"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
app waspMigrate {
|
||||
wasp: {
|
||||
version: "^0.7.3"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
title: "waspMigrate"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
app waspNew {
|
||||
wasp: {
|
||||
version: "^0.7.3"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
title: "waspNew"
|
||||
}
|
||||
|
@ -2,7 +2,8 @@ import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import LoginForm from '@wasp/auth/forms/Login'
|
||||
// import { GoogleSignInButton } from '@wasp/auth/buttons/Google'
|
||||
// import { SignInButton as GoogleSignInButton } from '@wasp/auth/helpers/Google'
|
||||
// import { SignInButton as GitHubSignInButton } from '@wasp/auth/helpers/GitHub'
|
||||
|
||||
const Login = () => {
|
||||
return (
|
||||
@ -15,6 +16,7 @@ const Login = () => {
|
||||
|
||||
{/* <div>
|
||||
<GoogleSignInButton/>
|
||||
<GitHubSignInButton/>
|
||||
</div> */}
|
||||
</>
|
||||
)
|
||||
|
16
waspc/examples/todoApp/src/server/auth/github.js
Normal 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 }
|
||||
}
|
@ -3,7 +3,7 @@ import { generateAvailableUsername } from '@wasp/core/auth.js'
|
||||
export function config() {
|
||||
console.log("Inside user-supplied Google config")
|
||||
return {
|
||||
clientId: process.env['GOOGLE_CLIENT_ID'],
|
||||
clientID: process.env['GOOGLE_CLIENT_ID'],
|
||||
clientSecret: process.env['GOOGLE_CLIENT_SECRET'],
|
||||
scope: ['profile']
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
app todoApp {
|
||||
wasp: {
|
||||
version: "^0.7.0"
|
||||
version: "^0.8.0"
|
||||
},
|
||||
title: "ToDo App",
|
||||
// head: [],
|
||||
@ -16,6 +16,10 @@ app todoApp {
|
||||
// google: {
|
||||
// configFn: import { config } from "@server/auth/google.js",
|
||||
// getUserFieldsFn: import { getUserFields } from "@server/auth/google.js"
|
||||
// },
|
||||
// gitHub: {
|
||||
// configFn: import { config } from "@server/auth/github.js",
|
||||
// getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
|
||||
// }
|
||||
},
|
||||
onAuthFailedRedirectTo: "/login",
|
||||
|
@ -4,11 +4,12 @@
|
||||
module Wasp.AppSpec.App.Auth
|
||||
( Auth (..),
|
||||
AuthMethods (..),
|
||||
GoogleConfig (..),
|
||||
ExternalAuthConfig (..),
|
||||
usernameAndPasswordConfig,
|
||||
isUsernameAndPasswordAuthEnabled,
|
||||
isGoogleAuthEnabled,
|
||||
isExternalAuthEnabled,
|
||||
isGoogleAuthEnabled,
|
||||
isGitHubAuthEnabled,
|
||||
)
|
||||
where
|
||||
|
||||
@ -29,7 +30,8 @@ data Auth = Auth
|
||||
|
||||
data AuthMethods = AuthMethods
|
||||
{ usernameAndPassword :: Maybe UsernameAndPasswordConfig,
|
||||
google :: Maybe GoogleConfig
|
||||
google :: Maybe ExternalAuthConfig,
|
||||
gitHub :: Maybe ExternalAuthConfig
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
||||
|
||||
@ -39,7 +41,7 @@ data UsernameAndPasswordConfig = UsernameAndPasswordConfig
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
||||
|
||||
data GoogleConfig = GoogleConfig
|
||||
data ExternalAuthConfig = ExternalAuthConfig
|
||||
{ configFn :: Maybe ExtImport,
|
||||
getUserFieldsFn :: Maybe ExtImport
|
||||
}
|
||||
@ -51,8 +53,11 @@ usernameAndPasswordConfig = UsernameAndPasswordConfig Nothing
|
||||
isUsernameAndPasswordAuthEnabled :: Auth -> Bool
|
||||
isUsernameAndPasswordAuthEnabled = isJust . usernameAndPassword . methods
|
||||
|
||||
isExternalAuthEnabled :: Auth -> Bool
|
||||
isExternalAuthEnabled auth = any ($ auth) [isGoogleAuthEnabled, isGitHubAuthEnabled]
|
||||
|
||||
isGoogleAuthEnabled :: Auth -> Bool
|
||||
isGoogleAuthEnabled = isJust . google . methods
|
||||
|
||||
isExternalAuthEnabled :: Auth -> Bool
|
||||
isExternalAuthEnabled auth = any ($ auth) [isGoogleAuthEnabled]
|
||||
isGitHubAuthEnabled :: Auth -> Bool
|
||||
isGitHubAuthEnabled = isJust . gitHub . methods
|
||||
|
@ -32,7 +32,6 @@ import qualified Wasp.AppSpec as AS
|
||||
import qualified Wasp.AppSpec.App as AS.App
|
||||
import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
|
||||
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
|
||||
import qualified Wasp.AppSpec.App.Dependency as App.Dependency
|
||||
import qualified Wasp.AppSpec.App.Server as AS.App.Server
|
||||
import qualified Wasp.AppSpec.Entity as AS.Entity
|
||||
import Wasp.AppSpec.Util (isPgBossJobExecutorUsed)
|
||||
@ -47,6 +46,7 @@ import qualified Wasp.Generator.NpmDependencies as N
|
||||
import Wasp.Generator.ServerGenerator.AuthG (genAuth)
|
||||
import qualified Wasp.Generator.ServerGenerator.Common as C
|
||||
import Wasp.Generator.ServerGenerator.ConfigG (genConfigFile)
|
||||
import Wasp.Generator.ServerGenerator.ExternalAuthG (depsRequiredByPassport)
|
||||
import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir, extServerCodeGeneratorStrategy, extSharedCodeGeneratorStrategy)
|
||||
import Wasp.Generator.ServerGenerator.JobGenerator (depsRequiredByJobs, genJobExecutors, genJobs)
|
||||
import Wasp.Generator.ServerGenerator.OperationsG (genOperations)
|
||||
@ -239,16 +239,6 @@ genRoutesDir spec =
|
||||
operationsRouteInRootRouter :: String
|
||||
operationsRouteInRootRouter = "operations"
|
||||
|
||||
depsRequiredByPassport :: AppSpec -> [App.Dependency.Dependency]
|
||||
depsRequiredByPassport spec =
|
||||
AS.Dependency.fromList $
|
||||
concat
|
||||
[ [("passport", "0.6.0") | (AS.App.Auth.isExternalAuthEnabled <$> maybeAuth) == Just True],
|
||||
[("passport-google-oauth20", "2.0.0") | (AS.App.Auth.isGoogleAuthEnabled <$> maybeAuth) == Just True]
|
||||
]
|
||||
where
|
||||
maybeAuth = AS.App.auth $ snd $ getApp spec
|
||||
|
||||
areServerPatchesUsed :: AppSpec -> Generator Bool
|
||||
areServerPatchesUsed spec = not . null <$> genPatches spec
|
||||
|
||||
|
@ -4,16 +4,12 @@ module Wasp.Generator.ServerGenerator.AuthG
|
||||
where
|
||||
|
||||
import Data.Aeson (object, (.=))
|
||||
import Data.Maybe (fromJust, fromMaybe, isJust)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import StrongPath
|
||||
( Dir,
|
||||
File',
|
||||
Path,
|
||||
( File',
|
||||
Path',
|
||||
Posix,
|
||||
Rel,
|
||||
reldir,
|
||||
reldirP,
|
||||
relfile,
|
||||
(</>),
|
||||
)
|
||||
@ -23,12 +19,10 @@ import qualified Wasp.AppSpec as AS
|
||||
import qualified Wasp.AppSpec.App as AS.App
|
||||
import qualified Wasp.AppSpec.App.Auth as AS.Auth
|
||||
import Wasp.AppSpec.Valid (getApp)
|
||||
import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
|
||||
import Wasp.Generator.FileDraft (FileDraft)
|
||||
import Wasp.Generator.JsImport (getJsImportDetailsForExtFnImport)
|
||||
import Wasp.Generator.Monad (Generator)
|
||||
import qualified Wasp.Generator.ServerGenerator.Common as C
|
||||
import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir)
|
||||
import Wasp.Generator.ServerGenerator.ExternalAuthG (genPassportAuth)
|
||||
import Wasp.Util ((<++>))
|
||||
import qualified Wasp.Util as Util
|
||||
|
||||
@ -135,24 +129,6 @@ genMeRoute auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplD
|
||||
[ "userEntityLower" .= (Util.toLowerFirst (AS.refName $ AS.Auth.userEntity auth) :: String)
|
||||
]
|
||||
|
||||
genPassportAuth :: AS.Auth.Auth -> Generator [FileDraft]
|
||||
genPassportAuth auth
|
||||
| AS.Auth.isExternalAuthEnabled auth = (:) <$> genPassportJs auth <*> genGoogleAuth auth
|
||||
| otherwise = return []
|
||||
|
||||
genPassportJs :: AS.Auth.Auth -> Generator FileDraft
|
||||
genPassportJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
|
||||
where
|
||||
tmplFile = C.srcDirInServerTemplatesDir </> SP.castRel passportFileInSrcDir
|
||||
dstFile = C.serverSrcDirInServerRootDir </> passportFileInSrcDir
|
||||
tmplData =
|
||||
object
|
||||
[ "isGoogleAuthEnabled" .= AS.Auth.isGoogleAuthEnabled auth
|
||||
]
|
||||
|
||||
passportFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
|
||||
passportFileInSrcDir = [relfile|routes/auth/passport/passport.js|]
|
||||
|
||||
genUtilsJs :: AS.Auth.Auth -> Generator FileDraft
|
||||
genUtilsJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
|
||||
where
|
||||
@ -172,46 +148,5 @@ genUtilsJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplD
|
||||
utilsFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
|
||||
utilsFileInSrcDir = [relfile|routes/auth/utils.js|]
|
||||
|
||||
genGoogleAuth :: AS.Auth.Auth -> Generator [FileDraft]
|
||||
genGoogleAuth auth
|
||||
| AS.Auth.isGoogleAuthEnabled auth =
|
||||
sequence
|
||||
[ copyTmplFile [relfile|routes/auth/passport/google/google.js|],
|
||||
copyTmplFile [relfile|routes/auth/passport/google/googleDefaults.js|],
|
||||
genGoogleConfigJs auth
|
||||
]
|
||||
| otherwise = return []
|
||||
where
|
||||
copyTmplFile = return . C.mkSrcTmplFd
|
||||
|
||||
genGoogleConfigJs :: AS.Auth.Auth -> Generator FileDraft
|
||||
genGoogleConfigJs auth = return $ C.mkTmplFdWithDstAndData tmplFile dstFile (Just tmplData)
|
||||
where
|
||||
tmplFile = C.srcDirInServerTemplatesDir </> SP.castRel googleConfigFileInSrcDir
|
||||
dstFile = C.serverSrcDirInServerRootDir </> googleConfigFileInSrcDir
|
||||
tmplData =
|
||||
object
|
||||
[ "doesConfigFnExist" .= isJust maybeConfigFn,
|
||||
"configFnImportStatement" .= fromMaybe "" maybeConfigFnImportStmt,
|
||||
"configFnIdentifier" .= fromMaybe "" maybeConfigFnImportIdentifier,
|
||||
"doesOnSignInFnExist" .= isJust maybeGetUserFieldsFn,
|
||||
"getUserFieldsFnImportStatement" .= fromMaybe "" maybeOnSignInFnImportStmt,
|
||||
"getUserFieldsFnIdentifier" .= fromMaybe "" maybeOnSignInFnImportIdentifier
|
||||
]
|
||||
|
||||
googleConfigFileInSrcDir :: Path' (Rel C.ServerSrcDir) File'
|
||||
googleConfigFileInSrcDir = [relfile|routes/auth/passport/google/googleConfig.js|]
|
||||
|
||||
maybeConfigFn = AS.Auth.configFn =<< AS.Auth.google (AS.Auth.methods auth)
|
||||
maybeConfigFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromGoogleAuthDirToExtSrcDir <$> maybeConfigFn
|
||||
(maybeConfigFnImportIdentifier, maybeConfigFnImportStmt) = (fst <$> maybeConfigFnImportDetails, snd <$> maybeConfigFnImportDetails)
|
||||
|
||||
maybeGetUserFieldsFn = AS.Auth.getUserFieldsFn =<< AS.Auth.google (AS.Auth.methods auth)
|
||||
maybeOnSignInFnImportDetails = getJsImportDetailsForExtFnImport relPosixPathFromGoogleAuthDirToExtSrcDir <$> maybeGetUserFieldsFn
|
||||
(maybeOnSignInFnImportIdentifier, maybeOnSignInFnImportStmt) = (fst <$> maybeOnSignInFnImportDetails, snd <$> maybeOnSignInFnImportDetails)
|
||||
|
||||
relPosixPathFromGoogleAuthDirToExtSrcDir :: Path Posix (Rel (Dir C.ServerSrcDir)) (Dir GeneratedExternalCodeDir)
|
||||
relPosixPathFromGoogleAuthDirToExtSrcDir = [reldirP|../../../../|] </> fromJust (SP.relDirToPosix extServerCodeDirInServerSrcDir)
|
||||
|
||||
getOnAuthSucceededRedirectToOrDefault :: AS.Auth.Auth -> String
|
||||
getOnAuthSucceededRedirectToOrDefault auth = fromMaybe "/" (AS.Auth.onAuthSucceededRedirectTo auth)
|
||||
|
163
waspc/src/Wasp/Generator/ServerGenerator/ExternalAuthG.hs
Normal 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")
|
@ -15,6 +15,7 @@ import StrongPath
|
||||
Posix,
|
||||
Rel,
|
||||
relDirToPosix,
|
||||
reldir,
|
||||
relfile,
|
||||
(</>),
|
||||
)
|
||||
@ -35,7 +36,12 @@ import Wasp.Generator.Monad (Generator)
|
||||
import qualified Wasp.Generator.NpmDependencies as N
|
||||
import Wasp.Generator.WebAppGenerator.AuthG (genAuth)
|
||||
import qualified Wasp.Generator.WebAppGenerator.Common as C
|
||||
import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator (extClientCodeDirInWebAppSrcDir, extClientCodeGeneratorStrategy, extSharedCodeGeneratorStrategy)
|
||||
import Wasp.Generator.WebAppGenerator.ExternalAuthG (ExternalAuthInfo (..), gitHubAuthInfo, googleAuthInfo)
|
||||
import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator
|
||||
( extClientCodeDirInWebAppSrcDir,
|
||||
extClientCodeGeneratorStrategy,
|
||||
extSharedCodeGeneratorStrategy,
|
||||
)
|
||||
import Wasp.Generator.WebAppGenerator.OperationsGenerator (genOperations)
|
||||
import Wasp.Generator.WebAppGenerator.RouterGenerator (genRouter)
|
||||
import Wasp.Util ((<++>))
|
||||
@ -147,24 +153,33 @@ genGitignore =
|
||||
genPublicDir :: AppSpec -> Generator [FileDraft]
|
||||
genPublicDir spec = do
|
||||
publicIndexHtmlFd <- genPublicIndexHtml spec
|
||||
return $
|
||||
return
|
||||
[ publicIndexHtmlFd,
|
||||
genFaviconFd,
|
||||
genManifestFd
|
||||
]
|
||||
++ genGoogleSigninImage
|
||||
<++> genSocialLoginIcons maybeAuth
|
||||
where
|
||||
maybeAuth = AS.App.auth $ snd $ getApp spec
|
||||
genFaviconFd = C.mkTmplFd (C.asTmplFile [relfile|public/favicon.ico|])
|
||||
genGoogleSigninImage =
|
||||
[ C.mkTmplFd (C.asTmplFile [relfile|public/images/btn_google_signin_dark_normal_web@2x.png|])
|
||||
| (AS.App.Auth.isGoogleAuthEnabled <$> maybeAuth) == Just True
|
||||
]
|
||||
genManifestFd =
|
||||
let tmplData = object ["appName" .= (fst (getApp spec) :: String)]
|
||||
tmplFile = C.asTmplFile [relfile|public/manifest.json|]
|
||||
in C.mkTmplFdWithData tmplFile tmplData
|
||||
|
||||
genSocialLoginIcons :: Maybe AS.App.Auth.Auth -> Generator [FileDraft]
|
||||
genSocialLoginIcons maybeAuth =
|
||||
return $
|
||||
[ C.mkTmplFd (C.asTmplFile fp)
|
||||
| (isEnabled, fp) <- socialIcons,
|
||||
(isEnabled <$> maybeAuth) == Just True
|
||||
]
|
||||
where
|
||||
socialIcons =
|
||||
[ (AS.App.Auth.isGoogleAuthEnabled, [reldir|public/images|] </> _logoFileName googleAuthInfo),
|
||||
(AS.App.Auth.isGitHubAuthEnabled, [reldir|public/images|] </> _logoFileName gitHubAuthInfo)
|
||||
]
|
||||
|
||||
genPublicIndexHtml :: AppSpec -> Generator FileDraft
|
||||
genPublicIndexHtml spec =
|
||||
return $
|
||||
|
@ -7,6 +7,7 @@ import Data.Aeson (object, (.=))
|
||||
import Data.Aeson.Types (Pair)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import StrongPath (File', Path', Rel', reldir, relfile, (</>))
|
||||
import qualified StrongPath as SP
|
||||
import Wasp.AppSpec (AppSpec)
|
||||
import qualified Wasp.AppSpec.App as AS.App
|
||||
import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
|
||||
@ -15,6 +16,8 @@ import Wasp.AppSpec.Valid (getApp)
|
||||
import Wasp.Generator.FileDraft (FileDraft)
|
||||
import Wasp.Generator.Monad (Generator)
|
||||
import Wasp.Generator.WebAppGenerator.Common as C
|
||||
import Wasp.Generator.WebAppGenerator.ExternalAuthG (ExternalAuthInfo, gitHubAuthInfo, googleAuthInfo)
|
||||
import qualified Wasp.Generator.WebAppGenerator.ExternalAuthG as ExternalAuthG
|
||||
import Wasp.Util ((<++>))
|
||||
|
||||
genAuth :: AppSpec -> Generator [FileDraft]
|
||||
@ -80,12 +83,33 @@ genSignupForm auth =
|
||||
|
||||
genExternalAuth :: AS.Auth.Auth -> Generator [FileDraft]
|
||||
genExternalAuth auth
|
||||
| AS.App.Auth.isExternalAuthEnabled auth = (:) <$> genOAuthCodeExchange auth <*> genSocialLoginButtons auth
|
||||
| AS.App.Auth.isExternalAuthEnabled auth = (:) <$> genOAuthCodeExchange auth <*> genSocialLoginHelpers auth
|
||||
| otherwise = return []
|
||||
|
||||
genSocialLoginButtons :: AS.Auth.Auth -> Generator [FileDraft]
|
||||
genSocialLoginButtons auth =
|
||||
return [C.mkTmplFd (C.asTmplFile [relfile|src/auth/buttons/Google.js|]) | AS.App.Auth.isGoogleAuthEnabled auth]
|
||||
genSocialLoginHelpers :: AS.Auth.Auth -> Generator [FileDraft]
|
||||
genSocialLoginHelpers auth =
|
||||
return $
|
||||
concat
|
||||
[ [gitHubHelpers | AS.App.Auth.isGitHubAuthEnabled auth],
|
||||
[googleHelpers | AS.App.Auth.isGoogleAuthEnabled auth]
|
||||
]
|
||||
where
|
||||
gitHubHelpers = mkHelpersFd gitHubAuthInfo [relfile|GitHub.js|]
|
||||
googleHelpers = mkHelpersFd googleAuthInfo [relfile|Google.js|]
|
||||
|
||||
mkHelpersFd :: ExternalAuthInfo -> Path' Rel' File' -> FileDraft
|
||||
mkHelpersFd externalAuthInfo helpersFp =
|
||||
mkTmplFdWithDstAndData
|
||||
[relfile|src/auth/helpers/Generic.js|]
|
||||
(SP.castRel $ [reldir|src/auth/helpers|] SP.</> helpersFp)
|
||||
(Just tmplData)
|
||||
where
|
||||
tmplData =
|
||||
object
|
||||
[ "signInPath" .= ExternalAuthG.serverLoginUrl externalAuthInfo,
|
||||
"iconName" .= SP.toFilePath (ExternalAuthG._logoFileName externalAuthInfo),
|
||||
"displayName" .= ExternalAuthG._displayName externalAuthInfo
|
||||
]
|
||||
|
||||
genOAuthCodeExchange :: AS.Auth.Auth -> Generator FileDraft
|
||||
genOAuthCodeExchange auth =
|
||||
|
53
waspc/src/Wasp/Generator/WebAppGenerator/ExternalAuthG.hs
Normal 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)
|
@ -23,6 +23,7 @@ import Wasp.Generator.FileDraft (FileDraft)
|
||||
import Wasp.Generator.Monad (Generator)
|
||||
import Wasp.Generator.WebAppGenerator.Common (asTmplFile, asWebAppSrcFile)
|
||||
import qualified Wasp.Generator.WebAppGenerator.Common as C
|
||||
import Wasp.Generator.WebAppGenerator.ExternalAuthG (ExternalAuthInfo (..), frontendLoginUrl, gitHubAuthInfo, googleAuthInfo, serverOauthRedirectHandlerUrl)
|
||||
import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator (extClientCodeDirInWebAppSrcDir)
|
||||
|
||||
data RouterTemplateData = RouterTemplateData
|
||||
@ -30,7 +31,7 @@ data RouterTemplateData = RouterTemplateData
|
||||
_pagesToImport :: ![PageTemplateData],
|
||||
_isAuthEnabled :: Bool,
|
||||
_isExternalAuthEnabled :: Bool,
|
||||
_isGoogleAuthEnabled :: Bool
|
||||
_externalAuthProviders :: ![ExternalAuthProviderTemplateData]
|
||||
}
|
||||
|
||||
instance ToJSON RouterTemplateData where
|
||||
@ -40,7 +41,7 @@ instance ToJSON RouterTemplateData where
|
||||
"pagesToImport" .= _pagesToImport routerTD,
|
||||
"isAuthEnabled" .= _isAuthEnabled routerTD,
|
||||
"isExternalAuthEnabled" .= _isExternalAuthEnabled routerTD,
|
||||
"isGoogleAuthEnabled" .= _isGoogleAuthEnabled routerTD
|
||||
"externalAuthProviders" .= _externalAuthProviders routerTD
|
||||
]
|
||||
|
||||
data RouteTemplateData = RouteTemplateData
|
||||
@ -68,6 +69,21 @@ instance ToJSON PageTemplateData where
|
||||
"importFrom" .= _importFrom pageTD
|
||||
]
|
||||
|
||||
data ExternalAuthProviderTemplateData = ExternalAuthProviderTemplateData
|
||||
{ _authFrontendUrl :: !String,
|
||||
_authServerOauthRedirectUrl :: !String,
|
||||
_authProviderEnabled :: Bool
|
||||
}
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance ToJSON ExternalAuthProviderTemplateData where
|
||||
toJSON externalProviderTD =
|
||||
object
|
||||
[ "authFrontendUrl" .= _authFrontendUrl externalProviderTD,
|
||||
"authServerOauthRedirectUrl" .= _authServerOauthRedirectUrl externalProviderTD,
|
||||
"authProviderEnabled" .= _authProviderEnabled externalProviderTD
|
||||
]
|
||||
|
||||
genRouter :: AppSpec -> Generator FileDraft
|
||||
genRouter spec = do
|
||||
return $
|
||||
@ -87,13 +103,30 @@ createRouterTemplateData spec =
|
||||
_pagesToImport = pages,
|
||||
_isAuthEnabled = isAuthEnabled spec,
|
||||
_isExternalAuthEnabled = (AS.App.Auth.isExternalAuthEnabled <$> maybeAuth) == Just True,
|
||||
_isGoogleAuthEnabled = (AS.App.Auth.isGoogleAuthEnabled <$> maybeAuth) == Just True
|
||||
_externalAuthProviders = externalAuthProviders
|
||||
}
|
||||
where
|
||||
routes = map (createRouteTemplateData spec) $ AS.getRoutes spec
|
||||
pages = map createPageTemplateData $ AS.getPages spec
|
||||
externalAuthProviders =
|
||||
map
|
||||
(createExternalAuthProviderTemplateData maybeAuth)
|
||||
[ (AS.App.Auth.isGoogleAuthEnabled, googleAuthInfo),
|
||||
(AS.App.Auth.isGitHubAuthEnabled, gitHubAuthInfo)
|
||||
]
|
||||
maybeAuth = AS.App.auth $ snd $ getApp spec
|
||||
|
||||
createExternalAuthProviderTemplateData ::
|
||||
Maybe AS.App.Auth.Auth ->
|
||||
(AS.App.Auth.Auth -> Bool, ExternalAuthInfo) ->
|
||||
ExternalAuthProviderTemplateData
|
||||
createExternalAuthProviderTemplateData maybeAuth (method, externalAuthInfo) =
|
||||
ExternalAuthProviderTemplateData
|
||||
{ _authFrontendUrl = frontendLoginUrl externalAuthInfo,
|
||||
_authServerOauthRedirectUrl = serverOauthRedirectHandlerUrl externalAuthInfo,
|
||||
_authProviderEnabled = (method <$> maybeAuth) == Just True
|
||||
}
|
||||
|
||||
createRouteTemplateData :: AppSpec -> (String, AS.Route.Route) -> RouteTemplateData
|
||||
createRouteTemplateData spec namedRoute@(_, route) =
|
||||
RouteTemplateData
|
||||
|
@ -124,7 +124,8 @@ spec_Analyzer = do
|
||||
Auth.methods =
|
||||
Auth.AuthMethods
|
||||
{ Auth.usernameAndPassword = Just Auth.usernameAndPasswordConfig,
|
||||
Auth.google = Nothing
|
||||
Auth.google = Nothing,
|
||||
Auth.gitHub = Nothing
|
||||
},
|
||||
Auth.onAuthFailedRedirectTo = "/",
|
||||
Auth.onAuthSucceededRedirectTo = Nothing
|
||||
|
@ -97,7 +97,8 @@ spec_AppSpecValid = do
|
||||
AS.Auth.methods =
|
||||
AS.Auth.AuthMethods
|
||||
{ AS.Auth.usernameAndPassword = Just AS.Auth.usernameAndPasswordConfig,
|
||||
AS.Auth.google = Nothing
|
||||
AS.Auth.google = Nothing,
|
||||
AS.Auth.gitHub = Nothing
|
||||
},
|
||||
AS.Auth.onAuthFailedRedirectTo = "/",
|
||||
AS.Auth.onAuthSucceededRedirectTo = Nothing
|
||||
|
@ -6,7 +6,7 @@ cabal-version: 2.4
|
||||
-- Consider using hpack, or maybe even hpack-dhall.
|
||||
|
||||
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>
|
||||
homepage: https://github.com/wasp-lang/wasp/waspc#readme
|
||||
bug-reports: https://github.com/wasp-lang/wasp/issues
|
||||
@ -237,6 +237,7 @@ library
|
||||
Wasp.Generator.ServerGenerator.AuthG
|
||||
Wasp.Generator.ServerGenerator.Common
|
||||
Wasp.Generator.ServerGenerator.ConfigG
|
||||
Wasp.Generator.ServerGenerator.ExternalAuthG
|
||||
Wasp.Generator.ServerGenerator.ExternalCodeGenerator
|
||||
Wasp.Generator.ServerGenerator.JobGenerator
|
||||
Wasp.Generator.ServerGenerator.OperationsG
|
||||
@ -249,6 +250,7 @@ library
|
||||
Wasp.Generator.WebAppGenerator
|
||||
Wasp.Generator.WebAppGenerator.AuthG
|
||||
Wasp.Generator.WebAppGenerator.Common
|
||||
Wasp.Generator.WebAppGenerator.ExternalAuthG
|
||||
Wasp.Generator.WebAppGenerator.ExternalCodeGenerator
|
||||
Wasp.Generator.WebAppGenerator.OperationsGenerator
|
||||
Wasp.Generator.WebAppGenerator.OperationsGenerator.ResourcesG
|
||||
|
@ -87,7 +87,7 @@ Follow the Google setup guide [here](https://wasp-lang.dev/docs/integrations/goo
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { GoogleSignInButton } from '@wasp/auth/buttons/Google'
|
||||
import { SignInButton as GoogleSignInButton } from '@wasp/auth/helpers/Google'
|
||||
import LoginForm from '@wasp/auth/forms/Login'
|
||||
|
||||
const Login = () => {
|
||||
|
@ -16,9 +16,9 @@ import ImgWithCaption from './components/ImgWithCaption'
|
||||
![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!
|
||||
|
||||
|
132
web/blog/2023-01-11-betathon-review.md
Normal 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 that’s exactly what they did!
|
||||
|
||||
As Wasp is still in its early days, we weren’t sure what the response would be, or if there’d be any response at all. Considering that we didn’t do much promotion of the Hackathon outside of our own channels, we were surprised by the results.
|
||||
|
||||
In this post, I’ll 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:
|
||||
|
||||
What’s a hackathon without the participants!? Let’s get this post off to a proper start by congratulating our winners and showcasing their work. 🔍
|
||||
|
||||
|
||||
### 🥇 Tim’s 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
|
||||
>
|
||||
|
||||
|
||||
### 🥈Chris’s “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
|
||||
>
|
||||
|
||||
|
||||
### 🥉 Richard’s Roadmap & Feature Voting App
|
||||
<ImgWithCaption
|
||||
alt="Richard’s 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.”* -
|
||||
>
|
||||
|
||||
|
||||
### 🥉 Emmanuel’s Notes App
|
||||
<ImgWithCaption
|
||||
alt="Emmanuel’s 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 [Emmanuel’s Betathon Blog Post](https://dev.to/emmanuelthecoder/making-something-waspy-a-review-of-wasp-571j)
|
||||
>
|
||||
|
||||
|
||||
## Hackathon How-to
|
||||
|
||||
Personally, I’ve never organized a hackathon before, and this was Wasp’s first hackathon as well, so when you’re 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 weren’t certain we’d 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 you’re an early-stage startup, you can’t offer big cash prizes, so we asked Railway if they’d be interested in sponsoring some prizes, as we’re 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 we’d spotlight the winner’s on our platforms and social media accounts.
|
||||
|
||||
|
||||
## Promotion
|
||||
|
||||
For the Wasp Beta Launch Week, we were active and publicising Wasp on many platforms. We didn’t 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 they’re 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 Wasp’s 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 we’d 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 we’ll 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. Here’s to the next one! 🍻**
|
@ -4,10 +4,12 @@ import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||
const ImgWithCaption = (props) => {
|
||||
return (
|
||||
<div>
|
||||
<p align="center">
|
||||
<p align='center'>
|
||||
<figure>
|
||||
<img style={{'width': props.width}} alt={props.alt} src={useBaseUrl(props.source)} />
|
||||
<figcaption class="image-caption">{props.caption}</figcaption>
|
||||
<img style={{ width: props.width }} alt={props.alt} src={useBaseUrl(props.source)} />
|
||||
<figcaption class='image-caption' style={{ fontStyle: 'italic', opacity: 0.6, fontSize: '0.9rem' }}>
|
||||
{props.caption}
|
||||
</figcaption>
|
||||
</figure>
|
||||
</p>
|
||||
</div>
|
||||
|
5
web/docs/_addExternalAuthEnvVarsReminder.md
Normal 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.
|
||||
|
||||
:::
|
@ -3,6 +3,8 @@ title: Deploying
|
||||
---
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
import AddExternalAuthEnvVarsReminder from './_addExternalAuthEnvVarsReminder.md'
|
||||
|
||||
:::info
|
||||
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!
|
||||
@ -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.
|
||||
- `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)
|
||||
|
||||
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 JWT_SECRET=<random_string_at_least_32_characters_long>
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
@ -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`.
|
||||
|
||||
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`.
|
||||
|
||||
|
26
web/docs/integrations/github.md
Normal 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
|
@ -831,12 +831,13 @@ app MyApp {
|
||||
Entity which represents the user (sometimes also referred to as *Principal*).
|
||||
|
||||
#### `externalAuthEntity: entity` (optional)
|
||||
Entity which associates a user with some external authentication provider. We currently offer support for [Google](#google).
|
||||
Entity which associates a user with some external authentication provider. We currently offer support for [Google](#google) and [GitHub](#github).
|
||||
|
||||
#### `methods: dict` (required)
|
||||
List of authentication methods that Wasp app supports. Currently supported methods are:
|
||||
* `usernameAndPassword`: Provides support for authentication with a username and password. See [here](#username-and-password) for more.
|
||||
* `google`: Provides support for login via Google accounts. See [here](#google) for more.
|
||||
* `gitHub`: Provides support for login via GitHub accounts. See [here](#github) for more.
|
||||
|
||||
#### `onAuthFailedRedirectTo: String` (required)
|
||||
Path where an unauthenticated user will be redirected to if they try to access a private page (which is declared by setting `authRequired: true` for a specific page).
|
||||
@ -1115,11 +1116,12 @@ import AuthError from '@wasp/core/AuthError.js'
|
||||
//...
|
||||
}
|
||||
```
|
||||
This method requires also requires that `externalAuthEntity` be specified in `auth` as [described here](features#externalauthentity).
|
||||
|
||||
If you require custom configuration setup or user entity field assignment, you can [override the defaults](#overrides).
|
||||
This method also requires that `externalAuthEntity` be specified in `auth` as [described here](features#externalauthentity). NOTE: The same `externalAuthEntity` can be used across different social login providers (e.g., both GitHub and Google can use the same entity).
|
||||
|
||||
#### Default settings
|
||||
If you require custom configuration setup or user entity field assignment, you can [override the defaults](features#google-overrides).
|
||||
|
||||
#### Google Default settings
|
||||
- Configuration:
|
||||
- By default, Wasp expects you to set two environment variables in order to use Google authentication:
|
||||
- `GOOGLE_CLIENT_ID`
|
||||
@ -1132,9 +1134,9 @@ If you require custom configuration setup or user entity field assignment, you c
|
||||
|
||||
Alternatively, you could add a `displayName` property to your User entity and assign it using the details of their Google account, as described in **Overrides** below
|
||||
:::
|
||||
- Here is a link to the default implementations: https://github.com/wasp-lang/wasp/blob/release/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleDefaults.js . These can be overriden as explained below.
|
||||
- Here is a link to the default implementations: https://github.com/wasp-lang/wasp/blob/release/waspc/data/Generator/templates/server/src/routes/auth/passport/google/defaults.js . These can be overriden as explained below.
|
||||
|
||||
#### Overrides
|
||||
#### Google Overrides
|
||||
If you require modifications to the above, you can add one or more of the following to your `auth.methods.google` dictionary:
|
||||
|
||||
```js
|
||||
@ -1156,9 +1158,9 @@ If you require modifications to the above, you can add one or more of the follow
|
||||
export function config() {
|
||||
// ...
|
||||
return {
|
||||
clientId, // look up from env or elsewhere,
|
||||
clientID, // look up from env or elsewhere,
|
||||
clientSecret, // look up from env or elsewhere,
|
||||
scope: ['profile'] // must include at least 'profile'
|
||||
scope: ['profile'] // must include at least 'profile' for Google
|
||||
}
|
||||
}
|
||||
|
||||
@ -1177,13 +1179,13 @@ If you require modifications to the above, you can add one or more of the follow
|
||||
```
|
||||
- `generateAvailableUsername` takes an array of Strings and an optional separator and generates a string ending with a random number that is not yet in the database. For example, the above could produce something like "Jim.Smith.3984" for a Google user Jim Smith.
|
||||
|
||||
#### UI helpers
|
||||
#### Google UI helpers
|
||||
|
||||
To use the Google sign-in button or URL on your login page, do either of the following:
|
||||
To use the Google sign-in button, logo or URL on your login page, do either of the following:
|
||||
|
||||
```js
|
||||
...
|
||||
import { GoogleSignInButton, googleSignInUrl } from '@wasp/auth/buttons/Google'
|
||||
import { SignInButton as GoogleSignInButton, signInUrl as googleSignInUrl, logoUrl as googleLogoUrl } from '@wasp/auth/helpers/Google'
|
||||
|
||||
const Login = () => {
|
||||
return (
|
||||
@ -1200,7 +1202,39 @@ const Login = () => {
|
||||
export default Login
|
||||
```
|
||||
|
||||
You can adjust the height of the button by setting a `height` prop (e.g., `<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`
|
||||
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:
|
||||
|
@ -48,7 +48,7 @@ app TodoApp {
|
||||
// Expects entity User to have (username:String) and (password:String) fields.
|
||||
userEntity: User,
|
||||
methods: {
|
||||
// We also support Google, with more on the way!
|
||||
// We also support Google and GitHub, with more on the way!
|
||||
usernameAndPassword: {}
|
||||
},
|
||||
// We'll see how this is used a bit later
|
||||
|
@ -19,6 +19,7 @@ module.exports = {
|
||||
'https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'
|
||||
],
|
||||
themeConfig: {
|
||||
/*
|
||||
announcementBar: {
|
||||
id: 'Beta_is_here',
|
||||
content: 'Wasp Hackathon #1 is underway! 🚀 <a href="https://betathon.wasp-lang.dev/">Join now</a>',
|
||||
@ -26,6 +27,7 @@ module.exports = {
|
||||
textColor: '#fff',
|
||||
isCloseable: false,
|
||||
},
|
||||
*/
|
||||
navbar: {
|
||||
title: '.wasp (beta)',
|
||||
logo: {
|
||||
@ -125,7 +127,7 @@ module.exports = {
|
||||
'@docusaurus/preset-classic',
|
||||
{
|
||||
gtag: {
|
||||
trackingID: 'G-3ZEDH3BVGE',
|
||||
trackingID: 'GTM-WJX89HZ',
|
||||
anonymizeIP: true,
|
||||
},
|
||||
docs: {
|
||||
|
@ -58,6 +58,7 @@ module.exports = {
|
||||
label: 'Integrations',
|
||||
collapsed: false,
|
||||
items: [
|
||||
'integrations/github',
|
||||
'integrations/google',
|
||||
'integrations/css-frameworks'
|
||||
]
|
||||
|
181
web/src/components/ExampleWaspApps.js
Normal 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
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||
import Link from '@docusaurus/Link'
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter'
|
||||
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 SectionContainer from './Layouts/SectionContainer'
|
||||
@ -90,7 +90,7 @@ page MainPage {
|
||||
/* Only logged in users can access this. */
|
||||
authRequired: true,
|
||||
/* import your React code */
|
||||
component: import Main from "@ext/Main.js"
|
||||
component: import Main from "@client/Main.js"
|
||||
}`
|
||||
|
||||
return (
|
||||
@ -145,16 +145,27 @@ page MainPage {
|
||||
<div className='lg:col-span-6 lg:mt-0 mt-16'>
|
||||
<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'>
|
||||
<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='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>
|
||||
|
||||
{/* Editor body */}
|
||||
<div className='w-full text-sm shadow-2xl rounded-b-md'>
|
||||
<SyntaxHighlighter
|
||||
language="javascript"
|
||||
|
@ -119,7 +119,7 @@ const Nav = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Announcement />
|
||||
{/* <Announcement /> */}
|
||||
<div className='sticky top-0 z-50'>
|
||||
<div className='bg-[#f5f4f0] absolute top-0 h-full w-full opacity-80'></div>
|
||||
<nav className='border-b backdrop-blur-sm'>
|
||||
|
@ -25,6 +25,14 @@ const testimonials = [
|
||||
img: 'img/lp/tm/reyero.jpg',
|
||||
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 I’m really not easy to impress. That’s gotta mean something… #programming #webdevelopment #FullStack",
|
||||
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!",
|
||||
url: 'https://www.producthunt.com/posts/wasp-lang-beta?comment=2048472',
|
||||
name: 'Erlis Kllogjri',
|
||||
handle: 'erlis_kllogjri',
|
||||
handle: '@erlis_kllogjri',
|
||||
img: 'img/lp/tm/erlis.jpg',
|
||||
source: PH
|
||||
},
|
||||
|
@ -1,62 +1,74 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Head from '@docusaurus/Head'
|
||||
import Nav from '../components/Nav/index'
|
||||
import Hero from '../components/Hero'
|
||||
import Benefits from '../components/Benefits'
|
||||
import Testimonials from '../components/Testimonials'
|
||||
import ExampleWaspApps from '../components/ExampleWaspApps'
|
||||
import HowItWorks from '../components/HowItWorks'
|
||||
import ShowcaseGallery from '../components/ShowcaseGallery'
|
||||
import Newsletter from '../components/Newsletter'
|
||||
import Faq from '../components/Faq'
|
||||
import Footer from '../components/Footer'
|
||||
|
||||
import waspLogo from '../../static/img/wasp-logo.png'
|
||||
import styles from './styles.module.css'
|
||||
import './index.css'
|
||||
import './preflight.css'
|
||||
|
||||
|
||||
const Background = () => {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const LightsTwo = () => (
|
||||
<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>
|
||||
)
|
||||
);
|
||||
|
||||
const Index = () => {
|
||||
return (
|
||||
<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 />
|
||||
<div className='min-h-screen'>
|
||||
<main>
|
||||
<Background />
|
||||
<div> {/* container */}
|
||||
|
||||
<div>
|
||||
{/* container */}
|
||||
<Hero />
|
||||
<Benefits />
|
||||
<Testimonials />
|
||||
|
||||
<ExampleWaspApps />
|
||||
<LightsTwo />
|
||||
|
||||
<HowItWorks />
|
||||
<ShowcaseGallery />
|
||||
<Newsletter />
|
||||
<Faq />
|
||||
|
||||
</div> {/* eof container */}
|
||||
</div>
|
||||
{/* eof container */}
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Index
|
||||
export default Index;
|
||||
|
BIN
web/static/img/betathon/betathonpage.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
web/static/img/betathon/chris.png
Normal file
After Width: | Height: | Size: 780 KiB |
BIN
web/static/img/betathon/emmanuel.png
Normal file
After Width: | Height: | Size: 121 KiB |
BIN
web/static/img/betathon/github.png
Normal file
After Width: | Height: | Size: 669 KiB |
BIN
web/static/img/betathon/keyboard.png
Normal file
After Width: | Height: | Size: 839 KiB |
BIN
web/static/img/betathon/richard.png
Normal file
After Width: | Height: | Size: 367 KiB |
BIN
web/static/img/betathon/tim.png
Normal file
After Width: | Height: | Size: 621 KiB |
BIN
web/static/img/integrations-github-1.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
web/static/img/lp/tm/tskaggs.png
Normal file
After Width: | Height: | Size: 407 KiB |