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 @@