diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh index 1e73e057..d57d3d74 100644 --- a/.devcontainer/post-start.sh +++ b/.devcontainer/post-start.sh @@ -1,3 +1,5 @@ +#!/bin/bash + # Silence warnings about the git repos not being owned by this user (since it's bind mounted from host) # This runs inside the container, so it's okay to do global git config git config --global --add safe.directory "/workspaces/Monorepo" diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 577d5412..40ac3d9f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -17,6 +17,7 @@ "wix.vscode-import-cost", "jock.svg", "herrmannplatz.npm-dependency-links", + "doggy8088.quicktype-refresh", // Backend "KevinRose.vsc-python-indent", diff --git a/Monorepo.wiki b/Monorepo.wiki index 49f9c0c2..47370651 160000 --- a/Monorepo.wiki +++ b/Monorepo.wiki @@ -1 +1 @@ -Subproject commit 49f9c0c26efff6b8ac8e74801e89b590eaae4157 +Subproject commit 47370651c9476dbc02ae8bbdba201eaf3c880e47 diff --git a/apps/backend/backend.Dockerfile b/apps/backend/backend.Dockerfile index c85bc913..075faa8f 100644 --- a/apps/backend/backend.Dockerfile +++ b/apps/backend/backend.Dockerfile @@ -25,6 +25,7 @@ RUN pip install pipenv COPY Pipfile . COPY Pipfile.lock . # Args explanation: https://stackoverflow.com/a/49705601 +# https://pipenv-fork.readthedocs.io/en/latest/basics.html#pipenv-install RUN pipenv install --system --deploy --ignore-pipfile WORKDIR /app diff --git a/apps/frontend/components/auth/AlreadySignedInModal.tsx b/apps/frontend/components/auth/AlreadySignedInModal.tsx new file mode 100644 index 00000000..18c289c7 --- /dev/null +++ b/apps/frontend/components/auth/AlreadySignedInModal.tsx @@ -0,0 +1,68 @@ +import Link from 'next/link'; +import Router from 'next/router'; +import { useAuth } from '../../firebase/fbAuth'; + +export interface AlreadySignedInModalProps { + continueButtonMessage: string, + linkTarget: string +} + +export const AlreadySignedInModal = ({ continueButtonMessage, linkTarget }: AlreadySignedInModalProps) => { + const { authService } = useAuth(); + + return
+
+
+
+

+ {"You're already signed in as"} +

