Skip to content

Commit

Permalink
Merge pull request #27 from Raghuboi/main
Browse files Browse the repository at this point in the history
added login/signup page with basic functionality. Closes #19
  • Loading branch information
xTrig authored Oct 16, 2021
2 parents 5e4236d + ddb5427 commit bc7518b
Show file tree
Hide file tree
Showing 12 changed files with 761 additions and 51 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ To check you've set it up correctly, run `docker-compose --version` and it shoul

2. Navigate to the directory: `cd student-net`

3. Create a .env file instead of the `cd web` directory.
3. Create a .env file inside of the `cd web` directory.
```sh
DB_HOST=db
DB_USER=root
Expand All @@ -69,7 +69,7 @@ To check you've set it up correctly, run `docker-compose --version` and it shoul

5. To **view the site** in the development mode, **navigate to `http://localhost:3000` **from a browser.

6. Try changing some of the HTML/CSS/JS content in `./web/pages/_app.tsx` (or wherever really, as long as it is inside `./web`) to see live changes on the page.
6. Try changing some of the HTML/CSS/JS content in `./web/pages/_app.js` (or wherever really, as long as it is inside `./web`) to see live changes on the page.

In fact, any changes you make to the files in `student-net/web/` will automatically trigger a re-render of the page from within your Docker container so you **don't have to start/stop any server or even refresh the page**. Sometimes, it may happen that you break some part of the app and save the file. For example, if you have `<di>Forgot a 'v' in 'div'. Oh no!</div>` in your code it will cause the app to break.

