Skip to content

Commit

Permalink
Implement responsive control panel login form for issue #822
Browse files Browse the repository at this point in the history
- 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
briansegura15 committed Dec 12, 2023
1 parent c0437b1 commit f355335
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 1 deletion.
4 changes: 4 additions & 0 deletions server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const path = require('path')
const express = require('express')
const history = require('connect-history-api-fallback')
const apiRouter = require('./api')
const authRouter = require('./routes/api/authentication')

const app = express()

Expand All @@ -12,6 +13,9 @@ app.set('trust proxy', true)
// Load the API router
app.use('/api', apiRouter)

// Load the authentication router
app.use('/api/v1/auth', authRouter)

// Fix for hash URLs in Vue
// IMPORTANT: MUST be added before the `express.static` middleware for the `dist/` directory
app.use(history())
Expand Down
34 changes: 33 additions & 1 deletion server/routes/api/authentication.js
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
Expand All @@ -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
204 changes: 204 additions & 0 deletions src/components/ControlPanelLoginForm.vue
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>
10 changes: 10 additions & 0 deletions src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ const routes = [
path: '/complete',
name: 'CompletePage',
component: () => import('../views/CompletePage.vue')
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue')
}
]

Expand Down
23 changes: 23 additions & 0 deletions src/views/Dashboard.vue
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>
14 changes: 14 additions & 0 deletions src/views/Login.vue
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>

0 comments on commit f355335

Please sign in to comment.