diff --git a/__tests__/userAuth.test.ts b/__tests__/userAuth.test.ts index 3b1ef8b1..c8b032de 100644 --- a/__tests__/userAuth.test.ts +++ b/__tests__/userAuth.test.ts @@ -1,150 +1,206 @@ -/** - * @jest-environment node - */ - -import app from '../server/server'; -import mockData from '../mockData'; -import { Sessions, Users } from '../server/models/reactypeModels'; -const request = require('supertest'); -const mongoose = require('mongoose'); -const mockNext = jest.fn(); // Mock nextFunction -const MONGO_DB = import.meta.env.MONGO_DB_TEST; -const { user } = mockData; -const PORT = 8080; - -const num = Math.floor(Math.random() * 1000); - -beforeAll(async () => { - await mongoose.connect(MONGO_DB, { - useNewUrlParser: true, - useUnifiedTopology: true - }); -}); - -afterAll(async () => { - const result = await Users.deleteMany({ - _id: { $ne: '64f551e5b28d5292975e08c8' } - }); //clear the users collection after tests are done except for the mockdata user account - const result2 = await Sessions.deleteMany({ - cookieId: { $ne: '64f551e5b28d5292975e08c8' } - }); - console.log( - `${result.deletedCount} and ${result2.deletedCount} documents deleted.` - ); - await mongoose.connection.close(); -}); - -describe('User Authentication tests', () => { - describe('initial connection test', () => { - it('should connect to the server', async () => { - const response = await request(app).get('/test'); - expect(response.status).toBe(200); - expect(response.text).toBe('test request is working'); - }); - }); - describe('/signup', () => { - describe('POST', () => { - //testing new signup - it('responds with status 200 and sessionId on valid new user signup', () => { - return request(app) - .post('/signup') - .set('Content-Type', 'application/json') - .send({ - username: `supertest${num}`, - email: `test${num}@test.com`, - password: `${num}` - }) - .expect(200) - .then((res) => expect(res.body.sessionId).not.toBeNull()); - }); - - it('responds with status 400 and json string on invalid new user signup (Already taken)', () => { - return request(app) - .post('/signup') - .send(user) - .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect(400) - .then((res) => expect(typeof res.body).toBe('string')); - }); - }); - }); - - describe('/login', () => { - // tests whether existing login information permits user to log in - describe('POST', () => { - it('responds with status 200 and json object on verified user login', () => { - return request(app) - .post('/login') - .set('Accept', 'application/json') - .send(user) - .expect(200) - .expect('Content-Type', /json/) - .then((res) => expect(res.body.sessionId).toEqual(user.userId)); - }); - // if invalid username/password, should respond with status 400 - it('responds with status 400 and json string on invalid user login', () => { - return request(app) - .post('/login') - .send({ username: 'wrongusername', password: 'wrongpassword' }) - .expect(400) - .expect('Content-Type', /json/) - .then((res) => expect(typeof res.body).toBe('string')); - }); - it("returns the message 'No Username Input' when no username is entered", () => { - return request(app) - .post('/login') - .send({ - username: '', - password: 'Reactype123!@#', - isFbOauth: false - }) - .then((res) => expect(res.text).toBe('"No Username Input"')); - }); - - it("returns the message 'No Username Input' when no username is entered", () => { - return request(app) - .post('/login') - .send({ - username: '', - password: 'Reactype123!@#', - isFbOauth: false - }) - .then((res) => expect(res.text).toBe('"No Username Input"')); - }); - - it("returns the message 'No Password Input' when no password is entered", () => { - return request(app) - .post('/login') - .send({ - username: 'reactype123', - password: '', - isFbOauth: false - }) - .then((res) => expect(res.text).toBe('"No Password Input"')); - }); - - it("returns the message 'Invalid Username' when username does not exist", () => { - return request(app) - .post('/login') - .send({ - username: 'l!b', - password: 'test', - isFbOauth: false - }) - .then((res) => expect(res.text).toBe('"Invalid Username"')); - }); - }); - - it("returns the message 'Incorrect Password' when password does not match", () => { - return request(app) - .post('/login') - .send({ - username: 'test', - password: 'test', - isFbOauth: false - }) - .then((res) => expect(res.text).toBe('"Incorrect Password"')); - }); - }); -}); +/** + * @jest-environment node + */ + +import app from '../server/server'; +import mockData from '../mockData'; +import { Sessions, Users } from '../server/models/reactypeModels'; +const request = require('supertest'); +const mongoose = require('mongoose'); +const mockNext = jest.fn(); // Mock nextFunction +const MONGO_DB = import.meta.env.MONGO_DB_TEST; +const { user } = mockData; +const PORT = 8080; + +const num = Math.floor(Math.random() * 1000); + +beforeAll(async () => { + await mongoose.connect(MONGO_DB, { + useNewUrlParser: true, + useUnifiedTopology: true + }); +}); + +afterAll(async () => { + const result = await Users.deleteMany({ + _id: { $ne: '64f551e5b28d5292975e08c8' } + }); //clear the users collection after tests are done except for the mockdata user account + const result2 = await Sessions.deleteMany({ + cookieId: { $ne: '64f551e5b28d5292975e08c8' } + }); + console.log( + `${result.deletedCount} and ${result2.deletedCount} documents deleted.` + ); + await mongoose.connection.close(); +}); + +describe('User Authentication tests', () => { + describe('initial connection test', () => { + it('should connect to the server', async () => { + const response = await request(app).get('/test'); + expect(response.status).toBe(200); + expect(response.text).toBe('test request is working'); + }); + }); + describe('/signup', () => { + describe('POST', () => { + //testing new signup + it('responds with status 200 and sessionId on valid new user signup', () => { + return request(app) + .post('/signup') + .set('Content-Type', 'application/json') + .send({ + username: `supertest${num}`, + email: `test${num}@test.com`, + password: `${num}` + }) + .expect(200) + .then((res) => expect(res.body.sessionId).not.toBeNull()); + }); + + it('responds with status 400 and json string on invalid new user signup (Already taken)', () => { + return request(app) + .post('/signup') + .send(user) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(400) + .then((res) => expect(typeof res.body).toBe('string')); + }); + }); + }); + + describe('/login', () => { + // tests whether existing login information permits user to log in + describe('POST', () => { + it('responds with status 200 and json object on verified user login', () => { + return request(app) + .post('/login') + .set('Accept', 'application/json') + .send(user) + .expect(200) + .expect('Content-Type', /json/) + .then((res) => expect(res.body.sessionId).toEqual(user.userId)); + }); + // if invalid username/password, should respond with status 400 + it('responds with status 400 and json string on invalid user login', () => { + return request(app) + .post('/login') + .send({ username: 'wrongusername', password: 'wrongpassword' }) + .expect(400) + .expect('Content-Type', /json/) + .then((res) => expect(typeof res.body).toBe('string')); + }); + it("returns the message 'No Username Input' when no username is entered", () => { + return request(app) + .post('/login') + .send({ + username: '', + password: 'Reactype123!@#', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Username Input"')); + }); + + it("returns the message 'No Username Input' when no username is entered", () => { + return request(app) + .post('/login') + .send({ + username: '', + password: 'Reactype123!@#', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Username Input"')); + }); + + it("returns the message 'No Password Input' when no password is entered", () => { + return request(app) + .post('/login') + .send({ + username: 'reactype123', + password: '', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Password Input"')); + }); + + it("returns the message 'Invalid Username' when username does not exist", () => { + return request(app) + .post('/login') + .send({ + username: 'l!b', + password: 'test', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"Invalid Username"')); + }); + }); + + it("returns the message 'Incorrect Password' when password does not match", () => { + return request(app) + .post('/login') + .send({ + username: 'test', + password: 'test', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"Incorrect Password"')); + }); + }); + + describe('/updatePassword', () => { + describe('POST', () => { + //testing update password + const testUsername = `supertest${Date.now()}`; + const testPassword = `password${Date.now()}`; + it('responds with status 200 and json string on valid password update (Success)', () => { + return request(app) + .post('/updatePassword') + .set('Content-Type', 'application/json') + .send({ + username: testUsername, + password: testPassword + }) + .expect(200) + .then((res) => expect(res.body.message).toBe('Success')); // might need to be res.text instead of res.body.message + }); + + it('responds with status 400 and json string if no password is provided (Password is required.)', () => { + return request(app) + .post('/updatePassword') + .send({ username: testUsername }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(400) + .then((res) => expect(res.body.error).toBe('Password is required.')); + }); + + it('responds with status 404 and json string if user is not found (User not found.)', () => { + return request(app) + .post('/updatePassword') + .send({ username: 'doesntexist', password: 'fakepassword' }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(404) + .then((res) => expect(res.body.error).toBe('User not found.')); + }); + + it('responds with status 400 and json string the provided password is the same as the current password (New password must be different from the current password.)', () => { + return request(app) + .post('/updatePassword') + .send({ + username: testUsername, + password: testPassword + }) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(400) + .then((res) => + expect(res.body.error).toBe( + 'New password must be different from the current password.' + ) + ); + }); + }); + }); +}); diff --git a/app/src/components/login/FBPassWord.tsx b/app/src/components/login/FBPassWord.tsx index ff79fb0e..f594abee 100644 --- a/app/src/components/login/FBPassWord.tsx +++ b/app/src/components/login/FBPassWord.tsx @@ -1,206 +1,261 @@ -import React, { useState, MouseEvent } from 'react'; -import { LoginInt } from '../../interfaces/Interfaces'; -import { - Link as RouteLink, - withRouter, - RouteComponentProps -} from 'react-router-dom'; -import { newUserIsCreated } from '../../helperFunctions/auth'; -import Avatar from '@mui/material/Avatar'; -import Button from '@mui/material/Button'; -import CssBaseline from '@mui/material/CssBaseline'; -import TextField from '@mui/material/TextField'; -import Grid from '@mui/material/Grid'; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import makeStyles from '@mui/styles/makeStyles'; -import Container from '@mui/material/Container'; - -function Copyright() { - return ( - - {'Copyright © ReacType '} - {new Date().getFullYear()} - {'.'} - - ); -} - -const useStyles = makeStyles((theme) => ({ - paper: { - marginTop: theme.spacing(8), - display: 'flex', - flexDirection: 'column', - alignItems: 'center' - }, - avatar: { - margin: theme.spacing(1), - backgroundColor: '#3EC1AC' - }, - form: { - width: '100%', // Fix IE 11 issue. - marginTop: theme.spacing(3) - }, - submit: { - margin: theme.spacing(3, 0, 2) - }, - root: { - '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { - borderColor: '#3EC1AC' - } - } -})); - -const SignUp: React.FC = (props) => { - const classes = useStyles(); - const [password, setPassword] = useState(''); - const [passwordVerify, setPasswordVerify] = useState(''); - - const [invalidPasswordMsg, setInvalidPasswordMsg] = useState(''); - const [invalidVerifyPasswordMsg, setInvalidVerifyPasswordMsg] = useState(''); - - const [invalidPassword, setInvalidPassword] = useState(false); - const [invalidVerifyPassword, setInvalidVerifyPassword] = useState(false); - - const handleChange = (e: React.ChangeEvent) => { - let inputVal = e.target.value; - switch (e.target.name) { - case 'password': - setPassword(inputVal); - break; - case 'passwordVerify': - setPasswordVerify(inputVal); - break; - } - }; - - const handleSignUp = (e: React.MouseEvent) => { - e.preventDefault(); - const email = props.location.state.email; - // Reset Error Validation - setInvalidPasswordMsg(''); - setInvalidVerifyPasswordMsg(''); - setInvalidPassword(false); - setInvalidVerifyPassword(false); - - if (password === '') { - setInvalidPassword(true); - setInvalidPasswordMsg('No Password Entered'); - return; - } else if (password.length < 8) { - setInvalidPassword(true); - setInvalidPasswordMsg('Minimum 8 Characters'); - return; - } else if ( - !/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/i.test( - password - ) - ) { - setInvalidPassword(true); - setInvalidPasswordMsg('Minimum 1 Letter, Number, and Special Character'); - return; - } else if (password !== passwordVerify) { - setInvalidPassword(true); - setInvalidVerifyPassword(true); - setInvalidPasswordMsg('Verification Failed'); - setInvalidVerifyPasswordMsg('Verification Failed'); - setPasswordVerify(''); - return; - } else { - setInvalidPassword(false); - } - - if (password !== passwordVerify) { - setInvalidPassword(true); - setInvalidVerifyPassword(true); - setInvalidPasswordMsg('Verification Failed'); - setInvalidVerifyPasswordMsg('Verification Failed'); - setPasswordVerify(''); - return; - } else { - setInvalidVerifyPassword(false); - } - - // get username and email from FB - newUserIsCreated(email, email, password).then((userCreated) => { - if (userCreated === 'Success') { - props.history.push('/'); - } else { - } - }); - }; - - return ( - - -
- - - - - Please enter in your new password - -
- - - - - - - - - - + - - - - Already have an account? Sign In - - - -
-
- - - -
- ); -}; - -export default withRouter(SignUp); +import React, { useState, MouseEvent } from 'react'; +import { LoginInt } from '../../interfaces/Interfaces'; +import { SigninDark } from '../../../../app/src/public/styles/theme'; +import { + Link as RouteLink, + withRouter, + RouteComponentProps, + useHistory +} from 'react-router-dom'; +import { + validateInputs, + handleChange, + resetErrorValidation, + updatePassword +} from '../../helperFunctions/auth'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import CssBaseline from '@mui/material/CssBaseline'; +import TextField from '@mui/material/TextField'; +import Grid from '@mui/material/Grid'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { styled } from '@mui/material/styles'; +import Container from '@mui/material/Container'; +import AssignmentIcon from '@mui/icons-material/Assignment'; +import { + StyledEngineProvider, + Theme, + ThemeProvider +} from '@mui/material/styles'; + +declare module '@mui/styles/defaultTheme' { + interface DefaultTheme extends Theme {} +} + +function Copyright() { + return ( + + {'Copyright © ReacType '} + {new Date().getFullYear()} + {'.'} + + ); +} + +const StyledPaper = styled(Box)(({ theme }) => ({ + marginTop: theme.spacing(8), + display: 'flex', + flexDirection: 'column', + alignItems: 'center' +})); + +const StyledAvatar = styled(Avatar)(({ theme }) => ({ + margin: theme.spacing(1), + backgroundColor: 'white' +})); + +const StyledForm = styled('form')(({ theme }) => ({ + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing(3) +})); + +const StyledButton = styled(Button)(({ theme }) => ({ + margin: theme.spacing(3, 0, 2) +})); + +const StyledTextField = styled(TextField)(({ theme }) => ({ + '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: '#white' + } +})); + +const FBPassWord: React.FC = () => { + const history = useHistory(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [passwordVerify, setPasswordVerify] = useState(''); + + const [invalidUserMsg, setInvalidUserMsg] = useState(''); + const [invalidPasswordMsg, setInvalidPasswordMsg] = useState(''); + const [invalidVerifyPasswordMsg, setInvalidVerifyPasswordMsg] = useState(''); + + const [invalidUser, setInvalidUser] = useState(false); + const [invalidPassword, setInvalidPassword] = useState(false); + const [invalidVerifyPassword, setInvalidVerifyPassword] = useState(false); + + // define error setters to pass to resetErrorValidation function + const errorSetters = { + setInvalidUser, + setInvalidUserMsg, + setInvalidPassword, + setInvalidPasswordMsg, + setInvalidVerifyPassword, + setInvalidVerifyPasswordMsg + }; + // define handle change setters to pass to handleChange function + const handleChangeSetters = { + setUsername, + setPassword, + setPasswordVerify + }; + + /** + * Handles input changes for form fields and updates the state accordingly. + * This function delegates to the `handleChange` function, passing the event + * and the `handleChangeSetters` for updating the specific state tied to the input fields. + * @param {React.ChangeEvent} e - The event object that triggered the change. + */ + const handleInputChange = (e: React.ChangeEvent): void => { + handleChange(e, handleChangeSetters); + }; + + /** + * Handles the form submission for user password change. Prevents the default form submission behavior, + * resets any previous validation errors, and, if the input validation passes, attempts to update the user's password. + * Upon successful password update, the user is redirected to the login page. If the update fails or validation fails, + * appropriate error messages are displayed. + * @param {React.FormEvent} e - The event object that triggered the form submission, + * used to prevent the default form behavior. + * @returns {void} Nothing is returned from this function as it handles redirection or error display internally. + */ + const handleUpdatePassword = async (e: React.FormEvent) => { + e.preventDefault(); + resetErrorValidation(errorSetters); // Reset validation errors before a new password update attempt. + const isValid = validateInputs({ + username, + password, + passwordVerify, + errorSetters + }); // Validate Inputs using Auth helper function + if (!isValid) { + console.log('Validation failed, not updating password.'); + return; + } + try { + const isUpdated = await updatePassword(username, password); + console.log(isUpdated); + if (isUpdated === 'Success') { + history.push('/login'); + } else { + console.log( + 'Update password failed: Unknown or unhandled error', + isUpdated + ); + } + } catch (err) { + console.error( + 'Error during password updating in handleUpdatePassword:', + err + ); + } + }; + + return ( + + + + + + + + + + Please Enter In Your New Password + + + + + + + + + + + + + + + Update Password + + + + + + Already have an account? + Sign In + + + + + + + + + + + + + ); +}; + +export default withRouter(FBPassWord); diff --git a/app/src/components/login/SignIn.tsx b/app/src/components/login/SignIn.tsx index f32d2642..ac1b83b7 100644 --- a/app/src/components/login/SignIn.tsx +++ b/app/src/components/login/SignIn.tsx @@ -1,392 +1,425 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { RouteComponentProps, Link as RouteLink } from 'react-router-dom'; -import { SigninDark } from '../../../../app/src/public/styles/theme'; -import { - StyledEngineProvider, - Theme, - ThemeProvider -} from '@mui/material/styles'; -import { useDispatch } from 'react-redux'; -import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; -import { LoginInt } from '../../interfaces/Interfaces'; -import serverConfig from '../../serverConfig.js'; -import makeStyles from '@mui/styles/makeStyles'; -import { sessionIsCreated } from '../../helperFunctions/auth'; -import { - Divider, - Box, - Avatar, - Button, - Container, - CssBaseline, - Grid, - TextField, - Typography -} from '@mui/material'; - -const { API_BASE_URL } = serverConfig; - -declare module '@mui/styles/defaultTheme' { - interface DefaultTheme extends Theme {} -} - -function Copyright() { - return ( - - {'Copyright © ReacType '} - {new Date().getFullYear()} - {'.'} - - ); -} - -const useStyles = makeStyles((theme) => ({ - paper: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center' - }, - avatar: { - backgroundColor: 'white' - }, - form: { - width: '100%' - }, - submit: { - cursor: 'pointer' - }, - root: { - '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { - borderColor: 'white' - } - } -})); - -const SignIn: React.FC = (props) => { - const classes = useStyles(); - const dispatch = useDispatch(); - - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [invalidUserMsg, setInvalidUserMsg] = useState(''); - const [invalidPassMsg, setInvalidPassMsg] = useState(''); - const [invalidUser, setInvalidUser] = useState(false); - const [invalidPass, setInvalidPass] = useState(false); - - useEffect(() => { - const githubCookie = setInterval(() => { - window.api?.setCookie(); - window.api?.getCookie((cookie) => { - if (cookie[0]) { - window.localStorage.setItem('ssid', cookie[0].value); - clearInterval(githubCookie); - props.history.push('/'); - } else if (window.localStorage.getItem('ssid')) { - clearInterval(githubCookie); - } - }); - }, 2000); - }, []); - - const handleChange = (e: React.ChangeEvent) => { - let inputVal = e.target.value; - - switch (e.target.name) { - case 'username': - setUsername(inputVal); - break; - - case 'password': - setPassword(inputVal); - break; - } - }; - - const handleLogin = (e: React.MouseEvent) => { - e.preventDefault(); - setInvalidUser(false); - setInvalidUserMsg(''); - setInvalidPass(false); - setInvalidPassMsg(''); - sessionIsCreated(username, password, false).then((loginStatus) => { - if (loginStatus === 'Success') { - props.history.push('/'); - } else { - switch (loginStatus) { - case 'No Username Input': - setInvalidUser(true); - setInvalidUserMsg(loginStatus); - break; - - case 'No Password Input': - setInvalidPass(true); - setInvalidPassMsg(loginStatus); - break; - - case 'Invalid Username': - setInvalidUser(true); - setInvalidUserMsg(loginStatus); - break; - - case 'Incorrect Password': - setInvalidPass(true); - setInvalidPassMsg(loginStatus); - break; - } - } - }); - }; - - const keyBindSignIn = useCallback((e) => { - if (e.key === 'Enter') { - e.preventDefault(); - document.getElementById('SignIn').click(); - } - }, []); - - useEffect(() => { - document.addEventListener('keydown', keyBindSignIn); - return () => { - document.removeEventListener('keydown', keyBindSignIn); - }; - }, []); - - const handleLoginGuest = ( - e: React.MouseEvent - ) => { - e.preventDefault(); - window.localStorage.setItem('ssid', 'guest'); - props.history.push('/'); - }; - - const handleGithubLogin = ( - e: React.MouseEvent - ) => { - e.preventDefault(); - window.location.assign(`${API_BASE_URL}/auth/github`); - }; - - const classBtn = - 'MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-4 MuiButton-fullWidth'; - - return ( - - - - -
- - - - - Log in - - - - - - - - OR - - - handleGithubLogin(e)} - sx={{ - marginBottom: '1rem', - textTransform: 'none', - fontSize: '1rem', - color: 'white', - '$:hover': { - cursor: 'pointer', - color: 'black', - textDecoration: 'underline' - } - }} - > - - - - Sign In With Github - - { - e.preventDefault(); - window.location.assign(`${API_BASE_URL}/auth/google`); - }} - sx={{ - marginBottom: '1rem', - textTransform: 'none', - fontSize: '1rem', - color: 'white', - '$:hover': { - cursor: 'pointer', - color: 'black' - } - }} - > - - - - Sign in With Google - - handleLoginGuest(e)} - sx={{ - marginBottom: '1rem', - textTransform: 'none', - fontSize: '1rem', - color: 'white', - '$:hover': { - cursor: 'pointer', - color: 'black' - } - }} - > - - - - - Continue as Guest - - - Forgot password? - - - - - Don't have an account? - Sign Up - - -
- - - -
-
-
- ); -}; - -export default SignIn; +import React, { useCallback, useEffect, useState } from 'react'; +import { + RouteComponentProps, + Link as RouteLink, + useHistory +} from 'react-router-dom'; +import { SigninDark } from '../../../../app/src/public/styles/theme'; +import { + StyledEngineProvider, + Theme, + ThemeProvider +} from '@mui/material/styles'; +import { styled } from '@mui/material/styles'; +import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; +import { LoginInt } from '../../interfaces/Interfaces'; +import serverConfig from '../../serverConfig.js'; +import makeStyles from '@mui/styles/makeStyles'; +import { + sessionIsCreated, + handleChange, + resetErrorValidation, + validateInputs +} from '../../helperFunctions/auth'; +import { + Divider, + Box, + Avatar, + Button, + Container, + CssBaseline, + TextField, + Typography +} from '@mui/material'; + +const { API_BASE_URL } = serverConfig; + +declare module '@mui/styles/defaultTheme' { + interface DefaultTheme extends Theme {} +} + +function Copyright() { + return ( + + {'Copyright © ReacType '} + {new Date().getFullYear()} + {'.'} + + ); +} + +const useStyles = makeStyles((theme) => ({ + paper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + }, + avatar: { + backgroundColor: 'white' + }, + form: { + width: '100%' + }, + submit: { + cursor: 'pointer' + }, + root: { + '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: 'white' + } + } +})); + +const StyledForm = styled('form')(({ theme }) => ({ + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing(3) +})); + +const SignIn: React.FC = () => { + const classes = useStyles(); + const history = useHistory(); + + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [invalidUserMsg, setInvalidUserMsg] = useState(''); + const [invalidPassMsg, setInvalidPasswordMsg] = useState(''); + const [invalidUser, setInvalidUser] = useState(false); + const [invalidPass, setInvalidPassword] = useState(false); + + /** + * Periodically checks for specific cookies and manages session state based on their presence. + * If a specific cookie is found, it stores the session ID in local storage and redirects to the home page. + * The check stops once the necessary cookie is found or if a session ID already exists in local storage. + */ + useEffect(() => { + const githubCookie = setInterval(() => { + window.api?.setCookie(); + window.api?.getCookie((cookie) => { + if (cookie[0]) { + window.localStorage.setItem('ssid', cookie[0].value); + clearInterval(githubCookie); + history.push('/'); + } else if (window.localStorage.getItem('ssid')) { + clearInterval(githubCookie); + } + }); + }, 2000); + }, []); + + // define error setters to pass to resetErrorValidation function + const errorSetters = { + setInvalidUser, + setInvalidUserMsg, + setInvalidPassword, + setInvalidPasswordMsg + }; + // define handle change setters to pass to handleChange function + const handleChangeSetters = { + setUsername, + setPassword + }; + + /** + * Handles input changes for form fields and updates the state accordingly. + * This function delegates to the `handleChange` function, passing the event + * and the `handleChangeSetters` for updating the specific state tied to the input fields. + * @param {React.ChangeEvent} e - The event object that triggered the change. + */ + const handleInputChange = (e: React.ChangeEvent): void => { + handleChange(e, handleChangeSetters); + }; + + /** + * Handles the form submission for user login. This function prevents the default form submission behavior, + * resets any previous validation errors, and attempts to create a session with the provided credentials. + * If successful, the user is redirected to the home page. Otherwise, it updates the UI with appropriate error messages + * based on the error type returned from the login attempt. + * @param {React.FormEvent} e - The event object that triggered the form submission, typically used to prevent the default form behavior. + */ + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); // Prevent default form submission behavior. + resetErrorValidation(errorSetters); // Reset validation errors before a new login attempt. + const isValid = validateInputs({ + username, + password, + errorSetters + }); // Validate Inputs using Auth helper function + if (!isValid) { + console.log('Validation failed, login attempt not processed.'); + return; + } + // Attempt to create a session using the provided credentials. + try { + const loginStatus = await sessionIsCreated(username, password, false); + if (loginStatus === 'Success') { + console.log('Login successful, redirecting...'); + history.push('/'); + } else { + if ( + [ + 'No Username Input', + 'No Password Input', + 'Invalid Username', + 'Incorrect Password' + ].includes(loginStatus) + ) { + setInvalidUser(true); + setInvalidUserMsg(loginStatus); + } else { + console.error('Unhandled error during login:', loginStatus); + } + } + } catch (err) { + console.error('Error during signin in handleLogin:', err); + } + }; + + /** + * Handles the "Enter" key press to trigger a sign-in button click. + * This function checks if the pressed key is "Enter" and, if so, prevents the default action + * and programmatically clicks the sign-in button. This allows users to submit the form by pressing Enter. + * @param {KeyboardEvent} e - The keyboard event that triggered this handler. + */ + const keyBindSignIn = useCallback((e) => { + if (e.key === 'Enter') { + e.preventDefault(); + document.getElementById('SignIn').click(); + } + }, []); + + /** + * Sets up and cleans up the keydown event listener for the sign-in form. + * This effect binds the 'Enter' key to trigger a sign-in button click across the component. + * It ensures that the event listener is removed when the component unmounts to prevent memory leaks + * and unintended behavior in other parts of the application. + */ + useEffect(() => { + document.addEventListener('keydown', keyBindSignIn); + return () => { + document.removeEventListener('keydown', keyBindSignIn); + }; + }, []); + + return ( + + + + +
+ + + + + Log in + + + + + + + + + OR + + + { + e.preventDefault(); + window.location.assign(`${API_BASE_URL}/auth/github`); + }} + sx={{ + marginBottom: '1rem', + textTransform: 'none', + fontSize: '1rem', + color: 'white', + '$:hover': { + cursor: 'pointer', + color: 'black', + textDecoration: 'underline' + } + }} + > + + + + Sign In With Github + + { + e.preventDefault(); + window.location.assign(`${API_BASE_URL}/auth/google`); + }} + sx={{ + marginBottom: '1rem', + textTransform: 'none', + fontSize: '1rem', + color: 'white', + '$:hover': { + cursor: 'pointer', + color: 'black' + } + }} + > + + + + Sign in With Google + + { + e.preventDefault(); + window.localStorage.setItem('ssid', 'guest'); + history.push('/'); + }} + sx={{ + marginBottom: '1rem', + textTransform: 'none', + fontSize: '1rem', + color: 'white', + '$:hover': { + cursor: 'pointer', + color: 'black' + } + }} + > + + + + + Continue as Guest + + + Forgot password? + + + + + Don't have an account? + Sign Up + + +
+ + + +
+
+
+ ); +}; + +export default SignIn; diff --git a/app/src/components/login/SignUp.tsx b/app/src/components/login/SignUp.tsx index 5def1d5b..726d2664 100644 --- a/app/src/components/login/SignUp.tsx +++ b/app/src/components/login/SignUp.tsx @@ -2,18 +2,15 @@ import React, { useState } from 'react'; import { RouteComponentProps, Link as RouteLink, - withRouter + withRouter, + useHistory } from 'react-router-dom'; -import { - SigninDark, - SigninLight -} from '../../../../app/src/public/styles/theme'; +import { SigninDark } from '../../../../app/src/public/styles/theme'; import { StyledEngineProvider, Theme, ThemeProvider } from '@mui/material/styles'; -import { useDispatch, useSelector } from 'react-redux'; import { Box, Avatar, @@ -27,10 +24,15 @@ import { import AssignmentIcon from '@mui/icons-material/Assignment'; import { LoginInt } from '../../interfaces/Interfaces'; -import { RootState } from '../../redux/store'; import makeStyles from '@mui/styles/makeStyles'; -import { newUserIsCreated } from '../../helperFunctions/auth'; +import { + newUserIsCreated, + handleChange, + resetErrorValidation, + validateInputs, + setErrorMessages +} from '../../helperFunctions/auth'; declare module '@mui/styles/defaultTheme' { // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -66,135 +68,97 @@ const useStyles = makeStyles((theme) => ({ } })); -const SignUp: React.FC = (props) => { +const SignUp: React.FC = () => { const classes = useStyles(); - const dispatch = useDispatch(); + const history = useHistory(); const [email, setEmail] = useState(''); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [passwordVerify, setPasswordVerify] = useState(''); const [invalidEmailMsg, setInvalidEmailMsg] = useState(''); - const [invalidUsernameMsg, setInvalidUsernameMsg] = useState(''); + const [invalidUsernameMsg, setInvalidUserMsg] = useState(''); const [invalidPasswordMsg, setInvalidPasswordMsg] = useState(''); const [invalidVerifyPasswordMsg, setInvalidVerifyPasswordMsg] = useState(''); const [invalidEmail, setInvalidEmail] = useState(false); - const [invalidUsername, setInvalidUsername] = useState(false); + const [invalidUsername, setInvalidUser] = useState(false); const [invalidPassword, setInvalidPassword] = useState(false); const [invalidVerifyPassword, setInvalidVerifyPassword] = useState(false); - const handleChange = (e: React.ChangeEvent) => { - let inputVal = e.target.value; - switch (e.target.name) { - case 'email': - setEmail(inputVal); - break; - case 'username': - setUsername(inputVal); - break; - case 'password': - setPassword(inputVal); - break; - case 'passwordVerify': - setPasswordVerify(inputVal); - break; - } + // define error setters to pass to resetErrorValidation function + const errorSetters = { + setInvalidEmail, + setInvalidEmailMsg, + setInvalidUser, + setInvalidUserMsg, + setInvalidPassword, + setInvalidPasswordMsg, + setInvalidVerifyPassword, + setInvalidVerifyPasswordMsg + }; + // define handle change setters to pass to handleChange function + const handleChangeSetters = { + setEmail, + setUsername, + setPassword, + setPasswordVerify }; - const handleSignUp = (e: React.MouseEvent) => { - e.preventDefault(); - - // Reset Error Validation - setInvalidEmailMsg(''); - setInvalidUsernameMsg(''); - setInvalidPasswordMsg(''); - setInvalidVerifyPasswordMsg(''); - setInvalidEmail(false); - setInvalidUsername(false); - setInvalidPassword(false); - setInvalidVerifyPassword(false); - - if (email === '') { - setInvalidEmail(true); - setInvalidEmailMsg('No Email Entered'); - return; - } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(email)) { - setInvalidEmail(true); - setInvalidEmailMsg('Invalid Email Format'); - return; - } else { - setInvalidEmail(false); - } - - if (username === '') { - setInvalidUsername(true); - setInvalidUsernameMsg('No Username Entered'); - return; - } else if (!/^[\w\s-]{4,15}$/i.test(username)) { - setInvalidUsername(true); - setInvalidUsernameMsg('Must Be 4 - 15 Characters Long'); - return; - } else if (!/^[\w-]+$/i.test(username)) { - setInvalidUsername(true); - setInvalidUsernameMsg('Cannot Contain Spaces or Special Characters'); - return; - } else { - setInvalidUsername(false); - } + /** + * Handles input changes for form fields and updates the state accordingly. + * This function delegates to the `handleChange` function, passing the event + * and the `handleChangeSetters` for updating the specific state tied to the input fields. + * @param {React.ChangeEvent} e - The event object that triggered the change. + */ + const handleInputChange = (e: React.ChangeEvent): void => { + handleChange(e, handleChangeSetters); + }; - if (password === '') { - setInvalidPassword(true); - setInvalidPasswordMsg('No Password Entered'); - return; - } else if (password.length < 8) { - setInvalidPassword(true); - setInvalidPasswordMsg('Minimum 8 Characters'); - return; - } else if ( - !/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/i.test( - password - ) - ) { - setInvalidPassword(true); - setInvalidPasswordMsg('Minimum 1 Letter, Number, and Special Character'); - return; - } else if (password !== passwordVerify) { - setInvalidPassword(true); - setInvalidVerifyPassword(true); - setInvalidPasswordMsg('Verification Failed'); - setInvalidVerifyPasswordMsg('Verification Failed'); - setPasswordVerify(''); - return; - } else { - setInvalidPassword(false); - } + /** + * Handles the form submission for user registration. Prevents default form behavior, + * validates user inputs, and attempts to register a new user. Redirects to home on success, + * otherwise displays error messages based on the response. + * @param {React.MouseEvent} e - The event object that triggered the submission. + */ + const handleSignUp = async ( + e: React.MouseEvent + ) => { + e.preventDefault(); + resetErrorValidation(errorSetters); // Reset validation errors before a new signup attempt. + const isValid = validateInputs({ + email, + username, + password, + passwordVerify, + errorSetters + }); // Validate Inputs using Auth helper function - if (password !== passwordVerify) { - setInvalidPassword(true); - setInvalidVerifyPassword(true); - setInvalidPasswordMsg('Verification Failed'); - setInvalidVerifyPasswordMsg('Verification Failed'); - setPasswordVerify(''); + if (!isValid) { + console.log('Validation failed, account not created.'); return; - } else { - setInvalidVerifyPassword(false); } - - newUserIsCreated(username, email, password).then((userCreated) => { + try { + const userCreated = await newUserIsCreated(username, email, password); if (userCreated === 'Success') { - props.history.push('/'); + console.log('Account creation successful, redirecting...'); + history.push('/'); } else { switch (userCreated) { case 'Email Taken': - setInvalidEmail(true); - setInvalidEmailMsg('Email Taken'); + setErrorMessages('email', 'Email Taken', errorSetters); break; case 'Username Taken': - setInvalidUsername(true); - setInvalidUsernameMsg('Username Taken'); + setErrorMessages('username', 'Username Taken', errorSetters); break; + default: + console.log( + 'Signup failed: Unknown or unhandled error', + userCreated + ); } } - }); + } catch (error) { + console.error('Error during signup in handleSignUp:', error); + } }; return ( @@ -227,15 +191,15 @@ const SignUp: React.FC = (props) => { className={classes.root} variant="outlined" required - fullWidth id="email" label="Email" name="email" autoComplete="email" value={email} - onChange={handleChange} + onChange={handleInputChange} helperText={invalidEmailMsg} error={invalidEmail} + sx={{ width: '100%' }} /> @@ -243,15 +207,15 @@ const SignUp: React.FC = (props) => { className={classes.root} variant="outlined" required - fullWidth id="username" label="Username" name="username" autoComplete="username" value={username} - onChange={handleChange} + onChange={handleInputChange} helperText={invalidUsernameMsg} error={invalidUsername} + sx={{ width: '100%' }} /> @@ -259,16 +223,16 @@ const SignUp: React.FC = (props) => { className={classes.root} variant="outlined" required - fullWidth name="password" label="Password" type="password" id="password" autoComplete="current-password" value={password} - onChange={handleChange} + onChange={handleInputChange} helperText={invalidPasswordMsg} error={invalidPassword} + sx={{ width: '100%' }} /> @@ -276,16 +240,16 @@ const SignUp: React.FC = (props) => { className={classes.root} variant="outlined" required - fullWidth name="passwordVerify" label="Verify Password" type="password" id="passwordVerify" autoComplete="verify-password" value={passwordVerify} - onChange={handleChange} + onChange={handleInputChange} helperText={invalidVerifyPasswordMsg} error={invalidVerifyPassword} + sx={{ width: '100%' }} /> @@ -315,7 +279,6 @@ const SignUp: React.FC = (props) => {