Implemented Auth UI. (#1093)

This commit is contained in:
Matija Sosic 2023-04-05 22:56:05 +02:00 committed by GitHub
parent 0e5aa85014
commit 8ad582ac7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 926 additions and 159 deletions

View File

@ -0,0 +1,306 @@
{{={= =}=}}
import React, { useState, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { createStitches, createTheme } from '@stitches/react'
import { errorMessage } from '../../utils.js'
{=# isUsernameAndPasswordAuthEnabled =}
import signup from '../signup.js'
import login from '../login.js'
{=/ isUsernameAndPasswordAuthEnabled =}
{=# isExternalAuthEnabled =}
import * as SocialIcons from './SocialIcons'
{=/ isExternalAuthEnabled =}
import config from '../../config.js'
import { styled, css } from '../../stitches.config'
const socialButtonsContainerStyle = {
maxWidth: '20rem'
}
const logoStyle = {
height: '3rem'
}
const Container = styled('div', {
display: 'flex',
flexDirection: 'column',
})
const HeaderText = styled('h2', {
fontSize: '1.875rem',
fontWeight: '700',
marginTop: '1.5rem'
})
const SocialAuth = styled('div', {
marginTop: '1.5rem'
})
const SocialAuthLabel = styled('div', {
fontWeight: '500',
fontSize: '$sm'
})
const SocialAuthButtons = styled('div', {
marginTop: '0.5rem',
display: 'flex',
variants: {
direction: {
horizontal: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(48px, 1fr))',
},
vertical: {
flexDirection: 'column',
margin: '8px 0',
}
},
gap: {
small: {
gap: '4px',
},
medium: {
gap: '8px',
},
large: {
gap: '16px',
}
}
}
})
const SocialButton = styled('a', {
display: 'flex',
justifyContent: 'center',
cursor: 'pointer',
// NOTE(matija): icon is otherwise blue, since that
// is link's default font color.
color: 'inherit',
backgroundColor: '#f0f0f0',
borderRadius: '0.375rem',
borderWidth: '1px',
borderColor: '$gray600',
fontSize: '13px',
padding: '10px 15px',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
'&:visited': {
color: 'inherit',
},
'&:hover': {
backgroundColor: '$gray500',
},
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '100ms'
})
const OrContinueWith = styled('div', {
position: 'relative',
marginTop: '1.5rem'
})
const OrContinueWithLineContainer = styled('div', {
position: 'absolute',
inset: '0px',
display: 'flex',
alignItems: 'center'
})
const OrContinueWithLine = styled('div', {
width: '100%',
borderTopWidth: '1px',
borderColor: '$gray500'
})
const OrContinueWithTextContainer = styled('div', {
position: 'relative',
display: 'flex',
justifyContent: 'center',
fontSize: '$sm'
})
const OrContinueWithText = styled('span', {
backgroundColor: 'white',
paddingLeft: '0.5rem',
paddingRight: '0.5rem'
})
// Email/username form
const UserPassForm = styled('form', {
marginTop: '1.5rem'
})
const FormItemGroup = styled('div', {
'& + div': {
marginTop: '1.5rem'
}
})
const FormLabel = styled('label', {
display: 'block',
fontSize: '$sm',
fontWeight: '500'
})
const FormInput = styled('input', {
display: 'block',
lineHeight: '1.5rem',
fontSize: '$sm',
borderWidth: '1px',
borderColor: '$gray600',
backgroundColor: '#f8f4ff',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
'&:focus': {
borderWidth: '1px',
borderColor: '$gray700',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
},
borderRadius: '0.375rem',
width: '100%',
paddingTop: '0.375rem',
paddingBottom: '0.375rem',
marginTop: '0.5rem'
})
const SubmitButton = styled('button', {
display: 'flex',
justifyContent: 'center',
width: '100%',
borderRadius: '0.375rem',
borderWidth: '1px',
borderColor: '$brand',
backgroundColor: '$brand',
color: '$submitButtonText',
padding: '0.5rem 0.75rem',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
fontWeight: '600',
fontSize: '$sm',
lineHeight: '1.25rem',
borderRadius: '0.375rem',
// TODO(matija): extract this into separate BaseButton component and then inherit it.
'&:hover': {
backgroundColor: '$brandAccent',
borderColor: '$brandAccent',
},
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '100ms'
})
const googleSignInUrl = `${config.apiUrl}{= googleSignInPath =}`
const gitHubSignInUrl = `${config.apiUrl}{= gitHubSignInPath =}`
// TODO(matija): introduce type for appearance
const Auth = ({ isLogin, appearance, logo, socialLayout } :
{ isLogin: boolean; logo: string; socialLayout: "horizontal" | "vertical" }) => {
const history = useHistory()
const [usernameFieldVal, setUsernameFieldVal] = useState('')
const [passwordFieldVal, setPasswordFieldVal] = useState('')
const handleSubmit = async (event) => {
event.preventDefault()
try {
if (!isLogin) {
await signup({ username: usernameFieldVal, password: passwordFieldVal })
}
await login (usernameFieldVal, passwordFieldVal)
setUsernameFieldVal('')
setPasswordFieldVal('')
// Redirect to configured page, defaults to /.
history.push('{= onAuthSucceededRedirectTo =}')
} catch (err) {
console.log(err)
window.alert(errorMessage(err))
}
}
// TODO(matija): this is called on every render, is it a problem?
// If we do it in useEffect(), then there is a glitch between the default color and the
// user provided one.
const customTheme = createTheme(appearance)
const cta = isLogin ? 'Log in' : 'Sign up'
const title = isLogin ? 'Log in to your account' : 'Create a new account'
const socialButtonsDirection = socialLayout === 'vertical' ? 'vertical' : 'horizontal'
return (
<Container className={customTheme}>
<div>
{logo && (
<img style={logoStyle} src={logo} alt='Your Company' />
)}
<HeaderText>{title}</HeaderText>
</div>
{=# isExternalAuthEnabled =}
<SocialAuth>
<SocialAuthLabel>{cta} with</SocialAuthLabel>
<SocialAuthButtons gap='large' direction={socialButtonsDirection}>
{=# isGoogleAuthEnabled =}
<SocialButton href={googleSignInUrl}><SocialIcons.Google/></SocialButton>
{=/ isGoogleAuthEnabled =}
{=# isGitHubAuthEnabled =}
<SocialButton href={gitHubSignInUrl}><SocialIcons.GitHub/></SocialButton>
{=/ isGitHubAuthEnabled =}
</SocialAuthButtons>
</SocialAuth>
{=/ isExternalAuthEnabled =}
{=# areBothExternalAndUsernameAndPasswordAuthEnabled =}
<OrContinueWith>
<OrContinueWithLineContainer>
<OrContinueWithLine/>
</OrContinueWithLineContainer>
<OrContinueWithTextContainer>
<OrContinueWithText>Or continue with</OrContinueWithText>
</OrContinueWithTextContainer>
</OrContinueWith>
{=/ areBothExternalAndUsernameAndPasswordAuthEnabled =}
{=# isUsernameAndPasswordAuthEnabled =}
<UserPassForm onSubmit={handleSubmit}>
<FormItemGroup>
<FormLabel>Username</FormLabel>
<FormInput
type="text"
value={usernameFieldVal}
onChange={e => setUsernameFieldVal(e.target.value)}
/>
</FormItemGroup>
<FormItemGroup>
<FormLabel>Password</FormLabel>
<FormInput
type="password"
value={passwordFieldVal}
onChange={e => setPasswordFieldVal(e.target.value)}
/>
</FormItemGroup>
<FormItemGroup>
<SubmitButton type="submit">{cta}</SubmitButton>
</FormItemGroup>
</UserPassForm>
{=/ isUsernameAndPasswordAuthEnabled =}
</Container>
)
}
export default Auth

View File

@ -1,46 +1,15 @@
{{={= =}=}} {{={= =}=}}
import React, { useState } from 'react' import React, { useState } from 'react'
import { useHistory } from 'react-router-dom' import Auth from './Auth'
import login from '../login.js' const LoginForm = ({ appearance, logo, socialLayout }) => {
import { errorMessage } from '../../utils.js'
const LoginForm = () => {
const history = useHistory()
const [usernameFieldVal, setUsernameFieldVal] = useState('')
const [passwordFieldVal, setPasswordFieldVal] = useState('')
const handleLogin = async (event) => {
event.preventDefault()
try {
await login(usernameFieldVal, passwordFieldVal)
// Redirect to configured page, defaults to /.
history.push('{= onAuthSucceededRedirectTo =}')
} catch (err) {
console.log(err)
window.alert(errorMessage(err))
}
}
return ( return (
<form onSubmit={handleLogin} className="login-form auth-form"> <Auth
<h2>Username</h2> appearance={appearance}
<input logo={logo}
type="text" socialLayout={socialLayout}
value={usernameFieldVal} isLogin={true}
onChange={e => setUsernameFieldVal(e.target.value)} />
/>
<h2>Password</h2>
<input
type="password"
value={passwordFieldVal}
onChange={e => setPasswordFieldVal(e.target.value)}
/>
<div>
<input type="submit" value="Log in"/>
</div>
</form>
) )
} }

View File

@ -1,53 +1,19 @@
{{={= =}=}} {{={= =}=}}
import React, { useState } from 'react' import React, { useState } from 'react'
import { useHistory } from 'react-router-dom'
import signup from '../signup.js' import Auth from './Auth'
import login from '../login.js'
import { errorMessage } from '../../utils.js'
const SignupForm = () => { const SignupForm = ({ appearance, logo, socialLayout }) => {
const history = useHistory()
const [usernameFieldVal, setUsernameFieldVal] = useState('')
const [passwordFieldVal, setPasswordFieldVal] = useState('')
const handleSignup = async (event) => {
event.preventDefault()
try {
await signup({ username: usernameFieldVal, password: passwordFieldVal })
await login (usernameFieldVal, passwordFieldVal)
setUsernameFieldVal('')
setPasswordFieldVal('')
// Redirect to configured page, defaults to /.
history.push('{= onAuthSucceededRedirectTo =}')
} catch (err) {
console.log(err)
window.alert(errorMessage(err))
}
}
return ( return (
<form onSubmit={handleSignup} className='signup-form auth-form'> <Auth
<h2>Username</h2> appearance={appearance}
<input logo={logo}
type="text" socialLayout={socialLayout}
value={usernameFieldVal} isLogin={false}
onChange={e => setUsernameFieldVal(e.target.value)} />
/>
<h2>Password</h2>
<input
type="password"
value={passwordFieldVal}
onChange={e => setPasswordFieldVal(e.target.value)}
/>
<div>
<input type="submit" value="Sign up"/>
</div>
</form>
) )
} }
export default SignupForm export default SignupForm

View File

@ -0,0 +1,34 @@
import { css } from '@stitches/react'
const defaultStyles = css({
width: '1.25rem',
height: '1.25rem',
})
export const Google = () => (
<svg
className={defaultStyles()}
ariaHidden="true"
fill="currentColor"
viewBox="0 0 24 24">
<g id="brand" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="google" fill="#000000" fill-rule="nonzero">
<path d="M11.99,13.9 L11.99,10.18 L21.35,10.18 C21.49,10.81 21.6,11.4 21.6,12.23 C21.6,17.94 17.77,22 12,22 C6.48,22 2,17.52 2,12 C2,6.48 6.48,2 12,2 C14.7,2 16.96,2.99 18.69,4.61 L15.85,7.37 C15.13,6.69 13.87,5.89 12,5.89 C8.69,5.89 5.99,8.64 5.99,12.01 C5.99,15.38 8.69,18.13 12,18.13 C15.83,18.13 17.24,15.48 17.5,13.91 L11.99,13.91 L11.99,13.9 Z" id="Shape">
</path>
</g>
</g>
</svg>
)
export const GitHub = () => (
<svg
className={defaultStyles()}
ariaHidden="true"
fill="currentColor"
viewBox="0 0 20 20">
<path fillRule="evenodd"
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
clipRule="evenodd"
/>
</svg>
)

View File

@ -0,0 +1,24 @@
import { createStitches } from '@stitches/react'
export const {
styled,
css
} = createStitches({
theme: {
colors: {
waspYellow: '#ffcc00',
gray700: '#a1a5ab',
gray600: '#d1d5db',
gray500: 'gainsboro',
gray400: '#f0f0f0',
brand: '$waspYellow',
brandAccent: '#ffdb46',
submitButtonText: 'black'
},
fontSizes: {
sm: '0.875rem'
}
}
})

View File

@ -84,6 +84,10 @@ waspComplexTest/.wasp/out/web-app/src/actions/core.d.ts
waspComplexTest/.wasp/out/web-app/src/actions/core.js waspComplexTest/.wasp/out/web-app/src/actions/core.js
waspComplexTest/.wasp/out/web-app/src/actions/index.ts waspComplexTest/.wasp/out/web-app/src/actions/index.ts
waspComplexTest/.wasp/out/web-app/src/api.ts waspComplexTest/.wasp/out/web-app/src/api.ts
waspComplexTest/.wasp/out/web-app/src/auth/forms/Auth.tsx
waspComplexTest/.wasp/out/web-app/src/auth/forms/Login.jsx
waspComplexTest/.wasp/out/web-app/src/auth/forms/Signup.jsx
waspComplexTest/.wasp/out/web-app/src/auth/forms/SocialIcons.tsx
waspComplexTest/.wasp/out/web-app/src/auth/helpers/Google.jsx waspComplexTest/.wasp/out/web-app/src/auth/helpers/Google.jsx
waspComplexTest/.wasp/out/web-app/src/auth/helpers/user.ts waspComplexTest/.wasp/out/web-app/src/auth/helpers/user.ts
waspComplexTest/.wasp/out/web-app/src/auth/logout.js waspComplexTest/.wasp/out/web-app/src/auth/logout.js
@ -110,6 +114,7 @@ waspComplexTest/.wasp/out/web-app/src/queries/index.d.ts
waspComplexTest/.wasp/out/web-app/src/queries/index.js waspComplexTest/.wasp/out/web-app/src/queries/index.js
waspComplexTest/.wasp/out/web-app/src/queryClient.js waspComplexTest/.wasp/out/web-app/src/queryClient.js
waspComplexTest/.wasp/out/web-app/src/router.jsx waspComplexTest/.wasp/out/web-app/src/router.jsx
waspComplexTest/.wasp/out/web-app/src/stitches.config.js
waspComplexTest/.wasp/out/web-app/src/storage.ts waspComplexTest/.wasp/out/web-app/src/storage.ts
waspComplexTest/.wasp/out/web-app/src/test/index.ts waspComplexTest/.wasp/out/web-app/src/test/index.ts
waspComplexTest/.wasp/out/web-app/src/test/vitest/helpers.tsx waspComplexTest/.wasp/out/web-app/src/test/vitest/helpers.tsx

View File

@ -508,7 +508,7 @@
"file", "file",
"web-app/package.json" "web-app/package.json"
], ],
"bbb4abefcbfeb37e6feaee0906b849939df6bd85d70821a81217be6cff2c6b1d" "242405370b2d81ba452a0979203c6825d510e601334397e4c4eb2923a3b6edf2"
], ],
[ [
[ [
@ -580,6 +580,34 @@
], ],
"f509fac2e6e742d7df53f9999141644664d9ffc7c82eeb292fd3f060bf9233a3" "f509fac2e6e742d7df53f9999141644664d9ffc7c82eeb292fd3f060bf9233a3"
], ],
[
[
"file",
"web-app/src/auth/forms/Auth.tsx"
],
"3c15faec2c79eafeb0e796bb849c04eaf3c6c233636b96de6765352670816d97"
],
[
[
"file",
"web-app/src/auth/forms/Login.jsx"
],
"a0bf5bc76dc48aacd73436c4dc15881f039f7f7d3d9a5b2e9dabda87b72d24b0"
],
[
[
"file",
"web-app/src/auth/forms/Signup.jsx"
],
"cba167b5decece31212894297a27cf2509bccb6a55fc0fba0a54322602d9d21e"
],
[
[
"file",
"web-app/src/auth/forms/SocialIcons.tsx"
],
"4e89c92b63539e28fe0d084b1fdcf870a426eb228ce37c57b374597005ff235c"
],
[ [
[ [
"file", "file",
@ -762,6 +790,13 @@
], ],
"5c34b729d2244c792086224a6fae18c7bdfc029098a1fff9fe1685ba40af9d9b" "5c34b729d2244c792086224a6fae18c7bdfc029098a1fff9fe1685ba40af9d9b"
], ],
[
[
"file",
"web-app/src/stitches.config.js"
],
"25680fb34bd5200a8416e0a5ebfe8cb0e4240e7848ed3a065c56d1b55d7edd09"
],
[ [
[ [
"file", "file",

View File

@ -1 +1 @@
{"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^12.1.5"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}} {"npmDepsForServer":{"dependencies":[{"name":"cookie-parser","version":"~1.4.6"},{"name":"cors","version":"^2.8.5"},{"name":"express","version":"~4.18.1"},{"name":"morgan","version":"~1.10.0"},{"name":"@prisma/client","version":"4.5.0"},{"name":"jsonwebtoken","version":"^8.5.1"},{"name":"secure-password","version":"^4.0.0"},{"name":"dotenv","version":"16.0.2"},{"name":"helmet","version":"^6.0.0"},{"name":"patch-package","version":"^6.4.7"},{"name":"uuid","version":"^9.0.0"},{"name":"lodash.merge","version":"^4.6.2"},{"name":"passport","version":"0.6.0"},{"name":"passport-google-oauth20","version":"2.0.0"},{"name":"pg-boss","version":"^8.4.2"},{"name":"@sendgrid/mail","version":"^7.7.0"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"nodemon","version":"^2.0.19"},{"name":"standard","version":"^17.0.0"},{"name":"prisma","version":"4.5.0"},{"name":"typescript","version":"^4.8.4"},{"name":"@types/express","version":"^4.17.13"},{"name":"@types/express-serve-static-core","version":"^4.17.13"},{"name":"@types/node","version":"^18.11.9"},{"name":"@tsconfig/node18","version":"^1.0.1"}]},"npmDepsForWebApp":{"dependencies":[{"name":"axios","version":"^0.27.2"},{"name":"react","version":"^17.0.2"},{"name":"react-dom","version":"^17.0.2"},{"name":"@tanstack/react-query","version":"^4.13.0"},{"name":"react-router-dom","version":"^5.3.3"},{"name":"@prisma/client","version":"4.5.0"},{"name":"@stitches/react","version":"^1.2.8"},{"name":"react-redux","version":"^7.1.3"},{"name":"redux","version":"^4.0.5"}],"devDependencies":[{"name":"vite","version":"^4.1.0"},{"name":"typescript","version":"^4.9.3"},{"name":"@types/react","version":"^17.0.53"},{"name":"@types/react-dom","version":"^17.0.19"},{"name":"@types/react-router-dom","version":"^5.3.3"},{"name":"@vitejs/plugin-react-swc","version":"^3.0.0"},{"name":"dotenv","version":"^16.0.3"},{"name":"@tsconfig/vite-react","version":"^1.0.1"},{"name":"vitest","version":"^0.29.3"},{"name":"@vitest/ui","version":"^0.29.3"},{"name":"jsdom","version":"^21.1.1"},{"name":"@testing-library/react","version":"^12.1.5"},{"name":"@testing-library/jest-dom","version":"^5.16.5"},{"name":"msw","version":"^1.1.0"}]}}

View File

@ -13,6 +13,7 @@
}, },
"dependencies": { "dependencies": {
"@prisma/client": "4.5.0", "@prisma/client": "4.5.0",
"@stitches/react": "^1.2.8",
"@tanstack/react-query": "^4.13.0", "@tanstack/react-query": "^4.13.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"react": "^17.0.2", "react": "^17.0.2",

View File

@ -0,0 +1,257 @@
import React, { useState, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import { createStitches, createTheme } from '@stitches/react'
import { errorMessage } from '../../utils.js'
import * as SocialIcons from './SocialIcons'
import config from '../../config.js'
import { styled, css } from '../../stitches.config'
const socialButtonsContainerStyle = {
maxWidth: '20rem'
}
const logoStyle = {
height: '3rem'
}
const Container = styled('div', {
display: 'flex',
flexDirection: 'column',
})
const HeaderText = styled('h2', {
fontSize: '1.875rem',
fontWeight: '700',
marginTop: '1.5rem'
})
const SocialAuth = styled('div', {
marginTop: '1.5rem'
})
const SocialAuthLabel = styled('div', {
fontWeight: '500',
fontSize: '$sm'
})
const SocialAuthButtons = styled('div', {
marginTop: '0.5rem',
display: 'flex',
variants: {
direction: {
horizontal: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(48px, 1fr))',
},
vertical: {
flexDirection: 'column',
margin: '8px 0',
}
},
gap: {
small: {
gap: '4px',
},
medium: {
gap: '8px',
},
large: {
gap: '16px',
}
}
}
})
const SocialButton = styled('a', {
display: 'flex',
justifyContent: 'center',
cursor: 'pointer',
// NOTE(matija): icon is otherwise blue, since that
// is link's default font color.
color: 'inherit',
backgroundColor: '#f0f0f0',
borderRadius: '0.375rem',
borderWidth: '1px',
borderColor: '$gray600',
fontSize: '13px',
padding: '10px 15px',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
'&:visited': {
color: 'inherit',
},
'&:hover': {
backgroundColor: '$gray500',
},
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '100ms'
})
const OrContinueWith = styled('div', {
position: 'relative',
marginTop: '1.5rem'
})
const OrContinueWithLineContainer = styled('div', {
position: 'absolute',
inset: '0px',
display: 'flex',
alignItems: 'center'
})
const OrContinueWithLine = styled('div', {
width: '100%',
borderTopWidth: '1px',
borderColor: '$gray500'
})
const OrContinueWithTextContainer = styled('div', {
position: 'relative',
display: 'flex',
justifyContent: 'center',
fontSize: '$sm'
})
const OrContinueWithText = styled('span', {
backgroundColor: 'white',
paddingLeft: '0.5rem',
paddingRight: '0.5rem'
})
// Email/username form
const UserPassForm = styled('form', {
marginTop: '1.5rem'
})
const FormItemGroup = styled('div', {
'& + div': {
marginTop: '1.5rem'
}
})
const FormLabel = styled('label', {
display: 'block',
fontSize: '$sm',
fontWeight: '500'
})
const FormInput = styled('input', {
display: 'block',
lineHeight: '1.5rem',
fontSize: '$sm',
borderWidth: '1px',
borderColor: '$gray600',
backgroundColor: '#f8f4ff',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
'&:focus': {
borderWidth: '1px',
borderColor: '$gray700',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
},
borderRadius: '0.375rem',
width: '100%',
paddingTop: '0.375rem',
paddingBottom: '0.375rem',
marginTop: '0.5rem'
})
const SubmitButton = styled('button', {
display: 'flex',
justifyContent: 'center',
width: '100%',
borderRadius: '0.375rem',
borderWidth: '1px',
borderColor: '$brand',
backgroundColor: '$brand',
color: '$submitButtonText',
padding: '0.5rem 0.75rem',
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
fontWeight: '600',
fontSize: '$sm',
lineHeight: '1.25rem',
borderRadius: '0.375rem',
// TODO(matija): extract this into separate BaseButton component and then inherit it.
'&:hover': {
backgroundColor: '$brandAccent',
borderColor: '$brandAccent',
},
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '100ms'
})
const googleSignInUrl = `${config.apiUrl}/auth/google/login`
const gitHubSignInUrl = `${config.apiUrl}/auth/github/login`
// TODO(matija): introduce type for appearance
const Auth = ({ isLogin, appearance, logo, socialLayout } :
{ isLogin: boolean; logo: string; socialLayout: "horizontal" | "vertical" }) => {
const history = useHistory()
const [usernameFieldVal, setUsernameFieldVal] = useState('')
const [passwordFieldVal, setPasswordFieldVal] = useState('')
const handleSubmit = async (event) => {
event.preventDefault()
try {
if (!isLogin) {
await signup({ username: usernameFieldVal, password: passwordFieldVal })
}
await login (usernameFieldVal, passwordFieldVal)
setUsernameFieldVal('')
setPasswordFieldVal('')
// Redirect to configured page, defaults to /.
history.push('/')
} catch (err) {
console.log(err)
window.alert(errorMessage(err))
}
}
// TODO(matija): this is called on every render, is it a problem?
// If we do it in useEffect(), then there is a glitch between the default color and the
// user provided one.
const customTheme = createTheme(appearance)
const cta = isLogin ? 'Log in' : 'Sign up'
const title = isLogin ? 'Log in to your account' : 'Create a new account'
const socialButtonsDirection = socialLayout === 'vertical' ? 'vertical' : 'horizontal'
return (
<Container className={customTheme}>
<div>
{logo && (
<img style={logoStyle} src={logo} alt='Your Company' />
)}
<HeaderText>{title}</HeaderText>
</div>
<SocialAuth>
<SocialAuthLabel>{cta} with</SocialAuthLabel>
<SocialAuthButtons gap='large' direction={socialButtonsDirection}>
<SocialButton href={googleSignInUrl}><SocialIcons.Google/></SocialButton>
</SocialAuthButtons>
</SocialAuth>
</Container>
)
}
export default Auth

View File

@ -0,0 +1,15 @@
import React, { useState } from 'react'
import Auth from './Auth'
const LoginForm = ({ appearance, logo, socialLayout }) => {
return (
<Auth
appearance={appearance}
logo={logo}
socialLayout={socialLayout}
isLogin={true}
/>
)
}
export default LoginForm

View File

@ -0,0 +1,18 @@
import React, { useState } from 'react'
import Auth from './Auth'
const SignupForm = ({ appearance, logo, socialLayout }) => {
return (
<Auth
appearance={appearance}
logo={logo}
socialLayout={socialLayout}
isLogin={false}
/>
)
}
export default SignupForm

View File

@ -0,0 +1,34 @@
import { css } from '@stitches/react'
const defaultStyles = css({
width: '1.25rem',
height: '1.25rem',
})
export const Google = () => (
<svg
className={defaultStyles()}
ariaHidden="true"
fill="currentColor"
viewBox="0 0 24 24">
<g id="brand" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="google" fill="#000000" fill-rule="nonzero">
<path d="M11.99,13.9 L11.99,10.18 L21.35,10.18 C21.49,10.81 21.6,11.4 21.6,12.23 C21.6,17.94 17.77,22 12,22 C6.48,22 2,17.52 2,12 C2,6.48 6.48,2 12,2 C14.7,2 16.96,2.99 18.69,4.61 L15.85,7.37 C15.13,6.69 13.87,5.89 12,5.89 C8.69,5.89 5.99,8.64 5.99,12.01 C5.99,15.38 8.69,18.13 12,18.13 C15.83,18.13 17.24,15.48 17.5,13.91 L11.99,13.91 L11.99,13.9 Z" id="Shape">
</path>
</g>
</g>
</svg>
)
export const GitHub = () => (
<svg
className={defaultStyles()}
ariaHidden="true"
fill="currentColor"
viewBox="0 0 20 20">
<path fillRule="evenodd"
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
clipRule="evenodd"
/>
</svg>
)

View File

@ -0,0 +1,24 @@
import { createStitches } from '@stitches/react'
export const {
styled,
css
} = createStitches({
theme: {
colors: {
waspYellow: '#ffcc00',
gray700: '#a1a5ab',
gray600: '#d1d5db',
gray500: 'gainsboro',
gray400: '#f0f0f0',
brand: '$waspYellow',
brandAccent: '#ffdb46',
submitButtonText: 'black'
},
fontSizes: {
sm: '0.875rem'
}
}
})

View File

@ -27,7 +27,9 @@ export function App({ children }) {
)} )}
</header> </header>
<main>{children}</main> <main>{children}</main>
<footer className="mt-8 text-center">Created with Wasp</footer> <footer className="mt-8 text-center">
Created with Wasp
</footer>
</div> </div>
) )
} }

