Create your account
Step 1 of 3: Your Details
+diff --git a/app.exe b/app.exe new file mode 100644 index 0000000..23cd084 Binary files /dev/null and b/app.exe differ diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index 7ed6673..7f7be83 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -71,6 +71,8 @@ func main() { // POST Request: JSON API endpoints mux.HandleFunc("POST /v1/loginauth", authHandler.LoginAuthHandler) // Login authentication endpoint mux.HandleFunc("POST /v1/signupauth", authHandler.SignupHandler) + mux.HandleFunc("POST /v1/signup/check-details", authHandler.CheckDetailsHandler) + mux.HandleFunc("POST /v1/signup/check-card", authHandler.CheckCardHandler) mux.HandleFunc("GET /login", authHandler.LoginView) mux.HandleFunc("GET /signup", authHandler.SignupView) mux.HandleFunc("GET /dashboard", userHandler.DashboardHandler) diff --git a/backend/internal/auth/signup.go b/backend/internal/auth/signup.go index 9ce2fa2..2aa824c 100644 --- a/backend/internal/auth/signup.go +++ b/backend/internal/auth/signup.go @@ -1,7 +1,8 @@ package authentication import ( - "database/sql" + //"database/sql" + "encoding/json" "errors" "fmt" @@ -46,6 +47,105 @@ type User struct { CreatedAt string `db:"created_at"` } +type CheckDetailsRequest struct { + Email string `json:"email"` + ContactNumber string `json:"contact_number"` +} + +/*type CheckCardRequest struct { + CardNumber string `json:"card_number"` +}*/ + +// CheckDetailsHandler checks if email and phone are available +func (h *Handler) CheckDetailsHandler(w http.ResponseWriter, r *http.Request) { + var req CheckDetailsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid request", + }) + return + } + + // Check Email + exists, err := account.IsEmailExist(h.DB, req.Email) + if err != nil { + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Database error", + }) + return + } + if exists { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Email already registered", + Field: "email", + }) + return + } + + // Check Phone + exists, err = h.isPhoneExist(req.ContactNumber) + if err != nil { + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Database error", + }) + return + } + if exists { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid phone number", + Field: "phone", + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Details are valid", + }) +} + +// CheckCardHandler checks if card is valid for registration +func (h *Handler) CheckCardHandler(w http.ResponseWriter, r *http.Request) { + var req SignupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid request", + }) + return + } + + var status string + err := h.DB.QueryRow("SELECT status FROM cards WHERE card_number = ?", req.CardNumber).Scan(&status) + if err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Card not found", + }) + return + } + + if status != "Inactive" { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Card is invalid", + Field: "card", + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Card is valid", + Field: "card", + }) +} + // View Handler (GET) // You can now simplify this because JS handles the errors! func (h *Handler) SignupView(w http.ResponseWriter, r *http.Request) { @@ -111,79 +211,6 @@ func (h *Handler) SignupHandler(w http.ResponseWriter, r *http.Request) { return } - // Validation: Password Length (fail fast before any DB calls) - /* len(req.Password) < 8 { - log.Printf("Validation failed: Password must be at least 8 characters long") - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ - Success: false, - Message: "Password must be at least 8 characters long.", - }) - return - }*/ - - // Check Card Status - var cardStatus string - err = h.DB.QueryRowContext(ctx, "SELECT status FROM cards WHERE card_number = ?", req.CardNumber).Scan(&cardStatus) - if err == sql.ErrNoRows { - log.Printf("Card not found: %v", req.CardNumber) - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ - Success: false, - Message: "Card number not found. Please check your card.", - }) - return - } else if err != nil { - log.Printf("Error checking card status: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ - Success: false, - Message: "System error checking card status.", - }) - return - } - - if cardStatus != CardStatusInactive { - log.Printf("Card is not inactive: %v", cardStatus) - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ - Success: false, - Message: fmt.Sprintf("Card is currently '%s'. Please contact support.", cardStatus), - }) - return - } - - // Check Email - exists, err := account.IsEmailExist(h.DB, req.Email) - if err != nil { - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ - Success: false, - Message: "System error checking email.", - }) - return - } - if exists { - jsonwrite.WriteJSON(w, http.StatusConflict, jsonwrite.APIResponse{ - Success: false, - Message: "Email already registered. Please use a different email.", - }) - return - } - - // Check Phone - exists, err = h.isPhoneExist(req.ContactNumber) - if err != nil { - log.Printf("Phone number check error: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ - Success: false, - Message: "System error checking phone number.", - }) - return - } - if exists { - jsonwrite.WriteJSON(w, http.StatusConflict, jsonwrite.APIResponse{ - Success: false, - Message: "Phone number already registered. Please use a different number.", - }) - return - } - // Hash Password hashedPassword, err := account.HashPassword(req.Password) if err != nil { diff --git a/backend/internal/pkg/handler/jsonWrite.go b/backend/internal/pkg/handler/jsonWrite.go index 46ce8b9..17caeb1 100644 --- a/backend/internal/pkg/handler/jsonWrite.go +++ b/backend/internal/pkg/handler/jsonWrite.go @@ -9,6 +9,7 @@ import ( type APIResponse struct { Success bool `json:"success"` Message string `json:"message"` + Field string `json:"field,omitempty"` } // Login specific response — returns user data after login diff --git a/frontend/assets/js/signup.js b/frontend/assets/js/signup.js index 1019e2c..c19573e 100644 --- a/frontend/assets/js/signup.js +++ b/frontend/assets/js/signup.js @@ -1,38 +1,10 @@ -// Global helper functions for input restriction -function isValidEmail(email) { - const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; - return regex.test(email); -} - -function isNumber(evt) { - evt = (evt) ? evt : window.event; - var charCode = (evt.which) ? evt.which : evt.keyCode; - // Allow only numbers - if (charCode > 31 && (charCode < 48 || charCode > 57)) { - return false; - } - return true; -} - -function isAlpha(evt) { - evt = (evt) ? evt : window.event; - var charCode = (evt.which) ? evt.which : evt.keyCode; - // Allow letters and spaces. Also allow control characters < 32. - if (charCode < 32) return true; - if ((charCode >= 65 && charCode <= 90) || - (charCode >= 97 && charCode <= 122) || - charCode === 32) { - return true; - } - return false; -} - document.addEventListener("DOMContentLoaded", function () { // STEP ELEMENTS const step1 = document.getElementById('step-1'); const step2 = document.getElementById('step-2'); const step3 = document.getElementById('step-3'); const stepSubtitle = document.getElementById('step-subtitle'); + const stepProgress = document.getElementById('step-progress'); // BUTTONS const btnStep1 = document.getElementById('btn-step-1'); @@ -42,247 +14,284 @@ document.addEventListener("DOMContentLoaded", function () { const createAccountBtn = document.getElementById('create-account-btn'); const signupForm = document.getElementById('signup-form'); - // INPUTS + // INPUT FIELDS const firstNameInput = document.getElementById('first_name'); const lastNameInput = document.getElementById('last_name'); const emailInput = document.getElementById('email'); const contactNumberInput = document.getElementById('contact_number'); - - // INPUTS - const cardIdInput = document.getElementById('card_id'); - const cardIdError = document.getElementById('card-id-error'); - - // INPUTS + const cardNumberInput = document.getElementById('card_number'); const passwordInput = document.getElementById('password'); const confirmPasswordInput = document.getElementById('confirm_password'); - const checklist = document.getElementById('validation-checklist'); - const lengthCheck = document.getElementById('length-check'); - const matchCheck = document.getElementById('match-check'); - - // MODAL (ADDED) - const successModal = document.getElementById('success-modal'); - const modalCloseBtn = document.getElementById('modal-close-btn'); - // GLOBAL + // ERROR DISPLAYS const errorMessage = document.getElementById('error-message'); + const cardNumberError = document.getElementById('card-number-error'); + + // MODAL + const successModal = document.getElementById('success-modal'); + const modalCloseBtn = document.getElementById('modal-close-btn'); - // FORM DATA STORAGE - const formData = { + // STATE + let formData = { firstName: '', lastName: '', email: '', - cardId: '', - password: '', + contactNumber: '', + cardNumber: '', + password: '' }; - // ROBUSTNESS CHECK - if (!step1 || !step2 || !step3 || !stepSubtitle || !btnStep1 || !btnBack2 || !btnStep2 || !btnBack3 || !createAccountBtn || !signupForm || !firstNameInput || !emailInput || !cardIdInput || !cardIdError || !passwordInput || !confirmPasswordInput || !checklist || !lengthCheck || !matchCheck || !errorMessage || !successModal || !modalCloseBtn) { - console.error("Signup Script Error: Not all required HTML elements were found on the page."); - return; // Stop the script - } - - // INITIALIZATION - // HELPER FUNCTIONS function showStep(stepNumber) { - step1.classList.add('hidden'); - step2.classList.add('hidden'); - step3.classList.add('hidden'); + [step1, step2, step3].forEach(s => s?.classList.add('hidden')); errorMessage.classList.add('hidden'); if (stepNumber === 1) { step1.classList.remove('hidden'); stepSubtitle.textContent = 'Step 1 of 3: Your Details'; + if (stepProgress) stepProgress.style.width = '33.33%'; } else if (stepNumber === 2) { step2.classList.remove('hidden'); stepSubtitle.textContent = 'Step 2 of 3: Card Verification'; + if (stepProgress) stepProgress.style.width = '66.66%'; } else if (stepNumber === 3) { step3.classList.remove('hidden'); stepSubtitle.textContent = 'Step 3 of 3: Create Password'; + if (stepProgress) stepProgress.style.width = '100%'; } } - function updateChecklistItem(checkElement, isValid) { - const icon = checkElement.querySelector('i'); - if (isValid) { - checkElement.classList.add('valid'); - icon.classList.remove('fa-times-circle', 'text-red-500'); - icon.classList.add('fa-check-circle', 'text-green-600'); - } else { - checkElement.classList.remove('valid'); - icon.classList.remove('fa-check-circle', 'text-green-600'); - icon.classList.add('fa-times-circle', 'text-red-500'); - } - } - - function showError(message) { - errorMessage.textContent = message; + function showError(msg) { + errorMessage.textContent = msg; errorMessage.classList.remove('hidden'); + window.scrollTo({ top: 0, behavior: 'smooth' }); } - // VALIDATION LOGIC - - // Real-time validation for Name and Email fields - function validateStep1Realtime() { - const firstName = firstNameInput.value.trim(); - const lastName = lastNameInput.value.trim(); - const email = emailInput.value.trim(); - const contactNumber = contactNumberInput.value.trim(); - - const isNameValid = firstName !== '' && lastName !== ''; - const isEmailValid = email !== '' && isValidEmail(email); - const isContactValid = contactNumber !== ''; - - if (isNameValid && isEmailValid && isContactValid) { - errorMessage.classList.add('hidden'); - } - } - - // Click validation (shows error) - function validateStep1() { - const firstName = firstNameInput.value.trim(); - const lastName = lastNameInput.value.trim(); - const email = emailInput.value.trim(); - const contactNumber = contactNumberInput.value.trim(); - - if (firstName === '' || lastName === '' || email === '' || !isValidEmail(email) || contactNumber === '') { - showError('Please fill all fields and provide a valid email address.'); + // VALIDATION FUNCTIONS + async function validateStep1() { + const inputs = [firstNameInput, lastNameInput, emailInput, contactNumberInput]; + let isValid = true; + inputs.forEach(input => { + if (!validateInput(input)) isValid = false; + }); + + if (!isValid) return false; + + // Backend check for Email/Phone availability + try { + btnStep1.disabled = true; + btnStep1.textContent = 'Checking...'; + + const response = await fetch('/v1/signup/check-details', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: emailInput.value.trim(), + contact_number: contactNumberInput.value.trim() + }) + }); + + const data = await response.json(); + if (!data.success) { + showError(data.message); + if (data.field === 'email') { + emailInput.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500'); + emailInput.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500'); + } + if (data.field === 'phone') { + contactNumberInput.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500'); + contactNumberInput.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500'); + } + return false; + } + + formData.firstName = firstNameInput.value.trim(); + formData.lastName = lastNameInput.value.trim(); + formData.email = emailInput.value.trim(); + formData.contactNumber = contactNumberInput.value.trim(); + + return true; + } catch (err) { + showError('Network error. Please try again.'); return false; + } finally { + btnStep1.disabled = false; + btnStep1.textContent = 'Next'; } - - // TODO: Backend check for email - formData.firstName = firstName; - formData.lastName = lastName; - formData.email = email; - formData.contactNumber = contactNumber; - return true; } - // Validate Card ID (real-time and click) - function validateStep2() { - const cardId = cardIdInput.value.trim(); - - if (cardId === "") { - cardIdError.textContent = 'Please enter your Card ID.'; - cardIdError.classList.remove('hidden'); - return false; - } - - const isCardIdOnlyNumbers = /^\d+$/.test(cardId); - const isCardIdValidLength = cardId.length === 16; - - if (!isCardIdOnlyNumbers) { - cardIdError.textContent = 'Card ID must contain only numbers.'; - cardIdError.classList.remove('hidden'); - return false; - } else if (!isCardIdValidLength) { - cardIdError.textContent = 'Card ID must be exactly 10 digits long.'; - cardIdError.classList.remove('hidden'); + async function validateStep2() { + if (!validateInput(cardNumberInput)) return false; + + try { + btnStep2.disabled = true; + btnStep2.textContent = 'Verifying...'; + + const response = await fetch('/v1/signup/check-card', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ card_number: cardNumberInput.value.trim() }) + }); + + const data = await response.json(); + if (!data.success) { + showError(data.message); + cardNumberInput.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500'); + cardNumberInput.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500'); + return false; + } + + formData.cardNumber = cardNumberInput.value.trim(); + return true; + } catch (err) { + showError('Network error. Please try again.'); return false; + } finally { + btnStep2.disabled = false; + btnStep2.textContent = 'Next'; } - - // TODO: Backend check for card ID - - cardIdError.classList.add('hidden'); - formData.cardId = cardId; - return true; } - // Validate Password - function validateStep3() { + function updatePasswordChecklist() { const password = passwordInput.value; const confirmPassword = confirmPasswordInput.value; - + const isLengthValid = password.length >= 8; - const passwordsMatch = password === confirmPassword && password.length > 0; + const isMatchValid = password === confirmPassword && confirmPassword !== ''; - updateChecklistItem(lengthCheck, isLengthValid); - updateChecklistItem(matchCheck, passwordsMatch); - - const allValid = isLengthValid && passwordsMatch; + const lengthCheck = document.getElementById('length-check').querySelector('.icon'); + const matchCheck = document.getElementById('match-check').querySelector('.icon'); - if (allValid) { - formData.password = password; - return true; - } - return false; + lengthCheck.className = `icon fas ${isLengthValid ? 'fa-check-circle text-green-500' : 'fa-times-circle text-red-500'}`; + matchCheck.className = `icon fas ${isMatchValid ? 'fa-check-circle text-green-500' : 'fa-times-circle text-red-500'}`; + + return isLengthValid && isMatchValid; } - // EVENT LISTENERS + passwordInput.addEventListener('input', updatePasswordChecklist); + confirmPasswordInput.addEventListener('input', updatePasswordChecklist); - // Add real-time listeners for Step 1 - firstNameInput.addEventListener('input', validateStep1Realtime); - lastNameInput.addEventListener('input', validateStep1Realtime); - contactNumberInput.addEventListener('input', validateStep1Realtime); + function validateStep3() { + const inputs = [passwordInput, confirmPasswordInput]; + let isValid = true; + inputs.forEach(input => { + if (!validateInput(input)) isValid = false; + }); - // Next from Step 1 - btnStep1.addEventListener('click', function () { - if (validateStep1()) { - showStep(2); - } - }); + if (!updatePasswordChecklist()) return false; + if (!isValid) return false; - // Back from Step 2 - btnBack2.addEventListener('click', function () { - showStep(1); - }); + formData.password = passwordInput.value; + return true; + } - // Next from Step 2 - btnStep2.addEventListener('click', function () { - if (validateStep2()) { - showStep(3); - } + // EVENT LISTENERS + btnStep1.addEventListener('click', async () => { + if (await validateStep1()) showStep(2); }); - - // Real-time validation for Card ID as user types - cardIdInput.addEventListener('input', validateStep2); - // Back from Step 3 - btnBack3.addEventListener('click', function () { - showStep(2); + btnBack2.addEventListener('click', () => showStep(1)); + btnStep2.addEventListener('click', async () => { + if (await validateStep2()) showStep(3); }); - // Real-time validation for Password fields - passwordInput.addEventListener('input', validateStep3); - confirmPasswordInput.addEventListener('input', validateStep3); + btnBack3.addEventListener('click', () => showStep(2)); - // Final Form Submission - signupForm.addEventListener('submit', function (event) { - event.preventDefault(); - - if (validateStep3()) { - fetch("/v1/signupauth", { - method: "POST", - headers: {"Content-Type": "application/json"}, + signupForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + if (!validateStep3()) return; + + try { + createAccountBtn.disabled = true; + createAccountBtn.innerHTML = ' Creating Account...'; + + const response = await fetch('/v1/signupauth', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, body: JSON.stringify({ first_name: formData.firstName, last_name: formData.lastName, email: formData.email, contact_number: formData.contactNumber, - card_number: formData.cardId, + card_number: formData.cardNumber, password: formData.password }) - }) - .then(res => res.json()) - .then(data => { - if (data.success) { - successModal.classList.remove('hidden'); - } else { - showError(data.message || 'Failed to create account'); - } - }) - .catch(err => { - console.error(err); - showError('Network error occurred.'); }); - } else { - showError('Please correct the errors in the password fields.'); + + const data = await response.json(); + + if (data.success) { + successModal.classList.remove('hidden'); + } else { + showError(data.message || 'Registration failed. Please try again.'); + } + } catch (error) { + showError('A network error occurred. Please try again.'); + } finally { + createAccountBtn.disabled = false; + createAccountBtn.innerHTML = 'Create Account'; } }); - // MODAL BUTTON (ADDED) - // Add event listener for the modal's "Go to Login" button - modalCloseBtn.addEventListener('click', function() { - window.location.href = "/login"; + modalCloseBtn.addEventListener('click', () => { + window.location.href = '/login'; }); + + // Initialize formatting functions + window.validateInput = function(input) { + const errorElement = document.getElementById(`error-${input.id}`); + let isValid = true; + let errorMsg = ''; + + input.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500', 'border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500'); + if(errorElement) errorElement.classList.add('hidden'); + + if (!input.value.trim() && input.hasAttribute('required')) { + isValid = false; + errorMsg = 'This field is required'; + } else if (input.hasAttribute('pattern')) { + const regex = new RegExp(`^${input.getAttribute('pattern')}$`); + if (!regex.test(input.value)) { + isValid = false; + errorMsg = 'Invalid format'; + } + } + + if (!isValid) { + input.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500'); + if(errorElement) { + errorElement.textContent = errorMsg; + errorElement.classList.remove('hidden'); + } + } else { + input.classList.add('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500'); + } + + return isValid; + }; + + window.isNumber = function(evt) { + evt = (evt) ? evt : window.event; + var charCode = (evt.which) ? evt.which : evt.keyCode; + if (charCode > 31 && (charCode < 48 || charCode > 57)) { + return false; + } + return true; + }; + + window.isAlpha = function(evt) { + evt = (evt) ? evt : window.event; + var charCode = (evt.which) ? evt.which : evt.keyCode; + if ((charCode >= 65 && charCode <= 90) || + (charCode >= 97 && charCode <= 122) || + charCode == 32) { + return true; + } + return false; + }; + + showStep(1); }); \ No newline at end of file diff --git a/frontend/assets/style/index.css b/frontend/assets/style/index.css deleted file mode 100644 index 40fa6e9..0000000 --- a/frontend/assets/style/index.css +++ /dev/null @@ -1,4 +0,0 @@ -.required { - color: red; - margin-left: 3px; -} \ No newline at end of file diff --git a/frontend/assets/style/style.css b/frontend/assets/style/style.css new file mode 100644 index 0000000..c1ed756 --- /dev/null +++ b/frontend/assets/style/style.css @@ -0,0 +1,19 @@ +.required label::after { + content: " *"; + color: #ef4444; +} + +input.error { + border-color: #ef4444 !important; + outline: 2px solid transparent !important; + outline-offset: 2px !important; + --tw-ring-color: #ef4444 !important; + ring-width: 2px !important; +} + +.error-text { + color: #ef4444; + font-size: 0.75rem; + margin-top: 0.25rem; + display: block; +} \ No newline at end of file diff --git a/frontend/templates/signup.html b/frontend/templates/signup.html index 473abac..f3f4ac6 100644 --- a/frontend/templates/signup.html +++ b/frontend/templates/signup.html @@ -10,12 +10,12 @@ - + -
+Step 1 of 3: Your Details
+