From 3a09d77973ef871710066a2ce05ae29199b6cedb Mon Sep 17 00:00:00 2001 From: cdl431 Date: Mon, 20 Oct 2025 16:42:19 -0500 Subject: [PATCH 01/14] Added an edit function --- server/controllers/authController.js | 51 ++++++++++++++++++++++++---- server/routes/authRoutes.js | 2 +- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/server/controllers/authController.js b/server/controllers/authController.js index e9037fa..7962d04 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -71,20 +71,59 @@ const login = async(req,res) => { } }; -const deleteByUser = async (req,res) => { +const editByUser = async (req, res) => { + try { + const { id } = req.params; + const { username, email, password } = req.body; + + const user = await User.findById(id); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + const updatedFields = {}; + + if (username) updatedFields.username = username; + if (email) { + const existingUser = await User.findByEmail(email); + if (existingUser && existingUser.id !== parseInt(id)) { + return res.status(400).json({ error: 'Email already in use' }); + } + updatedFields.email = email; + } + if (password) { + const saltRounds = 12; + const salt = await bcrypt.genSalt(saltRounds); + const hashedPassword = await bcrypt.hash(password, salt); + updatedFields.password = hashedPassword; + } + + const updatedUser = await User.updateUser(id, updatedFields); + + res.status(200).json({ + message: 'User updated successfully', + user: updatedUser + }); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Error updating user' }); + } +}; + +async function deleteByUser(req, res) { const { id } = req.params; - try{ + try { await User.deleteUser(id); - res.json({ + res.json({ message: "User deleted successfullyS" - }) + }); } - catch (error){ + catch (error) { console.error(error); - res.status(500);json({error:"Error deleting user"}) + res.status(500); json({ error: "Error deleting user" }); } } diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index a14ab28..3121695 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -2,7 +2,7 @@ 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 { register, login, deleteByUser, findByEmail, findById, getAllUsers, } = require('../controllers/authController'); // Assuming you have a controller for your registration logic console.log('Register function:', register); From f5e554f038c56fd2af03e9a60c2a370d5c12d24a Mon Sep 17 00:00:00 2001 From: cdl431 Date: Mon, 3 Nov 2025 17:57:34 -0600 Subject: [PATCH 02/14] Added an edit folder to client:component --- client/src/components/edit/edit.js | 17 +++++ client/src/components/edit/popup.css | 5 ++ client/src/components/edit/styles.js | 99 ++++++++++++++++++++++++++++ server/routes/treeMemberRoute.js | 2 +- 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 client/src/components/edit/edit.js create mode 100644 client/src/components/edit/popup.css create mode 100644 client/src/components/edit/styles.js diff --git a/client/src/components/edit/edit.js b/client/src/components/edit/edit.js new file mode 100644 index 0000000..270c28d --- /dev/null +++ b/client/src/components/edit/edit.js @@ -0,0 +1,17 @@ +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 * as styles from './styles'; +import './popup.css'; +import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; +import { ReactComponent as ImportIcon } from '../../assets/import.svg'; +import { useCurrentUser } from '../../CurrentUserProvider'; +import { editTreeMember } from '../../../../server/controllers/treeMemberController'; + +Function editTreeMember({ trigger, userid }) { + + + +} \ No newline at end of file diff --git a/client/src/components/edit/popup.css b/client/src/components/edit/popup.css new file mode 100644 index 0000000..c230a6a --- /dev/null +++ b/client/src/components/edit/popup.css @@ -0,0 +1,5 @@ +.popup-content { + width: auto; + border-radius: 30px; + min-width: 0px; +} \ No newline at end of file diff --git a/client/src/components/edit/styles.js b/client/src/components/edit/styles.js new file mode 100644 index 0000000..80059fe --- /dev/null +++ b/client/src/components/edit/styles.js @@ -0,0 +1,99 @@ +export const DefaultStyle = { + fontFamily: 'Alata', +}; + +export const FieldStyle = { + borderRadius: '5px', + border: '1px solid #000000', + marginLeft: '10px' +}; + +export const ListStyle = { + listStyleType: 'none', + fontFamily: 'Alata', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', + marginRight: '15%' +}; + +export const ButtonDivStyle = { + fontFamily: 'Alata', + display: 'flex', + justifyContent: 'center', +} + +export const ButtonStyle = { + fontFamily: 'Alata', + backgroundColor: '#3a5a40', + color: 'white', + borderRadius: '20px', + border: 'none', + padding: '10px 30px', + margin: '10px', + cursor: 'pointer', +} + +export const GrayButtonStyle = { + fontFamily: 'Alata', + backgroundColor: '#D9D9D9', + color: 'black', + borderRadius: '20px', + border: 'none', + padding: '10px 20px', + margin: '10px', + cursor: 'pointer', + display: 'flex', + flexDirection: 'row', + boxShadow: 'gray 0px 10px 10px -8px', +} + +export const FormStyle = { + padding: '2vw', + paddingTop: '0px', + minWidth: '360px', +} + +export const ItemStyle = { + margin: '10px 0px' +} + +export const DateFieldStyle = { + borderRadius: '5px', + border: '1px solid #000000', + marginLeft: '10px', + width: '147px', + fontFamily: 'Alata' +}; + + +export const MainContainerStyle = { + display: 'flex', + flexDirection: 'column', + padding: '2vw', + paddingTop: '0px', + alignItems: 'center', + minWidth: '360px', + minHeight: '150px', + justifyContent: 'space-between', +} + +export const AddOptionsStyle = { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + width: '90%', + padding: '10px', + fontFamily: 'Alata', + // border: '1px solid gray', + // borderRadius: '5px', + marginTop: '10px', + height: '200px', + overflow: 'auto' +} + +export const ListingStyle = { + padding: '10px', + border: '1px solid gray', + width: '90%' +} \ No newline at end of file diff --git a/server/routes/treeMemberRoute.js b/server/routes/treeMemberRoute.js index 7c45829..5435d13 100644 --- a/server/routes/treeMemberRoute.js +++ b/server/routes/treeMemberRoute.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); -const { addTreeMember, editTreeMember,getMembersByUser, getMembersByOtherUser, deleteByUser, getMemberById, getActiveMemberId } = require('../controllers/treeMemberController'); +const { addTreeMember, editTreeMember,getMembersByUser, getMembersByOtherUser, deleteByUser, getMemberById, getActiveMemberId} = require('../controllers/treeMemberController'); router.post('/', addTreeMember); router.put('/:id', editTreeMember); From 7cfd62d56c08f3e1c74f4edf993819d11bcef546 Mon Sep 17 00:00:00 2001 From: cdl431 Date: Mon, 10 Nov 2025 16:22:20 -0600 Subject: [PATCH 03/14] Finished code for EditFamilyMember function --- .../EditFamilyMember/EditFamilyMember.js | 254 ++++++++++++++++++ .../{edit => EditFamilyMember}/popup.css | 0 .../{edit => EditFamilyMember}/styles.js | 0 client/src/components/edit/edit.js | 17 -- 4 files changed, 254 insertions(+), 17 deletions(-) create mode 100644 client/src/components/EditFamilyMember/EditFamilyMember.js rename client/src/components/{edit => EditFamilyMember}/popup.css (100%) rename client/src/components/{edit => EditFamilyMember}/styles.js (100%) delete mode 100644 client/src/components/edit/edit.js diff --git a/client/src/components/EditFamilyMember/EditFamilyMember.js b/client/src/components/EditFamilyMember/EditFamilyMember.js new file mode 100644 index 0000000..2d4369c --- /dev/null +++ b/client/src/components/EditFamilyMember/EditFamilyMember.js @@ -0,0 +1,254 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import Popup from 'reactjs-popup'; +import 'reactjs-popup/dist/index.css'; +import { useForm } from 'react-hook-form'; +import * as styles from './styles'; +import './popup.css'; +import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; +import { useCurrentUser } from '../../CurrentUserProvider'; + +function EditFamilyMemberPopup({ trigger, memberId, onSaved }) { + const { currentUserID } = useCurrentUser(); + + const [loading, setLoading] = useState(true); + const [view, setView] = useState("basic"); + + const [memberData, setMemberData] = useState(null); + const [relationshipData, setRelationshipData] = useState(null); + + const matPat = useMemo(() => ["parent", "cousin", "aunt", "uncle", "grandparent", "niece", "nephew"], []); + + + + const { + register, + handleSubmit, + reset + } = useForm(); + + const { + register: registerRel, + handleSubmit: handleSubmitRel, + reset: resetRel + } = useForm(); + + + + useEffect(() => { + const load = async () => { + try { + setLoading(true); + + const memberRes = await fetch(`http://localhost:5000/api/family-members/${memberId}`); + const memberJson = await memberRes.json(); + + const relRes = await fetch(`http://localhost:5000/api/relationships/member/${memberId}`); + const relJson = await relRes.json(); + + setMemberData(memberJson); + setRelationshipData(relJson); + + // preload forms + reset({ + firstName: memberJson.firstName, + lastName: memberJson.lastName, + location: memberJson.location, + birthday: memberJson.birthDate, + birthplace: memberJson.birthplace, + deathdate: memberJson.deathDate + }); + + resetRel({ + relationship: relJson.relationshipType, + matPat: relJson.side + }); + + } catch (err) { + console.error(err); + } + + setLoading(false); + }; + + if (memberId) load(); + }, [memberId, reset, resetRel]); + + // ----------- CLOSE HANDLER ----------- + + const closeModal = (close) => { + reset(); + resetRel(); + close(); + }; + + // ----------- SAVE BASIC INFO ----------- + + const onSubmitBasic = async (data) => { + try { + const res = await fetch(`http://localhost:5000/api/family-members/${memberId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + firstName: data.firstName, + lastName: data.lastName, + location: data.location || null, + birthDate: data.birthday || null, + birthplace: data.birthplace || null, + deathDate: data.deathdate || null + }) + }); + + if (res.ok) onSaved && onSaved(); + } catch (err) { + console.error(err); + } + }; + + + + const onSubmitRelationship = async (data) => { + try { + const res = await fetch(`http://localhost:5000/api/relationships/${relationshipData.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + relationshipType: data.relationship, + side: matPat.includes(data.relationship) ? data.matPat : null, + relationshipStatus: "active" + }) + }); + + if (res.ok) onSaved && onSaved(); + + } catch (err) { + console.error(err); + } + }; + + if (loading) return null; + + return ( + + {(close) => ( +
+ + {/* Close button */} +
+ +
+ +

+ Edit Family Member +

+ + {/* Tab Selector */} +
+ + +
+ + {/* BASIC INFO FORM */} + {view === "basic" && ( +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ +
+ +
+
+ )} + + {/* RELATIONSHIP FORM */} + {view === "relationship" && ( +
+
    +
  • + +
  • + + {/* Maternal/paternal only shown when needed */} + {matPat.includes(relationshipData.relationshipType) && ( +
  • + Maternal + Paternal +
  • + )} +
+ +
+ +
+
+ )} + +
+ )} +
+ ); +} + +export default EditFamilyMemberPopup; \ No newline at end of file diff --git a/client/src/components/edit/popup.css b/client/src/components/EditFamilyMember/popup.css similarity index 100% rename from client/src/components/edit/popup.css rename to client/src/components/EditFamilyMember/popup.css diff --git a/client/src/components/edit/styles.js b/client/src/components/EditFamilyMember/styles.js similarity index 100% rename from client/src/components/edit/styles.js rename to client/src/components/EditFamilyMember/styles.js diff --git a/client/src/components/edit/edit.js b/client/src/components/edit/edit.js deleted file mode 100644 index 270c28d..0000000 --- a/client/src/components/edit/edit.js +++ /dev/null @@ -1,17 +0,0 @@ -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 * as styles from './styles'; -import './popup.css'; -import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; -import { ReactComponent as ImportIcon } from '../../assets/import.svg'; -import { useCurrentUser } from '../../CurrentUserProvider'; -import { editTreeMember } from '../../../../server/controllers/treeMemberController'; - -Function editTreeMember({ trigger, userid }) { - - - -} \ No newline at end of file From b921cf9462a8cc97f87035fd5de70e0b2f5c9014 Mon Sep 17 00:00:00 2001 From: cdl431 Date: Fri, 21 Nov 2025 14:28:13 -0600 Subject: [PATCH 04/14] Some changes --- README.md | 14 +- 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 | 162 ++++++-------- client/src/pages/Home/Home.js | 2 +- client/src/pages/Login/Login.js | 191 ++++++++++++---- client/src/pages/Reset/Reset.js | 62 +++++- .../pages/WebsiteSettings/WebsiteSettings.js | 209 +++++++++++++++++- client/src/pages/WebsiteSettings/styles.js | 27 +++ docs/.env.example | 7 + server/controllers/authController.js | 106 +++------ server/controllers/backupController.js | 29 ++- server/controllers/relationshipController.js | 85 +++++-- server/controllers/sharedTreeController.js | 4 +- server/controllers/treeInfoController.js | 16 +- server/controllers/treeMemberController.js | 72 ++++-- server/controllers/treeSummaryController.js | 17 +- server/db/knex.js | 7 - server/knexfile.js | 23 -- .../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/package-lock.json | 148 +++++++++++++ server/package.json | 1 + server/routes/authRoutes.js | 13 +- server/server.js | 35 +-- 35 files changed, 1635 insertions(+), 509 deletions(-) create mode 100644 docs/.env.example delete mode 100644 server/db/knex.js delete mode 100644 server/knexfile.js diff --git a/README.md b/README.md index 2816407..894b9be 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,19 @@ The current KinTree project team as of Spring 2025 includes Owen Adams, Kennedi ### Prerequisites +<<<<<<< Updated upstream 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 get your Supabase URL, CLIENT KEY, and SERVICE KEY from your project dashboard [here](https://supabase.com/) +>>>>>>> Stashed changes -### 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. @@ -35,6 +44,7 @@ Then, from the same directory, run the following command to run the server/API: `node server.js` +<<<<<<< Updated upstream ### Database Setup Run the command `npm install knex mysql2` @@ -48,3 +58,5 @@ Run the command `knex:migrate status` to ensure proper migration files are loade Run the command `knex migrate:latest` to create existing database tables. +======= +>>>>>>> Stashed changes 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/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 9fdc592..ef1e39e 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'; @@ -23,109 +23,63 @@ 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; - - // 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, { + 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', { + // 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 = '/login'; // redirect after registration to login + } catch (error) { + setErrorMessage(error.message); + console.error('Password:', data.password); + } + }; const ButtonStyle = { fontFamily: 'Alata', @@ -147,6 +101,14 @@ const CreateAccount = () => { KinTree Logo