View File

@ -1,23 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
import LoginForm from '@wasp/auth/forms/Login'
// import { SignInButton as GoogleSignInButton } from '@wasp/auth/helpers/Google'
// import { SignInButton as GitHubSignInButton } from '@wasp/auth/helpers/GitHub'
const Login = () => {
return (
<div className="flex flex-col gap-5">
<LoginForm />
<span>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
{/* <div className="flex flex-col gap-2 max-w-xs">
<GoogleSignInButton/>
<GitHubSignInButton/>
</div> */}
</div>
)
}
export default Login

View File

@ -0,0 +1,34 @@
import React from 'react'
import { Link } from 'react-router-dom'
import LoginForm from '@wasp/auth/forms/Login'
import appearance from './appearance'
import todoLogo from '../../todoLogo.png'
const Login = () => {
return (
<div className='w-full h-full bg-white'>
<div className='min-w-full min-h-[75vh] flex items-center justify-center'>
<div className='w-full h-full max-w-sm p-5 bg-white'>
<div>
<LoginForm
appearance={appearance}
logo={todoLogo}
socialLayout='horizontal'
/>
<br/>
<span className='text-sm font-medium text-gray-900'>
Don't have an account yet? (<Link to="/signup">go to signup</Link>).
</span>
<br/>
</div>
</div>
</div>
</div>
)
}
export default Login

