From 3c34e8cb34461be83be67333c8e6254d686eba60 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose <156560539+aambrose1@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:24:38 -0500 Subject: [PATCH 01/86] Update README.md --- README.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2816407..782edef 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,15 @@ This repository contains the codebase for the KinTree project. This project stems from the CSE 3213/3223 Software Engineering Senior Project I/II course sequence at Mississippi State University. -KinTree is an application that allows users to connect with family members and build virtual, visualized, and dynamic family trees collaboratively. This project began in the Fall of 2024 and is ongoing. KinTree utilizes React, Node.js, and MySQL. +KinTree is an application that allows users to connect with family members and build virtual, visualized, and dynamic family trees collaboratively. KinTree utilizes React, Node.js, and MySQL. This project originated in [Fall 2024](https://github.com/OwenAdams2023/SeniorProject_KinTree#). The project is ongoing through this forked repository in Fall 2025. -The current KinTree project team as of Spring 2025 includes Owen Adams, Kennedi James, Destiny Milsap, and Jade Thompson. The primary stakeholder for this project is Dr. Charan Gudla. +The current KinTree project team as of Fall 2025 includes Andrea Ambrose, Matthew Loyed, Xiang Chen, and Charles Lenson. The primary stakeholder for this project is Dr. Charan Gudla. # Install ### Prerequisites -Node.js (install the correct version for your own OS [here](https://nodejs.org/en)) -MySQL Install (https://dev.mysql.com/downloads/mysql/) +Node.js (install the correct version for your own OS [here](https://nodejs.org/en)) and install MySQL [here](https://dev.mysql.com/downloads/installer/). Set up account information through the Configurator application or through the terminal. ### Setup @@ -21,13 +20,13 @@ From the command line, navigate to the /SeniorProject_KinTree/client directory, `npm install` -Once all dependencies are installed, you can run the frontend of the project (from the /SeniorProject_KinTree/client directory) by running the following command: +Once all dependencies are installed, you can run the frontend of the project from the /SeniorProject_KinTree/client directory by running the following command: `npm start` -This command runs the application in development mode. You can open http://localhost:3000 in your browser to view the application. Any changes made to the source code will cause the application page to refresh and show reflected changes (once the changed file is saved). +This command runs the application in development mode. You can open http://localhost:3000 in your browser to view the application and saved changes. -To run the backend of code (server/API), you must first install the backend node dependencies. Open another command line window and run the following command in the backend directory (/SeniorProject_KinTree/server): +To run the backend, you must first install the backend node dependencies. Open another command line window and run the following command in the backend directory (/SeniorProject_KinTree/server): `npm install` @@ -37,14 +36,22 @@ Then, from the same directory, run the following command to run the server/API: ### Database Setup -Run the command `npm install knex mysql2` +Open the MySQL Client Terminal, login with your password to run the mySQL server. -Create a .env file with MySQL information. Add username, password, and database name. +Create a new database instance on your machine: +`CREATE DATABASE ` -Run the command `node mysql-connection.js` to verify the connection. +Create a .env file with MySQL information. Example env is in the project's root folder. -Run the command `knex:migrate status` to ensure proper migration files are loaded. +Open another command line window in /SeniorProject_KinTree/server/ and run the command `npm install knex mysql2` to install Knex and mySQL2. -Run the command `knex migrate:latest` to create existing database tables. +Verify the connection: +`node mysql-connection.js` + +Ensure proper migration files are loaded: + +`npx knex:migrate status` + +Run the command `npx knex migrate:latest` to create and/or update existing database tables. From bac690ef295c4f23cd21f4baeea9f5a4d8df1d5b Mon Sep 17 00:00:00 2001 From: Andrea Ambrose <156560539+aambrose1@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:37:49 -0500 Subject: [PATCH 02/86] Create .env.example --- docs/.env.example | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/.env.example diff --git a/docs/.env.example b/docs/.env.example new file mode 100644 index 0000000..a5626c2 --- /dev/null +++ b/docs/.env.example @@ -0,0 +1,5 @@ +DB_HOST=localhost +DB_PORT=3306 +DB_USER=my_mysql_username +DB_PASSWORD=my_sql_password +DB_DATABASE=my_database_name From 07a3427c744876b9ab562668cea77fbacc38fcb5 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Thu, 25 Sep 2025 17:58:32 -0500 Subject: [PATCH 03/86] update database env variables --- README.md | 2 +- docs/.env.example | 2 +- server/mysql-connection.js | 2 +- server/package-lock.json | 20 ++++++++++++-------- server/package.json | 2 +- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 782edef..782e7ca 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Open the MySQL Client Terminal, login with your password to run the mySQL server Create a new database instance on your machine: `CREATE DATABASE ` -Create a .env file with MySQL information. Example env is in the project's root folder. +Create a .env file with MySQL information. Example env is in the project's /docs/ folder. Open another command line window in /SeniorProject_KinTree/server/ and run the command `npm install knex mysql2` to install Knex and mySQL2. diff --git a/docs/.env.example b/docs/.env.example index a5626c2..2814bb3 100644 --- a/docs/.env.example +++ b/docs/.env.example @@ -1,5 +1,5 @@ DB_HOST=localhost DB_PORT=3306 -DB_USER=my_mysql_username +DB_USER=root DB_PASSWORD=my_sql_password DB_DATABASE=my_database_name diff --git a/server/mysql-connection.js b/server/mysql-connection.js index 8092625..191170d 100644 --- a/server/mysql-connection.js +++ b/server/mysql-connection.js @@ -7,7 +7,7 @@ const connection = mysql.createConnection({ host: process.env.DB_HOST, // localhost user: process.env.DB_USER, // Make sure DB_USER is set password: process.env.DB_PASSWORD, // Ensure DB_PASSWORD is set - database: process.env.DB_NAME, // Ensure DB_NAME is set + database: process.env.DB_DATABASE, // Ensure DB_DATABASE is set port: process.env.DB_PORT || 3306 // Port should be 3306 }); diff --git a/server/package-lock.json b/server/package-lock.json index 0dea215..99734e4 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,7 +15,7 @@ "express": "^4.21.1", "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", - "mysql2": "^3.14.1", + "mysql2": "^3.15.1", "nodemon": "^3.1.7" }, "devDependencies": { @@ -1714,15 +1714,15 @@ "license": "MIT" }, "node_modules/mysql2": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz", - "integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.1.tgz", + "integrity": "sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", @@ -1734,15 +1734,19 @@ } }, "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/named-placeholders": { diff --git a/server/package.json b/server/package.json index 877fbe2..6a0f438 100644 --- a/server/package.json +++ b/server/package.json @@ -17,7 +17,7 @@ "express": "^4.21.1", "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", - "mysql2": "^3.14.1", + "mysql2": "^3.15.1", "nodemon": "^3.1.7" }, "devDependencies": { From 72a95b60644338593e53c709e904135083217967 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Sun, 28 Sep 2025 15:09:40 -0500 Subject: [PATCH 04/86] added migrations for missing info table and missing member column --- ...8_add_memberUserId_to_treeMembers_table.js | 19 ++++++++++++++++ .../20250928184742_create_tree_info.js | 22 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 server/migrations/20250928183328_add_memberUserId_to_treeMembers_table.js create mode 100644 server/migrations/20250928184742_create_tree_info.js diff --git a/server/migrations/20250928183328_add_memberUserId_to_treeMembers_table.js b/server/migrations/20250928183328_add_memberUserId_to_treeMembers_table.js new file mode 100644 index 0000000..1d46549 --- /dev/null +++ b/server/migrations/20250928183328_add_memberUserId_to_treeMembers_table.js @@ -0,0 +1,19 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema.table("treeMembers", function (table) { + table.integer("memberUserId").unsigned().nullable(); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema.table("treeMembers", function (table) { + table.dropColumn("memberUserId"); + }); +}; diff --git a/server/migrations/20250928184742_create_tree_info.js b/server/migrations/20250928184742_create_tree_info.js new file mode 100644 index 0000000..492cf14 --- /dev/null +++ b/server/migrations/20250928184742_create_tree_info.js @@ -0,0 +1,22 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema.createTable("treeInfo", (table) => { + table.increments("id").primary(); + table.json("object").notNullable(); + table.integer("userId").unsigned().notNullable(); + table.foreign("userId").references("users.id").onDelete("CASCADE"); + table.timestamps(true, true); + }); +} + + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema.dropTableIfExists("treeInfo"); +}; From 153b469ab7d2a36ee9605156a6f255d0681e236c Mon Sep 17 00:00:00 2001 From: Andrea Ambrose <156560539+aambrose1@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:07:35 -0500 Subject: [PATCH 05/86] correct mySQL link & specify env directory in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 782e7ca..733eb52 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The current KinTree project team as of Fall 2025 includes Andrea Ambrose, Matthe ### Prerequisites -Node.js (install the correct version for your own OS [here](https://nodejs.org/en)) and install MySQL [here](https://dev.mysql.com/downloads/installer/). Set up account information through the Configurator application or through the terminal. +Node.js (install the correct version for your own OS [here](https://nodejs.org/en)) and install MySQL [here](https://dev.mysql.com/downloads/mysql/). Set up account information through the Configurator application or through the terminal. ### Setup @@ -41,7 +41,7 @@ Open the MySQL Client Terminal, login with your password to run the mySQL server Create a new database instance on your machine: `CREATE DATABASE ` -Create a .env file with MySQL information. Example env is in the project's /docs/ folder. +In the /server/ directory, create a .env file with MySQL information. Example env is in the project's /docs/ folder. Open another command line window in /SeniorProject_KinTree/server/ and run the command `npm install knex mysql2` to install Knex and mySQL2. From 3f5a7347075b6c5b0c03cd9392a35b1d0c313fe4 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose <156560539+aambrose1@users.noreply.github.com> Date: Sun, 5 Oct 2025 18:51:32 -0500 Subject: [PATCH 06/86] docs: add contributing guidelines to CONTRIBUTING.md This document outlines the workflow standards for contributing to the KinTree project, including guidelines for issues, branch naming, commit messages, and pull requests. --- docs/CONTRIBUTING.md | 88 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/CONTRIBUTING.md diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..c5faa80 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,88 @@ +# Contributing Guidelines + +This document explains the workflow standards for contributing to the KinTree project. +Following these guidelines will help keep our codebase consistent and organized. + +## Table of Contents +1. [Issues](#issues) +2. [Branch Naming](#branch_naming) +3. [Commit Messages](#commit_messages) +4. [Pull Requests](#pull_requests) + +--- +## Issues +Each feature, bug, or task should have its own GitHub issue. + +When creating an issue: + +- Title: Short and specific (e.g., “Add login form validation”, "Refactor navigation component") +- Description: + * What needs to be done + * When it happens +- Labels: Use tags like `frontend`, `backend`, `bug`, etc. +- Assignee: Assign to the person(s) responsible. + +--- +## Branch Naming +Create a new branch for each task from main. + +When creating a branch: + +Format: `/`
+Types: +- feat → new feature +- fix → bug fix +- refactor → code improvement +- docs → documentation update +- test → testing work + +Examples: +- feat/login-validation +- fix/db-connection +- refactor/user-model + +--- +## Commit Messages +Write concise commit messages that describe the change.
+Always commit in *small focused increments* instead of *large and broad*. + +Format: `: `
+Types: +- feat → new feature +- fix → bug fix +- refactor → code improvement +- docs → documentation update +- test → testing work + +Examples: +- feat: add media upload feature + * integrated AWS S3 for image storage + * added upload button to profile page + + --- +## Pull Requests (PRs) +When your branch is ready, open a pull request into main. + +When you're opening a new PR: + +### PR Template: +- Title: Add a short summary +- Summary: Explain what was done and why. +- Changes: + * Added X feature + * Fixed Y issue +- Link an issue that it addresses/closes + +### PR Guidelines: +- Pull the latest version of main. +- Test locally and confirm everything runs correctly. +- Remove any unused code, debug statements, or console logs. + +### PR Review and Approval +- Do not merge your own PR, assign reviewer(s) to your PR. +- At least one team member must review, comment, and approve before merging. + + * Before merging, reviewers should verify: + * Code runs without unexpected errors. + * PR does what it set out to do without breaking other existing functionalities. + From a66765f87cc4d384cc11aae1645c2e0f95f09a29 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Fri, 10 Oct 2025 18:45:33 -0500 Subject: [PATCH 07/86] add gender field to family member forms and db models --- .../AddFamilyMember/AddFamilyMember.js | 34 +++++++++++--- .../src/pages/CreateAccount/CreateAccount.js | 15 ++++++- server/controllers/treeMemberController.js | 5 ++- .../20251010183743_add_gender_to_users.js | 29 ++++++++++++ .../20251010212756_update_rel_table.js | 44 +++++++++++++++++++ 5 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 server/migrations/20251010183743_add_gender_to_users.js create mode 100644 server/migrations/20251010212756_update_rel_table.js diff --git a/client/src/components/AddFamilyMember/AddFamilyMember.js b/client/src/components/AddFamilyMember/AddFamilyMember.js index a81ee3d..6a96571 100644 --- a/client/src/components/AddFamilyMember/AddFamilyMember.js +++ b/client/src/components/AddFamilyMember/AddFamilyMember.js @@ -27,7 +27,7 @@ function AddFamilyMemberPopup({ trigger, userid }) { reset, watch, handleSubmit, - } = useForm({defaultValues: {selectedMember: '', selectedMemberRelationship: '', matPat: ''}}); + } = useForm({defaultValues: {selectedMember: '', selectedMemberRelationship: '', matPat: '', gender: ''}}); // stored list of family members that require maternal/paternal distinction (maybe shift this to retrieval from backend, so that it can be updated without changing code) let matPat = useMemo(() => ["parent", "cousin", "aunt", "uncle", "grandparent", "niece", "nephew"], []); @@ -48,7 +48,7 @@ function AddFamilyMemberPopup({ trigger, userid }) { const { register: register2, handleSubmit: handleSubmit2, - } = useForm({defaultValues: {firstName: '', lastName: '', relationship: '', matPat2: '', location: '', birthday: '', birthplace: '', deathdate: ''}}); + } = useForm({defaultValues: {firstName: '', lastName: '', relationship: '', matPat2: '', location: '', birthday: '', birthplace: '', deathdate: '', gender: ''}}); @@ -132,7 +132,8 @@ function AddFamilyMemberPopup({ trigger, userid }) { "location": null, "phoneNumber": null, "userId": userid, - "memberUserId": users.current.find(user => user.id === Number(memberId)).id + "memberUserId": users.current.find(user => user.id === Number(memberId)).id, + "gender": data.gender, // Ensure gender is explicitly handled and not undefined }) }; @@ -159,7 +160,6 @@ function AddFamilyMemberPopup({ trigger, userid }) { relationshipStatus: "active", side: data.matPat || null, userId: userid, - memberUserId: null }) }; return fetch(`http://localhost:5000/api/relationships/`, nextRequestOptions); @@ -201,6 +201,7 @@ function AddFamilyMemberPopup({ trigger, userid }) { // form submission (manual entry) const onSubmitManual = (data) => { console.log("submit attempted"); + console.log("Form data:", data); // Log the form data to see what we're getting // add new member to family members table let requestOptions = { method: 'POST', @@ -212,7 +213,9 @@ function AddFamilyMemberPopup({ trigger, userid }) { "deathDate" : data.deathDate || null, "location": data.location || null, "phoneNumber": "", - "userId": userid + "userId": userid, + "memberUserId": null, + "gender": data.gender // Send the gender value directly, don't use || null }) }; @@ -313,6 +316,17 @@ function AddFamilyMemberPopup({ trigger, userid }) { )} + {/* select gender */} +
+ +
+ {/* select relationship */}
+
  • + +
  • {/* note: might need to consider adding options to connect new family member to previous ones, to more accurately place them on tree */}
  • diff --git a/client/src/pages/CreateAccount/CreateAccount.js b/client/src/pages/CreateAccount/CreateAccount.js index 9fdc592..9045347 100644 --- a/client/src/pages/CreateAccount/CreateAccount.js +++ b/client/src/pages/CreateAccount/CreateAccount.js @@ -12,6 +12,7 @@ const yupValidation = yup.object().shape( firstname: yup.string().required("First name is a required field."), lastname: yup.string().required("Last name is a required field."), birthdate: yup.date().required("Birthdate is a required field."), + gender: yup.string().oneOf(['M', 'F'], 'Please select a valid option').required('Gender field is required'), email: yup.string().required("Email is a required field.") .matches( "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" @@ -64,7 +65,7 @@ const CreateAccount = () => { // Use responseData.user directly const accountID = responseData.user; - // Initialize user's tree by adding themself + // Add user as a family member return fetch(`http://localhost:5000/api/family-members/`, { method: 'POST', headers: { @@ -79,8 +80,9 @@ const CreateAccount = () => { phoneNumber: data.phonenum, userId: accountID, memberUserId: accountID, + gender: data.gender, }), - }).then(async (response) => { + }).then(async (response) => { // Initialize user's tree by adding themself if (response.ok) { const familyMemberResponse = await response.json(); console.log(familyMemberResponse); @@ -95,6 +97,7 @@ const CreateAccount = () => { "data": { "first name": data.firstname, "last name": data.lastname, + "gender": data.gender, }, "rels": { "children": [], @@ -163,6 +166,14 @@ const CreateAccount = () => { {errors.birthdate &&

    {errors.birthdate.message}

    }
  • +
    + + +
    diff --git a/server/controllers/treeMemberController.js b/server/controllers/treeMemberController.js index 1ecaa08..425af44 100644 --- a/server/controllers/treeMemberController.js +++ b/server/controllers/treeMemberController.js @@ -3,7 +3,7 @@ const relationship = require('../models/relationshipModel'); const addTreeMember = async (req, res) => { try { - const { firstName, lastName, birthDate, deathDate, location, phoneNumber, relationships, userId, memberUserId } = req.body; + const { firstName, lastName, birthDate, deathDate, location, phoneNumber, relationships, userId, memberUserId, gender } = req.body; // ensure all necessary fields are passed in the request body const [newMember] = await treeMember.addMember({ @@ -14,7 +14,8 @@ const addTreeMember = async (req, res) => { location, phoneNumber, userId, - memberUserId + memberUserId, + gender }); /// need to fix that a value can be left empty (deathDate) diff --git a/server/migrations/20251010183743_add_gender_to_users.js b/server/migrations/20251010183743_add_gender_to_users.js new file mode 100644 index 0000000..ac3ea86 --- /dev/null +++ b/server/migrations/20251010183743_add_gender_to_users.js @@ -0,0 +1,29 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return Promise.all([ + knex.schema.table('users', function(table) { + table.string('gender').notNullable().defaultTo('unknown'); + }), + knex.schema.table('treeMembers', function(table) { + table.string('gender').notNullable().defaultTo('unknown'); + }) + ]); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return Promise.all([ + knex.schema.table('users', function(table) { + table.dropColumn('gender'); + }), + knex.schema.table('treeMembers', function(table) { + table.dropColumn('gender'); + }) + ]); +}; diff --git a/server/migrations/20251010212756_update_rel_table.js b/server/migrations/20251010212756_update_rel_table.js new file mode 100644 index 0000000..cd28459 --- /dev/null +++ b/server/migrations/20251010212756_update_rel_table.js @@ -0,0 +1,44 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema.table('relationships', function(table) { + // First drop the existing column if it exists + table.dropColumn('relationshipType'); + }) + .then(() => { + return knex.schema.table('relationships', function(table) { + // Add the new enum column + table.enu('relationshipType', [ + 'parent', + 'child', + 'sibling', + 'aunt', + 'uncle', + 'niece', + 'nephew', + 'spouse', + 'grandparent', + 'grandchild' + ]).notNullable(); + }); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema.table('relationships', function(table) { + // Drop the enum column + table.dropColumn('relationshipType'); + }) + .then(() => { + return knex.schema.table('relationships', function(table) { + // Add back a simple string column + table.string('relationshipType'); + }); + }); +}; From 1ee09c06db3f919ec1f635c27e037778c4de47d5 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Sat, 11 Oct 2025 22:39:37 -0500 Subject: [PATCH 08/86] !! updated family-chart package --- client/package-lock.json | 348 +-------------------------------------- client/package.json | 2 +- 2 files changed, 6 insertions(+), 344 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 782fd83..ec3b1a8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,7 +14,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.7.7", "d3": "^7.9.0", - "family-chart": "^0.2.1", + "family-chart": "^0.8.1", "knex": "^3.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -9434,352 +9434,14 @@ "license": "MIT" }, "node_modules/family-chart": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/family-chart/-/family-chart-0.2.1.tgz", - "integrity": "sha512-lB8dOg8Mll/PVfSR7wTG+mP/nk6fxbY+3paEGsr85xRKGJMIsC3Va9/D2J5QOVSk67nikur/MiTFnGeLS7kT2Q==", - "license": "ISC", - "dependencies": { - "d3": "6" - } - }, - "node_modules/family-chart/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/family-chart/node_modules/d3": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-6.7.0.tgz", - "integrity": "sha512-hNHRhe+yCDLUG6Q2LwvR/WdNFPOJQ5VWqsJcwIYVeI401+d2/rrCjxSXkiAdIlpx7/73eApFB4Olsmh3YN7a6g==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "2", - "d3-axis": "2", - "d3-brush": "2", - "d3-chord": "2", - "d3-color": "2", - "d3-contour": "2", - "d3-delaunay": "5", - "d3-dispatch": "2", - "d3-drag": "2", - "d3-dsv": "2", - "d3-ease": "2", - "d3-fetch": "2", - "d3-force": "2", - "d3-format": "2", - "d3-geo": "2", - "d3-hierarchy": "2", - "d3-interpolate": "2", - "d3-path": "2", - "d3-polygon": "2", - "d3-quadtree": "2", - "d3-random": "2", - "d3-scale": "3", - "d3-scale-chromatic": "2", - "d3-selection": "2", - "d3-shape": "2", - "d3-time": "2", - "d3-time-format": "3", - "d3-timer": "2", - "d3-transition": "2", - "d3-zoom": "2" - } - }, - "node_modules/family-chart/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "license": "BSD-3-Clause", - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/family-chart/node_modules/d3-axis": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-2.1.0.tgz", - "integrity": "sha512-z/G2TQMyuf0X3qP+Mh+2PimoJD41VOCjViJzT0BHeL/+JQAofkiWZbWxlwFGb1N8EN+Cl/CW+MUKbVzr1689Cw==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-brush": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-2.1.0.tgz", - "integrity": "sha512-cHLLAFatBATyIKqZOkk/mDHUbzne2B3ZwxkzMHvFTCZCmLaXDpZRihQSn8UNXTkGD/3lb/W2sQz0etAftmHMJQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dispatch": "1 - 2", - "d3-drag": "2", - "d3-interpolate": "1 - 2", - "d3-selection": "2", - "d3-transition": "2" - } - }, - "node_modules/family-chart/node_modules/d3-chord": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-2.0.0.tgz", - "integrity": "sha512-D5PZb7EDsRNdGU4SsjQyKhja8Zgu+SHZfUSO5Ls8Wsn+jsAKUUGkcshLxMg9HDFxG3KqavGWaWkJ8EpU8ojuig==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", - "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-contour": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-2.0.0.tgz", - "integrity": "sha512-9unAtvIaNk06UwqBmvsdHX7CZ+NPDZnn8TtNH1myW93pWJkhsV25JcgnYAu0Ck5Veb1DHiCv++Ic5uvJ+h50JA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "2" - } - }, - "node_modules/family-chart/node_modules/d3-delaunay": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz", - "integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/family-chart/-/family-chart-0.8.1.tgz", + "integrity": "sha512-u8FwMJle5Q4daNNCp2vp33NC2bN25IxPLHmBPBx/+fJ99HcIAa3Nu2lm7psPX0634a6pN27xRKruQGsv5zFGQg==", "license": "ISC", "dependencies": { - "delaunator": "4" + "d3": "^7.9.0" } }, - "node_modules/family-chart/node_modules/d3-dispatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", - "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-drag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz", - "integrity": "sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dispatch": "1 - 2", - "d3-selection": "2" - } - }, - "node_modules/family-chart/node_modules/d3-dsv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-2.0.0.tgz", - "integrity": "sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==", - "license": "BSD-3-Clause", - "dependencies": { - "commander": "2", - "iconv-lite": "0.4", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json", - "csv2tsv": "bin/dsv2dsv", - "dsv2dsv": "bin/dsv2dsv", - "dsv2json": "bin/dsv2json", - "json2csv": "bin/json2dsv", - "json2dsv": "bin/json2dsv", - "json2tsv": "bin/json2dsv", - "tsv2csv": "bin/dsv2dsv", - "tsv2json": "bin/dsv2json" - } - }, - "node_modules/family-chart/node_modules/d3-ease": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz", - "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-fetch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-2.0.0.tgz", - "integrity": "sha512-TkYv/hjXgCryBeNKiclrwqZH7Nb+GaOwo3Neg24ZVWA3MKB+Rd+BY84Nh6tmNEMcjUik1CSUWjXYndmeO6F7sw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dsv": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-force": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz", - "integrity": "sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dispatch": "1 - 2", - "d3-quadtree": "1 - 2", - "d3-timer": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-format": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz", - "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-geo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz", - "integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^2.5.0" - } - }, - "node_modules/family-chart/node_modules/d3-hierarchy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz", - "integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-interpolate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", - "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-color": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz", - "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-polygon": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz", - "integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-quadtree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz", - "integrity": "sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-random": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-2.2.2.tgz", - "integrity": "sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-scale": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz", - "integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^2.3.0", - "d3-format": "1 - 2", - "d3-interpolate": "1.2.0 - 2", - "d3-time": "^2.1.1", - "d3-time-format": "2 - 3" - } - }, - "node_modules/family-chart/node_modules/d3-scale-chromatic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz", - "integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-color": "1 - 2", - "d3-interpolate": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-selection": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz", - "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-shape": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz", - "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-time": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", - "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "2" - } - }, - "node_modules/family-chart/node_modules/d3-time-format": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", - "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-time": "1 - 2" - } - }, - "node_modules/family-chart/node_modules/d3-timer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz", - "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==", - "license": "BSD-3-Clause" - }, - "node_modules/family-chart/node_modules/d3-transition": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz", - "integrity": "sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-color": "1 - 2", - "d3-dispatch": "1 - 2", - "d3-ease": "1 - 2", - "d3-interpolate": "1 - 2", - "d3-timer": "1 - 2" - }, - "peerDependencies": { - "d3-selection": "2" - } - }, - "node_modules/family-chart/node_modules/d3-zoom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz", - "integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dispatch": "1 - 2", - "d3-drag": "2", - "d3-interpolate": "1 - 2", - "d3-selection": "2", - "d3-transition": "2" - } - }, - "node_modules/family-chart/node_modules/delaunator": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", - "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==", - "license": "ISC" - }, - "node_modules/family-chart/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/family-chart/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/client/package.json b/client/package.json index 70c255b..0263d7f 100644 --- a/client/package.json +++ b/client/package.json @@ -9,7 +9,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.7.7", "d3": "^7.9.0", - "family-chart": "^0.2.1", + "family-chart": "^0.8.1", "knex": "^3.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", From 26889021c6085a1430de50366ed5ca63ed2033c1 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Sat, 11 Oct 2025 22:46:06 -0500 Subject: [PATCH 09/86] refactored relationship assignments in to tree object + err handling --- client/src/components/AddToTree/AddToTree.js | 174 +++++-------------- client/src/utils/treeUtils.js | 112 ++++++++++++ 2 files changed, 159 insertions(+), 127 deletions(-) create mode 100644 client/src/utils/treeUtils.js diff --git a/client/src/components/AddToTree/AddToTree.js b/client/src/components/AddToTree/AddToTree.js index 5816c2a..8e53b76 100644 --- a/client/src/components/AddToTree/AddToTree.js +++ b/client/src/components/AddToTree/AddToTree.js @@ -7,6 +7,7 @@ import './popup.css'; import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; import { Link } from 'react-router-dom'; import { useCurrentUser } from '../../CurrentUserProvider'; // import the context +import { addRelationship }from '../../utils/treeUtils.js'; // john jane parent jane is john's mom function AddTreeMember (userId, accountUserId, relativeUserId, relativeRelationship, accountUserName, treeData, results, currentAccountID) { @@ -14,11 +15,12 @@ function AddTreeMember (userId, accountUserId, relativeUserId, relativeRelations // maybe - check if account user is already in tree, return error if they are (user will need to delete them and re-add) + // Link relative to user; console.log("relative " + relativeUserId); console.log("account " + accountUserId); console.log(results.current.find(result => Number(result.id) === Number(accountUserId))) - // intialize user in tree + // Initialize user in tree treeIndex[`${accountUserId}`] = { "id": `${accountUserId}`, "rels": { @@ -27,109 +29,15 @@ function AddTreeMember (userId, accountUserId, relativeUserId, relativeRelations }, "data": { "first name": `${accountUserName.split(" ")[0]}`, - "last name": `${accountUserName.split(" ")[1]}`, "gender": `${results.current.find(result => Number(result.id) === Number(accountUserId))["gender"]}` } } - - if(relativeRelationship === "parent"){ - // add account user as child to relative - treeIndex[`${relativeUserId}`]["rels"]["children"].push(`${accountUserId}`); - // add relative as parent to account user - if(treeIndex[`${relativeUserId}`]["data"]["gender"] === "M") { - treeIndex[`${accountUserId}`]["rels"]["father"] = `${relativeUserId}`; - } - else if(treeIndex[`${relativeUserId}`]["data"]["gender"] === "F") { - treeIndex[`${accountUserId}`]["rels"]["mother"] = `${relativeUserId}`; - } - else{ - console.log("no gender found"); - } - // add relative's spouse(s) (if any) as parent to account user - treeIndex[`${relativeUserId}`]["rels"]["spouses"].forEach(spouse => { - if(treeIndex[`${spouse}`]["data"]["gender"] === "M") { - treeIndex[`${accountUserId}`]["rels"]["father"] = `${spouse}`; - } - else if(treeIndex[`${spouse}`]["data"]["gender"] === "F") { - treeIndex[`${accountUserId}`]["rels"]["mother"] = `${spouse}`; - } - else { - console.log("no gender found"); - } - }); - // add account user as child to relative's spouse(s) (if any) - treeIndex[`${relativeUserId}`]["rels"]["spouses"].forEach(spouse => { - treeIndex[`${spouse}`]["rels"]["children"].push(`${accountUserId}`); - }); - } - - else if(relativeRelationship === "child") { - // add account user as parent to relative - if(treeIndex[`${accountUserId}`]["data"]["gender"] === "M"){ - treeIndex[`${relativeUserId}`]["rels"]["father"] = `${accountUserId}`; - } - else if(treeIndex[`${accountUserId}`]["data"]["gender"] === "F"){ - treeIndex[`${relativeUserId}`]["rels"]["mother"] = `${accountUserId}`; - } - else{ - console.log("no gender found"); - } - // add relative as child to account user - treeIndex[`${accountUserId}`]["rels"]["children"].push(`${relativeUserId}`); - // add account user as spouse to relative's existing parent(s) (if any) + make account user parent of relative's siblings (if any) - if(treeIndex[`${relativeUserId}`]["rels"]["father"] !== undefined && treeIndex[`${accountUserId}`]["data"]["gender"] === "F") { // make account user the wife - treeIndex[`${relativeUserId}`["rels"]["father"]]["rels"]["spouses"].push(`${accountUserId}`); - treeIndex[`${relativeUserId}`["rels"]["father"]]["rels"]["children"].forEach(child => { // make account user mother of relative's siblings - if(child !== `${relativeUserId}`) { - treeIndex[`${child}`]["rels"]["mother"] = `${accountUserId}`; - } - }); - } - else if(treeIndex[`${relativeUserId}`]["rels"]["mother"] !== undefined && treeIndex[`${accountUserId}`]["data"]["gender"] === "M"){ // make account user the husband - treeIndex[`${relativeUserId}`["rels"]["mother"]]["rels"]["spouses"].push(`${accountUserId}`); - treeIndex[`${relativeUserId}`["rels"]["father"]]["rels"]["children"].forEach(child => { // make account user father of relative's siblings - if(child !== `${relativeUserId}`) { - treeIndex[`${child}`]["rels"]["father"] = `${accountUserId}`; - } - }); - } - else{ - console.log("no gender found or no existing parent"); - } - } - - else if(relativeRelationship === "sibling") { - // add account user as child to relative's parent(s) - if(treeIndex[`${relativeUserId}`]["rels"]["father"] !== undefined) { - treeIndex[`${relativeUserId}`["rels"]["father"]]["rels"]["children"].push(`${accountUserId}`); - treeIndex[`${accountUserId}`]["rels"]["father"] = `${treeIndex[`${relativeUserId}`]["rels"]["father"]}`; - } - if(treeIndex[`${relativeUserId}`]["rels"]["mother"] !== undefined) { - treeIndex[`${relativeUserId}`["rels"]["mother"]]["rels"]["children"].push(`${accountUserId}`); - treeIndex[`${accountUserId}`]["rels"]["mother"] = `${treeIndex[`${relativeUserId}`]["rels"]["mother"]}`; - } - } - - else if(relativeRelationship === "spouse") { - // add account user as spouse to relative - treeIndex[`${relativeUserId}`]["rels"]["spouses"].push(`${accountUserId}`); - // add relative as spouse to account user - treeIndex[`${accountUserId}`]["rels"]["spouses"].push(`${relativeUserId}`); - // add account user as parent to relative's children (if any) - treeIndex[`${relativeUserId}`]["rels"]["children"].forEach(child => { - if(treeIndex[`${accountUserId}`]["data"]["gender"] === "M") { - treeIndex[`${child}`]["rels"]["father"] = `${accountUserId}`; - } - else if(treeIndex[`${accountUserId}`]["data"]["gender"] === "F"){ - treeIndex[`${child}`]["rels"]["mother"] = `${accountUserId}`; - } - treeIndex[`${accountUserId}`]["rels"]["children"].push(`${child}`); - }); - } - - else { - console.log("Error: invalid relationship type"); + // Add the primary relationship + try { + addRelationship(treeIndex, accountUserId, relativeUserId, relativeRelationship); + } catch (error) { + throw new Error(error.message); // Re-throw the error to be caught by onSubmit } let updatedTreeData = Object.values(treeIndex); @@ -141,13 +49,16 @@ function AddTreeMember (userId, accountUserId, relativeUserId, relativeRelations headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedTreeData) }; - fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`, requestOptions) + + return fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`, requestOptions) .then(async(response) => { if (response.ok) { console.log("Tree object updated successfully"); + return true; } else{ console.error('Error:', response); + return false; } }); } @@ -155,6 +66,7 @@ function AddTreeMember (userId, accountUserId, relativeUserId, relativeRelations function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAccountRelationshipType, userId }) { const [searchTerm, setSearchTerm] = useState(""); const [searchResults, setSearchResults] = useState([]); + const [errorMessage, setErrorMessage] = useState(""); var results = useRef([]); var filteredResults = useRef([]); const { currentAccountID } = useCurrentUser(); @@ -167,7 +79,7 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc .then(async(response) => { if (response.ok) { results.current = await response.json(); - console.log(results.current); + console.log('AddToTree Current Family Members:', results.current); } else { console.log('Error:', response); @@ -179,11 +91,12 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc .then(async(response) => { if (response.ok) { let responseData = await response.json(); - console.log(responseData.object); + console.log('AddToTree Current Tree Object Data:', responseData.object); setTreeData(responseData.object); } else { console.error('Error:', response); + throw new Error('Error fetching tree data'); } }); }) @@ -203,7 +116,7 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc ); console.log("Filtered Results:", filteredResults.current); - }, [treeData, results.current]); + }, [treeData]); // form const { @@ -216,27 +129,22 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc let selectedMember = watch("selectedMember"); const onSubmit = async (data, close) => { - console.log(data); - reset(); - close(); - // wait for the API request to complete - const response = await fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' } - }); - - if (response.ok) { - // parse the response - const treeResponse = await response.json(); - setTreeData(treeResponse.object); - console.log(treeData); - - // call after the API request is complete - return AddTreeMember(userId, accountUserId, data.selectedMember, data.memberRelationshipType, accountUserName, treeData, results, currentAccountID); - } - else { - const errorData = await response.json(); - console.error('Error:', errorData.message); + console.log("Submission data:", data); + setErrorMessage(""); // Clear any previous error messages + + try { + const result = await AddTreeMember(userId, accountUserId, data.selectedMember, data.memberRelationshipType, accountUserName, treeData, results, currentAccountID); + if (!result) { + setErrorMessage("Failed to update tree data."); + return; + } + reset(); + close(); + return window.location.href = `/tree`; + } catch (error) { + console.error("Error adding member to tree:", error.message); + setErrorMessage(error.message || "Failed to add member to tree."); + return; } }; @@ -270,7 +178,7 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc
    {/* close button */}
    -
    @@ -325,7 +233,7 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc
  • + {errorMessage && ( +
    + {errorMessage} +
    + )}
    diff --git a/client/src/utils/treeUtils.js b/client/src/utils/treeUtils.js new file mode 100644 index 0000000..f9ec704 --- /dev/null +++ b/client/src/utils/treeUtils.js @@ -0,0 +1,112 @@ +export function addRelationship(treeIndex, accountUserId, relativeId, relationship) { + if (!treeIndex[accountUserId] || !treeIndex[relativeId]) { + throw new Error("Account user or relative not found in tree"); + } + + const accountUser = treeIndex[accountUserId]; // the user that the relationship is being added to + const relative = treeIndex[relativeId]; // the user that is being added as a relationship + + switch (relationship) { + case "parent": // Relative is the parent of the account user + // Check if user already has a parent of this gender + if (relative.data.gender === "M" && accountUser.rels.father) { + throw new Error(`${accountUser.data["first name"]} already has a father in the tree`); + } + if (relative.data.gender === "F" && accountUser.rels.mother) { + throw new Error(`${accountUser.data["first name"]} already has a mother in the tree`); + } + + // Add user as a child to relative + if (!relative.rels.children.includes(accountUserId)) { + relative.rels.children.push(accountUserId); + } + + // Set relative as father/mother of account user based on gender + if (relative.data.gender === "M") { + accountUser.rels.father = relativeId; + if (!accountUser.rels.mother) { + accountUser.rels.mother = relative.rels.spouses[0] || null; // set mother as first spouse if exists + } + } else if (relative.data.gender === "F") { + accountUser.rels.mother = relativeId; + if (!accountUser.rels.father) { + accountUser.rels.father = relative.rels.spouses[0] || null; // set father as first spouse if exists + } + } + break; + + case "child": // Relative is a child of the account user + // Check if child already has a parent of this gender + if (accountUser.data.gender === "M" && relative.rels.father) { + throw new Error(`${relative.data["first name"]} already has a father in the tree`); + } + if (accountUser.data.gender === "F" && relative.rels.mother) { + throw new Error(`${relative.data["first name"]} already has a mother in the tree`); + } + + // Add relative as child to user + if (!accountUser.rels.children.includes(relativeId)) { + accountUser.rels.children.push(relativeId); + } + + // Set user as father/mother of relative based on gender + if (accountUser.data.gender === "M") { + relative.rels.father = accountUserId; + } else if (accountUser.data.gender === "F") { + relative.rels.mother = accountUserId; + } + break; + + case "sibling": // Relative is a sibling of the account user + // Check if relative has at least one parent + if (!relative.rels.father && !relative.rels.mother) { + throw new Error(`Cannot add as sibling: Selected member has no parents`); + } + + if (relative.rels.father) { // Add the relative's father as the account user's father + const fatherID = relative.rels.father; + accountUser.rels.father = fatherID; + const father = treeIndex[fatherID]; + if (!father.rels.children.includes(accountUserId)) { + father.rels.children.push(accountUserId); + } + } + if (relative.rels.mother) { // Add the relative's mother as the account user's mother + const motherID = relative.rels.mother; + accountUser.rels.mother = motherID; + const mother = treeIndex[motherID]; + if (!mother.rels.children.includes(accountUserId)) { + mother.rels.children.push(accountUserId); + } + } + break; + + case "spouse": // Relative is a spouse of the account user + // Add each other as spouses + if (!accountUser.rels.spouses.includes(relativeId)) { + accountUser.rels.spouses.push(relativeId); + } + if (!relative.rels.spouses.includes(accountUserId)) { + relative.rels.spouses.push(accountUserId); + } + + // Add relative's children to account user + relative.rels.children.forEach(childId => { + const child = treeIndex[childId]; + if (accountUser.data.gender === "M") { + child.rels.father = accountUserId; + } else if (accountUser.data.gender === "F") { + child.rels.mother = accountUserId; + } + if (!accountUser.rels.children.includes(childId)) { + accountUser.rels.children.push(childId); + } + }); + break; + + default: + throw new Error("Invalid relationship type"); + } + + return true; +} From 11cb23d95fafca214a2ce5b06338d0afda227d1c Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Sat, 11 Oct 2025 22:48:00 -0500 Subject: [PATCH 10/86] update to f3 family chart + clearer console messages --- client/src/pages/Tree/Tree.js | 71 +++++++++++------------- server/controllers/treeInfoController.js | 4 +- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/client/src/pages/Tree/Tree.js b/client/src/pages/Tree/Tree.js index 5758956..d57891c 100644 --- a/client/src/pages/Tree/Tree.js +++ b/client/src/pages/Tree/Tree.js @@ -1,11 +1,9 @@ import React, { useRef, useEffect } from 'react'; import * as styles from './styles'; // import { ReactComponent as TreeIcon } from '../../assets/background-tree.svg'; // background tree image from Figma; TODO configure overlay with tree svg -import f3 from 'family-chart'; +import * as f3 from 'family-chart'; import './tree.css'; // styling adapted from family-chart package sample code import { ReactComponent as PlusSign } from '../../assets/plus-sign.svg'; -// import { ReactComponent as ArrowTR } from '../../assets/arrow-1.svg'; -// import { ReactComponent as ArrowBL } from '../../assets/arrow-2.svg'; import AddFamilyMemberPopup from '../../components/AddFamilyMember/AddFamilyMember'; import { Link } from 'react-router-dom'; import NavBar from '../../components/NavBar/NavBar'; @@ -16,35 +14,40 @@ import { useCurrentUser } from '../../CurrentUserProvider'; // import the contex // see https://github.com/donatso/family-chart/ function FamilyTree() { - const contRef = useRef(null); // Use a ref for the container - const { currentAccountID } = useCurrentUser(); // Use the hook in the function component + const contRef = React.createRef(); + const { currentAccountID } = useCurrentUser(); useEffect(() => { - if (!contRef.current) { - console.log("failure"); - return; - } - console.log("success"); + if (!contRef.current) return; + + let chart = null; function create(data) { - const f3Chart = f3.createChart('#FamilyChart', data) - .setTransitionTime(0) + // Clean up any existing chart first + const existingChart = document.querySelector('#FamilyChart'); + if (existingChart) { + existingChart.innerHTML = ''; + } + + chart = f3.createChart('#FamilyChart', data) + .setTransitionTime(500) .setCardXSpacing(250) .setCardYSpacing(150) + .setSingleParentEmptyCard(false, {label: ''}) + .setShowSiblingsOfMain(true) .setOrientationVertical() - .setSingleParentEmptyCard(false); - - const f3Card = f3Chart.setCard(f3.CardHtml) - .setCardDisplay([["first name"], []]) - .setCardDim({ width: 80, height: 80 }) + + + chart.setCardHtml() + .setCardDisplay([["first name"],[]]) + .setCardDim({}) .setMiniTree(false) .setStyle('imageCircle') - .setOnHoverPathToMain(); - - f3Card.setOnCardClick((e, d) => {}); // Remove zooming transitions + .setOnCardClick((e, data) => { + window.location.href = `/account/${data.data.id}`; + }); - f3Chart.updateMainId("21"); - f3Chart.updateTree({ initial: true }); + chart.updateTree({initial: true}); } let getRequestOptions = { @@ -56,21 +59,20 @@ function FamilyTree() { .then(async (response) => { if (response.ok) { let treeData = await response.json(); - console.log(treeData.object); const parsedData = treeData.object; - console.log(parsedData); + console.log("Tree data: ", parsedData); create(parsedData); } else { - console.error('Error:', response); + console.error('Error in Loading Tree Data:', response); } }); - }, [currentAccountID]); + + + }, [contRef, currentAccountID]); return
    ; } -let parsedData = []; - // builds the actual page function Tree() { const { currentAccountID, currentUserName, fetchCurrentUserID } = useCurrentUser(); // Use the hook in the function component @@ -82,19 +84,8 @@ function Tree() { return (
    - {/* TODO make these work again, removed them for now so I could work with the header placement; might want to integrate these with actual background somehow */} - {/*
    -
    - - -
    -
    */} {isTreePage ? ( - -
    - {/* Home */} - - +
    {/* header content */}
    {/* titles */} diff --git a/server/controllers/treeInfoController.js b/server/controllers/treeInfoController.js index a55195d..f153433 100644 --- a/server/controllers/treeInfoController.js +++ b/server/controllers/treeInfoController.js @@ -10,7 +10,7 @@ const addObject = async (req, res) => { }); res.status(201).json({ - message: 'Tree object added successfully', + message: 'Tree object added successfully to DB', object: newObject }); } catch (error) { @@ -70,7 +70,7 @@ const getObject = async (req, res) => { error: 'Object not found' }); } - console.log("Data sent from backend:", retrievedObject.object); + console.log("treeInfo getObject triggered. Data sent from backend:", retrievedObject.object); res.status(200).json(retrievedObject); } catch (error) { From f98e986ed78fb885883e2c9c94f01a301de452d3 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Sat, 11 Oct 2025 22:49:41 -0500 Subject: [PATCH 11/86] form data assignments + minor jsx / styling changes --- .../AddFamilyMember/AddFamilyMember.js | 16 ++++++++-------- client/src/components/AddFamilyMember/popup.css | 1 + client/src/components/AddFamilyMember/styles.js | 2 +- client/src/pages/CreateAccount/CreateAccount.js | 4 +++- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/client/src/components/AddFamilyMember/AddFamilyMember.js b/client/src/components/AddFamilyMember/AddFamilyMember.js index 6a96571..f40a26d 100644 --- a/client/src/components/AddFamilyMember/AddFamilyMember.js +++ b/client/src/components/AddFamilyMember/AddFamilyMember.js @@ -133,7 +133,7 @@ function AddFamilyMemberPopup({ trigger, userid }) { "phoneNumber": null, "userId": userid, "memberUserId": users.current.find(user => user.id === Number(memberId)).id, - "gender": data.gender, // Ensure gender is explicitly handled and not undefined + "gender": data.gender, }) }; @@ -199,7 +199,7 @@ function AddFamilyMemberPopup({ trigger, userid }) { }; // form submission (manual entry) - const onSubmitManual = (data) => { + const onSubmitManual = (data, close) => { console.log("submit attempted"); console.log("Form data:", data); // Log the form data to see what we're getting // add new member to family members table @@ -212,10 +212,10 @@ function AddFamilyMemberPopup({ trigger, userid }) { "birthDate": data.birthday || null, "deathDate" : data.deathDate || null, "location": data.location || null, - "phoneNumber": "", + "phoneNumber": data.phoneNumber || null, "userId": userid, "memberUserId": null, - "gender": data.gender // Send the gender value directly, don't use || null + "gender": data.gender }) }; @@ -258,6 +258,8 @@ function AddFamilyMemberPopup({ trigger, userid }) { console.error('Error:', response); } }); + reset(); + close(); }; return ( @@ -409,9 +411,8 @@ function AddFamilyMemberPopup({ trigger, userid }) { -
  • -
  • + + {errors.gender &&

    {errors.gender.message}

    }
  • From cb883e674c58fe1b2b0d7fb5cac2e196138b57c7 Mon Sep 17 00:00:00 2001 From: MatthewLoyed Date: Sun, 19 Oct 2025 19:38:54 -0500 Subject: [PATCH 12/86] Successfully integrated Supabase Registration, Login, and Signout. Still need to store additional signup info in mySql. --- client/package-lock.json | 130 ++++++++++++++- client/package.json | 1 + client/src/pages/Account/Account.js | 12 +- .../src/pages/CreateAccount/CreateAccount.js | 128 +++++---------- client/src/pages/Home/Home.js | 2 +- client/src/pages/Login/Login.js | 46 ++---- .../pages/WebsiteSettings/WebsiteSettings.js | 7 +- client/src/utils/auth.js | 23 +++ client/src/utils/authHandlers.js | 39 +++++ client/src/utils/supabaseClient.js | 6 + server/controllers/authController.js | 78 +-------- server/db/supabase-init.sql | 65 ++++++++ .../20250416174536_add_user_tree_table.js | 2 +- server/models/userModel.js | 2 + server/package-lock.json | 148 ++++++++++++++++++ server/package.json | 1 + server/routes/authRoutes.js | 6 +- 17 files changed, 477 insertions(+), 219 deletions(-) create mode 100644 client/src/utils/auth.js create mode 100644 client/src/utils/authHandlers.js create mode 100644 client/src/utils/supabaseClient.js create mode 100644 server/db/supabase-init.sql diff --git a/client/package-lock.json b/client/package-lock.json index 782fd83..af787b0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^4.1.3", + "@supabase/supabase-js": "^2.75.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -3700,6 +3701,123 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz", + "integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz", + "integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@supabase/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@supabase/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz", + "integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz", + "integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/realtime-js/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz", + "integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz", + "integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.75.0", + "@supabase/functions-js": "2.75.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.75.0", + "@supabase/realtime-js": "2.75.0", + "@supabase/storage-js": "2.75.0" + } + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -4765,6 +4883,12 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/prettier": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", @@ -4896,9 +5020,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", "dependencies": { "@types/node": "*" diff --git a/client/package.json b/client/package.json index 70c255b..396ed46 100644 --- a/client/package.json +++ b/client/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@hookform/resolvers": "^4.1.3", + "@supabase/supabase-js": "^2.75.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/client/src/pages/Account/Account.js b/client/src/pages/Account/Account.js index 00081f2..e38f998 100644 --- a/client/src/pages/Account/Account.js +++ b/client/src/pages/Account/Account.js @@ -1,11 +1,12 @@ import { React, useEffect, useState } from 'react'; import * as styles from './styles'; -import { Link, useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import NavBar from '../../components/NavBar/NavBar'; import AddToTreePopup from '../../components/AddToTree/AddToTree'; import { CurrentUserProvider, useCurrentUser } from '../../CurrentUserProvider'; function Account() { + const navigate = useNavigate(); // used to change route without refreshing page, used to prevent infinite refreshes const [ownAccount, setOwnAccount] = useState(false); // will be retrieved const [existsInTree, setExistsInTree] = useState(false); // will be retrieved const [relationshipType, setRelationshipType] = useState(''); // will be retrieved @@ -24,12 +25,11 @@ function Account() { // if no id is provided, retrieve current user's id and show that page useEffect(() => { - if (!id) { - id = currentUserID; - setOwnAccount(true); - window.location.href = `/account/${currentUserID}`; + if (!id && currentUserID) { + setOwnAccount(true); + navigate(`/account/${currentUserID}`, { replace: true }); } - }, [id, currentUserID]); + }, [id, currentUserID, navigate]); // TODO: query for data of account user & verify that userID of logged in user matches diff --git a/client/src/pages/CreateAccount/CreateAccount.js b/client/src/pages/CreateAccount/CreateAccount.js index 9fdc592..2864c38 100644 --- a/client/src/pages/CreateAccount/CreateAccount.js +++ b/client/src/pages/CreateAccount/CreateAccount.js @@ -1,7 +1,7 @@ -import { set, useForm } from 'react-hook-form' +import { useForm } from 'react-hook-form' import { React, useState } from 'react' -import { Link } from 'react-router-dom' import { yupResolver } from "@hookform/resolvers/yup" +import { handleRegister } from '../../utils/authHandlers'; import * as yup from "yup" import * as styles from './styles' import logo from '../../assets/kintreelogo-adobe.png'; @@ -27,105 +27,49 @@ const yupValidation = yup.object().shape( , "Invalid phone number format." ), zipcode: yup.string().matches(/^\d{5}(?:[-\s]\d{4})?$/, "Invalid zip code format."), - password: yup.string().required("Password is a required field.") + password: yup.string().required("Password is required") .matches( - /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/ - , "Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character" + /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{8,})/, + "Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character" ) - } ); const CreateAccount = () => { const {register, handleSubmit, formState: {errors}} = useForm({resolver: yupResolver(yupValidation)}); + const [errorMessage, setErrorMessage] = useState(""); const [isHovering, setIsHovering] = useState(false); - const [formData, setFormData] = useState({}); - const onSubmit = (data) => { - console.log(data); - - // register account - fetch(`http://localhost:5000/api/auth/register`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - username: data.firstname + " " + data.lastname, - email: data.email, - password: data.password, - }), - }) - .then(async (response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData); - - // Use responseData.user directly - const accountID = responseData.user; - - // Initialize user's tree by adding themself - return fetch(`http://localhost:5000/api/family-members/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - firstName: data.firstname, - lastName: data.lastname, - birthdate: data.birthdate, - email: data.email, - location: `${data.address}, ${data.city}, ${data.state} ${data.zipcode}, ${data.country}`, - phoneNumber: data.phonenum, - userId: accountID, - memberUserId: accountID, - }), - }).then(async (response) => { - if (response.ok) { - const familyMemberResponse = await response.json(); - console.log(familyMemberResponse); - return fetch(`http://localhost:5000/api/tree-info/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - object: [{ - "id": familyMemberResponse.member, - "data": { - "first name": data.firstname, - "last name": data.lastname, - }, - "rels": { - "children": [], - "spouses": [], - } - }], - userId: accountID, - }), - }); - }}) - } - else { - const errorData = await response.json(); - console.error('Error registering account:', errorData); - throw new Error('Account registration failed'); - } - }) - .then(async (response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData); - window.location.href = '/'; - } else { - const errorData = await response.json(); - console.error('Error initializing family member:', errorData); - } - }) - .catch((error) => { - console.error('Error:', error); - }); - }; + const onSubmit = async (data) => { + setErrorMessage(""); // clear previous errors + try { + const user = await handleRegister(data.email, data.password); // frontend Supabase registration + + // TODO: store additional info in mysqldatabase later + // await fetch('http://localhost:5000/api/users', { + // method: 'POST', + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify({ + // userId: user.id, + // firstname: data.firstname, + // lastname: data.lastname, + // birthdate: data.birthdate, + // address: data.address, + // city: data.city, + // state: data.state, + // zipcode: data.zipcode, + // country: data.country, + // phonenum: data.phonenum + // }) + // }); + + console.log('Registration successful:', user); + window.location.href = '/home'; // redirect after registration to login, can change to login if we want + } catch (error) { + setErrorMessage(error.message); + console.error('Password:', data.password); + } + }; const ButtonStyle = { fontFamily: 'Alata', diff --git a/client/src/pages/Home/Home.js b/client/src/pages/Home/Home.js index 0e90317..95898a9 100644 --- a/client/src/pages/Home/Home.js +++ b/client/src/pages/Home/Home.js @@ -8,7 +8,7 @@ import CreateEventPopup from '../../components/CreateEvent/CreateEvent'; import CreateMemoryPopup from '../../components/CreateMemory/CreateMemory'; import NavBar from '../../components/NavBar/NavBar'; -function Home() { +function Home() { document.body.style.overflow = 'hidden'; document.body.style.width = '100%'; return ( diff --git a/client/src/pages/Login/Login.js b/client/src/pages/Login/Login.js index c508f38..f9c8d3c 100644 --- a/client/src/pages/Login/Login.js +++ b/client/src/pages/Login/Login.js @@ -4,47 +4,21 @@ import logo from '../../assets/kintreelogo-adobe.png'; import { useForm } from 'react-hook-form'; import { Link } from 'react-router-dom'; import { useCurrentUser } from '../../CurrentUserProvider'; +import { handleLogin } from '../../utils/authHandlers'; function Login() { const { register, handleSubmit } = useForm(); const [ errorMessage, setErrorMessage ] = useState(""); const { setCurrentAccountID, fetchCurrentUserID, fetchCurrentAccountID } = useCurrentUser(); - const onSubmit = (data) => { - const requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }; - fetch('http://localhost:5000/api/auth/login', requestOptions) - .then(async(response) => { - if (response.ok) { - fetch(`http://localhost:5000/api/auth/user/email/${data.email}`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' } - }) - .then(async(response) => { - if (response.ok) { - let userData = await response.json(); - await setCurrentAccountID(userData.id); // set the current user ID in context - console.log("set currentAccountID to: ", userData.id); - await fetchCurrentUserID(); - window.location.href='/' - } - }) - return response.json(); - } - else { - const errorData = await response.json(); - console.error('Error:', errorData.message); - setErrorMessage(errorData.message); - throw new Error('Network response was not ok'); - } - }) - .catch(error => { - console.error('There was a problem with the fetch operation:', error); - }) - }; + const onSubmit = async (data) => { + setErrorMessage(""); // clear previous errors + try { + await handleLogin(data.email, data.password); // just call the handler + } catch (error) { + setErrorMessage(error.message); + } + }; document.body.style.overflow = 'hidden'; document.body.style.width = '100%'; @@ -54,7 +28,7 @@ function Login() {
    KinTree Logo

    Sign In

    -
    onSubmit(data))} style={styles.FormStyle}> +
    diff --git a/client/src/utils/auth.js b/client/src/utils/auth.js new file mode 100644 index 0000000..8ba9c14 --- /dev/null +++ b/client/src/utils/auth.js @@ -0,0 +1,23 @@ +import { supabase } from './supabaseClient'; + +// These functions are used to handle the authentication of the user, but only the pure login, logout, etc functionality. It should not include frontend logic like redirects. + +// Register new user +export async function registerUser(email, password) { + const { data, error } = await supabase.auth.signUp({ email, password }); + if (error) throw error; + return data; +} + +// Login existing user +export async function loginUser(email, password) { + const { data, error } = await supabase.auth.signInWithPassword({ email, password }); + if (error) throw error; + return data; +} + +// Logout current user +export async function logoutUser() { + const { error } = await supabase.auth.signOut(); + if (error) throw error; +} diff --git a/client/src/utils/authHandlers.js b/client/src/utils/authHandlers.js new file mode 100644 index 0000000..4947670 --- /dev/null +++ b/client/src/utils/authHandlers.js @@ -0,0 +1,39 @@ +// src/handlers/authHandlers.js +import { loginUser, registerUser, logoutUser } from '../utils/auth'; + +// This page is used to handle authentication and includes redirects and error handling. + +export async function handleLogin(email, password) { + try { + const data = await loginUser(email, password); // call the pure login function + console.log("Logged in user:", data.user); + window.location.href = '/home'; // redirect after login to home + } catch (error) { + console.error('Login error:', error.message); + alert(error.message); + throw error; // so form onSubmit can catch it + } +} + +export async function handleRegister(email, password) { + try { + const data = await registerUser(email, password); + console.log("Registered user:", data.user); + return data; // Return the data so the calling function can use it + } catch (error) { + console.error('Registration error:', error.message); + alert(error.message); + throw error; // so form onSubmit can catch it + } +} + +// Logout handler +export async function handleLogout() { + try { + await logoutUser(); + window.location.href = '/login'; // redirect after logout + } catch (error) { + console.error('Logout error:', error.message); + alert(error.message); + } +} diff --git a/client/src/utils/supabaseClient.js b/client/src/utils/supabaseClient.js new file mode 100644 index 0000000..aafdeac --- /dev/null +++ b/client/src/utils/supabaseClient.js @@ -0,0 +1,6 @@ +import { createClient } from '@supabase/supabase-js'; + +const supabaseUrl = process.env.REACT_APP_SUPABASE_URL; +const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY; + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); diff --git a/server/controllers/authController.js b/server/controllers/authController.js index e9037fa..ff90223 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -1,75 +1,5 @@ -// authController.js -const bcrypt = require('bcryptjs'); -const User = require('../models/userModel'); - -const register = async (req, res) => { - console.log('Regiater function called'); - try { - const { username, email, password } = req.body; - - if (!email || !password || !username) { - return res.status(400).json({ error: 'All fields are required' }); - } - - const existingUser = await User.findByEmail(email); - if (existingUser) return res.status(400).json({ - error: 'Email already in use' - }); - - const saltRounds = 12; - const salt = await bcrypt.genSalt(saltRounds); - const hashedPassword = await bcrypt.hash(password, salt); - - const [newUser] = await User.register({ - username, - email, - password: hashedPassword - }); - - res.status(201).json({ - message: 'User registered successfully', user: newUser - }); - } catch (error) { - console.error(error); - res.status(500).json({ - error: 'Registration failed' - }); - } -}; - -const login = async(req,res) => { - try{ - const { email, password } = req.body; - if(!email || !password){ - return res.status(400).json({ - message: 'Missing an email or password' - }); - } - const existingUser = await User.findByEmail(email); - if(!existingUser){ - return res.status(401).json({ - message: 'User is not found. Please register!' - }); - } - const passwordCompare = await bcrypt.compare(password, existingUser.password) - if(!passwordCompare){ - return res.status(401).json({ - message: "Invalid credentials" - }); - } - - res.status(200).json({ - message: "You are logged in!" - }); - } - catch (error){ - console.error(error); - res.status(500).json({ - error: 'Registration failed' - }); - - } -}; +// authController.js - the main backend file for user registration, signin, etc +const User = require('../models/userModel'); // delete once done repalcing with supabase const deleteByUser = async (req,res) => { const { id } = req.params; @@ -84,7 +14,7 @@ const deleteByUser = async (req,res) => { } catch (error){ console.error(error); - res.status(500);json({error:"Error deleting user"}) + res.status(500).json({error:"Error deleting user"}) } } @@ -126,4 +56,4 @@ const getAllUsers = async (req, res) => { } } -module.exports = { register,login, deleteByUser, findById, findByEmail, getAllUsers }; +module.exports = { deleteByUser, findById, findByEmail, getAllUsers }; diff --git a/server/db/supabase-init.sql b/server/db/supabase-init.sql new file mode 100644 index 0000000..58f104f --- /dev/null +++ b/server/db/supabase-init.sql @@ -0,0 +1,65 @@ +-- ======================== +-- Supabase Schema Init File +-- Created from MySQL Knex migrations +-- ======================== + +-- 1. Users Table +create table users ( + id serial primary key, + username text unique not null, + password text not null, + email text unique not null, + firstName text, + lastName text, + phoneNumber text, + birthDate date, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now() +); + +-- 2. Tree Members Table +create table treeMembers ( + id serial primary key, + firstName text not null, + lastName text not null, + birthDate date, + deathDate date, + location text, + phoneNumber text, + userId integer not null references users(id) on delete cascade, + memberUserId integer, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now() +); + +-- 3. Relationships Table +create table relationships ( + id serial primary key, + person1_id integer not null references treeMembers(id) on delete cascade, + person2_id integer not null references treeMembers(id) on delete cascade, + relationshipType text not null check (relationshipType in ('parent','child','sibling','spouse','stepparent','stepchild','ex-spouse')), + relationshipStatus text check (relationshipStatus in ('active','inactive')), + side text check (side in ('paternal','maternal')), + userId integer not null references users(id) on delete cascade, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now() +); + +-- 4. Shared Trees Table +create table sharedTrees ( + sharedTreeID serial primary key, + senderID integer not null references users(id), + recieverID integer, + perms text check (perms in ('view','edit')), + parentalSide text check (parentalSide in ('paternal','maternal','both')), + sahreDate timestamp, + treeInfo json +); + +-- 5. Backups Table +create table backups ( + backupId serial primary key, + userId integer not null references users(id), + backupData json, + createdAt timestamp default now() +); diff --git a/server/migrations/20250416174536_add_user_tree_table.js b/server/migrations/20250416174536_add_user_tree_table.js index 80a2584..4d611a2 100644 --- a/server/migrations/20250416174536_add_user_tree_table.js +++ b/server/migrations/20250416174536_add_user_tree_table.js @@ -19,5 +19,5 @@ exports.up = function(knex) { */ exports.down = function(knex) { return knex.schema.dropTableIfExists('userTreeSummaries') - + }; diff --git a/server/models/userModel.js b/server/models/userModel.js index 49d9eae..8e2aa0a 100644 --- a/server/models/userModel.js +++ b/server/models/userModel.js @@ -1,3 +1,5 @@ +// deprecated if we use supabase i believe + const db = require('../db/knex'); const { get } = require('../routes/treeMemberRoute'); diff --git a/server/package-lock.json b/server/package-lock.json index 99734e4..56effa1 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@supabase/supabase-js": "^2.74.0", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.5.0", @@ -562,6 +563,104 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@supabase/auth-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.74.0.tgz", + "integrity": "sha512-EJYDxYhBCOS40VJvfQ5zSjo8Ku7JbTICLTcmXt4xHMQZt4IumpRfHg11exXI9uZ6G7fhsQlNgbzDhFN4Ni9NnA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.74.0.tgz", + "integrity": "sha512-VqWYa981t7xtIFVf7LRb9meklHckbH/tqwaML5P3LgvlaZHpoSPjMCNLcquuLYiJLxnh2rio7IxLh+VlvRvSWw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.74.0.tgz", + "integrity": "sha512-9Ypa2eS0Ib/YQClE+BhDSjx7OKjYEF6LAGjTB8X4HucdboGEwR0LZKctNfw6V0PPIAVjjzZxIlNBXGv0ypIkHw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.74.0.tgz", + "integrity": "sha512-K5VqpA4/7RO1u1nyD5ICFKzWKu58bIDcPxHY0aFA7MyWkFd0pzi/XYXeoSsAifnD9p72gPIpgxVXCQZKJg1ktQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.74.0.tgz", + "integrity": "sha512-o0cTQdMqHh4ERDLtjUp1/KGPbQoNwKRxUh6f8+KQyjC5DSmiw/r+jgFe/WHh067aW+WU8nA9Ytw9ag7OhzxEkQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.74.0.tgz", + "integrity": "sha512-IEMM/V6gKdP+N/X31KDIczVzghDpiPWFGLNjS8Rus71KvV6y6ueLrrE/JGCHDrU+9pq5copF3iCa0YQh+9Lq9Q==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.74.0", + "@supabase/functions-js": "2.74.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.74.0", + "@supabase/realtime-js": "2.74.0", + "@supabase/storage-js": "2.74.0" + } + }, + "node_modules/@types/node": { + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", + "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2265,6 +2364,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2284,6 +2389,12 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2343,6 +2454,43 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/server/package.json b/server/package.json index 6a0f438..cd8e1cb 100644 --- a/server/package.json +++ b/server/package.json @@ -11,6 +11,7 @@ "author": "", "license": "ISC", "dependencies": { + "@supabase/supabase-js": "^2.74.0", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.5.0", diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index a14ab28..5b980cf 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -2,12 +2,8 @@ const express = require('express'); const router = express.Router(); -const { register, login, deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); // Assuming you have a controller for your registration logic +const { deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); // Assuming you have a controller for your registration logic -console.log('Register function:', register); - -router.post('/register', register); -router.post('/login', login); router.delete('/remove/:id', deleteByUser); router.get('/user/:id', findById); router.get('/user/email/:email', findByEmail); From 18f4ecc121cd153e4d5d08b24372d89831191db4 Mon Sep 17 00:00:00 2001 From: MatthewLoyed Date: Mon, 20 Oct 2025 17:16:48 -0500 Subject: [PATCH 13/86] Add login button to Register page. --- client/src/pages/CreateAccount/CreateAccount.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/pages/CreateAccount/CreateAccount.js b/client/src/pages/CreateAccount/CreateAccount.js index 2864c38..9bcfa20 100644 --- a/client/src/pages/CreateAccount/CreateAccount.js +++ b/client/src/pages/CreateAccount/CreateAccount.js @@ -156,6 +156,15 @@ const CreateAccount = () => {
    +
    +

    + Already have an account? + + Login here + +

    +
    +
    From bf9b0264b331a44c5bad2431bf5d58e67b06f32d Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Sun, 26 Oct 2025 16:00:20 -0500 Subject: [PATCH 14/86] added err msgs, refactoring, link memberUserId to --- .../AddFamilyMember/AddFamilyMember.js | 326 +++++++++--------- 1 file changed, 167 insertions(+), 159 deletions(-) diff --git a/client/src/components/AddFamilyMember/AddFamilyMember.js b/client/src/components/AddFamilyMember/AddFamilyMember.js index f40a26d..740db48 100644 --- a/client/src/components/AddFamilyMember/AddFamilyMember.js +++ b/client/src/components/AddFamilyMember/AddFamilyMember.js @@ -11,11 +11,16 @@ import { useCurrentUser } from '../../CurrentUserProvider'; // TODO: make form clear when dismissed by clicking outside of modal // make sync contact button functional +const getRequestOptions = { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, +} function AddFamilyMemberPopup({ trigger, userid }) { const [manual, setManual] = useState(false); const [searchTerm, setSearchTerm] = useState(""); const [searchResults, setSearchResults] = useState([]); + const [errorMessage, setErrorMessage] = useState(""); const { currentUserID, currentAccountID } = useCurrentUser(); var family = useRef([]); @@ -27,7 +32,7 @@ function AddFamilyMemberPopup({ trigger, userid }) { reset, watch, handleSubmit, - } = useForm({defaultValues: {selectedMember: '', selectedMemberRelationship: '', matPat: '', gender: ''}}); + } = useForm({defaultValues: {selectedMember: '', selectedMemberRelationship: '', matPat: ''}}); // stored list of family members that require maternal/paternal distinction (maybe shift this to retrieval from backend, so that it can be updated without changing code) let matPat = useMemo(() => ["parent", "cousin", "aunt", "uncle", "grandparent", "niece", "nephew"], []); @@ -62,16 +67,8 @@ function AddFamilyMemberPopup({ trigger, userid }) { // get non-friends const fetchResults = async () => { - let requestOptionsMembers = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - } - let requestOptionsUsers = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - } - fetch(`http://localhost:5000/api/family-members/user/${currentAccountID}`, requestOptionsMembers) // gets all family members + fetch(`http://localhost:5000/api/family-members/user/${currentAccountID}`, getRequestOptions) // gets all family members .then(async(response) => { if (response.ok) { const responseData = await response.json(); @@ -83,13 +80,13 @@ function AddFamilyMemberPopup({ trigger, userid }) { console.error('Error:', response); } }) + // TODO: make this // fetch all users (this is totally scalable) - .then(fetch(`http://localhost:5000/api/auth/users`, requestOptionsUsers) + .then(fetch(`http://localhost:5000/api/auth/users`, getRequestOptions) .then(async(response) => { if (response.ok) { const responseData = await response.json(); - console.log(responseData); - users.current = responseData.filter(user => + users.current = responseData.filter(user => user.username.toLowerCase().includes(searchTerm.toLowerCase()) && !family.current.some(member => member.memberUserId === user.id) ); @@ -103,7 +100,6 @@ function AddFamilyMemberPopup({ trigger, userid }) { ) }; - // go fetch! fetchResults(); }, [searchTerm, currentAccountID]); @@ -114,152 +110,143 @@ function AddFamilyMemberPopup({ trigger, userid }) { setManual(false); setSearchTerm(""); setSearchResults([]); + setErrorMessage(""); reset(); }; // form submission (existing user) - const onSubmitExisting = (data) => { + const onSubmitExisting = async (data, close) => { console.log("submit attempted"); - let memberId = data.selectedMember; - let requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - "firstName": users.current.find(user => user.id === Number(memberId)).username.split(" ")[0], - "lastName": users.current.find(user => user.id === Number(memberId)).username.split(" ")[1], - "birthDate": null, - "deathDate" : null, - "location": null, - "phoneNumber": null, - "userId": userid, - "memberUserId": users.current.find(user => user.id === Number(memberId)).id, - "gender": data.gender, - }) - }; + console.log("Form data:", data); // Log the form data to see what we're getting + setErrorMessage(""); + try { + let memberId = data.selectedMember; + const selectedUser = users.current.find(user => user.id === Number(memberId)); + + let requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + "firstName": selectedUser.username.split(" ")[0] || selectedUser.firstName, + "lastName": selectedUser.username.split(" ")[1] || selectedUser.lastName, + "birthDate": selectedUser.birthDate || null, + "deathDate": selectedUser.deathDate || null, + "location": selectedUser.location || null, + "phoneNumber": selectedUser.phoneNumber || null, + "userId": currentUserID, // The user adding the family member + "memberUserId": selectedUser.id, // Existing user's ID + "gender": selectedUser.gender, + }) + }; - let nextRequestOptions = {}; // will populate later - - let treeUserId = currentUserID; - let treeMemberId; - - // add user to family members table - fetch(`http://localhost:5000/api/family-members/`, requestOptions) // add new family member - .then(async(response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData); - console.log(responseData.message); - treeMemberId = responseData.member; - nextRequestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - person1_id: treeUserId, - person2_id: treeMemberId, - relationshipType: data.selectedMemberRelationship, - relationshipStatus: "active", - side: data.matPat || null, - userId: userid, - }) - }; - return fetch(`http://localhost:5000/api/relationships/`, nextRequestOptions); - } - else { - // print message in return body - const errorData = await response.json(); - console.error('Error:', errorData.message); - nextRequestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - person1_id: treeUserId, - person2_id: treeMemberId, - relationshipType: data.selectedMemberRelationship, - relationshipStatus: "active", - side: data.matPat || null, - userId: userid, - }) - }; - } - }) - .then(async(response) => { - if (response.ok) { - const data = await response.json(); - console.log(data.message); - return window.location.href = `/account/${treeMemberId}`; + let treeUserId = currentUserID; + let treeMemberId; + + const memberResponse = await fetch(`http://localhost:5000/api/family-members/`, requestOptions); // add new family member + console.log('Member response', memberResponse); + const memberData = await memberResponse.json(); + if (!memberResponse.ok) { + throw new Error(memberData.error || 'Failed to add family member'); } - else { - // print message in return body - console.error('Error:', response); + console.log('memberData', memberData); + treeMemberId = memberData.member; + + // relationship table uses id's from treeMembers table, not user ids! + let relRequestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + person1_id: treeUserId, // user adding the member + person2_id: treeMemberId, // added member + relationshipType: data.selectedMemberRelationship, + relationshipStatus: "active", + side: data.matPat || null, + userId: currentUserID, + }) + }; + const relResponse = await fetch(`http://localhost:5000/api/relationships/`, relRequestOptions); // add relationship + const relData = await relResponse.json(); + if (!relResponse.ok) { + throw new Error(relData.error || 'Failed to add relationship'); } - }) - .catch(error => { + + reset(); + close(); + + console.log(relData.message); + return window.location.href = `/account/${treeMemberId}`; // redirect to account page + + } catch (error) { console.error('Error:', error); - }); + setErrorMessage(error.message); + } }; // form submission (manual entry) - const onSubmitManual = (data, close) => { + const onSubmitManual = async (data, close) => { console.log("submit attempted"); - console.log("Form data:", data); // Log the form data to see what we're getting - // add new member to family members table - let requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - "firstName": data.firstName, - "lastName": data.lastName, - "birthDate": data.birthday || null, - "deathDate" : data.deathDate || null, - "location": data.location || null, - "phoneNumber": data.phoneNumber || null, - "userId": userid, - "memberUserId": null, - "gender": data.gender - }) - }; + // console.log("Form data:", data); + setErrorMessage(""); + + try { + // add new member to family members table + let requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + "firstName": data.firstName, + "lastName": data.lastName, + "birthDate": data.birthDate, + "deathDate": data.deathDate, + "location": data.location || null, + "phoneNumber": data.phoneNumber || null, + "userId": currentUserID, // The user adding the family member + "memberUserId": null, // manually added members do not have associated user accounts + "gender": data.gender + }) + }; - let treeUserId = currentUserID; // TODO: will retrieve this from a service or something - let treeMemberId; - - fetch(`http://localhost:5000/api/family-members/`, requestOptions) // add new family member - .then(async(response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData.message); - treeMemberId = responseData.member; - } - else { - // print message in return body - console.error('Error:', response); - } - return fetch(`http://localhost:5000/api/relationships/`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - person1_id: treeUserId, - person2_id: treeMemberId, - relationshipType: data.relationship, - relationshipStatus: "active", - side: data.matPat2 || null, - userId: userid, - }) - }); - }) - .then(async(response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData.message); - // redirect to account page - return window.location.href = `/account/${treeMemberId}`; - } - else { - // print message in return body - console.error('Error:', response); - } + let treeUserId = currentUserID; // TODO: will retrieve this from a service or something + let treeMemberId; + + const memberResponse = await fetch(`http://localhost:5000/api/family-members/`, requestOptions); + const memberData = await memberResponse.json(); + + if (!memberResponse.ok) { + throw new Error(memberData.error || 'Failed to add family member'); + } + + console.log(memberData.message); + treeMemberId = memberData.member; + + const relResponse = await fetch(`http://localhost:5000/api/relationships/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + person1_id: treeUserId, + person2_id: treeMemberId, + relationshipType: data.relationship, + relationshipStatus: "active", + side: data.matPat2 || null, + userId: userid, + }) }); + + const relData = await relResponse.json(); + + if (!relResponse.ok) { + throw new Error(relData.error || 'Failed to add relationship'); + } + + console.log(relData.message); reset(); close(); + return window.location.href = `/account/${treeMemberId}`; + + } catch (error) { + console.error('Error:', error); + setErrorMessage(error.message); + } }; return ( @@ -282,7 +269,23 @@ function AddFamilyMemberPopup({ trigger, userid }) {

    Add Family Member

    - + + {/* Error message display */} + {errorMessage && ( +
    + {errorMessage} +
    + )} + {/* search for existing user */}
    - {/* select gender */} -
    - -
    - {/* select relationship */}
    - Maternal - Paternal + * Maternal + * Paternal
    {/* add button */} @@ -396,6 +388,22 @@ function AddFamilyMemberPopup({ trigger, userid }) {
    + + {/* Error message display */} + {errorMessage && ( +
    + {errorMessage} +
    + )}
      {/* required fields */} @@ -457,7 +465,7 @@ function AddFamilyMemberPopup({ trigger, userid }) {
    • @@ -469,7 +477,7 @@ function AddFamilyMemberPopup({ trigger, userid }) {
    From f9b9bedfdd80e1cf6e17babfe110fa1743edbef0 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Sun, 26 Oct 2025 16:01:52 -0500 Subject: [PATCH 15/86] added date format to backend --- .../src/pages/CreateAccount/CreateAccount.js | 2 +- server/controllers/treeMemberController.js | 28 ++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/client/src/pages/CreateAccount/CreateAccount.js b/client/src/pages/CreateAccount/CreateAccount.js index 2eff45b..d256f5f 100644 --- a/client/src/pages/CreateAccount/CreateAccount.js +++ b/client/src/pages/CreateAccount/CreateAccount.js @@ -163,7 +163,7 @@ const CreateAccount = () => {
    - + {errors.birthdate &&

    {errors.birthdate.message}

    }
    diff --git a/server/controllers/treeMemberController.js b/server/controllers/treeMemberController.js index 425af44..0f893a2 100644 --- a/server/controllers/treeMemberController.js +++ b/server/controllers/treeMemberController.js @@ -1,23 +1,39 @@ const treeMember = require('../models/treeMemberModel'); const relationship = require('../models/relationshipModel'); +const { update } = require('../db/knex'); + +// format dates to YYYY-MM-DD +const formatDate = (dateValue) => { + const d = new Date(dateValue); + if (isNaN(d.getTime())) return null; // Invalid date + + // Extract YYYY-MM-DD from the date object + const year = d.getUTCFullYear(); + const month = String(d.getUTCMonth() + 1).padStart(2, '0'); // month starts at zero + const day = String(d.getUTCDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +}; const addTreeMember = async (req, res) => { try { const { firstName, lastName, birthDate, deathDate, location, phoneNumber, relationships, userId, memberUserId, gender } = req.body; + const formattedBirthDate = formatDate(birthDate); + const formattedDeathDate = formatDate(deathDate); + // ensure all necessary fields are passed in the request body const [newMember] = await treeMember.addMember({ firstName, lastName, - birthDate, - deathDate, + birthDate: formattedBirthDate, + deathDate: formattedDeathDate, location, phoneNumber, userId, memberUserId, gender }); - /// need to fix that a value can be left empty (deathDate) // if there are relationships, add them to the database if (relationships && relationships.length > 0) { @@ -57,11 +73,15 @@ const editTreeMember = async (req, res) => { //this works } - // delete empty or undefined fields from updateData + for (let key in updateData) { + // delete empty or undefined fields from updateData if (updateData[key] === '' || updateData[key] === undefined) { delete updateData[key]; } + // Verify YYYY-MM-DD format before sending it to the database + if (key === 'birthDate') { updateData.birthDate = formatDate(updateData.birthDate);} + if (key === 'deathDate') { updateData.deathDate = formatDate(updateData.deathDate);} } if (Object.keys(updateData).length === 0) { From ccd4a121f06b7f6333d2e7d9368ed2b3e772e972 Mon Sep 17 00:00:00 2001 From: MatthewLoyed Date: Thu, 30 Oct 2025 14:51:38 -0500 Subject: [PATCH 16/86] Implemented MFA, Session, OAuth, and Password Reset. --- client/src/CurrentUserProvider.js | 71 +++++-- .../ProtectedRoute/ProtectedRoute.js | 4 +- client/src/index.js | 12 ++ client/src/pages/Account/Account.js | 139 +++++++++--- .../src/pages/CreateAccount/CreateAccount.js | 27 ++- client/src/pages/Login/Login.js | 151 ++++++++++++- client/src/pages/Reset/Reset.js | 62 +++++- client/src/pages/Reset/UpdatePassword.js | 59 ++++++ .../pages/WebsiteSettings/WebsiteSettings.js | 199 +++++++++++++++++- client/src/utils/auth.js | 34 ++- client/src/utils/authHandlers.js | 44 +++- server/routes/authRoutes.js | 2 +- 12 files changed, 718 insertions(+), 86 deletions(-) create mode 100644 client/src/pages/Reset/UpdatePassword.js diff --git a/client/src/CurrentUserProvider.js b/client/src/CurrentUserProvider.js index 45a788e..d814e72 100644 --- a/client/src/CurrentUserProvider.js +++ b/client/src/CurrentUserProvider.js @@ -1,13 +1,14 @@ import { React, useState, createContext, useContext, useEffect } from "react" -import { set } from "react-hook-form"; +import { supabase } from "./utils/supabaseClient"; export const currentContext = createContext(); export const CurrentUserProvider = ({ children }) => { const [currentUserID, setCurrentUserIDState] = useState(''); - const [currentAccountID, setCurrentAccountIDState] = useState(''); // TODO login will set this + const [currentAccountID, setCurrentAccountIDState] = useState(''); const [currentUserName, setCurrentUserNameState] = useState(''); const [loading, setLoading] = useState(true); + const [supabaseUser, setSupabaseUser] = useState(null); const setCurrentAccountID = (accountID) => { // logging in will trigger this localStorage.setItem("currentAccountID", accountID); @@ -52,29 +53,61 @@ export const CurrentUserProvider = ({ children }) => { } - // init + // Initialize Supabase auth state useEffect(() => { - const initializeState = () => { - const storedAccountID = localStorage.getItem("currentAccountID"); - const storedUserID = localStorage.getItem("currentUserID"); - const storedUserName = localStorage.getItem("currentUserName"); - - if (storedAccountID) { - setCurrentAccountIDState(storedAccountID); - } - if (storedUserID) { - setCurrentUserIDState(storedUserID); - } - if (storedUserName) { - setCurrentUserNameState(storedUserName); + const initializeAuth = async () => { + try { + // Get initial session + const { data: { session } } = await supabase.auth.getSession(); + + if (session?.user) { + setSupabaseUser(session.user); + setCurrentAccountIDState(session.user.id); + setCurrentUserNameState(session.user.email); // Use email as default username + } + + setLoading(false); + } catch (error) { + console.error('Error initializing auth:', error); + setLoading(false); } - setLoading(false); }; - initializeState(); + + initializeAuth(); + + // Listen for auth changes + const { data: { subscription } } = supabase.auth.onAuthStateChange( + async (event, session) => { + if (session?.user) { + setSupabaseUser(session.user); + setCurrentAccountIDState(session.user.id); + setCurrentUserNameState(session.user.email); + } else { + setSupabaseUser(null); + setCurrentAccountIDState(''); + setCurrentUserNameState(''); + } + setLoading(false); + } + ); + + return () => subscription.unsubscribe(); }, []); return ( - + {children} ) diff --git a/client/src/components/ProtectedRoute/ProtectedRoute.js b/client/src/components/ProtectedRoute/ProtectedRoute.js index a1db22c..15b1b88 100644 --- a/client/src/components/ProtectedRoute/ProtectedRoute.js +++ b/client/src/components/ProtectedRoute/ProtectedRoute.js @@ -3,14 +3,14 @@ import { Navigate } from 'react-router-dom'; import { useCurrentUser } from '../../CurrentUserProvider'; function ProtectedRoute({ children }) { - const { currentAccountID, loading } = useCurrentUser(); + const { supabaseUser, loading } = useCurrentUser(); if (loading) { return
    Loading...
    ; } // redirect to login - if (!currentAccountID) { + if (!supabaseUser) { return ; } diff --git a/client/src/index.js b/client/src/index.js index f720285..2cda063 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -8,6 +8,8 @@ import Home from './pages/Home/Home'; import Account from './pages/Account/Account'; import Tree from './pages/Tree/Tree'; import Login from './pages/Login/Login'; +import ResetPassword from './pages/Reset/Reset'; +import UpdatePassword from './pages/Reset/UpdatePassword'; import Family from './pages/Family/Family'; import ShareTree from './pages/Tree/ShareTree/ShareTree'; import ViewSharedTrees from './pages/Tree/ViewSharedTrees/ViewSharedTrees'; @@ -55,6 +57,16 @@ const router = createBrowserRouter([ path: '/register', element: , }, + + { + path: '/reset-password', + element: , + }, + { + path: '/update-password', + element: , + }, + { path: '/tree', element: ( diff --git a/client/src/pages/Account/Account.js b/client/src/pages/Account/Account.js index e38f998..a119abb 100644 --- a/client/src/pages/Account/Account.js +++ b/client/src/pages/Account/Account.js @@ -3,7 +3,7 @@ import * as styles from './styles'; import { useParams, useNavigate } from 'react-router-dom'; import NavBar from '../../components/NavBar/NavBar'; import AddToTreePopup from '../../components/AddToTree/AddToTree'; -import { CurrentUserProvider, useCurrentUser } from '../../CurrentUserProvider'; +import { useCurrentUser } from '../../CurrentUserProvider'; function Account() { const navigate = useNavigate(); // used to change route without refreshing page, used to prevent infinite refreshes @@ -11,43 +11,69 @@ function Account() { const [existsInTree, setExistsInTree] = useState(false); // will be retrieved const [relationshipType, setRelationshipType] = useState(''); // will be retrieved - const { currentUserID, fetchCurrentUserID, currentAccountID } = useCurrentUser(); - useEffect(() => { - // define a regular function to call the async function - const fetchData = async () => { - await fetchCurrentUserID(); - }; + const { currentUserID, supabaseUser, loading } = useCurrentUser(); - fetchData(); - }, [fetchCurrentUserID]); + // Redirect to login if not authenticated + useEffect(() => { + if (!loading && !supabaseUser) { + navigate('/login'); + } + }, [loading, supabaseUser, navigate]); // takes id from url path let { id } = useParams(); // if no id is provided, retrieve current user's id and show that page useEffect(() => { - if (!id && currentUserID) { + if (!id && supabaseUser?.id) { setOwnAccount(true); - navigate(`/account/${currentUserID}`, { replace: true }); + navigate(`/account/${supabaseUser.id}`, { replace: true }); } - }, [id, currentUserID, navigate]); + }, [id, supabaseUser?.id, navigate]); // TODO: query for data of account user & verify that userID of logged in user matches + const [userData, setUserData] = useState({ id: id, - username: 'Loading...', + firstName: 'Loading...', + lastName: '', + email: '', + birthdate: '', + address: '', + city: '', + state: '', + country: '', + phone_number: '', + zipcode: '' }) - // fetch user info - const requestOptions = { - method: 'GET', - headers: { 'Content-Type': 'application/json' } - }; - - // find this person's account info + // Fetch user info - check if it's a Supabase user or family member useEffect(() => { if (!id) return; + // Check if this is the current Supabase user + if (id === supabaseUser?.id) { + console.log('Supabase user data:', supabaseUser); + console.log('User metadata:', supabaseUser.user_metadata); + + setUserData({ + id: supabaseUser.id, + firstName: supabaseUser.user_metadata?.first_name || 'User', + lastName: supabaseUser.user_metadata?.last_name || '', + email: supabaseUser.email, + birthdate: supabaseUser.user_metadata?.birthdate || '', + address: supabaseUser.user_metadata?.address || '', + city: supabaseUser.user_metadata?.city || '', + state: supabaseUser.user_metadata?.state || '', + country: supabaseUser.user_metadata?.country || '', + phone_number: supabaseUser.user_metadata?.phone_number || '', + zipcode: supabaseUser.user_metadata?.zipcode || '' + }); + setOwnAccount(true); + return; + } + + // Otherwise, try to fetch from family members API const requestOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' }, @@ -60,26 +86,38 @@ function Account() { setUserData(data); } else { console.error('Error fetching user data:', response); + // If family member not found, show basic info + setUserData({ + id: id, + firstName: 'Unknown', + lastName: 'User', + email: '', + }); } }) .catch((error) => { console.error('There was a problem with the fetch operation:', error); }); - }, [id]); + }, [id, supabaseUser]); useEffect(() => { - if(!userData.memberUserId){ - setOwnAccount(false); - } - else if(userData.userId === userData.memberUserId) { - // don't fetch relationship + // Check if this is the current user's own account + if (id === supabaseUser?.id) { setOwnAccount(true); return; } + + // If it's not the current user, check relationships (only for family members) + if (!userData.memberUserId) { + setOwnAccount(false); + return; + } + const requestOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' }, }; + // if not self, determine relationship to user fetch(`http://localhost:5000/api/relationships/${id}`, requestOptions) .then(async(response) => { @@ -90,7 +128,7 @@ function Account() { if(relationships[i].person1_id === parseInt(currentUserID) && relationships[i].person2_id === parseInt(id)) { // this is the relationship setRelationshipType(relationships[i].relationshipType); - return; // check this + return; } } } @@ -103,18 +141,18 @@ function Account() { .catch(error => { console.error('There was a problem with the fetch operation:', error); }); - }, [id, currentUserID, userData.id]); + }, [id, currentUserID, userData.id, userData.memberUserId, supabaseUser?.id]); // check if user exists in tree useEffect(() => { - if (!id || !currentAccountID) return; + if (!id || !supabaseUser?.id) return; const requestOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' }, }; - fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`, requestOptions) + fetch(`http://localhost:5000/api/tree-info/${supabaseUser.id}`, requestOptions) .then(async (response) => { if (response.ok) { console.log("tree info response"); @@ -135,7 +173,7 @@ function Account() { .catch((error) => { console.error('There was a problem with the fetch operation:', error); }); - }, [id, currentAccountID]); + }, [id, supabaseUser?.id]); return (
    @@ -154,7 +192,7 @@ function Account() { {/* if someone else's account, show buttons */} {!ownAccount && (
    - Add To Tree} accountUserName={userData.firstName} accountUserId={id} userId={currentUserID} currentUserAccountRelationshipType={relationshipType} /> + Add To Tree} accountUserName={userData.firstName} accountUserId={id} userId={supabaseUser?.id} currentUserAccountRelationshipType={relationshipType} />
    )} @@ -163,6 +201,43 @@ function Account() { {/* divider line */}
    + + {/* User Information Section */} +
    +

    Profile Information

    + +
    + {/* Basic Info */} +
    +

    Basic Information

    +
    +
    Email: {userData?.email || 'Not provided'}
    + {userData?.birthdate &&
    Birth Date: {new Date(userData.birthdate).toLocaleDateString()}
    } + {userData?.phone_number &&
    Phone: {userData.phone_number}
    } +
    +
    + + {/* Address Info */} +
    +

    Address Information

    +
    + {userData?.address &&
    Address: {userData.address}
    } + {(userData?.city || userData?.state) && ( +
    City, State: {[userData.city, userData.state].filter(Boolean).join(', ')}
    + )} + {userData?.zipcode &&
    ZIP Code: {userData.zipcode}
    } + {userData?.country &&
    Country: {userData.country}
    } +
    +
    +
    + + {/* Show message if no additional info is available */} + {!userData?.birthdate && !userData?.phone_number && !userData?.address && !userData?.city && !userData?.state && !userData?.zipcode && !userData?.country && ( +
    + No additional profile information available. Update your profile to add more details. +
    + )} +
    diff --git a/client/src/pages/CreateAccount/CreateAccount.js b/client/src/pages/CreateAccount/CreateAccount.js index 9bcfa20..9139b6c 100644 --- a/client/src/pages/CreateAccount/CreateAccount.js +++ b/client/src/pages/CreateAccount/CreateAccount.js @@ -23,7 +23,7 @@ const yupValidation = yup.object().shape( country: yup.string().required("Country of residence is a required field."), phonenum: yup.string() .matches( - /^(\+\d{1,2}\s?)?1?\-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/ + /^(\+\d{1,2}\s?)?1?-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/ , "Invalid phone number format." ), zipcode: yup.string().matches(/^\d{5}(?:[-\s]\d{4})?$/, "Invalid zip code format."), @@ -43,7 +43,17 @@ const CreateAccount = () => { const onSubmit = async (data) => { setErrorMessage(""); // clear previous errors try { - const user = await handleRegister(data.email, data.password); // frontend Supabase registration + const user = await handleRegister(data.email, data.password, { + first_name: data.firstname, + last_name: data.lastname, + birthdate: data.birthdate, + address: data.address, + city: data.city, + state: data.state, + country: data.country, + phone_number: data.phonenum, + zipcode: data.zipcode + }); // frontend Supabase registration // TODO: store additional info in mysqldatabase later // await fetch('http://localhost:5000/api/users', { @@ -91,6 +101,14 @@ const CreateAccount = () => { KinTree Logo

    Create Account

    + + {/* Error Message Display */} + {errorMessage && ( +
    + {errorMessage} +
    + )} +
    @@ -150,7 +168,10 @@ const CreateAccount = () => {
    - diff --git a/client/src/pages/Login/Login.js b/client/src/pages/Login/Login.js index f9c8d3c..21ff37d 100644 --- a/client/src/pages/Login/Login.js +++ b/client/src/pages/Login/Login.js @@ -4,22 +4,84 @@ import logo from '../../assets/kintreelogo-adobe.png'; import { useForm } from 'react-hook-form'; import { Link } from 'react-router-dom'; import { useCurrentUser } from '../../CurrentUserProvider'; -import { handleLogin } from '../../utils/authHandlers'; +import { handleLogin, handleSignInWithGoogle } from '../../utils/authHandlers'; +import { supabase } from '../../utils/supabaseClient'; function Login() { const { register, handleSubmit } = useForm(); const [ errorMessage, setErrorMessage ] = useState(""); + const [ needsConfirm, setNeedsConfirm ] = useState(false); + const [ attemptedEmail, setAttemptedEmail ] = useState(""); + const [ resendLoading, setResendLoading ] = useState(false); const { setCurrentAccountID, fetchCurrentUserID, fetchCurrentAccountID } = useCurrentUser(); + const [ mfaStep, setMfaStep ] = useState(false); + const [ mfaFactorId, setMfaFactorId ] = useState(""); + const [ mfaChallengeId, setMfaChallengeId ] = useState(""); + const [ mfaCode, setMfaCode ] = useState(""); + const [ mfaError, setMfaError ] = useState(""); const onSubmit = async (data) => { setErrorMessage(""); // clear previous errors + setNeedsConfirm(false); + setAttemptedEmail(data.email); try { - await handleLogin(data.email, data.password); // just call the handler + await handleLogin(data.email, data.password); // password step + // After password login, check for verified TOTP factor + const { data: factorsData, error: lfErr } = await supabase.auth.mfa.listFactors(); + if (lfErr) throw lfErr; + const totp = factorsData?.all?.find(f => f.factor_type === 'totp' && f.status === 'verified'); + if (totp) { + const { data: challengeData, error: chErr } = await supabase.auth.mfa.challenge({ factorId: totp.id }); + if (chErr) throw chErr; + setMfaFactorId(totp.id); + setMfaChallengeId(challengeData?.id || ""); + setMfaStep(true); + return; // wait for MFA verify + } + // No MFA required → proceed + window.location.href = '/'; } catch (error) { - setErrorMessage(error.message); + const msg = String(error?.message || '').toLowerCase(); + const requiresConfirm = msg.includes('confirm') || msg.includes('not confirmed'); + if (requiresConfirm) { + setNeedsConfirm(true); + } else { + setErrorMessage(error.message); + } } }; + const onSubmitMfa = async (e) => { + e.preventDefault(); + setMfaError(""); + try { + const { error } = await supabase.auth.mfa.verify({ factorId: mfaFactorId, challengeId: mfaChallengeId, code: mfaCode }); + if (error) throw error; + window.location.href = '/'; + } catch (e2) { + setMfaError(e2.message || 'Verification failed'); + } + } + + const handleResendConfirmation = async () => { + if (!attemptedEmail) return; + setResendLoading(true); + try { + const { error } = await supabase.auth.resend({ + type: 'signup', + email: attemptedEmail, + options: { emailRedirectTo: `${window.location.origin}/login` } + }); + if (error) throw error; + // surface a lightweight notice + setErrorMessage('Confirmation email sent. Please check your inbox.'); + } catch (e) { + setErrorMessage(e.message); + } finally { + setResendLoading(false); + } + } + document.body.style.overflow = 'hidden'; document.body.style.width = '100%'; @@ -28,7 +90,24 @@ function Login() {
    KinTree Logo

    Sign In

    + {!mfaStep && ( + {needsConfirm && ( +
    +
    Please confirm your email to continue. We sent a link to
    {attemptedEmail}
    + +
    + )}
    ) diff --git a/client/src/pages/Reset/Reset.js b/client/src/pages/Reset/Reset.js index 986f727..cb22a75 100644 --- a/client/src/pages/Reset/Reset.js +++ b/client/src/pages/Reset/Reset.js @@ -1,12 +1,56 @@ -import React from 'react'; -import * as styles from './styles'; +import { useState } from "react"; +import { handleResetPassword } from '../../utils/authHandlers'; +import * as styles from '../Login/styles'; +import logo from '../../assets/kintreelogo-adobe.png'; -function Reset() { - return ( -
    +export default function ResetPassword() { + const [email, setEmail] = useState(""); + const [message, setMessage] = useState(""); -
    - ) -} + const onSubmit = async (e) => { + e.preventDefault(); + try { + await handleResetPassword(email); + setMessage('Check your email for a reset link.'); + } catch (error) { + // message handled by handler alert + } + }; -export default Reset; \ No newline at end of file + return ( +
    +
    + KinTree Logo +

    Reset Password

    +
    + {message && ( +
    + {message} +
    + )} +
      +
    • + + setEmail(e.target.value)} + style={styles.FieldStyle} + required + /> +
    • +
    +
    + +
    +
    +

    + Remembered your password? Back to Sign In +

    +
    +
    +
    +
    + ); +} diff --git a/client/src/pages/Reset/UpdatePassword.js b/client/src/pages/Reset/UpdatePassword.js new file mode 100644 index 0000000..b82ea4c --- /dev/null +++ b/client/src/pages/Reset/UpdatePassword.js @@ -0,0 +1,59 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { handleUpdatePassword } from "../../utils/authHandlers"; +import * as styles from '../Login/styles'; +import logo from '../../assets/kintreelogo-adobe.png'; + +export default function UpdatePassword() { + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const navigate = useNavigate(); + + const onSubmit = async (e) => { + e.preventDefault(); + try { + await handleUpdatePassword(password); + setMessage("Password updated. Redirecting to login..."); + setTimeout(() => navigate('/login'), 1200); + } catch (error) { + // message handled by handler alert + } + }; + + return ( +
    +
    + KinTree Logo +

    Update Password

    +
    + {message && ( +
    + {message} +
    + )} +
      +
    • + + setPassword(e.target.value)} + style={styles.FieldStyle} + required + /> +
    • +
    +
    + +
    +
    +

    + Back to Sign In +

    +
    +
    +
    +
    + ); +} diff --git a/client/src/pages/WebsiteSettings/WebsiteSettings.js b/client/src/pages/WebsiteSettings/WebsiteSettings.js index 8216dcd..7647add 100644 --- a/client/src/pages/WebsiteSettings/WebsiteSettings.js +++ b/client/src/pages/WebsiteSettings/WebsiteSettings.js @@ -1,11 +1,143 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import * as styles from "./styles"; import NavBar from "../../components/NavBar/NavBar"; import { handleLogout } from '../../utils/authHandlers'; +import { supabase } from '../../utils/supabaseClient'; function WebsiteSettings() { const [notifications, setNotifications] = useState(true); const [darkMode, setDarkMode] = useState(false); + const [totpFactorId, setTotpFactorId] = useState(""); + const [totpQr, setTotpQr] = useState(""); + const [totpCode, setTotpCode] = useState(""); + const [totpStatus, setTotpStatus] = useState(""); + const [totpLoading, setTotpLoading] = useState(false); + const [totpVerified, setTotpVerified] = useState(false); + + async function loadFactors() { + try { + const { data: factorsData, error } = await supabase.auth.mfa.listFactors(); + if (error) throw error; + const verified = factorsData?.all?.find(f => f.factor_type === 'totp' && f.status === 'verified'); + const unverified = factorsData?.all?.find(f => f.factor_type === 'totp' && f.status === 'unverified'); + if (verified) { + setTotpVerified(true); + setTotpFactorId(verified.id); + setTotpQr(""); + } else if (unverified) { + setTotpVerified(false); + setTotpFactorId(unverified.id); + setTotpQr(""); // we can't re-fetch QR; allow verify via code + } else { + setTotpVerified(false); + setTotpFactorId(""); + setTotpQr(""); + } + } catch (e) { + console.error('Load factors error:', e); + } + } + + useEffect(() => { + loadFactors(); + }, []); + + async function startTotpEnroll() { + setTotpStatus(""); + setTotpLoading(true); + try { + // Avoid starting a new enroll while one is pending + if (totpVerified) { + setTotpStatus('Two-factor authentication is already enabled.'); + return; + } + if (totpFactorId && !totpVerified) { + setTotpStatus('A TOTP setup is pending. Enter a code from your authenticator, or click Start over.'); + return; + } + const { data, error } = await supabase.auth.mfa.enroll({ factorType: 'totp' }); + if (error) throw error; + console.log('Enroll data:', data); + setTotpFactorId(data.id); + setTotpQr(data.totp?.qr_code || ""); + } catch (e) { + setTotpStatus(e.message); + } finally { + setTotpLoading(false); + } + } + + async function verifyTotp() { + if (!totpFactorId || !totpCode) return; + setTotpLoading(true); + setTotpStatus(""); + try { + // Ensure we have the correct pending factorId in case state was lost + if (!totpFactorId) { + const { data: factorsData, error: factorsErr } = await supabase.auth.mfa.listFactors(); + if (factorsErr) throw factorsErr; + console.log('Factors:', factorsData); + const pending = factorsData?.all?.find(f => f.factor_type === 'totp' && f.status === 'unverified'); + if (pending) setTotpFactorId(pending.id); + } else { + const { data: factorsData, error: factorsErr } = await supabase.auth.mfa.listFactors(); + if (!factorsErr) console.log('Factors:', factorsData); + } + + console.log('Using factorId:', totpFactorId, 'Code:', totpCode); + // Create a challenge, then verify with challengeId (works across SDK versions) + const { data: challengeData, error: challengeErr } = await supabase.auth.mfa.challenge({ factorId: totpFactorId }); + if (challengeErr) throw challengeErr; + console.log('Challenge data:', challengeData); + const challengeId = challengeData?.id; + const { error } = await supabase.auth.mfa.verify({ factorId: totpFactorId, challengeId, code: totpCode }); + if (error) throw error; + setTotpStatus('Two-factor authentication enabled.'); + setTotpQr(""); + setTotpCode(""); + setTotpVerified(true); + } catch (e) { + console.error('TOTP verify error:', e); + setTotpStatus(e.message || 'Verification failed'); + } finally { + setTotpLoading(false); + } + } + + async function disableTotp() { + if (!totpFactorId) return; + setTotpLoading(true); + setTotpStatus(""); + try { + const { error } = await supabase.auth.mfa.unenroll({ factorId: totpFactorId }); + if (error) throw error; + setTotpVerified(false); + setTotpFactorId(""); + setTotpStatus('Two-factor authentication disabled.'); + } catch (e) { + setTotpStatus(e.message || 'Failed to disable'); + } finally { + setTotpLoading(false); + } + } + + async function restartTotpEnroll() { + // For lingering unverified factor: unenroll then start fresh + if (totpFactorId && !totpVerified) { + try { + const { error } = await supabase.auth.mfa.unenroll({ factorId: totpFactorId }); + if (error) throw error; + setTotpFactorId(""); + setTotpQr(""); + setTotpCode(""); + setTotpStatus('Previous pending setup cleared.'); + } catch (e) { + setTotpStatus(e.message || 'Could not reset existing setup'); + return; + } + } + await startTotpEnroll(); + } return (
    @@ -31,13 +163,64 @@ function WebsiteSettings() {

    - - - setNotifications(!notifications)} - /> +
    + + {totpVerified && ( +
    + Enabled + + {totpStatus && {totpStatus}} +
    + )} + {!totpVerified && !totpQr && !totpFactorId && ( +
    + + {totpStatus && {totpStatus}} +
    + )} + {!totpVerified && totpFactorId && !totpQr && ( +
    +
    Enter a 6‑digit code from your authenticator to complete setup.
    + setTotpCode(e.target.value)} + /> +
    + + +
    + {totpStatus && {totpStatus}} +
    + )} + {!totpVerified && totpQr && ( +
    +
    Scan this QR with Duo/Google Authenticator, then enter the 6‑digit code:
    + TOTP QR + setTotpCode(e.target.value)} + /> + + {totpStatus && {totpStatus}} +
    + )} +
    {/* Profile & Personalization */} diff --git a/client/src/utils/auth.js b/client/src/utils/auth.js index 8ba9c14..b286c37 100644 --- a/client/src/utils/auth.js +++ b/client/src/utils/auth.js @@ -1,10 +1,18 @@ import { supabase } from './supabaseClient'; -// These functions are used to handle the authentication of the user, but only the pure login, logout, etc functionality. It should not include frontend logic like redirects. +// These functions are used to handle the authentication of the user, but only the pure login, logout, etc functionality. +// It should not include frontend logic like redirects. // Register new user -export async function registerUser(email, password) { - const { data, error } = await supabase.auth.signUp({ email, password }); +export async function registerUser(email, password, metadata = {}) { + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: metadata, + emailRedirectTo: `${window.location.origin}/login` + } + }); if (error) throw error; return data; } @@ -21,3 +29,23 @@ export async function logoutUser() { const { error } = await supabase.auth.signOut(); if (error) throw error; } + +export async function resetPassword(email, url) { + const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo: url }); + if (error) throw error; +} + +export async function updatePassword(password) { + const { error } = await supabase.auth.updateUser({ password }); + if (error) throw error; +} + +// OAuth: Google sign-in +export async function signInWithGoogle() { + const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { redirectTo: `${window.location.origin}/` }, + }); + if (error) throw error; + return data; +} \ No newline at end of file diff --git a/client/src/utils/authHandlers.js b/client/src/utils/authHandlers.js index 4947670..774f3cc 100644 --- a/client/src/utils/authHandlers.js +++ b/client/src/utils/authHandlers.js @@ -1,5 +1,7 @@ // src/handlers/authHandlers.js -import { loginUser, registerUser, logoutUser } from '../utils/auth'; +import { loginUser, registerUser, logoutUser, resetPassword, updatePassword, signInWithGoogle } from '../utils/auth'; + +const BASE_URL = process.env.REACT_APP_BASE_URL || 'http://localhost:3000'; // This page is used to handle authentication and includes redirects and error handling. @@ -7,7 +9,8 @@ export async function handleLogin(email, password) { try { const data = await loginUser(email, password); // call the pure login function console.log("Logged in user:", data.user); - window.location.href = '/home'; // redirect after login to home + // Do not redirect here; caller will handle MFA step and navigation + return data; } catch (error) { console.error('Login error:', error.message); alert(error.message); @@ -15,9 +18,9 @@ export async function handleLogin(email, password) { } } -export async function handleRegister(email, password) { +export async function handleRegister(email, password, metadata = {}) { try { - const data = await registerUser(email, password); + const data = await registerUser(email, password, metadata); console.log("Registered user:", data.user); return data; // Return the data so the calling function can use it } catch (error) { @@ -37,3 +40,36 @@ export async function handleLogout() { alert(error.message); } } + +export async function handleResetPassword(email) { + try { + const url = `${BASE_URL}/update-password`; + await resetPassword(email, url); + } catch (error) { + console.error('Reset Password error:', error.message); + alert(error.message); + throw error; // so form onSubmit can catch it + } +} + +export async function handleUpdatePassword(password) { + try { + await updatePassword(password); + window.location.href = '/login'; + } catch (error) { + console.error('Update Password error:', error.message); + alert(error.message); + throw error; // so form onSubmit can catch it + } +} + +// Google OAuth handler +export async function handleSignInWithGoogle() { + try { + await signInWithGoogle(); // redirects to Google, then back to our site + } catch (error) { + console.error('Google sign-in error:', error.message); + alert(error.message); + throw error; + } +} \ No newline at end of file diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index 5b980cf..d9ae2a1 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); -const { deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); // Assuming you have a controller for your registration logic +const { deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); router.delete('/remove/:id', deleteByUser); router.get('/user/:id', findById); From 2e47a71b309fd5782bdd3425c20cbd3f95acc19c Mon Sep 17 00:00:00 2001 From: MatthewLoyed Date: Thu, 30 Oct 2025 18:06:34 -0500 Subject: [PATCH 17/86] Migrated backend to Supabase. Deleted Knex but still use Express. --- client/src/CurrentUserProvider.js | 19 +++ client/src/utils/auth.js | 25 ++++ server/controllers/authController.js | 24 +++- server/controllers/backupController.js | 29 ++-- server/controllers/relationshipController.js | 85 +++++++++--- server/controllers/sharedTreeController.js | 4 +- server/controllers/treeInfoController.js | 16 ++- server/controllers/treeMemberController.js | 69 ++++++++-- server/controllers/treeSummaryController.js | 17 ++- server/db/knex.js | 7 - server/db/supabase-init.sql | 9 ++ server/knexfile.js | 23 ---- server/lib/supabase.js | 16 +++ server/models/backupModel.js | 46 ++++++- server/models/relationshipModel.js | 75 ++++++++-- server/models/sharedTreeModel.js | 84 +++++++----- server/models/treeInfoModel.js | 34 +++-- server/models/treeMemberModel.js | 137 ++++++++++++++++--- server/models/treeSummaryModel.js | 54 +++++++- server/models/userModel.js | 102 ++++++++++++-- server/mysql-connection.js | 43 ------ server/routes/authRoutes.js | 3 +- server/server.js | 35 +---- 23 files changed, 690 insertions(+), 266 deletions(-) delete mode 100644 server/db/knex.js delete mode 100644 server/knexfile.js create mode 100644 server/lib/supabase.js delete mode 100644 server/mysql-connection.js diff --git a/client/src/CurrentUserProvider.js b/client/src/CurrentUserProvider.js index d814e72..510cc5e 100644 --- a/client/src/CurrentUserProvider.js +++ b/client/src/CurrentUserProvider.js @@ -82,6 +82,25 @@ export const CurrentUserProvider = ({ children }) => { setSupabaseUser(session.user); setCurrentAccountIDState(session.user.id); setCurrentUserNameState(session.user.email); + // Auto-sync profile into public.users using auth metadata when available + try { + const m = session.user.user_metadata || {}; + await fetch('http://localhost:5000/api/auth/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + auth_uid: session.user.id, + email: session.user.email, + username: session.user.email, + firstName: m.firstName || m.first_name || null, + lastName: m.lastName || m.last_name || null, + phoneNumber: m.phoneNumber || m.phone_number || m.phonenum || null, + birthDate: m.birthDate || m.birthdate || null, + }) + }); + } catch (e) { + console.warn('Auth sync failed:', e?.message || e); + } } else { setSupabaseUser(null); setCurrentAccountIDState(''); diff --git a/client/src/utils/auth.js b/client/src/utils/auth.js index b286c37..93a975e 100644 --- a/client/src/utils/auth.js +++ b/client/src/utils/auth.js @@ -14,6 +14,31 @@ export async function registerUser(email, password, metadata = {}) { } }); if (error) throw error; + + // After successful signup, upsert the profile into public.users via backend + try { + const user = data?.user || (await supabase.auth.getUser()).data?.user; + if (user) { + await fetch('http://localhost:5000/api/auth/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + auth_uid: user.id, + email: user.email, + username: user.email, + // Map possible metadata key variants + firstName: metadata.firstName || metadata.first_name || null, + lastName: metadata.lastName || metadata.last_name || null, + phoneNumber: metadata.phoneNumber || metadata.phone_number || metadata.phonenum || null, + birthDate: metadata.birthDate || metadata.birthdate || null, + }) + }); + } + } catch (e) { + // Non-fatal: keep signup success even if sync fails + console.warn('Profile sync skipped:', e?.message || e); + } + return data; } diff --git a/server/controllers/authController.js b/server/controllers/authController.js index ff90223..d161f0e 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -1,5 +1,5 @@ // authController.js - the main backend file for user registration, signin, etc -const User = require('../models/userModel'); // delete once done repalcing with supabase +const User = require('../models/userModel'); // now backed by Supabase const deleteByUser = async (req,res) => { const { id } = req.params; @@ -57,3 +57,25 @@ const getAllUsers = async (req, res) => { } module.exports = { deleteByUser, findById, findByEmail, getAllUsers }; + +// Add a sync endpoint: POST /api/auth/sync +// Body: { auth_uid, email, username, firstName, lastName, phoneNumber, birthDate } +const syncAuthUser = async (req, res) => { + try { + const { auth_uid, email, username, firstName, lastName, phoneNumber, birthDate } = req.body || {}; + if (!auth_uid || !email) { + return res.status(400).json({ error: 'auth_uid and email are required' }); + } + const user = await User.upsertByAuthUser({ auth_uid, email, username, firstName, lastName, phoneNumber, birthDate }); + res.status(200).json(user); + } catch (error) { + console.error('Sync error:', error); + res.status(500).json({ + error: 'Error syncing auth user', + details: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined + }); + } +}; + +module.exports.syncAuthUser = syncAuthUser; diff --git a/server/controllers/backupController.js b/server/controllers/backupController.js index b03ebc9..6a09648 100644 --- a/server/controllers/backupController.js +++ b/server/controllers/backupController.js @@ -8,13 +8,17 @@ const backupInfo = require('../models/backupModel'); const backupUser = async (req, res) => { try{ const { id } = req.params; - const user = await userInfo.findById(id); + // Resolve UUID to integer if needed + const User = require('../models/userModel'); + const userIdInt = await User.resolveUserIdFromAuthUid(id) || id; + + const user = await userInfo.findById(userIdInt); if(!user) { - return { error: "User not found"}; + return res.status(404).json({ error: "User not found"}); } - const tree = await treeMember.getAllMembersbyId(id); - const relationships = await relationship.getRelationships(id); - const sharedTree = await sharedTrees.getSharedTreebySender(id); + const tree = await treeMember.getMembersByUser(userIdInt); + const relationships = await relationship.getRelationshipByUser(userIdInt); + const sharedTree = await sharedTrees.getSharedTreebySender(userIdInt); const data = { user, @@ -23,7 +27,7 @@ const backupUser = async (req, res) => { sharedTree }; - await backupInfo.addBackup(id, JSON.stringify(data)); + await backupInfo.addBackup(userIdInt, JSON.stringify(data)); res.json({ message: 'Backup completed' }); @@ -31,7 +35,8 @@ const backupUser = async (req, res) => { catch (error){ console.error(error); res.status(500).json({ - error: 'Error backing up data' + error: 'Error backing up data', + details: error.message }); } }; @@ -39,21 +44,25 @@ const backupUser = async (req, res) => { const restoreUser = async (req, res) => { try{ const {id} = req.params; - const existingUser = await userInfo.findById(id); + // Resolve UUID to integer if needed + const User = require('../models/userModel'); + const userIdInt = await User.resolveUserIdFromAuthUid(id) || id; + + const existingUser = await userInfo.findById(userIdInt); if(!existingUser){ return res.status(404).json({ error: "User not found. No data to restore" }) } - const backup = await backupInfo.getLatestBackup(id); + const backup = await backupInfo.getLatestBackup(userIdInt); if(!backup) { return res.status(404).json({ error: "No backup available" }) } - const backupData = backup.backupData + const backupData = JSON.parse(backup.backupdata || backup.backupData); for( const member of backupData.tree) { const exists= await treeMember.getMemberById(member.id) diff --git a/server/controllers/relationshipController.js b/server/controllers/relationshipController.js index e5b1520..81db440 100644 --- a/server/controllers/relationshipController.js +++ b/server/controllers/relationshipController.js @@ -1,4 +1,6 @@ const Relationship = require('../models/relationshipModel'); +const User = require('../models/userModel'); +const treeMember = require('../models/treeMemberModel'); const getRelationships = async (req, res) => { try{ @@ -43,28 +45,69 @@ const getRelationshipsByOtherUser = async (req,res) => { }; const addRelationship = async (req,res) =>{ - //need to add functionality to refuse a relationship if it already exists -try{ - const {person1_id, person2_id, relationshipType, relationshipStatus, side, userId} = req.body; - const [newRelationship] = await Relationship.addRelationship({ - person1_id, - person2_id, - relationshipType, - relationshipStatus, - side, - userId - }); - res.status(201).json({ - message: 'Relationship added successfully', - member: newRelationship - }); -} catch (error) { - console.error(error); - res.status(500).json({ - error: 'Error adding relationship' - }); -} + try{ + let {person1_id, person2_id, relationshipType, relationshipStatus, side, userId} = req.body; + + console.log('addRelationship received:', {person1_id, person2_id, userId, typeof_person1_id: typeof person1_id}); + + // Resolve person1_id if it's a UUID (user ID) - need to find the member ID for that user + if (typeof person1_id === 'string' && person1_id.includes('-')) { + const userIdInt = await User.resolveUserIdFromAuthUid(person1_id); + if (!userIdInt) { + return res.status(400).json({ error: 'Invalid person1_id: User not found' }); + } + // Find the active member for this user + const member = await treeMember.getActiveMemberId(userIdInt); + if (!member) { + return res.status(400).json({ error: 'No active member found for person1_id user' }); + } + person1_id = member.id; + } + + // Resolve person2_id if it's a UUID (user ID) + if (typeof person2_id === 'string' && person2_id.includes('-')) { + const userIdInt = await User.resolveUserIdFromAuthUid(person2_id); + if (!userIdInt) { + return res.status(400).json({ error: 'Invalid person2_id: User not found' }); + } + // Find the active member for this user + const member = await treeMember.getActiveMemberId(userIdInt); + if (!member) { + return res.status(400).json({ error: 'No active member found for person2_id user' }); + } + person2_id = member.id; + } + + // Resolve userId from UUID to integer + if (userId && typeof userId === 'string' && userId.includes('-')) { + userId = await User.resolveUserIdFromAuthUid(userId); + if (!userId) { + return res.status(400).json({ error: 'Invalid userId: User not found' }); + } + } + + console.log('addRelationship resolved:', {person1_id, person2_id, userId}); + + const newRelationship = await Relationship.addRelationship({ + person1_id, + person2_id, + relationshipType, + relationshipStatus, + side, + userId + }); + res.status(201).json({ + message: 'Relationship added successfully', + member: newRelationship + }); + } catch (error) { + console.error(error); + res.status(500).json({ + error: 'Error adding relationship', + details: error.message + }); + } } const filterBySide = async (req,res) => { diff --git a/server/controllers/sharedTreeController.js b/server/controllers/sharedTreeController.js index 2d0e2c9..0f06de3 100644 --- a/server/controllers/sharedTreeController.js +++ b/server/controllers/sharedTreeController.js @@ -48,8 +48,8 @@ const getSharedTreeByToken = async (req, res) => { const getSharedTreeBySender = async (req, res) => { try{ const { id} = req.params; - const relationships = await sharedTrees.getSharedTreebySender(id); - res.status(200).json(sharedTrees); + const trees = await sharedTrees.getSharedTreebySender(id); + res.status(200).json(trees); } catch(error){ console.error(error); diff --git a/server/controllers/treeInfoController.js b/server/controllers/treeInfoController.js index a55195d..bd66866 100644 --- a/server/controllers/treeInfoController.js +++ b/server/controllers/treeInfoController.js @@ -1,12 +1,15 @@ const treeInfo = require('../models/treeInfoModel'); +const User = require('../models/userModel'); const addObject = async (req, res) => { try { const { object, userId } = req.body; + // Resolve UUID to integer user ID if needed + const userIdInt = await User.resolveUserIdFromAuthUid(userId) || userId; - const [newObject] = await treeInfo.addObject({ + const newObject = await treeInfo.addObject({ object: JSON.stringify(object), - userId: userId + userId: userIdInt }); res.status(201).json({ @@ -63,8 +66,12 @@ const updateObject = async (req, res) => { const getObject = async (req, res) => { try { const { id } = req.params; - - const retrievedObject = await treeInfo.getObject(id); + // Resolve UUID to integer user ID first + const userId = await User.resolveUserIdFromAuthUid(id); + if (!userId) { + return res.status(404).json({ error: 'User not found' }); + } + const retrievedObject = await treeInfo.getObject(userId); if (!retrievedObject) { return res.status(404).json({ error: 'Object not found' @@ -77,6 +84,7 @@ const getObject = async (req, res) => { console.error(error); res.status(500).json({ error: 'Error retrieving tree object', + details: error.message }); } } diff --git a/server/controllers/treeMemberController.js b/server/controllers/treeMemberController.js index 1ecaa08..c4af765 100644 --- a/server/controllers/treeMemberController.js +++ b/server/controllers/treeMemberController.js @@ -1,30 +1,62 @@ const treeMember = require('../models/treeMemberModel'); const relationship = require('../models/relationshipModel'); +const User = require('../models/userModel'); const addTreeMember = async (req, res) => { try { const { firstName, lastName, birthDate, deathDate, location, phoneNumber, relationships, userId, memberUserId } = req.body; + // Resolve UUIDs to integer user IDs - CRITICAL: database requires integers, not UUIDs + console.log('addTreeMember received userId:', userId, typeof userId); + const userIdInt = await User.resolveUserIdFromAuthUid(userId); + console.log('Resolved userIdInt:', userIdInt, typeof userIdInt); + if (!userIdInt) { + return res.status(400).json({ + error: 'Invalid user ID. User not found in database. Please sync your account first.', + received: userId + }); + } + + const memberUserIdInt = memberUserId ? await User.resolveUserIdFromAuthUid(memberUserId) : null; + if (memberUserId && !memberUserIdInt) { + return res.status(400).json({ + error: 'Invalid member user ID. User not found in database.', + received: memberUserId + }); + } + // ensure all necessary fields are passed in the request body - const [newMember] = await treeMember.addMember({ + const newMember = await treeMember.addMember({ firstName, lastName, birthDate, deathDate, location, phoneNumber, - userId, - memberUserId + userId: userIdInt, // Now guaranteed to be an integer + memberUserId: memberUserIdInt // Now guaranteed to be an integer or null }); /// need to fix that a value can be left empty (deathDate) // if there are relationships, add them to the database if (relationships && relationships.length > 0) { for (const rel of relationships) { + // Ensure person2_id is an integer, not a UUID + let person2_id = rel.person2_id; + if (typeof person2_id === 'string' && person2_id.includes('-')) { + // If it looks like a UUID, try to resolve it + person2_id = await User.resolveUserIdFromAuthUid(person2_id); + if (!person2_id) { + console.error('Could not resolve person2_id UUID:', rel.person2_id); + continue; // Skip this relationship + } + } await relationship.addRelationship({ person1_id: newMember.id, - person2_id: rel.person2_id, // Corrected from 'relationship.person2_id' to 'rel.person2_id' - relationship_status: 'active' + person2_id: person2_id, + relationshipType: rel.relationshipType || 'sibling', + relationshipStatus: 'active', + userId: userIdInt // Need to include userId for the relationship }); } } @@ -85,16 +117,21 @@ const editTreeMember = async (req, res) => { const getMembersByUser = async (req,res) =>{ try{ - const { userId } = req.params; - const members = await treeMember.getMembersByUser(userId) + // Resolve UUID to integer user ID first + const userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) { + return res.status(404).json({ error: 'User not found' }); + } + const members = await treeMember.getMembersByUser(userIdInt) console.log(members); res.status(200).json(members); } catch(error){ console.error(error); res.status(500).json({ - error: 'Error fetching members' + error: 'Error fetching members', + details: error.message }); } @@ -103,7 +140,7 @@ const getMembersByUser = async (req,res) =>{ const getMembersByOtherUser = async (req,res) =>{ try{ const { userId} = req.params; - const members = await treeMember.getMembersByOtherUser(userId) + const members = await treeMember.getMembeByOtherUser(userId) res.status(200).json(members); } catch(error){ @@ -129,7 +166,7 @@ const deleteByUser = async (req, res) => { } catch (error){ console.error(error); - res.status(500);json({error:"Error deleting family member"}) + res.status(500).json({error:"Error deleting family member"}) } } @@ -150,14 +187,18 @@ const getMemberById = async (req, res) => { const getActiveMemberId = async (req, res) => { try { const { id } = req.params; - const member = await treeMember.getActiveMemberId(id); - if (!member) { - return res.status(404).json({ error: 'Family member not found' }); + // Resolve UUID to integer user ID first + const userId = await User.resolveUserIdFromAuthUid(id); + if (!userId) { + return res.status(200).json({}); } + const member = await treeMember.getActiveMemberId(userId); + // If none found, return empty object to avoid frontend JSON parse errors + if (!member) return res.status(200).json({}); res.status(200).json(member); } catch (error) { console.error(error); - res.status(500).json({ error: 'Error fetching family member' }); + res.status(500).json({ error: 'Error fetching family member', details: error.message }); } } diff --git a/server/controllers/treeSummaryController.js b/server/controllers/treeSummaryController.js index e07320d..f09611f 100644 --- a/server/controllers/treeSummaryController.js +++ b/server/controllers/treeSummaryController.js @@ -8,18 +8,22 @@ const treeSummary = require('../models/treeSummaryModel'); const updateUserTreeSummary = async (req, res) => { const { userId } = req.params; try { - const members = await treeMember.getMemberByUser(userId); - const relationships = await relationship.getRelationshipByUser(userId); + // Resolve UUID to integer if needed + const User = require('../models/userModel'); + const userIdInt = await User.resolveUserIdFromAuthUid(userId) || userId; + + const members = await treeMember.getMembersByUser(userIdInt); + const relationships = await relationship.getRelationshipByUser(userIdInt); const summary = { members, relationships}; - const existing = treeSummary.getSummaryByUser(userId); + const existing = await treeSummary.getSummaryByUser(userIdInt); if(existing){ - await treeSummary.updateSummary(userId, summary); + await treeSummary.updateSummary(userIdInt, summary); } else{ - await treeSummary.createSummary(userId,summary) + await treeSummary.createSummary(userIdInt, summary); } res.json({ message: 'Tree summary updated' @@ -28,7 +32,8 @@ const updateUserTreeSummary = async (req, res) => { catch (error) { console.error(error); res.status(500).json({ - error: 'Failed to update tree summary' + error: 'Failed to update tree summary', + details: error.message }); } diff --git a/server/db/knex.js b/server/db/knex.js deleted file mode 100644 index cb9e3d3..0000000 --- a/server/db/knex.js +++ /dev/null @@ -1,7 +0,0 @@ -require('dotenv').config() -const knex = require('knex'); -const config = require('../knexfile.js'); - -const db = knex(config.development); - -module.exports = db; diff --git a/server/db/supabase-init.sql b/server/db/supabase-init.sql index 58f104f..46e639b 100644 --- a/server/db/supabase-init.sql +++ b/server/db/supabase-init.sql @@ -63,3 +63,12 @@ create table backups ( backupData json, createdAt timestamp default now() ); + +-- 6. Tree Info Table +create table treeinfo ( + id serial primary key, + userid integer not null references users(id) on delete cascade, + object jsonb, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now() +); \ No newline at end of file diff --git a/server/knexfile.js b/server/knexfile.js deleted file mode 100644 index d49ce6a..0000000 --- a/server/knexfile.js +++ /dev/null @@ -1,23 +0,0 @@ -require('dotenv').config(); - -module.exports = { - development: { - client: 'mysql2', - connection: { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_DATABASE, - port: process.env.DB_PORT || 3306 - }, - migrations: { - directory: './migrations' - }, - seeds: { - directory: './seeds' - } - } -}; - - - diff --git a/server/lib/supabase.js b/server/lib/supabase.js new file mode 100644 index 0000000..56d3147 --- /dev/null +++ b/server/lib/supabase.js @@ -0,0 +1,16 @@ +require('dotenv').config(); +const { createClient } = require('@supabase/supabase-js'); + +// Server-side Supabase client using the Service Role key +// Note: Do not expose the service role key to the client. +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE_KEY, + { + auth: { persistSession: false }, + } +); + +module.exports = supabase; + + diff --git a/server/models/backupModel.js b/server/models/backupModel.js index 506837b..6c3dd16 100644 --- a/server/models/backupModel.js +++ b/server/models/backupModel.js @@ -1,14 +1,48 @@ -const db = require('../db/knex'); +// backupModel.js - model for backups table (Supabase) +const supabase = require('../lib/supabase'); const backup = { - addBackup: async(user, data) => { - return db('backups').insert({'userId': user, 'backupData': data}); + addBackup: async(userId, data) => { + // Resolve UUID to integer if needed + let userIdInt = userId; + if (typeof userId === 'string' && userId.includes('-')) { + const User = require('./userModel'); + userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) throw new Error('User not found'); + } + const { data: inserted, error } = await supabase + .from('backups') + .insert([{ userid: userIdInt, backupdata: data }]) + .select('*') + .single(); + if (error) throw error; + return inserted; }, getBackups: async (id) => { - return db('backups').where({id}); + const { data, error } = await supabase + .from('backups') + .select('*') + .eq('backupid', id); + if (error) throw error; + return data; }, - getLatestBackup: async (id) => { - return db('backups').where('backupId', id).orderBy('createdAt', 'desc').first(); + getLatestBackup: async (userId) => { + // Resolve UUID to integer if needed + let userIdInt = userId; + if (typeof userId === 'string' && userId.includes('-')) { + const User = require('./userModel'); + userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) throw new Error('User not found'); + } + const { data, error } = await supabase + .from('backups') + .select('*') + .eq('userid', userIdInt) + .order('createdat', { ascending: false }) + .limit(1) + .maybeSingle(); + if (error) throw error; + return data; } }; diff --git a/server/models/relationshipModel.js b/server/models/relationshipModel.js index cc8dd89..5f03682 100644 --- a/server/models/relationshipModel.js +++ b/server/models/relationshipModel.js @@ -1,36 +1,85 @@ -const db = require('../db/knex'); +// relationshipModel.js - the model for the relationships table +// this file was replaced with the supabase model +const supabase = require('../lib/supabase'); +// all functions for the relationship to interact with the database const Relationships = { addRelationship: async (data) => { - return db('relationships').insert(data); + // Map camelCase to lowercase column names for Postgres + const mappedData = { + person1_id: data.person1_id || data.person1Id, + person2_id: data.person2_id || data.person2Id, + relationshiptype: data.relationshipType || data.relationshiptype, + relationshipstatus: data.relationshipStatus || data.relationshipstatus, + side: data.side, + userid: data.userId || data.userid, + }; + // Remove undefined/null values + Object.keys(mappedData).forEach(key => mappedData[key] === undefined && delete mappedData[key]); + const { data: inserted, error } = await supabase + .from('relationships') + .insert([ mappedData ]) + .select('*') + .single(); + if (error) throw error; + return inserted; }, - getRelationships:async (personId) => { - return db('relationships').where('person1_id', personId).orWhere('person2_id', personId); + getRelationships: async (personId) => { + // person1_id = personId OR person2_id = personId + const { data, error } = await supabase + .from('relationships') + .select('*') + .or(`person1_id.eq.${personId},person2_id.eq.${personId}`); + if (error) throw error; + return data; }, - filterBySide: async(personId, side) => { - return db('relationships').where('person1_id', personId).andWhere('side',side); + filterBySide: async (personId, side) => { + const { data, error } = await supabase + .from('relationships') + .select('*') + .eq('person1_id', personId) + .eq('side', side); + if (error) throw error; + return data; }, getRelationshipbyId: async (personId) => { - return db('relationships').where('person1_id', personId).andWhere('person2_id', personId); + const { data, error } = await supabase + .from('relationships') + .select('*') + .eq('person1_id', personId) + .eq('person2_id', personId); + if (error) throw error; + return data; }, getRelationshipByUser: async (userId) => { - return db('relationship').where('userId', userId).select('*'); + const { data, error } = await supabase + .from('relationships') + .select('*') + .eq('userid', userId); + if (error) throw error; + return data; }, getRelationshipByOtherUser: async (userId) => { - return db('relationship').whereNot('userId', userId).select('*'); + const { data, error } = await supabase + .from('relationships') + .select('*') + .not('userid', 'eq', userId); + if (error) throw error; + return data; }, deleteByUser: async (userId) => { - return db('relationship').where({userId}).del(); + const { error } = await supabase + .from('relationships') + .delete() + .eq('userid', userId); + if (error) throw error; } - - - }; module.exports = Relationships; diff --git a/server/models/sharedTreeModel.js b/server/models/sharedTreeModel.js index 6ac65ec..8c128f8 100644 --- a/server/models/sharedTreeModel.js +++ b/server/models/sharedTreeModel.js @@ -1,53 +1,65 @@ -const db = require('../db/knex'); -const Relationships = require('./relationshipModel'); +// sharedTreeModel.js - the model for the sharedTrees table +// this file was replaced with the supabase model +const supabase = require('../lib/supabase'); -const sharedTrees ={ - addSharedTree: async(data) => { - return db('sharedTrees').insert(data, ['id', 'token']) +// all functions for the sharedTree to interact with the database +const sharedTrees = { + addSharedTree: async (data) => { + // Table and column names are lowercase in Postgres + const { data: inserted, error } = await supabase + .from('sharedtrees') + .insert([ data ]) + .select('*') + .single(); + if (error) throw error; + return inserted; }, getALLSharedTree: async () => { - return db('sharedTrees').select('*'); + const { data, error } = await supabase + .from('sharedtrees') + .select('*'); + if (error) throw error; + return data; }, - - getSharedTreeById: async(id) =>{ - return db('sharedTrees').where('sharedTreeID',id).first(); + getSharedTreeById: async (id) => { + const { data, error } = await supabase + .from('sharedtrees') + .select('*') + .eq('sharedtreeid', id) + .single(); + if (error) throw error; + return data; }, - getSharedTreebySender: async(id) => { - return db('sharedTrees').where('senderId', id); + getSharedTreebySender: async (id) => { + const { data, error } = await supabase + .from('sharedtrees') + .select('*') + .eq('senderid', id); + if (error) throw error; + return data; }, - getSharedTreebyReciever: async(id) => { - return db('sharedTrees').where({ recieverId: id }).select('*'); + getSharedTreebyReciever: async (id) => { + const { data, error } = await supabase + .from('sharedtrees') + .select('*') + .eq('recieverid', id); + if (error) throw error; + return data; }, getSharedTreeByToken: async (token) => { - return db('sharedTrees').where('token', token).first(); - }, - - shareTree: async(data) => { - return db('relationship').where('person1_id', personId).andWhere('side','side'); - }, - - mergeTree: async(id, data) => { - for (const member of data){ - await db('treeMembers').insert({ - owner_id: recieverID, - name : member.name, - relationship: member.relationship, - }); - } - return {message: "Members merged successfully"}; - - }, - - getMemberstoMerge: async(senderId, recieverId) => { - return db('sharedTrees').where(senderId, senderId).select('*'); + const { data, error } = await supabase + .from('sharedtrees') + .select('*') + .eq('token', token) + .maybeSingle(); + if (error) throw error; + return data; } - - }; module.exports = sharedTrees; \ No newline at end of file diff --git a/server/models/treeInfoModel.js b/server/models/treeInfoModel.js index 82627e1..7ef2122 100644 --- a/server/models/treeInfoModel.js +++ b/server/models/treeInfoModel.js @@ -1,18 +1,36 @@ -const db = require('../db/knex'); +// treeInfoModel.js - model for treeInfo table (Supabase) +const supabase = require('../lib/supabase'); const treeInfo = { addObject: async (data) => { - return db('treeInfo').insert(data, ['id']); + const { data: inserted, error } = await supabase + .from('treeinfo') + .insert([ data ]) + .select('*') + .single(); + if (error) throw error; + return inserted; }, - updateObject: async (id, data) => { - await db('treeInfo').where({ userId: id }).update(data); - const updatedObject = await db('treeInfo').where({ id }).first(); - return updatedObject; + updateObject: async (userId, data) => { + const { data: updated, error } = await supabase + .from('treeinfo') + .update(data) + .eq('userid', userId) + .select('*') + .single(); + if (error) throw error; + return updated; }, - getObject: async (id) => { - return db('treeInfo').where({ userId: id }).first(); + getObject: async (userId) => { + const { data, error } = await supabase + .from('treeinfo') + .select('*') + .eq('userid', userId) + .maybeSingle(); + if (error) throw error; + return data; }, }; diff --git a/server/models/treeMemberModel.js b/server/models/treeMemberModel.js index 459f6a5..5badff6 100644 --- a/server/models/treeMemberModel.js +++ b/server/models/treeMemberModel.js @@ -1,53 +1,148 @@ -const db = require('../db/knex'); +// treeMemberModel.js - the model for the treeMembers table +// this file was replaced with the supabase model +const supabase = require('../lib/supabase'); +// all functions for the treeMember to interact with the database const treeMember = { addMember: async (data) => { - return db('treeMembers').insert(data, ['id']); + // Map camelCase to lowercase column names for Postgres + const mappedData = { + firstname: data.firstName || data.firstname, + lastname: data.lastName || data.lastname, + birthdate: data.birthDate || data.birthdate, + deathdate: data.deathDate || data.deathdate, + location: data.location, + phonenumber: data.phoneNumber || data.phonenumber, + userid: data.userId || data.userid, + memberuserid: data.memberUserId || data.memberuserid, + }; + // Remove undefined/null values + Object.keys(mappedData).forEach(key => mappedData[key] === undefined && delete mappedData[key]); + + // Validate that userid is an integer (not a UUID) + if (mappedData.userid && (typeof mappedData.userid === 'string' && mappedData.userid.includes('-'))) { + throw new Error(`Invalid userid: expected integer, got UUID: ${mappedData.userid}`); + } + if (mappedData.memberuserid && (typeof mappedData.memberuserid === 'string' && mappedData.memberuserid.includes('-'))) { + throw new Error(`Invalid memberuserid: expected integer, got UUID: ${mappedData.memberuserid}`); + } + + console.log('addMember mappedData:', JSON.stringify(mappedData, null, 2)); + const { data: inserted, error } = await supabase + .from('treemembers') + .insert([ mappedData ]) + .select('id') + .single(); + if (error) { + console.error('addMember Supabase error:', error); + throw error; + } + return inserted; }, getAllMembers: async () => { - return db('treeMembers').select('*'); + const { data, error } = await supabase + .from('treemembers') + .select('*'); + if (error) throw error; + return data; }, - getAllMembersbyId: async (id) => { - return db('treeMembers').where({id}).select('*'); + const { data, error } = await supabase + .from('treemembers') + .select('*') + .eq('id', id); + if (error) throw error; + return data; }, - getMemberById: async (id) => { // Fixed the typo - return db('treeMembers').where({ id }).first(); + getMemberById: async (id) => { + const { data, error } = await supabase + .from('treemembers') + .select('*') + .eq('id', id) + .single(); + if (error) throw error; + return data; }, getMembersByUser: async (userId) => { - return db('treeMembers').where({userId}).select('*'); + const { data, error } = await supabase + .from('treemembers') + .select('*') + .eq('userid', userId); + if (error) throw error; + return data; }, getMembeByOtherUser: async (userId) => { - return db('treeMembers').whereNot({userId}).select('*'); - + const { data, error } = await supabase + .from('treemembers') + .select('*') + .not('userid', 'eq', userId); + if (error) throw error; + return data; }, + // i cant get this to workkkkkkk updateMemberInfo: async (id, data) => { - await db('treeMembers').where({ id }).update(data); - const updatedRecord = await db('treeMembers').where({ id }).first(); - return updatedRecord; + // Map camelCase to lowercase column names for Postgres + const mappedData = {}; + if (data.firstName !== undefined) mappedData.firstname = data.firstName; + if (data.lastName !== undefined) mappedData.lastname = data.lastName; + if (data.birthDate !== undefined) mappedData.birthdate = data.birthDate; + if (data.deathDate !== undefined) mappedData.deathdate = data.deathDate; + if (data.location !== undefined) mappedData.location = data.location; + if (data.phoneNumber !== undefined) mappedData.phonenumber = data.phoneNumber; + if (data.userId !== undefined) mappedData.userid = data.userId; + if (data.memberUserId !== undefined) mappedData.memberuserid = data.memberUserId; + // Also handle lowercase variants + if (data.firstname !== undefined) mappedData.firstname = data.firstname; + if (data.lastname !== undefined) mappedData.lastname = data.lastname; + if (data.birthdate !== undefined) mappedData.birthdate = data.birthdate; + if (data.deathdate !== undefined) mappedData.deathdate = data.deathdate; + if (data.phonenumber !== undefined) mappedData.phonenumber = data.phonenumber; + if (data.userid !== undefined) mappedData.userid = data.userid; + if (data.memberuserid !== undefined) mappedData.memberuserid = data.memberuserid; + const { data: updated, error } = await supabase + .from('treemembers') + .update(mappedData) + .eq('id', id) + .select('*') + .single(); + if (error) throw error; + return updated; }, + assignNewMemberRelationship: async (recieverId, getMemberById, relationshipType) => { - return db('treeMembers').where({person1_id: recieverId, person2_id: recieverId}).update({relationshipType: relationshipType}) + // Update an existing relationship record tying two members together + const { error } = await supabase + .from('relationships') + .update({ relationshipType }) + .match({ person1_id: recieverId, person2_id: getMemberById }); + if (error) throw error; + return { success: true }; }, - deleteByUser: async (userId) => { - return db('treeMembers').where({userId}).del(); + const { error } = await supabase + .from('treemembers') + .delete() + .eq('userid', userId); + if (error) throw error; }, getActiveMemberId: async (id) => { - // userId and memberUserId are both equal to the id - return db('treeMembers').where({userId: id, memberUserId: id}).first(); - + const { data, error } = await supabase + .from('treemembers') + .select('*') + .eq('userid', id) + .eq('memberuserid', id) + .maybeSingle(); + if (error) throw error; + return data; } - - }; module.exports = treeMember; diff --git a/server/models/treeSummaryModel.js b/server/models/treeSummaryModel.js index f83847d..f82b830 100644 --- a/server/models/treeSummaryModel.js +++ b/server/models/treeSummaryModel.js @@ -1,16 +1,58 @@ -const db = require('../db/knex'); +// treeSummaryModel.js - model for tree summaries (Supabase) +// Note: This table may need to be created in Supabase if it doesn't exist +const supabase = require('../lib/supabase'); const treeSummary = { getSummaryByUser: async (userId) => { - return db('userTreeSummaries').where({userId}).first(); + // Resolve UUID to integer if needed + let userIdInt = userId; + if (typeof userId === 'string' && userId.includes('-')) { + const User = require('./userModel'); + userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) return null; + } + const { data, error } = await supabase + .from('usertreesummaries') + .select('*') + .eq('userid', userIdInt) + .maybeSingle(); + if (error) throw error; + return data; }, - createSummary: async (userId, userData) =>{ - return db('userTreeSummaries').insert({'userId': userId, 'currentTreeSummary': userData}); + createSummary: async (userId, userData) => { + // Resolve UUID to integer if needed + let userIdInt = userId; + if (typeof userId === 'string' && userId.includes('-')) { + const User = require('./userModel'); + userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) throw new Error('User not found'); + } + const { data, error } = await supabase + .from('usertreesummaries') + .insert([{ userid: userIdInt, currenttreesummary: userData }]) + .select('*') + .single(); + if (error) throw error; + return data; }, - updateSummary: async (userId, userData) =>{ - return db('userTreeSummaries').where({userId}).update({'currentTreeSummary': userData}); + updateSummary: async (userId, userData) => { + // Resolve UUID to integer if needed + let userIdInt = userId; + if (typeof userId === 'string' && userId.includes('-')) { + const User = require('./userModel'); + userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) throw new Error('User not found'); + } + const { data, error } = await supabase + .from('usertreesummaries') + .update({ currenttreesummary: userData }) + .eq('userid', userIdInt) + .select('*') + .single(); + if (error) throw error; + return data; } }; diff --git a/server/models/userModel.js b/server/models/userModel.js index 8e2aa0a..b7c5f7d 100644 --- a/server/models/userModel.js +++ b/server/models/userModel.js @@ -1,34 +1,112 @@ -// deprecated if we use supabase i believe - -const db = require('../db/knex'); -const { get } = require('../routes/treeMemberRoute'); +// userModel.js - the model for the user table +// this file was replaced with the supabase model +const supabase = require('../lib/supabase'); +// all functions for the user to interact with the database const User = { register: async (userData) => { - return db('users').insert(userData, ['id', 'firstName', 'lastName', 'email']); + const { data, error } = await supabase + .from('users') + .insert([ userData ]) + .select('id, firstname, lastname, email, phonenumber, birthdate') + .single(); + if (error) throw error; + return data; }, findByEmail: async (email) => { - return db('users').where({email}).first(); + const { data, error } = await supabase + .from('users') + .select('*') + .eq('email', email) + .maybeSingle(); + if (error) throw error; + return data; }, findById: async (id) => { - return db('users').where({id}).first(); + const { data, error } = await supabase + .from('users') + .select('*') + .eq('id', id) + .single(); + if (error) throw error; + return data; }, - updateUserInfo: async(id, userData) => { - return db('users').insert(userData, '').where({id}).first().insert(userData, []); + updateUserInfo: async (id, userData) => { + const { data, error } = await supabase + .from('users') + .update(userData) + .eq('id', id) + .select('*') + .single(); + if (error) throw error; + return data; }, - deleteUser: async (id) => { - return db('users').where({id}).del(); + const { error } = await supabase + .from('users') + .delete() + .eq('id', id); + if (error) throw error; }, getAllUsers: async () => { - return db('users').select('*'); + const { data, error } = await supabase + .from('users') + .select('*'); + if (error) throw error; + return data; + }, + + findByAuthUid: async (authUid) => { + const { data, error } = await supabase + .from('users') + .select('*') + .eq('auth_uid', authUid) + .maybeSingle(); + if (error) throw error; + return data; }, + upsertByAuthUser: async ({ auth_uid, email, username, firstName, lastName, phoneNumber, birthDate }) => { + // Map to lowercase columns and drop null/undefined so we don't overwrite with nulls + const rawPayload = { + auth_uid, + email, + username, + firstname: firstName, + lastname: lastName, + phonenumber: phoneNumber, + birthdate: birthDate, + }; + const payload = Object.fromEntries( + Object.entries(rawPayload).filter(([_, v]) => v !== undefined && v !== null && v !== '') + ); + const { data, error } = await supabase + .from('users') + .upsert([ payload ], { onConflict: 'auth_uid' }) + .select('id, auth_uid, email, username, firstname, lastname, phonenumber, birthdate') + .single(); + if (error) throw error; + return data; + }, }; +// Helper to resolve UUID (auth_uid) to integer user ID +const resolveUserIdFromAuthUid = async (authUidOrIntId) => { + // If it's already an integer, return it + if (!isNaN(authUidOrIntId) && !authUidOrIntId.toString().includes('-')) { + return parseInt(authUidOrIntId); + } + // Otherwise look up by auth_uid + const user = await User.findByAuthUid(authUidOrIntId); + if (!user) return null; + return user.id; +}; + +User.resolveUserIdFromAuthUid = resolveUserIdFromAuthUid; + module.exports = User; \ No newline at end of file diff --git a/server/mysql-connection.js b/server/mysql-connection.js deleted file mode 100644 index 191170d..0000000 --- a/server/mysql-connection.js +++ /dev/null @@ -1,43 +0,0 @@ -require('dotenv').config(); -const mysql = require('mysql2'); - -console.log('Database Config:', process.env.DB_USER, process.env.DB_PASSWORD, process.env.DB_DATABASE); - -const connection = mysql.createConnection({ - host: process.env.DB_HOST, // localhost - user: process.env.DB_USER, // Make sure DB_USER is set - password: process.env.DB_PASSWORD, // Ensure DB_PASSWORD is set - database: process.env.DB_DATABASE, // Ensure DB_DATABASE is set - port: process.env.DB_PORT || 3306 // Port should be 3306 -}); - -connection.connect((err) => { - if (err) { - console.error('Error connecting to MySQL:', err.stack); - return; - } - - console.log('Connected to MySQL as id ' + connection.threadId); - - // Example query to check connection - - connection.query('SELECT DATABASE()', (err, results) => { - if (err) { - console.error('Error running query:', err.stack); - return; - } - console.log('Connected to the database:', results[0]['DATABASE()']); - }); - - connection.query('SHOW DATABASES', (err, results) => { - if (err) { - console.error('Error fetching databases:', err); - } else { - console.log('Databases:', results.map(db => db.Database)); - } - connection.end(); // Close the connection - }); - - // Close the connection - connection.end(); -}); diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index d9ae2a1..4b3636e 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -2,12 +2,13 @@ const express = require('express'); const router = express.Router(); -const { deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); +const { deleteByUser, findByEmail, findById, getAllUsers, syncAuthUser } = require('../controllers/authController'); router.delete('/remove/:id', deleteByUser); router.get('/user/:id', findById); router.get('/user/email/:email', findByEmail); router.get('/users', getAllUsers); +router.post('/sync', syncAuthUser); module.exports = router; diff --git a/server/server.js b/server/server.js index 7cf76c9..f2539c3 100644 --- a/server/server.js +++ b/server/server.js @@ -1,31 +1,19 @@ // server.js const express = require('express'); -const knex = require('knex'); const dotenv = require('dotenv'); const cors = require('cors'); -const knexConfig = require('./knexfile'); const authRoutes = require('./routes/authRoutes'); -const treeMemberRoutes = require('./routes/treeMemberRoute'); // Fixed typo -const relationshipRoutes = require('./routes/relationshipRoutes'); // Fixed typo +const treeMemberRoutes = require('./routes/treeMemberRoute'); +const relationshipRoutes = require('./routes/relationshipRoutes'); const sharedTreeRoutes = require('./routes/sharedTreeRoutes'); const backupRoutes = require('./routes/backupRoutes'); -const treeInfoRoutes = require('./routes/treeInfoRoutes'); // Fixed typo +const treeInfoRoutes = require('./routes/treeInfoRoutes'); dotenv.config(); const app = express(); const port = process.env.PORT || 5000; -const db = knex({ - client: 'mysql2', - connection: { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME - } -}); - app.use(express.json()); app.use(cors()); @@ -39,20 +27,3 @@ app.use('/api/tree-info', treeInfoRoutes); app.listen(port, () => { console.log(`Server running on port ${port}`); }); - - -// Example route -- follow this template for other routes - -/* -app.get('/api/items', async (req, res) => { - try { - const items = await db('items').select('*'); - res.json(items); - } catch (error) { - res.status(500).json({ error: 'An error occurred' }); - } - }); - -*/ - - From 2f16191547bbd15e329eaab118ce6c6787ae1c16 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Fri, 31 Oct 2025 01:43:21 -0500 Subject: [PATCH 18/86] sb setup added to readme --- README.md | 30 +++++++----------------------- docs/.env.example | 12 +++++++----- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 733eb52..e3b34c1 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,14 @@ The current KinTree project team as of Fall 2025 includes Andrea Ambrose, Matthe ### Prerequisites -Node.js (install the correct version for your own OS [here](https://nodejs.org/en)) and install MySQL [here](https://dev.mysql.com/downloads/mysql/). Set up account information through the Configurator application or through the terminal. +Node.js (install the correct version for your own OS [here](https://nodejs.org/en)) and get your Supabase URL, CLIENT KEY, and SERVICE KEY from your project dashboard [here](https://supabase.com/) -### Setup +### Database Setup + +In the /server/ folder, add an .env file with variables `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY`. +In the /client/ folder, add an .env file with variables `REACT_APP_SUPABASE_URL` and `REACT_APP_SUPABASE_ANON_KEY`. + +### Web Application Setup To set up the KinTree codebase on your own machine, start by cloning the repository to your local file system. @@ -34,24 +39,3 @@ Then, from the same directory, run the following command to run the server/API: `node server.js` -### Database Setup - -Open the MySQL Client Terminal, login with your password to run the mySQL server. - -Create a new database instance on your machine: -`CREATE DATABASE ` - -In the /server/ directory, create a .env file with MySQL information. Example env is in the project's /docs/ folder. - -Open another command line window in /SeniorProject_KinTree/server/ and run the command `npm install knex mysql2` to install Knex and mySQL2. - -Verify the connection: - -`node mysql-connection.js` - -Ensure proper migration files are loaded: - -`npx knex:migrate status` - -Run the command `npx knex migrate:latest` to create and/or update existing database tables. - diff --git a/docs/.env.example b/docs/.env.example index 2814bb3..f25ad8d 100644 --- a/docs/.env.example +++ b/docs/.env.example @@ -1,5 +1,7 @@ -DB_HOST=localhost -DB_PORT=3306 -DB_USER=root -DB_PASSWORD=my_sql_password -DB_DATABASE=my_database_name +# SERVER ENV +SUPABASE_URL= +SUPABASE_SERVICE_ROLE_KEY= + +# CLIENT ENV +REACT_APP_SUPABASE_URL= +REACT_APP_SUPABASE_ANON_KEY= \ No newline at end of file From 05eb11bf0b711afcafcf62d2b8d519d94b025c1b Mon Sep 17 00:00:00 2001 From: MatthewLoyed Date: Mon, 3 Nov 2025 17:13:18 -0600 Subject: [PATCH 19/86] Made signout button more visible and change request redirects to login. --- .../src/pages/CreateAccount/CreateAccount.js | 2 +- .../pages/WebsiteSettings/WebsiteSettings.js | 9 +++++-- client/src/pages/WebsiteSettings/styles.js | 27 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/client/src/pages/CreateAccount/CreateAccount.js b/client/src/pages/CreateAccount/CreateAccount.js index 9139b6c..ef1e39e 100644 --- a/client/src/pages/CreateAccount/CreateAccount.js +++ b/client/src/pages/CreateAccount/CreateAccount.js @@ -74,7 +74,7 @@ const CreateAccount = () => { // }); console.log('Registration successful:', user); - window.location.href = '/home'; // redirect after registration to login, can change to login if we want + window.location.href = '/login'; // redirect after registration to login } catch (error) { setErrorMessage(error.message); console.error('Password:', data.password); diff --git a/client/src/pages/WebsiteSettings/WebsiteSettings.js b/client/src/pages/WebsiteSettings/WebsiteSettings.js index 7647add..a9936b2 100644 --- a/client/src/pages/WebsiteSettings/WebsiteSettings.js +++ b/client/src/pages/WebsiteSettings/WebsiteSettings.js @@ -252,8 +252,13 @@ function WebsiteSettings() {
    -
    - +
    +
    diff --git a/client/src/pages/WebsiteSettings/styles.js b/client/src/pages/WebsiteSettings/styles.js index 5a08a9b..6aa3b72 100644 --- a/client/src/pages/WebsiteSettings/styles.js +++ b/client/src/pages/WebsiteSettings/styles.js @@ -63,4 +63,31 @@ export const Input = { export const ToggleSwitch = { marginLeft: '10px', transform: 'scale(1.2)' +}; + +export const SignOutContainer = { + display: 'flex', + justifyContent: 'flex-end', + padding: '20px', + marginTop: '30px', + borderTop: '1px solid #e0e0e0' +}; + +export const SignOutButton = { + backgroundColor: '#dc3545', + color: 'white', + border: 'none', + borderRadius: '8px', + padding: '12px 24px', + fontSize: '16px', + fontWeight: '600', + cursor: 'pointer', + boxShadow: '0 2px 4px rgba(220, 53, 69, 0.2)', + transition: 'all 0.2s ease', + minWidth: '120px' +}; + +export const SignOutButtonHover = { + backgroundColor: '#c82333', + boxShadow: '0 4px 8px rgba(220, 53, 69, 0.3)' }; \ No newline at end of file From cb5d56ec8df4fd052782ae4734b3cc9022c91e1c Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Sat, 8 Nov 2025 15:00:10 -0600 Subject: [PATCH 20/86] Squashed commit of the following: commit 5315e049f5602a4d1eb3fed3abe518fd4b3917f5 Merge: 153b469 fcc335f Author: Andrea Ambrose <156560539+aambrose1@users.noreply.github.com> Date: Mon Nov 3 20:21:30 2025 -0600 Merge pull request #16 from aambrose1/feature/supabase-integration Supabase Feature Integration commit fcc335f04543b34e4e79ad1b74be86df33d14373 Merge: 05eb11b 2f16191 Author: MatthewLoyed Date: Mon Nov 3 17:14:54 2025 -0600 Merge branch 'feature/supabase-integration' of https://github.com/aambrose1/SeniorProject_KinTree into feature/supabase-integration commit 05eb11bf0b711afcafcf62d2b8d519d94b025c1b Author: MatthewLoyed Date: Mon Nov 3 17:13:18 2025 -0600 Made signout button more visible and change request redirects to login. commit 2f16191547bbd15e329eaab118ce6c6787ae1c16 Author: Andrea Ambrose Date: Fri Oct 31 01:43:21 2025 -0500 sb setup added to readme commit 2e47a71b309fd5782bdd3425c20cbd3f95acc19c Author: MatthewLoyed Date: Thu Oct 30 18:06:34 2025 -0500 Migrated backend to Supabase. Deleted Knex but still use Express. commit ccd4a121f06b7f6333d2e7d9368ed2b3e772e972 Author: MatthewLoyed Date: Thu Oct 30 14:51:38 2025 -0500 Implemented MFA, Session, OAuth, and Password Reset. commit 18f4ecc121cd153e4d5d08b24372d89831191db4 Author: MatthewLoyed Date: Mon Oct 20 17:16:48 2025 -0500 Add login button to Register page. commit cb883e674c58fe1b2b0d7fb5cac2e196138b57c7 Author: MatthewLoyed Date: Sun Oct 19 19:38:54 2025 -0500 Successfully integrated Supabase Registration, Login, and Signout. Still need to store additional signup info in mySql. --- README.md | 30 +-- client/package-lock.json | 130 ++++++++++- client/package.json | 1 + client/src/CurrentUserProvider.js | 90 ++++++-- .../ProtectedRoute/ProtectedRoute.js | 4 +- client/src/index.js | 12 + client/src/pages/Account/Account.js | 145 +++++++++--- .../src/pages/CreateAccount/CreateAccount.js | 168 ++++++-------- client/src/pages/Home/Home.js | 2 +- client/src/pages/Login/Login.js | 191 ++++++++++++---- client/src/pages/Reset/Reset.js | 62 +++++- client/src/pages/Reset/UpdatePassword.js | 59 +++++ .../pages/WebsiteSettings/WebsiteSettings.js | 209 +++++++++++++++++- client/src/pages/WebsiteSettings/styles.js | 27 +++ client/src/utils/auth.js | 76 +++++++ client/src/utils/authHandlers.js | 75 +++++++ client/src/utils/supabaseClient.js | 6 + docs/.env.example | 12 +- server/controllers/authController.js | 100 +++------ server/controllers/backupController.js | 29 ++- server/controllers/relationshipController.js | 85 +++++-- server/controllers/sharedTreeController.js | 4 +- server/controllers/treeInfoController.js | 16 +- server/controllers/treeMemberController.js | 70 ++++-- server/controllers/treeSummaryController.js | 17 +- server/db/knex.js | 7 - server/db/supabase-init.sql | 74 +++++++ server/knexfile.js | 23 -- server/lib/supabase.js | 16 ++ .../20250416174536_add_user_tree_table.js | 2 +- server/models/backupModel.js | 46 +++- server/models/relationshipModel.js | 75 +++++-- server/models/sharedTreeModel.js | 84 ++++--- server/models/treeInfoModel.js | 34 ++- server/models/treeMemberModel.js | 137 ++++++++++-- server/models/treeSummaryModel.js | 54 ++++- server/models/userModel.js | 100 ++++++++- server/mysql-connection.js | 43 ---- server/package-lock.json | 148 +++++++++++++ server/package.json | 1 + server/routes/authRoutes.js | 7 +- server/server.js | 35 +-- 42 files changed, 1922 insertions(+), 584 deletions(-) create mode 100644 client/src/pages/Reset/UpdatePassword.js create mode 100644 client/src/utils/auth.js create mode 100644 client/src/utils/authHandlers.js create mode 100644 client/src/utils/supabaseClient.js delete mode 100644 server/db/knex.js create mode 100644 server/db/supabase-init.sql delete mode 100644 server/knexfile.js create mode 100644 server/lib/supabase.js delete mode 100644 server/mysql-connection.js diff --git a/README.md b/README.md index 733eb52..e3b34c1 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,14 @@ The current KinTree project team as of Fall 2025 includes Andrea Ambrose, Matthe ### Prerequisites -Node.js (install the correct version for your own OS [here](https://nodejs.org/en)) and install MySQL [here](https://dev.mysql.com/downloads/mysql/). Set up account information through the Configurator application or through the terminal. +Node.js (install the correct version for your own OS [here](https://nodejs.org/en)) and get your Supabase URL, CLIENT KEY, and SERVICE KEY from your project dashboard [here](https://supabase.com/) -### Setup +### Database Setup + +In the /server/ folder, add an .env file with variables `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY`. +In the /client/ folder, add an .env file with variables `REACT_APP_SUPABASE_URL` and `REACT_APP_SUPABASE_ANON_KEY`. + +### Web Application Setup To set up the KinTree codebase on your own machine, start by cloning the repository to your local file system. @@ -34,24 +39,3 @@ Then, from the same directory, run the following command to run the server/API: `node server.js` -### Database Setup - -Open the MySQL Client Terminal, login with your password to run the mySQL server. - -Create a new database instance on your machine: -`CREATE DATABASE ` - -In the /server/ directory, create a .env file with MySQL information. Example env is in the project's /docs/ folder. - -Open another command line window in /SeniorProject_KinTree/server/ and run the command `npm install knex mysql2` to install Knex and mySQL2. - -Verify the connection: - -`node mysql-connection.js` - -Ensure proper migration files are loaded: - -`npx knex:migrate status` - -Run the command `npx knex migrate:latest` to create and/or update existing database tables. - diff --git a/client/package-lock.json b/client/package-lock.json index ec3b1a8..8b74732 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^4.1.3", + "@supabase/supabase-js": "^2.75.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -3700,6 +3701,123 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz", + "integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz", + "integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@supabase/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@supabase/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz", + "integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz", + "integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/realtime-js/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz", + "integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.75.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz", + "integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.75.0", + "@supabase/functions-js": "2.75.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.75.0", + "@supabase/realtime-js": "2.75.0", + "@supabase/storage-js": "2.75.0" + } + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -4765,6 +4883,12 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/prettier": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", @@ -4896,9 +5020,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", "dependencies": { "@types/node": "*" diff --git a/client/package.json b/client/package.json index 0263d7f..f682f3d 100644 --- a/client/package.json +++ b/client/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@hookform/resolvers": "^4.1.3", + "@supabase/supabase-js": "^2.75.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/client/src/CurrentUserProvider.js b/client/src/CurrentUserProvider.js index 45a788e..510cc5e 100644 --- a/client/src/CurrentUserProvider.js +++ b/client/src/CurrentUserProvider.js @@ -1,13 +1,14 @@ import { React, useState, createContext, useContext, useEffect } from "react" -import { set } from "react-hook-form"; +import { supabase } from "./utils/supabaseClient"; export const currentContext = createContext(); export const CurrentUserProvider = ({ children }) => { const [currentUserID, setCurrentUserIDState] = useState(''); - const [currentAccountID, setCurrentAccountIDState] = useState(''); // TODO login will set this + const [currentAccountID, setCurrentAccountIDState] = useState(''); const [currentUserName, setCurrentUserNameState] = useState(''); const [loading, setLoading] = useState(true); + const [supabaseUser, setSupabaseUser] = useState(null); const setCurrentAccountID = (accountID) => { // logging in will trigger this localStorage.setItem("currentAccountID", accountID); @@ -52,29 +53,80 @@ export const CurrentUserProvider = ({ children }) => { } - // init + // Initialize Supabase auth state useEffect(() => { - const initializeState = () => { - const storedAccountID = localStorage.getItem("currentAccountID"); - const storedUserID = localStorage.getItem("currentUserID"); - const storedUserName = localStorage.getItem("currentUserName"); - - if (storedAccountID) { - setCurrentAccountIDState(storedAccountID); - } - if (storedUserID) { - setCurrentUserIDState(storedUserID); - } - if (storedUserName) { - setCurrentUserNameState(storedUserName); + const initializeAuth = async () => { + try { + // Get initial session + const { data: { session } } = await supabase.auth.getSession(); + + if (session?.user) { + setSupabaseUser(session.user); + setCurrentAccountIDState(session.user.id); + setCurrentUserNameState(session.user.email); // Use email as default username + } + + setLoading(false); + } catch (error) { + console.error('Error initializing auth:', error); + setLoading(false); } - setLoading(false); }; - initializeState(); + + initializeAuth(); + + // Listen for auth changes + const { data: { subscription } } = supabase.auth.onAuthStateChange( + async (event, session) => { + if (session?.user) { + setSupabaseUser(session.user); + setCurrentAccountIDState(session.user.id); + setCurrentUserNameState(session.user.email); + // Auto-sync profile into public.users using auth metadata when available + try { + const m = session.user.user_metadata || {}; + await fetch('http://localhost:5000/api/auth/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + auth_uid: session.user.id, + email: session.user.email, + username: session.user.email, + firstName: m.firstName || m.first_name || null, + lastName: m.lastName || m.last_name || null, + phoneNumber: m.phoneNumber || m.phone_number || m.phonenum || null, + birthDate: m.birthDate || m.birthdate || null, + }) + }); + } catch (e) { + console.warn('Auth sync failed:', e?.message || e); + } + } else { + setSupabaseUser(null); + setCurrentAccountIDState(''); + setCurrentUserNameState(''); + } + setLoading(false); + } + ); + + return () => subscription.unsubscribe(); }, []); return ( - + {children} ) diff --git a/client/src/components/ProtectedRoute/ProtectedRoute.js b/client/src/components/ProtectedRoute/ProtectedRoute.js index a1db22c..15b1b88 100644 --- a/client/src/components/ProtectedRoute/ProtectedRoute.js +++ b/client/src/components/ProtectedRoute/ProtectedRoute.js @@ -3,14 +3,14 @@ import { Navigate } from 'react-router-dom'; import { useCurrentUser } from '../../CurrentUserProvider'; function ProtectedRoute({ children }) { - const { currentAccountID, loading } = useCurrentUser(); + const { supabaseUser, loading } = useCurrentUser(); if (loading) { return
    Loading...
    ; } // redirect to login - if (!currentAccountID) { + if (!supabaseUser) { return ; } diff --git a/client/src/index.js b/client/src/index.js index f720285..2cda063 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -8,6 +8,8 @@ import Home from './pages/Home/Home'; import Account from './pages/Account/Account'; import Tree from './pages/Tree/Tree'; import Login from './pages/Login/Login'; +import ResetPassword from './pages/Reset/Reset'; +import UpdatePassword from './pages/Reset/UpdatePassword'; import Family from './pages/Family/Family'; import ShareTree from './pages/Tree/ShareTree/ShareTree'; import ViewSharedTrees from './pages/Tree/ViewSharedTrees/ViewSharedTrees'; @@ -55,6 +57,16 @@ const router = createBrowserRouter([ path: '/register', element: , }, + + { + path: '/reset-password', + element: , + }, + { + path: '/update-password', + element: , + }, + { path: '/tree', element: ( diff --git a/client/src/pages/Account/Account.js b/client/src/pages/Account/Account.js index 00081f2..a119abb 100644 --- a/client/src/pages/Account/Account.js +++ b/client/src/pages/Account/Account.js @@ -1,53 +1,79 @@ import { React, useEffect, useState } from 'react'; import * as styles from './styles'; -import { Link, useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import NavBar from '../../components/NavBar/NavBar'; import AddToTreePopup from '../../components/AddToTree/AddToTree'; -import { CurrentUserProvider, useCurrentUser } from '../../CurrentUserProvider'; +import { useCurrentUser } from '../../CurrentUserProvider'; function Account() { + const navigate = useNavigate(); // used to change route without refreshing page, used to prevent infinite refreshes const [ownAccount, setOwnAccount] = useState(false); // will be retrieved const [existsInTree, setExistsInTree] = useState(false); // will be retrieved const [relationshipType, setRelationshipType] = useState(''); // will be retrieved - const { currentUserID, fetchCurrentUserID, currentAccountID } = useCurrentUser(); - useEffect(() => { - // define a regular function to call the async function - const fetchData = async () => { - await fetchCurrentUserID(); - }; + const { currentUserID, supabaseUser, loading } = useCurrentUser(); - fetchData(); - }, [fetchCurrentUserID]); + // Redirect to login if not authenticated + useEffect(() => { + if (!loading && !supabaseUser) { + navigate('/login'); + } + }, [loading, supabaseUser, navigate]); // takes id from url path let { id } = useParams(); // if no id is provided, retrieve current user's id and show that page useEffect(() => { - if (!id) { - id = currentUserID; - setOwnAccount(true); - window.location.href = `/account/${currentUserID}`; + if (!id && supabaseUser?.id) { + setOwnAccount(true); + navigate(`/account/${supabaseUser.id}`, { replace: true }); } - }, [id, currentUserID]); + }, [id, supabaseUser?.id, navigate]); // TODO: query for data of account user & verify that userID of logged in user matches + const [userData, setUserData] = useState({ id: id, - username: 'Loading...', + firstName: 'Loading...', + lastName: '', + email: '', + birthdate: '', + address: '', + city: '', + state: '', + country: '', + phone_number: '', + zipcode: '' }) - // fetch user info - const requestOptions = { - method: 'GET', - headers: { 'Content-Type': 'application/json' } - }; - - // find this person's account info + // Fetch user info - check if it's a Supabase user or family member useEffect(() => { if (!id) return; + // Check if this is the current Supabase user + if (id === supabaseUser?.id) { + console.log('Supabase user data:', supabaseUser); + console.log('User metadata:', supabaseUser.user_metadata); + + setUserData({ + id: supabaseUser.id, + firstName: supabaseUser.user_metadata?.first_name || 'User', + lastName: supabaseUser.user_metadata?.last_name || '', + email: supabaseUser.email, + birthdate: supabaseUser.user_metadata?.birthdate || '', + address: supabaseUser.user_metadata?.address || '', + city: supabaseUser.user_metadata?.city || '', + state: supabaseUser.user_metadata?.state || '', + country: supabaseUser.user_metadata?.country || '', + phone_number: supabaseUser.user_metadata?.phone_number || '', + zipcode: supabaseUser.user_metadata?.zipcode || '' + }); + setOwnAccount(true); + return; + } + + // Otherwise, try to fetch from family members API const requestOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' }, @@ -60,26 +86,38 @@ function Account() { setUserData(data); } else { console.error('Error fetching user data:', response); + // If family member not found, show basic info + setUserData({ + id: id, + firstName: 'Unknown', + lastName: 'User', + email: '', + }); } }) .catch((error) => { console.error('There was a problem with the fetch operation:', error); }); - }, [id]); + }, [id, supabaseUser]); useEffect(() => { - if(!userData.memberUserId){ - setOwnAccount(false); - } - else if(userData.userId === userData.memberUserId) { - // don't fetch relationship + // Check if this is the current user's own account + if (id === supabaseUser?.id) { setOwnAccount(true); return; } + + // If it's not the current user, check relationships (only for family members) + if (!userData.memberUserId) { + setOwnAccount(false); + return; + } + const requestOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' }, }; + // if not self, determine relationship to user fetch(`http://localhost:5000/api/relationships/${id}`, requestOptions) .then(async(response) => { @@ -90,7 +128,7 @@ function Account() { if(relationships[i].person1_id === parseInt(currentUserID) && relationships[i].person2_id === parseInt(id)) { // this is the relationship setRelationshipType(relationships[i].relationshipType); - return; // check this + return; } } } @@ -103,18 +141,18 @@ function Account() { .catch(error => { console.error('There was a problem with the fetch operation:', error); }); - }, [id, currentUserID, userData.id]); + }, [id, currentUserID, userData.id, userData.memberUserId, supabaseUser?.id]); // check if user exists in tree useEffect(() => { - if (!id || !currentAccountID) return; + if (!id || !supabaseUser?.id) return; const requestOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' }, }; - fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`, requestOptions) + fetch(`http://localhost:5000/api/tree-info/${supabaseUser.id}`, requestOptions) .then(async (response) => { if (response.ok) { console.log("tree info response"); @@ -135,7 +173,7 @@ function Account() { .catch((error) => { console.error('There was a problem with the fetch operation:', error); }); - }, [id, currentAccountID]); + }, [id, supabaseUser?.id]); return (
    @@ -154,7 +192,7 @@ function Account() { {/* if someone else's account, show buttons */} {!ownAccount && (
    - Add To Tree} accountUserName={userData.firstName} accountUserId={id} userId={currentUserID} currentUserAccountRelationshipType={relationshipType} /> + Add To Tree} accountUserName={userData.firstName} accountUserId={id} userId={supabaseUser?.id} currentUserAccountRelationshipType={relationshipType} />
    )} @@ -163,6 +201,43 @@ function Account() { {/* divider line */}
    + + {/* User Information Section */} +
    +

    Profile Information

    + +
    + {/* Basic Info */} +
    +

    Basic Information

    +
    +
    Email: {userData?.email || 'Not provided'}
    + {userData?.birthdate &&
    Birth Date: {new Date(userData.birthdate).toLocaleDateString()}
    } + {userData?.phone_number &&
    Phone: {userData.phone_number}
    } +
    +
    + + {/* Address Info */} +
    +

    Address Information

    +
    + {userData?.address &&
    Address: {userData.address}
    } + {(userData?.city || userData?.state) && ( +
    City, State: {[userData.city, userData.state].filter(Boolean).join(', ')}
    + )} + {userData?.zipcode &&
    ZIP Code: {userData.zipcode}
    } + {userData?.country &&
    Country: {userData.country}
    } +
    +
    +
    + + {/* Show message if no additional info is available */} + {!userData?.birthdate && !userData?.phone_number && !userData?.address && !userData?.city && !userData?.state && !userData?.zipcode && !userData?.country && ( +
    + No additional profile information available. Update your profile to add more details. +
    + )} +
    diff --git a/client/src/pages/CreateAccount/CreateAccount.js b/client/src/pages/CreateAccount/CreateAccount.js index d256f5f..261055d 100644 --- a/client/src/pages/CreateAccount/CreateAccount.js +++ b/client/src/pages/CreateAccount/CreateAccount.js @@ -1,10 +1,11 @@ -import { set, useForm } from 'react-hook-form' +import { useForm } from 'react-hook-form' import { React, useState } from 'react' -import { Link } from 'react-router-dom' import { yupResolver } from "@hookform/resolvers/yup" +import { handleRegister } from '../../utils/authHandlers'; import * as yup from "yup" import * as styles from './styles' import logo from '../../assets/kintreelogo-adobe.png'; +import { familyTreeService } from '../../services/familyTreeService'; //validation functionality const yupValidation = yup.object().shape( @@ -24,111 +25,62 @@ const yupValidation = yup.object().shape( country: yup.string().required("Country of residence is a required field."), phonenum: yup.string() .matches( - /^(\+\d{1,2}\s?)?1?\-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/ + /^(\+\d{1,2}\s?)?1?-?\.?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/ , "Invalid phone number format." ), zipcode: yup.string().matches(/^\d{5}(?:[-\s]\d{4})?$/, "Invalid zip code format."), - password: yup.string().required("Password is a required field.") + password: yup.string().required("Password is required") .matches( - /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/ - , "Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character" + /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{8,})/, + "Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character" ) - } ); const CreateAccount = () => { const {register, handleSubmit, formState: {errors}} = useForm({resolver: yupResolver(yupValidation)}); + const [errorMessage, setErrorMessage] = useState(""); const [isHovering, setIsHovering] = useState(false); - const [formData, setFormData] = useState({}); - const onSubmit = (data) => { - console.log(data); - - // register account - fetch(`http://localhost:5000/api/auth/register`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - username: data.firstname + " " + data.lastname, - email: data.email, - password: data.password, - }), - }) - .then(async (response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData); - - // Use responseData.user directly - const accountID = responseData.user; - - // Add user as a family member - return fetch(`http://localhost:5000/api/family-members/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - firstName: data.firstname, - lastName: data.lastname, - birthdate: data.birthdate, - email: data.email, - location: `${data.address}, ${data.city}, ${data.state} ${data.zipcode}, ${data.country}`, - phoneNumber: data.phonenum, - userId: accountID, - memberUserId: accountID, - gender: data.gender, - }), - }).then(async (response) => { // Initialize user's tree by adding themself - if (response.ok) { - const familyMemberResponse = await response.json(); - console.log(familyMemberResponse); - return fetch(`http://localhost:5000/api/tree-info/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - object: [{ - "id": familyMemberResponse.member, - "data": { - "first name": data.firstname, - "last name": data.lastname, - "gender": data.gender, - }, - "rels": { - "children": [], - "spouses": [], - } - }], - userId: accountID, - }), - }); - }}) - } - else { - const errorData = await response.json(); - console.error('Error registering account:', errorData); - throw new Error('Account registration failed'); - } - }) - .then(async (response) => { - if (response.ok) { - const responseData = await response.json(); - console.log(responseData); - window.location.href = '/'; - } else { - const errorData = await response.json(); - console.error('Error initializing family member:', errorData); - } - }) - .catch((error) => { - console.error('Error:', error); - }); - }; + const onSubmit = async (formData) => { + setErrorMessage(""); // clear previous errors + + try { + const data = await handleRegister(formData.email, formData.password, { + first_name: formData.firstname, + last_name: formData.lastname, + birthdate: formData.birthdate, + address: formData.address, + city: formData.city, + state: formData.state, + country: formData.country, + phone_number: formData.phonenum, + zipcode: formData.zipcode, + gender: formData.gender + }); // frontend Supabase registration + + // add new user as family member + const memberData = { + first_name: formData.firstname, + last_name: formData.lastname, + birthdate: formData.birthdate, + email: formData.email, + location: `${formData.city}, ${formData.state}, ${formData.country}`, + userId: data.user.id, + memberUserId: data.user.id, + gender: formData.gender + }; + const memberId = await familyTreeService.createFamilyMember(memberData); + + // add new user to their tree object + await familyTreeService.initializeTreeInfo(memberId, memberData, data.user.id); + + console.log('Registration successful:', data); + window.location.href = '/login'; // redirect after registration to login + } catch (error) { + setErrorMessage(error.message); + } + }; const ButtonStyle = { fontFamily: 'Alata', @@ -150,6 +102,14 @@ const CreateAccount = () => { KinTree Logo

    Create Account

    + + {/* Error Message Display */} + {errorMessage && ( +
    + {errorMessage} +
    + )} +
    @@ -208,8 +168,8 @@ const CreateAccount = () => {
    - - {errors.phone &&

    {errors.phone.message}

    } + + {errors.phonenum &&

    {errors.phonenum.message}

    }
    @@ -219,12 +179,24 @@ const CreateAccount = () => {
    -
    +
    +

    + Already have an account? + + Login here + +

    +
    +
    diff --git a/client/src/pages/Home/Home.js b/client/src/pages/Home/Home.js index 0e90317..95898a9 100644 --- a/client/src/pages/Home/Home.js +++ b/client/src/pages/Home/Home.js @@ -8,7 +8,7 @@ import CreateEventPopup from '../../components/CreateEvent/CreateEvent'; import CreateMemoryPopup from '../../components/CreateMemory/CreateMemory'; import NavBar from '../../components/NavBar/NavBar'; -function Home() { +function Home() { document.body.style.overflow = 'hidden'; document.body.style.width = '100%'; return ( diff --git a/client/src/pages/Login/Login.js b/client/src/pages/Login/Login.js index c508f38..21ff37d 100644 --- a/client/src/pages/Login/Login.js +++ b/client/src/pages/Login/Login.js @@ -4,47 +4,83 @@ import logo from '../../assets/kintreelogo-adobe.png'; import { useForm } from 'react-hook-form'; import { Link } from 'react-router-dom'; import { useCurrentUser } from '../../CurrentUserProvider'; +import { handleLogin, handleSignInWithGoogle } from '../../utils/authHandlers'; +import { supabase } from '../../utils/supabaseClient'; function Login() { const { register, handleSubmit } = useForm(); const [ errorMessage, setErrorMessage ] = useState(""); + const [ needsConfirm, setNeedsConfirm ] = useState(false); + const [ attemptedEmail, setAttemptedEmail ] = useState(""); + const [ resendLoading, setResendLoading ] = useState(false); const { setCurrentAccountID, fetchCurrentUserID, fetchCurrentAccountID } = useCurrentUser(); + const [ mfaStep, setMfaStep ] = useState(false); + const [ mfaFactorId, setMfaFactorId ] = useState(""); + const [ mfaChallengeId, setMfaChallengeId ] = useState(""); + const [ mfaCode, setMfaCode ] = useState(""); + const [ mfaError, setMfaError ] = useState(""); - const onSubmit = (data) => { - const requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }; - fetch('http://localhost:5000/api/auth/login', requestOptions) - .then(async(response) => { - if (response.ok) { - fetch(`http://localhost:5000/api/auth/user/email/${data.email}`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' } - }) - .then(async(response) => { - if (response.ok) { - let userData = await response.json(); - await setCurrentAccountID(userData.id); // set the current user ID in context - console.log("set currentAccountID to: ", userData.id); - await fetchCurrentUserID(); - window.location.href='/' - } - }) - return response.json(); - } - else { - const errorData = await response.json(); - console.error('Error:', errorData.message); - setErrorMessage(errorData.message); - throw new Error('Network response was not ok'); - } - }) - .catch(error => { - console.error('There was a problem with the fetch operation:', error); - }) - }; + const onSubmit = async (data) => { + setErrorMessage(""); // clear previous errors + setNeedsConfirm(false); + setAttemptedEmail(data.email); + try { + await handleLogin(data.email, data.password); // password step + // After password login, check for verified TOTP factor + const { data: factorsData, error: lfErr } = await supabase.auth.mfa.listFactors(); + if (lfErr) throw lfErr; + const totp = factorsData?.all?.find(f => f.factor_type === 'totp' && f.status === 'verified'); + if (totp) { + const { data: challengeData, error: chErr } = await supabase.auth.mfa.challenge({ factorId: totp.id }); + if (chErr) throw chErr; + setMfaFactorId(totp.id); + setMfaChallengeId(challengeData?.id || ""); + setMfaStep(true); + return; // wait for MFA verify + } + // No MFA required → proceed + window.location.href = '/'; + } catch (error) { + const msg = String(error?.message || '').toLowerCase(); + const requiresConfirm = msg.includes('confirm') || msg.includes('not confirmed'); + if (requiresConfirm) { + setNeedsConfirm(true); + } else { + setErrorMessage(error.message); + } + } + }; + + const onSubmitMfa = async (e) => { + e.preventDefault(); + setMfaError(""); + try { + const { error } = await supabase.auth.mfa.verify({ factorId: mfaFactorId, challengeId: mfaChallengeId, code: mfaCode }); + if (error) throw error; + window.location.href = '/'; + } catch (e2) { + setMfaError(e2.message || 'Verification failed'); + } + } + + const handleResendConfirmation = async () => { + if (!attemptedEmail) return; + setResendLoading(true); + try { + const { error } = await supabase.auth.resend({ + type: 'signup', + email: attemptedEmail, + options: { emailRedirectTo: `${window.location.origin}/login` } + }); + if (error) throw error; + // surface a lightweight notice + setErrorMessage('Confirmation email sent. Please check your inbox.'); + } catch (e) { + setErrorMessage(e.message); + } finally { + setResendLoading(false); + } + } document.body.style.overflow = 'hidden'; document.body.style.width = '100%'; @@ -54,7 +90,24 @@ function Login() {
    KinTree Logo

    Sign In

    -
    onSubmit(data))} style={styles.FormStyle}> + {!mfaStep && ( + + {needsConfirm && ( +
    +
    Please confirm your email to continue. We sent a link to
    {attemptedEmail}
    + +
    + )}
    ) diff --git a/client/src/pages/Reset/Reset.js b/client/src/pages/Reset/Reset.js index 986f727..cb22a75 100644 --- a/client/src/pages/Reset/Reset.js +++ b/client/src/pages/Reset/Reset.js @@ -1,12 +1,56 @@ -import React from 'react'; -import * as styles from './styles'; +import { useState } from "react"; +import { handleResetPassword } from '../../utils/authHandlers'; +import * as styles from '../Login/styles'; +import logo from '../../assets/kintreelogo-adobe.png'; -function Reset() { - return ( -
    +export default function ResetPassword() { + const [email, setEmail] = useState(""); + const [message, setMessage] = useState(""); -
    - ) -} + const onSubmit = async (e) => { + e.preventDefault(); + try { + await handleResetPassword(email); + setMessage('Check your email for a reset link.'); + } catch (error) { + // message handled by handler alert + } + }; -export default Reset; \ No newline at end of file + return ( +
    +
    + KinTree Logo +

    Reset Password

    +
    + {message && ( +
    + {message} +
    + )} +
      +
    • + + setEmail(e.target.value)} + style={styles.FieldStyle} + required + /> +
    • +
    +
    + +
    +
    +

    + Remembered your password? Back to Sign In +

    +
    +
    +
    +
    + ); +} diff --git a/client/src/pages/Reset/UpdatePassword.js b/client/src/pages/Reset/UpdatePassword.js new file mode 100644 index 0000000..b82ea4c --- /dev/null +++ b/client/src/pages/Reset/UpdatePassword.js @@ -0,0 +1,59 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { handleUpdatePassword } from "../../utils/authHandlers"; +import * as styles from '../Login/styles'; +import logo from '../../assets/kintreelogo-adobe.png'; + +export default function UpdatePassword() { + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const navigate = useNavigate(); + + const onSubmit = async (e) => { + e.preventDefault(); + try { + await handleUpdatePassword(password); + setMessage("Password updated. Redirecting to login..."); + setTimeout(() => navigate('/login'), 1200); + } catch (error) { + // message handled by handler alert + } + }; + + return ( +
    +
    + KinTree Logo +

    Update Password

    +
    + {message && ( +
    + {message} +
    + )} +
      +
    • + + setPassword(e.target.value)} + style={styles.FieldStyle} + required + /> +
    • +
    +
    + +
    +
    +

    + Back to Sign In +

    +
    +
    +
    +
    + ); +} diff --git a/client/src/pages/WebsiteSettings/WebsiteSettings.js b/client/src/pages/WebsiteSettings/WebsiteSettings.js index a229e53..a9936b2 100644 --- a/client/src/pages/WebsiteSettings/WebsiteSettings.js +++ b/client/src/pages/WebsiteSettings/WebsiteSettings.js @@ -1,11 +1,144 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import * as styles from "./styles"; import NavBar from "../../components/NavBar/NavBar"; +import { handleLogout } from '../../utils/authHandlers'; +import { supabase } from '../../utils/supabaseClient'; function WebsiteSettings() { const [notifications, setNotifications] = useState(true); const [darkMode, setDarkMode] = useState(false); + const [totpFactorId, setTotpFactorId] = useState(""); + const [totpQr, setTotpQr] = useState(""); + const [totpCode, setTotpCode] = useState(""); + const [totpStatus, setTotpStatus] = useState(""); + const [totpLoading, setTotpLoading] = useState(false); + const [totpVerified, setTotpVerified] = useState(false); + async function loadFactors() { + try { + const { data: factorsData, error } = await supabase.auth.mfa.listFactors(); + if (error) throw error; + const verified = factorsData?.all?.find(f => f.factor_type === 'totp' && f.status === 'verified'); + const unverified = factorsData?.all?.find(f => f.factor_type === 'totp' && f.status === 'unverified'); + if (verified) { + setTotpVerified(true); + setTotpFactorId(verified.id); + setTotpQr(""); + } else if (unverified) { + setTotpVerified(false); + setTotpFactorId(unverified.id); + setTotpQr(""); // we can't re-fetch QR; allow verify via code + } else { + setTotpVerified(false); + setTotpFactorId(""); + setTotpQr(""); + } + } catch (e) { + console.error('Load factors error:', e); + } + } + + useEffect(() => { + loadFactors(); + }, []); + + async function startTotpEnroll() { + setTotpStatus(""); + setTotpLoading(true); + try { + // Avoid starting a new enroll while one is pending + if (totpVerified) { + setTotpStatus('Two-factor authentication is already enabled.'); + return; + } + if (totpFactorId && !totpVerified) { + setTotpStatus('A TOTP setup is pending. Enter a code from your authenticator, or click Start over.'); + return; + } + const { data, error } = await supabase.auth.mfa.enroll({ factorType: 'totp' }); + if (error) throw error; + console.log('Enroll data:', data); + setTotpFactorId(data.id); + setTotpQr(data.totp?.qr_code || ""); + } catch (e) { + setTotpStatus(e.message); + } finally { + setTotpLoading(false); + } + } + + async function verifyTotp() { + if (!totpFactorId || !totpCode) return; + setTotpLoading(true); + setTotpStatus(""); + try { + // Ensure we have the correct pending factorId in case state was lost + if (!totpFactorId) { + const { data: factorsData, error: factorsErr } = await supabase.auth.mfa.listFactors(); + if (factorsErr) throw factorsErr; + console.log('Factors:', factorsData); + const pending = factorsData?.all?.find(f => f.factor_type === 'totp' && f.status === 'unverified'); + if (pending) setTotpFactorId(pending.id); + } else { + const { data: factorsData, error: factorsErr } = await supabase.auth.mfa.listFactors(); + if (!factorsErr) console.log('Factors:', factorsData); + } + + console.log('Using factorId:', totpFactorId, 'Code:', totpCode); + // Create a challenge, then verify with challengeId (works across SDK versions) + const { data: challengeData, error: challengeErr } = await supabase.auth.mfa.challenge({ factorId: totpFactorId }); + if (challengeErr) throw challengeErr; + console.log('Challenge data:', challengeData); + const challengeId = challengeData?.id; + const { error } = await supabase.auth.mfa.verify({ factorId: totpFactorId, challengeId, code: totpCode }); + if (error) throw error; + setTotpStatus('Two-factor authentication enabled.'); + setTotpQr(""); + setTotpCode(""); + setTotpVerified(true); + } catch (e) { + console.error('TOTP verify error:', e); + setTotpStatus(e.message || 'Verification failed'); + } finally { + setTotpLoading(false); + } + } + + async function disableTotp() { + if (!totpFactorId) return; + setTotpLoading(true); + setTotpStatus(""); + try { + const { error } = await supabase.auth.mfa.unenroll({ factorId: totpFactorId }); + if (error) throw error; + setTotpVerified(false); + setTotpFactorId(""); + setTotpStatus('Two-factor authentication disabled.'); + } catch (e) { + setTotpStatus(e.message || 'Failed to disable'); + } finally { + setTotpLoading(false); + } + } + + async function restartTotpEnroll() { + // For lingering unverified factor: unenroll then start fresh + if (totpFactorId && !totpVerified) { + try { + const { error } = await supabase.auth.mfa.unenroll({ factorId: totpFactorId }); + if (error) throw error; + setTotpFactorId(""); + setTotpQr(""); + setTotpCode(""); + setTotpStatus('Previous pending setup cleared.'); + } catch (e) { + setTotpStatus(e.message || 'Could not reset existing setup'); + return; + } + } + await startTotpEnroll(); + } + return (
    @@ -30,13 +163,64 @@ function WebsiteSettings() {

    - - - setNotifications(!notifications)} - /> +
    + + {totpVerified && ( +
    + Enabled + + {totpStatus && {totpStatus}} +
    + )} + {!totpVerified && !totpQr && !totpFactorId && ( +
    + + {totpStatus && {totpStatus}} +
    + )} + {!totpVerified && totpFactorId && !totpQr && ( +
    +
    Enter a 6‑digit code from your authenticator to complete setup.
    + setTotpCode(e.target.value)} + /> +
    + + +
    + {totpStatus && {totpStatus}} +
    + )} + {!totpVerified && totpQr && ( +
    +
    Scan this QR with Duo/Google Authenticator, then enter the 6‑digit code:
    + TOTP QR + setTotpCode(e.target.value)} + /> + + {totpStatus && {totpStatus}} +
    + )} +
    {/* Profile & Personalization */} @@ -67,6 +251,15 @@ function WebsiteSettings() {
    + +
    + +
    diff --git a/client/src/pages/WebsiteSettings/styles.js b/client/src/pages/WebsiteSettings/styles.js index 5a08a9b..6aa3b72 100644 --- a/client/src/pages/WebsiteSettings/styles.js +++ b/client/src/pages/WebsiteSettings/styles.js @@ -63,4 +63,31 @@ export const Input = { export const ToggleSwitch = { marginLeft: '10px', transform: 'scale(1.2)' +}; + +export const SignOutContainer = { + display: 'flex', + justifyContent: 'flex-end', + padding: '20px', + marginTop: '30px', + borderTop: '1px solid #e0e0e0' +}; + +export const SignOutButton = { + backgroundColor: '#dc3545', + color: 'white', + border: 'none', + borderRadius: '8px', + padding: '12px 24px', + fontSize: '16px', + fontWeight: '600', + cursor: 'pointer', + boxShadow: '0 2px 4px rgba(220, 53, 69, 0.2)', + transition: 'all 0.2s ease', + minWidth: '120px' +}; + +export const SignOutButtonHover = { + backgroundColor: '#c82333', + boxShadow: '0 4px 8px rgba(220, 53, 69, 0.3)' }; \ No newline at end of file diff --git a/client/src/utils/auth.js b/client/src/utils/auth.js new file mode 100644 index 0000000..93a975e --- /dev/null +++ b/client/src/utils/auth.js @@ -0,0 +1,76 @@ +import { supabase } from './supabaseClient'; + +// These functions are used to handle the authentication of the user, but only the pure login, logout, etc functionality. +// It should not include frontend logic like redirects. + +// Register new user +export async function registerUser(email, password, metadata = {}) { + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: metadata, + emailRedirectTo: `${window.location.origin}/login` + } + }); + if (error) throw error; + + // After successful signup, upsert the profile into public.users via backend + try { + const user = data?.user || (await supabase.auth.getUser()).data?.user; + if (user) { + await fetch('http://localhost:5000/api/auth/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + auth_uid: user.id, + email: user.email, + username: user.email, + // Map possible metadata key variants + firstName: metadata.firstName || metadata.first_name || null, + lastName: metadata.lastName || metadata.last_name || null, + phoneNumber: metadata.phoneNumber || metadata.phone_number || metadata.phonenum || null, + birthDate: metadata.birthDate || metadata.birthdate || null, + }) + }); + } + } catch (e) { + // Non-fatal: keep signup success even if sync fails + console.warn('Profile sync skipped:', e?.message || e); + } + + return data; +} + +// Login existing user +export async function loginUser(email, password) { + const { data, error } = await supabase.auth.signInWithPassword({ email, password }); + if (error) throw error; + return data; +} + +// Logout current user +export async function logoutUser() { + const { error } = await supabase.auth.signOut(); + if (error) throw error; +} + +export async function resetPassword(email, url) { + const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo: url }); + if (error) throw error; +} + +export async function updatePassword(password) { + const { error } = await supabase.auth.updateUser({ password }); + if (error) throw error; +} + +// OAuth: Google sign-in +export async function signInWithGoogle() { + const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { redirectTo: `${window.location.origin}/` }, + }); + if (error) throw error; + return data; +} \ No newline at end of file diff --git a/client/src/utils/authHandlers.js b/client/src/utils/authHandlers.js new file mode 100644 index 0000000..774f3cc --- /dev/null +++ b/client/src/utils/authHandlers.js @@ -0,0 +1,75 @@ +// src/handlers/authHandlers.js +import { loginUser, registerUser, logoutUser, resetPassword, updatePassword, signInWithGoogle } from '../utils/auth'; + +const BASE_URL = process.env.REACT_APP_BASE_URL || 'http://localhost:3000'; + +// This page is used to handle authentication and includes redirects and error handling. + +export async function handleLogin(email, password) { + try { + const data = await loginUser(email, password); // call the pure login function + console.log("Logged in user:", data.user); + // Do not redirect here; caller will handle MFA step and navigation + return data; + } catch (error) { + console.error('Login error:', error.message); + alert(error.message); + throw error; // so form onSubmit can catch it + } +} + +export async function handleRegister(email, password, metadata = {}) { + try { + const data = await registerUser(email, password, metadata); + console.log("Registered user:", data.user); + return data; // Return the data so the calling function can use it + } catch (error) { + console.error('Registration error:', error.message); + alert(error.message); + throw error; // so form onSubmit can catch it + } +} + +// Logout handler +export async function handleLogout() { + try { + await logoutUser(); + window.location.href = '/login'; // redirect after logout + } catch (error) { + console.error('Logout error:', error.message); + alert(error.message); + } +} + +export async function handleResetPassword(email) { + try { + const url = `${BASE_URL}/update-password`; + await resetPassword(email, url); + } catch (error) { + console.error('Reset Password error:', error.message); + alert(error.message); + throw error; // so form onSubmit can catch it + } +} + +export async function handleUpdatePassword(password) { + try { + await updatePassword(password); + window.location.href = '/login'; + } catch (error) { + console.error('Update Password error:', error.message); + alert(error.message); + throw error; // so form onSubmit can catch it + } +} + +// Google OAuth handler +export async function handleSignInWithGoogle() { + try { + await signInWithGoogle(); // redirects to Google, then back to our site + } catch (error) { + console.error('Google sign-in error:', error.message); + alert(error.message); + throw error; + } +} \ No newline at end of file diff --git a/client/src/utils/supabaseClient.js b/client/src/utils/supabaseClient.js new file mode 100644 index 0000000..aafdeac --- /dev/null +++ b/client/src/utils/supabaseClient.js @@ -0,0 +1,6 @@ +import { createClient } from '@supabase/supabase-js'; + +const supabaseUrl = process.env.REACT_APP_SUPABASE_URL; +const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY; + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); diff --git a/docs/.env.example b/docs/.env.example index 2814bb3..f25ad8d 100644 --- a/docs/.env.example +++ b/docs/.env.example @@ -1,5 +1,7 @@ -DB_HOST=localhost -DB_PORT=3306 -DB_USER=root -DB_PASSWORD=my_sql_password -DB_DATABASE=my_database_name +# SERVER ENV +SUPABASE_URL= +SUPABASE_SERVICE_ROLE_KEY= + +# CLIENT ENV +REACT_APP_SUPABASE_URL= +REACT_APP_SUPABASE_ANON_KEY= \ No newline at end of file diff --git a/server/controllers/authController.js b/server/controllers/authController.js index e9037fa..d161f0e 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -1,75 +1,5 @@ -// authController.js -const bcrypt = require('bcryptjs'); -const User = require('../models/userModel'); - -const register = async (req, res) => { - console.log('Regiater function called'); - try { - const { username, email, password } = req.body; - - if (!email || !password || !username) { - return res.status(400).json({ error: 'All fields are required' }); - } - - const existingUser = await User.findByEmail(email); - if (existingUser) return res.status(400).json({ - error: 'Email already in use' - }); - - const saltRounds = 12; - const salt = await bcrypt.genSalt(saltRounds); - const hashedPassword = await bcrypt.hash(password, salt); - - const [newUser] = await User.register({ - username, - email, - password: hashedPassword - }); - - res.status(201).json({ - message: 'User registered successfully', user: newUser - }); - } catch (error) { - console.error(error); - res.status(500).json({ - error: 'Registration failed' - }); - } -}; - -const login = async(req,res) => { - try{ - const { email, password } = req.body; - if(!email || !password){ - return res.status(400).json({ - message: 'Missing an email or password' - }); - } - const existingUser = await User.findByEmail(email); - if(!existingUser){ - return res.status(401).json({ - message: 'User is not found. Please register!' - }); - } - const passwordCompare = await bcrypt.compare(password, existingUser.password) - if(!passwordCompare){ - return res.status(401).json({ - message: "Invalid credentials" - }); - } - - res.status(200).json({ - message: "You are logged in!" - }); - } - catch (error){ - console.error(error); - res.status(500).json({ - error: 'Registration failed' - }); - - } -}; +// authController.js - the main backend file for user registration, signin, etc +const User = require('../models/userModel'); // now backed by Supabase const deleteByUser = async (req,res) => { const { id } = req.params; @@ -84,7 +14,7 @@ const deleteByUser = async (req,res) => { } catch (error){ console.error(error); - res.status(500);json({error:"Error deleting user"}) + res.status(500).json({error:"Error deleting user"}) } } @@ -126,4 +56,26 @@ const getAllUsers = async (req, res) => { } } -module.exports = { register,login, deleteByUser, findById, findByEmail, getAllUsers }; +module.exports = { deleteByUser, findById, findByEmail, getAllUsers }; + +// Add a sync endpoint: POST /api/auth/sync +// Body: { auth_uid, email, username, firstName, lastName, phoneNumber, birthDate } +const syncAuthUser = async (req, res) => { + try { + const { auth_uid, email, username, firstName, lastName, phoneNumber, birthDate } = req.body || {}; + if (!auth_uid || !email) { + return res.status(400).json({ error: 'auth_uid and email are required' }); + } + const user = await User.upsertByAuthUser({ auth_uid, email, username, firstName, lastName, phoneNumber, birthDate }); + res.status(200).json(user); + } catch (error) { + console.error('Sync error:', error); + res.status(500).json({ + error: 'Error syncing auth user', + details: error.message, + stack: process.env.NODE_ENV === 'development' ? error.stack : undefined + }); + } +}; + +module.exports.syncAuthUser = syncAuthUser; diff --git a/server/controllers/backupController.js b/server/controllers/backupController.js index b03ebc9..6a09648 100644 --- a/server/controllers/backupController.js +++ b/server/controllers/backupController.js @@ -8,13 +8,17 @@ const backupInfo = require('../models/backupModel'); const backupUser = async (req, res) => { try{ const { id } = req.params; - const user = await userInfo.findById(id); + // Resolve UUID to integer if needed + const User = require('../models/userModel'); + const userIdInt = await User.resolveUserIdFromAuthUid(id) || id; + + const user = await userInfo.findById(userIdInt); if(!user) { - return { error: "User not found"}; + return res.status(404).json({ error: "User not found"}); } - const tree = await treeMember.getAllMembersbyId(id); - const relationships = await relationship.getRelationships(id); - const sharedTree = await sharedTrees.getSharedTreebySender(id); + const tree = await treeMember.getMembersByUser(userIdInt); + const relationships = await relationship.getRelationshipByUser(userIdInt); + const sharedTree = await sharedTrees.getSharedTreebySender(userIdInt); const data = { user, @@ -23,7 +27,7 @@ const backupUser = async (req, res) => { sharedTree }; - await backupInfo.addBackup(id, JSON.stringify(data)); + await backupInfo.addBackup(userIdInt, JSON.stringify(data)); res.json({ message: 'Backup completed' }); @@ -31,7 +35,8 @@ const backupUser = async (req, res) => { catch (error){ console.error(error); res.status(500).json({ - error: 'Error backing up data' + error: 'Error backing up data', + details: error.message }); } }; @@ -39,21 +44,25 @@ const backupUser = async (req, res) => { const restoreUser = async (req, res) => { try{ const {id} = req.params; - const existingUser = await userInfo.findById(id); + // Resolve UUID to integer if needed + const User = require('../models/userModel'); + const userIdInt = await User.resolveUserIdFromAuthUid(id) || id; + + const existingUser = await userInfo.findById(userIdInt); if(!existingUser){ return res.status(404).json({ error: "User not found. No data to restore" }) } - const backup = await backupInfo.getLatestBackup(id); + const backup = await backupInfo.getLatestBackup(userIdInt); if(!backup) { return res.status(404).json({ error: "No backup available" }) } - const backupData = backup.backupData + const backupData = JSON.parse(backup.backupdata || backup.backupData); for( const member of backupData.tree) { const exists= await treeMember.getMemberById(member.id) diff --git a/server/controllers/relationshipController.js b/server/controllers/relationshipController.js index e5b1520..81db440 100644 --- a/server/controllers/relationshipController.js +++ b/server/controllers/relationshipController.js @@ -1,4 +1,6 @@ const Relationship = require('../models/relationshipModel'); +const User = require('../models/userModel'); +const treeMember = require('../models/treeMemberModel'); const getRelationships = async (req, res) => { try{ @@ -43,28 +45,69 @@ const getRelationshipsByOtherUser = async (req,res) => { }; const addRelationship = async (req,res) =>{ - //need to add functionality to refuse a relationship if it already exists -try{ - const {person1_id, person2_id, relationshipType, relationshipStatus, side, userId} = req.body; - const [newRelationship] = await Relationship.addRelationship({ - person1_id, - person2_id, - relationshipType, - relationshipStatus, - side, - userId - }); - res.status(201).json({ - message: 'Relationship added successfully', - member: newRelationship - }); -} catch (error) { - console.error(error); - res.status(500).json({ - error: 'Error adding relationship' - }); -} + try{ + let {person1_id, person2_id, relationshipType, relationshipStatus, side, userId} = req.body; + + console.log('addRelationship received:', {person1_id, person2_id, userId, typeof_person1_id: typeof person1_id}); + + // Resolve person1_id if it's a UUID (user ID) - need to find the member ID for that user + if (typeof person1_id === 'string' && person1_id.includes('-')) { + const userIdInt = await User.resolveUserIdFromAuthUid(person1_id); + if (!userIdInt) { + return res.status(400).json({ error: 'Invalid person1_id: User not found' }); + } + // Find the active member for this user + const member = await treeMember.getActiveMemberId(userIdInt); + if (!member) { + return res.status(400).json({ error: 'No active member found for person1_id user' }); + } + person1_id = member.id; + } + + // Resolve person2_id if it's a UUID (user ID) + if (typeof person2_id === 'string' && person2_id.includes('-')) { + const userIdInt = await User.resolveUserIdFromAuthUid(person2_id); + if (!userIdInt) { + return res.status(400).json({ error: 'Invalid person2_id: User not found' }); + } + // Find the active member for this user + const member = await treeMember.getActiveMemberId(userIdInt); + if (!member) { + return res.status(400).json({ error: 'No active member found for person2_id user' }); + } + person2_id = member.id; + } + + // Resolve userId from UUID to integer + if (userId && typeof userId === 'string' && userId.includes('-')) { + userId = await User.resolveUserIdFromAuthUid(userId); + if (!userId) { + return res.status(400).json({ error: 'Invalid userId: User not found' }); + } + } + + console.log('addRelationship resolved:', {person1_id, person2_id, userId}); + + const newRelationship = await Relationship.addRelationship({ + person1_id, + person2_id, + relationshipType, + relationshipStatus, + side, + userId + }); + res.status(201).json({ + message: 'Relationship added successfully', + member: newRelationship + }); + } catch (error) { + console.error(error); + res.status(500).json({ + error: 'Error adding relationship', + details: error.message + }); + } } const filterBySide = async (req,res) => { diff --git a/server/controllers/sharedTreeController.js b/server/controllers/sharedTreeController.js index 2d0e2c9..0f06de3 100644 --- a/server/controllers/sharedTreeController.js +++ b/server/controllers/sharedTreeController.js @@ -48,8 +48,8 @@ const getSharedTreeByToken = async (req, res) => { const getSharedTreeBySender = async (req, res) => { try{ const { id} = req.params; - const relationships = await sharedTrees.getSharedTreebySender(id); - res.status(200).json(sharedTrees); + const trees = await sharedTrees.getSharedTreebySender(id); + res.status(200).json(trees); } catch(error){ console.error(error); diff --git a/server/controllers/treeInfoController.js b/server/controllers/treeInfoController.js index f153433..825a654 100644 --- a/server/controllers/treeInfoController.js +++ b/server/controllers/treeInfoController.js @@ -1,12 +1,15 @@ const treeInfo = require('../models/treeInfoModel'); +const User = require('../models/userModel'); const addObject = async (req, res) => { try { const { object, userId } = req.body; + // Resolve UUID to integer user ID if needed + const userIdInt = await User.resolveUserIdFromAuthUid(userId) || userId; - const [newObject] = await treeInfo.addObject({ + const newObject = await treeInfo.addObject({ object: JSON.stringify(object), - userId: userId + userId: userIdInt }); res.status(201).json({ @@ -63,8 +66,12 @@ const updateObject = async (req, res) => { const getObject = async (req, res) => { try { const { id } = req.params; - - const retrievedObject = await treeInfo.getObject(id); + // Resolve UUID to integer user ID first + const userId = await User.resolveUserIdFromAuthUid(id); + if (!userId) { + return res.status(404).json({ error: 'User not found' }); + } + const retrievedObject = await treeInfo.getObject(userId); if (!retrievedObject) { return res.status(404).json({ error: 'Object not found' @@ -77,6 +84,7 @@ const getObject = async (req, res) => { console.error(error); res.status(500).json({ error: 'Error retrieving tree object', + details: error.message }); } } diff --git a/server/controllers/treeMemberController.js b/server/controllers/treeMemberController.js index 0f893a2..27c91d1 100644 --- a/server/controllers/treeMemberController.js +++ b/server/controllers/treeMemberController.js @@ -1,6 +1,5 @@ const treeMember = require('../models/treeMemberModel'); const relationship = require('../models/relationshipModel'); -const { update } = require('../db/knex'); // format dates to YYYY-MM-DD const formatDate = (dateValue) => { @@ -14,34 +13,66 @@ const formatDate = (dateValue) => { return `${year}-${month}-${day}`; }; +const User = require('../models/userModel'); const addTreeMember = async (req, res) => { try { const { firstName, lastName, birthDate, deathDate, location, phoneNumber, relationships, userId, memberUserId, gender } = req.body; + // Resolve UUIDs to integer user IDs - CRITICAL: database requires integers, not UUIDs + console.log('addTreeMember received userId:', userId, typeof userId); + const userIdInt = await User.resolveUserIdFromAuthUid(userId); + console.log('Resolved userIdInt:', userIdInt, typeof userIdInt); + if (!userIdInt) { + return res.status(400).json({ + error: 'Invalid user ID. User not found in database. Please sync your account first.', + received: userId + }); + } + + const memberUserIdInt = memberUserId ? await User.resolveUserIdFromAuthUid(memberUserId) : null; + if (memberUserId && !memberUserIdInt) { + return res.status(400).json({ + error: 'Invalid member user ID. User not found in database.', + received: memberUserId + }); + } + const formattedBirthDate = formatDate(birthDate); const formattedDeathDate = formatDate(deathDate); // ensure all necessary fields are passed in the request body - const [newMember] = await treeMember.addMember({ + const newMember = await treeMember.addMember({ firstName, lastName, birthDate: formattedBirthDate, deathDate: formattedDeathDate, location, phoneNumber, - userId, - memberUserId, + userId: userIdInt, // Now guaranteed to be an integer + memberUserId: memberUserIdInt, // Now guaranteed to be an integer or null, gender }); // if there are relationships, add them to the database if (relationships && relationships.length > 0) { for (const rel of relationships) { + // Ensure person2_id is an integer, not a UUID + let person2_id = rel.person2_id; + if (typeof person2_id === 'string' && person2_id.includes('-')) { + // If it looks like a UUID, try to resolve it + person2_id = await User.resolveUserIdFromAuthUid(person2_id); + if (!person2_id) { + console.error('Could not resolve person2_id UUID:', rel.person2_id); + continue; // Skip this relationship + } + } await relationship.addRelationship({ person1_id: newMember.id, - person2_id: rel.person2_id, // Corrected from 'relationship.person2_id' to 'rel.person2_id' - relationship_status: 'active' + person2_id: person2_id, + relationshipType: rel.relationshipType || 'sibling', + relationshipStatus: 'active', + userId: userIdInt // Need to include userId for the relationship }); } } @@ -106,16 +137,21 @@ const editTreeMember = async (req, res) => { const getMembersByUser = async (req,res) =>{ try{ - const { userId } = req.params; - const members = await treeMember.getMembersByUser(userId) + // Resolve UUID to integer user ID first + const userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) { + return res.status(404).json({ error: 'User not found' }); + } + const members = await treeMember.getMembersByUser(userIdInt) console.log(members); res.status(200).json(members); } catch(error){ console.error(error); res.status(500).json({ - error: 'Error fetching members' + error: 'Error fetching members', + details: error.message }); } @@ -124,7 +160,7 @@ const getMembersByUser = async (req,res) =>{ const getMembersByOtherUser = async (req,res) =>{ try{ const { userId} = req.params; - const members = await treeMember.getMembersByOtherUser(userId) + const members = await treeMember.getMembeByOtherUser(userId) res.status(200).json(members); } catch(error){ @@ -150,7 +186,7 @@ const deleteByUser = async (req, res) => { } catch (error){ console.error(error); - res.status(500);json({error:"Error deleting family member"}) + res.status(500).json({error:"Error deleting family member"}) } } @@ -171,14 +207,18 @@ const getMemberById = async (req, res) => { const getActiveMemberId = async (req, res) => { try { const { id } = req.params; - const member = await treeMember.getActiveMemberId(id); - if (!member) { - return res.status(404).json({ error: 'Family member not found' }); + // Resolve UUID to integer user ID first + const userId = await User.resolveUserIdFromAuthUid(id); + if (!userId) { + return res.status(200).json({}); } + const member = await treeMember.getActiveMemberId(userId); + // If none found, return empty object to avoid frontend JSON parse errors + if (!member) return res.status(200).json({}); res.status(200).json(member); } catch (error) { console.error(error); - res.status(500).json({ error: 'Error fetching family member' }); + res.status(500).json({ error: 'Error fetching family member', details: error.message }); } } diff --git a/server/controllers/treeSummaryController.js b/server/controllers/treeSummaryController.js index e07320d..f09611f 100644 --- a/server/controllers/treeSummaryController.js +++ b/server/controllers/treeSummaryController.js @@ -8,18 +8,22 @@ const treeSummary = require('../models/treeSummaryModel'); const updateUserTreeSummary = async (req, res) => { const { userId } = req.params; try { - const members = await treeMember.getMemberByUser(userId); - const relationships = await relationship.getRelationshipByUser(userId); + // Resolve UUID to integer if needed + const User = require('../models/userModel'); + const userIdInt = await User.resolveUserIdFromAuthUid(userId) || userId; + + const members = await treeMember.getMembersByUser(userIdInt); + const relationships = await relationship.getRelationshipByUser(userIdInt); const summary = { members, relationships}; - const existing = treeSummary.getSummaryByUser(userId); + const existing = await treeSummary.getSummaryByUser(userIdInt); if(existing){ - await treeSummary.updateSummary(userId, summary); + await treeSummary.updateSummary(userIdInt, summary); } else{ - await treeSummary.createSummary(userId,summary) + await treeSummary.createSummary(userIdInt, summary); } res.json({ message: 'Tree summary updated' @@ -28,7 +32,8 @@ const updateUserTreeSummary = async (req, res) => { catch (error) { console.error(error); res.status(500).json({ - error: 'Failed to update tree summary' + error: 'Failed to update tree summary', + details: error.message }); } diff --git a/server/db/knex.js b/server/db/knex.js deleted file mode 100644 index cb9e3d3..0000000 --- a/server/db/knex.js +++ /dev/null @@ -1,7 +0,0 @@ -require('dotenv').config() -const knex = require('knex'); -const config = require('../knexfile.js'); - -const db = knex(config.development); - -module.exports = db; diff --git a/server/db/supabase-init.sql b/server/db/supabase-init.sql new file mode 100644 index 0000000..46e639b --- /dev/null +++ b/server/db/supabase-init.sql @@ -0,0 +1,74 @@ +-- ======================== +-- Supabase Schema Init File +-- Created from MySQL Knex migrations +-- ======================== + +-- 1. Users Table +create table users ( + id serial primary key, + username text unique not null, + password text not null, + email text unique not null, + firstName text, + lastName text, + phoneNumber text, + birthDate date, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now() +); + +-- 2. Tree Members Table +create table treeMembers ( + id serial primary key, + firstName text not null, + lastName text not null, + birthDate date, + deathDate date, + location text, + phoneNumber text, + userId integer not null references users(id) on delete cascade, + memberUserId integer, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now() +); + +-- 3. Relationships Table +create table relationships ( + id serial primary key, + person1_id integer not null references treeMembers(id) on delete cascade, + person2_id integer not null references treeMembers(id) on delete cascade, + relationshipType text not null check (relationshipType in ('parent','child','sibling','spouse','stepparent','stepchild','ex-spouse')), + relationshipStatus text check (relationshipStatus in ('active','inactive')), + side text check (side in ('paternal','maternal')), + userId integer not null references users(id) on delete cascade, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now() +); + +-- 4. Shared Trees Table +create table sharedTrees ( + sharedTreeID serial primary key, + senderID integer not null references users(id), + recieverID integer, + perms text check (perms in ('view','edit')), + parentalSide text check (parentalSide in ('paternal','maternal','both')), + sahreDate timestamp, + treeInfo json +); + +-- 5. Backups Table +create table backups ( + backupId serial primary key, + userId integer not null references users(id), + backupData json, + createdAt timestamp default now() +); + +-- 6. Tree Info Table +create table treeinfo ( + id serial primary key, + userid integer not null references users(id) on delete cascade, + object jsonb, + created_at timestamp with time zone default now(), + updated_at timestamp with time zone default now() +); \ No newline at end of file diff --git a/server/knexfile.js b/server/knexfile.js deleted file mode 100644 index d49ce6a..0000000 --- a/server/knexfile.js +++ /dev/null @@ -1,23 +0,0 @@ -require('dotenv').config(); - -module.exports = { - development: { - client: 'mysql2', - connection: { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_DATABASE, - port: process.env.DB_PORT || 3306 - }, - migrations: { - directory: './migrations' - }, - seeds: { - directory: './seeds' - } - } -}; - - - diff --git a/server/lib/supabase.js b/server/lib/supabase.js new file mode 100644 index 0000000..56d3147 --- /dev/null +++ b/server/lib/supabase.js @@ -0,0 +1,16 @@ +require('dotenv').config(); +const { createClient } = require('@supabase/supabase-js'); + +// Server-side Supabase client using the Service Role key +// Note: Do not expose the service role key to the client. +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE_KEY, + { + auth: { persistSession: false }, + } +); + +module.exports = supabase; + + diff --git a/server/migrations/20250416174536_add_user_tree_table.js b/server/migrations/20250416174536_add_user_tree_table.js index 80a2584..4d611a2 100644 --- a/server/migrations/20250416174536_add_user_tree_table.js +++ b/server/migrations/20250416174536_add_user_tree_table.js @@ -19,5 +19,5 @@ exports.up = function(knex) { */ exports.down = function(knex) { return knex.schema.dropTableIfExists('userTreeSummaries') - + }; diff --git a/server/models/backupModel.js b/server/models/backupModel.js index 506837b..6c3dd16 100644 --- a/server/models/backupModel.js +++ b/server/models/backupModel.js @@ -1,14 +1,48 @@ -const db = require('../db/knex'); +// backupModel.js - model for backups table (Supabase) +const supabase = require('../lib/supabase'); const backup = { - addBackup: async(user, data) => { - return db('backups').insert({'userId': user, 'backupData': data}); + addBackup: async(userId, data) => { + // Resolve UUID to integer if needed + let userIdInt = userId; + if (typeof userId === 'string' && userId.includes('-')) { + const User = require('./userModel'); + userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) throw new Error('User not found'); + } + const { data: inserted, error } = await supabase + .from('backups') + .insert([{ userid: userIdInt, backupdata: data }]) + .select('*') + .single(); + if (error) throw error; + return inserted; }, getBackups: async (id) => { - return db('backups').where({id}); + const { data, error } = await supabase + .from('backups') + .select('*') + .eq('backupid', id); + if (error) throw error; + return data; }, - getLatestBackup: async (id) => { - return db('backups').where('backupId', id).orderBy('createdAt', 'desc').first(); + getLatestBackup: async (userId) => { + // Resolve UUID to integer if needed + let userIdInt = userId; + if (typeof userId === 'string' && userId.includes('-')) { + const User = require('./userModel'); + userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) throw new Error('User not found'); + } + const { data, error } = await supabase + .from('backups') + .select('*') + .eq('userid', userIdInt) + .order('createdat', { ascending: false }) + .limit(1) + .maybeSingle(); + if (error) throw error; + return data; } }; diff --git a/server/models/relationshipModel.js b/server/models/relationshipModel.js index cc8dd89..5f03682 100644 --- a/server/models/relationshipModel.js +++ b/server/models/relationshipModel.js @@ -1,36 +1,85 @@ -const db = require('../db/knex'); +// relationshipModel.js - the model for the relationships table +// this file was replaced with the supabase model +const supabase = require('../lib/supabase'); +// all functions for the relationship to interact with the database const Relationships = { addRelationship: async (data) => { - return db('relationships').insert(data); + // Map camelCase to lowercase column names for Postgres + const mappedData = { + person1_id: data.person1_id || data.person1Id, + person2_id: data.person2_id || data.person2Id, + relationshiptype: data.relationshipType || data.relationshiptype, + relationshipstatus: data.relationshipStatus || data.relationshipstatus, + side: data.side, + userid: data.userId || data.userid, + }; + // Remove undefined/null values + Object.keys(mappedData).forEach(key => mappedData[key] === undefined && delete mappedData[key]); + const { data: inserted, error } = await supabase + .from('relationships') + .insert([ mappedData ]) + .select('*') + .single(); + if (error) throw error; + return inserted; }, - getRelationships:async (personId) => { - return db('relationships').where('person1_id', personId).orWhere('person2_id', personId); + getRelationships: async (personId) => { + // person1_id = personId OR person2_id = personId + const { data, error } = await supabase + .from('relationships') + .select('*') + .or(`person1_id.eq.${personId},person2_id.eq.${personId}`); + if (error) throw error; + return data; }, - filterBySide: async(personId, side) => { - return db('relationships').where('person1_id', personId).andWhere('side',side); + filterBySide: async (personId, side) => { + const { data, error } = await supabase + .from('relationships') + .select('*') + .eq('person1_id', personId) + .eq('side', side); + if (error) throw error; + return data; }, getRelationshipbyId: async (personId) => { - return db('relationships').where('person1_id', personId).andWhere('person2_id', personId); + const { data, error } = await supabase + .from('relationships') + .select('*') + .eq('person1_id', personId) + .eq('person2_id', personId); + if (error) throw error; + return data; }, getRelationshipByUser: async (userId) => { - return db('relationship').where('userId', userId).select('*'); + const { data, error } = await supabase + .from('relationships') + .select('*') + .eq('userid', userId); + if (error) throw error; + return data; }, getRelationshipByOtherUser: async (userId) => { - return db('relationship').whereNot('userId', userId).select('*'); + const { data, error } = await supabase + .from('relationships') + .select('*') + .not('userid', 'eq', userId); + if (error) throw error; + return data; }, deleteByUser: async (userId) => { - return db('relationship').where({userId}).del(); + const { error } = await supabase + .from('relationships') + .delete() + .eq('userid', userId); + if (error) throw error; } - - - }; module.exports = Relationships; diff --git a/server/models/sharedTreeModel.js b/server/models/sharedTreeModel.js index 6ac65ec..8c128f8 100644 --- a/server/models/sharedTreeModel.js +++ b/server/models/sharedTreeModel.js @@ -1,53 +1,65 @@ -const db = require('../db/knex'); -const Relationships = require('./relationshipModel'); +// sharedTreeModel.js - the model for the sharedTrees table +// this file was replaced with the supabase model +const supabase = require('../lib/supabase'); -const sharedTrees ={ - addSharedTree: async(data) => { - return db('sharedTrees').insert(data, ['id', 'token']) +// all functions for the sharedTree to interact with the database +const sharedTrees = { + addSharedTree: async (data) => { + // Table and column names are lowercase in Postgres + const { data: inserted, error } = await supabase + .from('sharedtrees') + .insert([ data ]) + .select('*') + .single(); + if (error) throw error; + return inserted; }, getALLSharedTree: async () => { - return db('sharedTrees').select('*'); + const { data, error } = await supabase + .from('sharedtrees') + .select('*'); + if (error) throw error; + return data; }, - - getSharedTreeById: async(id) =>{ - return db('sharedTrees').where('sharedTreeID',id).first(); + getSharedTreeById: async (id) => { + const { data, error } = await supabase + .from('sharedtrees') + .select('*') + .eq('sharedtreeid', id) + .single(); + if (error) throw error; + return data; }, - getSharedTreebySender: async(id) => { - return db('sharedTrees').where('senderId', id); + getSharedTreebySender: async (id) => { + const { data, error } = await supabase + .from('sharedtrees') + .select('*') + .eq('senderid', id); + if (error) throw error; + return data; }, - getSharedTreebyReciever: async(id) => { - return db('sharedTrees').where({ recieverId: id }).select('*'); + getSharedTreebyReciever: async (id) => { + const { data, error } = await supabase + .from('sharedtrees') + .select('*') + .eq('recieverid', id); + if (error) throw error; + return data; }, getSharedTreeByToken: async (token) => { - return db('sharedTrees').where('token', token).first(); - }, - - shareTree: async(data) => { - return db('relationship').where('person1_id', personId).andWhere('side','side'); - }, - - mergeTree: async(id, data) => { - for (const member of data){ - await db('treeMembers').insert({ - owner_id: recieverID, - name : member.name, - relationship: member.relationship, - }); - } - return {message: "Members merged successfully"}; - - }, - - getMemberstoMerge: async(senderId, recieverId) => { - return db('sharedTrees').where(senderId, senderId).select('*'); + const { data, error } = await supabase + .from('sharedtrees') + .select('*') + .eq('token', token) + .maybeSingle(); + if (error) throw error; + return data; } - - }; module.exports = sharedTrees; \ No newline at end of file diff --git a/server/models/treeInfoModel.js b/server/models/treeInfoModel.js index 82627e1..7ef2122 100644 --- a/server/models/treeInfoModel.js +++ b/server/models/treeInfoModel.js @@ -1,18 +1,36 @@ -const db = require('../db/knex'); +// treeInfoModel.js - model for treeInfo table (Supabase) +const supabase = require('../lib/supabase'); const treeInfo = { addObject: async (data) => { - return db('treeInfo').insert(data, ['id']); + const { data: inserted, error } = await supabase + .from('treeinfo') + .insert([ data ]) + .select('*') + .single(); + if (error) throw error; + return inserted; }, - updateObject: async (id, data) => { - await db('treeInfo').where({ userId: id }).update(data); - const updatedObject = await db('treeInfo').where({ id }).first(); - return updatedObject; + updateObject: async (userId, data) => { + const { data: updated, error } = await supabase + .from('treeinfo') + .update(data) + .eq('userid', userId) + .select('*') + .single(); + if (error) throw error; + return updated; }, - getObject: async (id) => { - return db('treeInfo').where({ userId: id }).first(); + getObject: async (userId) => { + const { data, error } = await supabase + .from('treeinfo') + .select('*') + .eq('userid', userId) + .maybeSingle(); + if (error) throw error; + return data; }, }; diff --git a/server/models/treeMemberModel.js b/server/models/treeMemberModel.js index 459f6a5..5badff6 100644 --- a/server/models/treeMemberModel.js +++ b/server/models/treeMemberModel.js @@ -1,53 +1,148 @@ -const db = require('../db/knex'); +// treeMemberModel.js - the model for the treeMembers table +// this file was replaced with the supabase model +const supabase = require('../lib/supabase'); +// all functions for the treeMember to interact with the database const treeMember = { addMember: async (data) => { - return db('treeMembers').insert(data, ['id']); + // Map camelCase to lowercase column names for Postgres + const mappedData = { + firstname: data.firstName || data.firstname, + lastname: data.lastName || data.lastname, + birthdate: data.birthDate || data.birthdate, + deathdate: data.deathDate || data.deathdate, + location: data.location, + phonenumber: data.phoneNumber || data.phonenumber, + userid: data.userId || data.userid, + memberuserid: data.memberUserId || data.memberuserid, + }; + // Remove undefined/null values + Object.keys(mappedData).forEach(key => mappedData[key] === undefined && delete mappedData[key]); + + // Validate that userid is an integer (not a UUID) + if (mappedData.userid && (typeof mappedData.userid === 'string' && mappedData.userid.includes('-'))) { + throw new Error(`Invalid userid: expected integer, got UUID: ${mappedData.userid}`); + } + if (mappedData.memberuserid && (typeof mappedData.memberuserid === 'string' && mappedData.memberuserid.includes('-'))) { + throw new Error(`Invalid memberuserid: expected integer, got UUID: ${mappedData.memberuserid}`); + } + + console.log('addMember mappedData:', JSON.stringify(mappedData, null, 2)); + const { data: inserted, error } = await supabase + .from('treemembers') + .insert([ mappedData ]) + .select('id') + .single(); + if (error) { + console.error('addMember Supabase error:', error); + throw error; + } + return inserted; }, getAllMembers: async () => { - return db('treeMembers').select('*'); + const { data, error } = await supabase + .from('treemembers') + .select('*'); + if (error) throw error; + return data; }, - getAllMembersbyId: async (id) => { - return db('treeMembers').where({id}).select('*'); + const { data, error } = await supabase + .from('treemembers') + .select('*') + .eq('id', id); + if (error) throw error; + return data; }, - getMemberById: async (id) => { // Fixed the typo - return db('treeMembers').where({ id }).first(); + getMemberById: async (id) => { + const { data, error } = await supabase + .from('treemembers') + .select('*') + .eq('id', id) + .single(); + if (error) throw error; + return data; }, getMembersByUser: async (userId) => { - return db('treeMembers').where({userId}).select('*'); + const { data, error } = await supabase + .from('treemembers') + .select('*') + .eq('userid', userId); + if (error) throw error; + return data; }, getMembeByOtherUser: async (userId) => { - return db('treeMembers').whereNot({userId}).select('*'); - + const { data, error } = await supabase + .from('treemembers') + .select('*') + .not('userid', 'eq', userId); + if (error) throw error; + return data; }, + // i cant get this to workkkkkkk updateMemberInfo: async (id, data) => { - await db('treeMembers').where({ id }).update(data); - const updatedRecord = await db('treeMembers').where({ id }).first(); - return updatedRecord; + // Map camelCase to lowercase column names for Postgres + const mappedData = {}; + if (data.firstName !== undefined) mappedData.firstname = data.firstName; + if (data.lastName !== undefined) mappedData.lastname = data.lastName; + if (data.birthDate !== undefined) mappedData.birthdate = data.birthDate; + if (data.deathDate !== undefined) mappedData.deathdate = data.deathDate; + if (data.location !== undefined) mappedData.location = data.location; + if (data.phoneNumber !== undefined) mappedData.phonenumber = data.phoneNumber; + if (data.userId !== undefined) mappedData.userid = data.userId; + if (data.memberUserId !== undefined) mappedData.memberuserid = data.memberUserId; + // Also handle lowercase variants + if (data.firstname !== undefined) mappedData.firstname = data.firstname; + if (data.lastname !== undefined) mappedData.lastname = data.lastname; + if (data.birthdate !== undefined) mappedData.birthdate = data.birthdate; + if (data.deathdate !== undefined) mappedData.deathdate = data.deathdate; + if (data.phonenumber !== undefined) mappedData.phonenumber = data.phonenumber; + if (data.userid !== undefined) mappedData.userid = data.userid; + if (data.memberuserid !== undefined) mappedData.memberuserid = data.memberuserid; + const { data: updated, error } = await supabase + .from('treemembers') + .update(mappedData) + .eq('id', id) + .select('*') + .single(); + if (error) throw error; + return updated; }, + assignNewMemberRelationship: async (recieverId, getMemberById, relationshipType) => { - return db('treeMembers').where({person1_id: recieverId, person2_id: recieverId}).update({relationshipType: relationshipType}) + // Update an existing relationship record tying two members together + const { error } = await supabase + .from('relationships') + .update({ relationshipType }) + .match({ person1_id: recieverId, person2_id: getMemberById }); + if (error) throw error; + return { success: true }; }, - deleteByUser: async (userId) => { - return db('treeMembers').where({userId}).del(); + const { error } = await supabase + .from('treemembers') + .delete() + .eq('userid', userId); + if (error) throw error; }, getActiveMemberId: async (id) => { - // userId and memberUserId are both equal to the id - return db('treeMembers').where({userId: id, memberUserId: id}).first(); - + const { data, error } = await supabase + .from('treemembers') + .select('*') + .eq('userid', id) + .eq('memberuserid', id) + .maybeSingle(); + if (error) throw error; + return data; } - - }; module.exports = treeMember; diff --git a/server/models/treeSummaryModel.js b/server/models/treeSummaryModel.js index f83847d..f82b830 100644 --- a/server/models/treeSummaryModel.js +++ b/server/models/treeSummaryModel.js @@ -1,16 +1,58 @@ -const db = require('../db/knex'); +// treeSummaryModel.js - model for tree summaries (Supabase) +// Note: This table may need to be created in Supabase if it doesn't exist +const supabase = require('../lib/supabase'); const treeSummary = { getSummaryByUser: async (userId) => { - return db('userTreeSummaries').where({userId}).first(); + // Resolve UUID to integer if needed + let userIdInt = userId; + if (typeof userId === 'string' && userId.includes('-')) { + const User = require('./userModel'); + userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) return null; + } + const { data, error } = await supabase + .from('usertreesummaries') + .select('*') + .eq('userid', userIdInt) + .maybeSingle(); + if (error) throw error; + return data; }, - createSummary: async (userId, userData) =>{ - return db('userTreeSummaries').insert({'userId': userId, 'currentTreeSummary': userData}); + createSummary: async (userId, userData) => { + // Resolve UUID to integer if needed + let userIdInt = userId; + if (typeof userId === 'string' && userId.includes('-')) { + const User = require('./userModel'); + userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) throw new Error('User not found'); + } + const { data, error } = await supabase + .from('usertreesummaries') + .insert([{ userid: userIdInt, currenttreesummary: userData }]) + .select('*') + .single(); + if (error) throw error; + return data; }, - updateSummary: async (userId, userData) =>{ - return db('userTreeSummaries').where({userId}).update({'currentTreeSummary': userData}); + updateSummary: async (userId, userData) => { + // Resolve UUID to integer if needed + let userIdInt = userId; + if (typeof userId === 'string' && userId.includes('-')) { + const User = require('./userModel'); + userIdInt = await User.resolveUserIdFromAuthUid(userId); + if (!userIdInt) throw new Error('User not found'); + } + const { data, error } = await supabase + .from('usertreesummaries') + .update({ currenttreesummary: userData }) + .eq('userid', userIdInt) + .select('*') + .single(); + if (error) throw error; + return data; } }; diff --git a/server/models/userModel.js b/server/models/userModel.js index 49d9eae..b7c5f7d 100644 --- a/server/models/userModel.js +++ b/server/models/userModel.js @@ -1,32 +1,112 @@ -const db = require('../db/knex'); -const { get } = require('../routes/treeMemberRoute'); +// userModel.js - the model for the user table +// this file was replaced with the supabase model +const supabase = require('../lib/supabase'); +// all functions for the user to interact with the database const User = { register: async (userData) => { - return db('users').insert(userData, ['id', 'firstName', 'lastName', 'email']); + const { data, error } = await supabase + .from('users') + .insert([ userData ]) + .select('id, firstname, lastname, email, phonenumber, birthdate') + .single(); + if (error) throw error; + return data; }, findByEmail: async (email) => { - return db('users').where({email}).first(); + const { data, error } = await supabase + .from('users') + .select('*') + .eq('email', email) + .maybeSingle(); + if (error) throw error; + return data; }, findById: async (id) => { - return db('users').where({id}).first(); + const { data, error } = await supabase + .from('users') + .select('*') + .eq('id', id) + .single(); + if (error) throw error; + return data; }, - updateUserInfo: async(id, userData) => { - return db('users').insert(userData, '').where({id}).first().insert(userData, []); + updateUserInfo: async (id, userData) => { + const { data, error } = await supabase + .from('users') + .update(userData) + .eq('id', id) + .select('*') + .single(); + if (error) throw error; + return data; }, - deleteUser: async (id) => { - return db('users').where({id}).del(); + const { error } = await supabase + .from('users') + .delete() + .eq('id', id); + if (error) throw error; }, getAllUsers: async () => { - return db('users').select('*'); + const { data, error } = await supabase + .from('users') + .select('*'); + if (error) throw error; + return data; + }, + + findByAuthUid: async (authUid) => { + const { data, error } = await supabase + .from('users') + .select('*') + .eq('auth_uid', authUid) + .maybeSingle(); + if (error) throw error; + return data; + }, + + upsertByAuthUser: async ({ auth_uid, email, username, firstName, lastName, phoneNumber, birthDate }) => { + // Map to lowercase columns and drop null/undefined so we don't overwrite with nulls + const rawPayload = { + auth_uid, + email, + username, + firstname: firstName, + lastname: lastName, + phonenumber: phoneNumber, + birthdate: birthDate, + }; + const payload = Object.fromEntries( + Object.entries(rawPayload).filter(([_, v]) => v !== undefined && v !== null && v !== '') + ); + const { data, error } = await supabase + .from('users') + .upsert([ payload ], { onConflict: 'auth_uid' }) + .select('id, auth_uid, email, username, firstname, lastname, phonenumber, birthdate') + .single(); + if (error) throw error; + return data; }, +}; +// Helper to resolve UUID (auth_uid) to integer user ID +const resolveUserIdFromAuthUid = async (authUidOrIntId) => { + // If it's already an integer, return it + if (!isNaN(authUidOrIntId) && !authUidOrIntId.toString().includes('-')) { + return parseInt(authUidOrIntId); + } + // Otherwise look up by auth_uid + const user = await User.findByAuthUid(authUidOrIntId); + if (!user) return null; + return user.id; }; +User.resolveUserIdFromAuthUid = resolveUserIdFromAuthUid; + module.exports = User; \ No newline at end of file diff --git a/server/mysql-connection.js b/server/mysql-connection.js deleted file mode 100644 index 191170d..0000000 --- a/server/mysql-connection.js +++ /dev/null @@ -1,43 +0,0 @@ -require('dotenv').config(); -const mysql = require('mysql2'); - -console.log('Database Config:', process.env.DB_USER, process.env.DB_PASSWORD, process.env.DB_DATABASE); - -const connection = mysql.createConnection({ - host: process.env.DB_HOST, // localhost - user: process.env.DB_USER, // Make sure DB_USER is set - password: process.env.DB_PASSWORD, // Ensure DB_PASSWORD is set - database: process.env.DB_DATABASE, // Ensure DB_DATABASE is set - port: process.env.DB_PORT || 3306 // Port should be 3306 -}); - -connection.connect((err) => { - if (err) { - console.error('Error connecting to MySQL:', err.stack); - return; - } - - console.log('Connected to MySQL as id ' + connection.threadId); - - // Example query to check connection - - connection.query('SELECT DATABASE()', (err, results) => { - if (err) { - console.error('Error running query:', err.stack); - return; - } - console.log('Connected to the database:', results[0]['DATABASE()']); - }); - - connection.query('SHOW DATABASES', (err, results) => { - if (err) { - console.error('Error fetching databases:', err); - } else { - console.log('Databases:', results.map(db => db.Database)); - } - connection.end(); // Close the connection - }); - - // Close the connection - connection.end(); -}); diff --git a/server/package-lock.json b/server/package-lock.json index 99734e4..56effa1 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@supabase/supabase-js": "^2.74.0", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.5.0", @@ -562,6 +563,104 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@supabase/auth-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.74.0.tgz", + "integrity": "sha512-EJYDxYhBCOS40VJvfQ5zSjo8Ku7JbTICLTcmXt4xHMQZt4IumpRfHg11exXI9uZ6G7fhsQlNgbzDhFN4Ni9NnA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.74.0.tgz", + "integrity": "sha512-VqWYa981t7xtIFVf7LRb9meklHckbH/tqwaML5P3LgvlaZHpoSPjMCNLcquuLYiJLxnh2rio7IxLh+VlvRvSWw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.74.0.tgz", + "integrity": "sha512-9Ypa2eS0Ib/YQClE+BhDSjx7OKjYEF6LAGjTB8X4HucdboGEwR0LZKctNfw6V0PPIAVjjzZxIlNBXGv0ypIkHw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.74.0.tgz", + "integrity": "sha512-K5VqpA4/7RO1u1nyD5ICFKzWKu58bIDcPxHY0aFA7MyWkFd0pzi/XYXeoSsAifnD9p72gPIpgxVXCQZKJg1ktQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.74.0.tgz", + "integrity": "sha512-o0cTQdMqHh4ERDLtjUp1/KGPbQoNwKRxUh6f8+KQyjC5DSmiw/r+jgFe/WHh067aW+WU8nA9Ytw9ag7OhzxEkQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.74.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.74.0.tgz", + "integrity": "sha512-IEMM/V6gKdP+N/X31KDIczVzghDpiPWFGLNjS8Rus71KvV6y6ueLrrE/JGCHDrU+9pq5copF3iCa0YQh+9Lq9Q==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.74.0", + "@supabase/functions-js": "2.74.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.74.0", + "@supabase/realtime-js": "2.74.0", + "@supabase/storage-js": "2.74.0" + } + }, + "node_modules/@types/node": { + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", + "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2265,6 +2364,12 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2284,6 +2389,12 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -2343,6 +2454,43 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/server/package.json b/server/package.json index 6a0f438..cd8e1cb 100644 --- a/server/package.json +++ b/server/package.json @@ -11,6 +11,7 @@ "author": "", "license": "ISC", "dependencies": { + "@supabase/supabase-js": "^2.74.0", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "dotenv": "^16.5.0", diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index a14ab28..4b3636e 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -2,16 +2,13 @@ const express = require('express'); const router = express.Router(); -const { register, login, deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); // Assuming you have a controller for your registration logic +const { deleteByUser, findByEmail, findById, getAllUsers, syncAuthUser } = require('../controllers/authController'); -console.log('Register function:', register); - -router.post('/register', register); -router.post('/login', login); router.delete('/remove/:id', deleteByUser); router.get('/user/:id', findById); router.get('/user/email/:email', findByEmail); router.get('/users', getAllUsers); +router.post('/sync', syncAuthUser); module.exports = router; diff --git a/server/server.js b/server/server.js index 7cf76c9..f2539c3 100644 --- a/server/server.js +++ b/server/server.js @@ -1,31 +1,19 @@ // server.js const express = require('express'); -const knex = require('knex'); const dotenv = require('dotenv'); const cors = require('cors'); -const knexConfig = require('./knexfile'); const authRoutes = require('./routes/authRoutes'); -const treeMemberRoutes = require('./routes/treeMemberRoute'); // Fixed typo -const relationshipRoutes = require('./routes/relationshipRoutes'); // Fixed typo +const treeMemberRoutes = require('./routes/treeMemberRoute'); +const relationshipRoutes = require('./routes/relationshipRoutes'); const sharedTreeRoutes = require('./routes/sharedTreeRoutes'); const backupRoutes = require('./routes/backupRoutes'); -const treeInfoRoutes = require('./routes/treeInfoRoutes'); // Fixed typo +const treeInfoRoutes = require('./routes/treeInfoRoutes'); dotenv.config(); const app = express(); const port = process.env.PORT || 5000; -const db = knex({ - client: 'mysql2', - connection: { - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME - } -}); - app.use(express.json()); app.use(cors()); @@ -39,20 +27,3 @@ app.use('/api/tree-info', treeInfoRoutes); app.listen(port, () => { console.log(`Server running on port ${port}`); }); - - -// Example route -- follow this template for other routes - -/* -app.get('/api/items', async (req, res) => { - try { - const items = await db('items').select('*'); - res.json(items); - } catch (error) { - res.status(500).json({ error: 'An error occurred' }); - } - }); - -*/ - - From c38b0c8d365a2a33bd7ef8655a1e57b70110f494 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Sat, 8 Nov 2025 16:08:12 -0600 Subject: [PATCH 21/86] added tree service for more centralized fetching and resolved conflicts --- .../AddFamilyMember/AddFamilyMember.js | 72 +++--- client/src/components/AddToTree/AddToTree.js | 4 +- client/src/pages/Account/Account.js | 6 +- .../src/pages/CreateAccount/CreateAccount.js | 3 +- client/src/pages/Tree/Tree.js | 5 +- client/src/services/familyTreeService.js | 95 ++++++++ .../utils/{treeUtils.js => relationUtil.js} | 0 server/models/treeMemberModel.js | 1 + server/package-lock.json | 226 +----------------- server/package.json | 1 - 10 files changed, 137 insertions(+), 276 deletions(-) create mode 100644 client/src/services/familyTreeService.js rename client/src/utils/{treeUtils.js => relationUtil.js} (100%) diff --git a/client/src/components/AddFamilyMember/AddFamilyMember.js b/client/src/components/AddFamilyMember/AddFamilyMember.js index 740db48..9a9b0dc 100644 --- a/client/src/components/AddFamilyMember/AddFamilyMember.js +++ b/client/src/components/AddFamilyMember/AddFamilyMember.js @@ -8,6 +8,7 @@ import './popup.css'; import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; import { ReactComponent as ImportIcon } from '../../assets/import.svg'; import { useCurrentUser } from '../../CurrentUserProvider'; +import { familyTreeService } from '../../services/familyTreeService'; // TODO: make form clear when dismissed by clicking outside of modal // make sync contact button functional @@ -68,40 +69,28 @@ function AddFamilyMemberPopup({ trigger, userid }) { // get non-friends const fetchResults = async () => { - fetch(`http://localhost:5000/api/family-members/user/${currentAccountID}`, getRequestOptions) // gets all family members + family.current = await familyTreeService.getFamilyMembersByUserId(currentUserID); + + // fetch all users (this is totally scalable) + fetch(`http://localhost:5000/api/auth/users`, getRequestOptions) .then(async(response) => { if (response.ok) { const responseData = await response.json(); - console.log(responseData); - family.current = responseData; - } + users.current = responseData.filter(user => + user.username.toLowerCase().includes(searchTerm.toLowerCase()) && + !family.current.some(member => member.memberUserId === user.id) + ); + setSearchResults(users.current); + } else { // print message in return body console.error('Error:', response); } }) - // TODO: make this - // fetch all users (this is totally scalable) - .then(fetch(`http://localhost:5000/api/auth/users`, getRequestOptions) - .then(async(response) => { - if (response.ok) { - const responseData = await response.json(); - users.current = responseData.filter(user => - user.username.toLowerCase().includes(searchTerm.toLowerCase()) && - !family.current.some(member => member.memberUserId === user.id) - ); - setSearchResults(users.current); - } - else { - // print message in return body - console.error('Error:', response); - } - }) - ) }; fetchResults(); - }, [searchTerm, currentAccountID]); + }, [searchTerm, currentAccountID, currentUserID]); @@ -120,36 +109,31 @@ function AddFamilyMemberPopup({ trigger, userid }) { console.log("Form data:", data); // Log the form data to see what we're getting setErrorMessage(""); try { - let memberId = data.selectedMember; - const selectedUser = users.current.find(user => user.id === Number(memberId)); - - let requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - "firstName": selectedUser.username.split(" ")[0] || selectedUser.firstName, - "lastName": selectedUser.username.split(" ")[1] || selectedUser.lastName, - "birthDate": selectedUser.birthDate || null, - "deathDate": selectedUser.deathDate || null, - "location": selectedUser.location || null, - "phoneNumber": selectedUser.phoneNumber || null, - "userId": currentUserID, // The user adding the family member - "memberUserId": selectedUser.id, // Existing user's ID - "gender": selectedUser.gender, - }) + const selectedUser = users.current.find(user => user.id === Number(data.selectedMember)); + + const selectedUserData = { + firstName: selectedUser.firstName, + lastName: selectedUser.lastName, + birthDate: selectedUser.birthDate || null, + deathDate: selectedUser.deathDate || null, + location: selectedUser.location || null, + phoneNumber: selectedUser.phoneNumber || null, + userId: currentUserID, // The user adding the family member + memberUserId: selectedUser.id, // Existing user's ID + gender: selectedUser.gender, }; + let treeUserId = currentUserID; - let treeMemberId; - const memberResponse = await fetch(`http://localhost:5000/api/family-members/`, requestOptions); // add new family member - console.log('Member response', memberResponse); + const memberResponse = await familyTreeService.createFamilyMember(selectedUserData); const memberData = await memberResponse.json(); if (!memberResponse.ok) { throw new Error(memberData.error || 'Failed to add family member'); } console.log('memberData', memberData); - treeMemberId = memberData.member; + + const treeMemberId = memberData.member.id; // relationship table uses id's from treeMembers table, not user ids! let relRequestOptions = { diff --git a/client/src/components/AddToTree/AddToTree.js b/client/src/components/AddToTree/AddToTree.js index 8e53b76..1bdacb0 100644 --- a/client/src/components/AddToTree/AddToTree.js +++ b/client/src/components/AddToTree/AddToTree.js @@ -7,7 +7,8 @@ import './popup.css'; import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; import { Link } from 'react-router-dom'; import { useCurrentUser } from '../../CurrentUserProvider'; // import the context -import { addRelationship }from '../../utils/treeUtils.js'; +import { addRelationship }from '../../utils/relationUtil.js'; +import { familyTreeService } from '../../services/familyTreeService'; // john jane parent jane is john's mom function AddTreeMember (userId, accountUserId, relativeUserId, relativeRelationship, accountUserName, treeData, results, currentAccountID) { @@ -71,6 +72,7 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc var filteredResults = useRef([]); const { currentAccountID } = useCurrentUser(); const [treeData, setTreeData] = useState([]); + // retrieve a list of all family members that are within the tree object useEffect(() => { diff --git a/client/src/pages/Account/Account.js b/client/src/pages/Account/Account.js index a119abb..f4f0f0f 100644 --- a/client/src/pages/Account/Account.js +++ b/client/src/pages/Account/Account.js @@ -44,7 +44,8 @@ function Account() { state: '', country: '', phone_number: '', - zipcode: '' + zipcode: '', + gender: '' }) // Fetch user info - check if it's a Supabase user or family member @@ -67,7 +68,8 @@ function Account() { state: supabaseUser.user_metadata?.state || '', country: supabaseUser.user_metadata?.country || '', phone_number: supabaseUser.user_metadata?.phone_number || '', - zipcode: supabaseUser.user_metadata?.zipcode || '' + zipcode: supabaseUser.user_metadata?.zipcode || '', + gender: supabaseUser.user_metadata?.gender || '' }); setOwnAccount(true); return; diff --git a/client/src/pages/CreateAccount/CreateAccount.js b/client/src/pages/CreateAccount/CreateAccount.js index 261055d..b2df535 100644 --- a/client/src/pages/CreateAccount/CreateAccount.js +++ b/client/src/pages/CreateAccount/CreateAccount.js @@ -70,7 +70,8 @@ const CreateAccount = () => { memberUserId: data.user.id, gender: formData.gender }; - const memberId = await familyTreeService.createFamilyMember(memberData); + const memberResponse = await familyTreeService.createFamilyMember(memberData); + const memberId = memberResponse.member.id; // add new user to their tree object await familyTreeService.initializeTreeInfo(memberId, memberData, data.user.id); diff --git a/client/src/pages/Tree/Tree.js b/client/src/pages/Tree/Tree.js index d57891c..1114e1f 100644 --- a/client/src/pages/Tree/Tree.js +++ b/client/src/pages/Tree/Tree.js @@ -1,11 +1,10 @@ -import React, { useRef, useEffect } from 'react'; +import React, { useEffect } from 'react'; import * as styles from './styles'; // import { ReactComponent as TreeIcon } from '../../assets/background-tree.svg'; // background tree image from Figma; TODO configure overlay with tree svg import * as f3 from 'family-chart'; import './tree.css'; // styling adapted from family-chart package sample code import { ReactComponent as PlusSign } from '../../assets/plus-sign.svg'; import AddFamilyMemberPopup from '../../components/AddFamilyMember/AddFamilyMember'; -import { Link } from 'react-router-dom'; import NavBar from '../../components/NavBar/NavBar'; import { useLocation, Outlet } from 'react-router-dom'; import { useCurrentUser } from '../../CurrentUserProvider'; // import the context @@ -29,7 +28,7 @@ function FamilyTree() { existingChart.innerHTML = ''; } - chart = f3.createChart('#FamilyChart', data) + chart = f3.default.createChart('#FamilyChart', data) .setTransitionTime(500) .setCardXSpacing(250) .setCardYSpacing(150) diff --git a/client/src/services/familyTreeService.js b/client/src/services/familyTreeService.js new file mode 100644 index 0000000..e9da43e --- /dev/null +++ b/client/src/services/familyTreeService.js @@ -0,0 +1,95 @@ +export const familyTreeService = { + /** + * + * @param {*} memberData + * @returns treeMemberId + */ + async createFamilyMember(memberData) { + const response = await fetch(`http://localhost:5000/api/family-members/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + firstName: memberData.firstName, + lastName: memberData.lastName, + birthdate: memberData.birthdate, + email: memberData.email, + location: memberData.location, + phoneNumber: memberData.phoneNumber, + userId: memberData.userId, // who added this member + memberUserId: memberData.memberUserId, // the member's user account (if exists) + gender: memberData.gender, + }), + }); + const responseData = await response.json(); + if (!response.ok) { + throw new Error(responseData.message || 'Failed to create family member'); + } + return responseData; // returns memberId + }, + /** + * + * @param {*} memberId + * @param {*} memberData + * @param {*} userId + * @returns treeInfo Object + */ + async initializeTreeInfo(memberId, memberData, userId) { + const response = await fetch(`http://localhost:5000/api/tree-info/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + object: [{ + "id": memberId, + "data": { + "first name": memberData.firstName, + "last name": memberData.lastName, + "gender": memberData.gender, + }, + "rels": { + "children": [], + "spouses": [], + } + }], + userId: userId, // who is creating the tree + }), + }); + const responseData = await response.json(); + if (!response.ok) { + throw new Error(responseData.message || 'Failed to add member to tree'); + } + return responseData; + }, + + async getFamilyMembersByUserId(userId) { + const response = await fetch(`http://localhost:5000/api/family-members/user/${userId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const responseData = await response.json(); + if (!response.ok) { + throw new Error(responseData.message || 'Failed to fetch family members'); + } + return responseData; // returns all family members of the user + }, + + async getRegisteredUsers() { + const response = await fetch(`http://localhost:5000/api/users/`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to fetch registered users'); + } + + return response.json(); + } +}; \ No newline at end of file diff --git a/client/src/utils/treeUtils.js b/client/src/utils/relationUtil.js similarity index 100% rename from client/src/utils/treeUtils.js rename to client/src/utils/relationUtil.js diff --git a/server/models/treeMemberModel.js b/server/models/treeMemberModel.js index 5badff6..82ca4cc 100644 --- a/server/models/treeMemberModel.js +++ b/server/models/treeMemberModel.js @@ -15,6 +15,7 @@ const treeMember = { phonenumber: data.phoneNumber || data.phonenumber, userid: data.userId || data.userid, memberuserid: data.memberUserId || data.memberuserid, + gender: data.gender }; // Remove undefined/null values Object.keys(mappedData).forEach(key => mappedData[key] === undefined && delete mappedData[key]); diff --git a/server/package-lock.json b/server/package-lock.json index 56effa1..26c10dd 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,7 +15,6 @@ "dotenv": "^16.5.0", "express": "^4.21.1", "jsonwebtoken": "^9.0.2", - "knex": "^3.1.0", "mysql2": "^3.15.1", "nodemon": "^3.1.7" }, @@ -886,21 +885,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1087,7 +1071,9 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -1098,15 +1084,6 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1272,21 +1249,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/getopts": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", - "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==", - "license": "MIT" - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1418,15 +1380,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1448,21 +1401,6 @@ "node": ">=8" } }, - "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1582,86 +1520,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/knex": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", - "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", - "license": "MIT", - "dependencies": { - "colorette": "2.0.19", - "commander": "^10.0.0", - "debug": "4.3.4", - "escalade": "^3.1.1", - "esm": "^3.2.25", - "get-package-type": "^0.1.0", - "getopts": "2.3.0", - "interpret": "^2.2.0", - "lodash": "^4.17.21", - "pg-connection-string": "2.6.2", - "rechoir": "^0.8.0", - "resolve-from": "^5.0.0", - "tarn": "^3.0.2", - "tildify": "2.0.0" - }, - "bin": { - "knex": "bin/cli.js" - }, - "engines": { - "node": ">=16" - }, - "peerDependenciesMeta": { - "better-sqlite3": { - "optional": true - }, - "mysql": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-native": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "tedious": { - "optional": true - } - } - }, - "node_modules/knex/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/knex/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -1979,24 +1837,12 @@ "node": ">= 0.8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/pg-connection-string": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", - "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2086,44 +1932,6 @@ "node": ">=8.10.0" } }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2304,36 +2112,6 @@ "node": ">=4" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tarn": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", - "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/tildify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", - "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/server/package.json b/server/package.json index cd8e1cb..e64e1ea 100644 --- a/server/package.json +++ b/server/package.json @@ -17,7 +17,6 @@ "dotenv": "^16.5.0", "express": "^4.21.1", "jsonwebtoken": "^9.0.2", - "knex": "^3.1.0", "mysql2": "^3.15.1", "nodemon": "^3.1.7" }, From 49d056013d173017db63dda6f95e1bdf16b58c11 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Sun, 9 Nov 2025 16:07:14 -0600 Subject: [PATCH 22/86] registration member & tree init working --- .../src/pages/CreateAccount/CreateAccount.js | 15 ++-- client/src/services/familyTreeService.js | 51 +++++++---- server/controllers/treeInfoController.js | 14 ++- server/controllers/treeMemberController.js | 87 ++++++++++--------- server/models/treeInfoModel.js | 13 +-- server/models/treeMemberModel.js | 7 +- 6 files changed, 105 insertions(+), 82 deletions(-) diff --git a/client/src/pages/CreateAccount/CreateAccount.js b/client/src/pages/CreateAccount/CreateAccount.js index b2df535..9527f96 100644 --- a/client/src/pages/CreateAccount/CreateAccount.js +++ b/client/src/pages/CreateAccount/CreateAccount.js @@ -61,20 +61,19 @@ const CreateAccount = () => { // add new user as family member const memberData = { - first_name: formData.firstname, - last_name: formData.lastname, + firstname: formData.firstname, + lastname: formData.lastname, birthdate: formData.birthdate, - email: formData.email, location: `${formData.city}, ${formData.state}, ${formData.country}`, - userId: data.user.id, - memberUserId: data.user.id, + phonenumber: formData.phonenum, + userid: data.user.id, + memberuserid: data.user.id, gender: formData.gender }; - const memberResponse = await familyTreeService.createFamilyMember(memberData); - const memberId = memberResponse.member.id; + await familyTreeService.createFamilyMember(memberData); // add new user to their tree object - await familyTreeService.initializeTreeInfo(memberId, memberData, data.user.id); + await familyTreeService.initializeTreeInfo(data.user.id, memberData, data.user.id); console.log('Registration successful:', data); window.location.href = '/login'; // redirect after registration to login diff --git a/client/src/services/familyTreeService.js b/client/src/services/familyTreeService.js index e9da43e..94ac74a 100644 --- a/client/src/services/familyTreeService.js +++ b/client/src/services/familyTreeService.js @@ -5,26 +5,28 @@ export const familyTreeService = { * @returns treeMemberId */ async createFamilyMember(memberData) { + console.log('Creating family member with data:', memberData); const response = await fetch(`http://localhost:5000/api/family-members/`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - firstName: memberData.firstName, - lastName: memberData.lastName, - birthdate: memberData.birthdate, - email: memberData.email, - location: memberData.location, - phoneNumber: memberData.phoneNumber, - userId: memberData.userId, // who added this member - memberUserId: memberData.memberUserId, // the member's user account (if exists) - gender: memberData.gender, + "firstname": memberData.firstname, + "lastname": memberData.lastname, + "birthdate": memberData.birthdate, + "deathdate": memberData.deathdate, + "location": memberData.location, + "phonenumber": memberData.phonenum, + "userid": memberData.userid, // who added this member + "memberuserid": memberData.memberuserid, // the member's user account (if exists) + "gender": memberData.gender, }), }); const responseData = await response.json(); if (!response.ok) { - throw new Error(responseData.message || 'Failed to create family member'); + console.log('Failed to create family member:', responseData.error); + throw new Error(responseData.error); } return responseData; // returns memberId }, @@ -35,7 +37,8 @@ export const familyTreeService = { * @param {*} userId * @returns treeInfo Object */ - async initializeTreeInfo(memberId, memberData, userId) { + async initializeTreeInfo(memberId, memberData, userid) { + console.log('Initializing tree info for memberId:', memberId, 'with data:', memberData, 'for userId:', userid); const response = await fetch(`http://localhost:5000/api/tree-info/`, { method: 'POST', headers: { @@ -54,16 +57,21 @@ export const familyTreeService = { "spouses": [], } }], - userId: userId, // who is creating the tree + userid: userid, // who is creating the tree }), }); const responseData = await response.json(); if (!response.ok) { - throw new Error(responseData.message || 'Failed to add member to tree'); + console.error('Failed to add member to tree:', responseData.error); + throw new Error(responseData.error); } return responseData; }, - + /** + * + * @param {int} userId + * @returns JSON Object of the user's family members + */ async getFamilyMembersByUserId(userId) { const response = await fetch(`http://localhost:5000/api/family-members/user/${userId}`, { method: 'GET', @@ -73,11 +81,15 @@ export const familyTreeService = { }); const responseData = await response.json(); if (!response.ok) { - throw new Error(responseData.message || 'Failed to fetch family members'); + console.error('Failed to fetch family members:', responseData.error); + throw new Error(responseData.error || 'Failed to fetch family members'); } return responseData; // returns all family members of the user }, - + /** + * + * @returns JSON Array of all registered users + */ async getRegisteredUsers() { const response = await fetch(`http://localhost:5000/api/users/`, { method: 'GET', @@ -85,11 +97,12 @@ export const familyTreeService = { 'Content-Type': 'application/json', }, }); - + const responseData = await response.json(); if (!response.ok) { - throw new Error('Failed to fetch registered users'); + console.error('Failed to fetch registered users:', responseData.error); + throw new Error(responseData.error || 'Failed to fetch registered users'); } - return response.json(); + return responseData; } }; \ No newline at end of file diff --git a/server/controllers/treeInfoController.js b/server/controllers/treeInfoController.js index 825a654..41e270a 100644 --- a/server/controllers/treeInfoController.js +++ b/server/controllers/treeInfoController.js @@ -3,13 +3,21 @@ const User = require('../models/userModel'); const addObject = async (req, res) => { try { - const { object, userId } = req.body; + const { object, userid } = req.body; // Resolve UUID to integer user ID if needed - const userIdInt = await User.resolveUserIdFromAuthUid(userId) || userId; + const userIdInt = await User.resolveUserIdFromAuthUid(userid); + console.log('Resolved userIdInt:', userIdInt); + + if (!userIdInt) { + return res.status(400).json({ + error: 'Invalid user ID. User not found in database.', + received: userid + }); + } const newObject = await treeInfo.addObject({ object: JSON.stringify(object), - userId: userIdInt + userid: userIdInt }); res.status(201).json({ diff --git a/server/controllers/treeMemberController.js b/server/controllers/treeMemberController.js index 27c91d1..0c500a6 100644 --- a/server/controllers/treeMemberController.js +++ b/server/controllers/treeMemberController.js @@ -17,66 +17,67 @@ const User = require('../models/userModel'); const addTreeMember = async (req, res) => { try { - const { firstName, lastName, birthDate, deathDate, location, phoneNumber, relationships, userId, memberUserId, gender } = req.body; - + const { firstname, lastname, birthdate, deathdate, location, phonenumber, userid, memberuserid, gender } = req.body; + // Resolve UUIDs to integer user IDs - CRITICAL: database requires integers, not UUIDs - console.log('addTreeMember received userId:', userId, typeof userId); - const userIdInt = await User.resolveUserIdFromAuthUid(userId); + console.log('addTreeMember received userId:', userid, typeof userid); + const userIdInt = await User.resolveUserIdFromAuthUid(userid); console.log('Resolved userIdInt:', userIdInt, typeof userIdInt); if (!userIdInt) { return res.status(400).json({ error: 'Invalid user ID. User not found in database. Please sync your account first.', - received: userId + received: userid }); } - - const memberUserIdInt = memberUserId ? await User.resolveUserIdFromAuthUid(memberUserId) : null; - if (memberUserId && !memberUserIdInt) { + + const memberUserIdInt = memberuserid ? await User.resolveUserIdFromAuthUid(memberuserid) : null; + if (memberuserid && !memberUserIdInt) { return res.status(400).json({ error: 'Invalid member user ID. User not found in database.', - received: memberUserId + received: memberuserid }); } - const formattedBirthDate = formatDate(birthDate); - const formattedDeathDate = formatDate(deathDate); + const formattedBirthDate = formatDate(birthdate); + const formattedDeathDate = formatDate(deathdate); // ensure all necessary fields are passed in the request body const newMember = await treeMember.addMember({ - firstName, - lastName, - birthDate: formattedBirthDate, - deathDate: formattedDeathDate, - location, - phoneNumber, - userId: userIdInt, // Now guaranteed to be an integer - memberUserId: memberUserIdInt, // Now guaranteed to be an integer or null, - gender + firstname: firstname, + lastname: lastname, + birthdate: formattedBirthDate, + deathdate: formattedDeathDate, + location: location, + phonenumber: phonenumber, + userid: userIdInt, // Now guaranteed to be an integer + memberuserid: memberUserIdInt || null, // Now guaranteed to be an integer or null, + gender: gender }); - // if there are relationships, add them to the database - if (relationships && relationships.length > 0) { - for (const rel of relationships) { - // Ensure person2_id is an integer, not a UUID - let person2_id = rel.person2_id; - if (typeof person2_id === 'string' && person2_id.includes('-')) { - // If it looks like a UUID, try to resolve it - person2_id = await User.resolveUserIdFromAuthUid(person2_id); - if (!person2_id) { - console.error('Could not resolve person2_id UUID:', rel.person2_id); - continue; // Skip this relationship - } - } - await relationship.addRelationship({ - person1_id: newMember.id, - person2_id: person2_id, - relationshipType: rel.relationshipType || 'sibling', - relationshipStatus: 'active', - userId: userIdInt // Need to include userId for the relationship - }); - } - } - //realtionship function does not work + + // // if there are relationships, add them to the database + // if (relationships && relationships.length > 0) { + // for (const rel of relationships) { + // // Ensure person2_id is an integer, not a UUID + // let person2_id = rel.person2_id; + // if (typeof person2_id === 'string' && person2_id.includes('-')) { + // // If it looks like a UUID, try to resolve it + // person2_id = await User.resolveUserIdFromAuthUid(person2_id); + // if (!person2_id) { + // console.error('Could not resolve person2_id UUID:', rel.person2_id); + // continue; // Skip this relationship + // } + // } + // await relationship.addRelationship({ + // person1_id: newMember.id, + // person2_id: person2_id, + // relationshipType: rel.relationshipType || 'sibling', + // relationshipStatus: 'active', + // userId: userIdInt // Need to include userId for the relationship + // }); + // } + // } + //relationship function does not work res.status(201).json({ message: 'Family member added successfully', diff --git a/server/models/treeInfoModel.js b/server/models/treeInfoModel.js index 7ef2122..314e32d 100644 --- a/server/models/treeInfoModel.js +++ b/server/models/treeInfoModel.js @@ -3,31 +3,32 @@ const supabase = require('../lib/supabase'); const treeInfo = { addObject: async (data) => { - const { data: inserted, error } = await supabase + console.log('addObject called with userid:', data.userid, 'and object:', data.object); + const { inserted, error } = await supabase .from('treeinfo') - .insert([ data ]) + .insert(data) .select('*') .single(); if (error) throw error; return inserted; }, - updateObject: async (userId, data) => { + updateObject: async (userid, data) => { const { data: updated, error } = await supabase .from('treeinfo') .update(data) - .eq('userid', userId) + .eq('userid', userid) .select('*') .single(); if (error) throw error; return updated; }, - getObject: async (userId) => { + getObject: async (userid) => { const { data, error } = await supabase .from('treeinfo') .select('*') - .eq('userid', userId) + .eq('userid', userid) .maybeSingle(); if (error) throw error; return data; diff --git a/server/models/treeMemberModel.js b/server/models/treeMemberModel.js index 82ca4cc..788cd77 100644 --- a/server/models/treeMemberModel.js +++ b/server/models/treeMemberModel.js @@ -5,6 +5,7 @@ const supabase = require('../lib/supabase'); // all functions for the treeMember to interact with the database const treeMember = { addMember: async (data) => { + console.log('addMember received data:', data); // Map camelCase to lowercase column names for Postgres const mappedData = { firstname: data.firstName || data.firstname, @@ -17,8 +18,8 @@ const treeMember = { memberuserid: data.memberUserId || data.memberuserid, gender: data.gender }; - // Remove undefined/null values - Object.keys(mappedData).forEach(key => mappedData[key] === undefined && delete mappedData[key]); + // // Remove undefined/null values + // Object.keys(mappedData).forEach(key => mappedData[key] === undefined && delete mappedData[key]); // Validate that userid is an integer (not a UUID) if (mappedData.userid && (typeof mappedData.userid === 'string' && mappedData.userid.includes('-'))) { @@ -28,7 +29,7 @@ const treeMember = { throw new Error(`Invalid memberuserid: expected integer, got UUID: ${mappedData.memberuserid}`); } - console.log('addMember mappedData:', JSON.stringify(mappedData, null, 2)); + console.log('addMember mappedData:', mappedData); const { data: inserted, error } = await supabase .from('treemembers') .insert([ mappedData ]) From 4c89d2812f00737ec752bc839d8abcd64789e176 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Mon, 10 Nov 2025 21:47:03 -0600 Subject: [PATCH 23/86] fixed tree generation + updated npm package and style --- client/package-lock.json | 8 +- client/package.json | 2 +- client/src/pages/Tree/Tree.js | 88 +++++----- client/src/pages/Tree/styles.js | 1 + client/src/pages/Tree/tree.css | 199 ++++++++--------------- client/src/services/familyTreeService.js | 6 +- 6 files changed, 121 insertions(+), 183 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 8b74732..2d46024 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,7 +15,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.7.7", "d3": "^7.9.0", - "family-chart": "^0.8.1", + "family-chart": "^0.9.0", "knex": "^3.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -9558,9 +9558,9 @@ "license": "MIT" }, "node_modules/family-chart": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/family-chart/-/family-chart-0.8.1.tgz", - "integrity": "sha512-u8FwMJle5Q4daNNCp2vp33NC2bN25IxPLHmBPBx/+fJ99HcIAa3Nu2lm7psPX0634a6pN27xRKruQGsv5zFGQg==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/family-chart/-/family-chart-0.9.0.tgz", + "integrity": "sha512-+JdLr1Oo+YFnQWUXgdnk4nCMTbe1MXKdpbx3KEBXPeq2oX+2v5ccmrcK39CZ761/zQfgSHFZ2cT/+gbaeeACcA==", "license": "ISC", "dependencies": { "d3": "^7.9.0" diff --git a/client/package.json b/client/package.json index f682f3d..f0b2061 100644 --- a/client/package.json +++ b/client/package.json @@ -10,7 +10,7 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.7.7", "d3": "^7.9.0", - "family-chart": "^0.8.1", + "family-chart": "^0.9.0", "knex": "^3.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/client/src/pages/Tree/Tree.js b/client/src/pages/Tree/Tree.js index 1114e1f..c61da2a 100644 --- a/client/src/pages/Tree/Tree.js +++ b/client/src/pages/Tree/Tree.js @@ -7,76 +7,82 @@ import { ReactComponent as PlusSign } from '../../assets/plus-sign.svg'; import AddFamilyMemberPopup from '../../components/AddFamilyMember/AddFamilyMember'; import NavBar from '../../components/NavBar/NavBar'; import { useLocation, Outlet } from 'react-router-dom'; -import { useCurrentUser } from '../../CurrentUserProvider'; // import the context - +import { useCurrentUser, supabaseUser } from '../../CurrentUserProvider'; // import the context // FamilyTree class structure derived from family-chart package sample code // see https://github.com/donatso/family-chart/ function FamilyTree() { - const contRef = React.createRef(); + const contRef = React.useRef(); const { currentAccountID } = useCurrentUser(); + useEffect(() => { - if (!contRef.current) return; - - let chart = null; - function create(data) { + if (!Array.isArray(data) || data.length === 0) { + console.error('Invalid data for createChart:', data); + return; + } + // Clean up any existing chart first const existingChart = document.querySelector('#FamilyChart'); if (existingChart) { existingChart.innerHTML = ''; } - chart = f3.default.createChart('#FamilyChart', data) - .setTransitionTime(500) + if (!contRef.current) return; + + try{ + const f3chart = f3.createChart('#FamilyChart', data) + .setTransitionTime(1000) .setCardXSpacing(250) .setCardYSpacing(150) - .setSingleParentEmptyCard(false, {label: ''}) + .setSingleParentEmptyCard(true, {label: 'ADD'}) .setShowSiblingsOfMain(true) .setOrientationVertical() - - - chart.setCardHtml() - .setCardDisplay([["first name"],[]]) - .setCardDim({}) - .setMiniTree(false) - .setStyle('imageCircle') - .setOnCardClick((e, data) => { - window.location.href = `/account/${data.data.id}`; - }); - chart.updateTree({initial: true}); + const f3Card = f3chart.setCardHtml() + .setCardDisplay([["first name"],[]]) + .setCardDim({}) + .setMiniTree(true) + .setStyle('imageCircle') + .setOnHoverPathToMain() + .setOnCardClick((e, data) => { + window.location.href = `/account/${data.data.id}`; + }); + + f3chart.updateTree({initial: true}); + } catch (error) { + console.error('Error creating family tree chart:', error); + } + } - let getRequestOptions = { + + fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, - }; - - fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`, getRequestOptions) - .then(async (response) => { - if (response.ok) { - let treeData = await response.json(); - const parsedData = treeData.object; - console.log("Tree data: ", parsedData); - create(parsedData); - } else { - console.error('Error in Loading Tree Data:', response); - } - }); - - - }, [contRef, currentAccountID]); + }) + .then(async (response) => { + if (response.ok) { + let treeData = await response.json(); + const parsedData = JSON.parse(treeData.object); + console.log("Tree data: ", parsedData); + create(parsedData); + } else { + console.error('Error in Loading Tree Data:', response.json().error); + window.alert('Failed to load family tree data. Please try again.'); + } + }); + }, [currentAccountID, contRef]); return
    ; + } // builds the actual page function Tree() { - const { currentAccountID, currentUserName, fetchCurrentUserID } = useCurrentUser(); // Use the hook in the function component - fetchCurrentUserID(); const location = useLocation(); + const { currentAccountID, supabaseUser } = useCurrentUser(); const isTreePage = location.pathname === '/tree'; document.body.style.overflow = 'hidden'; document.body.style.width = '100%'; @@ -97,7 +103,7 @@ function Tree() { width: '200px', borderColor : '#000000' }}/> -

    The {currentUserName?.split(" ")[1]} Family

    +

    The {supabaseUser?.user_metadata?.last_name} Family

    {/* add family member button */}
    diff --git a/client/src/pages/Tree/styles.js b/client/src/pages/Tree/styles.js index 550856c..1d0e8ab 100644 --- a/client/src/pages/Tree/styles.js +++ b/client/src/pages/Tree/styles.js @@ -59,6 +59,7 @@ export const HeaderStyle = { export const FamilyTreeContainerStyle = { width: '80%', height: '90vh', + margin: 'auto', borderStyle: 'double', // maxWidth: '800px', borderRadius: '30px' diff --git a/client/src/pages/Tree/tree.css b/client/src/pages/Tree/tree.css index 5d68791..6993b20 100644 --- a/client/src/pages/Tree/tree.css +++ b/client/src/pages/Tree/tree.css @@ -1,13 +1,33 @@ -.f3 { + .main-container { + @media screen and (max-width: 768px) { + flex-direction: column; + } + } + .f3 { --background-color: none; - + position: relative; + display: flex; font-family: 'Alata'; } - .f3 * { box-sizing: border-box; } - + .f3.f3-cont { + width:100%; + height:90vh; + max-height:90vh; + color:#fff + } + .f3 { + --female-color: rgb(196, 138, 146); + --male-color: rgb(120, 159, 172); + --genderless-color: lightgray; + --background-color: rgb(33, 33, 33); + --text-color: #0e0e0e; + + font-family: 'Alata', sans-serif; + } + .f3 .cursor-pointer { cursor: pointer; } @@ -18,77 +38,46 @@ .f3 svg.main_svg text { fill: currentColor; } - .f3 .card_add .card-body-rect { - fill: #3b5560; - stroke-width: 4px; - stroke: #fff; - cursor: pointer; - } - .f3 g.card_add text { - fill: #fff; - } - .f3 .card-main-outline { - stroke: currentColor; - stroke-width: 3px; - } - .f3 .card_family_tree rect { - transition: 0.3s; - } - .f3 .card_family_tree:hover rect { - transform: scale(1.1); - } - .f3.f3-cont { - width:100%; - height:900px; - max-height:70vh; - color:#fff; - } - .f3 { - position: relative; - display: flex; - } /* card-html */ .f3 div.card { cursor: pointer; - color: rgba(0, 0, 0, 0.75); + color: var(--text-color); position: relative; line-height: 1.2; } - + .f3 div.card-image-circle { border-radius: 50%; padding: 5px; width: 90px; height: 90px; } - + .f3 div.card-image-circle div.card-label { position: absolute; - bottom: -15px; + bottom: -10px; left: 50%; transform: translate(-50%, 50%); max-width: 150%; min-height: 22px; text-align: center; + background-color: var(--genderless-color); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-radius: 3px; padding: 0 5px; - color: black; - font-family: 'Alata'; - background-color: #e7f2d7; } - + .f3 div.card-image-circle img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; } - + .f3 div.card-image-circle svg { width: 100%; height: 100%; @@ -96,100 +85,31 @@ border-radius: 50%; object-fit: cover; } - - .f3 div.card-image-circle img { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; - } - - .f3 div.card-rect { - padding: 5px; - border-radius: 3px; - width: 120px; - min-height: 70px; - overflow: hidden; - text-align: center; - display: flex; - flex-direction: column; - justify-content: center; - } - - - .f3 div.card-image-rect { - width: 200px; - min-height: 70px; - display: flex; - align-items: center; - border-radius: 5px; - } - - .f3 div.card-image-rect .person-icon { - height: 70px; - width: 70px; - object-fit: cover; - flex: 0 0 auto; - padding: 5px; - margin-right: 10px; + + .f3 div.card:hover > div { + transform: translate(0, -2px); } - - .f3 div.card-image-rect img { - height: 70px; - width: 70px; - object-fit: cover; - flex: 0 0 auto; - padding: 5px; - margin-right: 10px; - border-radius: 8px; + .f3 div.card-main .card-inner, .f3 div.card:hover .card-inner { + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.8); } - - .f3 div.card-image-rect svg { - object-fit: cover; - width: 100%; - height: 100%; - padding: 5px; - border-radius: 7px; + + .f3 div.card-main .card-inner { + outline: 4px solid rgba(220, 220, 220, 1); } - - .f3 div.card-image-rect div.card-label { - height: 100%; - overflow: hidden; - display: flex; - flex-direction: column; - justify-content: center; + + .f3 div.card-inner.f3-path-to-main { + outline: 4px solid rgba(255, 255, 255, 1); } - - .f3 div.mini-tree { - text-align: right; - position: absolute; - top: -15px; - right: -2px; - z-index: -1; + .f3 div.card-female .card-inner, .f3 div.card-female .person-icon svg { + background-color: var(--female-color); } - .f3 div.mini-tree svg { - width: 55px; - } - - .f3 .card-inner { - outline: 0px solid rgba(255, 255, 255, 1); - transition: outline 0.5s ease-in-out; + .f3 div.card-male .card-inner, .f3 div.card-male .person-icon svg { + background-color: var(--male-color); } - .f3 div.card-genderless .card-inner, .f3 div.card-genderless .person-icon svg { - background-color: #738856; - } - - .f3 div.card-new-rel .card-inner { - border-width: 1px; - border-style: dashed; - outline: 0px !important; - } - - .f3 div.card-inner.f3-path-to-main { - outline: 4px solid rgba(255, 255, 255, 1); + background-color: var(--genderless-color); } - + /* branches between nodes */ .f3 .link { transition: stroke-width 0.2s ease-in-out; @@ -201,8 +121,21 @@ stroke-width: 4px; } - .main-container { - @media screen and (max-width: 768px) { - flex-direction: column; - } - } \ No newline at end of file + + /* card styling */ +.f3 .card-main-outline { + stroke: currentColor; + stroke-width: 3px; + } + +.f3 .card-genderless .card-body-rect, .f3 .card-genderless .text-overflow-mask { + fill: var(--genderless-color); +} + +.f3 g.card_add text { + fill: #fff; +} +.f3 .card-main-outline { + stroke: currentColor; + stroke-width: 3px; +} \ No newline at end of file diff --git a/client/src/services/familyTreeService.js b/client/src/services/familyTreeService.js index 94ac74a..ca59f27 100644 --- a/client/src/services/familyTreeService.js +++ b/client/src/services/familyTreeService.js @@ -48,13 +48,11 @@ export const familyTreeService = { object: [{ "id": memberId, "data": { - "first name": memberData.firstName, - "last name": memberData.lastName, + "first name": memberData.firstname, + "last name": memberData.lastname, "gender": memberData.gender, }, "rels": { - "children": [], - "spouses": [], } }], userid: userid, // who is creating the tree From 1dc97fdf18016f729209e33eae6d8d467de3f221 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Mon, 10 Nov 2025 22:14:51 -0600 Subject: [PATCH 24/86] added a fetch treeobject to familyservice --- client/src/pages/Tree/Tree.js | 68 +++++++++++------------- client/src/services/familyTreeService.js | 20 +++++++ 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/client/src/pages/Tree/Tree.js b/client/src/pages/Tree/Tree.js index c61da2a..c26b17a 100644 --- a/client/src/pages/Tree/Tree.js +++ b/client/src/pages/Tree/Tree.js @@ -8,7 +8,8 @@ import AddFamilyMemberPopup from '../../components/AddFamilyMember/AddFamilyMemb import NavBar from '../../components/NavBar/NavBar'; import { useLocation, Outlet } from 'react-router-dom'; import { useCurrentUser, supabaseUser } from '../../CurrentUserProvider'; // import the context -// FamilyTree class structure derived from family-chart package sample code +import { familyTreeService } from '../../services/familyTreeService'; + // see https://github.com/donatso/family-chart/ function FamilyTree() { @@ -31,48 +32,39 @@ function FamilyTree() { if (!contRef.current) return; - try{ - const f3chart = f3.createChart('#FamilyChart', data) - .setTransitionTime(1000) - .setCardXSpacing(250) - .setCardYSpacing(150) - .setSingleParentEmptyCard(true, {label: 'ADD'}) - .setShowSiblingsOfMain(true) - .setOrientationVertical() + const f3chart = f3.createChart('#FamilyChart', data) + .setTransitionTime(1000) + .setCardXSpacing(250) + .setCardYSpacing(150) + .setSingleParentEmptyCard(true, {label: 'ADD'}) + .setShowSiblingsOfMain(true) + .setOrientationVertical() - const f3Card = f3chart.setCardHtml() - .setCardDisplay([["first name"],[]]) - .setCardDim({}) - .setMiniTree(true) - .setStyle('imageCircle') - .setOnHoverPathToMain() - .setOnCardClick((e, data) => { - window.location.href = `/account/${data.data.id}`; - }); + const f3Card = f3chart.setCardHtml() + .setCardDisplay([["first name"],[]]) + .setCardDim({}) + .setMiniTree(true) + .setStyle('imageCircle') + .setOnHoverPathToMain() + .setOnCardClick((e, data) => { + window.location.href = `/account/${data.data.id}`; + }); - f3chart.updateTree({initial: true}); - } catch (error) { - console.error('Error creating family tree chart:', error); - } - + f3chart.updateTree({initial: true}); } + try{ + const fetchData = async () => { + const treeData = await familyTreeService.getFamilyTreeByUserId(currentAccountID); + console.log('Tree data fetched:', treeData); + create(treeData); + }; + fetchData(); + } catch (error) { + console.error('Error creating family tree chart:', error); + } + - fetch(`http://localhost:5000/api/tree-info/${currentAccountID}`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }) - .then(async (response) => { - if (response.ok) { - let treeData = await response.json(); - const parsedData = JSON.parse(treeData.object); - console.log("Tree data: ", parsedData); - create(parsedData); - } else { - console.error('Error in Loading Tree Data:', response.json().error); - window.alert('Failed to load family tree data. Please try again.'); - } - }); }, [currentAccountID, contRef]); return
    ; diff --git a/client/src/services/familyTreeService.js b/client/src/services/familyTreeService.js index ca59f27..b0d1ec1 100644 --- a/client/src/services/familyTreeService.js +++ b/client/src/services/familyTreeService.js @@ -102,5 +102,25 @@ export const familyTreeService = { } return responseData; + }, + /** + * + * @param {int} userId + * @returns Array of the user's treeInfo Object + */ + async getFamilyTreeByUserId(userId) { + const response = await fetch(`http://localhost:5000/api/tree-info/${userId}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }) + let responseData = await response.json(); + if (!response.ok) { + console.error('Error in Loading Tree Data:', responseData.error); + throw new Error(responseData.error || 'Failed to load family tree data'); + } + const parsedData = JSON.parse(responseData.object); + console.log("Tree data: ", parsedData); + + return parsedData; } }; \ No newline at end of file From 8a31fff473dd0766c45c12f0e573ec24aa828ad4 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Tue, 11 Nov 2025 05:15:38 -0600 Subject: [PATCH 25/86] fixes to family member --- client/src/CurrentUserProvider.js | 14 +- .../AddFamilyMember/AddFamilyMember.js | 99 ++++++------ client/src/pages/Family/Family.js | 151 +++--------------- client/src/services/familyTreeService.js | 27 +++- server/controllers/authController.js | 7 +- server/controllers/treeMemberController.js | 8 +- server/models/treeMemberModel.js | 5 +- 7 files changed, 122 insertions(+), 189 deletions(-) diff --git a/client/src/CurrentUserProvider.js b/client/src/CurrentUserProvider.js index 510cc5e..0c92732 100644 --- a/client/src/CurrentUserProvider.js +++ b/client/src/CurrentUserProvider.js @@ -62,7 +62,12 @@ export const CurrentUserProvider = ({ children }) => { if (session?.user) { setSupabaseUser(session.user); - setCurrentAccountIDState(session.user.id); + let response = await fetch(`http://localhost:5000/api/auth/user/${session.user.id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + response = await response.json(); + setCurrentAccountIDState(response.id); setCurrentUserNameState(session.user.email); // Use email as default username } @@ -80,7 +85,12 @@ export const CurrentUserProvider = ({ children }) => { async (event, session) => { if (session?.user) { setSupabaseUser(session.user); - setCurrentAccountIDState(session.user.id); + let response = await fetch(`http://localhost:5000/api/auth/user/${session.user.id}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + response = await response.json(); + setCurrentAccountIDState(response.id); setCurrentUserNameState(session.user.email); // Auto-sync profile into public.users using auth metadata when available try { diff --git a/client/src/components/AddFamilyMember/AddFamilyMember.js b/client/src/components/AddFamilyMember/AddFamilyMember.js index 9a9b0dc..09c1b2c 100644 --- a/client/src/components/AddFamilyMember/AddFamilyMember.js +++ b/client/src/components/AddFamilyMember/AddFamilyMember.js @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { Link } from 'react-router-dom'; import Popup from 'reactjs-popup'; import 'reactjs-popup/dist/index.css'; -import { useForm } from 'react-hook-form'; +import { set, useForm } from 'react-hook-form'; import * as styles from './styles'; import './popup.css'; import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; @@ -12,10 +12,6 @@ import { familyTreeService } from '../../services/familyTreeService'; // TODO: make form clear when dismissed by clicking outside of modal // make sync contact button functional -const getRequestOptions = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, -} function AddFamilyMemberPopup({ trigger, userid }) { const [manual, setManual] = useState(false); @@ -60,37 +56,41 @@ function AddFamilyMemberPopup({ trigger, userid }) { // populate search results for existing user search useEffect(() => { + setErrorMessage(""); // no search term, clear results if (searchTerm === "") { setSearchResults([]); return; } - // get non-friends const fetchResults = async () => { + // get family members of current user + try { + family.current = await familyTreeService.getFamilyMembersByUserId(currentAccountID); + } catch (error) { + console.error('Error fetching family members for user', currentAccountID, error.message); + setErrorMessage(error.message) + return; + } - family.current = await familyTreeService.getFamilyMembersByUserId(currentUserID); - - // fetch all users (this is totally scalable) - fetch(`http://localhost:5000/api/auth/users`, getRequestOptions) - .then(async(response) => { - if (response.ok) { - const responseData = await response.json(); - users.current = responseData.filter(user => - user.username.toLowerCase().includes(searchTerm.toLowerCase()) && - !family.current.some(member => member.memberUserId === user.id) - ); - setSearchResults(users.current); - } - else { - // print message in return body - console.error('Error:', response); - } - }) + // populate users list + try{ + const responseData = await familyTreeService.getRegisteredUsers(); + console.log('Registered users:', responseData); + users.current = responseData.filter(user => + user.username.toLowerCase().includes(searchTerm.toLowerCase()) && + !family.current.some(member => member.memberUserId === user.id)); + setSearchResults(users.current); + } catch (error) { + console.error('Error fetching users:', error.message); + setErrorMessage(error.message); + setSearchResults([]); + return; + } }; fetchResults(); - }, [searchTerm, currentAccountID, currentUserID]); + }, [searchTerm, currentAccountID]); @@ -110,42 +110,47 @@ function AddFamilyMemberPopup({ trigger, userid }) { setErrorMessage(""); try { const selectedUser = users.current.find(user => user.id === Number(data.selectedMember)); + // TODO : fetch metadata for user and add user data to treemember data + + if (!selectedUser) { + setErrorMessage("Selected user not found"); + return; + } const selectedUserData = { - firstName: selectedUser.firstName, - lastName: selectedUser.lastName, - birthDate: selectedUser.birthDate || null, - deathDate: selectedUser.deathDate || null, + firstname: selectedUser.firstname, + lastname: selectedUser.lastname, + birthdate: selectedUser.birthdate || null, + deathdate: selectedUser.deathdate || null, location: selectedUser.location || null, - phoneNumber: selectedUser.phoneNumber || null, - userId: currentUserID, // The user adding the family member - memberUserId: selectedUser.id, // Existing user's ID - gender: selectedUser.gender, + phonenumber: selectedUser.phonenumber || null, + userid: currentAccountID, // The user adding the family member + memberuserid: selectedUser.id, // Existing user's ID + gender: selectedUser.gender || "F" // default for now lol }; + // get account treemember id + let treeUser = await familyTreeService.getFamilyMemberByUserId(currentAccountID); + const treeUserId = treeUser.id; + console.log(treeUserId, 'user treemember id'); + console.log('current Account ID', currentAccountID); - let treeUserId = currentUserID; - - const memberResponse = await familyTreeService.createFamilyMember(selectedUserData); - const memberData = await memberResponse.json(); - if (!memberResponse.ok) { - throw new Error(memberData.error || 'Failed to add family member'); - } - console.log('memberData', memberData); - - const treeMemberId = memberData.member.id; + // add new member to treemembers table + const treeMember = await familyTreeService.createFamilyMember(selectedUserData); + const treeMemberId = treeMember.member.id; + console.log('added user treeMemberId', treeMemberId); // relationship table uses id's from treeMembers table, not user ids! let relRequestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - person1_id: treeUserId, // user adding the member - person2_id: treeMemberId, // added member + person1_id: treeUserId, // user adding the member (treemember id) + person2_id: treeMemberId, // added member (treemember id) relationshipType: data.selectedMemberRelationship, relationshipStatus: "active", side: data.matPat || null, - userId: currentUserID, + userId: currentAccountID }) }; const relResponse = await fetch(`http://localhost:5000/api/relationships/`, relRequestOptions); // add relationship @@ -158,7 +163,7 @@ function AddFamilyMemberPopup({ trigger, userid }) { close(); console.log(relData.message); - return window.location.href = `/account/${treeMemberId}`; // redirect to account page + return window.location.href = `/account/${selectedUser.id}`; // redirect to account page } catch (error) { console.error('Error:', error); diff --git a/client/src/pages/Family/Family.js b/client/src/pages/Family/Family.js index 709afd7..c71ebc4 100644 --- a/client/src/pages/Family/Family.js +++ b/client/src/pages/Family/Family.js @@ -5,6 +5,7 @@ import Popup from 'reactjs-popup'; import { ReactComponent as DropdownIcon } from '../../assets/dropdown-arrow.svg'; import NavBar from '../../components/NavBar/NavBar'; import { useCurrentUser } from '../../CurrentUserProvider'; +import { familyTreeService } from '../../services/familyTreeService'; const defaultAvatar = require('../../assets/default-avatar.png'); @@ -16,137 +17,22 @@ function Family() { const [searchTerm, setSearchTerm] = useState(""); const [filteredData, setFilteredData] = useState([]); const [familyData, setFamilyData] = useState([]); - const [user_lastname, setUserLastName] = useState(""); - const { currentUserID, currentUserName, fetchCurrentUserID, currentAccountID } = useCurrentUser(); - - // get user's last name - useEffect(() => { - const fetchUserData = async () => { - try { - await fetchCurrentUserID(); // Wait for fetchCurrentUserID to complete - const name = currentUserName; // Retrieve the last name after fetch - setUserLastName(name?.split(" ")[1]); // Store it in state - } catch (error) { - console.error("Error fetching current user ID:", error); - } - }; - - fetchUserData(); // Call the async function - }, [fetchCurrentUserID]); + const { supabaseUser, currentAccountID } = useCurrentUser(); document.body.style.overflow = 'hidden'; document.body.style.width = '100%'; - - // const familyData = useMemo(() => [ - // { - // "id": "0", - // "rels": { - // "father": "1", - // "mother": "2", - // "children": ["5"] - // }, - // "data": { - // "first name": "Ronald", - // "last name": "Smith", - // "avatar": "https://i.imgur.com/mfojszj.png" - // } - // }, - // { - // "id": "1", - // "rels": { - // "father": "3", - // "mother": "4", - // "spouses": [ - // "2" - // ], - // "children": [ - // "0" - // ] - // }, - // "data": { - // "first name": "John", - // "last name": "Smith", - // "flag": "paternal" - // } - // }, - // { - // "id": "2", - // "rels": { - // "spouses": [ - // "1" - // ], - // "children": [ - // "0" - // ] - // }, - // "data": { - // "first name": "Jane", - // "last name": "Smith", - // "flag": "maternal" - // } - // }, - // { - // "id": "3", - // "rels": { - // "children": ["1"], - // "spouses": ["4"] - // }, - // "data": { - // "first name": "Alice", - // "last name": "Smith", - // "flag": "paternal" - // } - // }, - // { - // "id": "4", - // "rels": { - // "children": ["1"], - // "spouses": ["3"] - // }, - // "data": { - // "first name": "Bob", - // "last name": "Smith", - // "flag": "paternal" - // } - // }, - // { - // "id": "5", - // "rels": { - // "father": "0", - // }, - // "data": { - // "first name": "Tom", - // "last name": "Smith" - // } - // }, - // ], []); - - // fetch family data from API useEffect(() => { - if (!currentAccountID) { - console.error('No current account ID'); - return; - } - const fetchFamilyData = async () => { try { - console.log('Current account ID:', currentAccountID); - - const response = await fetch(`http://localhost:5000/api/family-members/user/${currentAccountID}`); - if (!response.ok) { - throw new Error(`Failed to fetch family data: ${response.statusText}`); - } - - const responseData = await response.json(); // [{...}, {...}, ...] + const responseData = await familyTreeService.getFamilyMembersByUserId(currentAccountID); setFamilyData(responseData); - console.log('Family data:', responseData); } catch (error) { - console.error('Error fetching family data:', error); + console.error('Error fetching family data:', error.message); } }; - + fetchFamilyData(); }, [currentAccountID]); @@ -155,8 +41,8 @@ function Family() { if (searchTerm !== "") { filtered = filtered.filter(member => - member["firstName"].toLowerCase().includes(searchTerm.toLowerCase()) || - member["lastName"].toLowerCase().includes(searchTerm.toLowerCase()) + member["firstname"].toLowerCase().includes(searchTerm.toLowerCase()) || + member["lastname"].toLowerCase().includes(searchTerm.toLowerCase()) ); } @@ -167,18 +53,19 @@ function Family() { if (sortSelection !== "") { filtered = filtered.sort((a, b) => { - if(sortSelection === "firstName") { - return a["firstName"].localeCompare(b["firstName"]); + if(sortSelection === "firstname") { + return a["firstname"].localeCompare(b["firstname"]); } - else if(sortSelection === "lastName") { - return a["lastName"].localeCompare(b["lastName"]); + else if(sortSelection === "lastname") { + return a["lastname"].localeCompare(b["lastname"]); } return 0; }); } + console.log('Filtered family data:', filtered); setFilteredData(filtered); - }, [searchTerm, filterSelection, familyData, sortSelection]); + }, [searchTerm, filterSelection, sortSelection, familyData]); return (
    @@ -187,7 +74,7 @@ function Family() {
    {/* header */} -

    The {user_lastname} Family

    +

    The {supabaseUser?.user_metadata?.last_name} Family


    - - + +
    @@ -276,11 +163,11 @@ function Family() { {filteredData.map((member) => (
    - {`${member["firstName"]} - {member["firstName"]} {member["lastName"]} + {`${member["firstname"]} + {member["firstname"]} {member["lastname"]}
    - + View
    diff --git a/client/src/services/familyTreeService.js b/client/src/services/familyTreeService.js index b0d1ec1..824ab5c 100644 --- a/client/src/services/familyTreeService.js +++ b/client/src/services/familyTreeService.js @@ -2,7 +2,7 @@ export const familyTreeService = { /** * * @param {*} memberData - * @returns treeMemberId + * @returns treeMember object */ async createFamilyMember(memberData) { console.log('Creating family member with data:', memberData); @@ -28,7 +28,7 @@ export const familyTreeService = { console.log('Failed to create family member:', responseData.error); throw new Error(responseData.error); } - return responseData; // returns memberId + return responseData; // returns member object }, /** * @@ -84,12 +84,31 @@ export const familyTreeService = { } return responseData; // returns all family members of the user }, + /** + * + * @param {*} userId + * @returns the users primary treeMember object + */ + async getFamilyMemberByUserId(userId) { + const response = await fetch(`http://localhost:5000/api/family-members/${userId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const responseData = await response.json(); + if (!response.ok) { + console.error('Failed to fetch family member:', responseData.error); + throw new Error(responseData.error || 'Failed to fetch family member'); + } + return responseData; + }, /** * * @returns JSON Array of all registered users */ async getRegisteredUsers() { - const response = await fetch(`http://localhost:5000/api/users/`, { + const response = await fetch(`http://localhost:5000/api/auth/users/`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -120,7 +139,7 @@ export const familyTreeService = { } const parsedData = JSON.parse(responseData.object); console.log("Tree data: ", parsedData); - + return parsedData; } }; \ No newline at end of file diff --git a/server/controllers/authController.js b/server/controllers/authController.js index d161f0e..3f8795a 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -1,5 +1,6 @@ // authController.js - the main backend file for user registration, signin, etc const User = require('../models/userModel'); // now backed by Supabase +const { get } = require('../routes/authRoutes'); const deleteByUser = async (req,res) => { const { id } = req.params; @@ -21,7 +22,11 @@ const deleteByUser = async (req,res) => { const findById = async (req, res) => { try { const { id } = req.params; - const user = await User.findById(id); + const userId = await User.resolveUserIdFromAuthUid(id); + if (!userId) { + return res.status(500).json({ error: 'Error resolving user ID' }); + } + const user = await User.findById(userId); if (!user) { return res.status(404).json({ error: 'User not found' }); } diff --git a/server/controllers/treeMemberController.js b/server/controllers/treeMemberController.js index 0c500a6..bf183d7 100644 --- a/server/controllers/treeMemberController.js +++ b/server/controllers/treeMemberController.js @@ -194,7 +194,13 @@ const deleteByUser = async (req, res) => { const getMemberById = async (req, res) => { try { const { id } = req.params; - const member = await treeMember.getMemberById(id); + // Resolve UUID to integer user ID first + const userId = await User.resolveUserIdFromAuthUid(id); + console.log('getMemberById resolved userId:', userId); + if (!userId) { + return res.status(200).json({}); + } + const member = await treeMember.getMemberById(userId); if (!member) { return res.status(404).json({ error: 'Family member not found' }); } diff --git a/server/models/treeMemberModel.js b/server/models/treeMemberModel.js index 788cd77..1a93b22 100644 --- a/server/models/treeMemberModel.js +++ b/server/models/treeMemberModel.js @@ -54,7 +54,7 @@ const treeMember = { const { data, error } = await supabase .from('treemembers') .select('*') - .eq('id', id); + .eq('userid', id); if (error) throw error; return data; }, @@ -63,7 +63,8 @@ const treeMember = { const { data, error } = await supabase .from('treemembers') .select('*') - .eq('id', id) + .eq('userid', id) + .eq('memberuserid', id) .single(); if (error) throw error; return data; From 55f26dde20900abbe287ea4aebfcefe4b39eb0b5 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Tue, 11 Nov 2025 05:15:50 -0600 Subject: [PATCH 26/86] fixes to tree --- client/src/components/AddToTree/AddToTree.js | 17 ++++++++++++----- client/src/pages/Tree/Tree.js | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/client/src/components/AddToTree/AddToTree.js b/client/src/components/AddToTree/AddToTree.js index 1bdacb0..eef3c69 100644 --- a/client/src/components/AddToTree/AddToTree.js +++ b/client/src/components/AddToTree/AddToTree.js @@ -113,11 +113,18 @@ function AddToTreePopup({ trigger, accountUserName, accountUserId, currentUserAc } // populate filteredResults - filteredResults.current = results.current.filter((result) => - treeData.map((person) => Number(person.id)).includes(Number(result.id)) - ); - - console.log("Filtered Results:", filteredResults.current); + if (Array.isArray(treeData)) { + try { + const parsedData = JSON.parse(treeData); + filteredResults.current = results.current.filter((result) => + parsedData.map((person) => Number(person.id)).includes(Number(result.id))); + console.log("Filtered Results:", filteredResults.current); + } catch (error) { + console.error("Error parsing treeData:", error); + return; + } + } + }, [treeData]); // form diff --git a/client/src/pages/Tree/Tree.js b/client/src/pages/Tree/Tree.js index c26b17a..2594629 100644 --- a/client/src/pages/Tree/Tree.js +++ b/client/src/pages/Tree/Tree.js @@ -47,7 +47,7 @@ function FamilyTree() { .setStyle('imageCircle') .setOnHoverPathToMain() .setOnCardClick((e, data) => { - window.location.href = `/account/${data.data.id}`; + window.location.href = `/account/${data.userid}`; }); f3chart.updateTree({initial: true}); From ed5990d64835480a8ac927f260a4c80ecb20de19 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Tue, 11 Nov 2025 05:16:12 -0600 Subject: [PATCH 27/86] fix typo --- server/controllers/relationshipController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/relationshipController.js b/server/controllers/relationshipController.js index 81db440..fa2f156 100644 --- a/server/controllers/relationshipController.js +++ b/server/controllers/relationshipController.js @@ -145,7 +145,7 @@ const deleteByUser = async (req, res) => { } catch (error){ console.error(error); - res.status(500);json({error:"Error deleting relationship"}) + res.status(500).json({error:"Error deleting relationship"}) } } From 984c38e169ac19c269ca5d6382aab2f38f50bf82 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Tue, 11 Nov 2025 14:18:58 -0600 Subject: [PATCH 28/86] fix account page for non-user & member usage --- client/src/CurrentUserProvider.js | 9 +- client/src/pages/Account/Account.js | 162 ++++++++++++--------- client/src/pages/Family/Family.js | 2 +- client/src/pages/Tree/Tree.js | 2 +- client/src/services/familyTreeService.js | 19 +++ server/controllers/treeMemberController.js | 20 ++- server/models/treeMemberModel.js | 10 ++ server/routes/treeMemberRoute.js | 2 + 8 files changed, 144 insertions(+), 82 deletions(-) diff --git a/client/src/CurrentUserProvider.js b/client/src/CurrentUserProvider.js index 0c92732..0b05045 100644 --- a/client/src/CurrentUserProvider.js +++ b/client/src/CurrentUserProvider.js @@ -41,7 +41,7 @@ export const CurrentUserProvider = ({ children }) => { const data = await response.json(); // console.log('Current user ID:', data); setCurrentUserID(data.id); - setCurrentUserName(data.firstName + " " + data.lastName); + setCurrentUserName(data.firstname + " " + data.lastname); } else { console.error('Error:', response); @@ -62,12 +62,7 @@ export const CurrentUserProvider = ({ children }) => { if (session?.user) { setSupabaseUser(session.user); - let response = await fetch(`http://localhost:5000/api/auth/user/${session.user.id}`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' } - }); - response = await response.json(); - setCurrentAccountIDState(response.id); + setCurrentAccountIDState(session.user.id); // logged in user's user.id setCurrentUserNameState(session.user.email); // Use email as default username } diff --git a/client/src/pages/Account/Account.js b/client/src/pages/Account/Account.js index f4f0f0f..424afa8 100644 --- a/client/src/pages/Account/Account.js +++ b/client/src/pages/Account/Account.js @@ -4,6 +4,12 @@ import { useParams, useNavigate } from 'react-router-dom'; import NavBar from '../../components/NavBar/NavBar'; import AddToTreePopup from '../../components/AddToTree/AddToTree'; import { useCurrentUser } from '../../CurrentUserProvider'; +import { set } from 'react-hook-form'; + +const requestOptions = { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, +}; function Account() { const navigate = useNavigate(); // used to change route without refreshing page, used to prevent infinite refreshes @@ -11,7 +17,7 @@ function Account() { const [existsInTree, setExistsInTree] = useState(false); // will be retrieved const [relationshipType, setRelationshipType] = useState(''); // will be retrieved - const { currentUserID, supabaseUser, loading } = useCurrentUser(); + const { currentUserID, CurrentAccountID, supabaseUser, loading } = useCurrentUser(); // Redirect to login if not authenticated useEffect(() => { @@ -22,6 +28,7 @@ function Account() { // takes id from url path let { id } = useParams(); + // if no id is provided, retrieve current user's id and show that page useEffect(() => { if (!id && supabaseUser?.id) { @@ -51,74 +58,94 @@ function Account() { // Fetch user info - check if it's a Supabase user or family member useEffect(() => { if (!id) return; - - // Check if this is the current Supabase user - if (id === supabaseUser?.id) { - console.log('Supabase user data:', supabaseUser); - console.log('User metadata:', supabaseUser.user_metadata); - - setUserData({ - id: supabaseUser.id, - firstName: supabaseUser.user_metadata?.first_name || 'User', - lastName: supabaseUser.user_metadata?.last_name || '', - email: supabaseUser.email, - birthdate: supabaseUser.user_metadata?.birthdate || '', - address: supabaseUser.user_metadata?.address || '', - city: supabaseUser.user_metadata?.city || '', - state: supabaseUser.user_metadata?.state || '', - country: supabaseUser.user_metadata?.country || '', - phone_number: supabaseUser.user_metadata?.phone_number || '', - zipcode: supabaseUser.user_metadata?.zipcode || '', - gender: supabaseUser.user_metadata?.gender || '' - }); - setOwnAccount(true); - return; - } - - // Otherwise, try to fetch from family members API - const requestOptions = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }; - - fetch(`http://localhost:5000/api/family-members/${id}`, requestOptions) - .then(async (response) => { - if (response.ok) { - const data = await response.json(); - setUserData(data); - } else { - console.error('Error fetching user data:', response); - // If family member not found, show basic info + const fetchUserData = async () => { + try { + const user = await fetch(`http://localhost:5000/api/auth/user/${id}`); // if the memberuserid matches a user in user db + if (user.status === 404) { // user not found, so it must be a manually added member + // Fetch manually added member data + const member = await fetch(`http://localhost:5000/api/family-members/member/${id}`); + const memberData = await member.json(); + setUserData({ + id: id, + firstName: memberData.firstname, + lastName: memberData.lastname, + email: memberData.email || '', + birthdate: memberData.birthdate || '', + address: memberData.address || '', + city: memberData.city || '', + state: memberData.state || '', + country: memberData.country || '', + phone_number: memberData.phonenumber || '', + zipcode: memberData.zipcode || '', + gender: memberData.gender || 'Unspecified' + }); + } else if (user.ok) { // user found in user db + const userData = await user.json(); + if (userData.auth_uid) { + checkOwnAccount(); + console.log('Checked own account for auth_uid:', userData.auth_uid, 'vs', supabaseUser?.id); + } + console.log('Fetched Supabase user data', userData); + // TODO: add fields in db for address, phone, etc. since there are not available outside of logged in user_metadata setUserData({ id: id, - firstName: 'Unknown', - lastName: 'User', - email: '', + firstName: userData.firstname || 'User', + lastName: userData.lastname || '', + email: userData.email || '', + birthdate: userData.birthdate || '', + address: userData.address || '', + city: userData.city || '', + state: userData.state || '', + country: userData.country || '', + phone_number: userData.phone_number || '', + zipcode: userData.zipcode || '', + gender: userData.gender || 'Unspecified', + auth_uid: userData.auth_uid || '' }); } - }) - .catch((error) => { - console.error('There was a problem with the fetch operation:', error); - }); - }, [id, supabaseUser]); - - useEffect(() => { - // Check if this is the current user's own account - if (id === supabaseUser?.id) { - setOwnAccount(true); - return; - } - - // If it's not the current user, check relationships (only for family members) - if (!userData.memberUserId) { - setOwnAccount(false); - return; - } - - const requestOptions = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, + } catch (error) { + console.error('Error fetching user data:', error); + setUserData({ + id: id, + firstName: 'Unknown', + lastName: 'User', + email: '', + }); + } + }; + const checkOwnAccount = () => { + // Check if this is the current logged in Account user + if (userData?.auth_uid === supabaseUser?.id) { + console.log('This is the own account'); + console.log('Supabase user data:', supabaseUser); + console.log('User metadata:', supabaseUser.user_metadata); + setOwnAccount(true); + setUserData({ + id: supabaseUser.id, + firstName: supabaseUser.user_metadata?.first_name || 'User', + lastName: supabaseUser.user_metadata?.last_name || '', + email: supabaseUser.email, + birthdate: supabaseUser.user_metadata?.birthdate || '', + address: supabaseUser.user_metadata?.address || '', + city: supabaseUser.user_metadata?.city || '', + state: supabaseUser.user_metadata?.state || '', + country: supabaseUser.user_metadata?.country || '', + phone_number: supabaseUser.user_metadata?.phone_number || '', + zipcode: supabaseUser.user_metadata?.zipcode || '', + gender: supabaseUser.user_metadata?.gender || '' + }); + } + else { + console.log('This is NOT the own account'); + setOwnAccount(false); + } }; + fetchUserData(); + + }, [id, supabaseUser, userData.auth_uid]); + + // determine relationship type + useEffect(() => { // if not self, determine relationship to user fetch(`http://localhost:5000/api/relationships/${id}`, requestOptions) @@ -143,17 +170,12 @@ function Account() { .catch(error => { console.error('There was a problem with the fetch operation:', error); }); - }, [id, currentUserID, userData.id, userData.memberUserId, supabaseUser?.id]); + }, [id, currentUserID, userData.id, supabaseUser?.id]); // check if user exists in tree useEffect(() => { if (!id || !supabaseUser?.id) return; - const requestOptions = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }; - fetch(`http://localhost:5000/api/tree-info/${supabaseUser.id}`, requestOptions) .then(async (response) => { if (response.ok) { diff --git a/client/src/pages/Family/Family.js b/client/src/pages/Family/Family.js index c71ebc4..7c5b671 100644 --- a/client/src/pages/Family/Family.js +++ b/client/src/pages/Family/Family.js @@ -167,7 +167,7 @@ function Family() { {member["firstname"]} {member["lastname"]}
    - + View
    diff --git a/client/src/pages/Tree/Tree.js b/client/src/pages/Tree/Tree.js index 2594629..c53c6ae 100644 --- a/client/src/pages/Tree/Tree.js +++ b/client/src/pages/Tree/Tree.js @@ -47,7 +47,7 @@ function FamilyTree() { .setStyle('imageCircle') .setOnHoverPathToMain() .setOnCardClick((e, data) => { - window.location.href = `/account/${data.userid}`; + window.location.href = `/account/${data.memberuserid}`; }); f3chart.updateTree({initial: true}); diff --git a/client/src/services/familyTreeService.js b/client/src/services/familyTreeService.js index 824ab5c..f136baa 100644 --- a/client/src/services/familyTreeService.js +++ b/client/src/services/familyTreeService.js @@ -103,6 +103,25 @@ export const familyTreeService = { } return responseData; }, + /** + * + * @param {int} id + * @returns the treeMember object by id + */ + async getFamilyMemberByFamilyMemberId(id) { + const response = await fetch(`http://localhost:5000/api/family-members/member/${id}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const responseData = await response.json(); + if (!response.ok) { + console.error('Failed to fetch family member:', responseData.error); + throw new Error(responseData.error || 'Failed to fetch family member'); + } + return responseData; + }, /** * * @returns JSON Array of all registered users diff --git a/server/controllers/treeMemberController.js b/server/controllers/treeMemberController.js index bf183d7..9e2c5f7 100644 --- a/server/controllers/treeMemberController.js +++ b/server/controllers/treeMemberController.js @@ -221,13 +221,27 @@ const getActiveMemberId = async (req, res) => { } const member = await treeMember.getActiveMemberId(userId); // If none found, return empty object to avoid frontend JSON parse errors - if (!member) return res.status(200).json({}); + if (!member) return res.status(404).json({}); res.status(200).json(member); } catch (error) { console.error(error); res.status(500).json({ error: 'Error fetching family member', details: error.message }); } -} +}; + +const getMemberbyMemberId = async (req, res) => { + try { + const { id } = req.params; + const member = await treeMember.getMemberbyMemberId(id); + if (!member) { + return res.status(404).json({ error: 'Family member not found' }); + } + res.status(200).json(member); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Error fetching family member' }); + } +}; -module.exports = { addTreeMember, editTreeMember, getMembersByUser, getMembersByOtherUser, deleteByUser, getMemberById, getActiveMemberId }; +module.exports = { addTreeMember, editTreeMember, getMembersByUser, getMembersByOtherUser, deleteByUser, getMemberById, getActiveMemberId, getMemberbyMemberId }; diff --git a/server/models/treeMemberModel.js b/server/models/treeMemberModel.js index 1a93b22..9ac535a 100644 --- a/server/models/treeMemberModel.js +++ b/server/models/treeMemberModel.js @@ -145,6 +145,16 @@ const treeMember = { .maybeSingle(); if (error) throw error; return data; + }, + + getMemberbyMemberId: async (id) => { + const { data, error } = await supabase + .from('treemembers') + .select('*') + .eq('id', id) + .maybeSingle(); + if (error) throw error; + return data; } }; diff --git a/server/routes/treeMemberRoute.js b/server/routes/treeMemberRoute.js index 7c45829..111607b 100644 --- a/server/routes/treeMemberRoute.js +++ b/server/routes/treeMemberRoute.js @@ -3,12 +3,14 @@ const router = express.Router(); const { addTreeMember, editTreeMember,getMembersByUser, getMembersByOtherUser, deleteByUser, getMemberById, getActiveMemberId } = require('../controllers/treeMemberController'); +const { getMemberbyMemberId } = require('../models/treeMemberModel'); router.post('/', addTreeMember); router.put('/:id', editTreeMember); router.get('/user/:userId', getMembersByUser); router.get('/:id', getMemberById); router.get('/active/:id', getActiveMemberId); +router.get('/member/:id', getMemberbyMemberId); module.exports = router; \ No newline at end of file From 169886df93e0935535256c6d9f0fc4f9dd616dc2 Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Tue, 11 Nov 2025 14:55:38 -0600 Subject: [PATCH 29/86] add auth_uid to userdata state --- client/src/pages/Account/Account.js | 42 +++++++++++++++++------------ 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/client/src/pages/Account/Account.js b/client/src/pages/Account/Account.js index 424afa8..deecb83 100644 --- a/client/src/pages/Account/Account.js +++ b/client/src/pages/Account/Account.js @@ -19,6 +19,7 @@ function Account() { const { currentUserID, CurrentAccountID, supabaseUser, loading } = useCurrentUser(); + // Redirect to login if not authenticated useEffect(() => { if (!loading && !supabaseUser) { @@ -28,20 +29,8 @@ function Account() { // takes id from url path let { id } = useParams(); - - // if no id is provided, retrieve current user's id and show that page - useEffect(() => { - if (!id && supabaseUser?.id) { - setOwnAccount(true); - navigate(`/account/${supabaseUser.id}`, { replace: true }); - } - }, [id, supabaseUser?.id, navigate]); - - // TODO: query for data of account user & verify that userID of logged in user matches - - const [userData, setUserData] = useState({ - id: id, + id: '', firstName: 'Loading...', lastName: '', email: '', @@ -52,9 +41,26 @@ function Account() { country: '', phone_number: '', zipcode: '', - gender: '' + gender: '', + auth_uid: '' }) + // if no id is provided, retrieve current user's id and show that page + useEffect(() => { + if (!id && supabaseUser?.id) { + // Fetch current user's data + const fetchOwnUserData = async () => { + const user = await fetch(`http://localhost:5000/api/auth/user/${supabaseUser.id}`); + const userData = await user.json(); + setUserData(userData); + setOwnAccount(true); + navigate(`/account/${userData.id}`, { replace: true }); + } + fetchOwnUserData(); + } + }, [id, supabaseUser?.id, navigate]); + + // Fetch user info - check if it's a Supabase user or family member useEffect(() => { if (!id) return; @@ -81,9 +87,11 @@ function Account() { }); } else if (user.ok) { // user found in user db const userData = await user.json(); + setUserData(userData); if (userData.auth_uid) { checkOwnAccount(); console.log('Checked own account for auth_uid:', userData.auth_uid, 'vs', supabaseUser?.id); + if (ownAccount) return; } console.log('Fetched Supabase user data', userData); // TODO: add fields in db for address, phone, etc. since there are not available outside of logged in user_metadata @@ -118,10 +126,9 @@ function Account() { if (userData?.auth_uid === supabaseUser?.id) { console.log('This is the own account'); console.log('Supabase user data:', supabaseUser); - console.log('User metadata:', supabaseUser.user_metadata); setOwnAccount(true); setUserData({ - id: supabaseUser.id, + id: userData.id, firstName: supabaseUser.user_metadata?.first_name || 'User', lastName: supabaseUser.user_metadata?.last_name || '', email: supabaseUser.email, @@ -132,7 +139,8 @@ function Account() { country: supabaseUser.user_metadata?.country || '', phone_number: supabaseUser.user_metadata?.phone_number || '', zipcode: supabaseUser.user_metadata?.zipcode || '', - gender: supabaseUser.user_metadata?.gender || '' + gender: supabaseUser.user_metadata?.gender || '', + auth_uid: supabaseUser.id }); } else { From b3ab5eada153d5aacc1272e874058664bbdb0d2f Mon Sep 17 00:00:00 2001 From: Andrea Ambrose Date: Fri, 14 Nov 2025 11:05:15 -0600 Subject: [PATCH 30/86] gender added to userModel + passed on registration --- client/src/components/AddFamilyMember/AddFamilyMember.js | 2 +- client/src/utils/auth.js | 1 + server/controllers/authController.js | 4 ++-- server/models/userModel.js | 5 +++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/src/components/AddFamilyMember/AddFamilyMember.js b/client/src/components/AddFamilyMember/AddFamilyMember.js index 09c1b2c..e301313 100644 --- a/client/src/components/AddFamilyMember/AddFamilyMember.js +++ b/client/src/components/AddFamilyMember/AddFamilyMember.js @@ -126,7 +126,7 @@ function AddFamilyMemberPopup({ trigger, userid }) { phonenumber: selectedUser.phonenumber || null, userid: currentAccountID, // The user adding the family member memberuserid: selectedUser.id, // Existing user's ID - gender: selectedUser.gender || "F" // default for now lol + gender: selectedUser.gender }; // get account treemember id diff --git a/client/src/utils/auth.js b/client/src/utils/auth.js index 93a975e..c44359c 100644 --- a/client/src/utils/auth.js +++ b/client/src/utils/auth.js @@ -31,6 +31,7 @@ export async function registerUser(email, password, metadata = {}) { lastName: metadata.lastName || metadata.last_name || null, phoneNumber: metadata.phoneNumber || metadata.phone_number || metadata.phonenum || null, birthDate: metadata.birthDate || metadata.birthdate || null, + gender: metadata.gender || null, }) }); } diff --git a/server/controllers/authController.js b/server/controllers/authController.js index 3f8795a..02ecdf5 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -67,11 +67,11 @@ module.exports = { deleteByUser, findById, findByEmail, getAllUsers }; // Body: { auth_uid, email, username, firstName, lastName, phoneNumber, birthDate } const syncAuthUser = async (req, res) => { try { - const { auth_uid, email, username, firstName, lastName, phoneNumber, birthDate } = req.body || {}; + const { auth_uid, email, username, firstName, lastName, phoneNumber, birthDate, gender } = req.body || {}; if (!auth_uid || !email) { return res.status(400).json({ error: 'auth_uid and email are required' }); } - const user = await User.upsertByAuthUser({ auth_uid, email, username, firstName, lastName, phoneNumber, birthDate }); + const user = await User.upsertByAuthUser({ auth_uid, email, username, firstName, lastName, phoneNumber, birthDate, gender }); res.status(200).json(user); } catch (error) { console.error('Sync error:', error); diff --git a/server/models/userModel.js b/server/models/userModel.js index b7c5f7d..e904a58 100644 --- a/server/models/userModel.js +++ b/server/models/userModel.js @@ -71,7 +71,7 @@ const User = { return data; }, - upsertByAuthUser: async ({ auth_uid, email, username, firstName, lastName, phoneNumber, birthDate }) => { + upsertByAuthUser: async ({ auth_uid, email, username, firstName, lastName, phoneNumber, birthDate, gender }) => { // Map to lowercase columns and drop null/undefined so we don't overwrite with nulls const rawPayload = { auth_uid, @@ -81,6 +81,7 @@ const User = { lastname: lastName, phonenumber: phoneNumber, birthdate: birthDate, + gender: gender }; const payload = Object.fromEntries( Object.entries(rawPayload).filter(([_, v]) => v !== undefined && v !== null && v !== '') @@ -88,7 +89,7 @@ const User = { const { data, error } = await supabase .from('users') .upsert([ payload ], { onConflict: 'auth_uid' }) - .select('id, auth_uid, email, username, firstname, lastname, phonenumber, birthdate') + .select('id, auth_uid, email, username, firstname, lastname, phonenumber, birthdate, gender') .single(); if (error) throw error; return data; From ef9ffc359203390f523286388ff4886a881aebad Mon Sep 17 00:00:00 2001 From: MatthewLoyed Date: Mon, 17 Nov 2025 15:32:27 -0600 Subject: [PATCH 31/86] Added functionality to add profile picture, edit account info, and delete account. Improved and fixed bugs regarding registering account and accessing database info. Added dark mode toggle but needs work. --- client/src/App.css | 10 +- client/src/CurrentUserProvider.js | 50 +- client/src/ThemeProvider.js | 55 + client/src/components/NavBar/NavBar.css | 55 + client/src/index.css | 22 +- client/src/index.js | 17 +- client/src/pages/Account/Account.js | 1276 +++++++++++++++-- client/src/pages/Account/styles.js | 498 ++++++- .../src/pages/CreateAccount/CreateAccount.js | 54 +- client/src/pages/Family/Family.js | 11 +- client/src/pages/Home/Home.js | 14 +- client/src/pages/Login/Login.js | 62 +- client/src/pages/Tree/Tree.js | 13 +- .../pages/WebsiteSettings/WebsiteSettings.js | 296 +--- client/src/pages/WebsiteSettings/styles.js | 56 +- client/src/pages/WebsiteSettings/toggle.css | 58 + client/src/utils/auth.js | 45 +- client/src/utils/authHandlers.js | 3 +- client/src/utils/metadataHelpers.js | 59 + server/controllers/authController.js | 356 ++++- server/db/supabase-init.sql | 19 +- server/lib/metadataHelpers.js | 26 + server/lib/supabase.js | 18 +- server/models/userModel.js | 283 +++- server/package-lock.json | 130 ++ server/package.json | 1 + server/routes/authRoutes.js | 37 +- 27 files changed, 2945 insertions(+), 579 deletions(-) create mode 100644 client/src/ThemeProvider.js create mode 100644 client/src/pages/WebsiteSettings/toggle.css create mode 100644 client/src/utils/metadataHelpers.js create mode 100644 server/lib/metadataHelpers.js diff --git a/client/src/App.css b/client/src/App.css index aea3993..2dfcff0 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -14,15 +14,16 @@ } .App-header { - background-color: #E7F2D8; + background-color: var(--bg-color); min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); - color: white; + color: var(--text-color); gap: 40px; + transition: background-color 0.3s ease, color 0.3s ease; } .App-link { @@ -42,11 +43,12 @@ display: flex; align-items: center; justify-content: center; - background-color: white; - color: black; + background-color: var(--card-bg); + color: var(--text-color); border-radius: 50px; padding: 20px 40px; margin: 0px 20px; + transition: background-color 0.3s ease, color 0.3s ease; } .font-face-alata { diff --git a/client/src/CurrentUserProvider.js b/client/src/CurrentUserProvider.js index 510cc5e..5096423 100644 --- a/client/src/CurrentUserProvider.js +++ b/client/src/CurrentUserProvider.js @@ -1,5 +1,6 @@ import { React, useState, createContext, useContext, useEffect } from "react" import { supabase } from "./utils/supabaseClient"; +import { buildSyncPayload } from "./utils/metadataHelpers"; export const currentContext = createContext(); @@ -82,24 +83,37 @@ export const CurrentUserProvider = ({ children }) => { setSupabaseUser(session.user); setCurrentAccountIDState(session.user.id); setCurrentUserNameState(session.user.email); - // Auto-sync profile into public.users using auth metadata when available - try { - const m = session.user.user_metadata || {}; - await fetch('http://localhost:5000/api/auth/sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - auth_uid: session.user.id, - email: session.user.email, - username: session.user.email, - firstName: m.firstName || m.first_name || null, - lastName: m.lastName || m.last_name || null, - phoneNumber: m.phoneNumber || m.phone_number || m.phonenum || null, - birthDate: m.birthDate || m.birthdate || null, - }) - }); - } catch (e) { - console.warn('Auth sync failed:', e?.message || e); + + // Only sync on SIGNED_IN event to avoid redundant syncs + // The trigger handles initial user creation, and registration sync handles new users + if (event === 'SIGNED_IN') { + // Auto-sync profile into public.users using auth metadata when available + // Note: The trigger handle_new_auth_user should already create the user, + // but we sync here to ensure metadata is up to date + try { + const m = session.user.user_metadata || {}; + // Only sync if metadata has extended fields (to avoid unnecessary updates) + const hasExtendedMetadata = m.address || m.city || m.state || m.country || m.zipcode || + m.firstName || m.first_name || m.lastName || m.last_name || + m.phoneNumber || m.phone_number; + + if (hasExtendedMetadata) { + const syncPayload = buildSyncPayload(session.user.id, session.user.email, m); + const syncResponse = await fetch('http://localhost:5000/api/auth/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(syncPayload) + }); + + if (!syncResponse.ok) { + // Don't fail login - trigger may have already created the user + console.warn('Auth sync failed on login'); + } + } + } catch (e) { + // Don't fail login - trigger may have already created the user + console.warn('Auth sync error on login:', e?.message || e); + } } } else { setSupabaseUser(null); diff --git a/client/src/ThemeProvider.js b/client/src/ThemeProvider.js new file mode 100644 index 0000000..c21bec6 --- /dev/null +++ b/client/src/ThemeProvider.js @@ -0,0 +1,55 @@ +import { React, useState, createContext, useContext, useEffect } from "react"; + +export const themeContext = createContext(); + +export const ThemeProvider = ({ children }) => { + const [darkMode, setDarkModeState] = useState(() => { + // Initialize from localStorage or default to false + const saved = localStorage.getItem("darkMode"); + return saved ? JSON.parse(saved) : false; + }); + + // Apply dark mode class on mount and whenever darkMode changes + useEffect(() => { + // Apply dark mode class to document body + if (darkMode) { + document.documentElement.classList.add("dark-mode"); + document.body.classList.add("dark-mode"); + } else { + document.documentElement.classList.remove("dark-mode"); + document.body.classList.remove("dark-mode"); + } + }, [darkMode]); + + // Save to localStorage whenever darkMode changes + useEffect(() => { + localStorage.setItem("darkMode", JSON.stringify(darkMode)); + }, [darkMode]); + + const toggleDarkMode = () => { + setDarkModeState(prev => !prev); + }; + + const setDarkMode = (value) => { + setDarkModeState(value); + }; + + return ( + + {children} + + ); +}; + +export const useTheme = () => { + const context = useContext(themeContext); + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +}; + diff --git a/client/src/components/NavBar/NavBar.css b/client/src/components/NavBar/NavBar.css index 22b8000..77aa24b 100644 --- a/client/src/components/NavBar/NavBar.css +++ b/client/src/components/NavBar/NavBar.css @@ -130,4 +130,59 @@ .divider { border-bottom-width: flex; border-bottom-color: #ccc; +} + +/* Dark Mode Styles */ +.dark-mode .navbar { + background-color: #2d2d2d; +} + +.dark-mode .nav-item { + background-color: #2d2d2d; + color: #e0e0e0; +} + +.dark-mode .nav-item:hover { + background-color: #3a3a3a; +} + +.dark-mode .nav-item-active { + background-color: #3a3a3a; + color: #e0e0e0; +} + +.dark-mode .nested-navbar { + background-color: #3a3a3a; +} + +.dark-mode .nav-item-nested { + background-color: #3a3a3a; + color: #e0e0e0; +} + +.dark-mode .nav-item-nested:hover { + background-color: #4a4a4a; +} + +.dark-mode .nav-item-nested-active { + background-color: #4a4a4a; + color: #e0e0e0; +} + +.dark-mode .settings-icon { + background-color: #2d2d2d; + color: #e0e0e0; +} + +.dark-mode .settings-icon:hover { + color: #4CAF50; +} + +.dark-mode .help-icon { + background-color: #2d2d2d; + color: #e0e0e0; +} + +.dark-mode .help-icon:hover { + color: #4CAF50; } \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index c680534..a693625 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,3 +1,21 @@ +:root { + --bg-color: #E7F2D8; + --text-color: #000000; + --card-bg: #ffffff; + --border-color: #ddd; + --input-bg: #ffffff; + --input-border: #ccc; +} + +.dark-mode { + --bg-color: #1a1a1a; + --text-color: #e0e0e0; + --card-bg: #2d2d2d; + --border-color: #444; + --input-bg: #3a3a3a; + --input-border: #555; +} + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', @@ -5,7 +23,9 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - background-color: #E7F2D8; + background-color: var(--bg-color); + color: var(--text-color); + transition: background-color 0.3s ease, color 0.3s ease; } code { diff --git a/client/src/index.js b/client/src/index.js index 2cda063..de10c96 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -17,6 +17,7 @@ import Dashboard from './components/UserActivityDashboard/UserActivityDash'; import WebsiteSettings from './pages/WebsiteSettings/WebsiteSettings'; // import Register from './pages/Register/Register'; import { CurrentUserProvider } from './CurrentUserProvider'; +import { ThemeProvider } from './ThemeProvider'; import Help from './pages/Help/Help'; import Chat from './pages/Chat/Chat'; import ViewSharedTree from './pages/ViewSharedTree/ViewSharedTree'; @@ -155,13 +156,15 @@ const router = createBrowserRouter([ const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - - - - - - - + + + + + + + + + ); diff --git a/client/src/pages/Account/Account.js b/client/src/pages/Account/Account.js index a119abb..35afb7c 100644 --- a/client/src/pages/Account/Account.js +++ b/client/src/pages/Account/Account.js @@ -1,17 +1,21 @@ -import { React, useEffect, useState } from 'react'; +import { React, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import * as styles from './styles'; import { useParams, useNavigate } from 'react-router-dom'; import NavBar from '../../components/NavBar/NavBar'; -import AddToTreePopup from '../../components/AddToTree/AddToTree'; import { useCurrentUser } from '../../CurrentUserProvider'; +import { supabase } from '../../utils/supabaseClient'; +import { handleLogout } from '../../utils/authHandlers'; function Account() { const navigate = useNavigate(); // used to change route without refreshing page, used to prevent infinite refreshes const [ownAccount, setOwnAccount] = useState(false); // will be retrieved - const [existsInTree, setExistsInTree] = useState(false); // will be retrieved - const [relationshipType, setRelationshipType] = useState(''); // will be retrieved + const [editingSection, setEditingSection] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(''); + const [saveSuccess, setSaveSuccess] = useState(''); + const fileInputRef = useRef(null); - const { currentUserID, supabaseUser, loading } = useCurrentUser(); + const { supabaseUser, loading } = useCurrentUser(); // Redirect to login if not authenticated useEffect(() => { @@ -35,6 +39,7 @@ function Account() { const [userData, setUserData] = useState({ id: id, + displayName: '', firstName: 'Loading...', lastName: '', email: '', @@ -44,8 +49,51 @@ function Account() { state: '', country: '', phone_number: '', - zipcode: '' - }) + zipcode: '', + profilePictureUrl: '' + }); + const [formValues, setFormValues] = useState({ + displayName: '', + firstName: '', + lastName: '', + birthdate: '', + address: '', + city: '', + state: '', + country: '', + phone_number: '', + zipcode: '', + bio: '', + profilePictureUrl: '' + }); + const displayName = useMemo(() => { + if (userData.displayName) return userData.displayName; + const parts = [userData.firstName, userData.lastName].filter(Boolean); + if (parts.length) return parts.join(' '); + return 'User'; + }, [userData.displayName, userData.firstName, userData.lastName]); + const summaryLocation = useMemo(() => { + return [userData.city, userData.state, userData.country].filter(Boolean).join(', '); + }, [userData.city, userData.state, userData.country]); + const [profilePicturePreview, setProfilePicturePreview] = useState(''); + const [profilePictureFile, setProfilePictureFile] = useState(null); + const [removeProfilePicture, setRemoveProfilePicture] = useState(false); + const [backendUserId, setBackendUserId] = useState(null); + const [deleteState, setDeleteState] = useState({ loading: false, error: '', success: '' }); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [totpFactorId, setTotpFactorId] = useState(''); + const [totpQr, setTotpQr] = useState(''); + const [totpCode, setTotpCode] = useState(''); + const [totpStatus, setTotpStatus] = useState(''); + const [totpLoading, setTotpLoading] = useState(false); + const [totpVerified, setTotpVerified] = useState(false); + const totpStatusIsError = useMemo(() => { + if (!totpStatus) return false; + return /error|fail|unable|invalid|problem|could not/i.test(totpStatus); + }, [totpStatus]); + const [emailVerified, setEmailVerified] = useState(false); + const [emailVerificationStatus, setEmailVerificationStatus] = useState(''); + const [emailVerificationLoading, setEmailVerificationLoading] = useState(false); // Fetch user info - check if it's a Supabase user or family member useEffect(() => { @@ -53,22 +101,71 @@ function Account() { // Check if this is the current Supabase user if (id === supabaseUser?.id) { - console.log('Supabase user data:', supabaseUser); - console.log('User metadata:', supabaseUser.user_metadata); - - setUserData({ - id: supabaseUser.id, - firstName: supabaseUser.user_metadata?.first_name || 'User', - lastName: supabaseUser.user_metadata?.last_name || '', - email: supabaseUser.email, - birthdate: supabaseUser.user_metadata?.birthdate || '', - address: supabaseUser.user_metadata?.address || '', - city: supabaseUser.user_metadata?.city || '', - state: supabaseUser.user_metadata?.state || '', - country: supabaseUser.user_metadata?.country || '', - phone_number: supabaseUser.user_metadata?.phone_number || '', - zipcode: supabaseUser.user_metadata?.zipcode || '' - }); + // Database is source of truth - fetch from database first + fetch(`http://localhost:5000/api/auth/user/email/${encodeURIComponent(supabaseUser.email)}`) + .then(async (response) => { + if (response.ok) { + const dbUser = await response.json(); + // Map database fields to frontend format + setUserData({ + id: dbUser.id, + displayName: dbUser.display_name || '', + firstName: dbUser.firstname || dbUser.firstName || 'User', + lastName: dbUser.lastname || dbUser.lastName || '', + email: dbUser.email || supabaseUser.email, + birthdate: dbUser.birthdate || dbUser.birthDate || '', + address: dbUser.address || '', + city: dbUser.city || '', + state: dbUser.state || '', + country: dbUser.country || '', + phone_number: dbUser.phonenumber || dbUser.phoneNumber || '', + zipcode: dbUser.zipcode || '', + bio: dbUser.bio || '', + profilePictureUrl: dbUser.profile_picture_url || '' + }); + } else { + // Fallback to auth metadata if database lookup fails + console.warn('Database lookup failed, using auth metadata'); + const metadata = supabaseUser.user_metadata || {}; + setUserData({ + id: supabaseUser.id, + displayName: metadata.display_name || '', + firstName: metadata.first_name || 'User', + lastName: metadata.last_name || '', + email: supabaseUser.email, + birthdate: metadata.birthdate || '', + address: metadata.address || '', + city: metadata.city || '', + state: metadata.state || '', + country: metadata.country || '', + phone_number: metadata.phone_number || '', + zipcode: metadata.zipcode || '', + bio: metadata.bio || '', + profilePictureUrl: metadata.profile_picture_url || '' + }); + } + }) + .catch((error) => { + console.error('Error fetching user from database:', error); + // Fallback to auth metadata + const metadata = supabaseUser.user_metadata || {}; + setUserData({ + id: supabaseUser.id, + displayName: metadata.display_name || '', + firstName: metadata.first_name || 'User', + lastName: metadata.last_name || '', + email: supabaseUser.email, + birthdate: metadata.birthdate || '', + address: metadata.address || '', + city: metadata.city || '', + state: metadata.state || '', + country: metadata.country || '', + phone_number: metadata.phone_number || '', + zipcode: metadata.zipcode || '', + bio: metadata.bio || '', + profilePictureUrl: metadata.profile_picture_url || '' + }); + }); setOwnAccount(true); return; } @@ -83,12 +180,29 @@ function Account() { .then(async (response) => { if (response.ok) { const data = await response.json(); - setUserData(data); + setUserData({ + displayName: data.displayName || '', + firstName: data.firstName, + lastName: data.lastName, + email: data.email, + birthdate: data.birthdate, + address: data.address, + city: data.city, + state: data.state, + country: data.country, + phone_number: data.phone_number, + zipcode: data.zipcode, + id: data.id, + memberUserId: data.memberUserId, + profilePictureUrl: data.profilePictureUrl || '', + bio: data.bio || '' + }); } else { console.error('Error fetching user data:', response); // If family member not found, show basic info setUserData({ id: id, + displayName: '', firstName: 'Unknown', lastName: 'User', email: '', @@ -101,79 +215,461 @@ function Account() { }, [id, supabaseUser]); useEffect(() => { - // Check if this is the current user's own account - if (id === supabaseUser?.id) { - setOwnAccount(true); - return; + if (editingSection === null) { + setFormValues({ + displayName: userData.displayName || '', + firstName: userData.firstName || '', + lastName: userData.lastName || '', + birthdate: userData.birthdate ? userData.birthdate.split('T')[0] : '', + address: userData.address || '', + city: userData.city || '', + state: userData.state || '', + country: userData.country || '', + phone_number: userData.phone_number || '', + zipcode: userData.zipcode || '', + bio: userData.bio || '', + profilePictureUrl: userData.profilePictureUrl || '' + }); + if (profilePicturePreview) { + URL.revokeObjectURL(profilePicturePreview); + } + setProfilePicturePreview(''); + setProfilePictureFile(null); + setRemoveProfilePicture(false); } + }, [editingSection, userData, profilePicturePreview]); - // If it's not the current user, check relationships (only for family members) - if (!userData.memberUserId) { - setOwnAccount(false); + useEffect(() => { + let isMounted = true; + if (!ownAccount || !supabaseUser?.email) { + setBackendUserId(null); return; } - const requestOptions = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }; - - // if not self, determine relationship to user - fetch(`http://localhost:5000/api/relationships/${id}`, requestOptions) - .then(async(response) => { + const loadBackendUserId = async () => { + try { + const response = await fetch(`http://localhost:5000/api/auth/user/email/${encodeURIComponent(supabaseUser.email)}`); + if (!isMounted) { + return; + } if (response.ok) { - let relationships = await response.json(); // [{id: '', relationshipType: ''}, {}, {}] - console.log("relationships", relationships); - for (let i = 0; i < relationships.length; i++) { - if(relationships[i].person1_id === parseInt(currentUserID) && relationships[i].person2_id === parseInt(id)) { - // this is the relationship - setRelationshipType(relationships[i].relationshipType); - return; - } + const data = await response.json(); + setBackendUserId(data?.id ?? null); + } else { + setBackendUserId(null); + } + } catch (error) { + if (isMounted) { + console.error('Failed to load backend user id:', error); + } + } + }; + + loadBackendUserId(); + + return () => { + isMounted = false; + }; + }, [ownAccount, supabaseUser?.email]); + + const loadTotpFactors = useCallback(async () => { + if (!supabaseUser?.id) return; + try { + const { data: factorsData, error } = await supabase.auth.mfa.listFactors(); + if (error) throw error; + const verified = factorsData?.all?.find((f) => f.factor_type === 'totp' && f.status === 'verified'); + const unverified = factorsData?.all?.find((f) => f.factor_type === 'totp' && f.status === 'unverified'); + if (verified) { + setTotpVerified(true); + setTotpFactorId(verified.id); + setTotpQr(''); + } else if (unverified) { + setTotpVerified(false); + setTotpFactorId(unverified.id); + setTotpQr(''); + } else { + setTotpVerified(false); + setTotpFactorId(''); + setTotpQr(''); + } + } catch (error) { + console.error('Load factors error:', error); + setTotpStatus(error.message || 'Unable to load multi-factor settings.'); + } + }, [supabaseUser?.id]); + + // Check email verification status + const checkEmailVerification = useCallback(async () => { + if (!supabaseUser) return; + try { + const { data: { user }, error } = await supabase.auth.getUser(); + if (error) throw error; + setEmailVerified(user?.email_confirmed_at ? true : false); + } catch (error) { + console.error('Error checking email verification:', error); + } + }, [supabaseUser]); + + useEffect(() => { + if (!ownAccount || !supabaseUser?.id) return; + loadTotpFactors(); + checkEmailVerification(); + }, [ownAccount, supabaseUser?.id, loadTotpFactors, checkEmailVerification]); + + useEffect(() => { + if (ownAccount) return; + setTotpFactorId(''); + setTotpQr(''); + setTotpCode(''); + setTotpStatus(''); + setTotpVerified(false); + setTotpLoading(false); + }, [ownAccount]); + + const handleFieldChange = (field, value) => { + setFormValues((prev) => ({ + ...prev, + [field]: value + })); + }; + + const handleStartEditing = (section) => { + setSaveError(''); + setSaveSuccess(''); + setEditingSection(section); + }; + + const handleCancelEditing = () => { + setSaveError(''); + setSaveSuccess(''); + setEditingSection(null); + }; + + const handleSaveProfile = async (event) => { + event.preventDefault(); + if (!ownAccount || !supabaseUser) return; + + setIsSaving(true); + setSaveError(''); + setSaveSuccess(''); + + try { + let nextProfileUrl = userData.profilePictureUrl || ''; + + if (profilePictureFile) { + // Use FormData for efficient file upload (bypasses RLS via backend) + const formData = new FormData(); + formData.append('file', profilePictureFile); + formData.append('auth_uid', supabaseUser.id); + + // Upload via backend using service role (bypasses RLS) + const uploadResponse = await fetch('http://localhost:5000/api/auth/upload-profile-picture', { + method: 'POST', + // Don't set Content-Type header - browser will set it with boundary for FormData + body: formData + }); + + if (!uploadResponse.ok) { + const errorData = await uploadResponse.json().catch(() => ({})); + throw new Error(errorData?.error || errorData?.details || 'Failed to upload profile picture'); + } + + const uploadResult = await uploadResponse.json(); + nextProfileUrl = uploadResult.publicUrl || ''; + } else if (removeProfilePicture && userData.profilePictureUrl) { + try { + const pathFromUrl = userData.profilePictureUrl.split('/profile-pictures/')[1]; + if (pathFromUrl) { + await supabase.storage + .from('profile-pictures') + .remove([pathFromUrl]); } - } - else { - // print message in return body - const errorData = await response.json(); - console.error('Error:', errorData.message); + } catch (removeError) { + console.warn('Unable to remove old avatar:', removeError); } - }) - .catch(error => { - console.error('There was a problem with the fetch operation:', error); + nextProfileUrl = ''; + } + + // Update database first (source of truth) + const profilePayload = { + auth_uid: supabaseUser.id, + email: supabaseUser.email, // Keep existing email unless explicitly changed + username: supabaseUser.email, // Use email as username for now + displayName: formValues.displayName?.trim() || null, + firstName: formValues.firstName?.trim() || null, + lastName: formValues.lastName?.trim() || null, + birthdate: formValues.birthdate || null, + address: formValues.address?.trim() || null, + city: formValues.city?.trim() || null, + state: formValues.state?.trim() || null, + country: formValues.country?.trim() || null, + phone_number: formValues.phone_number?.trim() || null, + zipcode: formValues.zipcode?.trim() || null, + bio: formValues.bio?.trim() || null, + profilePictureUrl: nextProfileUrl || null + }; + + const response = await fetch('http://localhost:5000/api/auth/profile', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(profilePayload) }); - }, [id, currentUserID, userData.id, userData.memberUserId, supabaseUser?.id]); - // check if user exists in tree - useEffect(() => { - if (!id || !supabaseUser?.id) return; + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + const errorMsg = errorData?.error || errorData?.details || 'Failed to update profile'; + console.error('Profile update error:', errorData); + throw new Error(errorMsg); + } - const requestOptions = { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }; + const updatedUser = await response.json(); - fetch(`http://localhost:5000/api/tree-info/${supabaseUser.id}`, requestOptions) - .then(async (response) => { - if (response.ok) { - console.log("tree info response"); - const treeMembers = await response.json(); // {id: accountID, object: []} - console.log(treeMembers); - for (let i = 0; i < treeMembers.object.length; i++) { - if (treeMembers.object[i].id === id) { - console.log("account exists in user's tree"); - setExistsInTree(true); + // Map database fields back to frontend format + setUserData((prev) => ({ + ...prev, + displayName: updatedUser.display_name || '', + firstName: updatedUser.firstname || '', + lastName: updatedUser.lastname || '', + birthdate: updatedUser.birthdate || '', + address: updatedUser.address || '', + city: updatedUser.city || '', + state: updatedUser.state || '', + country: updatedUser.country || '', + phone_number: updatedUser.phonenumber || '', + zipcode: updatedUser.zipcode || '', + bio: updatedUser.bio || '', + profilePictureUrl: updatedUser.profile_picture_url || '', + email: updatedUser.email || prev.email + })); + + setSaveSuccess('Profile updated successfully.'); + setEditingSection(null); + } catch (error) { + console.error('Error updating profile:', error); + setSaveError(error.message || 'Something went wrong while saving your profile.'); + } finally { + setIsSaving(false); + } + }; + + const handleProfileImageClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleProfileImageChange = (event) => { + const file = event.target.files && event.target.files[0]; + if (!file) return; + if (profilePicturePreview) { + URL.revokeObjectURL(profilePicturePreview); + } + setProfilePictureFile(file); + setProfilePicturePreview(URL.createObjectURL(file)); + setRemoveProfilePicture(false); + }; + + const handleRemoveProfileImage = () => { + if (profilePicturePreview) { + URL.revokeObjectURL(profilePicturePreview); + } + setProfilePicturePreview(''); + setProfilePictureFile(null); + setRemoveProfilePicture(true); + }; + + const startTotpEnroll = async () => { + if (!ownAccount) return; + setTotpStatus(''); + setTotpLoading(true); + try { + if (totpVerified) { + setTotpStatus('Two-factor authentication is already enabled.'); + return; + } + if (totpFactorId && !totpVerified) { + setTotpStatus('A setup is pending. Enter the code below or start over.'); return; } + const { data, error } = await supabase.auth.mfa.enroll({ factorType: 'totp' }); + if (error) throw error; + setTotpFactorId(data.id); + setTotpQr(data.totp?.qr_code || ''); + } catch (error) { + console.error('Enroll error:', error); + setTotpStatus(error.message || 'Unable to start authenticator setup.'); + } finally { + setTotpLoading(false); + } + }; + + const verifyTotp = async () => { + if (!ownAccount || !totpFactorId || !totpCode) return; + setTotpLoading(true); + setTotpStatus(''); + try { + const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({ factorId: totpFactorId }); + if (challengeError) throw challengeError; + const challengeId = challengeData?.id; + const { error } = await supabase.auth.mfa.verify({ factorId: totpFactorId, challengeId, code: totpCode }); + if (error) throw error; + setTotpStatus('Two-factor authentication enabled.'); + setTotpCode(''); + setTotpQr(''); + await loadTotpFactors(); + } catch (error) { + console.error('TOTP verify error:', error); + setTotpStatus(error.message || 'Verification failed.'); + } finally { + setTotpLoading(false); + } + }; + + const disableTotp = async () => { + if (!ownAccount || !totpFactorId) return; + setTotpLoading(true); + setTotpStatus(''); + try { + const { error } = await supabase.auth.mfa.unenroll({ factorId: totpFactorId }); + if (error) throw error; + setTotpStatus('Two-factor authentication disabled.'); + setTotpFactorId(''); + setTotpVerified(false); + setTotpQr(''); + setTotpCode(''); + await loadTotpFactors(); + } catch (error) { + console.error('Disable TOTP error:', error); + setTotpStatus(error.message || 'Failed to disable authenticator.'); + } finally { + setTotpLoading(false); + } + }; + + const restartTotpEnroll = async () => { + if (!ownAccount) return; + setTotpStatus(''); + setTotpLoading(true); + try { + if (totpFactorId) { + const { error } = await supabase.auth.mfa.unenroll({ factorId: totpFactorId }); + if (error) throw error; + } + setTotpFactorId(''); + setTotpQr(''); + setTotpCode(''); + setTotpLoading(false); + await startTotpEnroll(); + } catch (error) { + console.error('Restart TOTP error:', error); + setTotpLoading(false); + setTotpStatus(error.message || 'Could not restart setup.'); + } + }; + + const resendVerificationEmail = async () => { + if (!ownAccount || !supabaseUser?.email) return; + setEmailVerificationLoading(true); + setEmailVerificationStatus(''); + try { + const { error } = await supabase.auth.resend({ + type: 'signup', + email: supabaseUser.email, + options: { + emailRedirectTo: `${window.location.origin}/account/${supabaseUser.id}` + } + }); + if (error) throw error; + setEmailVerificationStatus('Verification email sent! Please check your inbox and click the confirmation link.'); + // Refresh verification status after a delay + setTimeout(() => { + checkEmailVerification(); + }, 2000); + } catch (error) { + console.error('Resend verification email error:', error); + setEmailVerificationStatus(error.message || 'Failed to send verification email.'); + } finally { + setEmailVerificationLoading(false); + } + }; + + const handleSignOut = async () => { + await handleLogout(); + }; + + const handleDeleteAccountClick = () => { + setShowDeleteConfirm(true); + setDeleteState({ loading: false, error: '', success: '' }); + }; + + const handleDeleteCancel = () => { + setShowDeleteConfirm(false); + setDeleteState({ loading: false, error: '', success: '' }); + }; + + const handleDeleteAccount = async () => { + if (!ownAccount || !supabaseUser) return; + + setDeleteState({ loading: true, error: '', success: '' }); + setShowDeleteConfirm(false); + + try { + // Delete profile picture from storage if it exists + if (userData.profilePictureUrl) { + try { + const pathFromUrl = userData.profilePictureUrl.split('/profile-pictures/')[1]; + if (pathFromUrl) { + await supabase.storage + .from('profile-pictures') + .remove([pathFromUrl]); } - } else { - const errorData = await response.json(); - console.error('Error fetching tree info:', errorData.message); + } catch (storageError) { + console.warn('Unable to delete profile picture:', storageError); + // Continue with account deletion even if picture deletion fails } - }) - .catch((error) => { - console.error('There was a problem with the fetch operation:', error); + } + + // Get backend user ID + let apiUserId = backendUserId; + if (!apiUserId && supabaseUser.email) { + const response = await fetch(`http://localhost:5000/api/auth/user/email/${encodeURIComponent(supabaseUser.email)}`); + if (response.ok) { + const data = await response.json(); + apiUserId = data?.id; + setBackendUserId(data?.id ?? null); + } + } + + if (!apiUserId) { + throw new Error('Unable to locate your account record for deletion.'); + } + + // Delete account from backend (handles database, tree members, relationships, and Supabase auth) + console.log('Attempting to delete account - User ID:', apiUserId); + const response = await fetch(`http://localhost:5000/api/auth/remove/${apiUserId}`, { + method: 'DELETE' }); - }, [id, supabaseUser?.id]); + + console.log('Delete account response status:', response.status); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + console.error('Delete account error response:', errorData); + throw new Error(errorData?.error || errorData?.details || 'Failed to delete account.'); + } + + const result = await response.json().catch(() => ({})); + console.log('Delete account success:', result); + + // Logout and redirect to login + await handleLogout(); + window.location.href = '/login'; + } catch (error) { + console.error('Delete account error:', error); + setDeleteState({ loading: false, error: error.message || 'Failed to delete account.', success: '' }); + setShowDeleteConfirm(true); // Show dialog again on error + } + }; return (
    @@ -182,64 +678,606 @@ function Account() {
    -
    -
    -
    -

    {userData?.firstName} {userData?.lastName}

    -

    {ownAccount ? "You" : (relationshipType.charAt(0).toUpperCase() + relationshipType.slice(1))}

    + {/* User Information Section */} +
    +
    +
    +
    +

    My Profile

    +

    Your public profile details

    +
    + {ownAccount && ( + editingSection === 'profile' ? ( +
    + + +
    + ) : ( + + ) + )} +
    +
    +
    +
    + {profilePicturePreview || userData.profilePictureUrl ? ( + {`${displayName} + ) : ( +
    + {displayName?.charAt(0)?.toUpperCase() || '?'} +
    + )} + {ownAccount && editingSection === 'profile' && ( + <> + + {(userData.profilePictureUrl || profilePicturePreview) && ( + + )} + + + )} +
    +
    + {editingSection === 'profile' ? ( +
    + + handleFieldChange('displayName', e.target.value)} + style={styles.FieldStyle} + /> +
    + ) : ( +
    +

    {displayName}

    +
    + )} +
    + {summaryLocation && {summaryLocation}} +
    +
    +
    - - {/* if someone else's account, show buttons */} - {!ownAccount && ( -
    - Add To Tree} accountUserName={userData.firstName} accountUserId={id} userId={supabaseUser?.id} currentUserAccountRelationshipType={relationshipType} /> - +
    + +
    +
    +
    +

    Personal information

    +

    Basics about you

    + {ownAccount && ( + editingSection === 'personal' ? ( +
    + + +
    + ) : ( + + ) )}
    - {/* divider line */} -
    + {editingSection === 'personal' ? ( +
    +
    + + handleFieldChange('firstName', e.target.value)} + style={styles.FieldStyle} + /> +
    +
    + + handleFieldChange('lastName', e.target.value)} + style={styles.FieldStyle} + /> +
    +
    + + +
    +
    + + handleFieldChange('phone_number', e.target.value)} + style={styles.FieldStyle} + /> +
    +
    + + handleFieldChange('birthdate', e.target.value)} + style={styles.FieldStyle} + /> +
    +
    + +