From 6abe2cdc94f5fe9baeb2ab130eca39e74c283b39 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sat, 25 Apr 2026 09:32:33 +0800 Subject: [PATCH 1/4] feat: implement initial frontend templates, backend handlers, and project documentation for the core application structure --- .github/workflows/restrict-pr.yml | 36 ++ ROADMAP.md | 254 ++++++++--- {cmd => backend/cmd}/app/main.go | 10 +- .../internal}/admin/addCard.go | 2 +- .../internal}/admin/deactivateCard.go | 2 +- .../internal}/admin/handler.go | 2 +- .../internal}/auth/dashboard.go | 2 +- .../internal}/auth/forgotPassword.go | 4 +- .../internal}/auth/handler.go | 0 {internal => backend/internal}/auth/login.go | 25 +- .../internal}/auth/rateLimit.go | 2 +- .../internal}/auth/repository.go | 0 {internal => backend/internal}/auth/signup.go | 4 +- backend/internal/auth/types.go | 2 + {internal => backend/internal}/auth/utils.go | 0 backend/internal/middleware/rateLimit.go | 55 +++ .../internal}/pkg/account/services.go | 0 .../internal}/pkg/handler/jsonWrite.go | 3 +- .../internal}/pkg/structMessage.go | 0 backend/internal/user/dashboard.go | 54 +++ backend/internal/user/handler.go | 16 + {pkg => backend/pkg}/databases/db.go | 0 {assets => frontend/assets}/admin/addcard.css | 0 .../assets}/images/Fare Receipt Details.png | Bin .../assets}/images/Fare Receipt.png | Bin .../assets}/images/Retail Receipt Details.png | Bin .../assets}/images/Retail Receipt.png | Bin frontend/assets/js/card.js | 201 +++++++++ frontend/assets/js/dashboard.js | 148 +++++++ frontend/assets/js/forgot-password.js | 65 +++ frontend/assets/js/login.js | 40 ++ frontend/assets/js/profile.js | 187 ++++++++ frontend/assets/js/reset-password.js | 106 +++++ frontend/assets/js/signup.js | 270 ++++++++++++ frontend/assets/js/tailwind-config.js | 9 + {assets => frontend/assets}/style/index.css | 0 .../templates}/addCards.html | 2 +- frontend/templates/card.html | 399 ++++++++++++++++++ frontend/templates/dashboard.html | 307 ++++++++++++++ .../templates}/deactivateCard.html | 2 +- frontend/templates/forgot-password.html | 137 ++++++ frontend/templates/login.html | 121 ++++++ frontend/templates/profile.html | 381 +++++++++++++++++ frontend/templates/reset-password.html | 141 +++++++ frontend/templates/settings.html | 239 +++++++++++ frontend/templates/signup.html | 291 +++++++++++++ frontend/templates/topup.html | 358 ++++++++++++++++ frontend/templates/transaction.html | 329 +++++++++++++++ go.mod | 15 +- go.sum | 32 +- templates/dashboard.html | 64 --- templates/login.html | 92 ---- templates/signup.html | 106 ----- test/login_test.go | 2 +- 54 files changed, 4159 insertions(+), 358 deletions(-) create mode 100644 .github/workflows/restrict-pr.yml rename {cmd => backend/cmd}/app/main.go (91%) rename {internal => backend/internal}/admin/addCard.go (99%) rename {internal => backend/internal}/admin/deactivateCard.go (98%) rename {internal => backend/internal}/admin/handler.go (99%) rename {internal => backend/internal}/auth/dashboard.go (99%) rename {internal => backend/internal}/auth/forgotPassword.go (97%) rename {internal => backend/internal}/auth/handler.go (100%) rename {internal => backend/internal}/auth/login.go (71%) rename {internal => backend/internal}/auth/rateLimit.go (99%) rename {internal => backend/internal}/auth/repository.go (100%) rename {internal => backend/internal}/auth/signup.go (98%) create mode 100644 backend/internal/auth/types.go rename {internal => backend/internal}/auth/utils.go (100%) create mode 100644 backend/internal/middleware/rateLimit.go rename {internal => backend/internal}/pkg/account/services.go (100%) rename {internal => backend/internal}/pkg/handler/jsonWrite.go (77%) rename {internal => backend/internal}/pkg/structMessage.go (100%) create mode 100644 backend/internal/user/dashboard.go create mode 100644 backend/internal/user/handler.go rename {pkg => backend/pkg}/databases/db.go (100%) rename {assets => frontend/assets}/admin/addcard.css (100%) rename {assets => frontend/assets}/images/Fare Receipt Details.png (100%) rename {assets => frontend/assets}/images/Fare Receipt.png (100%) rename {assets => frontend/assets}/images/Retail Receipt Details.png (100%) rename {assets => frontend/assets}/images/Retail Receipt.png (100%) create mode 100644 frontend/assets/js/card.js create mode 100644 frontend/assets/js/dashboard.js create mode 100644 frontend/assets/js/forgot-password.js create mode 100644 frontend/assets/js/login.js create mode 100644 frontend/assets/js/profile.js create mode 100644 frontend/assets/js/reset-password.js create mode 100644 frontend/assets/js/signup.js create mode 100644 frontend/assets/js/tailwind-config.js rename {assets => frontend/assets}/style/index.css (100%) rename {templates => frontend/templates}/addCards.html (96%) create mode 100644 frontend/templates/card.html create mode 100644 frontend/templates/dashboard.html rename {templates => frontend/templates}/deactivateCard.html (95%) create mode 100644 frontend/templates/forgot-password.html create mode 100644 frontend/templates/login.html create mode 100644 frontend/templates/profile.html create mode 100644 frontend/templates/reset-password.html create mode 100644 frontend/templates/settings.html create mode 100644 frontend/templates/signup.html create mode 100644 frontend/templates/topup.html create mode 100644 frontend/templates/transaction.html delete mode 100644 templates/dashboard.html delete mode 100644 templates/login.html delete mode 100644 templates/signup.html diff --git a/.github/workflows/restrict-pr.yml b/.github/workflows/restrict-pr.yml new file mode 100644 index 0000000..e7a754a --- /dev/null +++ b/.github/workflows/restrict-pr.yml @@ -0,0 +1,36 @@ +name: Enforce PR Flow + +on: + pull_request: + # Remove "branches: - main" to run script to any Pull Request + +jobs: + check-pr-flow: + runs-on: ubuntu-latest + steps: + - name: Validate PR Source and Target + env: + TARGET_BRANCH: ${{ github.base_ref }} # papunta ang PR + SOURCE_BRANCH: ${{ github.head_ref }} # Kungaling ang PR + run: | + echo "validate ang PR mula '$SOURCE_BRANCH' papuntang '$TARGET_BRANCH'..." + + # RULE 1: Protection for 'main' branch + if [[ "$TARGET_BRANCH" == "main" ]]; then + if [[ "$SOURCE_BRANCH" != "development" && "$SOURCE_BRANCH" != *"-alpha"* && "$SOURCE_BRANCH" != *"-beta"* && "$SOURCE_BRANCH" != *"-rc"* ]]; then + echo "ERROR: Bawal dumiretso sa main mula sa $SOURCE_BRANCH!" + echo "Allowed lang ay 'development', '-alpha', '-beta', o '-rc'." + exit 1 + fi + fi + + # RULE 2: Protection for 'alpha', 'beta', at 'rc' branches + if [[ "$TARGET_BRANCH" == *"-alpha"* || "$TARGET_BRANCH" == *"-beta"* || "$TARGET_BRANCH" == *"-rc"* ]]; then + if [[ "$SOURCE_BRANCH" != "development" && "$SOURCE_BRANCH" != *"-alpha"* && "$SOURCE_BRANCH" != *"-beta"* && "$SOURCE_BRANCH" != *"-rc"* ]]; then + echo "ERROR: Bawal ipasok ang feature/ branch ($SOURCE_BRANCH) diretso sa $TARGET_BRANCH!" + echo "Ang mga testing branches ay tumatanggap lang ng code galing sa 'development' o sa testing branches." + exit 1 + fi + fi + + echo "Pumasa sa validation! Tama ang flow ng PR mo." diff --git a/ROADMAP.md b/ROADMAP.md index 16e436b..4bc8a4e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,65 +1,213 @@ +Based on the Unicard Project Roadmap, here are some additional necessary options you could add to enhance functionality, security, usability, and scalability for a digital wallet/card system. These suggestions focus on common features in fintech projects: + +- **Multi-Currency Support**: Allow users to hold and transact in multiple currencies (e.g., USD, EUR, crypto). +- **Integration with Payment Gateways**: Enable real-time deposits/withdrawals via banks, PayPal, or Stripe. +- **Fraud Detection and Prevention**: Implement AI-based monitoring for suspicious activities, like anomaly detection in transactions. +- **KYC/AML Compliance**: Add Know Your Customer and Anti-Money Laundering checks during registration and high-value transactions. +- **Mobile App Development**: Create native iOS/Android apps for on-the-go access. +- **API for Third-Party Integrations**: Provide developer APIs for merchants or partners to integrate Unicard payments. +- **Backup and Recovery Options**: Allow users to backup wallet data and recover accounts via seed phrases or secure keys. +- **Customer Support System**: Include in-app chat, ticketing, or FAQ for user assistance. +- **Audit Logging**: Maintain detailed logs for all transactions and admin actions for compliance and debugging. + +These could be added to Phase 4: Backlog & Enhancements, or a new Phase 5 if needed. Below is the rewritten markdown with these additions integrated into Phase 4 (assuming $SELECTION_PLACEHOLDER$ refers to the end of Phase 4's list). + # Unicard Project Roadmap -This document serves as a high-level overview of the features, improvements, and future plans for the Unicard system. +A high-level plan for the Unicard system, organized by phases from MVP to advanced features. + +## Phase 1: Foundation & MVP -## Phase 1: Foundation & MVP (Minimum Viable Product) +**Planned Start**: December 2025 +**Target Completion**: 3rd Quarter 2026 +**Actual Completion**: -**Goal**: Get the basic system running with essential user management and balance viewing. +**Goal**: Build the core user flow with authentication, wallet creation, balance visibility, transaction history, dashboard, card management, and complete user profile system. -- ### [x] Project Initialization - - [x] Set up Repository & Version Control - - [x] Configure Database (MySQL/Firebase/MongoDB) - - [x] Setup basic folder structure -- ### [x] Authentication System - - [x] User Registration (Sign Up) - - [x] User Login (Sign In) - - [x] Forgot Password flow -- ### [ ] Core Wallet Features - - [ ] Create User Wallet upon registration - - [ ] View Current Balance - - [ ] View Digital Card ID/Number +- ### [x] Project Setup (Completed: December 2025) + - [x] Initialize repository and version control + - [x] Configure database support (MySQL / Firebase / MongoDB) + - [x] Establish project structure +- ### [ ] Authentication (Completed: ) + - [x] User login (sign in with Phone number and Password) + - [ ] User registration (sign up with email verification) + - [ ] Password recovery flow with OTP via email + - [ ] Session management and auto-logout + - [ ] Email verification on registration +- ### [ ] Dashboard (Completed: ) + - [ ] Overview of wallet balance and recent activity + - [ ] Quick access to all key features (transactions, top-up, card info) + - [ ] Display user profile information (name, avatar) + - [ ] Recent transactions widget + - [ ] Card status widget +- ### [ ] Card/Wallet Management (Completed: ) + - [ ] Create wallet and digital card when user registers + - [ ] Display current balance and wallet info + - [ ] Show digital card details (card number, expiry, CVV, cardholder name) + - [ ] Card info/details page with complete card information + - [ ] Display card status (active/inactive) +- ### [ ] Card Report & Management (Completed: ) + - [ ] Report card as stolen + - [ ] Report card as damaged + - [ ] Request card replacement + - [ ] View card report history + - [ ] Card replacement status tracking +- ### [ ] Top-up Page (Completed: ) + - [ ] Add funds to wallet (initial top-up) + - [ ] Multiple payment method options + - [ ] Transaction confirmation and receipt + - [ ] Top-up history +- ### [ ] Transaction History & Page (Completed: ) + - [ ] List all transactions with detailed information + - [ ] Filter by date range and transaction type + - [ ] View transaction details and receipts + - [ ] Search transactions by amount or reference + - [ ] Export transaction history +- ### [ ] User Profile Page (Completed: ) + - [ ] View complete profile information + - [ ] Edit personal information (name, phone, address) + - [ ] Upload and manage avatar/profile picture + - [ ] View account verification status + - [ ] Account linking options +- ### [ ] User Settings Page (Completed: ) + - [ ] Change email address + - [ ] Change password with old password verification + - [ ] Enable/disable two-factor authentication (2FA) + - [ ] Manage notification preferences + - [ ] Privacy and security settings + - [ ] Change language/display preferences +- ### [ ] Basic Admin Dashboard & Management (Completed: ) + - [ ] View all registered users list + - [ ] Basic user search and filtering + - [ ] Display basic system analytics (total users, active users, daily transactions) + - [ ] Freeze or suspend user accounts + - [ ] View user profile and transaction history + - [ ] Basic account verification interface + - [ ] System configuration management (basic) +- ### [ ] Additional MVP Features + - [ ] Email notifications for transactions and account activities + - [ ] In-app notifications and alerts + - [ ] Help/FAQ section + - [ ] Account deactivation option + - [ ] Data backup and account recovery options ## Phase 2: Transactions & Payments -**Goal**: Allow money to move between accounts or be spent. - -- ### [ ] Transaction Logic - - [ ] Deposit funds (Admin side or Simulation) - - [ ] Transfer funds between users - - [ ] Payment simulation (Deduct balance) -- ### [ ] Transaction History - - [ ] List of recent transactions - - [ ] Filter by date or type (Credit/Debit) -- ### [ ] QR Code Integration - - [ ] Generate unique QR code for User ID - - [ ] Scanner feature to read QR codes - -## Phase 3: Admin & Security - -**Goal**: Manage the ecosystem and ensure data safety. - -- ### [ ] Admin Dashboard - - [ ] View all registered users - - [ ] Manually freeze/suspend accounts - - [ ] System-wide analytics (Total volume, user count) -- ### [ ] Security Enhancements - - [ ] Input validation & Sanitization - - [ ] Role-Based Access Control (User vs Admin) - - [ ] Session management (Auto-logout) - -## Phase 4: Future Enhancements (Backlog) - -**Goal**: Polish the UI and add advanced features. - -- [ ] **Notification System** (Email or In-App alerts for payments) -- [ ] **Dark Mode** support -- [ ] **Profile Settings** (Change avatar, update email) -- [ ] **NFC Support** (Tap-to-pay via mobile) -- [ ] **Export Data** (Download transaction history as PDF/CSV) + +**Planned Start**: December 2026 +**Target Completion**: 2nd Quarter of 2027 +**Actual Completion**: + +**Goal**: Enable comprehensive funds movement with peer-to-peer transfers, merchant payments, QR code support, and advanced transaction features. + +- ### [ ] Transaction Processing + - [ ] Deposit funds (top-up wallet via multiple methods) + - [ ] Transfer funds between users (peer-to-peer with recipient lookup) + - [ ] Bill and utility payments (electricity, water, internet, etc.) + - [ ] Request money from other users (payment requests) + - [ ] Merchant/merchant payment integration + - [ ] Payment reversals and refunds +- ### [ ] QR Code & Payment Links + - [ ] Generate unique QR code for user ID + - [ ] Scan QR codes for quick payments + - [ ] Create shareable payment links + - [ ] Mobile wallet integration for QR scanning +- ### [ ] Receipts & Documentation + - [ ] Generate transaction receipts (PDF/digital) + - [ ] Send receipts via email + - [ ] Invoice generation for merchants + - [ ] Receipt storage and retrieval +- ### [ ] Spending Analytics + - [ ] Spending breakdown by category + - [ ] Monthly/yearly spending reports + - [ ] Budget setting and tracking + - [ ] Spending alerts and notifications +- ### [ ] Payment Confirmations + - [ ] Real-time payment notifications (SMS/email) + - [ ] Payment confirmation screens + - [ ] OTP verification for sensitive transactions + - [ ] Transaction status updates + +## Phase 3: Advanced Admin, Compliance & Security + +**Planned Start**: August 2027 +**Target Completion**: 2nd Quarter 2028 +**Actual Completion**: + +**Goal**: Add advanced administrative controls, compliance workflows, transaction monitoring, and comprehensive security hardening. + +- ### [ ] Advanced Admin Dashboard + - [ ] Real-time dashboard statistics and KPIs + - [ ] System health monitoring and alerts + - [ ] Advanced reporting and custom report generation + - [ ] User data export and bulk operations +- ### [ ] Advanced User Account Management + - [ ] Verify user KYC documents and identity + - [ ] Approve/reject account verification submissions + - [ ] Flag suspicious user accounts + - [ ] User risk scoring + - [ ] User detailed profile and full activity logs + - [ ] Backup user data and account recovery +- ### [ ] Advanced Transaction Monitoring & Approval + - [ ] Monitor high-value transactions in real-time + - [ ] Manual approval workflow for flagged transactions + - [ ] Set transaction limits and velocity checks per user + - [ ] Detect unusual spending patterns (anomaly detection) + - [ ] Transaction dispute management and resolution + - [ ] Refund/reversal processing with audit trail +- ### [ ] Security & Compliance Hardening + - [ ] Input validation and sanitization + - [ ] Advanced role-based access control (RBAC) + - [ ] Manage sessions and auto-logout policies + - [ ] Two-factor authentication (2FA) enforcement + - [ ] IP whitelisting/blacklisting + - [ ] Account lockout after failed login attempts + - [ ] Advanced password policy enforcement + - [ ] End-to-end data encryption (at rest and in transit) +- ### [ ] Audit Logging & Reporting + - [ ] Comprehensive audit logs for all admin actions + - [ ] Detailed transaction audit trail + - [ ] User activity logs with timestamps + - [ ] System event logging + - [ ] Compliance reports generation + - [ ] Export audit logs for external audits + - [ ] Log retention and archival policies +- ### [ ] Compliance & Regulations + - [ ] KYC (Know Your Customer) verification workflow + - [ ] AML (Anti-Money Laundering) monitoring and alerts + - [ ] Sanctions list checking and OFAC compliance + - [ ] Compliance documentation and record keeping + - [ ] Regulatory report generation + - [ ] Terms of Service and Privacy Policy management + - [ ] Data residency and export compliance + +## Phase 4: Backlog & Enhancements + +**Planned Start**: TBA +**Target Completion**: TBA +**Actual Completion**: TBA + +**Goal**: Improve usability and add advanced features. + +- [ ] Notification system (email or in-app alerts) +- [ ] Dark mode support +- [ ] Profile settings (avatar, email updates) +- [ ] NFC support for tap-to-pay +- [ ] Export transactions to PDF/CSV +- [ ] Multi-currency support (e.g., USD, EUR, crypto) +- [ ] Integration with payment gateways (banks, PayPal, Stripe) +- [ ] Fraud detection and prevention (AI-based anomaly monitoring) +- [ ] KYC/AML compliance checks +- [ ] Mobile app development (iOS/Android) +- [ ] API for third-party integrations +- [ ] Backup and recovery options (seed phrases, secure keys) +- [ ] Customer support system (in-app chat, ticketing) +- [ ] Audit logging for transactions and admin actions --- -## Legend: -`[x]` : Completed +## Legend + +`[x]` : Completed `[ ]` : Pending / To Do ---- \ No newline at end of file +--- \ No newline at end of file diff --git a/cmd/app/main.go b/backend/cmd/app/main.go similarity index 91% rename from cmd/app/main.go rename to backend/cmd/app/main.go index 4d0ef92..7ed6673 100644 --- a/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -7,9 +7,9 @@ import ( "log" "net/http" "os" - "unicard-go/internal/admin" - authentication "unicard-go/internal/auth" - "unicard-go/internal/user" + "unicard-go/backend/internal/admin" + authentication "unicard-go/backend/internal/auth" + "unicard-go/backend/internal/user" _ "github.com/go-sql-driver/mysql" "github.com/joho/godotenv" @@ -39,7 +39,7 @@ func main() { dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName) // Setup Templates - tpl, err = template.ParseGlob("./templates/*.html") + tpl, err = template.ParseGlob("./frontend/templates/*.html") if err != nil { log.Fatal("Templates loaded but variable is nil. Check your folder path.") } @@ -65,7 +65,7 @@ func main() { mux := http.NewServeMux() // Serve static files (CSS, JS, images) - fileServer := http.FileServer(http.Dir("./assets")) + fileServer := http.FileServer(http.Dir("./frontend/assets")) mux.Handle("/assets/", http.StripPrefix("/assets/", fileServer)) // POST Request: JSON API endpoints diff --git a/internal/admin/addCard.go b/backend/internal/admin/addCard.go similarity index 99% rename from internal/admin/addCard.go rename to backend/internal/admin/addCard.go index 83e3ed7..e51fb32 100644 --- a/internal/admin/addCard.go +++ b/backend/internal/admin/addCard.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" "time" - message "unicard-go/internal/pkg" + message "unicard-go/backend/internal/pkg" ) // This struct represents a card and its attributes. diff --git a/internal/admin/deactivateCard.go b/backend/internal/admin/deactivateCard.go similarity index 98% rename from internal/admin/deactivateCard.go rename to backend/internal/admin/deactivateCard.go index c8e0bfe..b0d0ea2 100644 --- a/internal/admin/deactivateCard.go +++ b/backend/internal/admin/deactivateCard.go @@ -4,7 +4,7 @@ import ( "fmt" "net/http" "strings" - message "unicard-go/internal/pkg" + message "unicard-go/backend/internal/pkg" ) // This struct represents the details of a card that we want to deactivate. diff --git a/internal/admin/handler.go b/backend/internal/admin/handler.go similarity index 99% rename from internal/admin/handler.go rename to backend/internal/admin/handler.go index 6ca091d..538a3db 100644 --- a/internal/admin/handler.go +++ b/backend/internal/admin/handler.go @@ -17,4 +17,4 @@ func NewHandler(db *sql.DB, tpl *template.Template) *Handler { DB: db, Tpl: tpl, } -} \ No newline at end of file +} diff --git a/internal/auth/dashboard.go b/backend/internal/auth/dashboard.go similarity index 99% rename from internal/auth/dashboard.go rename to backend/internal/auth/dashboard.go index 2229575..f540620 100644 --- a/internal/auth/dashboard.go +++ b/backend/internal/auth/dashboard.go @@ -51,4 +51,4 @@ func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { }, } h.Tpl.ExecuteTemplate(w, "dashboard.html", dashboardUser) -} \ No newline at end of file +} diff --git a/internal/auth/forgotPassword.go b/backend/internal/auth/forgotPassword.go similarity index 97% rename from internal/auth/forgotPassword.go rename to backend/internal/auth/forgotPassword.go index 6254880..eca7b90 100644 --- a/internal/auth/forgotPassword.go +++ b/backend/internal/auth/forgotPassword.go @@ -4,8 +4,8 @@ import ( "database/sql" "fmt" "net/http" - message "unicard-go/internal/pkg" - "unicard-go/internal/pkg/account" + message "unicard-go/backend/internal/pkg" + "unicard-go/backend/internal/pkg/account" ) // This function renders the forgot password HTML template. diff --git a/internal/auth/handler.go b/backend/internal/auth/handler.go similarity index 100% rename from internal/auth/handler.go rename to backend/internal/auth/handler.go diff --git a/internal/auth/login.go b/backend/internal/auth/login.go similarity index 71% rename from internal/auth/login.go rename to backend/internal/auth/login.go index 120b80c..9df0fbf 100644 --- a/internal/auth/login.go +++ b/backend/internal/auth/login.go @@ -5,18 +5,17 @@ import ( "errors" "log" "net/http" - jsonwrite "unicard-go/internal/pkg/handler" + jsonwrite "unicard-go/backend/internal/pkg/handler" "github.com/go-playground/validator/v10" // For struct validation "golang.org/x/crypto/bcrypt" ) -// LoginRequest represents the JSON request for login type LoginRequest struct { - ID string `json:"id,omitempty" db:"ID"` // Optional: can be used for direct ID login - UserID string `json:"userId,omitempty" db:"user_id"` // Optional: can be used for direct user ID login - PhoneNumber string `json:"phoneNumber" db:"phone" validate:"required,numeric,len=11"` // Allow login via phone number - Password string `json:"password" db:"password_hash" validate:"required"` // Expecting the password hash in the database + ID string `json:"id,omitempty" db:"ID"` // Optional: can be used for direct ID login + UserID string `json:"userId,omitempty" db:"user_id"` // Optional: can be used for direct user ID login + Identifier string `json:"identifier" validate:"required"` // Allow login via email or username + Password string `json:"password" db:"password_hash" validate:"required"` // Expecting the password hash in the database } // Validator instance for struct validation @@ -44,7 +43,7 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { }) return } - log.Printf("Login attempt for: %s", loginReq.PhoneNumber) + log.Printf("Login attempt for: %s", loginReq.Identifier) // validate the login request err := validate.Struct(loginReq) @@ -60,8 +59,8 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { firstErr := validationErrs[0] // Just look at the first error to keep it simple // 3. Update the message based on exactly what failed - if firstErr.Field() == "PhoneNumber" { - errorMessage = "Please enter a valid 11-digit phone number." + if firstErr.Field() == "Identifier" { + errorMessage = "Please enter a valid email or username." } else if firstErr.Field() == "Password" { errorMessage = "Please enter your password." } @@ -80,8 +79,8 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { userID string // Store the user ID for successful login response ) - stmt := "SELECT ID, user_id, password_hash FROM users WHERE phone = ?" - err = h.DB.QueryRow(stmt, loginReq.PhoneNumber).Scan(&ID, &userID, &hash) + stmt := "SELECT ID, user_id, password_hash FROM users WHERE email = ? OR username = ? OR phone = ?" + err = h.DB.QueryRow(stmt, loginReq.Identifier, loginReq.Identifier, loginReq.Identifier).Scan(&ID, &userID, &hash) // User not found if err != nil { @@ -96,7 +95,7 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { // Verify password log.Println("Hash found, verifying password...") if err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(loginReq.Password)); err != nil { - log.Printf("Password mismatch for user: %s", loginReq.PhoneNumber) + log.Printf("Password mismatch for user: %s", loginReq.Identifier) jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ Success: false, Message: "Password is incorrect", @@ -105,7 +104,7 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { } // SUCCESS - log.Printf("Login success for user: %s", loginReq.PhoneNumber) + log.Printf("Login success for user: %s", loginReq.Identifier) jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.LoginResponse{ Success: true, Message: "Login successful", diff --git a/internal/auth/rateLimit.go b/backend/internal/auth/rateLimit.go similarity index 99% rename from internal/auth/rateLimit.go rename to backend/internal/auth/rateLimit.go index 23821d9..15f993f 100644 --- a/internal/auth/rateLimit.go +++ b/backend/internal/auth/rateLimit.go @@ -52,4 +52,4 @@ func RateLimitMiddleware(next http.HandlerFunc) http.HandlerFunc { // If they are within the limit, allow the request to pass through to your handler next.ServeHTTP(w, r) } -} \ No newline at end of file +} diff --git a/internal/auth/repository.go b/backend/internal/auth/repository.go similarity index 100% rename from internal/auth/repository.go rename to backend/internal/auth/repository.go diff --git a/internal/auth/signup.go b/backend/internal/auth/signup.go similarity index 98% rename from internal/auth/signup.go rename to backend/internal/auth/signup.go index dad4fe9..a835642 100644 --- a/internal/auth/signup.go +++ b/backend/internal/auth/signup.go @@ -7,8 +7,8 @@ import ( "log" "net/http" "strings" - "unicard-go/internal/pkg/account" - jsonwrite "unicard-go/internal/pkg/handler" + "unicard-go/backend/internal/pkg/account" + jsonwrite "unicard-go/backend/internal/pkg/handler" ) // Create a struct to catch the incoming JSON from the frontend diff --git a/backend/internal/auth/types.go b/backend/internal/auth/types.go new file mode 100644 index 0000000..a29f513 --- /dev/null +++ b/backend/internal/auth/types.go @@ -0,0 +1,2 @@ +package authentication + diff --git a/internal/auth/utils.go b/backend/internal/auth/utils.go similarity index 100% rename from internal/auth/utils.go rename to backend/internal/auth/utils.go diff --git a/backend/internal/middleware/rateLimit.go b/backend/internal/middleware/rateLimit.go new file mode 100644 index 0000000..15f993f --- /dev/null +++ b/backend/internal/middleware/rateLimit.go @@ -0,0 +1,55 @@ +package authentication + +import ( + "encoding/json" + "net/http" + "sync" + "time" + + "golang.org/x/time/rate" +) + +// We need a map to store a rate limiter for each individual IP address +var visitors = make(map[string]*rate.Limiter) +var mu sync.Mutex + +// getVisitor checks if an IP already has a limiter. If not, it makes a new one. +func getVisitor(ip string) *rate.Limiter { + mu.Lock() + defer mu.Unlock() + + limiter, exists := visitors[ip] + if !exists { + // Create a new limiter: allow 1 request per second, with a burst maximum of 3 + limiter = rate.NewLimiter(rate.Every(1*time.Second), 2) + visitors[ip] = limiter + } + + return limiter +} + +// RateLimitMiddleware acts as a shield in front of your handlers +func RateLimitMiddleware(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get the user's IP address (simplified for this example) + ip := r.RemoteAddr + + // Get the rate limiter for this specific IP + limiter := getVisitor(ip) + + // Ask the limiter if we are allowed to proceed + if !limiter.Allow() { + // If they are going too fast, block them! + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusTooManyRequests) // 429 Error code + json.NewEncoder(w).Encode(map[string]interface{}{ + "success": false, + "message": "Too many login attempts. Please wait a moment and try again.", + }) + return + } + + // If they are within the limit, allow the request to pass through to your handler + next.ServeHTTP(w, r) + } +} diff --git a/internal/pkg/account/services.go b/backend/internal/pkg/account/services.go similarity index 100% rename from internal/pkg/account/services.go rename to backend/internal/pkg/account/services.go diff --git a/internal/pkg/handler/jsonWrite.go b/backend/internal/pkg/handler/jsonWrite.go similarity index 77% rename from internal/pkg/handler/jsonWrite.go rename to backend/internal/pkg/handler/jsonWrite.go index 27e4473..46ce8b9 100644 --- a/internal/pkg/handler/jsonWrite.go +++ b/backend/internal/pkg/handler/jsonWrite.go @@ -15,7 +15,8 @@ type APIResponse struct { type LoginResponse struct { Success bool `json:"success"` Message string `json:"message"` - UserID string `json:"user_id"` + ID string `json:"id,omitempty"` // Optional: include user ID in response + UserID string `json:"userid,omitempty"` // Optional: include user ID in response } // Auth Handler (POST) - Converted to JSON API diff --git a/internal/pkg/structMessage.go b/backend/internal/pkg/structMessage.go similarity index 100% rename from internal/pkg/structMessage.go rename to backend/internal/pkg/structMessage.go diff --git a/backend/internal/user/dashboard.go b/backend/internal/user/dashboard.go new file mode 100644 index 0000000..fb06ddb --- /dev/null +++ b/backend/internal/user/dashboard.go @@ -0,0 +1,54 @@ +package user + +import ( + "fmt" + "net/http" +) + +// Transaction struct represents a user's transaction for the dashboard view +type Transaction struct { + Date string `json:"date" db:"date"` + Description string `json:"description" db:"description"` + Type string `json:"type" db:"transaction_type"` + Amount float64 `json:"amount" db:"transaction_amount"` +} + +// DashboardUser info struct for the user dashboard view +type DashboardUser struct { + ID int `json:"id,omitempty" db:"id"` + UserID string `json:"user_id,omitempty" db:"user_id"` + Username string `json:"username" db:"username"` + Name string `json:"name" db:"name"` + Transaction string `json:"transaction" db:"transaction"` + Balance float64 `json:"balance" db:"balance"` + LoyaltyPoints int `json:"loyalty_points" db:"loyalty_points"` + AccountType string `db:"account_type" json:"account_type"` + RecentTransactions []Transaction `json:"recent_transactions"` // Add recent transactions to the dashboard response +} + +func (h *Handler) DashboardView(w http.ResponseWriter, r *http.Request) { + fmt.Println("Dashboard view is running...") + h.Tpl.ExecuteTemplate(w, "dashboard.html", nil) +} + +func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { + fmt.Println("Dashboard handler is running...") + + // For demonstration, we'll create a dummy user with some transactions + dashboardUser := DashboardUser{ + ID: 1, + UserID: "user123", + Username: "johndoe", + Name: "John Doe", + Transaction: "Transaction", + Balance: 150.75, + LoyaltyPoints: 200, + AccountType: "Premium", + RecentTransactions: []Transaction{ + {Date: "2024-06-01", Description: "Grocery Store", Type: "Debit", Amount: 50.25}, + {Date: "2024-06-03", Description: "Salary", Type: "Credit", Amount: 2000.00}, + {Date: "2024-06-05", Description: "Online Shopping", Type: "Debit", Amount: 75.50}, + }, + } + h.Tpl.ExecuteTemplate(w, "dashboard.html", dashboardUser) +} diff --git a/backend/internal/user/handler.go b/backend/internal/user/handler.go new file mode 100644 index 0000000..4b42394 --- /dev/null +++ b/backend/internal/user/handler.go @@ -0,0 +1,16 @@ +package user + +import ( + "database/sql" + "html/template" +) + +type Handler struct { + DB *sql.DB + Tpl *template.Template +} + + +func NewHandler(db *sql.DB, tpl *template.Template) *Handler { + return &Handler{DB: db, Tpl: tpl} +} diff --git a/pkg/databases/db.go b/backend/pkg/databases/db.go similarity index 100% rename from pkg/databases/db.go rename to backend/pkg/databases/db.go diff --git a/assets/admin/addcard.css b/frontend/assets/admin/addcard.css similarity index 100% rename from assets/admin/addcard.css rename to frontend/assets/admin/addcard.css diff --git a/assets/images/Fare Receipt Details.png b/frontend/assets/images/Fare Receipt Details.png similarity index 100% rename from assets/images/Fare Receipt Details.png rename to frontend/assets/images/Fare Receipt Details.png diff --git a/assets/images/Fare Receipt.png b/frontend/assets/images/Fare Receipt.png similarity index 100% rename from assets/images/Fare Receipt.png rename to frontend/assets/images/Fare Receipt.png diff --git a/assets/images/Retail Receipt Details.png b/frontend/assets/images/Retail Receipt Details.png similarity index 100% rename from assets/images/Retail Receipt Details.png rename to frontend/assets/images/Retail Receipt Details.png diff --git a/assets/images/Retail Receipt.png b/frontend/assets/images/Retail Receipt.png similarity index 100% rename from assets/images/Retail Receipt.png rename to frontend/assets/images/Retail Receipt.png diff --git a/frontend/assets/js/card.js b/frontend/assets/js/card.js new file mode 100644 index 0000000..2780459 --- /dev/null +++ b/frontend/assets/js/card.js @@ -0,0 +1,201 @@ +document.addEventListener("DOMContentLoaded", function () { + + // --- Report Card Modal Elements --- + const reportButton = document.getElementById('report-card-button'); + const reportModal = document.getElementById('report-card-modal'); + const reportModalContent = document.getElementById('report-card-modal-content'); + const closeReportModalButton = document.getElementById('report-modal-close-button'); + const cancelReportModalButton = document.getElementById('report-modal-cancel-button'); + const confirmReportButton = document.getElementById('report-modal-confirm-button'); + + // --- Replacement Modal Elements --- + const replacementButton = document.getElementById('request-replacement-button'); + const replacementModal = document.getElementById('replacement-modal'); + const replacementModalContent = document.getElementById('replacement-modal-content'); + const closeReplacementModalButton = document.getElementById('replacement-modal-close-button'); + const cancelReplacementModalButton = document.getElementById('replacement-modal-cancel-button'); + const confirmReplacementButton = document.getElementById('replacement-modal-confirm-button'); + + // --- Status Badge Element --- + const cardStatusBadge = document.getElementById('card-status-badge'); + + + // --- Robustness Check --- + const reportElementsExist = reportButton && reportModal && reportModalContent && + closeReportModalButton && cancelReportModalButton && confirmReportButton; + + const replacementElementsExist = replacementButton && replacementModal && replacementModalContent && + closeReplacementModalButton && cancelReplacementModalButton && confirmReplacementButton; + + if (!reportElementsExist) { + console.error("My Card script error: Report Card modal elements were not found. Check your IDs."); + } + + if (!replacementElementsExist) { + console.error("My Card script error: Replacement Modal elements were not found. Check your IDs."); + } + + if (!cardStatusBadge) { + console.error("My Card script error: Card Status Badge element not found. Check your ID."); + } + + // Stop the script if critical elements are missing + if (!reportElementsExist || !replacementElementsExist || !cardStatusBadge) { + return; + } + + // --- Generic Modal Logic --- + // Function to open a modal + function openModal(modal, modalContent) { + modal.classList.remove('hidden'); + // Animate in + setTimeout(() => { + modal.classList.add('opacity-100'); + modalContent.classList.add('scale-100', 'opacity-100'); + modalContent.classList.remove('scale-95', 'opacity-0'); + }, 10); // 10ms delay to allow CSS to catch up + } + + // Function to close a modal + function closeModal(modal, modalContent) { + // Animate out + modalContent.classList.add('scale-95', 'opacity-0'); + modalContent.classList.remove('scale-100', 'opacity-100'); + modal.classList.remove('opacity-100'); + + // Hide after animation (300ms) + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); + } + + + // --- Attach Listeners for Report Modal --- + reportButton.addEventListener('click', (e) => { + e.preventDefault(); + openModal(reportModal, reportModalContent); + }); + + closeReportModalButton.addEventListener('click', () => closeModal(reportModal, reportModalContent)); + cancelReportModalButton.addEventListener('click', () => closeModal(reportModal, reportModalContent)); + + reportModal.addEventListener('click', (e) => { + if (e.target === reportModal) { + closeModal(reportModal, reportModalContent); + } + }); + + confirmReportButton.addEventListener('click', () => { + console.log('Reporting card as lost/stolen...'); + + // --- THIS IS A FRONT-END-ONLY DEMO --- + // In a real app, you would send a fetch() request to your Go backend here + // to update the card_blocks table and the user's status. + + // --- Logic Update --- + // Disable the "Report" button, but leave "Request Replacement" active. + reportButton.disabled = true; + reportButton.innerHTML = ' Card Blocked'; + reportButton.classList.add('opacity-50', 'cursor-not-allowed'); + + // Update the status badge + setCardStatus("Blocked"); + + closeModal(reportModal, reportModalContent); + }); + + + // --- Attach Listeners for Replacement Modal --- + replacementButton.addEventListener('click', (e) => { + e.preventDefault(); + openModal(replacementModal, replacementModalContent); + }); + + closeReplacementModalButton.addEventListener('click', () => closeModal(replacementModal, replacementModalContent)); + cancelReplacementModalButton.addEventListener('click', () => closeModal(replacementModal, replacementModalContent)); + + replacementModal.addEventListener('click', (e) => { + if (e.target === replacementModal) { + closeModal(replacementModal, replacementModalContent); + } + }); + + confirmReplacementButton.addEventListener('click', () => { + console.log('Requesting new card...'); + + // --- THIS IS A FRONT-END-ONLY DEMO --- + // In a real app, you would send a fetch() request to your Go backend here + // to deduct the fee and log the replacement. + + // --- Logic Update --- + // Disable BOTH buttons since a replacement has been requested. + reportButton.disabled = true; + reportButton.innerHTML = ' Card Blocked'; + reportButton.classList.add('opacity-50', 'cursor-not-allowed'); + + replacementButton.disabled = true; + replacementButton.textContent = 'Replacement Requested'; + replacementButton.classList.add('opacity-50', 'cursor-not-allowed'); + + // Update the status badge + setCardStatus("Replaced"); + + closeModal(replacementModal, replacementModalContent); + }); + + + // --- Card Status Badge Logic --- + function setCardStatus(status) { + // Clear all existing color classes + cardStatusBadge.classList.remove('bg-green-100', 'text-green-800', 'bg-red-100', 'text-red-800', 'bg-yellow-100', 'text-yellow-800', 'bg-gray-100', 'text-gray-800'); + + switch (status) { + case 'Active': + cardStatusBadge.classList.add('bg-green-100', 'text-green-800'); + cardStatusBadge.textContent = 'Active'; + break; + case 'Blocked': + case 'Stolen': + case 'Lost': + cardStatusBadge.classList.add('bg-red-100', 'text-red-800'); + cardStatusBadge.textContent = 'Blocked'; + break; + case 'Replaced': + case 'Inactive': + cardStatusBadge.classList.add('bg-yellow-100', 'text-yellow-800'); + cardStatusBadge.textContent = 'Inactive'; + break; + default: + cardStatusBadge.classList.add('bg-gray-100', 'text-gray-800'); + cardStatusBadge.textContent = 'Unknown'; + } + } + + // --- This function simulates fetching the user's card status from the backend --- + function fetchCardStatus() { + // --- FRONTEND-ONLY DEMO --- + // In a real app, you would make a fetch() call to your Go backend here, + // get the user's real card status from the 'users' table, + // and then call setCardStatus() with the result. + + // For this demo, we'll just set it to "Active". + const currentDemoStatus = "Active"; // You can change this to "Blocked" to test + setCardStatus(currentDemoStatus); + + // Also, update the button states based on the fetched status + if (currentDemoStatus === "Blocked" || currentDemoStatus === "Replaced" || currentDemoStatus === "Inactive") { + reportButton.disabled = true; + reportButton.innerHTML = ' Card Blocked'; + reportButton.classList.add('opacity-50', 'cursor-not-allowed'); + } + if (currentDemoStatus === "Replaced") { + replacementButton.disabled = true; + replacementButton.textContent = 'Replacement Requested'; + replacementButton.classList.add('opacity-50', 'cursor-not-allowed'); + } + } + + // --- Run the functions on page load --- + fetchCardStatus(); + +}); \ No newline at end of file diff --git a/frontend/assets/js/dashboard.js b/frontend/assets/js/dashboard.js new file mode 100644 index 0000000..e75b2a3 --- /dev/null +++ b/frontend/assets/js/dashboard.js @@ -0,0 +1,148 @@ +document.addEventListener("DOMContentLoaded", function () { + const sidebar = document.getElementById('sidebar'); + const sidebarOverlay = document.getElementById('sidebar-overlay'); + const toggleButton = document.getElementById('menu-toggle-button'); + const openIcon = document.getElementById('icon-open'); + const closeIcon = document.getElementById('icon-close'); + const mainContent = document.getElementById('main-content'); + + // --- Profile Dropdown Elements --- + const profileButton = document.getElementById('profile-avatar-button'); + const profileMenu = document.getElementById('profile-dropdown-menu'); + const profileLogoutButton = document.getElementById('profile-logout-button'); // ADDED + + // --- Logout Modal Elements --- + const logoutButton = document.getElementById('logout-button'); + const logoutModal = document.getElementById('logout-modal'); + const logoutModalContent = document.getElementById('logout-modal-content'); + const closeModalButton = document.getElementById('modal-close-button'); + const cancelModalButton = document.getElementById('modal-cancel-button'); + const confirmLogoutButton = document.getElementById('modal-confirm-logout-button'); + + + // --- Sidebar Logic --- + if (sidebar && sidebarOverlay && toggleButton && openIcon && closeIcon && mainContent) { + + function toggleSidebar() { + sidebar.classList.toggle('-translate-x-full'); + mainContent.classList.toggle('md:pl-64'); + openIcon.classList.toggle('hidden'); + closeIcon.classList.toggle('hidden'); + if (window.innerWidth < 768) { + sidebarOverlay.classList.toggle('hidden'); + } + } + + toggleButton.addEventListener('click', function(e) { + e.stopPropagation(); + toggleSidebar(); + }); + + sidebarOverlay.addEventListener('click', function() { + toggleSidebar(); + }); + + // --- Auto-close sidebar on nav link click (for mobile) --- + const navLinks = sidebar.querySelectorAll('nav a'); + navLinks.forEach(link => { + link.addEventListener('click', () => { + if (window.innerWidth < 768 && !closeIcon.classList.contains('hidden')) { + toggleSidebar(); + } + }); + }); + + } else { + console.error("Sidebar elements not found. Make sure all IDs are correct."); + } + + // --- Profile Dropdown Logic --- + if (profileButton && profileMenu) { + + profileButton.addEventListener('click', function(event) { + event.stopPropagation(); + profileMenu.classList.toggle('hidden'); + }); + + document.addEventListener('click', function(event) { + if (!profileMenu.classList.contains('hidden') && + !profileButton.contains(event.target) && + !profileMenu.contains(event.target)) + { + profileMenu.classList.add('hidden'); + } + }); + + } else { + console.error("Profile dropdown elements not found. Make sure all IDs are correct."); + } + + // --- Logout Modal Logic --- + // Check for all required modal elements + const modalElementsExist = logoutModal && logoutModalContent && closeModalButton && cancelModalButton && confirmLogoutButton; + + if (modalElementsExist) { + + // Function to open the modal + function openLogoutModal() { + logoutModal.classList.remove('hidden'); + setTimeout(() => { + logoutModal.classList.add('opacity-100'); + logoutModalContent.classList.add('scale-100', 'opacity-100'); + logoutModalContent.classList.remove('scale-95', 'opacity-0'); + }, 10); + } + + // Function to close the modal + function closeLogoutModal() { + logoutModalContent.classList.add('scale-95', 'opacity-0'); + logoutModalContent.classList.remove('scale-100', 'opacity-100'); + logoutModal.classList.remove('opacity-100'); + + setTimeout(() => { + logoutModal.classList.add('hidden'); + }, 300); + } + + // --- UPDATED: Attach to all logout buttons --- + + // 1. Sidebar Logout Button + if (logoutButton) { + logoutButton.addEventListener('click', (e) => { + e.preventDefault(); + openLogoutModal(); + }); + } + + // 2. Profile Dropdown Logout Button + if (profileLogoutButton) { + profileLogoutButton.addEventListener('click', (e) => { + e.preventDefault(); + profileMenu.classList.add('hidden'); // Close dropdown + openLogoutModal(); + }); + } + // --- END OF UPDATE --- + + // Close modal buttons + closeModalButton.addEventListener('click', closeLogoutModal); + cancelModalButton.addEventListener('click', closeLogoutModal); + + // Also close if clicking on the background overlay + logoutModal.addEventListener('click', (e) => { + if (e.target === logoutModal) { + closeLogoutModal(); + } + }); + + // Confirm logout and redirect + confirmLogoutButton.addEventListener('click', () => { + console.log('Logging out...'); + window.location.href = "login.html"; + }); + + } else { + console.error("Logout modal elements not found. Make sure all IDs are correct."); + } + +}); \ No newline at end of file diff --git a/frontend/assets/js/forgot-password.js b/frontend/assets/js/forgot-password.js new file mode 100644 index 0000000..8856e73 --- /dev/null +++ b/frontend/assets/js/forgot-password.js @@ -0,0 +1,65 @@ +document.addEventListener("DOMContentLoaded", function () { + // Get all the elements + const forgotForm = document.getElementById('forgot-form'); + const emailStep = document.getElementById('email-step'); + const otpStep = document.getElementById('otp-step'); + + const formTitle = document.getElementById('form-title'); + const formSubtitle = document.getElementById('form-subtitle'); + + // Get the two buttons + const sendLinkBtn = document.getElementById('send-link-btn'); + const confirmOtpBtn = document.getElementById('confirm-otp-btn'); + + // This check is in case the script is loaded on a page without these elements + if (forgotForm && sendLinkBtn && confirmOtpBtn && emailStep && otpStep) { + + // --- STEP 1: Handle Email submission --- + // Listen for a CLICK on the "Send Reset Link" button + sendLinkBtn.addEventListener('click', function (event) { + event.preventDefault(); // Stop the form from submitting + + // --- THIS IS A FRONTEND-ONLY DEMO --- + // In a real app, you would make a fetch() call to your backend here + // to check if the email exists and to send the OTP. + + const email = document.getElementById('email').value; + + // For this demo, we'll just simulate a successful email send + // (assuming the email was valid). + console.log(`Simulating sending OTP to: ${email}`); + + // Hide the email step and show the OTP step + emailStep.classList.add('hidden'); + otpStep.classList.remove('hidden'); + + // Update the titles + formTitle.textContent = "Check your email"; + formSubtitle.textContent = "We sent a 6-digit code to your email."; + }); + + + // --- STEP 2: Handle OTP submission --- + // Listen for a CLICK on the "Confirm Code" button + confirmOtpBtn.addEventListener('click', function(event) { + event.preventDefault(); // Stop the form from submitting + + // --- THIS IS A FRONTEND-ONLY DEMO --- + // This is where you would verify the OTP with your backend + const otp = document.getElementById('otp').value; + + // For demo purposes, we'll just check for a static OTP + if(otp === "123456") { // Example: a valid OTP + alert("OTP Correct! You would now be redirected to reset your password."); + // redirect to login page + window.location.href = "../templates/resetpassword.html"; + + // In a real app, you would redirect to the password reset page + // window.location.href = "/reset-password.html"; + } else { + alert("Invalid OTP. Please try again."); + } + }); + } +}); + diff --git a/frontend/assets/js/login.js b/frontend/assets/js/login.js new file mode 100644 index 0000000..ba8a51b --- /dev/null +++ b/frontend/assets/js/login.js @@ -0,0 +1,40 @@ +document.addEventListener("DOMContentLoaded", function () { + const loginForm = document.getElementById("login-form"); + const errorMessage = document.getElementById("errorMessage"); + + if (loginForm) { + loginForm.addEventListener("submit", function (e) { + e.preventDefault(); + + // Hide error message initially + if (errorMessage) errorMessage.classList.add("hidden"); + + const idValue = document.getElementById("identifier").value; + const pass = document.getElementById("password").value; + + fetch("/api/v1/loginauth", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ identifier: idValue, password: pass }) + }) + .then(r => r.json()) + .then(data => { + if (data.success) { + window.location.href = "/dashboard"; + } else { + if (errorMessage) { + errorMessage.classList.remove("hidden"); + errorMessage.innerText = data.message || "An error occurred"; + } + } + }) + .catch(err => { + console.error(err); + if (errorMessage) { + errorMessage.classList.remove("hidden"); + errorMessage.innerText = "Network error. Please try again."; + } + }); + }); + } +}); \ No newline at end of file diff --git a/frontend/assets/js/profile.js b/frontend/assets/js/profile.js new file mode 100644 index 0000000..64b0c98 --- /dev/null +++ b/frontend/assets/js/profile.js @@ -0,0 +1,187 @@ +document.addEventListener("DOMContentLoaded", function () { + console.log("Profile page script loaded."); + + // --- Profile Edit Elements --- + const editProfileBtn = document.getElementById('edit-profile-btn'); + const profileView = document.getElementById('profile-details-view'); + const profileEditForm = document.getElementById('profile-details-edit'); + + if (editProfileBtn && profileView && profileEditForm) { + editProfileBtn.addEventListener('click', () => { + const isEditing = editProfileBtn.textContent.trim() === 'Save'; + + if (isEditing) { + // --- THIS IS A FRONTEND-ONLY DEMO --- + // In a real app, you would send a fetch() request to your backend + // to save the new details from the form. + + // For now, we'll just update the text and switch back + const newName = document.getElementById('full_name').value; + const newEmail = document.getElementById('email').value; + const newPhone = document.getElementById('phone').value; + + profileView.querySelector('dd:nth-of-type(1)').textContent = newName; + profileView.querySelector('dd:nth-of-type(2)').textContent = newEmail; + profileView.querySelector('dd:nth-of-type(3)').textContent = newPhone; + + profileView.classList.remove('hidden'); + profileEditForm.classList.add('hidden'); + editProfileBtn.innerHTML = ' Edit'; + } else { + // Switch to edit mode + profileView.classList.add('hidden'); + profileEditForm.classList.remove('hidden'); + editProfileBtn.innerHTML = ' Save'; + } + }); + } + + // --- Change Password Elements --- + const passwordForm = document.getElementById('change-password-form'); + const newPasswordInput = document.getElementById('new_password'); + const confirmPasswordInput = document.getElementById('confirm_password'); + const passwordSubmitBtn = document.getElementById('change-password-btn'); + const passwordErrorMsg = document.getElementById('password-error-message'); + + // Checklist elements + const checklist = document.getElementById('password-checklist'); + const lengthCheck = document.getElementById('length-check'); + const caseCheck = document.getElementById('case-check'); + const numCheck = document.getElementById('num-check'); + const matchCheck = document.getElementById('match-check'); + + // Regex for validation + const hasLower = new RegExp(/[a-z]/); + const hasUpper = new RegExp(/[A-Z]/); + const hasNumber = new RegExp(/[0-9]/); + + // Helper to update checklist items + function updateChecklistItem(checkElement, isValid) { + if (!checkElement) return; + 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'); + } + } + + // Main validation function + function validatePasswordForm() { + const password = newPasswordInput.value; + const confirmPassword = confirmPasswordInput.value; + + if (password.length > 0 || confirmPassword.length > 0) { + checklist.classList.remove('hidden'); + } else { + checklist.classList.add('hidden'); + } + + const isLengthValid = password.length >= 8; + const isCaseValid = hasLower.test(password) && hasUpper.test(password); + const isNumValid = hasNumber.test(password); + const passwordsMatch = password === confirmPassword && password.length > 0; + + updateChecklistItem(lengthCheck, isLengthValid); + updateChecklistItem(caseCheck, isCaseValid); + updateChecklistItem(numCheck, isNumValid); + updateChecklistItem(matchCheck, passwordsMatch); + + const allValid = isLengthValid && isCaseValid && isNumValid && passwordsMatch; + passwordSubmitBtn.disabled = !allValid; + } + + if (passwordForm) { + newPasswordInput.addEventListener('input', validatePasswordForm); + confirmPasswordInput.addEventListener('input', validatePasswordForm); + + passwordForm.addEventListener('submit', (e) => { + e.preventDefault(); + if (passwordSubmitBtn.disabled) { + passwordErrorMsg.textContent = 'Please fix the errors in the password checklist.'; + passwordErrorMsg.classList.remove('hidden'); + } else { + // --- FRONTEND-ONLY DEMO --- + // In a real app, you'd send a fetch() request to your backend + // to verify the *current* password and set the new one. + passwordErrorMsg.classList.add('hidden'); + alert('Password changed successfully!'); + passwordForm.reset(); + checklist.classList.add('hidden'); + passwordSubmitBtn.disabled = true; + } + }); + } + + // --- Delete Account Modal Elements --- + const deleteAccountBtn = document.getElementById('delete-account-btn'); + const deleteModal = document.getElementById('delete-account-modal'); + const deleteModalContent = document.getElementById('delete-modal-content'); + const deleteModalCloseBtn = document.getElementById('delete-modal-close-button'); + const deleteModalCancelBtn = document.getElementById('delete-modal-cancel-button'); + const deleteModalConfirmBtn = document.getElementById('delete-modal-confirm-button'); + const deleteConfirmText = document.getElementById('delete-confirm-text'); + + if (deleteAccountBtn && deleteModal && deleteModalContent && deleteModalCloseBtn && deleteModalCancelBtn && deleteModalConfirmBtn && deleteConfirmText) { + + function openDeleteModal() { + deleteModal.classList.remove('hidden'); + setTimeout(() => { + deleteModal.classList.add('opacity-100'); + deleteModalContent.classList.add('scale-100', 'opacity-100'); + deleteModalContent.classList.remove('scale-95', 'opacity-0'); + }, 10); + } + + function closeDeleteModal() { + deleteModalContent.classList.add('scale-95', 'opacity-0'); + deleteModalContent.classList.remove('scale-100', 'opacity-1ci00'); + deleteModal.classList.add('hidden', 'opacity-0'); + + // Reset the form in the modal + deleteConfirmText.value = ''; + deleteModalConfirmBtn.disabled = true; + deleteModalConfirmBtn.classList.add('bg-gray-400', 'cursor-not-allowed'); + deleteModalConfirmBtn.classList.remove('bg-red-600', 'hover:bg-red-700'); + + setTimeout(() => { + deleteModal.classList.add('hidden'); + }, 300); + } + + // Check if user has typed "DELETE" + deleteConfirmText.addEventListener('input', () => { + if (deleteConfirmText.value === 'DELETE') { + deleteModalConfirmBtn.disabled = false; + deleteModalConfirmBtn.classList.remove('bg-gray-400', 'cursor-not-allowed'); + deleteModalConfirmBtn.classList.add('bg-red-600', 'hover:bg-red-700'); + } else { + deleteModalConfirmBtn.disabled = true; + deleteModalConfirmBtn.classList.add('bg-gray-400', 'cursor-not-allowed'); + deleteModalConfirmBtn.classList.remove('bg-red-600', 'hover:bg-red-700'); + } + }); + + deleteAccountBtn.addEventListener('click', openDeleteModal); + deleteModalCloseBtn.addEventListener('click', closeDeleteModal); + deleteModalCancelBtn.addEventListener('click', closeDeleteModal); + + deleteModal.addEventListener('click', (e) => { + if (e.target === deleteModal) { + closeDeleteModal(); + } + }); + + deleteModalConfirmBtn.addEventListener('click', () => { + // --- FRONTEND-ONLY DEMO --- + // In a real app, send fetch() request to delete the user + alert('Account deleted. Redirecting...'); + window.location.href = "paycard_login.html"; + }); + } + +}); \ No newline at end of file diff --git a/frontend/assets/js/reset-password.js b/frontend/assets/js/reset-password.js new file mode 100644 index 0000000..f9ff139 --- /dev/null +++ b/frontend/assets/js/reset-password.js @@ -0,0 +1,106 @@ +document.addEventListener("DOMContentLoaded", function () { + // Find all the elements from the reset form + const resetForm = document.getElementById('reset-form'); + const passwordInput = document.getElementById('new_password'); + const confirmPasswordInput = document.getElementById('confirm_password'); + const submitButton = document.getElementById('reset-submit-btn'); + const errorMessage = document.getElementById('error-message'); + + // Validation checklist items + const checklist = document.getElementById('validation-checklist'); + const lengthCheck = document.getElementById('length-check'); + const caseCheck = document.getElementById('case-check'); + const numCheck = document.getElementById('num-check'); + const matchCheck = document.getElementById('match-check'); + + // Regex for validation + const hasLower = new RegExp(/[a-z]/); + const hasUpper = new RegExp(/[A-Z]/); + const hasNumber = new RegExp(/[0-9]/); + + // Helper function to update the checklist icons and colors + function updateChecklistItem(checkElement, isValid) { + // Stop if the element doesn't exist + if (!checkElement) return; + + 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'); + } + } + + // Main validation function that runs on every input + function validateForm() { + const password = passwordInput.value; + const confirmPassword = confirmPasswordInput.value; + + // Show the checklist as soon as the user starts typing in either password field + if (password.length > 0 || confirmPassword.length > 0) { + checklist.classList.remove('hidden'); + } else { + checklist.classList.add('hidden'); + } + + // 1. Check password complexity + const isLengthValid = password.length >= 8; + const isCaseValid = hasLower.test(password) && hasUpper.test(password); + const isNumValid = hasNumber.test(password); + + // 2. Check if passwords match + // Passwords only match if they are not empty and are equal + const passwordsMatch = password === confirmPassword && password.length > 0; + + // 3. Update the checklist UI + updateChecklistItem(lengthCheck, isLengthValid); + updateChecklistItem(caseCheck, isCaseValid); + updateChecklistItem(numCheck, isNumValid); + updateChecklistItem(matchCheck, passwordsMatch); + + // 4. Enable or disable the submit button + const allValid = isLengthValid && isCaseValid && isNumValid && passwordsMatch; + + if (allValid) { + submitButton.disabled = false; + errorMessage.classList.add('hidden'); + } else { + submitButton.disabled = true; + } + } + + // Add event listeners to both password fields + if (passwordInput && confirmPasswordInput) { + passwordInput.addEventListener('input', validateForm); + confirmPasswordInput.addEventListener('input', validateForm); + } + + // Handle form submission + if (resetForm) { + resetForm.addEventListener('submit', function (event) { + event.preventDefault(); // Stop default form submission + + // Double-check validation before "submitting" + validateForm(); + + if (submitButton.disabled) { + // Updated error message + errorMessage.textContent = 'Please fix the errors in the password checklist.'; + errorMessage.classList.remove('hidden'); + } else { + // --- THIS IS A FRONTEND-ONLY DEMO --- + // In a real app, you would make a fetch() call to your backend + // to set the new password. + errorMessage.classList.add('hidden'); + alert('Password reset successfully! Redirecting to login...'); + + // Redirect to login page after success + window.location.href = "paycard_login.html"; + } + }); + } +}); \ No newline at end of file diff --git a/frontend/assets/js/signup.js b/frontend/assets/js/signup.js new file mode 100644 index 0000000..c2f3252 --- /dev/null +++ b/frontend/assets/js/signup.js @@ -0,0 +1,270 @@ +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'); + + // --- BUTTONS --- + const btnStep1 = document.getElementById('btn-step-1'); + const btnBack2 = document.getElementById('btn-back-2'); + const btnStep2 = document.getElementById('btn-step-2'); + const btnBack3 = document.getElementById('btn-back-3'); + const createAccountBtn = document.getElementById('create-account-btn'); + const signupForm = document.getElementById('signup-form'); + + // --- STEP 1 INPUTS --- + const firstNameInput = document.getElementById('first_name'); + const lastNameInput = document.getElementById('last_name'); + const emailInput = document.getElementById('email'); + const contactNumberInput = document.getElementById('contact_number'); + + // --- STEP 2 INPUTS --- + const cardIdInput = document.getElementById('card_id'); + const cardIdError = document.getElementById('card-id-error'); + + // --- STEP 3 INPUTS --- + 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 --- + const errorMessage = document.getElementById('error-message'); + + // --- FORM DATA STORAGE --- + const formData = { + firstName: '', + lastName: '', + email: '', + cardId: '', + 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 + } + + // --- SET INITIAL BUTTON STATE --- + btnStep1.disabled = true; + btnStep2.disabled = true; + createAccountBtn.disabled = true; + + // --- HELPER FUNCTIONS --- + function showStep(stepNumber) { + step1.classList.add('step-hidden'); + step2.classList.add('step-hidden'); + step3.classList.add('step-hidden'); + errorMessage.classList.add('hidden'); + + if (stepNumber === 1) { + step1.classList.remove('step-hidden'); + stepSubtitle.textContent = 'Step 1 of 3: Your Details'; + } else if (stepNumber === 2) { + step2.classList.remove('step-hidden'); + stepSubtitle.textContent = 'Step 2 of 3: Card Verification'; + } else if (stepNumber === 3) { + step3.classList.remove('step-hidden'); + stepSubtitle.textContent = 'Step 3 of 3: Create Password'; + } + } + + 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; + errorMessage.classList.remove('hidden'); + } + + // --- STEP VALIDATION LOGIC --- + + // Step 1: 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 !== '' && email.includes('@'); + const isContactValid = contactNumber !== ''; + + if (isNameValid && isEmailValid && isContactValid) { + btnStep1.disabled = false; + errorMessage.classList.add('hidden'); + } else { + btnStep1.disabled = true; + } + } + + // Step 1: 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 === '' || !email.includes('@') || contactNumber === '') { + showError('Please fill all fields and provide a valid email address.'); + return false; + } + + // TODO: Backend check for email + formData.firstName = firstName; + formData.lastName = lastName; + formData.email = email; + formData.contactNumber = contactNumber; + return true; + } + + // Step 2: Validate Card ID (real-time and click) + function validateStep2() { + const cardId = cardIdInput.value.trim(); + + if (cardId === "") { + cardIdError.classList.add('hidden'); + btnStep2.disabled = true; + return false; + } + + const isCardIdOnlyNumbers = /^\d+$/.test(cardId); + const isCardIdValidLength = cardId.length === 10; + + if (!isCardIdOnlyNumbers) { + cardIdError.textContent = 'Card ID must contain only numbers.'; + cardIdError.classList.remove('hidden'); + btnStep2.disabled = true; + return false; + } else if (!isCardIdValidLength) { + cardIdError.textContent = 'Card ID must be exactly 10 digits long.'; + cardIdError.classList.remove('hidden'); + btnStep2.disabled = true; + return false; + } + + // TODO: Backend check for card ID + + cardIdError.classList.add('hidden'); + formData.cardId = cardId; + btnStep2.disabled = false; + return true; + } + + // Step 3: Validate Password + function validateStep3() { + const password = passwordInput.value; + const confirmPassword = confirmPasswordInput.value; + + const isLengthValid = password.length >= 8; + const passwordsMatch = password === confirmPassword && password.length > 0; + + updateChecklistItem(lengthCheck, isLengthValid); + updateChecklistItem(matchCheck, passwordsMatch); + + const allValid = isLengthValid && passwordsMatch; + createAccountBtn.disabled = !allValid; + + if (allValid) { + formData.password = password; + return true; + } + return false; + } + + // --- EVENT LISTENERS --- + + // --- Add real-time listeners for Step 1 --- + firstNameInput.addEventListener('input', validateStep1Realtime); + lastNameInput.addEventListener('input', validateStep1Realtime); + emailInput.addEventListener('input', validateStep1Realtime); + contactNumberInput.addEventListener('input', validateStep1Realtime); + + // Next from Step 1 + btnStep1.addEventListener('click', function () { + if (validateStep1()) { + showStep(2); + } + }); + + // Back from Step 2 + btnBack2.addEventListener('click', function () { + showStep(1); + }); + + // Next from Step 2 + btnStep2.addEventListener('click', function () { + if (validateStep2()) { + showStep(3); + } + }); + + // Real-time validation for Card ID as user types + cardIdInput.addEventListener('input', validateStep2); + + // Back from Step 3 + btnBack3.addEventListener('click', function () { + showStep(2); + }); + + // Real-time validation for Password fields + passwordInput.addEventListener('input', validateStep3); + confirmPasswordInput.addEventListener('input', validateStep3); + + // Final Form Submission + signupForm.addEventListener('submit', function (event) { + event.preventDefault(); + + if (validateStep3()) { + fetch("/api/v1/signupauth", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + firstName: formData.firstName, + lastName: formData.lastName, + email: formData.email, + contactNumber: formData.contactNumber, + cardNumber: formData.cardId, + 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.'); + } + }); + + // --- MODAL BUTTON (ADDED) --- + // Add event listener for the modal's "Go to Login" button + modalCloseBtn.addEventListener('click', function() { + window.location.href = "login.html"; + }); +}); \ No newline at end of file diff --git a/frontend/assets/js/tailwind-config.js b/frontend/assets/js/tailwind-config.js new file mode 100644 index 0000000..2f2acd6 --- /dev/null +++ b/frontend/assets/js/tailwind-config.js @@ -0,0 +1,9 @@ +tailwind.config = { + theme: { + extend: { + fontFamily: { + sans: ["Inter", "sans-serif"], + }, + }, + }, +}; \ No newline at end of file diff --git a/assets/style/index.css b/frontend/assets/style/index.css similarity index 100% rename from assets/style/index.css rename to frontend/assets/style/index.css diff --git a/templates/addCards.html b/frontend/templates/addCards.html similarity index 96% rename from templates/addCards.html rename to frontend/templates/addCards.html index a3f301e..45079fa 100644 --- a/templates/addCards.html +++ b/frontend/templates/addCards.html @@ -16,7 +16,7 @@