View File

@ -6,18 +6,35 @@ import getNumTasks from '@wasp/queries/getNumTasks'
import { useQuery } from '@wasp/queries' import { useQuery } from '@wasp/queries'
import { getTotalTaskCountMessage } from './helpers' import { getTotalTaskCountMessage } from './helpers'
import appearance from './appearance'
import todoLogo from '../../todoLogo.png'
const Signup = () => { const Signup = () => {
const { data: numTasks } = useQuery(getNumTasks) const { data: numTasks } = useQuery(getNumTasks)
return ( return (
<> <div className='w-full h-full bg-white'>
<SignupForm /> <div className='min-w-full min-h-[75vh] flex items-center justify-center'>
<br /> <div className='w-full h-full max-w-sm p-5 bg-white'>
<span>
I already have an account (<Link to="/login">go to login</Link>). <div>
</span> <SignupForm
<br /> appearance={appearance}
<span>{getTotalTaskCountMessage(numTasks)}</span> logo={todoLogo}
</> socialLayout='horizontal'
/>
<br/>
<span className='text-sm font-medium text-gray-900'>
I already have an account (<Link to="/login">go to login</Link>).
</span>
<br/>
</div>
<br/><br/>
<span>{getTotalTaskCountMessage(numTasks)}</span>
</div>
</div>
</div>
) )
} }