+ + {authService.userEmail} + +
+ +
+ + + {continueButtonMessage} + + +
+ +
+ +
+ { + return ( + authService + .signOut() + .then(() => { + Router.reload(); + }) + .catch((err) => console.log('Sign out error', err)) + ); + }} + > + Log Out + +
+
+
+
; +}; diff --git a/apps/frontend/components/auth/SignInModal.tsx b/apps/frontend/components/auth/SignInModal.tsx new file mode 100644 index 00000000..ccff059f --- /dev/null +++ b/apps/frontend/components/auth/SignInModal.tsx @@ -0,0 +1,217 @@ +import { joiResolver, useForm } from '@mantine/form'; +import Link from 'next/link'; +import { useState } from 'react'; +import { useAuth } from '../../firebase/fbAuth'; +import { emailSchema, signInSchema } from '../../utils/validators'; + +export const DEFAULT_SIGN_IN_TEXT = 'Sign in'; +export const SIGN_IN_LOADING_TEXT = 'Loading...'; + +export interface SignInModalProps { + afterSignIn: () => void +} + +export const SignInModal = ({ afterSignIn }: SignInModalProps) => { + const form = useForm({ + initialValues: { + email: '', + password: '', + }, + schema: joiResolver(signInSchema), + }); + + const { authService } = useAuth(); + const [buttonDisabled, setButtonDisabled] = useState(false); + const [buttonText, setButtonText] = useState(DEFAULT_SIGN_IN_TEXT); + + return
+
+
{ + const { email, password } = values; + setButtonDisabled(true); + setButtonText(SIGN_IN_LOADING_TEXT); + await authService.signInWithEmailAndPassword( + email, + password + ).then(() => { + afterSignIn(); + }).catch((error) => { + alert(`Problem logging in: ${error}`); + setButtonText(DEFAULT_SIGN_IN_TEXT); + setButtonDisabled(false); + }); + })} + > +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+
+
+
+ + Or continue with + +
+
+ + + (OAuth login support coming soon™) + + + {/* TODO implement OAuth sign in */} + {/* */} +
+
+ + +
+
; +}; diff --git a/apps/frontend/components/auth/SignUpModal.tsx b/apps/frontend/components/auth/SignUpModal.tsx new file mode 100644 index 00000000..63c199bf --- /dev/null +++ b/apps/frontend/components/auth/SignUpModal.tsx @@ -0,0 +1,247 @@ +import { joiResolver, useForm } from '@mantine/form'; +import Link from 'next/link'; +import { useEffect, useMemo, useState } from 'react'; +import { AiOutlineGithub, AiOutlineGoogle, AiOutlineTwitter } from 'react-icons/ai'; +import { useAuth } from '../../firebase/fbAuth'; +import { signUpSchema } from '../../utils/validators'; + +export const SignUpModal = ({ afterSignUp }) => { + const form = useForm({ + initialValues: { + email: '', + password: '', + passwordRepeat: '', + }, + schema: joiResolver(signUpSchema), + }); + const DEFAULT_SIGN_UP_TEXT = 'Create your account'; + const SIGN_UP_LOADING_TEXT = 'Loading...'; + const { authService } = useAuth(); + const [buttonDisabled, setButtonDisabled] = useState(false); + const [buttonText, setButtonText] = useState(DEFAULT_SIGN_UP_TEXT); + + const formHasError = useMemo(() => { + return Object.keys(form.errors).length !== 0; + }, [form.errors]); + + useEffect(() => { + if (formHasError) { + console.log('Form errors', form.errors); + } + }, [formHasError, form.errors]); + + return
+
+
+
+

+ Sign in with +

+ + (OAuth login support coming soon™) + +
+ {/* TODO implement OAuth sign in */} + + {/* + + + + */} +
+
+ +
+ + +
+
+

+ Create an Account +

+
+
{ + console.log('Form is invalid (this only triggers when Mantine already has a hover tooltip for stuff)'); + }} + onSubmit={form.onSubmit(async (values) => { + console.log('Sign in was submitted'); + const { email, password } = values; + setButtonDisabled(true); + setButtonText(SIGN_UP_LOADING_TEXT); + try { + await authService.signUpWithEmailAndPassword( + email, + password + ); + afterSignUp(); + } catch (error) { + console.log('Sign up error', error); + form.setErrors({ + firebaseError: error?.message, + }); + setButtonText(DEFAULT_SIGN_UP_TEXT); + setButtonDisabled(false); + } + })} + className='space-y-6' + > +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ { formHasError ? +
    + {Object.entries(form.errors).map((error) => { + return ( +
  • + {error[1]} +
  • + ); + })} +
: + null} +
+
+
+
+ + +
+
+

+ By signing up, you agree to our{' '} + + Terms + + ,{' '} + + Data Policy + {' '} + and{' '} + + Cookies Policy + + . +

