diff options
author | Robby Zambito <contact@robbyzambito.me> | 2025-08-02 19:06:42 -0400 |
---|---|---|
committer | Robby Zambito <contact@robbyzambito.me> | 2025-08-02 19:08:31 -0400 |
commit | acfde602ff33e09de5f55942220d9a6dafd5d889 (patch) | |
tree | 34450137a899a58948a4636e4a2d03c281f6ff39 | |
parent | 1ac73ff7cae110f9d0b1aa0bf62283b6d2e26022 (diff) |
Create login page
This is where the exploit will happen.
Prompt:
Create a login page. The login form should make a POST request with the form body to an appropriate API endpoint.
-rw-r--r-- | static/login-script.js | 341 | ||||
-rw-r--r-- | static/login-styles.css | 479 | ||||
-rw-r--r-- | static/login.html | 129 |
3 files changed, 949 insertions, 0 deletions
diff --git a/static/login-script.js b/static/login-script.js new file mode 100644 index 0000000..da69c7b --- /dev/null +++ b/static/login-script.js @@ -0,0 +1,341 @@ +// DOM Elements +const loginForm = document.getElementById('loginForm'); +const emailInput = document.getElementById('email'); +const passwordInput = document.getElementById('password'); +const passwordToggle = document.getElementById('passwordToggle'); +const loginButton = document.getElementById('loginButton'); +const toast = document.getElementById('toast'); + +// Error message elements +const emailError = document.getElementById('emailError'); +const passwordError = document.getElementById('passwordError'); +const generalError = document.getElementById('generalError'); + +// API Configuration +const API_BASE_URL = 'https://api.taskflow.com/v1'; +const LOGIN_ENDPOINT = `${API_BASE_URL}/auth/login`; + +// Password visibility toggle +passwordToggle.addEventListener('click', () => { + const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password'; + passwordInput.setAttribute('type', type); + + const icon = passwordToggle.querySelector('.toggle-icon'); + icon.textContent = type === 'password' ? '👁️' : '🙈'; +}); + +// Form validation functions +const validateEmail = (email) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +}; + +const validatePassword = (password) => { + return password.length >= 6; +}; + +const showError = (element, message) => { + element.textContent = message; + element.style.display = 'block'; +}; + +const hideError = (element) => { + element.textContent = ''; + element.style.display = 'none'; +}; + +const clearAllErrors = () => { + hideError(emailError); + hideError(passwordError); + generalError.classList.remove('show'); + + emailInput.classList.remove('error'); + passwordInput.classList.remove('error'); +}; + +// Real-time validation +emailInput.addEventListener('blur', () => { + const email = emailInput.value.trim(); + if (email && !validateEmail(email)) { + showError(emailError, 'Please enter a valid email address'); + emailInput.classList.add('error'); + } else { + hideError(emailError); + emailInput.classList.remove('error'); + } +}); + +passwordInput.addEventListener('blur', () => { + const password = passwordInput.value; + if (password && !validatePassword(password)) { + showError(passwordError, 'Password must be at least 6 characters long'); + passwordInput.classList.add('error'); + } else { + hideError(passwordError); + passwordInput.classList.remove('error'); + } +}); + +// Clear errors on input +emailInput.addEventListener('input', () => { + if (emailInput.classList.contains('error')) { + hideError(emailError); + emailInput.classList.remove('error'); + } +}); + +passwordInput.addEventListener('input', () => { + if (passwordInput.classList.contains('error')) { + hideError(passwordError); + passwordInput.classList.remove('error'); + } +}); + +// Show toast notification +const showToast = (message, type = 'success') => { + const toastContent = toast.querySelector('.toast-content'); + const toastIcon = toast.querySelector('.toast-icon'); + const toastMessage = toast.querySelector('.toast-message'); + + // Set icon and color based on type + if (type === 'success') { + toastIcon.textContent = '✓'; + toast.style.background = 'var(--success)'; + } else if (type === 'error') { + toastIcon.textContent = '✕'; + toast.style.background = 'var(--error)'; + } + + toastMessage.textContent = message; + toast.classList.add('show'); + + setTimeout(() => { + toast.classList.remove('show'); + }, 4000); +}; + +// Set loading state +const setLoadingState = (loading) => { + if (loading) { + loginButton.classList.add('loading'); + loginButton.disabled = true; + } else { + loginButton.classList.remove('loading'); + loginButton.disabled = false; + } +}; + +// Make login API request +const makeLoginRequest = async (credentials) => { + try { + const response = await fetch(LOGIN_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(credentials) + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || `HTTP error! status: ${response.status}`); + } + + return data; + } catch (error) { + // Handle network errors or API errors + if (error.name === 'TypeError' && error.message.includes('fetch')) { + throw new Error('Network error. Please check your connection and try again.'); + } + throw error; + } +}; + +// Handle successful login +const handleLoginSuccess = (data) => { + // Store authentication data + if (data.token) { + localStorage.setItem('taskflow_token', data.token); + } + + if (data.refreshToken) { + localStorage.setItem('taskflow_refresh_token', data.refreshToken); + } + + if (data.user) { + localStorage.setItem('taskflow_user', JSON.stringify(data.user)); + } + + // Show success message + showToast('Login successful! Redirecting to dashboard...', 'success'); + + // Redirect to dashboard after a short delay + setTimeout(() => { + window.location.href = 'index.html'; // or wherever the dashboard is located + }, 2000); +}; + +// Handle login error +const handleLoginError = (error) => { + console.error('Login error:', error); + + let errorMessage = 'An unexpected error occurred. Please try again.'; + + if (error.message.includes('Invalid credentials') || + error.message.includes('Unauthorized') || + error.message.includes('401')) { + errorMessage = 'Invalid email or password. Please check your credentials and try again.'; + } else if (error.message.includes('Network error')) { + errorMessage = error.message; + } else if (error.message.includes('Too many attempts')) { + errorMessage = 'Too many login attempts. Please try again later.'; + } + + generalError.textContent = errorMessage; + generalError.classList.add('show'); + showToast(errorMessage, 'error'); +}; + +// Form submission handler +loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + // Clear previous errors + clearAllErrors(); + + // Get form data + const email = emailInput.value.trim(); + const password = passwordInput.value; + const rememberMe = document.getElementById('rememberMe').checked; + + // Validate inputs + let hasErrors = false; + + if (!email) { + showError(emailError, 'Email is required'); + emailInput.classList.add('error'); + hasErrors = true; + } else if (!validateEmail(email)) { + showError(emailError, 'Please enter a valid email address'); + emailInput.classList.add('error'); + hasErrors = true; + } + + if (!password) { + showError(passwordError, 'Password is required'); + passwordInput.classList.add('error'); + hasErrors = true; + } else if (!validatePassword(password)) { + showError(passwordError, 'Password must be at least 6 characters long'); + passwordInput.classList.add('error'); + hasErrors = true; + } + + if (hasErrors) { + return; + } + + // Set loading state + setLoadingState(true); + + try { + // Prepare request payload + const credentials = { + email, + password, + rememberMe + }; + + // Make API request + const response = await makeLoginRequest(credentials); + + // Handle success + handleLoginSuccess(response); + + } catch (error) { + // Handle error + handleLoginError(error); + } finally { + // Remove loading state + setLoadingState(false); + } +}); + +// Social login handlers +document.querySelector('.btn-google').addEventListener('click', () => { + // In a real application, this would initiate OAuth flow + showToast('Google login would be initiated here', 'success'); + + // Simulate OAuth redirect + setTimeout(() => { + window.location.href = `${API_BASE_URL}/auth/google?redirect_uri=${encodeURIComponent(window.location.origin + '/dashboard')}`; + }, 1000); +}); + +document.querySelector('.btn-microsoft').addEventListener('click', () => { + // In a real application, this would initiate OAuth flow + showToast('Microsoft login would be initiated here', 'success'); + + // Simulate OAuth redirect + setTimeout(() => { + window.location.href = `${API_BASE_URL}/auth/microsoft?redirect_uri=${encodeURIComponent(window.location.origin + '/dashboard')}`; + }, 1000); +}); + +// Forgot password handler +document.querySelector('.forgot-password').addEventListener('click', (e) => { + e.preventDefault(); + + const email = emailInput.value.trim(); + if (email && validateEmail(email)) { + showToast(`Password reset link sent to ${email}`, 'success'); + } else { + showToast('Please enter a valid email address first', 'error'); + emailInput.focus(); + } +}); + +// Sign up link handler +document.querySelector('a[href="#signup"]').addEventListener('click', (e) => { + e.preventDefault(); + window.location.href = 'signup.html'; // Redirect to signup page +}); + +// Check if user is already logged in +window.addEventListener('load', () => { + const token = localStorage.getItem('taskflow_token'); + if (token) { + // Verify token is still valid (in a real app, you'd make an API call) + showToast('You are already logged in. Redirecting...', 'success'); + setTimeout(() => { + window.location.href = 'index.html'; + }, 2000); + } +}); + +// Demo credentials helper (for development/demo purposes) +const addDemoCredentials = () => { + const demoButton = document.createElement('button'); + demoButton.type = 'button'; + demoButton.className = 'btn btn-secondary btn-full'; + demoButton.textContent = 'Use Demo Credentials'; + demoButton.style.marginTop = '1rem'; + demoButton.style.fontSize = '0.875rem'; + + demoButton.addEventListener('click', () => { + emailInput.value = 'demo@taskflow.com'; + passwordInput.value = 'demo123'; + showToast('Demo credentials filled. Click Sign In to continue.', 'success'); + }); + + // Add after the social login section + const socialLogin = document.querySelector('.social-login'); + socialLogin.appendChild(demoButton); +}; + +// Add demo credentials in development +if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + addDemoCredentials(); +} diff --git a/static/login-styles.css b/static/login-styles.css new file mode 100644 index 0000000..a3c52d8 --- /dev/null +++ b/static/login-styles.css @@ -0,0 +1,479 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #6366f1; + --primary-dark: #4f46e5; + --secondary-color: #f1f5f9; + --text-primary: #1e293b; + --text-secondary: #64748b; + --background: #ffffff; + --surface: #f8fafc; + --border: #e2e8f0; + --error: #ef4444; + --success: #10b981; + --warning: #f59e0b; + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + --gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); +} + +body { + font-family: 'Inter', sans-serif; + line-height: 1.6; + color: var(--text-primary); + background: var(--surface); + min-height: 100vh; +} + +.login-container { + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 100vh; +} + +/* Left Side - Branding */ +.login-left { + background: var(--gradient); + color: white; + padding: 3rem; + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; + overflow: hidden; +} + +.login-left::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>'); + opacity: 0.3; +} + +.login-branding { + position: relative; + z-index: 1; +} + +.brand-logo { + font-size: 2rem; + font-weight: 700; + margin-bottom: 2rem; +} + +.login-branding h1 { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 1rem; + line-height: 1.2; +} + +.login-branding p { + font-size: 1.125rem; + opacity: 0.9; + line-height: 1.6; +} + +.login-testimonial { + position: relative; + z-index: 1; +} + +.login-testimonial blockquote { + font-size: 1.125rem; + font-style: italic; + margin-bottom: 1.5rem; + opacity: 0.95; + line-height: 1.6; +} + +.testimonial-author { + display: flex; + align-items: center; + gap: 1rem; +} + +.author-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + backdrop-filter: blur(10px); +} + +.author-name { + font-weight: 600; +} + +.author-title { + font-size: 0.875rem; + opacity: 0.8; +} + +/* Right Side - Form */ +.login-right { + background: white; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.login-form-container { + width: 100%; + max-width: 400px; +} + +.login-header { + text-align: center; + margin-bottom: 2rem; +} + +.login-header h2 { + font-size: 1.875rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.login-header p { + color: var(--text-secondary); +} + +/* Form Styles */ +.login-form { + margin-bottom: 2rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + font-weight: 500; + margin-bottom: 0.5rem; + color: var(--text-primary); +} + +.form-group input { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: 0.5rem; + font-size: 1rem; + transition: all 0.3s ease; + background: white; +} + +.form-group input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); +} + +.form-group input.error { + border-color: var(--error); +} + +.password-input-container { + position: relative; +} + +.password-toggle { + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + padding: 0.25rem; + color: var(--text-secondary); + transition: color 0.3s ease; +} + +.password-toggle:hover { + color: var(--text-primary); +} + +.toggle-icon { + font-size: 1rem; +} + +.error-message { + color: var(--error); + font-size: 0.875rem; + margin-top: 0.5rem; + min-height: 1.25rem; +} + +.form-options { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.checkbox-container { + display: flex; + align-items: center; + cursor: pointer; + font-size: 0.875rem; + color: var(--text-secondary); +} + +.checkbox-container input { + display: none; +} + +.checkmark { + width: 18px; + height: 18px; + border: 2px solid var(--border); + border-radius: 0.25rem; + margin-right: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.checkbox-container input:checked + .checkmark { + background: var(--primary-color); + border-color: var(--primary-color); +} + +.checkbox-container input:checked + .checkmark::after { + content: '✓'; + color: white; + font-size: 0.75rem; + font-weight: bold; +} + +.forgot-password { + color: var(--primary-color); + text-decoration: none; + font-size: 0.875rem; + font-weight: 500; + transition: color 0.3s ease; +} + +.forgot-password:hover { + color: var(--primary-dark); +} + +/* Buttons */ +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 0.5rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + font-size: 1rem; + position: relative; +} + +.btn-full { + width: 100%; +} + +.btn-primary { + background: var(--gradient); + color: white; + box-shadow: var(--shadow); +} + +.btn-primary:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: var(--shadow-lg); +} + +.btn-primary:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +.btn-social { + background: white; + color: var(--text-primary); + border: 2px solid var(--border); + margin-bottom: 0.75rem; +} + +.btn-social:hover { + border-color: var(--primary-color); + background: var(--surface); +} + +.loading-spinner { + display: none; + width: 20px; + height: 20px; + border: 2px solid transparent; + border-top: 2px solid currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.btn.loading .button-text { + opacity: 0; +} + +.btn.loading .loading-spinner { + display: block; + position: absolute; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.form-divider { + text-align: center; + margin: 2rem 0; + position: relative; + color: var(--text-secondary); + font-size: 0.875rem; +} + +.form-divider::before { + content: ''; + position: absolute; + top: 50%; + left: 0; + right: 0; + height: 1px; + background: var(--border); +} + +.form-divider span { + background: white; + padding: 0 1rem; + position: relative; +} + +.social-login { + display: flex; + flex-direction: column; +} + +.general-error { + background: #fef2f2; + border: 1px solid #fecaca; + color: var(--error); + padding: 0.75rem; + border-radius: 0.5rem; + font-size: 0.875rem; + margin-top: 1rem; + display: none; +} + +.general-error.show { + display: block; +} + +.login-footer { + text-align: center; + color: var(--text-secondary); + font-size: 0.875rem; +} + +.login-footer a { + color: var(--primary-color); + text-decoration: none; + font-weight: 500; +} + +.login-footer a:hover { + color: var(--primary-dark); +} + +/* Toast Notification */ +.toast { + position: fixed; + top: 2rem; + right: 2rem; + background: var(--success); + color: white; + padding: 1rem 1.5rem; + border-radius: 0.5rem; + box-shadow: var(--shadow-lg); + transform: translateX(100%); + transition: transform 0.3s ease; + z-index: 1000; +} + +.toast.show { + transform: translateX(0); +} + +.toast-content { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.toast-icon { + font-size: 1.25rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .login-container { + grid-template-columns: 1fr; + } + + .login-left { + display: none; + } + + .login-right { + padding: 1rem; + } + + .login-form-container { + max-width: none; + } + + .form-options { + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } +} + +@media (max-width: 480px) { + .login-header h2 { + font-size: 1.5rem; + } + + .btn { + padding: 1rem; + } + + .toast { + top: 1rem; + right: 1rem; + left: 1rem; + } +} diff --git a/static/login.html b/static/login.html new file mode 100644 index 0000000..9b6c437 --- /dev/null +++ b/static/login.html @@ -0,0 +1,129 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Sign In - TaskFlow</title> + <link rel="stylesheet" href="login-styles.css"> + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> +</head> +<body> + <div class="login-container"> + <div class="login-left"> + <div class="login-branding"> + <div class="brand-logo">TaskFlow</div> + <h1>Welcome back</h1> + <p>Sign in to your account to continue managing your projects</p> + </div> + <div class="login-testimonial"> + <blockquote> + "TaskFlow has transformed how our team collaborates. We've increased productivity by 40% since switching." + </blockquote> + <div class="testimonial-author"> + <div class="author-avatar">SM</div> + <div class="author-info"> + <div class="author-name">Sarah Mitchell</div> + <div class="author-title">Project Manager, TechCorp</div> + </div> + </div> + </div> + </div> + + <div class="login-right"> + <div class="login-form-container"> + <div class="login-header"> + <h2>Sign in to TaskFlow</h2> + <p>Enter your credentials to access your dashboard</p> + </div> + + <form id="loginForm" class="login-form"> + <div class="form-group"> + <label for="email">Email address</label> + <input + type="email" + id="email" + name="email" + required + placeholder="Enter your email" + autocomplete="email" + > + <div class="error-message" id="emailError"></div> + </div> + + <div class="form-group"> + <label for="password">Password</label> + <div class="password-input-container"> + <input + type="password" + id="password" + name="password" + required + placeholder="Enter your password" + autocomplete="current-password" + > + <button type="button" class="password-toggle" id="passwordToggle"> + <span class="toggle-icon">👁️</span> + </button> + </div> + <div class="error-message" id="passwordError"></div> + </div> + + <div class="form-options"> + <label class="checkbox-container"> + <input type="checkbox" id="rememberMe" name="rememberMe"> + <span class="checkmark"></span> + Remember me + </label> + <a href="#forgot-password" class="forgot-password">Forgot password?</a> + </div> + + <button type="submit" class="btn btn-primary btn-full" id="loginButton"> + <span class="button-text">Sign In</span> + <span class="loading-spinner" id="loadingSpinner"></span> + </button> + + <div class="form-divider"> + <span>or</span> + </div> + + <div class="social-login"> + <button type="button" class="btn btn-social btn-google"> + <svg width="18" height="18" viewBox="0 0 18 18"> + <path fill="#4285F4" d="M16.51 8H8.98v3h4.3c-.18 1-.74 1.48-1.6 2.04v2.01h2.6a7.8 7.8 0 0 0 2.38-5.88c0-.57-.05-.66-.15-1.18z"/> + <path fill="#34A853" d="M8.98 16c2.16 0 3.97-.72 5.3-1.94l-2.6-2.04a4.8 4.8 0 0 1-7.18-2.53H1.83v2.07A8 8 0 0 0 8.98 16z"/> + <path fill="#FBBC05" d="M4.5 9.49a4.8 4.8 0 0 1 0-3.07V4.35H1.83a8 8 0 0 0 0 7.28z"/> + <path fill="#EA4335" d="M8.98 4.72c1.17 0 2.23.4 3.06 1.2l2.3-2.3A8 8 0 0 0 1.83 4.35L4.5 6.42c.68-2.07 2.49-3.22 4.48-3.22z"/> + </svg> + Continue with Google + </button> + <button type="button" class="btn btn-social btn-microsoft"> + <svg width="18" height="18" viewBox="0 0 18 18"> + <path fill="#f25022" d="M0 0h8v8H0z"/> + <path fill="#00a4ef" d="M10 0h8v8h-8z"/> + <path fill="#7fba00" d="M0 10h8v8H0z"/> + <path fill="#ffb900" d="M10 10h8v8h-8z"/> + </svg> + Continue with Microsoft + </button> + </div> + + <div class="general-error" id="generalError"></div> + </form> + + <div class="login-footer"> + <p>Don't have an account? <a href="#signup">Sign up for free</a></p> + </div> + </div> + </div> + </div> + + <div class="toast" id="toast"> + <div class="toast-content"> + <span class="toast-icon">✓</span> + <span class="toast-message">Login successful! Redirecting...</span> + </div> + </div> + + <script src="login-script.js"></script> +</body> +</html> |