View File

@ -0,0 +1,10 @@
const appearance = {
colors: {
brand: '#5969b8', // blue
brandAccent: '#de5998', // pink
submitButtonText: 'white',
}
}
export default appearance

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -10,17 +10,17 @@ app todoApp {
], ],
auth: { auth: {
userEntity: User, userEntity: User,
// externalAuthEntity: SocialLogin, externalAuthEntity: SocialLogin,
methods: { methods: {
usernameAndPassword: {}, usernameAndPassword: {},
// 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: { gitHub: {
// configFn: import { config } from "@server/auth/github.js", configFn: import { config } from "@server/auth/github.js",
// getUserFieldsFn: import { getUserFields } from "@server/auth/github.js" getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
// } }
}, },
onAuthFailedRedirectTo: "/login", onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/profile" onAuthSucceededRedirectTo: "/profile"
@ -74,7 +74,7 @@ page SignupPage {
route LoginRoute { path: "/login", to: LoginPage } route LoginRoute { path: "/login", to: LoginPage }
page LoginPage { page LoginPage {
component: import Login from "@client/pages/auth/Login.jsx" component: import Login from "@client/pages/auth/Login.tsx"
} }
route HomeRoute { path: "/", to: MainPage } route HomeRoute { path: "/", to: MainPage }

View File

@ -7,6 +7,7 @@ module Wasp.AppSpec.App.Auth
ExternalAuthConfig (..), ExternalAuthConfig (..),
usernameAndPasswordConfig, usernameAndPasswordConfig,
isUsernameAndPasswordAuthEnabled, isUsernameAndPasswordAuthEnabled,
areBothExternalAndUsernameAndPasswordAuthEnabled,
isExternalAuthEnabled, isExternalAuthEnabled,
isGoogleAuthEnabled, isGoogleAuthEnabled,
isGitHubAuthEnabled, isGitHubAuthEnabled,
@ -53,6 +54,9 @@ usernameAndPasswordConfig = UsernameAndPasswordConfig Nothing
isUsernameAndPasswordAuthEnabled :: Auth -> Bool isUsernameAndPasswordAuthEnabled :: Auth -> Bool
isUsernameAndPasswordAuthEnabled = isJust . usernameAndPassword . methods isUsernameAndPasswordAuthEnabled = isJust . usernameAndPassword . methods
areBothExternalAndUsernameAndPasswordAuthEnabled :: Auth -> Bool
areBothExternalAndUsernameAndPasswordAuthEnabled auth = all ($ auth) [isExternalAuthEnabled, isUsernameAndPasswordAuthEnabled]
isExternalAuthEnabled :: Auth -> Bool isExternalAuthEnabled :: Auth -> Bool
isExternalAuthEnabled auth = any ($ auth) [isGoogleAuthEnabled, isGitHubAuthEnabled] isExternalAuthEnabled auth = any ($ auth) [isGoogleAuthEnabled, isGitHubAuthEnabled]

View File

@ -27,7 +27,7 @@ import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
import qualified Wasp.AppSpec.App.Client as AS.App.Client import qualified Wasp.AppSpec.App.Client as AS.App.Client
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import qualified Wasp.AppSpec.Entity as AS.Entity import qualified Wasp.AppSpec.Entity as AS.Entity
import Wasp.AppSpec.Valid (getApp) import Wasp.AppSpec.Valid (getApp, isAuthEnabled)
import Wasp.Env (envVarsToDotEnvContent) import Wasp.Env (envVarsToDotEnvContent)
import Wasp.Generator.AuthProviders (gitHubAuthProvider, googleAuthProvider) import Wasp.Generator.AuthProviders (gitHubAuthProvider, googleAuthProvider)
import qualified Wasp.Generator.AuthProviders.OAuth as OAuth import qualified Wasp.Generator.AuthProviders.OAuth as OAuth
@ -50,6 +50,7 @@ import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator
import Wasp.Generator.WebAppGenerator.JsImport (extImportToImportJson) import Wasp.Generator.WebAppGenerator.JsImport (extImportToImportJson)
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 qualified Wasp.SemanticVersion as SV
import Wasp.Util ((<++>)) import Wasp.Util ((<++>))
genWebApp :: AppSpec -> Generator [FileDraft] genWebApp :: AppSpec -> Generator [FileDraft]
@ -131,6 +132,7 @@ npmDepsForWasp spec =
-- https://github.com/wasp-lang/wasp/pull/962/ for details). -- https://github.com/wasp-lang/wasp/pull/962/ for details).
("@prisma/client", show prismaVersion) ("@prisma/client", show prismaVersion)
] ]
++ depsRequiredForAuth spec
++ depsRequiredByTailwind spec, ++ depsRequiredByTailwind spec,
N.waspDevDependencies = N.waspDevDependencies =
AS.Dependency.fromList AS.Dependency.fromList
@ -150,6 +152,12 @@ npmDepsForWasp spec =
++ depsRequiredForTesting ++ depsRequiredForTesting
} }
depsRequiredForAuth :: AppSpec -> [AS.Dependency.Dependency]
depsRequiredForAuth spec =
[AS.Dependency.make ("@stitches/react", show versionRange) | isAuthEnabled spec]
where
versionRange = SV.Range [SV.backwardsCompatibleWith (SV.Version 1 2 8)]
depsRequiredByTailwind :: AppSpec -> [AS.Dependency.Dependency] depsRequiredByTailwind :: AppSpec -> [AS.Dependency.Dependency]
depsRequiredByTailwind spec = depsRequiredByTailwind spec =
if G.CF.isTailwindUsed spec if G.CF.isTailwindUsed spec

