mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-23 19:29:17 +03:00
Auth UI integration with email & polish (#1107)
This commit is contained in:
parent
809eb90386
commit
5be9d1fa5e
Binary file not shown.
Before Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
@ -1,47 +0,0 @@
|
||||
{{={= =}=}}
|
||||
import { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import { login } from '../actions/login';
|
||||
import { errorMessage } from '../../../utils.js'
|
||||
|
||||
export const LoginForm = () => {
|
||||
const history = useHistory()
|
||||
|
||||
const [emailFieldVal, setEmailFieldVal] = useState('')
|
||||
const [passwordFieldVal, setPasswordFieldVal] = useState('')
|
||||
|
||||
const handleLogin = async (event) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await login({
|
||||
email: emailFieldVal,
|
||||
password: passwordFieldVal,
|
||||
})
|
||||
history.push('{= onAuthSucceededRedirectTo =}')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
window.alert(errorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleLogin} className="login-form auth-form">
|
||||
<h2>Email</h2>
|
||||
<input
|
||||
type="email"
|
||||
value={emailFieldVal}
|
||||
onChange={e => setEmailFieldVal(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>
|
||||
)
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
{{={= =}=}}
|
||||
import { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import { signup } from '../actions/signup'
|
||||
import { login } from '../actions/login'
|
||||
import { errorMessage } from '../../../utils.js'
|
||||
|
||||
export const SignupForm = () => {
|
||||
const history = useHistory()
|
||||
|
||||
const [emailFieldVal, setEmailFieldVal] = useState('')
|
||||
const [passwordFieldVal, setPasswordFieldVal] = useState('')
|
||||
|
||||
const handleSignup = async (event) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await signup({ email: emailFieldVal, password: passwordFieldVal })
|
||||
await login ({
|
||||
email: emailFieldVal,
|
||||
password: passwordFieldVal,
|
||||
})
|
||||
|
||||
setEmailFieldVal('')
|
||||
setPasswordFieldVal('')
|
||||
|
||||
history.push('{= onAuthSucceededRedirectTo =}')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
window.alert(errorMessage(err))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSignup} className='signup-form auth-form'>
|
||||
<h2>Email</h2>
|
||||
<input
|
||||
type="email"
|
||||
value={emailFieldVal}
|
||||
onChange={e => setEmailFieldVal(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>
|
||||
)
|
||||
}
|
@ -3,6 +3,3 @@ export { signup } from './actions/signup';
|
||||
export { requestPasswordReset } from './actions/passwordReset';
|
||||
export { resetPassword } from './actions/passwordReset';
|
||||
export { verifyEmail } from './actions/verifyEmail';
|
||||
|
||||
export { SignupForm } from './components/Signup';
|
||||
export { LoginForm } from './components/Login';
|
||||
|
@ -1,23 +1,26 @@
|
||||
{{={= =}=}}
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { createStitches, createTheme } from '@stitches/react'
|
||||
import { useState, FormEvent, useEffect, useCallback } from 'react'
|
||||
import { useHistory, useLocation } from 'react-router-dom'
|
||||
import { createTheme } from '@stitches/react'
|
||||
|
||||
import { errorMessage } from '../../utils.js'
|
||||
{=# isUsernameAndPasswordAuthEnabled =}
|
||||
import signup from '../signup.js'
|
||||
import login from '../login.js'
|
||||
{=/ isUsernameAndPasswordAuthEnabled =}
|
||||
{=# isEmailAuthEnabled =}
|
||||
import { signup } from '../email/actions/signup.js'
|
||||
import { login } from '../email/actions/login.js'
|
||||
import { requestPasswordReset, resetPassword } from '../email/actions/passwordReset.js'
|
||||
import { verifyEmail } from '../email/actions/verifyEmail.js'
|
||||
{=/ isEmailAuthEnabled =}
|
||||
{=# isExternalAuthEnabled =}
|
||||
import * as SocialIcons from './SocialIcons'
|
||||
import { SocialButton } from './SocialButton';
|
||||
{=/ isExternalAuthEnabled =}
|
||||
|
||||
import config from '../../config.js'
|
||||
import { styled, css } from '../../stitches.config'
|
||||
|
||||
const socialButtonsContainerStyle = {
|
||||
maxWidth: '20rem'
|
||||
}
|
||||
import { styled } from '../../stitches.config'
|
||||
import { State, CustomizationOptions } from './types'
|
||||
|
||||
const logoStyle = {
|
||||
height: '3rem'
|
||||
@ -36,7 +39,6 @@ const HeaderText = styled('h2', {
|
||||
|
||||
const SocialAuth = styled('div', {
|
||||
marginTop: '1.5rem'
|
||||
|
||||
})
|
||||
|
||||
const SocialAuthLabel = styled('div', {
|
||||
@ -73,31 +75,6 @@ const SocialAuthButtons = styled('div', {
|
||||
}
|
||||
})
|
||||
|
||||
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'
|
||||
@ -160,6 +137,13 @@ const FormInput = styled('input', {
|
||||
borderColor: '$gray700',
|
||||
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
backgroundColor: '$gray400',
|
||||
borderColor: '$gray400',
|
||||
color: '$gray500',
|
||||
},
|
||||
|
||||
borderRadius: '0.375rem',
|
||||
width: '100%',
|
||||
@ -175,7 +159,6 @@ const SubmitButton = styled('button', {
|
||||
justifyContent: 'center',
|
||||
|
||||
width: '100%',
|
||||
borderRadius: '0.375rem',
|
||||
borderWidth: '1px',
|
||||
borderColor: '$brand',
|
||||
backgroundColor: '$brand',
|
||||
@ -194,39 +177,90 @@ const SubmitButton = styled('button', {
|
||||
backgroundColor: '$brandAccent',
|
||||
borderColor: '$brandAccent',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
backgroundColor: '$gray400',
|
||||
borderColor: '$gray400',
|
||||
color: '$gray500',
|
||||
},
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDuration: '100ms'
|
||||
})
|
||||
|
||||
const Message = styled('div', {
|
||||
padding: '0.5rem 0.75rem',
|
||||
borderRadius: '0.375rem',
|
||||
marginTop: '1rem',
|
||||
background: '$gray400',
|
||||
})
|
||||
|
||||
const ErrorMessage = styled(Message, {
|
||||
background: '$errorBackground',
|
||||
color: '$errorText',
|
||||
})
|
||||
|
||||
const SuccessMessage = styled(Message, {
|
||||
background: '$successBackground',
|
||||
color: '$successText',
|
||||
})
|
||||
|
||||
{=# isGoogleAuthEnabled =}
|
||||
const googleSignInUrl = `${config.apiUrl}{= googleSignInPath =}`
|
||||
{=/ isGoogleAuthEnabled =}
|
||||
{=# isGitHubAuthEnabled =}
|
||||
const gitHubSignInUrl = `${config.apiUrl}{= gitHubSignInPath =}`
|
||||
{=/ isGitHubAuthEnabled =}
|
||||
|
||||
// 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('')
|
||||
|
||||
function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
|
||||
state: State;
|
||||
} & CustomizationOptions) {
|
||||
const isLogin = state === "login";
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
{=# isAnyPasswordBasedAuthEnabled =}
|
||||
const history = useHistory();
|
||||
const onErrorHandler = (error) => {
|
||||
setErrorMessage(error.message)
|
||||
};
|
||||
{=/ isAnyPasswordBasedAuthEnabled =}
|
||||
{=# isUsernameAndPasswordAuthEnabled =}
|
||||
const { handleSubmit, usernameFieldVal, passwordFieldVal, setUsernameFieldVal, setPasswordFieldVal } = useUsernameAndPassword({
|
||||
isLogin,
|
||||
onError: onErrorHandler,
|
||||
onSuccess() {
|
||||
// Redirect to configured page, defaults to /.
|
||||
history.push('{= onAuthSucceededRedirectTo =}')
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
window.alert(errorMessage(err))
|
||||
},
|
||||
});
|
||||
{=/ isUsernameAndPasswordAuthEnabled =}
|
||||
{=# isEmailAuthEnabled =}
|
||||
const { handleSubmit, emailFieldVal, passwordFieldVal, setEmailFieldVal, setPasswordFieldVal } = useEmail({
|
||||
isLogin,
|
||||
onError: onErrorHandler,
|
||||
showEmailVerificationPending() {
|
||||
setSuccessMessage(`You've signed up successfully! Check your email for the confirmation link.`)
|
||||
},
|
||||
onLoginSuccess() {
|
||||
// Redirect to configured page, defaults to /.
|
||||
history.push('{= onAuthSucceededRedirectTo =}')
|
||||
}
|
||||
});
|
||||
{=/ isEmailAuthEnabled =}
|
||||
{=# isAnyPasswordBasedAuthEnabled =}
|
||||
async function onSubmit (event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
setSuccessMessage(null);
|
||||
try {
|
||||
await handleSubmit();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
{=/ isAnyPasswordBasedAuthEnabled =}
|
||||
|
||||
// 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
|
||||
@ -234,19 +268,18 @@ const Auth = ({ isLogin, appearance, logo, socialLayout } :
|
||||
const customTheme = createTheme(appearance)
|
||||
|
||||
const cta = isLogin ? 'Log in' : 'Sign up'
|
||||
const title = isLogin ? 'Log in to your account' : 'Create a new account'
|
||||
const titles: Record<State, string> = {
|
||||
login: 'Log in to your account',
|
||||
signup: 'Create a new account',
|
||||
"forgot-password": "Forgot your password?",
|
||||
"reset-password": "Reset your password",
|
||||
"verify-email": "Email verification",
|
||||
}
|
||||
const title = titles[state]
|
||||
|
||||
const socialButtonsDirection = socialLayout === 'vertical' ? 'vertical' : 'horizontal'
|
||||
|
||||
return (
|
||||
<Container className={customTheme}>
|
||||
<div>
|
||||
{logo && (
|
||||
<img style={logoStyle} src={logo} alt='Your Company' />
|
||||
)}
|
||||
<HeaderText>{title}</HeaderText>
|
||||
</div>
|
||||
|
||||
const loginSignupForm = (<>
|
||||
{=# isExternalAuthEnabled =}
|
||||
<SocialAuth>
|
||||
<SocialAuthLabel>{cta} with</SocialAuthLabel>
|
||||
@ -262,7 +295,7 @@ const Auth = ({ isLogin, appearance, logo, socialLayout } :
|
||||
</SocialAuth>
|
||||
{=/ isExternalAuthEnabled =}
|
||||
|
||||
{=# areBothExternalAndUsernameAndPasswordAuthEnabled =}
|
||||
{=# areBothSocialAndPasswordBasedAuthEnabled =}
|
||||
<OrContinueWith>
|
||||
<OrContinueWithLineContainer>
|
||||
<OrContinueWithLine/>
|
||||
@ -271,36 +304,298 @@ const Auth = ({ isLogin, appearance, logo, socialLayout } :
|
||||
<OrContinueWithText>Or continue with</OrContinueWithText>
|
||||
</OrContinueWithTextContainer>
|
||||
</OrContinueWith>
|
||||
{=/ areBothExternalAndUsernameAndPasswordAuthEnabled =}
|
||||
|
||||
{=# isUsernameAndPasswordAuthEnabled =}
|
||||
<UserPassForm onSubmit={handleSubmit}>
|
||||
{=/ areBothSocialAndPasswordBasedAuthEnabled =}
|
||||
{=# isAnyPasswordBasedAuthEnabled =}
|
||||
<UserPassForm onSubmit={onSubmit}>
|
||||
{=# isUsernameAndPasswordAuthEnabled =}
|
||||
<FormItemGroup>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormInput
|
||||
type="text"
|
||||
required
|
||||
value={usernameFieldVal}
|
||||
onChange={e => setUsernameFieldVal(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormItemGroup>
|
||||
|
||||
{=/ isUsernameAndPasswordAuthEnabled =}
|
||||
{=# isEmailAuthEnabled =}
|
||||
<FormItemGroup>
|
||||
<FormLabel>E-mail</FormLabel>
|
||||
<FormInput
|
||||
type="email"
|
||||
required
|
||||
value={emailFieldVal}
|
||||
onChange={e => setEmailFieldVal(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormItemGroup>
|
||||
{=/ isEmailAuthEnabled =}
|
||||
<FormItemGroup>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormInput
|
||||
type="password"
|
||||
required
|
||||
value={passwordFieldVal}
|
||||
onChange={e => setPasswordFieldVal(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormItemGroup>
|
||||
|
||||
<FormItemGroup>
|
||||
<SubmitButton type="submit">{cta}</SubmitButton>
|
||||
<SubmitButton type="submit" disabled={isLoading}>{cta}</SubmitButton>
|
||||
</FormItemGroup>
|
||||
</UserPassForm>
|
||||
{=/ isUsernameAndPasswordAuthEnabled =}
|
||||
|
||||
{=/ isAnyPasswordBasedAuthEnabled =}
|
||||
</>)
|
||||
|
||||
return (
|
||||
<Container className={customTheme}>
|
||||
<div>
|
||||
{logo && (
|
||||
<img style={logoStyle} src={logo} alt='Your Company' />
|
||||
)}
|
||||
<HeaderText>{title}</HeaderText>
|
||||
</div>
|
||||
|
||||
{errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
|
||||
{successMessage && <SuccessMessage>{successMessage}</SuccessMessage>}
|
||||
{(state === 'login' || state === 'signup') && loginSignupForm}
|
||||
{=# isEmailAuthEnabled =}
|
||||
{state === 'forgot-password' && (<ForgotPasswordForm
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
setErrorMessage={setErrorMessage}
|
||||
setSuccessMessage={setSuccessMessage}
|
||||
/>)}
|
||||
{state === 'reset-password' && (<ResetPasswordForm
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
setErrorMessage={setErrorMessage}
|
||||
setSuccessMessage={setSuccessMessage}
|
||||
/>)}
|
||||
{state === 'verify-email' && (<VerifyEmailForm
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
setErrorMessage={setErrorMessage}
|
||||
setSuccessMessage={setSuccessMessage}
|
||||
/>)}
|
||||
{=/ isEmailAuthEnabled =}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Auth
|
||||
export default Auth;
|
||||
|
||||
{=# isEmailAuthEnabled =}
|
||||
const ForgotPasswordForm = ({ isLoading, setIsLoading, setErrorMessage, setSuccessMessage }) => {
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
setIsLoading(true)
|
||||
setErrorMessage(null)
|
||||
setSuccessMessage(null)
|
||||
try {
|
||||
await requestPasswordReset({ email })
|
||||
setSuccessMessage('Check your email for a password reset link.')
|
||||
setEmail('')
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserPassForm onSubmit={onSubmit}>
|
||||
<FormItemGroup>
|
||||
<FormLabel>E-mail</FormLabel>
|
||||
<FormInput
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormItemGroup>
|
||||
<FormItemGroup>
|
||||
<SubmitButton type="submit" disabled={isLoading}>Send password reset email</SubmitButton>
|
||||
</FormItemGroup>
|
||||
</UserPassForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ResetPasswordForm = ({ isLoading, setIsLoading, setErrorMessage, setSuccessMessage }) => {
|
||||
const location = useLocation()
|
||||
const token = new URLSearchParams(location.search).get('token')
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState('')
|
||||
|
||||
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!token) {
|
||||
setErrorMessage('The token is missing from the URL. Please check the link you received in your email.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!password || password !== passwordConfirmation) {
|
||||
setErrorMessage("Passwords don't match!")
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setErrorMessage(null)
|
||||
setSuccessMessage(null)
|
||||
try {
|
||||
await resetPassword({ password, token })
|
||||
setSuccessMessage('Your password has been reset.')
|
||||
setPassword('')
|
||||
setPasswordConfirmation('')
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserPassForm onSubmit={onSubmit}>
|
||||
<FormItemGroup>
|
||||
<FormLabel>New password</FormLabel>
|
||||
<FormInput
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormItemGroup>
|
||||
<FormItemGroup>
|
||||
<FormLabel>Confirm new password</FormLabel>
|
||||
<FormInput
|
||||
type="password"
|
||||
required
|
||||
value={passwordConfirmation}
|
||||
onChange={e => setPasswordConfirmation(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</FormItemGroup>
|
||||
<FormItemGroup>
|
||||
<SubmitButton type="submit" disabled={isLoading}>Reset password</SubmitButton>
|
||||
</FormItemGroup>
|
||||
</UserPassForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const VerifyEmailForm = ({ isLoading, setIsLoading, setErrorMessage, setSuccessMessage }) => {
|
||||
const location = useLocation()
|
||||
const token = new URLSearchParams(location.search).get('token')
|
||||
|
||||
const submitForm = useCallback(async () => {
|
||||
if (!token) {
|
||||
setErrorMessage('The token is missing from the URL. Please check the link you received in your email.')
|
||||
return
|
||||
}
|
||||
setIsLoading(true)
|
||||
setErrorMessage(null)
|
||||
setSuccessMessage(null)
|
||||
try {
|
||||
await verifyEmail({ token })
|
||||
setSuccessMessage('Your email has been verified. You can now log in.')
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
submitForm()
|
||||
}, [location])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <Message>Verifying email...</Message>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{=/ isEmailAuthEnabled =}
|
||||
|
||||
{=# isUsernameAndPasswordAuthEnabled =}
|
||||
function useUsernameAndPassword({
|
||||
onError,
|
||||
onSuccess,
|
||||
isLogin,
|
||||
}: {
|
||||
onError: (error: Error) => void;
|
||||
onSuccess: () => void;
|
||||
isLogin: boolean;
|
||||
}) {
|
||||
const [usernameFieldVal, setUsernameFieldVal] = useState('')
|
||||
const [passwordFieldVal, setPasswordFieldVal] = useState('')
|
||||
|
||||
async function handleSubmit () {
|
||||
try {
|
||||
if (!isLogin) {
|
||||
await signup({ username: usernameFieldVal, password: passwordFieldVal })
|
||||
}
|
||||
await login (usernameFieldVal, passwordFieldVal)
|
||||
|
||||
setUsernameFieldVal('')
|
||||
setPasswordFieldVal('')
|
||||
onSuccess()
|
||||
} catch (err: unknown) {
|
||||
onError(err as Error)
|
||||
}
|
||||
}
|
||||
|
||||
return { handleSubmit, usernameFieldVal, passwordFieldVal, setUsernameFieldVal, setPasswordFieldVal }
|
||||
}
|
||||
{=/ isUsernameAndPasswordAuthEnabled =}
|
||||
{=# isEmailAuthEnabled =}
|
||||
function useEmail({
|
||||
onError,
|
||||
showEmailVerificationPending,
|
||||
onLoginSuccess,
|
||||
isLogin,
|
||||
}: {
|
||||
onError: (error: Error) => void;
|
||||
showEmailVerificationPending: () => void;
|
||||
onLoginSuccess: () => void;
|
||||
isLogin: boolean;
|
||||
}) {
|
||||
const [emailFieldVal, setEmailFieldVal] = useState('')
|
||||
const [passwordFieldVal, setPasswordFieldVal] = useState('')
|
||||
|
||||
async function handleSubmit () {
|
||||
try {
|
||||
if (isLogin) {
|
||||
await login({ email: emailFieldVal, password: passwordFieldVal })
|
||||
onLoginSuccess()
|
||||
} else {
|
||||
await signup({ email: emailFieldVal, password: passwordFieldVal })
|
||||
{=# isEmailVerificationRequired =}
|
||||
showEmailVerificationPending()
|
||||
{=/ isEmailVerificationRequired =}
|
||||
{=^ isEmailVerificationRequired =}
|
||||
await login ({ email: emailFieldVal, password: passwordFieldVal})
|
||||
onLoginSuccess()
|
||||
{=/ isEmailVerificationRequired =}
|
||||
}
|
||||
|
||||
setEmailFieldVal('')
|
||||
setPasswordFieldVal('')
|
||||
} catch (err: unknown) {
|
||||
onError(err as Error)
|
||||
}
|
||||
}
|
||||
|
||||
return { handleSubmit, emailFieldVal, passwordFieldVal, setEmailFieldVal, setPasswordFieldVal }
|
||||
}
|
||||
{=/ isEmailAuthEnabled =}
|
||||
|
@ -0,0 +1,13 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
export function ForgotPasswordForm ({ appearance, logo, socialLayout }: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.ForgotPassword}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
{{={= =}=}}
|
||||
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,13 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
export function LoginForm ({ appearance, logo, socialLayout }: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.Login}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
export function ResetPasswordForm ({ appearance, logo, socialLayout }: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.ResetPassword}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
{{={= =}=}}
|
||||
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,13 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
export function SignupForm ({ appearance, logo, socialLayout }: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.Signup}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { styled } from '../../stitches.config'
|
||||
|
||||
export 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: '0.5rem 0.75rem',
|
||||
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
'&:visited': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: '$gray500',
|
||||
color: 'inherit',
|
||||
},
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDuration: '100ms'
|
||||
})
|
@ -8,11 +8,11 @@ const defaultStyles = css({
|
||||
export const Google = () => (
|
||||
<svg
|
||||
className={defaultStyles()}
|
||||
ariaHidden="true"
|
||||
aria-hidden="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">
|
||||
<g id="google" fill="#000000" fillRule="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>
|
||||
@ -23,7 +23,7 @@ export const Google = () => (
|
||||
export const GitHub = () => (
|
||||
<svg
|
||||
className={defaultStyles()}
|
||||
ariaHidden="true"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd"
|
||||
|
@ -0,0 +1,13 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
export function VerifyEmailForm ({ appearance, logo, socialLayout }: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.VerifyEmail}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { createTheme } from '@stitches/react'
|
||||
|
||||
export enum State {
|
||||
Login = "login",
|
||||
Signup = "signup",
|
||||
ForgotPassword = "forgot-password",
|
||||
ResetPassword = "reset-password",
|
||||
VerifyEmail = "verify-email",
|
||||
}
|
||||
|
||||
export type CustomizationOptions = {
|
||||
logo?: string;
|
||||
socialLayout?: "horizontal" | "vertical";
|
||||
appearance?: Parameters<typeof createTheme>[0];
|
||||
}
|
@ -1,45 +1,15 @@
|
||||
{{={= =}=}}
|
||||
|
||||
import config from '../../config.js'
|
||||
import { SocialButton } from '../forms/SocialButton'
|
||||
import * as SocialIcons from '../forms/SocialIcons'
|
||||
|
||||
export const signInUrl = `${config.apiUrl}{= signInPath =}`
|
||||
export const logoUrl = '/images/{= iconName =}'
|
||||
|
||||
const containerStyle = {
|
||||
border: '2px solid #cbd5e1',
|
||||
margin: 0,
|
||||
cursor: 'pointer',
|
||||
borderRadius: '.375rem',
|
||||
backgroundColor: '#f8fafc',
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
|
||||
outline: '2px solid transparent',
|
||||
outlineOffset: '2px',
|
||||
}
|
||||
|
||||
const linkStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
textDecoration: 'none',
|
||||
color: '#1e293b',
|
||||
paddingLeft: '1.5rem',
|
||||
paddingRight: '1.5rem',
|
||||
paddingTop: '.75rem',
|
||||
paddingBottom: '.75rem',
|
||||
}
|
||||
|
||||
const logoStyle = {
|
||||
maxHeight: '24px',
|
||||
marginRight: '0.75rem'
|
||||
}
|
||||
|
||||
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>
|
||||
<SocialButton href={signInUrl}>
|
||||
<SocialIcons.{= displayName =} />
|
||||
</SocialButton>
|
||||
)
|
||||
}
|
||||
|
@ -11,11 +11,18 @@ export const {
|
||||
gray600: '#d1d5db',
|
||||
gray500: 'gainsboro',
|
||||
gray400: '#f0f0f0',
|
||||
red: '#FED7D7',
|
||||
green: '#C6F6D5',
|
||||
|
||||
brand: '$waspYellow',
|
||||
brandAccent: '#ffdb46',
|
||||
errorBackground: '$red',
|
||||
errorText: '#2D3748',
|
||||
successBackground: '$green',
|
||||
successText: '#2D3748',
|
||||
|
||||
submitButtonText: 'black',
|
||||
|
||||
submitButtonText: 'black'
|
||||
},
|
||||
fontSizes: {
|
||||
sm: '0.875rem'
|
||||
|
@ -137,7 +137,9 @@ async function sendEmailAndLogTimestamp(
|
||||
} catch (e) {
|
||||
rethrowError(e);
|
||||
}
|
||||
await emailSender.send(content);
|
||||
emailSender.send(content).catch((e) => {
|
||||
console.error(`Failed to send email for ${field}`, e);
|
||||
});
|
||||
}
|
||||
|
||||
export function isEmailResendAllowed(
|
||||
|
@ -76,7 +76,6 @@ waspComplexTest/.wasp/out/web-app/index.html
|
||||
waspComplexTest/.wasp/out/web-app/netlify.toml
|
||||
waspComplexTest/.wasp/out/web-app/package.json
|
||||
waspComplexTest/.wasp/out/web-app/public/favicon.ico
|
||||
waspComplexTest/.wasp/out/web-app/public/images/google-logo-icon.png
|
||||
waspComplexTest/.wasp/out/web-app/public/manifest.json
|
||||
waspComplexTest/.wasp/out/web-app/scripts/universal/validators.mjs
|
||||
waspComplexTest/.wasp/out/web-app/scripts/validate-env.mjs
|
||||
@ -86,9 +85,14 @@ 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/ForgotPassword.tsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/forms/Login.tsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/forms/ResetPassword.tsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/forms/Signup.tsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/forms/SocialButton.tsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/forms/SocialIcons.tsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/forms/VerifyEmail.tsx
|
||||
waspComplexTest/.wasp/out/web-app/src/auth/forms/types.ts
|
||||
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
|
||||
|
@ -524,13 +524,6 @@
|
||||
],
|
||||
"1481f03584b46b63182c1f8d26e61a95ce4d8fae032b2b4f3fe5e00ab7e96c23"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/public/images/google-logo-icon.png"
|
||||
],
|
||||
"e2087f585c3b213ba537a56c8bc8e6134c69d6fa1a5728d306df56d697b4e7ab"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
@ -592,35 +585,70 @@
|
||||
"file",
|
||||
"web-app/src/auth/forms/Auth.tsx"
|
||||
],
|
||||
"3c15faec2c79eafeb0e796bb849c04eaf3c6c233636b96de6765352670816d97"
|
||||
"f62b429c2a3749e8147198184d4612de7d1a82d1370696487b1aa65eb2e4b5b9"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/Login.jsx"
|
||||
"web-app/src/auth/forms/ForgotPassword.tsx"
|
||||
],
|
||||
"cc899de975012417da95738016650b4d7416475a50fd730bae08c10b99c58d52"
|
||||
"39eda745fc19798d235cbecf7e755ab1050187d99775dfb06304b06804e5c7fc"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/Signup.jsx"
|
||||
"web-app/src/auth/forms/Login.tsx"
|
||||
],
|
||||
"413ba86964bbe7735ed102618d5c4efc8187eae2feb73cae72f2ddfda55a53b3"
|
||||
"788a896a5717cd6e082db23cd5e1f4f46816253e24d6bbe60b2d64fb5a208d3c"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/ResetPassword.tsx"
|
||||
],
|
||||
"be24461d42102d53c028713146054029950137e79e27906a166a16cf9dd64add"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/Signup.tsx"
|
||||
],
|
||||
"bea6a9756a546820ccd240aabfa3db3d14faf655f7681c7b9ca0791677556c3a"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/SocialButton.tsx"
|
||||
],
|
||||
"fb14b39e617bf0cb7eec96428fd2882c2fbcda8bebd453ed00866b3203420043"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/SocialIcons.tsx"
|
||||
],
|
||||
"4e89c92b63539e28fe0d084b1fdcf870a426eb228ce37c57b374597005ff235c"
|
||||
"18ce4ed1aec9fb0e71d4ce8376d5c42fcec1008d9b16d92dd18d51f34ee189a6"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/VerifyEmail.tsx"
|
||||
],
|
||||
"2cb996958b1332f383af1265a9fa7dd30d71aaef371cd2ac471f9feb44158568"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/forms/types.ts"
|
||||
],
|
||||
"601917a95f8218f9db51148bf852a21925ea9806729fe9f8a65a96f84e8ba22f"
|
||||
],
|
||||
[
|
||||
[
|
||||
"file",
|
||||
"web-app/src/auth/helpers/Google.jsx"
|
||||
],
|
||||
"c3d12e7a6cc1305c9e411d522e6aa8411d036337791ed0fc37e6048fe2d90cfd"
|
||||
"c6677ed5052cf7dc9aca312935b48dd59eaf22420d581ac1b79c01070d3c109e"
|
||||
],
|
||||
[
|
||||
[
|
||||
@ -802,7 +830,7 @@
|
||||
"file",
|
||||
"web-app/src/stitches.config.js"
|
||||
],
|
||||
"25680fb34bd5200a8416e0a5ebfe8cb0e4240e7848ed3a065c56d1b55d7edd09"
|
||||
"7de37836b80021870f286ff14d275e2ca7a1c2aa113ba5a5624ed0c77e178f76"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
@ -1,16 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { createStitches, createTheme } from '@stitches/react'
|
||||
import { useState, FormEvent, useEffect, useCallback } from 'react'
|
||||
import { useHistory, useLocation } from 'react-router-dom'
|
||||
import { createTheme } from '@stitches/react'
|
||||
|
||||
import { errorMessage } from '../../utils.js'
|
||||
import * as SocialIcons from './SocialIcons'
|
||||
import { SocialButton } from './SocialButton';
|
||||
|
||||
import config from '../../config.js'
|
||||
import { styled, css } from '../../stitches.config'
|
||||
|
||||
const socialButtonsContainerStyle = {
|
||||
maxWidth: '20rem'
|
||||
}
|
||||
import { styled } from '../../stitches.config'
|
||||
import { State, CustomizationOptions } from './types'
|
||||
|
||||
const logoStyle = {
|
||||
height: '3rem'
|
||||
@ -29,7 +26,6 @@ const HeaderText = styled('h2', {
|
||||
|
||||
const SocialAuth = styled('div', {
|
||||
marginTop: '1.5rem'
|
||||
|
||||
})
|
||||
|
||||
const SocialAuthLabel = styled('div', {
|
||||
@ -66,31 +62,6 @@ const SocialAuthButtons = styled('div', {
|
||||
}
|
||||
})
|
||||
|
||||
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'
|
||||
@ -153,6 +124,13 @@ const FormInput = styled('input', {
|
||||
borderColor: '$gray700',
|
||||
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
backgroundColor: '$gray400',
|
||||
borderColor: '$gray400',
|
||||
color: '$gray500',
|
||||
},
|
||||
|
||||
borderRadius: '0.375rem',
|
||||
width: '100%',
|
||||
@ -168,7 +146,6 @@ const SubmitButton = styled('button', {
|
||||
justifyContent: 'center',
|
||||
|
||||
width: '100%',
|
||||
borderRadius: '0.375rem',
|
||||
borderWidth: '1px',
|
||||
borderColor: '$brand',
|
||||
backgroundColor: '$brand',
|
||||
@ -187,39 +164,43 @@ const SubmitButton = styled('button', {
|
||||
backgroundColor: '$brandAccent',
|
||||
borderColor: '$brandAccent',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
backgroundColor: '$gray400',
|
||||
borderColor: '$gray400',
|
||||
color: '$gray500',
|
||||
},
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDuration: '100ms'
|
||||
})
|
||||
|
||||
const Message = styled('div', {
|
||||
padding: '0.5rem 0.75rem',
|
||||
borderRadius: '0.375rem',
|
||||
marginTop: '1rem',
|
||||
background: '$gray400',
|
||||
})
|
||||
|
||||
const ErrorMessage = styled(Message, {
|
||||
background: '$errorBackground',
|
||||
color: '$errorText',
|
||||
})
|
||||
|
||||
const SuccessMessage = styled(Message, {
|
||||
background: '$successBackground',
|
||||
color: '$successText',
|
||||
})
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
function Auth ({ state, appearance, logo, socialLayout = 'horizontal' }: {
|
||||
state: State;
|
||||
} & CustomizationOptions) {
|
||||
const isLogin = state === "login";
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 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
|
||||
@ -227,10 +208,28 @@ const Auth = ({ isLogin, appearance, logo, socialLayout } :
|
||||
const customTheme = createTheme(appearance)
|
||||
|
||||
const cta = isLogin ? 'Log in' : 'Sign up'
|
||||
const title = isLogin ? 'Log in to your account' : 'Create a new account'
|
||||
const titles: Record<State, string> = {
|
||||
login: 'Log in to your account',
|
||||
signup: 'Create a new account',
|
||||
"forgot-password": "Forgot your password?",
|
||||
"reset-password": "Reset your password",
|
||||
"verify-email": "Email verification",
|
||||
}
|
||||
const title = titles[state]
|
||||
|
||||
const socialButtonsDirection = socialLayout === 'vertical' ? 'vertical' : 'horizontal'
|
||||
|
||||
const loginSignupForm = (<>
|
||||
<SocialAuth>
|
||||
<SocialAuthLabel>{cta} with</SocialAuthLabel>
|
||||
<SocialAuthButtons gap='large' direction={socialButtonsDirection}>
|
||||
<SocialButton href={googleSignInUrl}><SocialIcons.Google/></SocialButton>
|
||||
|
||||
</SocialAuthButtons>
|
||||
</SocialAuth>
|
||||
|
||||
</>)
|
||||
|
||||
return (
|
||||
<Container className={customTheme}>
|
||||
<div>
|
||||
@ -240,18 +239,13 @@ const Auth = ({ isLogin, appearance, logo, socialLayout } :
|
||||
<HeaderText>{title}</HeaderText>
|
||||
</div>
|
||||
|
||||
<SocialAuth>
|
||||
<SocialAuthLabel>{cta} with</SocialAuthLabel>
|
||||
<SocialAuthButtons gap='large' direction={socialButtonsDirection}>
|
||||
<SocialButton href={googleSignInUrl}><SocialIcons.Google/></SocialButton>
|
||||
|
||||
</SocialAuthButtons>
|
||||
</SocialAuth>
|
||||
|
||||
|
||||
|
||||
{errorMessage && <ErrorMessage>{errorMessage}</ErrorMessage>}
|
||||
{successMessage && <SuccessMessage>{successMessage}</SuccessMessage>}
|
||||
{(state === 'login' || state === 'signup') && loginSignupForm}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Auth
|
||||
export default Auth;
|
||||
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
export function ForgotPasswordForm ({ appearance, logo, socialLayout }: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.ForgotPassword}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
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,13 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
export function LoginForm ({ appearance, logo, socialLayout }: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.Login}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
export function ResetPasswordForm ({ appearance, logo, socialLayout }: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.ResetPassword}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
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,13 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
export function SignupForm ({ appearance, logo, socialLayout }: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.Signup}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { styled } from '../../stitches.config'
|
||||
|
||||
export 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: '0.5rem 0.75rem',
|
||||
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
'&:visited': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: '$gray500',
|
||||
color: 'inherit',
|
||||
},
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDuration: '100ms'
|
||||
})
|
@ -8,11 +8,11 @@ const defaultStyles = css({
|
||||
export const Google = () => (
|
||||
<svg
|
||||
className={defaultStyles()}
|
||||
ariaHidden="true"
|
||||
aria-hidden="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">
|
||||
<g id="google" fill="#000000" fillRule="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>
|
||||
@ -23,7 +23,7 @@ export const Google = () => (
|
||||
export const GitHub = () => (
|
||||
<svg
|
||||
className={defaultStyles()}
|
||||
ariaHidden="true"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd"
|
||||
|
@ -0,0 +1,13 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
export function VerifyEmailForm ({ appearance, logo, socialLayout }: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.VerifyEmail}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { createTheme } from '@stitches/react'
|
||||
|
||||
export enum State {
|
||||
Login = "login",
|
||||
Signup = "signup",
|
||||
ForgotPassword = "forgot-password",
|
||||
ResetPassword = "reset-password",
|
||||
VerifyEmail = "verify-email",
|
||||
}
|
||||
|
||||
export type CustomizationOptions = {
|
||||
logo?: string;
|
||||
socialLayout?: "horizontal" | "vertical";
|
||||
appearance?: Parameters<typeof createTheme>[0];
|
||||
}
|
@ -1,44 +1,14 @@
|
||||
|
||||
import config from '../../config.js'
|
||||
import { SocialButton } from '../forms/SocialButton'
|
||||
import * as SocialIcons from '../forms/SocialIcons'
|
||||
|
||||
export const signInUrl = `${config.apiUrl}/auth/google/login`
|
||||
export const logoUrl = '/images/google-logo-icon.png'
|
||||
|
||||
const containerStyle = {
|
||||
border: '2px solid #cbd5e1',
|
||||
margin: 0,
|
||||
cursor: 'pointer',
|
||||
borderRadius: '.375rem',
|
||||
backgroundColor: '#f8fafc',
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
|
||||
outline: '2px solid transparent',
|
||||
outlineOffset: '2px',
|
||||
}
|
||||
|
||||
const linkStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
textDecoration: 'none',
|
||||
color: '#1e293b',
|
||||
paddingLeft: '1.5rem',
|
||||
paddingRight: '1.5rem',
|
||||
paddingTop: '.75rem',
|
||||
paddingBottom: '.75rem',
|
||||
}
|
||||
|
||||
const logoStyle = {
|
||||
maxHeight: '24px',
|
||||
marginRight: '0.75rem'
|
||||
}
|
||||
|
||||
export function SignInButton() {
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<a href={signInUrl} style={linkStyle}>
|
||||
<img alt="Google Icon" src={logoUrl} style={logoStyle} />
|
||||
<span>Log in with Google</span>
|
||||
</a>
|
||||
</div>
|
||||
<SocialButton href={signInUrl}>
|
||||
<SocialIcons.Google />
|
||||
</SocialButton>
|
||||
)
|
||||
}
|
||||
|
@ -11,11 +11,18 @@ export const {
|
||||
gray600: '#d1d5db',
|
||||
gray500: 'gainsboro',
|
||||
gray400: '#f0f0f0',
|
||||
red: '#FED7D7',
|
||||
green: '#C6F6D5',
|
||||
|
||||
brand: '$waspYellow',
|
||||
brandAccent: '#ffdb46',
|
||||
errorBackground: '$red',
|
||||
errorText: '#2D3748',
|
||||
successBackground: '$green',
|
||||
successText: '#2D3748',
|
||||
|
||||
submitButtonText: 'black',
|
||||
|
||||
submitButtonText: 'black'
|
||||
},
|
||||
fontSizes: {
|
||||
sm: '0.875rem'
|
||||
|
@ -33,7 +33,7 @@ test('handles mock data', async () => {
|
||||
})
|
||||
|
||||
test('handles multiple mock data sources', async () => {
|
||||
mockQuery(getMe, { username: 'elon' })
|
||||
mockQuery(getMe, { email: 'elon' })
|
||||
mockQuery(getTasks, mockTasks)
|
||||
|
||||
renderInContext(<App><Todo /></App>)
|
||||
|
@ -1,43 +1,27 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||
import { verifyEmail } from '@wasp/auth/email'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { VerifyEmailForm } from '@wasp/auth/forms/VerifyEmail'
|
||||
import appearance from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
|
||||
export function EmailVerification() {
|
||||
const location = useLocation()
|
||||
const history = useHistory()
|
||||
const verifyEmailInfo = useMutation<
|
||||
{ success: boolean },
|
||||
{ data: { success: boolean; reason?: string } },
|
||||
{ token: string },
|
||||
unknown
|
||||
>({
|
||||
mutationFn: verifyEmail,
|
||||
})
|
||||
useEffect(() => {
|
||||
const token = new URLSearchParams(location.search).get('token')
|
||||
if (!token) {
|
||||
history.push('/login')
|
||||
return
|
||||
}
|
||||
verifyEmailInfo.mutateAsync({ token })
|
||||
}, [location])
|
||||
return (
|
||||
<div>
|
||||
<h1>Email verification</h1>
|
||||
{verifyEmailInfo.isLoading && <p>Verifying email...</p>}
|
||||
{verifyEmailInfo.isError && (
|
||||
<p>
|
||||
Failed to verify email. Reason: {verifyEmailInfo.error.data?.reason}{' '}
|
||||
token
|
||||
</p>
|
||||
)}
|
||||
{verifyEmailInfo.isSuccess && verifyEmailInfo.data.success && (
|
||||
<>
|
||||
<p>Email verified successfully. ✅</p>
|
||||
<Link to="/login">Login</Link>
|
||||
</>
|
||||
)}
|
||||
<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>
|
||||
<VerifyEmailForm
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
socialLayout="horizontal"
|
||||
/>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
If everything is okay, <Link to="/login">go to login</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,30 +1,31 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import LoginForm from '@wasp/auth/forms/Login'
|
||||
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 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'
|
||||
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>).
|
||||
<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 />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
Forgot your password?{' '}
|
||||
<Link to="/request-password-reset">reset it</Link>.
|
||||
</span>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,36 +1,27 @@
|
||||
import { useHistory, useLocation } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { resetPassword } from '@wasp/auth/email'
|
||||
import { ResetPasswordForm } from '@wasp/auth/forms/ResetPassword'
|
||||
import appearance from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
|
||||
export function PasswordReset() {
|
||||
const location = useLocation()
|
||||
const history = useHistory()
|
||||
const token = new URLSearchParams(location.search).get('token')
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault()
|
||||
if (!token) {
|
||||
alert('Invalid token!')
|
||||
return
|
||||
}
|
||||
const password = e.target[0].value as string
|
||||
const passwordConfirmation = e.target[1].value as string
|
||||
if (!password || password !== passwordConfirmation) {
|
||||
alert("Passwords don't match!")
|
||||
return
|
||||
}
|
||||
try {
|
||||
await resetPassword({ password, token })
|
||||
history.push('/login')
|
||||
} catch (e: any) {
|
||||
alert(e.message)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<form className="flex flex-col gap-3 max-w-xs" onSubmit={handleSubmit}>
|
||||
<h1>Reset password</h1>
|
||||
<input type="password" placeholder="Enter new password" />
|
||||
<input type="password" placeholder="Confirm new password" />
|
||||
<button className="btn btn-primary">Reset password</button>
|
||||
</form>
|
||||
<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>
|
||||
<ResetPasswordForm
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
socialLayout="horizontal"
|
||||
/>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
If everything is okay, <Link to="/login">go to login</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,24 +1,21 @@
|
||||
import { requestPasswordReset } from '@wasp/auth/email'
|
||||
import { errorMessage } from '@wasp/utils'
|
||||
import { ForgotPasswordForm } from '@wasp/auth/forms/ForgotPassword'
|
||||
import appearance from './appearance'
|
||||
import todoLogo from '../../todoLogo.png'
|
||||
|
||||
export function RequestPasswordReset() {
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault()
|
||||
const email = e.target[0].value as string
|
||||
try {
|
||||
await requestPasswordReset({ email })
|
||||
window.alert('Check your e-mail!')
|
||||
} catch (e) {
|
||||
window.alert(errorMessage(e))
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<form className="flex flex-col gap-3 max-w-xs" onSubmit={handleSubmit}>
|
||||
<h1>Request password reset</h1>
|
||||
<input type="email" placeholder="Enter your e-mail" />
|
||||
<button className="btn btn-primary">Request password reset</button>
|
||||
</form>
|
||||
<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>
|
||||
<ForgotPasswordForm
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
socialLayout="horizontal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { SignupForm } from '@wasp/auth/email'
|
||||
import { SignupForm } from '@wasp/auth/forms/Signup'
|
||||
import getNumTasks from '@wasp/queries/getNumTasks'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
import { getTotalTaskCountMessage } from './helpers'
|
||||
@ -12,25 +12,24 @@ const Signup = () => {
|
||||
const { data: numTasks } = useQuery(getNumTasks)
|
||||
|
||||
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 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'
|
||||
appearance={appearance}
|
||||
logo={todoLogo}
|
||||
socialLayout="horizontal"
|
||||
/>
|
||||
<br/>
|
||||
<span className='text-sm font-medium text-gray-900'>
|
||||
<br />
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
I already have an account (<Link to="/login">go to login</Link>).
|
||||
</span>
|
||||
<br/>
|
||||
<br />
|
||||
</div>
|
||||
<br/><br/>
|
||||
<br />
|
||||
<br />
|
||||
<span>{getTotalTaskCountMessage(numTasks)}</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,10 +1,9 @@
|
||||
|
||||
const appearance = {
|
||||
colors: {
|
||||
brand: '#5969b8', // blue
|
||||
brandAccent: '#de5998', // pink
|
||||
submitButtonText: 'white',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default appearance
|
||||
|
@ -17,12 +17,11 @@ app todoApp {
|
||||
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"
|
||||
// }
|
||||
gitHub: {
|
||||
// configFn: import { config } from "@server/auth/github.js",
|
||||
// getUserFieldsFn: import { getUserFields } from "@server/auth/github.js"
|
||||
},
|
||||
email: {
|
||||
allowUnverifiedLogin: true,
|
||||
fromField: {
|
||||
name: "ToDO App",
|
||||
email: "mihovil@ilakovac.com"
|
||||
@ -96,7 +95,7 @@ psl=}
|
||||
|
||||
route SignupRoute { path: "/signup", to: SignupPage }
|
||||
page SignupPage {
|
||||
component: import Signup from "@client/pages/auth/Signup.jsx"
|
||||
component: import Signup from "@client/pages/auth/Signup.tsx"
|
||||
}
|
||||
|
||||
route LoginRoute { path: "/login", to: LoginPage }
|
||||
|
@ -8,11 +8,11 @@ module Wasp.AppSpec.App.Auth
|
||||
EmailAuthConfig (..),
|
||||
usernameAndPasswordConfig,
|
||||
isUsernameAndPasswordAuthEnabled,
|
||||
areBothExternalAndUsernameAndPasswordAuthEnabled,
|
||||
isExternalAuthEnabled,
|
||||
isGoogleAuthEnabled,
|
||||
isGitHubAuthEnabled,
|
||||
isEmailAuthEnabled,
|
||||
isEmailVerificationRequired,
|
||||
)
|
||||
where
|
||||
|
||||
@ -68,9 +68,6 @@ 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]
|
||||
|
||||
@ -82,3 +79,8 @@ isGitHubAuthEnabled = isJust . gitHub . methods
|
||||
|
||||
isEmailAuthEnabled :: Auth -> Bool
|
||||
isEmailAuthEnabled = isJust . email . methods
|
||||
|
||||
isEmailVerificationRequired :: Auth -> Bool
|
||||
isEmailVerificationRequired auth = case email . methods $ auth of
|
||||
Nothing -> False
|
||||
Just emailAuthConfig -> allowUnverifiedLogin emailAuthConfig /= Just True
|
||||
|
@ -1,7 +1,6 @@
|
||||
module Wasp.Generator.AuthProviders where
|
||||
|
||||
import Data.Maybe (fromJust)
|
||||
import StrongPath (relfile)
|
||||
import qualified Wasp.AppSpec.App.Dependency as App.Dependency
|
||||
import Wasp.Generator.AuthProviders.Common (makeProviderId)
|
||||
import qualified Wasp.Generator.AuthProviders.Email as E
|
||||
@ -14,7 +13,6 @@ googleAuthProvider =
|
||||
{ OA._providerId = fromJust $ makeProviderId "google",
|
||||
OA._displayName = "Google",
|
||||
OA._requiredScope = ["profile"],
|
||||
OA._logoFileName = [relfile|google-logo-icon.png|],
|
||||
OA._passportDependency = App.Dependency.make ("passport-google-oauth20", "2.0.0")
|
||||
}
|
||||
|
||||
@ -24,7 +22,6 @@ gitHubAuthProvider =
|
||||
{ OA._providerId = fromJust $ makeProviderId "github",
|
||||
OA._displayName = "GitHub",
|
||||
OA._requiredScope = [],
|
||||
OA._logoFileName = [relfile|github-logo-icon.png|],
|
||||
OA._passportDependency = App.Dependency.make ("passport-github2", "0.1.12")
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ module Wasp.Generator.AuthProviders.OAuth
|
||||
serverOauthRedirectHandlerUrl,
|
||||
providerId,
|
||||
displayName,
|
||||
logoFileName,
|
||||
passportDependency,
|
||||
scopeStr,
|
||||
clientIdEnvVarName,
|
||||
@ -15,7 +14,6 @@ where
|
||||
|
||||
import Data.Char (toUpper)
|
||||
import Data.List (intercalate)
|
||||
import StrongPath (File', Path', Rel')
|
||||
import Wasp.AppSpec.App.Dependency (Dependency)
|
||||
import Wasp.Generator.AuthProviders.Common (ProviderId, fromProviderId)
|
||||
|
||||
@ -25,7 +23,6 @@ data OAuthAuthProvider = OAuthAuthProvider
|
||||
-- Used for pretty printing
|
||||
_displayName :: String,
|
||||
_requiredScope :: OAuthScope,
|
||||
_logoFileName :: Path' Rel' File',
|
||||
_passportDependency :: Dependency
|
||||
}
|
||||
|
||||
@ -37,9 +34,6 @@ providerId = fromProviderId . _providerId
|
||||
displayName :: OAuthAuthProvider -> String
|
||||
displayName = _displayName
|
||||
|
||||
logoFileName :: OAuthAuthProvider -> Path' Rel' File'
|
||||
logoFileName = _logoFileName
|
||||
|
||||
clientIdEnvVarName :: OAuthAuthProvider -> String
|
||||
clientIdEnvVarName oai = upperCaseId oai ++ "_CLIENT_ID"
|
||||
|
||||
|
@ -15,7 +15,6 @@ import StrongPath
|
||||
Path',
|
||||
Posix,
|
||||
Rel,
|
||||
reldir,
|
||||
reldirP,
|
||||
relfile,
|
||||
(</>),
|
||||
@ -23,14 +22,11 @@ import StrongPath
|
||||
import Wasp.AppSpec (AppSpec)
|
||||
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.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, isAuthEnabled)
|
||||
import Wasp.Env (envVarsToDotEnvContent)
|
||||
import Wasp.Generator.AuthProviders (gitHubAuthProvider, googleAuthProvider)
|
||||
import qualified Wasp.Generator.AuthProviders.OAuth as OAuth
|
||||
import Wasp.Generator.Common
|
||||
( makeJsonWithEntityData,
|
||||
nodeVersionRange,
|
||||
@ -193,28 +189,13 @@ genPublicDir spec = do
|
||||
[ genFaviconFd,
|
||||
genManifestFd
|
||||
]
|
||||
<++> genSocialLoginIcons maybeAuth
|
||||
where
|
||||
maybeAuth = AS.App.auth $ snd $ getApp spec
|
||||
genFaviconFd = C.mkTmplFd (C.asTmplFile [relfile|public/favicon.ico|])
|
||||
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|] </> OAuth.logoFileName googleAuthProvider),
|
||||
(AS.App.Auth.isGitHubAuthEnabled, [reldir|public/images|] </> OAuth.logoFileName gitHubAuthProvider)
|
||||
]
|
||||
|
||||
genIndexHtml :: AppSpec -> Generator FileDraft
|
||||
genIndexHtml spec =
|
||||
return $
|
||||
|
@ -16,7 +16,6 @@ import Wasp.Generator.AuthProviders.Email
|
||||
)
|
||||
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 ((<++>))
|
||||
|
||||
@ -27,7 +26,6 @@ genEmailAuth auth
|
||||
[ genIndex
|
||||
]
|
||||
<++> genActions
|
||||
<++> genComponents auth
|
||||
| otherwise = return []
|
||||
|
||||
genIndex :: Generator FileDraft
|
||||
@ -73,24 +71,3 @@ genVerifyEmailAction =
|
||||
C.mkTmplFdWithData
|
||||
[relfile|src/auth/email/actions/verifyEmail.ts|]
|
||||
(object ["verifyEmailPath" .= serverVerifyEmailUrl emailAuthProvider])
|
||||
|
||||
genComponents :: AS.Auth.Auth -> Generator [FileDraft]
|
||||
genComponents auth =
|
||||
sequence
|
||||
[ genLoginComponent auth,
|
||||
genSignupComponent auth
|
||||
]
|
||||
|
||||
genLoginComponent :: AS.Auth.Auth -> Generator FileDraft
|
||||
genLoginComponent auth =
|
||||
return $
|
||||
C.mkTmplFdWithData
|
||||
[relfile|src/auth/email/components/Login.jsx|]
|
||||
(object ["onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth])
|
||||
|
||||
genSignupComponent :: AS.Auth.Auth -> Generator FileDraft
|
||||
genSignupComponent auth =
|
||||
return $
|
||||
C.mkTmplFdWithData
|
||||
[relfile|src/auth/email/components/Signup.jsx|]
|
||||
(object ["onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth])
|
||||
|
@ -4,7 +4,6 @@ module Wasp.Generator.WebAppGenerator.Auth.OAuthAuthG
|
||||
where
|
||||
|
||||
import Data.Aeson (object, (.=))
|
||||
import Data.Maybe (fromJust)
|
||||
import StrongPath (File', Path', Rel', reldir, relfile)
|
||||
import qualified StrongPath as SP
|
||||
import qualified Wasp.AppSpec.App.Auth as AS.Auth
|
||||
@ -45,7 +44,6 @@ genHelpers auth =
|
||||
tmplData =
|
||||
object
|
||||
[ "signInPath" .= OAuth.serverLoginUrl provider,
|
||||
"iconName" .= (SP.fromRelFileP . fromJust . SP.relFileToPosix) (OAuth.logoFileName provider),
|
||||
"displayName" .= OAuth.displayName provider
|
||||
]
|
||||
|
||||
|
@ -60,11 +60,16 @@ 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,
|
||||
[ genAuthForm auth,
|
||||
copyTmplFile [relfile|auth/forms/Login.tsx|],
|
||||
copyTmplFile [relfile|auth/forms/Signup.tsx|],
|
||||
copyTmplFile [relfile|auth/forms/ResetPassword.tsx|],
|
||||
copyTmplFile [relfile|auth/forms/ForgotPassword.tsx|],
|
||||
copyTmplFile [relfile|auth/forms/VerifyEmail.tsx|],
|
||||
copyTmplFile [relfile|auth/forms/types.ts|],
|
||||
copyTmplFile [relfile|stitches.config.js|],
|
||||
copyTmplFile [relfile|auth/forms/SocialIcons.tsx|]
|
||||
copyTmplFile [relfile|auth/forms/SocialIcons.tsx|],
|
||||
copyTmplFile [relfile|auth/forms/SocialButton.tsx|]
|
||||
]
|
||||
where
|
||||
copyTmplFile = return . C.mkSrcTmplFd
|
||||
@ -74,30 +79,24 @@ genAuthForm auth =
|
||||
compileTmplToSamePath
|
||||
[relfile|auth/forms/Auth.tsx|]
|
||||
[ "onAuthSucceededRedirectTo" .= getOnAuthSucceededRedirectToOrDefault auth,
|
||||
"isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth,
|
||||
"areBothExternalAndUsernameAndPasswordAuthEnabled" .= AS.Auth.areBothExternalAndUsernameAndPasswordAuthEnabled auth,
|
||||
"areBothSocialAndPasswordBasedAuthEnabled" .= areBothSocialAndPasswordBasedAuthEnabled,
|
||||
"isAnyPasswordBasedAuthEnabled" .= isAnyPasswordBasedAuthEnabled,
|
||||
"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
|
||||
"gitHubSignInPath" .= OAuth.serverLoginUrl gitHubAuthProvider,
|
||||
-- Username and password
|
||||
"isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth,
|
||||
-- Email
|
||||
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth,
|
||||
"isEmailVerificationRequired" .= AS.Auth.isEmailVerificationRequired auth
|
||||
]
|
||||
where
|
||||
areBothSocialAndPasswordBasedAuthEnabled = AS.Auth.isExternalAuthEnabled auth && isAnyPasswordBasedAuthEnabled
|
||||
isAnyPasswordBasedAuthEnabled = AS.Auth.isUsernameAndPasswordAuthEnabled auth || AS.Auth.isEmailAuthEnabled auth
|
||||
|
||||
compileTmplToSamePath :: Path' Rel' File' -> [Pair] -> Generator FileDraft
|
||||
compileTmplToSamePath tmplFileInTmplSrcDir keyValuePairs =
|
||||
|
Loading…
Reference in New Issue
Block a user