mirror of
https://github.com/wasp-lang/wasp.git
synced 2025-01-03 14:47:09 +03:00
Implemented Auth UI. (#1093)
This commit is contained in:
parent
0e5aa85014
commit
8ad582ac7b
306
waspc/data/Generator/templates/react-app/src/auth/forms/Auth.tsx
Normal file
306
waspc/data/Generator/templates/react-app/src/auth/forms/Auth.tsx
Normal 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
|
@ -1,46 +1,15 @@
|
||||
{{={= =}=}}
|
||||
import React, { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import login from '../login.js'
|
||||
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))
|
||||
}
|
||||
}
|
||||
import Auth from './Auth'
|
||||
|
||||
const LoginForm = ({ appearance, logo, socialLayout }) => {
|
||||
return (
|
||||
<form onSubmit={handleLogin} className="login-form auth-form">
|
||||
<h2>Username</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={usernameFieldVal}
|
||||
onChange={e => setUsernameFieldVal(e.target.value)}
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
isLogin={true}
|
||||
/>
|
||||
<h2>Password</h2>
|
||||
<input
|
||||
type="password"
|
||||
value={passwordFieldVal}
|
||||
onChange={e => setPasswordFieldVal(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<input type="submit" value="Log in"/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,53 +1,19 @@
|
||||
{{={= =}=}}
|
||||
import React, { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import signup from '../signup.js'
|
||||
import login from '../login.js'
|
||||
import { errorMessage } from '../../utils.js'
|
||||
|
||||
const SignupForm = () => {
|
||||
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))
|
||||
}
|
||||
}
|
||||
import Auth from './Auth'
|
||||
|
||||
const SignupForm = ({ appearance, logo, socialLayout }) => {
|
||||
return (
|
||||
<form onSubmit={handleSignup} className='signup-form auth-form'>
|
||||
<h2>Username</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={usernameFieldVal}
|
||||
onChange={e => setUsernameFieldVal(e.target.value)}
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
isLogin={false}
|
||||
/>
|
||||
<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
|
||||
|
@ -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>
|
||||
)
|
@ -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'
|
||||
}
|
||||
}
|
||||
})
|
@ -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/index.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/user.ts
|
||||
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/queryClient.js
|
||||
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/test/index.ts
|
||||
waspComplexTest/.wasp/out/web-app/src/test/vitest/helpers.tsx
|
||||
|
@ -508,7 +508,7 @@
|
||||
"file",
|
||||
"web-app/package.json"
|
||||
],
|
||||
"bbb4abefcbfeb37e6feaee0906b849939df6bd85d70821a81217be6cff2c6b1d"
|
||||
"242405370b2d81ba452a0979203c6825d510e601334397e4c4eb2923a3b6edf2"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -580,6 +580,34 @@
|
||||
],
|
||||
"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",
|
||||
@ -762,6 +790,13 @@
|
||||
],
|
||||
"5c34b729d2244c792086224a6fae18c7bdfc029098a1fff9fe1685ba40af9d9b"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/stitches.config.js"
|
||||
],
|
||||
"25680fb34bd5200a8416e0a5ebfe8cb0e4240e7848ed3a065c56d1b55d7edd09"
|
||||
],
|
||||
[
|
||||
[
|
||||
"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"}]}}
|
@ -13,6 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "4.5.0",
|
||||
"@stitches/react": "^1.2.8",
|
||||
"@tanstack/react-query": "^4.13.0",
|
||||
"axios": "^0.27.2",
|
||||
"react": "^17.0.2",
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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>
|
||||
)
|
@ -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'
|
||||
}
|
||||
}
|
||||
})
|
@ -27,7 +27,9 @@ export function App({ children }) {
|
||||
)}
|
||||
</header>
|
||||
<main>{children}</main>
|
||||
<footer className="mt-8 text-center">Created with Wasp</footer>
|
||||
<footer className="mt-8 text-center">
|
||||
Created with Wasp
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
34
waspc/examples/todoApp/src/client/pages/auth/Login.tsx
Normal file
34
waspc/examples/todoApp/src/client/pages/auth/Login.tsx
Normal 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
|
@ -6,18 +6,35 @@ import getNumTasks from '@wasp/queries/getNumTasks'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { getTotalTaskCountMessage } from './helpers'
|
||||
|
||||
import appearance from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
|
||||
const Signup = () => {
|
||||
const { data: numTasks } = useQuery(getNumTasks)
|
||||
|
||||
return (
|
||||
<>
|
||||
<SignupForm />
|
||||
<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>
|
||||
<SignupForm
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
socialLayout='horizontal'
|
||||
/>
|
||||
<br/>
|
||||
<span>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
10
waspc/examples/todoApp/src/client/pages/auth/appearance.js
Normal file
10
waspc/examples/todoApp/src/client/pages/auth/appearance.js
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
const appearance = {
|
||||
colors: {
|
||||
brand: '#5969b8', // blue
|
||||
brandAccent: '#de5998', // pink
|
||||
submitButtonText: 'white',
|
||||
}
|
||||
}
|
||||
|
||||
export default appearance
|
BIN
waspc/examples/todoApp/src/client/todoLogo.png
Normal file
BIN
waspc/examples/todoApp/src/client/todoLogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
@ -10,17 +10,17 @@ app todoApp {
|
||||
],
|
||||
auth: {
|
||||
userEntity: User,
|
||||
// externalAuthEntity: SocialLogin,
|
||||
externalAuthEntity: SocialLogin,
|
||||
methods: {
|
||||
usernameAndPassword: {},
|
||||
// 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"
|
||||
// }
|
||||
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",
|
||||
onAuthSucceededRedirectTo: "/profile"
|
||||
@ -74,7 +74,7 @@ page SignupPage {
|
||||
|
||||
route LoginRoute { path: "/login", to: 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 }
|
||||
|
@ -7,6 +7,7 @@ module Wasp.AppSpec.App.Auth
|
||||
ExternalAuthConfig (..),
|
||||
usernameAndPasswordConfig,
|
||||
isUsernameAndPasswordAuthEnabled,
|
||||
areBothExternalAndUsernameAndPasswordAuthEnabled,
|
||||
isExternalAuthEnabled,
|
||||
isGoogleAuthEnabled,
|
||||
isGitHubAuthEnabled,
|
||||
@ -53,6 +54,9 @@ usernameAndPasswordConfig = UsernameAndPasswordConfig Nothing
|
||||
isUsernameAndPasswordAuthEnabled :: Auth -> Bool
|
||||
isUsernameAndPasswordAuthEnabled = isJust . usernameAndPassword . methods
|
||||
|
||||
areBothExternalAndUsernameAndPasswordAuthEnabled :: Auth -> Bool
|
||||
areBothExternalAndUsernameAndPasswordAuthEnabled auth = all ($ auth) [isExternalAuthEnabled, isUsernameAndPasswordAuthEnabled]
|
||||
|
||||
isExternalAuthEnabled :: Auth -> Bool
|
||||
isExternalAuthEnabled auth = any ($ auth) [isGoogleAuthEnabled, isGitHubAuthEnabled]
|
||||
|
||||
|
@ -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.Dependency as AS.Dependency
|
||||
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.Generator.AuthProviders (gitHubAuthProvider, googleAuthProvider)
|
||||
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.OperationsGenerator (genOperations)
|
||||
import Wasp.Generator.WebAppGenerator.RouterGenerator (genRouter)
|
||||
import qualified Wasp.SemanticVersion as SV
|
||||
import Wasp.Util ((<++>))
|
||||
|
||||
genWebApp :: AppSpec -> Generator [FileDraft]
|
||||
@ -131,6 +132,7 @@ npmDepsForWasp spec =
|
||||
-- https://github.com/wasp-lang/wasp/pull/962/ for details).
|
||||
("@prisma/client", show prismaVersion)
|
||||
]
|
||||
++ depsRequiredForAuth spec
|
||||
++ depsRequiredByTailwind spec,
|
||||
N.waspDevDependencies =
|
||||
AS.Dependency.fromList
|
||||
@ -150,6 +152,12 @@ npmDepsForWasp spec =
|
||||
++ 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 spec =
|
||||
if G.CF.isTailwindUsed spec
|
||||
|
@ -10,14 +10,10 @@ import Wasp.Generator.AuthProviders (localAuthProvider)
|
||||
import Wasp.Generator.AuthProviders.Local (serverLoginUrl, serverSignupUrl)
|
||||
import Wasp.Generator.FileDraft (FileDraft)
|
||||
import Wasp.Generator.Monad (Generator)
|
||||
import Wasp.Generator.WebAppGenerator.Auth.Common (getOnAuthSucceededRedirectToOrDefault)
|
||||
import Wasp.Generator.WebAppGenerator.Common as C
|
||||
import Wasp.Util ((<++>))
|
||||
|
||||
genLocalAuth :: AS.Auth.Auth -> Generator [FileDraft]
|
||||
genLocalAuth auth =
|
||||
genActions auth
|
||||
<++> genForms auth
|
||||
genLocalAuth = genActions
|
||||
|
||||
genActions :: AS.Auth.Auth -> Generator [FileDraft]
|
||||
genActions auth
|
||||
@ -39,26 +35,3 @@ genLocalLoginAction :: Generator FileDraft
|
||||
genLocalLoginAction = return $ C.mkTmplFdWithData (C.asTmplFile [relfile|src/auth/login.js|]) tmplData
|
||||
where
|
||||
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])
|
||||
|
@ -4,13 +4,17 @@ module Wasp.Generator.WebAppGenerator.AuthG
|
||||
where
|
||||
|
||||
import Data.Aeson (object, (.=))
|
||||
import StrongPath (relfile)
|
||||
import Data.Aeson.Types (Pair)
|
||||
import StrongPath (File', Path', Rel', reldir, relfile, (</>))
|
||||
import Wasp.AppSpec (AppSpec)
|
||||
import qualified Wasp.AppSpec.App as AS.App
|
||||
import qualified Wasp.AppSpec.App.Auth as AS.Auth
|
||||
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.Monad (Generator)
|
||||
import Wasp.Generator.WebAppGenerator.Auth.Common (getOnAuthSucceededRedirectToOrDefault)
|
||||
import Wasp.Generator.WebAppGenerator.Auth.LocalAuthG (genLocalAuth)
|
||||
import Wasp.Generator.WebAppGenerator.Auth.OAuthAuthG (genOAuthAuth)
|
||||
import Wasp.Generator.WebAppGenerator.Common as C
|
||||
@ -26,6 +30,7 @@ genAuth spec =
|
||||
genCreateAuthRequiredPage auth,
|
||||
genUserHelpers
|
||||
]
|
||||
<++> genAuthForms auth
|
||||
<++> genLocalAuth auth
|
||||
<++> genOAuthAuth auth
|
||||
Nothing -> return []
|
||||
@ -50,5 +55,54 @@ genCreateAuthRequiredPage auth =
|
||||
genUseAuth :: Generator FileDraft
|
||||
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 = return $ C.mkTmplFd (C.asTmplFile [relfile|src/auth/helpers/user.ts|])
|
||||
|
Loading…
Reference in New Issue
Block a user