View File

@ -10,14 +10,10 @@ import Wasp.Generator.AuthProviders (localAuthProvider)
import Wasp.Generator.AuthProviders.Local (serverLoginUrl, serverSignupUrl) import Wasp.Generator.AuthProviders.Local (serverLoginUrl, serverSignupUrl)
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator) import Wasp.Generator.Monad (Generator)
import Wasp.Generator.WebAppGenerator.Auth.Common (getOnAuthSucceededRedirectToOrDefault)
import Wasp.Generator.WebAppGenerator.Common as C import Wasp.Generator.WebAppGenerator.Common as C
import Wasp.Util ((<++>))
genLocalAuth :: AS.Auth.Auth -> Generator [FileDraft] genLocalAuth :: AS.Auth.Auth -> Generator [FileDraft]
genLocalAuth auth = genLocalAuth = genActions
genActions auth
<++> genForms auth
genActions :: AS.Auth.Auth -> Generator [FileDraft] genActions :: AS.Auth.Auth -> Generator [FileDraft]
genActions auth genActions auth
@ -39,26 +35,3 @@ genLocalLoginAction :: Generator FileDraft
genLocalLoginAction = return $ C.mkTmplFdWithData (C.asTmplFile [relfile|src/auth/login.js|]) tmplData genLocalLoginAction = return $ C.mkTmplFdWithData (C.asTmplFile [relfile|src/auth/login.js|]) tmplData
where where
tmplData = object ["loginPath" .= serverLoginUrl localAuthProvider] tmplData = object ["loginPath" .= serverLoginUrl localAuthProvider]
genForms :: AS.Auth.Auth -> Generator [FileDraft]
genForms auth
| AS.Auth.isUsernameAndPasswordAuthEnabled auth =
sequence
[ genLoginForm auth,
genSignupForm auth
]
| otherwise = return []
genLoginForm :: AS.Auth.Auth -> Generator FileDraft
genLoginForm auth =
return $
C.mkTmplFdWithData
[relfile|src/auth/forms/Login.jsx|]
(object ["onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth])
genSignupForm :: AS.Auth.Auth -> Generator FileDraft
genSignupForm auth =
return $
C.mkTmplFdWithData
[relfile|src/auth/forms/Signup.jsx|]
(object ["onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth])

View File

@ -4,13 +4,17 @@ module Wasp.Generator.WebAppGenerator.AuthG
where where
import Data.Aeson (object, (.=)) import Data.Aeson (object, (.=))
import StrongPath (relfile) import Data.Aeson.Types (Pair)
import StrongPath (File', Path', Rel', reldir, relfile, (</>))
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.Auth import qualified Wasp.AppSpec.App.Auth as AS.Auth
import Wasp.AppSpec.Valid (getApp) import Wasp.AppSpec.Valid (getApp)
import Wasp.Generator.AuthProviders (gitHubAuthProvider, googleAuthProvider)
import qualified Wasp.Generator.AuthProviders.OAuth as OAuth
import Wasp.Generator.FileDraft (FileDraft) import Wasp.Generator.FileDraft (FileDraft)
import Wasp.Generator.Monad (Generator) import Wasp.Generator.Monad (Generator)
import Wasp.Generator.WebAppGenerator.Auth.Common (getOnAuthSucceededRedirectToOrDefault)
import Wasp.Generator.WebAppGenerator.Auth.LocalAuthG (genLocalAuth) import Wasp.Generator.WebAppGenerator.Auth.LocalAuthG (genLocalAuth)
import Wasp.Generator.WebAppGenerator.Auth.OAuthAuthG (genOAuthAuth) import Wasp.Generator.WebAppGenerator.Auth.OAuthAuthG (genOAuthAuth)
import Wasp.Generator.WebAppGenerator.Common as C import Wasp.Generator.WebAppGenerator.Common as C
@ -26,6 +30,7 @@ genAuth spec =
genCreateAuthRequiredPage auth, genCreateAuthRequiredPage auth,
genUserHelpers genUserHelpers
] ]
<++> genAuthForms auth
<++> genLocalAuth auth <++> genLocalAuth auth
<++> genOAuthAuth auth <++> genOAuthAuth auth
Nothing -> return [] Nothing -> return []
@ -50,5 +55,54 @@ genCreateAuthRequiredPage auth =
genUseAuth :: Generator FileDraft genUseAuth :: Generator FileDraft
genUseAuth = return $ C.mkTmplFd (C.asTmplFile [relfile|src/auth/useAuth.js|]) genUseAuth = return $ C.mkTmplFd (C.asTmplFile [relfile|src/auth/useAuth.js|])
genAuthForms :: AS.Auth.Auth -> Generator [FileDraft]
genAuthForms auth =
sequence
[ genLoginForm auth,
genSignupForm auth,
genAuthForm auth,
copyTmplFile [relfile|stitches.config.js|],
copyTmplFile [relfile|auth/forms/SocialIcons.tsx|]
]
where
copyTmplFile = return . C.mkSrcTmplFd
genAuthForm :: AS.Auth.Auth -> Generator FileDraft
genAuthForm auth =
compileTmplToSamePath
[relfile|auth/forms/Auth.tsx|]
[ "onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth,
"isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth,
"areBothExternalAndUsernameAndPasswordAuthEnabled" .= AS.Auth.areBothExternalAndUsernameAndPasswordAuthEnabled auth,
"isExternalAuthEnabled" .= AS.Auth.isExternalAuthEnabled auth,
-- Google
"isGoogleAuthEnabled" .= AS.Auth.isGoogleAuthEnabled auth,
"googleSignInPath" .= OAuth.serverLoginUrl googleAuthProvider,
-- GitHub
"isGitHubAuthEnabled" .= AS.Auth.isGitHubAuthEnabled auth,
"gitHubSignInPath" .= OAuth.serverLoginUrl gitHubAuthProvider
]
genLoginForm :: AS.Auth.Auth -> Generator FileDraft
genLoginForm auth =
compileTmplToSamePath
[relfile|auth/forms/Login.jsx|]
[ "onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth
]
genSignupForm :: AS.Auth.Auth -> Generator FileDraft
genSignupForm auth =
compileTmplToSamePath
[relfile|auth/forms/Signup.jsx|]
[ "onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth
]
compileTmplToSamePath :: Path' Rel' File' -> [Pair] -> Generator FileDraft
compileTmplToSamePath tmplFileInTmplSrcDir keyValuePairs =
return $
C.mkTmplFdWithData
(asTmplFile $ [reldir|src|] </> tmplFileInTmplSrcDir)
(object keyValuePairs)
genUserHelpers :: Generator FileDraft genUserHelpers :: Generator FileDraft
genUserHelpers = return $ C.mkTmplFd (C.asTmplFile [relfile|src/auth/helpers/user.ts|]) genUserHelpers = return $ C.mkTmplFd (C.asTmplFile [relfile|src/auth/helpers/user.ts|])