Add Card

{{if .Success}}
{{.Success}}
{{end}} -
+
diff --git a/frontend/templates/card.html b/frontend/templates/card.html new file mode 100644 index 0000000..b52988b --- /dev/null +++ b/frontend/templates/card.html @@ -0,0 +1,399 @@ + + + + + + + PayCard - My Card + + + + + + + + + + + + + +
+
+ + + + + + + + + + + +
+ + +
+ + + +
+ + + + +
+
+
+ + +
+ + +
+ + +
+ +
+
+ +

My Card

+ + +
+ +
+

Card Preview

+ +
+ +
+ +
+ + + + + PayCard +
+ + +
+ +
+ +
+
+
+ +

+ **** **** **9831 +

+
+ +
+ +
+

Cardholder Name

+

JUAN DELA CRUZ

+
+ +
+

Expires End

+

12/30

+
+
+
+
+ + +
+
+

Card Details

+ +
+
+
Card ID
+
**** **** 9831
+
+
+
Status
+
+ + + Loading... + +
+
+
+
Account Type
+
PWD (20% Discount)
+
+
+
Expiry Date
+
December 31, 2030
+
+
+
+
+ + +
+
+

Card Actions

+
+ + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/templates/dashboard.html b/frontend/templates/dashboard.html new file mode 100644 index 0000000..2334095 --- /dev/null +++ b/frontend/templates/dashboard.html @@ -0,0 +1,307 @@ + + + + + + + PayCard - Dashboard + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + +
+ + + + +
+
+
+ + +
+
+ +