Expand Down
502 changes: 453 additions & 49 deletions web/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"url": "https://github.com/UWCodeForce/student-net/issues"
},
"dependencies": {
"bcrypt": "^5.0.1",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"mysql2": "^2.3.0",
Expand Down
1 change: 1 addition & 0 deletions web/pages/_app.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '../styles/globals.css'
import '../styles/utilities.css'

function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
Expand Down
43 changes: 43 additions & 0 deletions web/pages/api/auth/signin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import nextConnect from 'next-connect'
import bcrypt from 'bcrypt'
import { query } from '../../../utils/query'

const handler = nextConnect()

.post(async (req, res) => {
const { email, password } = req.body

if (!email || !password) res.status(400).json({ error: 'Email or Password cannot be blank' })

try {
const results = await query(`
SELECT password FROM users WHERE email = ?
`, [email])

// will need to find a better way to do this in the future
// currently cannot directly parse the 'RowPacket' that mysql2 returns by using results[0].password
const hashedPW = JSON.parse(JSON.stringify(results[0][0].password))

if (!results[0]) return res.status(401).json({ error: 'Email not found!' })

const valid = await bcrypt.compare(password, hashedPW)

if (valid) {
/*
todo:
- create a session for the user, store it in db
- store session in cookie/localstorage/sessionstorage
- check session with server upon refresh
- two factor authentication ?
*/
return res.status(200).json({ message: 'Success!' })
}

else if (!valid) return res.status(401).json({ error: 'Incorrect Password!' })

} catch (e) {
res.status(500).json({ error: e.message })
}
})

export default handler
34 changes: 34 additions & 0 deletions web/pages/api/auth/signup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import nextConnect from 'next-connect'
import bcrypt from 'bcrypt'
import { query } from '../../../utils/query'
import { SALT_ROUNDS } from '../../../utils/systemconstants'

const handler = nextConnect()

.post(async (req, res) => {
const { email, password } = await req.body

if (!email || !password) res.json({ error: 'Email or Password cannot be blank!' })

try {

/*
todo:
- validate password: see if it has 8 digits and 1 symbol
- add check to see if email has @uwinnipeg.ca or @webmail.uwinnipeg.ca
- add check to see if email exists
- send validation link to email
*/

const hashedPW = await bcrypt.hash(password, SALT_ROUNDS)
const results = await query(`
INSERT INTO users(email, password) VALUES(?, ?)
`, [email, hashedPW])

return res.status(200).json({ message: 'Registered!' })
} catch (e) {
res.status(500).json({ message: e.message })
}
})

export default handler
50 changes: 50 additions & 0 deletions web/pages/signin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { useState } from 'react'
import styles from '../styles/auth.module.css'
import { useRouter } from 'next/router'

export default function signin() {
const Router = useRouter()
const [response, setResponse] = useState()

async function onSubmit(e) {
e.preventDefault()

const body = {
email: e.currentTarget.email.value,
password: e.currentTarget.password.value,
}

let res = await fetch('./api/auth/signin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})

if (res.status===200) {
// store session in cookie/localstorage/sessionstorage
Router.push('/')
} else {
res = await res.json()
setResponse(res)
}
}

return (
<div className={[styles.auth, "noselect"].join(' ')}> {/* hacky way to add multiple classes */}

<h1>Sign In</h1>

{response && response.message && <p style={{color: "green"}}>{response.message}</p>}
{response && response.error && <p style={{color: "red"}}>{response.error}</p>}

{/* could use Formik or another library in the future */}
<form className={styles.form} onSubmit={e => onSubmit(e)} >
<input className={styles.field} name="email" placeholder="Email"/>
<input className={styles.field} type="password" name="password" placeholder="Password"/>
<button className={styles.submit} type="submit">Sign In</button>
</form>

</div>
)
}

57 changes: 57 additions & 0 deletions web/pages/signup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { useState } from 'react'
import styles from '../styles/auth.module.css'

export default function signup() {
const [response, setResponse] = useState()

async function onSubmit(e) {
e.preventDefault()

const pass1 = e.currentTarget.password1.value
const pass2 = e.currentTarget.password2.value

if (pass1!==pass2) {
setResponse({ error: 'Passwords do not match!' })
return
} else if (!pass1 || !pass2) {
setResponse({ error: 'Please enter and confirm a password.' })
return
}

const body = {
email: e.currentTarget.email.value,
password: pass1,
}

let res = await fetch('./api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})

res = await res.json()
setResponse(res)
}



return (
<div className={[styles.auth, "noselect"].join(' ')}> {/* hacky way to add multiple classes */}

<h1>Sign Up</h1>

{response && response.message && <p className={styles.submitMessage} style={{color: "green"}}>{response.message}</p>}
{response && response.error && <p className={styles.submitMessage} style={{color: "red"}}>{response.error}</p>}

{/* could use Formik or another library in the future */}
<form className={styles.form} onSubmit={e => onSubmit(e)}>
<input className={styles.field} name="email" placeholder="Email"/>
<input className={styles.field} type="password" name="password1" placeholder="Password"/>
<input className={styles.field} type="password" name="password2" placeholder="Confirm Password"/>
<button className={styles.submit} type="submit">Sign Up</button>
</form>

</div>
)
}

90 changes: 90 additions & 0 deletions web/styles/auth.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
.form, .auth {
display: flex;
flex-direction: column;
align-items: center;
}

.auth {
gap: 2rem;
border-style: none;
padding: 3rem 3rem;
background-color: #fff;
border-radius: 1.5rem;
box-shadow: 0 1rem 2rem rgba(0,0,0,0.25),
0 0.75rem 0.75rem rgba(0,0,0,0.22);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
}

.auth h1 {
margin: 0;
pointer-events: none;
}

.auth p {
font-size: 14px;
line-height: 20px;
letter-spacing: 0.5px;
margin: 0;
}

.auth input {
font-size: 14px;
background-color: #eee;
border: none;
padding: 1rem 1.5rem;
}

.form {
font-size: 16px;
gap: 1rem;
width: fit-content;
height: fit-content;
}

.submit {
margin-top: 1rem;
border-radius: 1.25rem;
border: 1px solid #FF4B2B;
background-color: #FF4B2B;
color: #FFFFFF;
font-size: 12px;
font-weight: bold;
padding: 0.75rem 2.8125rem;
letter-spacing: 1px;
text-transform: uppercase;
cursor: pointer;
transition: transform 80ms ease-in;
}

.submit:active {
transform: scale(0.95);
}

.submit:focus {
outline: none;
}

.submit:hover {
transform: scale(1.1);
}

.submit.ghost {
background-color: transparent;
border-color: #FFFFFF;
}

@media only screen and (max-width: 600px) {
.form {
max-width: 75vw;
max-height: 50vh;
overflow: auto;
}

.auth input {
padding: 0.75rem;
max-width: 50vw;
}
}
10 changes: 10 additions & 0 deletions web/styles/utilities.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*to disable text selection highlighting*/
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */
}
19 changes: 19 additions & 0 deletions web/utils/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const mysql = require('mysql2/promise');

export const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_DATABASE,
waitForConnections: true,
connectionLimit: 10
})

export async function query(q, values) {
try {
const results = await pool.query(q, values)
return results
} catch (e) {
throw Error(e.message)
}
}
1 change: 1 addition & 0 deletions web/utils/systemconstants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SALT_ROUNDS = 10;

0 comments on commit bc7518b

Please sign in to comment.