-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement responsive control panel login form for issue #822
- Collect and send user credentials via POST request to /api/v1/auth/login - Handle response and store session cookie in browser's cookie jar - Redirect user to dashboard view after successful login - Ensure responsiveness of login form for all screen sizes This commit addresses issue #822 by implementing a responsive login form for the control panel. As per the specs, the form collects user credentials, communicates with the server, handles the response, stores the session cookie, and redirects the user to the dashboard view upon successful login.
- Loading branch information
1 parent
c0437b1
commit f355335
Showing
6 changed files
with
288 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
require('dotenv').config() | ||
const express = require('express') | ||
const router = express.Router() | ||
|
||
const uuid = require('uuid') // generates session IDs | ||
const { | ||
getPublicMessage, | ||
getProtectedMessage | ||
|
@@ -22,4 +22,36 @@ router.get('/protected-message', checkJwt, (req, res) => { | |
res.status(200).send(message) | ||
}) | ||
|
||
// below is the logic for issue #822 in regards to creating a login page | ||
|
||
// Mock user data | ||
const users = { | ||
'[email protected]': { password: 'testPassword' } | ||
} | ||
|
||
// Mock session store | ||
const sessions = {} | ||
|
||
router.post('/login', (req, res) => { | ||
const { email, password } = req.body | ||
|
||
// Look up user by email | ||
const user = users[email] | ||
|
||
if (!user || user.password !== password) { | ||
return res.status(400).json({ message: 'Invalid email/password' }).end() | ||
} | ||
|
||
// Generate a session ID | ||
const sessionId = uuid.v4() | ||
|
||
// Store session data on the server | ||
sessions[sessionId] = { email } | ||
|
||
// Set a cookie named 'session' that holds the value of sessionId that expires in 900000 ms (15 min) | ||
res.cookie('session', sessionId, { httpOnly: true, maxAge: 900000 }) | ||
|
||
res.status(200).json({ message: 'Login successful' }).end() | ||
}) | ||
|
||
module.exports = router |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
<template> | ||
<v-container> | ||
<v-row justify="center"> | ||
<v-col cols="12" sm="8" md="6" class="mx-auto ma-0 ma-sm-4 pa-0 pa-sm-4"> | ||
<v-card | ||
class="mx-auto pa-12 pb-8" | ||
elevation="9" | ||
max-width="600" | ||
rounded="lg" | ||
> | ||
<!-- Form title --> | ||
<div class="text-h4 font-weight-bold text-start"> | ||
Sign into Amplify | ||
</div> | ||
|
||
<v-form @submit.prevent="login"> | ||
<v-text-field | ||
v-model="email" | ||
label="Email Address" | ||
@input="onEmailInput" | ||
:error-messages="emailError" | ||
placeholder="Enter your email address" | ||
density="compact" | ||
type="email" | ||
required | ||
aria-label="Email Address" | ||
/> | ||
|
||
<v-text-field | ||
v-model="password" | ||
label="Password" | ||
@input="onPasswordInput" | ||
:error-messages="passwordError" | ||
:append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'" | ||
placeholder="Enter your password" | ||
density="compact" | ||
:type="show1 ? 'text' : 'password'" | ||
@click:append="show1 = !show1" | ||
required | ||
aria-label="Password" | ||
/> | ||
|
||
<div | ||
class="text-subtitle-1 text-medium-emphasis d-flex align-center justify-space-between" | ||
> | ||
<v-checkbox v-model="rememberMe" label="Remember me" /> | ||
<a | ||
class="forgot-password text-caption text-decoration-none text-blue" | ||
href="#" | ||
rel="noopener noreferrer" | ||
target="_blank" | ||
> | ||
Forgot password? | ||
</a> | ||
</div> | ||
|
||
<div v-if="errorMessage" class="error-message"> | ||
{{ errorMessage }} | ||
</div> | ||
|
||
<v-btn | ||
color="primary" | ||
type="submit" | ||
:disabled="!email || !password" | ||
block | ||
> | ||
<v-progress-circular v-if="loading" indeterminate size="24" /> | ||
<span v-else>Log in</span> | ||
</v-btn> | ||
</v-form> | ||
</v-card> | ||
</v-col> | ||
</v-row> | ||
</v-container> | ||
</template> | ||
|
||
<script> | ||
import axios from 'axios' | ||
export default { | ||
data() { | ||
return { | ||
email: '', | ||
password: '', | ||
show1: false, | ||
loading: false, | ||
rememberMe: false, | ||
errorMessage: '', | ||
emailError: '', | ||
emailTimer: null, | ||
passwordError: '', | ||
passwordTimer: null | ||
} | ||
}, | ||
created() { | ||
// Load saved email and rememberMe flag from localStorage | ||
const savedEmail = localStorage.getItem('rememberedEmail') | ||
const savedRememberMe = localStorage.getItem('rememberMe') | ||
if (savedRememberMe === 'true') { | ||
this.rememberMe = true | ||
this.email = savedEmail || '' | ||
} | ||
}, | ||
methods: { | ||
async login() { | ||
try { | ||
this.loading = true | ||
const response = await axios.post( | ||
'/api/v1/auth/login', | ||
{ | ||
// send the email and password to the server as part of the request body | ||
email: this.email, | ||
password: this.password | ||
}, | ||
{ | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
// enables cross origin cookie sending functionality | ||
withCredentials: true | ||
} | ||
) | ||
if (response.status === 200) { | ||
this.$router.push('/dashboard') | ||
} else { | ||
// If the login was unsuccessful, show a custom error message | ||
this.errorMessage = | ||
'Login failed. Please check your email and password and try again.' | ||
} | ||
} catch (error) { | ||
if (error.response) { | ||
// The request was made and the server responded with a status code that falls out of the range of 2xx | ||
this.errorMessage = | ||
'Login failed. Please check your email and password and try again.' | ||
console.error('Error response:', error.response) | ||
} else if (error.request) { | ||
// The request was made but no response was received | ||
this.errorMessage = | ||
'No response received from the server. Please check your network connection.' | ||
console.error('Error request:', error.request) | ||
} else { | ||
// Something happened in setting up the request that triggered an Error | ||
this.errorMessage = | ||
'An error occurred while trying to log in. Please try again later.' | ||
console.error('Error message:', error.message) | ||
} | ||
} finally { | ||
this.loading = false | ||
} | ||
// Save email and rememberMe flag to localStorage | ||
if (this.rememberMe) { | ||
localStorage.setItem('rememberedEmail', this.email) | ||
localStorage.setItem('rememberMe', 'true') | ||
} else { | ||
localStorage.removeItem('rememberedEmail') | ||
localStorage.removeItem('rememberMe') | ||
} | ||
}, | ||
onPasswordInput(val) { | ||
clearTimeout(this.passwordTimer) | ||
this.passwordTimer = setTimeout(() => { | ||
this.passwordError = | ||
val && val.length >= 5 | ||
? '' | ||
: 'Password required and should be at least 5 characters long' | ||
}, 2000) // 2 second delay | ||
}, | ||
onEmailInput(val) { | ||
clearTimeout(this.emailTimer) | ||
this.emailTimer = setTimeout(() => { | ||
const pattern = /^[^@]+@[^@]+\.[^@]+$/ | ||
this.emailError = pattern.test(val) ? '' : 'Invalid email.' | ||
}, 2000) // 2 second delay | ||
} | ||
} | ||
} | ||
</script> | ||
|
||
<style scoped> | ||
.forgot-password { | ||
font-size: 0.95rem !important; | ||
} | ||
.v-btn { | ||
margin: 1rem 0; | ||
} | ||
.v-application .text-h4 { | ||
padding: 15px 0; | ||
} | ||
.error-message { | ||
color: red; | ||
font-size: 0.9rem; | ||
margin: 1rem 0; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<template> | ||
<div> | ||
<br /> | ||
<br /> | ||
<br /> | ||
<br /> | ||
<h1>Dashboard</h1> | ||
<br /> | ||
<br /> | ||
<p>This is a stub for the Dashboard view.</p> | ||
<br /> | ||
<br /> | ||
<br /> | ||
<br /> | ||
<br /> | ||
</div> | ||
</template> | ||
|
||
<script> | ||
export default { | ||
name: 'Dashboard' | ||
} | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<template> | ||
<ControlPanelLoginForm /> | ||
</template> | ||
|
||
<script> | ||
import ControlPanelLoginForm from '@/components/ControlPanelLoginForm.vue' | ||
export default { | ||
name: 'Login', | ||
components: { | ||
ControlPanelLoginForm | ||
} | ||
} | ||
</script> |