Dashboard

+ + +
+ +
+
+
+ + + + +
+
+

Current Balance

+

₱139.60

+
+
+
+ + +
+
+
+ + + + +
+
+

Loyalty Points (Cashback)

+

₱12.85

+
+
+
+ + +
+
+
+ + + +
+
+

Account Type

+

PWD

+
+
+
+
+ + +
+
+

Recent Transactions

+ + See all + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Date + + Description + + Type + + Amount +
+ Nov 1, 2025 + + Retail - Main St. Coffee + + Payment + + -₱100.00 +
+ Nov 1, 2025 + + Transport - Route 42 + + Payment + + -₱10.40 +
+ Oct 31, 2025 + + Top-up via GCash + + Top-up + + +₱250.00 +
+
+
+
+
+
+
+ + + + + Logout + + + + \ No newline at end of file diff --git a/templates/deactivateCard.html b/frontend/templates/deactivateCard.html similarity index 95% rename from templates/deactivateCard.html rename to frontend/templates/deactivateCard.html index fe3ce5c..7e69025 100644 --- a/templates/deactivateCard.html +++ b/frontend/templates/deactivateCard.html @@ -16,7 +16,7 @@

Deactivate Card

{{if .Success}}
{{.Success}}
{{end}} - +
diff --git a/frontend/templates/forgot-password.html b/frontend/templates/forgot-password.html new file mode 100644 index 0000000..2a11bbb --- /dev/null +++ b/frontend/templates/forgot-password.html @@ -0,0 +1,137 @@ + + + + + + + PayCard - Forgot Password + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + + +

Forgot Password?

+

Enter your email to get a reset link.

+
+ + + + + +
+
+ +
+ + + + + + +
+
+ + +
+ +
+
+ + + + + + + + +
+
+ + + + + + + diff --git a/frontend/templates/login.html b/frontend/templates/login.html new file mode 100644 index 0000000..f67b3c1 --- /dev/null +++ b/frontend/templates/login.html @@ -0,0 +1,121 @@ + + + + + + + PayCard Login + + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + +

UniCard

+

Sign in to your account

+
+ +
+ +
+ +
+ + + + + + +
+
+ + +
+ +
+ + + + + + +
+
+ + + + + + + +
+ + +
+ +
+ or +
+
+ + +
+

+ Don't have an account? + + Sign up + +

+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/frontend/templates/profile.html b/frontend/templates/profile.html new file mode 100644 index 0000000..ad56539 --- /dev/null +++ b/frontend/templates/profile.html @@ -0,0 +1,381 @@ + + + + + + + PayCard - Profile + + + + + + + + + + + + +
+
+ + + + + + + + + + + +
+ + +
+ +
+ + +
+
+
+ + +
+ + + + + +
+ +
+
+ +

My Profile

+ + +
+ + +
+
+
+

Personal Information

+ + + + +
+ + +
+
+
Full Name
+
Juan Dela Cruz
+
+
+
Email Address
+
j.delacruz@example.com
+
+
+
Phone Number
+
+63 917 123 4567
+
+
+
Username
+
juandc
+
+
+ + + +
+
+ + +
+
+