Create Account

+ + {/* Error Message Display */} + {errorMessage && ( +
+ {errorMessage} +
+ )} +
@@ -206,12 +168,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/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/docs/.env.example b/docs/.env.example new file mode 100644 index 0000000..f25ad8d --- /dev/null +++ b/docs/.env.example @@ -0,0 +1,7 @@ +# 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..c6a8b6a 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,15 @@ const deleteByUser = async (req,res) => { } catch (error){ console.error(error); +<<<<<<< Updated upstream res.status(500);json({error:"Error deleting user"}) +======= +<<<<<<< HEAD + res.status(500); json({ error: "Error deleting user" }); +======= + res.status(500).json({error:"Error deleting user"}) +>>>>>>> 5315e049f5602a4d1eb3fed3abe518fd4b3917f5 +>>>>>>> Stashed changes } } @@ -126,4 +64,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 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..f6b562c 100644 --- a/server/controllers/treeMemberController.js +++ b/server/controllers/treeMemberController.js @@ -1,30 +1,61 @@ 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 - }); - /// need to fix that a value can be left empty (deathDate) + userId: userIdInt, // Now guaranteed to be an integer + memberUserId: memberUserIdInt // Now guaranteed to be an integer or null + });s // 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 +116,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 +139,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 +165,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 +186,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/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/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/package-lock.json b/server/package-lock.json index 0dea215..e9cdafd 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", @@ -2261,6 +2360,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", @@ -2280,6 +2385,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", @@ -2339,6 +2450,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 877fbe2..c9c261a 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..03d20ad 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -2,16 +2,21 @@ const express = require('express'); const router = express.Router(); +<<<<<<< Updated upstream const { register, login, deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); // Assuming you have a controller for your registration logic +======= +<<<<<<< HEAD +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'); +>>>>>>> 5315e049f5602a4d1eb3fed3abe518fd4b3917f5 +>>>>>>> Stashed changes -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 926d0eac14fac10585d056f616aac9b6fdcdce2d Mon Sep 17 00:00:00 2001 From: cdl431 Date: Fri, 21 Nov 2025 15:09:38 -0600 Subject: [PATCH 05/14] Added edit function to relationships and authorization --- server/controllers/authController.js | 76 ++++++++++++++++++++ server/controllers/relationshipController.js | 64 ++++++++++++++++- server/routes/authRoutes.js | 10 +++ server/routes/relationshipRoutes.js | 3 +- 4 files changed, 149 insertions(+), 4 deletions(-) diff --git a/server/controllers/authController.js b/server/controllers/authController.js index c6a8b6a..ebea6bb 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -1,5 +1,12 @@ +<<<<<<< Updated upstream // authController.js - the main backend file for user registration, signin, etc const User = require('../models/userModel'); // now backed by Supabase +======= +// authController.js +const bcrypt = require('bcryptjs'); +const User = require('../models/userModel'); +const FamilyMember = require('../models/familyMemberModel'); +>>>>>>> Stashed changes const deleteByUser = async (req,res) => { const { id } = req.params; @@ -14,10 +21,75 @@ const deleteByUser = async (req,res) => { } catch (error){ console.error(error); +<<<<<<< Updated upstream <<<<<<< Updated upstream res.status(500);json({error:"Error deleting user"}) ======= <<<<<<< HEAD +======= + res.status(500).json({ + error: 'Registration failed' + }); + + } +}; + +const editByUser = async (req, res) => { + try { + const { id } = req.params; + const { name, relation, age, phone, email } = req.body; + + + const member = await FamilyMember.findById(id); + if (!member) { + return res.status(404).json({ error: "Family member not found" }); + } + + const updatedFields = {}; + + if (name) updatedFields.name = name; + if (relation) updatedFields.relation = relation; + if (age) updatedFields.age = age; + if (phone) updatedFields.phone = phone; + if (email) { + const emailOwner = await FamilyMember.findByEmail(email); + if (emailOwner && emailOwner.id != id) { + return res.status(400).json({ error: "Email already used by another member" }); + } + updatedFields.email = email; + } + + if (Object.keys(updatedFields).length === 0) { + return res.status(400).json({ error: "No valid fields provided to update" }); + } + + const updatedMember = await FamilyMember.updateFamilyMember(id, updatedFields); + + res.status(200).json({ + message: "Family member updated successfully", + familyMember: updatedMember + }); + + } catch (error) { + console.error(error); + res.status(500).json({ error: "Error updating family member" }); + } +}; + +async function deleteByUser(req, res) { + const { id } = req.params; + + try { + await User.deleteUser(id); + + res.json({ + message: "User deleted successfullyS" + }); + + } + catch (error) { + console.error(error); +>>>>>>> Stashed changes res.status(500); json({ error: "Error deleting user" }); ======= res.status(500).json({error:"Error deleting user"}) @@ -64,6 +136,7 @@ const getAllUsers = async (req, res) => { } } +<<<<<<< Updated upstream module.exports = { deleteByUser, findById, findByEmail, getAllUsers }; // Add a sync endpoint: POST /api/auth/sync @@ -87,3 +160,6 @@ const syncAuthUser = async (req, res) => { }; module.exports.syncAuthUser = syncAuthUser; +======= +module.exports = { register, login, editByUser, deleteByUser, findById, findByEmail, getAllUsers }; +>>>>>>> Stashed changes diff --git a/server/controllers/relationshipController.js b/server/controllers/relationshipController.js index 81db440..c131f9b 100644 --- a/server/controllers/relationshipController.js +++ b/server/controllers/relationshipController.js @@ -110,6 +110,66 @@ const addRelationship = async (req,res) =>{ } } +const editRelationship = async (req, res) => { + try { + const { id } = req.params; + const { person1_id, person2_id, relationshipType, relationshipStatus, side } = req.body; + + const relationship = await Relationship.findById(id); + if (!relationship) { + return res.status(404).json({ error: "Relationship not found" }); + } + + const updatedFields = {}; + + if (person1_id) updatedFields.person1_id = person1_id; + if (person2_id) updatedFields.person2_id = person2_id; + if (relationshipType) updatedFields.relationshipType = relationshipType; + if (relationshipStatus) updatedFields.relationshipStatus = relationshipStatus; + + if (side) { + if (!["paternal", "maternal", "both"].includes(side)) { + return res.status(400).json({ + error: "Invalid side. Must be 'paternal', 'maternal', or 'both'." + }); + } + updatedFields.side = side; + } + + if (Object.keys(updatedFields).length === 0) { + return res.status(400).json({ + error: "No valid fields provided to update" + }); + } + + + if (updatedFields.person1_id || updatedFields.person2_id) { + const person1 = updatedFields.person1_id || relationship.person1_id; + const person2 = updatedFields.person2_id || relationship.person2_id; + + const duplicate = await Relationship.findExisting(person1, person2); + if (duplicate && duplicate.id != id) { + return res.status(400).json({ + error: "A relationship with these people already exists" + }); + } + } + + + const updatedRelationship = await Relationship.updateRelationship(id, updatedFields); + + res.status(200).json({ + message: "Relationship updated successfully", + relationship: updatedRelationship + }); + + } catch (error) { + console.error(error); + res.status(500).json({ error: "Error updating relationship" }); + } +}; + + const filterBySide = async (req,res) => { try{ const {id} = req.params; @@ -151,6 +211,4 @@ const deleteByUser = async (req, res) => { } - - -module.exports = {getRelationships,getRelationshipsByUser,getRelationshipsByOtherUser, addRelationship, filterBySide, deleteByUser}; +module.exports = {getRelationships,getRelationshipsByUser, editRelationship, getRelationshipsByOtherUser, addRelationship, filterBySide, deleteByUser}; diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index 03d20ad..0a08740 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); +<<<<<<< Updated upstream <<<<<<< Updated upstream const { register, login, deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); // Assuming you have a controller for your registration logic ======= @@ -12,6 +13,15 @@ const { deleteByUser, findByEmail, findById, getAllUsers, syncAuthUser } = requi >>>>>>> 5315e049f5602a4d1eb3fed3abe518fd4b3917f5 >>>>>>> Stashed changes +======= +const { register, login, editByUser, 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.patch('/edit/:id', editByUser); +>>>>>>> Stashed changes router.delete('/remove/:id', deleteByUser); router.get('/user/:id', findById); router.get('/user/email/:email', findByEmail); diff --git a/server/routes/relationshipRoutes.js b/server/routes/relationshipRoutes.js index 3459277..21b6651 100644 --- a/server/routes/relationshipRoutes.js +++ b/server/routes/relationshipRoutes.js @@ -1,11 +1,12 @@ const express = require('express'); const router = express.Router(); -const { getRelationships, getRelationshipsByUser, getRelationshipsByOtherUser, addRelationship, filterBySide, deleteByUser} = require('../controllers/relationshipController'); +const { getRelationships, getRelationshipsByUser, editRelationship, getRelationshipsByOtherUser, addRelationship, filterBySide, deleteByUser} = require('../controllers/relationshipController'); router.post('/', addRelationship); router.get('/:id', getRelationships); router.get('/user/:id', getRelationshipsByUser); +router.patch('/relationship/edit/:id', editRelationship); router.get('/assignedUser/:id', getRelationshipsByOtherUser); router.get('/family-side/:id', filterBySide); router.delete('/remove/:id', deleteByUser); From 749406d9a6cbbdff122264694ed4b035a37c5a97 Mon Sep 17 00:00:00 2001 From: cdl431 Date: Mon, 1 Dec 2025 14:51:10 -0600 Subject: [PATCH 06/14] Couple of little errors --- server/controllers/authController.js | 25 ++++++++++--------------- server/routes/authRoutes.js | 11 ++--------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/server/controllers/authController.js b/server/controllers/authController.js index ebea6bb..24ab396 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -1,12 +1,12 @@ -<<<<<<< Updated upstream + // authController.js - the main backend file for user registration, signin, etc const User = require('../models/userModel'); // now backed by Supabase -======= + // authController.js const bcrypt = require('bcryptjs'); const User = require('../models/userModel'); const FamilyMember = require('../models/familyMemberModel'); ->>>>>>> Stashed changes + const deleteByUser = async (req,res) => { const { id } = req.params; @@ -21,12 +21,9 @@ const deleteByUser = async (req,res) => { } catch (error){ console.error(error); -<<<<<<< Updated upstream -<<<<<<< Updated upstream res.status(500);json({error:"Error deleting user"}) -======= -<<<<<<< HEAD -======= + + res.status(500).json({ error: 'Registration failed' }); @@ -89,12 +86,12 @@ async function deleteByUser(req, res) { } catch (error) { console.error(error); ->>>>>>> Stashed changes + res.status(500); json({ error: "Error deleting user" }); -======= + res.status(500).json({error:"Error deleting user"}) ->>>>>>> 5315e049f5602a4d1eb3fed3abe518fd4b3917f5 ->>>>>>> Stashed changes + + } } @@ -136,7 +133,6 @@ const getAllUsers = async (req, res) => { } } -<<<<<<< Updated upstream module.exports = { deleteByUser, findById, findByEmail, getAllUsers }; // Add a sync endpoint: POST /api/auth/sync @@ -160,6 +156,5 @@ const syncAuthUser = async (req, res) => { }; module.exports.syncAuthUser = syncAuthUser; -======= module.exports = { register, login, editByUser, deleteByUser, findById, findByEmail, getAllUsers }; ->>>>>>> Stashed changes + diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index 0a08740..2069f90 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -2,18 +2,12 @@ const express = require('express'); const router = express.Router(); -<<<<<<< Updated upstream -<<<<<<< Updated upstream + const { register, login, deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); // Assuming you have a controller for your registration logic -======= -<<<<<<< HEAD + 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'); ->>>>>>> 5315e049f5602a4d1eb3fed3abe518fd4b3917f5 ->>>>>>> Stashed changes -======= const { register, login, editByUser, deleteByUser, findByEmail, findById, getAllUsers, } = require('../controllers/authController'); // Assuming you have a controller for your registration logic console.log('Register function:', register); @@ -21,7 +15,6 @@ console.log('Register function:', register); router.post('/register', register); router.post('/login', login); router.patch('/edit/:id', editByUser); ->>>>>>> Stashed changes router.delete('/remove/:id', deleteByUser); router.get('/user/:id', findById); router.get('/user/email/:email', findByEmail); From cd3dad8a7e56a2ef30173fa5077bf69bba6ad754 Mon Sep 17 00:00:00 2001 From: cdl431 Date: Sun, 15 Feb 2026 11:38:13 -0600 Subject: [PATCH 07/14] Update to edit function --- server/controllers/relationshipController.js | 60 ++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/server/controllers/relationshipController.js b/server/controllers/relationshipController.js index c131f9b..74f5bc6 100644 --- a/server/controllers/relationshipController.js +++ b/server/controllers/relationshipController.js @@ -170,6 +170,66 @@ const editRelationship = async (req, res) => { }; +const editRelationship = async (req, res) => { + try { + const { id } = req.params; + const { person1_id, person2_id, relationshipType, relationshipStatus, side } = req.body; + + const relationship = await Relationship.findById(id); + if (!relationship) { + return res.status(404).json({ error: "Relationship not found" }); + } + + const updatedFields = {}; + + if (person1_id) updatedFields.person1_id = person1_id; + if (person2_id) updatedFields.person2_id = person2_id; + if (relationshipType) updatedFields.relationshipType = relationshipType; + if (relationshipStatus) updatedFields.relationshipStatus = relationshipStatus; + + if (side) { + if (!["paternal", "maternal", "both"].includes(side)) { + return res.status(400).json({ + error: "Invalid side. Must be 'paternal', 'maternal', or 'both'." + }); + } + updatedFields.side = side; + } + + if (Object.keys(updatedFields).length === 0) { + return res.status(400).json({ + error: "No valid fields provided to update" + }); + } + + + if (updatedFields.person1_id || updatedFields.person2_id) { + const person1 = updatedFields.person1_id || relationship.person1_id; + const person2 = updatedFields.person2_id || relationship.person2_id; + + const duplicate = await Relationship.findExisting(person1, person2); + if (duplicate && duplicate.id != id) { + return res.status(400).json({ + error: "A relationship with these people already exists" + }); + } + } + + + const updatedRelationship = await Relationship.updateRelationship(id, updatedFields); + + res.status(200).json({ + message: "Relationship updated successfully", + relationship: updatedRelationship + }); + + } catch (error) { + console.error(error); + res.status(500).json({ error: "Error updating relationship" }); + } +}; + + const filterBySide = async (req,res) => { try{ const {id} = req.params; From aa762fb7dc6d014245c7b8175a323255f0caea36 Mon Sep 17 00:00:00 2001 From: cdl431 Date: Sun, 15 Feb 2026 11:39:05 -0600 Subject: [PATCH 08/14] Updated upstream --- server/controllers/authController.js | 4 ++++ server/routes/authRoutes.js | 3 +++ 2 files changed, 7 insertions(+) diff --git a/server/controllers/authController.js b/server/controllers/authController.js index 24ab396..f80061d 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -133,6 +133,7 @@ const getAllUsers = async (req, res) => { } } +<<<<<<< Updated upstream module.exports = { deleteByUser, findById, findByEmail, getAllUsers }; // Add a sync endpoint: POST /api/auth/sync @@ -158,3 +159,6 @@ const syncAuthUser = async (req, res) => { module.exports.syncAuthUser = syncAuthUser; module.exports = { register, login, editByUser, deleteByUser, findById, findByEmail, getAllUsers }; +======= +module.exports = { register, login, editByUser, deleteByUser, findById, findByEmail, getAllUsers }; +>>>>>>> Stashed changes diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index 2069f90..8b214a1 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -2,12 +2,15 @@ const express = require('express'); const router = express.Router(); +<<<<<<< Updated upstream const { register, login, deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); // Assuming you have a controller for your registration logic 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'); +======= +>>>>>>> Stashed changes const { register, login, editByUser, deleteByUser, findByEmail, findById, getAllUsers, } = require('../controllers/authController'); // Assuming you have a controller for your registration logic console.log('Register function:', register); From b9d3e740c5969a5fdb94a61c33486c9446fc0695 Mon Sep 17 00:00:00 2001 From: cdl431 Date: Sun, 15 Feb 2026 12:42:08 -0600 Subject: [PATCH 09/14] Added an update function --- server/controllers/relationshipController.js | 60 -------------------- server/models/relationshipModel.js | 29 ++++++++++ 2 files changed, 29 insertions(+), 60 deletions(-) diff --git a/server/controllers/relationshipController.js b/server/controllers/relationshipController.js index 74f5bc6..c131f9b 100644 --- a/server/controllers/relationshipController.js +++ b/server/controllers/relationshipController.js @@ -170,66 +170,6 @@ const editRelationship = async (req, res) => { }; -const editRelationship = async (req, res) => { - try { - const { id } = req.params; - const { person1_id, person2_id, relationshipType, relationshipStatus, side } = req.body; - - const relationship = await Relationship.findById(id); - if (!relationship) { - return res.status(404).json({ error: "Relationship not found" }); - } - - const updatedFields = {}; - - if (person1_id) updatedFields.person1_id = person1_id; - if (person2_id) updatedFields.person2_id = person2_id; - if (relationshipType) updatedFields.relationshipType = relationshipType; - if (relationshipStatus) updatedFields.relationshipStatus = relationshipStatus; - - if (side) { - if (!["paternal", "maternal", "both"].includes(side)) { - return res.status(400).json({ - error: "Invalid side. Must be 'paternal', 'maternal', or 'both'." - }); - } - updatedFields.side = side; - } - - if (Object.keys(updatedFields).length === 0) { - return res.status(400).json({ - error: "No valid fields provided to update" - }); - } - - - if (updatedFields.person1_id || updatedFields.person2_id) { - const person1 = updatedFields.person1_id || relationship.person1_id; - const person2 = updatedFields.person2_id || relationship.person2_id; - - const duplicate = await Relationship.findExisting(person1, person2); - if (duplicate && duplicate.id != id) { - return res.status(400).json({ - error: "A relationship with these people already exists" - }); - } - } - - - const updatedRelationship = await Relationship.updateRelationship(id, updatedFields); - - res.status(200).json({ - message: "Relationship updated successfully", - relationship: updatedRelationship - }); - - } catch (error) { - console.error(error); - res.status(500).json({ error: "Error updating relationship" }); - } -}; - - const filterBySide = async (req,res) => { try{ const {id} = req.params; diff --git a/server/models/relationshipModel.js b/server/models/relationshipModel.js index 5f03682..e30c3ca 100644 --- a/server/models/relationshipModel.js +++ b/server/models/relationshipModel.js @@ -14,6 +14,7 @@ const Relationships = { 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 @@ -24,6 +25,34 @@ const Relationships = { if (error) throw error; return inserted; }, + + updateRelationship: async (person1Id, person2Id, updates) => { + // Map camelCase fields to database column names + const mappedUpdates = { + person1_id: updates.person1_id || updates.person1Id, + person2_id: updates.person2_id || updates.person2Id, + relationshiptype: updates.relationshipType || updates.relationshiptype, + relationshipstatus: updates.relationshipStatus || updates.relationshipstatus, + side: updates.side, + userid: updates.userId || updates.userid, + }; + + // Remove undefined values so we only update what is provided + Object.keys(mappedUpdates).forEach( + key => mappedUpdates[key] === undefined && delete mappedUpdates[key] + ); + + const { data, error } = await supabase + .from('relationships') + .update(mappedUpdates) + .eq('person1_id', person1Id) + .eq('person2_id', person2Id) + .select('*') + .single(); + + if (error) throw error; + return data; + }, getRelationships: async (personId) => { // person1_id = personId OR person2_id = personId From 8d60d0d2163455f3e226210d7173c7990cf4b975 Mon Sep 17 00:00:00 2001 From: cdl431 Date: Mon, 16 Feb 2026 20:26:43 -0600 Subject: [PATCH 10/14] Added an EditFamilyMember Component --- .../EditFamilyMember/EditFamilyMember.js | 211 ++++++++++++++++++ .../src/components/EditFamilyMember/popup.css | 5 + .../src/components/EditFamilyMember/styles.js | 99 ++++++++ 3 files changed, 315 insertions(+) create mode 100644 client/src/components/EditFamilyMember/EditFamilyMember.js create mode 100644 client/src/components/EditFamilyMember/popup.css create mode 100644 client/src/components/EditFamilyMember/styles.js diff --git a/client/src/components/EditFamilyMember/EditFamilyMember.js b/client/src/components/EditFamilyMember/EditFamilyMember.js new file mode 100644 index 0000000..1674cff --- /dev/null +++ b/client/src/components/EditFamilyMember/EditFamilyMember.js @@ -0,0 +1,211 @@ +import React, { useEffect, useMemo } from 'react'; +import Popup from 'reactjs-popup'; +import 'reactjs-popup/dist/index.css'; +import { useForm } from 'react-hook-form'; +import * as styles from './styles'; +import './popup.css'; +import { ReactComponent as CloseIcon } from '../../assets/exit.svg'; +import { useCurrentUser } from '../../CurrentUserProvider'; + +function EditFamilyMemberPopup({ trigger, memberData, relationshipData }) { + const { currentUserID, currentAccountID } = useCurrentUser(); + + // relationships requiring maternal/paternal + const matPat = useMemo(() => [ + "parent", "cousin", "aunt", "uncle", "grandparent", "niece", "nephew" + ], []); + + // form + const { + register, + handleSubmit, + reset, + watch + } = useForm({ + defaultValues: { + firstName: '', + lastName: '', + relationship: '', + matPat: '', + location: '', + birthday: '', + deathDate: '' + } + }); + + // watch relationship to toggle maternal/paternal + const selectedRelationship = watch("relationship"); + + // populate form when modal opens + useEffect(() => { + if (memberData) { + reset({ + firstName: memberData.firstName || '', + lastName: memberData.lastName || '', + relationship: relationshipData?.relationshipType || '', + matPat: relationshipData?.side || '', + location: memberData.location || '', + birthday: memberData.birthDate || '', + deathDate: memberData.deathDate || '' + }); + } + }, [memberData, relationshipData, reset]); + + // close reset + const closeModal = () => { + reset(); + }; + + // submit handler + const onSubmitEdit = async (data) => { + try { + // 1️⃣ update family member + await fetch(`http://localhost:5000/api/family-members/${memberData.id}`, { + method: 'PUT', + 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, + }) + }); + + // 2️⃣ update relationship + await fetch(`http://localhost:5000/api/relationships/${relationshipData.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + person1_id: currentUserID, + person2_id: memberData.id, + relationshipType: data.relationship, + relationshipStatus: "active", + side: data.matPat || null, + userId: currentAccountID + }) + }); + + // refresh page + window.location.reload(); + + } catch (err) { + console.error("Edit failed:", err); + } + }; + + return ( + + {close => ( +
+ + {/* CLOSE BUTTON */} +
+ +
+ +
+ +
+