+
+
+
; +}; diff --git a/apps/frontend/firebase/fbAuth.tsx b/apps/frontend/firebase/fbAuth.tsx index a91065dd..49dd9d44 100644 --- a/apps/frontend/firebase/fbAuth.tsx +++ b/apps/frontend/firebase/fbAuth.tsx @@ -7,20 +7,32 @@ import React, { useDebugValue, } from 'react'; import { firebaseApp } from './firebaseClient'; -import { createUserWithEmailAndPassword, getAuth, onAuthStateChanged, signInWithEmailAndPassword, signOut, User } from 'firebase/auth'; +import { createUserWithEmailAndPassword, getAuth, onAuthStateChanged, sendPasswordResetEmail, signInWithEmailAndPassword, signOut, User } from 'firebase/auth'; import noImage from '../images/NoImage.png'; +import { FirebaseError } from 'firebase/app'; +import { StaticImageData } from 'next/image'; + +// We must explicitly type specify the contents of authService because the info gets "lost" when going through the useAuth hook +export interface AuthServiceType { + userId: string | undefined; + userEmail: string | null | undefined; + userPhotoUrl: string | StaticImageData; + signInWithEmailAndPassword: (email: string, password: string) => Promise; + signUpWithEmailAndPassword: (email: string, password: string) => Promise; + signInWithGoogle: () => Promise; + sendPasswordResetEmail: (email: string) => Promise; + signOut: () => Promise; +} export interface AuthContextType { user: User | null; userId: String | null; - authService; // TODO find a way to make a type for this, ideally without duplicating all the function names - } + authService: AuthServiceType; + loading: boolean; +} -const AuthContext = createContext({ - user: null, - userId: null, - authService: null, -}); +// We know this will become non-null immediately so okay to typecast https://stackoverflow.com/questions/63080452/react-createcontextnull-not-allowed-with-typescript +const AuthContext = createContext({} as AuthContextType); export const useAuth = () => useContext(AuthContext); @@ -46,7 +58,7 @@ export const AuthProvider: React.FC = ({ children }) => { } }), [auth]); - const authService = { + const authService: AuthServiceType = { userId: useMemo(() => { const newVal = user?.uid; console.log('userId is now', newVal); @@ -66,25 +78,38 @@ export const AuthProvider: React.FC = ({ children }) => { .then((userCredential) => { console.log('sign in success, UserCred is ', userCredential); // no need to set state because onAuthStateChanged will pick it up - }).catch((error) => { + }).catch((error: FirebaseError) => { console.error('Firebase sign in error', error); throw error; }); }, + signUpWithEmailAndPassword: async (email: string, password: string) => { return await createUserWithEmailAndPassword(auth, email, password) .then((userCredential) => { console.log('sign up success, UserCred is ', userCredential); - }).catch((error) => { + }).catch((error: FirebaseError) => { console.error('Firebase sign up error', error); throw error; }); }, + signInWithGoogle: async () => { console.error('TODO'); }, + + sendPasswordResetEmail: async (email: string) => { + try { + await sendPasswordResetEmail(auth, email); + } catch (error) { + console.error('Firebase password reset error', error); + throw error; + } + }, + signOut: async () => { - return await signOut(auth); + await signOut(auth); + setUser(null); }, }; @@ -92,7 +117,9 @@ export const AuthProvider: React.FC = ({ children }) => { {loading ? (
diff --git a/apps/frontend/pages/dashboard.tsx b/apps/frontend/pages/dashboard.tsx index e02d4adb..441b449d 100644 --- a/apps/frontend/pages/dashboard.tsx +++ b/apps/frontend/pages/dashboard.tsx @@ -107,9 +107,6 @@ const Navbar = (props) => { item.name === 'Sign out' && authService .signOut() - .then(() => { - Router.reload(); - }) .catch((err) => console.log('Sign out error', err)) ); }} diff --git a/apps/frontend/pages/index.tsx b/apps/frontend/pages/index.tsx index 0e024fb6..4c60601c 100644 --- a/apps/frontend/pages/index.tsx +++ b/apps/frontend/pages/index.tsx @@ -5,36 +5,19 @@ import styles from '../styles/Home.module.css'; import { Popover } from '@headlessui/react'; import { Bars3Icon } from '@heroicons/react/24/outline'; import { ChevronRightIcon } from '@heroicons/react/24/solid'; - -import { - AiOutlineGoogle, - AiOutlineTwitter, - AiOutlineGithub, -} from 'react-icons/ai'; -import { joiResolver, useForm } from '@mantine/form'; import { Logo } from '../components/Logo'; -import { signUpSchema } from '../utils/validators'; +import { SignUpModal } from '../components/auth/SignUpModal'; import { useAuth } from '../firebase/fbAuth'; +import { AlreadySignedInModal } from '../components/auth/AlreadySignedInModal'; +import { useState } from 'react'; -export default function HomePage() { - const signUpForm = useForm({ - initialValues: { - email: '', - password: '', - }, - schema: joiResolver(signUpSchema), - }); - - return ; -} - -function Home({ form }) { +const HomePage = () => { const router = useRouter(); - const { userId, authService } = useAuth(); + const { userId } = useAuth(); + const [loading, setLoading] = useState(false); + + console.log('userId', userId); - // if (userId) { - // // router.push('/dashboard'); - // } return ( -
-
-
- - -
- - -
-
{ - try { - const { error } = - await authService.signUpWithEmailAndPassword( - values.email, - values.password - ); - if (error) { - throw error; - } else { - router.push('/dashboard'); - } - } catch (error) { - console.log('Sign up error', error); - } - })} - className='space-y-6' - > -
- - -
- -
- - -
- -
- - -
- -
- -
-
-
-
-
-