Security

+
+
+ + +
+
+ + +
+
+ + +
+ + + + + + + + +
+
+ +
+

Danger Zone

+

+ Deleting your account is permanent and cannot be undone. All your data, balance, and transaction history will be removed. +

+ +
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/templates/reset-password.html b/frontend/templates/reset-password.html new file mode 100644 index 0000000..5aa21c3 --- /dev/null +++ b/frontend/templates/reset-password.html @@ -0,0 +1,141 @@ + + + + + + + PayCard - Reset Password + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + +

Set New Password

+

Please create a new, secure password.

+
+ + +
+ + +
+ +
+ + + + + +
+
+ + +
+ +
+ + + + + +
+
+ + + + + + + + + +
+ +
+
+ + + +
+
+ + + + + + + diff --git a/frontend/templates/settings.html b/frontend/templates/settings.html new file mode 100644 index 0000000..9b312df --- /dev/null +++ b/frontend/templates/settings.html @@ -0,0 +1,239 @@ + + + + + + + PayCard - Settings + + + + + + + + + + +
+
+ + + + + + + + + + + +
+ + +
+ +
+ + +
+
+
+ + +
+ + + + + +
+ +
+
+ +

Settings

+ + +
+
+ +
+ +

Page Under Maintenance

+

+ We're working hard to bring you this feature. Please check back soon! +