Edit Family Member

+
+ +
    + + {/* FIRST NAME */} +
  • + +
  • + + {/* LAST NAME */} +
  • + +
  • + + {/* RELATIONSHIP */} +
  • + +
  • + + {/* MAT/PAT */} +
    + Maternal + Paternal +
    + + {/* LOCATION */} +
  • + +
  • + + {/* BIRTH DATE */} +
  • + +
  • + + {/* DEATH DATE */} +
  • + +
  • + +
+ + {/* BUTTONS */} +
+ + +
+ +
+
+ )} +
+ ); +} + +export default EditFamilyMemberPopup; diff --git a/client/src/components/EditFamilyMember/popup.css b/client/src/components/EditFamilyMember/popup.css new file mode 100644 index 0000000..c230a6a --- /dev/null +++ b/client/src/components/EditFamilyMember/popup.css @@ -0,0 +1,5 @@ +.popup-content { + width: auto; + border-radius: 30px; + min-width: 0px; +} \ No newline at end of file diff --git a/client/src/components/EditFamilyMember/styles.js b/client/src/components/EditFamilyMember/styles.js new file mode 100644 index 0000000..80059fe --- /dev/null +++ b/client/src/components/EditFamilyMember/styles.js @@ -0,0 +1,99 @@ +export const DefaultStyle = { + fontFamily: 'Alata', +}; + +export const FieldStyle = { + borderRadius: '5px', + border: '1px solid #000000', + marginLeft: '10px' +}; + +export const ListStyle = { + listStyleType: 'none', + fontFamily: 'Alata', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-end', + marginRight: '15%' +}; + +export const ButtonDivStyle = { + fontFamily: 'Alata', + display: 'flex', + justifyContent: 'center', +} + +export const ButtonStyle = { + fontFamily: 'Alata', + backgroundColor: '#3a5a40', + color: 'white', + borderRadius: '20px', + border: 'none', + padding: '10px 30px', + margin: '10px', + cursor: 'pointer', +} + +export const GrayButtonStyle = { + fontFamily: 'Alata', + backgroundColor: '#D9D9D9', + color: 'black', + borderRadius: '20px', + border: 'none', + padding: '10px 20px', + margin: '10px', + cursor: 'pointer', + display: 'flex', + flexDirection: 'row', + boxShadow: 'gray 0px 10px 10px -8px', +} + +export const FormStyle = { + padding: '2vw', + paddingTop: '0px', + minWidth: '360px', +} + +export const ItemStyle = { + margin: '10px 0px' +} + +export const DateFieldStyle = { + borderRadius: '5px', + border: '1px solid #000000', + marginLeft: '10px', + width: '147px', + fontFamily: 'Alata' +}; + + +export const MainContainerStyle = { + display: 'flex', + flexDirection: 'column', + padding: '2vw', + paddingTop: '0px', + alignItems: 'center', + minWidth: '360px', + minHeight: '150px', + justifyContent: 'space-between', +} + +export const AddOptionsStyle = { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + width: '90%', + padding: '10px', + fontFamily: 'Alata', + // border: '1px solid gray', + // borderRadius: '5px', + marginTop: '10px', + height: '200px', + overflow: 'auto' +} + +export const ListingStyle = { + padding: '10px', + border: '1px solid gray', + width: '90%' +} \ No newline at end of file From be65394fa64d8f6f381ac307d0e1759839b16de9 Mon Sep 17 00:00:00 2001 From: cdl431 Date: Sun, 22 Feb 2026 19:03:22 -0600 Subject: [PATCH 11/14] Fixing mistakes --- server/controllers/authController.js | 29 +--------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/server/controllers/authController.js b/server/controllers/authController.js index f80061d..ca40fbb 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -133,32 +133,5 @@ const getAllUsers = async (req, res) => { } } -<<<<<<< Updated upstream -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; -module.exports = { register, login, editByUser, deleteByUser, findById, findByEmail, getAllUsers }; -======= -module.exports = { register, login, editByUser, deleteByUser, findById, findByEmail, getAllUsers }; ->>>>>>> Stashed changes +module.exports = { register, login, editByUser, deleteByUser, findById, findByEmail, getAllUsers }; \ No newline at end of file From 9472af1c94e9324824a88360b64cdc7640ceb5bc Mon Sep 17 00:00:00 2001 From: cdl431 Date: Sun, 22 Mar 2026 11:16:57 -0500 Subject: [PATCH 12/14] simple changes --- server/controllers/authController.js | 2 +- server/controllers/relationshipController.js | 60 ++++++++++++++++++++ server/routes/authRoutes.js | 3 - 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/server/controllers/authController.js b/server/controllers/authController.js index ca40fbb..14e0a9c 100644 --- a/server/controllers/authController.js +++ b/server/controllers/authController.js @@ -134,4 +134,4 @@ const getAllUsers = async (req, res) => { } -module.exports = { register, login, editByUser, deleteByUser, findById, findByEmail, getAllUsers }; \ No newline at end of file +module.exports = { register, login, editByUser, deleteByUser, findById, findByEmail, getAllUsers }; diff --git a/server/controllers/relationshipController.js b/server/controllers/relationshipController.js index c131f9b..74f5bc6 100644 --- a/server/controllers/relationshipController.js +++ b/server/controllers/relationshipController.js @@ -170,6 +170,66 @@ const editRelationship = async (req, res) => { }; +const editRelationship = async (req, res) => { + try { + const { id } = req.params; + const { person1_id, person2_id, relationshipType, relationshipStatus, side } = req.body; + + const relationship = await Relationship.findById(id); + if (!relationship) { + return res.status(404).json({ error: "Relationship not found" }); + } + + const updatedFields = {}; + + if (person1_id) updatedFields.person1_id = person1_id; + if (person2_id) updatedFields.person2_id = person2_id; + if (relationshipType) updatedFields.relationshipType = relationshipType; + if (relationshipStatus) updatedFields.relationshipStatus = relationshipStatus; + + if (side) { + if (!["paternal", "maternal", "both"].includes(side)) { + return res.status(400).json({ + error: "Invalid side. Must be 'paternal', 'maternal', or 'both'." + }); + } + updatedFields.side = side; + } + + if (Object.keys(updatedFields).length === 0) { + return res.status(400).json({ + error: "No valid fields provided to update" + }); + } + + + if (updatedFields.person1_id || updatedFields.person2_id) { + const person1 = updatedFields.person1_id || relationship.person1_id; + const person2 = updatedFields.person2_id || relationship.person2_id; + + const duplicate = await Relationship.findExisting(person1, person2); + if (duplicate && duplicate.id != id) { + return res.status(400).json({ + error: "A relationship with these people already exists" + }); + } + } + + + const updatedRelationship = await Relationship.updateRelationship(id, updatedFields); + + res.status(200).json({ + message: "Relationship updated successfully", + relationship: updatedRelationship + }); + + } catch (error) { + console.error(error); + res.status(500).json({ error: "Error updating relationship" }); + } +}; + + const filterBySide = async (req,res) => { try{ const {id} = req.params; diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js index 8b214a1..2069f90 100644 --- a/server/routes/authRoutes.js +++ b/server/routes/authRoutes.js @@ -2,15 +2,12 @@ const express = require('express'); const router = express.Router(); -<<<<<<< Updated upstream const { register, login, deleteByUser, findByEmail, findById, getAllUsers } = require('../controllers/authController'); // Assuming you have a controller for your registration logic 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'); -======= ->>>>>>> Stashed changes const { register, login, editByUser, deleteByUser, findByEmail, findById, getAllUsers, } = require('../controllers/authController'); // Assuming you have a controller for your registration logic console.log('Register function:', register); From 822ce86c3857e10bb66f7bc82bd6cd506d950dc9 Mon Sep 17 00:00:00 2001 From: cdl431 Date: Sun, 22 Mar 2026 11:49:23 -0500 Subject: [PATCH 13/14] Started Sync Contacts --- .../Sync Contacts/Sync_Contacts_Button.js | 68 +++++++++++++++++++ server/controllers/relationshipController.js | 60 ---------------- server/routes/contacts.js | 28 ++++++++ 3 files changed, 96 insertions(+), 60 deletions(-) create mode 100644 client/src/components/Sync Contacts/Sync_Contacts_Button.js create mode 100644 server/routes/contacts.js diff --git a/client/src/components/Sync Contacts/Sync_Contacts_Button.js b/client/src/components/Sync Contacts/Sync_Contacts_Button.js new file mode 100644 index 0000000..01717e9 --- /dev/null +++ b/client/src/components/Sync Contacts/Sync_Contacts_Button.js @@ -0,0 +1,68 @@ +const handleSyncContacts = async () => { + try { + // 1. Ask for permission + const allow = window.confirm("Allow access to contacts?"); + if (!allow) { + alert("Permission denied. No contacts synced."); + return; + } + + // 2. Mock contacts (since browser can't access real ones) + const syncedContacts = [ + { + firstName: "John", + lastName: "Doe", + relationship: "sibling", + location: "New York", + birthday: "1995-05-10" + }, + { + firstName: "Jane", + lastName: "Smith", + relationship: "cousin", + location: "California", + birthday: "1998-08-22" + } + ]; + + console.log("Synced Contacts:", syncedContacts); + + // 3. OPTIONAL: send to backend + try { + await fetch("http://localhost:5000/api/contacts", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(syncedContacts) + }); + } catch (err) { + console.warn("Backend not connected, continuing..."); + } + + // 4. Auto-fill the first contact into your manual form + const firstContact = syncedContacts[0]; + + if (firstContact) { + // fill your react-hook-form fields + document.querySelector('input[name="firstName"]').value = firstContact.firstName; + document.querySelector('input[name="lastName"]').value = firstContact.lastName; + + const relationshipSelect = document.querySelector('select[name="relationship"]'); + if (relationshipSelect) relationshipSelect.value = firstContact.relationship; + + const locationInput = document.querySelector('input[name="location"]'); + if (locationInput) locationInput.value = firstContact.location; + + const birthdayInput = document.querySelector('input[name="birthday"]'); + if (birthdayInput) birthdayInput.value = firstContact.birthday; + } + + // 5. Feedback to user + alert("Contacts synced and form auto-filled!"); + + } catch (error) { + console.error("Error syncing contacts:", error); + alert("Something went wrong while syncing contacts."); + } +}; \ No newline at end of file diff --git a/server/controllers/relationshipController.js b/server/controllers/relationshipController.js index 74f5bc6..c131f9b 100644 --- a/server/controllers/relationshipController.js +++ b/server/controllers/relationshipController.js @@ -170,66 +170,6 @@ const editRelationship = async (req, res) => { }; -const editRelationship = async (req, res) => { - try { - const { id } = req.params; - const { person1_id, person2_id, relationshipType, relationshipStatus, side } = req.body; - - const relationship = await Relationship.findById(id); - if (!relationship) { - return res.status(404).json({ error: "Relationship not found" }); - } - - const updatedFields = {}; - - if (person1_id) updatedFields.person1_id = person1_id; - if (person2_id) updatedFields.person2_id = person2_id; - if (relationshipType) updatedFields.relationshipType = relationshipType; - if (relationshipStatus) updatedFields.relationshipStatus = relationshipStatus; - - if (side) { - if (!["paternal", "maternal", "both"].includes(side)) { - return res.status(400).json({ - error: "Invalid side. Must be 'paternal', 'maternal', or 'both'." - }); - } - updatedFields.side = side; - } - - if (Object.keys(updatedFields).length === 0) { - return res.status(400).json({ - error: "No valid fields provided to update" - }); - } - - - if (updatedFields.person1_id || updatedFields.person2_id) { - const person1 = updatedFields.person1_id || relationship.person1_id; - const person2 = updatedFields.person2_id || relationship.person2_id; - - const duplicate = await Relationship.findExisting(person1, person2); - if (duplicate && duplicate.id != id) { - return res.status(400).json({ - error: "A relationship with these people already exists" - }); - } - } - - - const updatedRelationship = await Relationship.updateRelationship(id, updatedFields); - - res.status(200).json({ - message: "Relationship updated successfully", - relationship: updatedRelationship - }); - - } catch (error) { - console.error(error); - res.status(500).json({ error: "Error updating relationship" }); - } -}; - - const filterBySide = async (req,res) => { try{ const {id} = req.params; diff --git a/server/routes/contacts.js b/server/routes/contacts.js new file mode 100644 index 0000000..b176ff0 --- /dev/null +++ b/server/routes/contacts.js @@ -0,0 +1,28 @@ +const express = require("express"); +const router = express.Router(); + +// temporary storage (replace with DB later) +let contacts = []; + +// POST: save synced contacts +router.post("/", (req, res) => { + const newContacts = req.body; + + if (!newContacts || newContacts.length === 0) { + return res.status(400).json({ message: "No contacts provided" }); + } + + contacts = newContacts; + + res.status(200).json({ + message: "Contacts synced successfully", + data: contacts + }); +}); + +// GET: retrieve contacts +router.get("/", (req, res) => { + res.status(200).json(contacts); +}); + +module.exports = router; \ No newline at end of file From 528b2f5cfbee3bc18cd21fcfd4abfde5cb2f55e1 Mon Sep 17 00:00:00 2001 From: cdl431 Date: Sun, 3 May 2026 13:48:31 -0500 Subject: [PATCH 14/14] Update supabaseClient.js --- client/src/utils/supabaseClient.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/utils/supabaseClient.js b/client/src/utils/supabaseClient.js index aafdeac..09f1c10 100644 --- a/client/src/utils/supabaseClient.js +++ b/client/src/utils/supabaseClient.js @@ -1,6 +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; +const supabaseUrl = process.env.REACT_APP_SUPABASE_URL || 'https://bobchgtijgkxhaxfnvci.supabase.co/'; +const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJvYmNoZ3RpamdreGhheGZudmNpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk4NDk2OTMsImV4cCI6MjA3NTQyNTY5M30.3YH0lpoRH0lqKrj2TiOJSOPKhXDO7ULw9lPZeBkU3Bo'; export const supabase = createClient(supabaseUrl, supabaseAnonKey);