- By signing up, you agree to our{' '} - - Terms - - ,{' '} - - Data Policy - {' '} - and{' '} - - Cookies Policy - - . -

-
-
-
+ {userId && !loading ? + : + { + setLoading(true); + router.push('/dashboard'); + }}/>}
@@ -306,4 +134,6 @@ function Home({ form }) {
); -} +}; + +export default HomePage; diff --git a/apps/frontend/pages/signin.tsx b/apps/frontend/pages/signin.tsx index dc914291..5cece676 100644 --- a/apps/frontend/pages/signin.tsx +++ b/apps/frontend/pages/signin.tsx @@ -1,24 +1,7 @@ import { useRouter } from 'next/router'; -import { useForm, joiResolver } from '@mantine/form'; -import { signInSchema } from '../utils/validators'; -import { useAuth } from '../firebase/fbAuth'; -import { useState } from 'react'; -import Image from 'next/image'; - -const defaultSignInText = 'Sign in'; +import { SignInModal } from '../components/auth/SignInModal'; const SignInPage = () => { - const form = useForm({ - initialValues: { - email: '', - password: '', - }, - schema: joiResolver(signInSchema), - }); - - const { authService } = useAuth(); - const [signInDisabled, setSignInDisabled] = useState(false); - const [signInButtonText, setSignInButtonText] = useState(defaultSignInText); const router = useRouter(); return ( @@ -34,180 +17,14 @@ const SignInPage = () => { Sign in to your account
- -
-
-
{ - const { email, password } = values; - setSignInDisabled(true); - setSignInButtonText('Loading...'); - await authService.signInWithEmailAndPassword( - email, - password - ).then(() => { - console.log('Success, loading dashboard...'); - router.push('/dashboard'); - }).catch((error) => { - alert(`Problem logging in: ${error}`); - setSignInButtonText(defaultSignInText); - setSignInDisabled(false); - }); - })} - > -
- -
- -
-
- -
- -
- -
-
- -
- {/* replace with some sort of admin designation */} - {/*
- - -
*/} - - {/* */} -
- -
- -
-
- -
- -
-
+ { + console.log('Sign in success, loading dashboard...'); + router.push('/dashboard'); + }} />
); }; export default SignInPage; + diff --git a/apps/frontend/utils/validators.ts b/apps/frontend/utils/validators.ts index 00fea009..e3c31b04 100644 --- a/apps/frontend/utils/validators.ts +++ b/apps/frontend/utils/validators.ts @@ -29,6 +29,10 @@ const boolschema = Joi.object().keys({ default: Joi.boolean().required(), type: Joi.string().valid('bool'), }); + +export const emailSchema = Joi.string() + .email({ tlds: { allow: false } }); + export const experimentSchema = Joi.object().keys({ name: Joi.string().required(), description: Joi.string(), @@ -38,16 +42,14 @@ export const experimentSchema = Joi.object().keys({ }); export const signUpSchema = Joi.object().keys({ - email: Joi.string() - .email({ tlds: { allow: false } }) - .required(), + email: emailSchema.required(), password: Joi.string().required(), - passwordRepeat: Joi.string().required().valid(Joi.ref('password')), + passwordRepeat: Joi.string().required().valid(Joi.ref('password')).messages({ + '*': 'Make sure your passwords match', + }), }); export const signInSchema = Joi.object().keys({ - email: Joi.string() - .email({ tlds: { allow: false } }) - .required(), + email: emailSchema.required(), password: Joi.string().required(), });