+ + + + Go Back to Dashboard + +
+
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/templates/signup.html b/frontend/templates/signup.html new file mode 100644 index 0000000..fef4423 --- /dev/null +++ b/frontend/templates/signup.html @@ -0,0 +1,291 @@ + + + + + + + PayCard - Create Account + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ +
+ + +

Create your account

+

Step 1 of 3: Your Details

+
+ + + + + +
+ + +
+ +
+ +
+ + + + +
+ + +
+ + + + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ +
+ + + + +
+
+ + +
+ +
+
+ + +
+ +
+ +
+ + + + +
+ + +
+ + +
+ + +
+
+ + +
+ +
+ +
+ + + + +
+
+ + +
+ +
+ + + + +
+
+ + +
+
+ + At least 8 characters +
+
+ + Passwords match +
+
+ + +
+ + +
+
+
+ + +
+
+
+
+
+ + or + +
+
+ + + + +
+ + + +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/frontend/templates/topup.html b/frontend/templates/topup.html new file mode 100644 index 0000000..26f876a --- /dev/null +++ b/frontend/templates/topup.html @@ -0,0 +1,358 @@ + + + + + + + PayCard - My Card + + + + + + + + + + +
+
+ + + + + + + + + + + +
+ + +
+ +
+ + +
+
+
+ + +
+ + + + + +
+ +
+
+ +

Top Up Your Card

+ + +
+ + +
+
+

Add Balance

+ + +
+ +
+ +
+ +
+ + +
+ + + + + + + + +
+ + +
+

Select Payment Method

+
+ + + + + + + + +
+
+ + +
+ +
+
+
+ + +
+
+

Your Balance

+
+

Current Balance

+

₱139.60

+
+
+

Loyalty Points

+

₱12.85

+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/templates/transaction.html b/frontend/templates/transaction.html new file mode 100644 index 0000000..c121f89 --- /dev/null +++ b/frontend/templates/transaction.html @@ -0,0 +1,329 @@ + + + + + + + PayCard - Transactions + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + +
+ + + + + +
+
+
+ + +
+
+ +

Transaction History

+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Date & Time + + Description + + Type + + Amount + + Balance +
Nov 1, 2025, 4:15 + PMRetail + - Main St. CoffeePayment-₱100.00 + ₱39.60 +
Nov 1, 2025, 3:35 + PM + Transport - Route 42Payment-₱10.40 + ₱139.60 +
Oct 31, 2025, 9:00 + AMTop-up + via GCashTop-up + +₱250.00₱150.00 +
+ + + +
+
+
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/go.mod b/go.mod index aae0e72..6b97033 100644 --- a/go.mod +++ b/go.mod @@ -4,16 +4,23 @@ go 1.25.4 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/go-playground/validator/v10 v10.30.2 github.com/go-sql-driver/mysql v1.9.3 github.com/joho/godotenv v1.5.1 github.com/stretchr/testify v1.11.1 - golang.org/x/crypto v0.45.0 + golang.org/x/crypto v0.49.0 golang.org/x/time v0.15.0 ) require ( - filippo.io/edwards25519 v1.1.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bffa25e..597c732 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,36 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/templates/dashboard.html b/templates/dashboard.html deleted file mode 100644 index 9225ec3..0000000 --- a/templates/dashboard.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - Unicard Dashboard - - - - -
-

Welcome, {{ .Name }}!

-

Username: {{ .Username }} | Account: {{ .AccountType }}

-
- -
-
-

Current Balance

-

₱{{ printf "%.2f" .Balance }}

-
-
-

Loyalty Points

-

{{ .LoyaltyPoints }} pts

-
-
- -

Recent Transactions

- - - - - - - - - - - {{ range .RecentTransactions }} - - - - - - - {{ else }} - - - - {{ end }} - -
DateDescriptionTypeAmount
{{ .Date }}{{ .Description }}{{ .Type }} - ₱{{ printf "%.2f" .Amount }} -
No recent transactions found.
- - Logout - - - \ No newline at end of file diff --git a/templates/login.html b/templates/login.html deleted file mode 100644 index f2c0be5..0000000 --- a/templates/login.html +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - Login Sample - - - -

Login Page

- -
- -

- - -

- - Forgot Password? -

Don't have an account? Sign Up


- - - - -
- - - - - \ No newline at end of file diff --git a/templates/signup.html b/templates/signup.html deleted file mode 100644 index 3f50b11..0000000 --- a/templates/signup.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - Sign Up - - - -

Create an Account

- -
- -

- - -

- - -

- - -

- - -

- - -

- -

Already have an account? Login here


- - - - -
- - - - - \ No newline at end of file diff --git a/test/login_test.go b/test/login_test.go index 902742b..574f140 100644 --- a/test/login_test.go +++ b/test/login_test.go @@ -12,7 +12,7 @@ import ( "regexp" "strings" "testing" - "unicard-go/internal/auth" + "unicard-go/backend/internal/auth" "github.com/DATA-DOG/go-sqlmock" _ "github.com/go-sql-driver/mysql" // Import MySQL driver for real DB tests From 32838ff4cf1a6834f356ca2d2fab3fe3a1786b96 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sat, 25 Apr 2026 10:56:51 +0800 Subject: [PATCH 2/4] feat: implement login and multi-step signup authentication logic with form validation --- frontend/assets/js/login.js | 2 +- frontend/assets/js/signup.js | 63 ++++++++++++++++++++++------------ frontend/templates/signup.html | 49 ++++++++++++-------------- 3 files changed, 63 insertions(+), 51 deletions(-) diff --git a/frontend/assets/js/login.js b/frontend/assets/js/login.js index ba8a51b..90dcf11 100644 --- a/frontend/assets/js/login.js +++ b/frontend/assets/js/login.js @@ -12,7 +12,7 @@ document.addEventListener("DOMContentLoaded", function () { const idValue = document.getElementById("identifier").value; const pass = document.getElementById("password").value; - fetch("/api/v1/loginauth", { + fetch("v1/loginauth", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ identifier: idValue, password: pass }) diff --git a/frontend/assets/js/signup.js b/frontend/assets/js/signup.js index c2f3252..8983065 100644 --- a/frontend/assets/js/signup.js +++ b/frontend/assets/js/signup.js @@ -1,3 +1,32 @@ +// 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'); @@ -52,26 +81,23 @@ document.addEventListener("DOMContentLoaded", function () { return; // Stop the script } - // --- SET INITIAL BUTTON STATE --- - btnStep1.disabled = true; - btnStep2.disabled = true; - createAccountBtn.disabled = true; + // --- INITIALIZATION --- // --- HELPER FUNCTIONS --- function showStep(stepNumber) { - step1.classList.add('step-hidden'); - step2.classList.add('step-hidden'); - step3.classList.add('step-hidden'); + step1.classList.add('hidden'); + step2.classList.add('hidden'); + step3.classList.add('hidden'); errorMessage.classList.add('hidden'); if (stepNumber === 1) { - step1.classList.remove('step-hidden'); + step1.classList.remove('hidden'); stepSubtitle.textContent = 'Step 1 of 3: Your Details'; } else if (stepNumber === 2) { - step2.classList.remove('step-hidden'); + step2.classList.remove('hidden'); stepSubtitle.textContent = 'Step 2 of 3: Card Verification'; } else if (stepNumber === 3) { - step3.classList.remove('step-hidden'); + step3.classList.remove('hidden'); stepSubtitle.textContent = 'Step 3 of 3: Create Password'; } } @@ -104,14 +130,11 @@ document.addEventListener("DOMContentLoaded", function () { const contactNumber = contactNumberInput.value.trim(); const isNameValid = firstName !== '' && lastName !== ''; - const isEmailValid = email !== '' && email.includes('@'); + const isEmailValid = email !== '' && isValidEmail(email); const isContactValid = contactNumber !== ''; if (isNameValid && isEmailValid && isContactValid) { - btnStep1.disabled = false; errorMessage.classList.add('hidden'); - } else { - btnStep1.disabled = true; } } @@ -122,7 +145,7 @@ document.addEventListener("DOMContentLoaded", function () { const email = emailInput.value.trim(); const contactNumber = contactNumberInput.value.trim(); - if (firstName === '' || lastName === '' || email === '' || !email.includes('@') || contactNumber === '') { + if (firstName === '' || lastName === '' || email === '' || !isValidEmail(email) || contactNumber === '') { showError('Please fill all fields and provide a valid email address.'); return false; } @@ -140,8 +163,8 @@ document.addEventListener("DOMContentLoaded", function () { const cardId = cardIdInput.value.trim(); if (cardId === "") { - cardIdError.classList.add('hidden'); - btnStep2.disabled = true; + cardIdError.textContent = 'Please enter your Card ID.'; + cardIdError.classList.remove('hidden'); return false; } @@ -151,12 +174,10 @@ document.addEventListener("DOMContentLoaded", function () { if (!isCardIdOnlyNumbers) { cardIdError.textContent = 'Card ID must contain only numbers.'; cardIdError.classList.remove('hidden'); - btnStep2.disabled = true; return false; } else if (!isCardIdValidLength) { cardIdError.textContent = 'Card ID must be exactly 10 digits long.'; cardIdError.classList.remove('hidden'); - btnStep2.disabled = true; return false; } @@ -164,7 +185,6 @@ document.addEventListener("DOMContentLoaded", function () { cardIdError.classList.add('hidden'); formData.cardId = cardId; - btnStep2.disabled = false; return true; } @@ -180,7 +200,6 @@ document.addEventListener("DOMContentLoaded", function () { updateChecklistItem(matchCheck, passwordsMatch); const allValid = isLengthValid && passwordsMatch; - createAccountBtn.disabled = !allValid; if (allValid) { formData.password = password; @@ -233,7 +252,7 @@ document.addEventListener("DOMContentLoaded", function () { event.preventDefault(); if (validateStep3()) { - fetch("/api/v1/signupauth", { + fetch("v1/signupauth", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ diff --git a/frontend/templates/signup.html b/frontend/templates/signup.html index fef4423..7fcf237 100644 --- a/frontend/templates/signup.html +++ b/frontend/templates/signup.html @@ -20,18 +20,8 @@ - - + + @@ -75,6 +65,7 @@

Create your account

@@ -86,6 +77,7 @@

Create your account

@@ -100,7 +92,7 @@

Create your account

@@ -114,7 +106,9 @@

Create your account

- @@ -123,15 +117,17 @@

Create your account

-
+ + + -
+