diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..438657a9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.env + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/Final recording.gif b/Final recording.gif new file mode 100644 index 000000000..8ec9f9dff Binary files /dev/null and b/Final recording.gif differ diff --git a/Final recording.webm b/Final recording.webm new file mode 100644 index 000000000..ea72a0d53 Binary files /dev/null and b/Final recording.webm differ diff --git a/README.md b/README.md index 0e1211217..e0b56e222 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# [your app name here] +# [BudgetBuddy] CodePath WEB103 Final Project -Designed and developed by: [your names here] +Designed and developed by: [Ayushi Porwal] 🔗 Link to deployed app: @@ -10,40 +10,144 @@ Designed and developed by: [your names here] ### Description and Purpose -[text goes here] +BudgetBuddy helps users manage personal finances by tracking income, expenses, and savings goals. Users can plan budgets, categorize expenses, and visualize financial reports, ensuring better money management and meeting financial goals. ### Inspiration -[text goes here] +Inspired by the challenges of maintaining a personal budget, we created this app to simplify tracking finances and to promote responsible spending habits. ## Tech Stack -Frontend: +Frontend: React, Bootstrap -Backend: +Backend: Express.js, PostgreSQL, Node.js ## Features -### [Name of Feature 1] +### [User Registration & Login System] ✅ -[short description goes here] +[Secure authentication system that allows users to register accounts and log in to manage personal budgets.] + +[Click Me](https://i.imgur.com/UVGqAxr.gif) + +### [Income and Expense Tracking] ✅ + +[Feature enabling users to add, edit, and delete income and expenses, with real-time budget updates.] + +[Click Me](https://i.imgur.com/UVGqAxr.gif) + +### [Expense Categorization] ✅ + +[Categorize expenses into predefined or custom categories for better tracking and analysis.] + +[gif goes here] + +### [ADDITIONAL FEATURES GO HERE - ADD ALL FEATURES HERE IN THE FORMAT ABOVE; you will check these off and add gifs as you complete them] + +### [Savings Goals] ✅ + +[Allows users to set and monitor savings goals with progress tracking.] + +[Click Me](https://i.imgur.com/1dnVZTT.gif) + +### [Budget Dashboard] + +[A visual dashboard showing total income, expenses, remaining budget, and categorized breakdowns.] [gif goes here] -### [Name of Feature 2] +### [Expense Filtering and Sorting] ✅ + +[Filter and sort expenses by category, date, or amount for detailed insights.] + +[Click Me](https://i.imgur.com/1dnVZTT.gif) + +### [Monthly/Yearly Financial Reports] -[short description goes here] +[Automatically generated reports with charts showing spending trends over time.] [gif goes here] -### [Name of Feature 3] +### [Cloud Backup and Data Restore] -[short description goes here] +[Feature for securely backing up financial data to the cloud and restoring it when needed.] [gif goes here] -### [ADDITIONAL FEATURES GO HERE - ADD ALL FEATURES HERE IN THE FORMAT ABOVE; you will check these off and add gifs as you complete them] + +## Technical Feature List + +### [User Registration & Authentication] ✅ + +[Secure account creation and login system.] +[Password hashing and validation for security.] + +[Click Me](https://i.imgur.com/1dnVZTT.gif) + + +### [Database Design] ✅ + +[PostgreSQL database schema for users, transactions, and categories.] +[Relationships: one-to-many (users to transactions) and many-to-many (categories to transactions).] + +[Click Me](https://i.imgur.com/1dnVZTT.gif) + +### [API Development] ✅ + +[RESTful API with endpoints for user management, income and expense tracking.] +[Implement GET, POST, PATCH, and DELETE methods with proper naming conventions.] + +[Click Me](https://i.imgur.com/1dnVZTT.gif) + +### [Frontend Development] ✅ + +[React-based user interface with components for dashboards and forms.] +[Dynamic routes using React Router for navigation.] + +[Click Me](https://i.imgur.com/1dnVZTT.gif) + + +### [Budget Dashboard] ✅ + +[Visual representation of total income, expenses, and remaining budget.] +[Data visualization libraries (e.g., Chart.js) for displaying financial trends.] + +[Click Me](https://i.imgur.com/1dnVZTT.gif) + +### [Alerts & Notifications] + +[Implement user notifications for budget limits via email or in-app alerts.] + +[gif goes here] + +### [Expense Management] ✅ + +[Functionality for adding, editing, categorizing, and deleting expenses.] +[Sorting and filtering options for detailed analysis.] + +[Click Me](https://i.imgur.com/1dnVZTT.gif) + + +### [Reports Generation] + +[Monthly and yearly reports with visual charts showing trends.] + +[gif goes here] + +### [Cloud Backup] + +[Integration with cloud services for secure data storage and backup.] + +[gif goes here] + +### [Testing & Deployment] + +Use of unit and integration tests for backend and frontend. +Deployment on Railway with CI/CD pipeline for automatic updates. + ## Installation Instructions -[instructions go here] +[Clone the repository: git clone https://github.com/ayushiporwal13/web103_finalproject.git] +[Install dependencies: npm install] +[Run the app: npm start] diff --git a/README.md - web103_finalproject - Visual Studio Code 2024-10-23 21-07-23.gif b/README.md - web103_finalproject - Visual Studio Code 2024-10-23 21-07-23.gif new file mode 100644 index 000000000..9f38ec214 Binary files /dev/null and b/README.md - web103_finalproject - Visual Studio Code 2024-10-23 21-07-23.gif differ diff --git a/client/README.md b/client/README.md new file mode 100644 index 000000000..f768e33fc --- /dev/null +++ b/client/README.md @@ -0,0 +1,8 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 000000000..238d2e4e6 --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,38 @@ +import js from '@eslint/js' +import globals from 'globals' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + settings: { react: { version: '18.3' } }, + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + ...reactHooks.configs.recommended.rules, + 'react/jsx-no-target-blank': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/client/index.html b/client/index.html new file mode 100644 index 000000000..4593ffb3a --- /dev/null +++ b/client/index.html @@ -0,0 +1,20 @@ + + + +
+ + + + + +User | +Category | +Amount | +Description | +Date | +Actions | +|
---|---|---|---|---|---|---|
{entry.user_id} | ++ { + categories.find( + (category) => category.id === entry.category_id + )?.name + } + | +{entry.amount} | +{entry.description} | ++ {new Date(entry.date).toLocaleDateString("en-US", { + timeZone: "UTC", + })} + | +||
{entry.user_id} | ++ { + categories.find( + (category) => category.id === entry.category_id + )?.name + } + | +{entry.amount} | +{entry.description} | +{new Date(entry.date).toLocaleDateString()} | + {/*Edit | */} ++ + + | +
User | +Source | +Amount | +Date | +Actions | +
---|---|---|---|---|
{entry.user_id} | +{entry.source} | +{entry.amount} | +{new Date(entry.date).toLocaleDateString()} | ++ + | +
{entry.user_id} | +{entry.source} | +{entry.amount} | +{new Date(entry.date).toLocaleDateString()} | ++ + + + | +
Goal Name | +Target Amount | +Current Amount | +Deadline | +Actions | +
---|---|---|---|---|
{goal.goal_name} | +${goal.target_amount} | +${goal.current_amount} | +{new Date(goal.deadline).toLocaleDateString()} | ++ + + | +
{e[n]=Y6(t[n])}),e}function ti(t,e,n={clone:!0}){const r=n.clone?{...t}:t;return wo(t)&&wo(e)&&Object.keys(e).forEach(i=>{wo(e[i])&&Object.prototype.hasOwnProperty.call(t,i)&&wo(t[i])?r[i]=ti(t[i],e[i],n):n.clone?r[i]=wo(e[i])?Y6(e[i]):e[i]:r[i]=e[i]}),r}const v7=t=>{const e=Object.keys(t).map(n=>({key:n,val:t[n]}))||[];return e.sort((n,r)=>n.val-r.val),e.reduce((n,r)=>({...n,[r.key]:r.val}),{})};function b7(t){const{values:e={xs:0,sm:600,md:900,lg:1200,xl:1536},unit:n="px",step:r=5,...i}=t,o=v7(e),s=Object.keys(o);function a(p){return`@media (min-width:${typeof e[p]=="number"?e[p]:p}${n})`}function l(p){return`@media (max-width:${(typeof e[p]=="number"?e[p]:p)-r/100}${n})`}function c(p,d){const y=s.indexOf(d);return`@media (min-width:${typeof e[p]=="number"?e[p]:p}${n}) and (max-width:${(y!==-1&&typeof e[s[y]]=="number"?e[s[y]]:d)-r/100}${n})`}function u(p){return s.indexOf(p)+1 {if(!r._listeners)return;let o=r._listeners.length;for(;o-- >0;)r._listeners[o](i);r._listeners=null}),this.promise.then=i=>{let o;const s=new Promise(a=>{r.subscribe(a),o=a}).then(i);return s.cancel=function(){r.unsubscribe(o)},s},e(function(o,s,a){r.reason||(r.reason=new uu(o,s,a),n(r.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){if(this.reason){e(this.reason);return}this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;const n=this._listeners.indexOf(e);n!==-1&&this._listeners.splice(n,1)}toAbortSignal(){const e=new AbortController,n=r=>{e.abort(r)};return this.subscribe(n),e.signal.unsubscribe=()=>this.unsubscribe(n),e.signal}static source(){let e;return{token:new Wv(function(i){e=i}),cancel:e}}}function Y9(t){return function(n){return t.apply(null,n)}}function X9(t){return yt.isObject(t)&&t.isAxiosError===!0}const O5={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(O5).forEach(([t,e])=>{O5[e]=t});function FA(t){const e=new nl(t),n=dA(nl.prototype.request,e);return yt.extend(n,nl.prototype,e,{allOwnKeys:!0}),yt.extend(n,e,null,{allOwnKeys:!0}),n.create=function(i){return FA(dl(t,i))},n}const me=FA(zh);me.Axios=nl;me.CanceledError=uu;me.CancelToken=Wv;me.isCancel=NA;me.VERSION=IA;me.toFormData=n1;me.AxiosError=le;me.Cancel=me.CanceledError;me.all=function(e){return Promise.all(e)};me.spread=Y9;me.isAxiosError=X9;me.mergeConfig=dl;me.AxiosHeaders=jr;me.formToJSON=t=>LA(yt.isHTMLForm(t)?new FormData(t):t);me.getAdapter=RA.getAdapter;me.HttpStatusCode=O5;me.default=me;const $h="http://localhost:3000/api/expense",K9=async t=>(await me.get(`${$h}/${t}`)).data,J9=async t=>(await me.get(`${$h}/${t}`)).data,Q9=async t=>{const e=await fetch(`${$h}/add`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),n=await e.json();if(e.status>=400)throw new Error(n.errors);return n},Z9=async(t,e)=>(await me.put(`${$h}/update/${t}`,e)).data,tB=async t=>(await me.delete(`${$h}/delete/${t}`)).data,Yp={getExpenses:K9,getExpensesById:J9,addExpenses:Q9,updateExpenses:Z9,deleteExpenses:tB},Uh="http://localhost:3000/api/income",eB=async t=>{try{return(await me.get(`${Uh}/${t}`)).data}catch(e){throw console.error("Error fetching income:",e),e}},nB=async t=>{try{return(await me.get(`${Uh}/${t}`)).data}catch(e){throw console.error(`Error fetching income with id ${t}:`,e),e}},rB=async t=>{try{return(await me.post(`${Uh}/add`,t)).data}catch(e){throw console.error("Error adding income:",e),e}},iB=async(t,e)=>{try{return(await me.put(`${Uh}/update/${t}`,e)).data}catch(n){throw console.error(`Error updating income with id ${t}:`,n),n}},oB=async t=>{try{return(await me.delete(`${Uh}/delete/${t}`)).data}catch(e){throw console.error(`Error deleting income with id ${t}:`,e),e}},Xp={getIncome:eB,getIncomeById:nB,addIncome:rB,updateIncome:iB,deleteIncome:oB},DA=t=>{const{show:e,setShow:n,entry:r,type:i}=t,[o,s]=H.useState({...r}),a=u=>{u.preventDefault(),s({...o,[u.target.name]:u.target.value})},l=async u=>{u.preventDefault();try{o.date=new Date(o.date).toISOString(),i==="expense"?await Yp.updateExpenses(r.id,o):i==="income"&&await Xp.updateIncome(r.id,o),s({...o,isSubmitting:!0}),s(i==="expense"?{user_id:r.user_id,amount:"",description:"",date:"",category_id:""}:{user_id:r.user_id,amount:"",source:"",date:""}),n(!1),t.onUpdate()}catch(h){console.error("Error updating data:",h)}},c=()=>{n(!1)};return B.jsxs(Zu,{show:e,onHide:c,children:[B.jsx(Zu.Header,{closeButton:!0,children:B.jsx(Zu.Title,{children:"Update Entry"})}),B.jsx(Zu.Body,{children:B.jsxs(Tn,{children:[i==="expense"?B.jsxs(B.Fragment,{children:[B.jsxs(Tn.Group,{controlId:"formCategory",children:[B.jsx(Tn.Label,{children:"Category"}),B.jsx(Tn.Control,{as:"select",name:"category_id",value:o.category_id,onChange:u=>a(u),children:t.categories.map(u=>B.jsx("option",{value:u.id,children:u.name},u.id))})]}),B.jsxs(Tn.Group,{controlId:"formAmount",children:[B.jsx(Tn.Label,{children:"Amount"}),B.jsx(Tn.Control,{type:"number",name:"amount",value:o.amount,onChange:u=>a(u)})]}),B.jsxs(Tn.Group,{controlId:"formDescription",children:[B.jsx(Tn.Label,{children:"Description"}),B.jsx(Tn.Control,{type:"text",name:"description",value:o.description,onChange:u=>a(u)})]})]}):B.jsxs(B.Fragment,{children:[B.jsxs(Tn.Group,{controlId:"formSource",children:[B.jsx(Tn.Label,{children:"Source"}),B.jsx(Tn.Control,{type:"text",name:"source",value:o.source,onChange:u=>a(u)})]}),B.jsxs(Tn.Group,{controlId:"formAmount",children:[B.jsx(Tn.Label,{children:"Amount"}),B.jsx(Tn.Control,{type:"number",name:"amount",value:o.amount,onChange:u=>a(u)})]})]}),B.jsxs(Tn.Group,{controlId:"formDate",children:[B.jsx(Tn.Label,{children:"Date"}),B.jsx(Tn.Control,{type:"date",name:"date",value:new Date(o.date).toISOString().split("T")[0],onChange:u=>a(u)})]})]})}),B.jsxs(Zu.Footer,{children:[B.jsx(C5,{variant:"secondary",onClick:c,children:"Close"}),B.jsx(C5,{variant:"primary",onClick:l,children:"Save Changes"})]})]})},sB=()=>{const t=Number(Sg().user_id),[e,n]=H.useState([]),[r,i]=H.useState([]),[o,s]=H.useState({user_id:t,type:"income",amount:"",source:"",date:"",isSubmitting:!1}),[a,l]=H.useState({}),[c,u]=H.useState(!1);H.useEffect(()=>{(async()=>{try{const m=await Xp.getIncome(t);n(m.data)}catch(m){console.error("Error fetching data:",m)}})()},[e,t]);const h=_=>{u(!0),l(_)},p=async _=>{try{await Xp.deleteIncome(_),n(e.filter(m=>m.id!==_))}catch(m){console.error("Error deleting data:",m)}},d=async _=>{try{_.user_id=t,_.date=new Date(_.date).toISOString();const m=await Xp.addIncome(_);n([...e,m.data])}catch(m){console.error("Error adding data:",m)}},y=async _=>{_.preventDefault(),s({...o,isSubmitting:!0}),await d(o),s({type:"income",amount:"",source:"",date:""})},b=_=>{let{name:m,value:w}=_.target;s({...o,[m]:w})};return B.jsx(B.Fragment,{children:isNaN(t)?B.jsx("div",{children:B.jsx("h1",{children:"Please Login to access this page"})}):B.jsxs("div",{children:[B.jsx("h1",{children:"Track Your Incomes"}),B.jsxs("form",{onSubmit:y,children:[B.jsx("div",{children:B.jsxs("label",{children:[B.jsx("b",{children:"Source: "}),B.jsx("input",{type:"text",name:"source",value:o.source,onChange:b,required:!0,disabled:o.isSubmitting})]})}),B.jsx("div",{children:B.jsxs("label",{children:[B.jsx("b",{children:"Amount: "}),B.jsx("input",{type:"number",name:"amount",value:o.amount,onChange:b,required:!0,disabled:o.isSubmitting})]})}),B.jsx("div",{children:B.jsxs("label",{children:[B.jsx("b",{children:"Date: "}),B.jsx("input",{type:"date",name:"date",value:o.date,onChange:b,required:!0,disabled:o.isSubmitting})]})}),B.jsx("button",{type:"submit",className:"add-entry",disabled:o.isSubmitting,children:o.isSubmitting?"Submitting...":"Add Entry"})]}),B.jsx("h2",{children:"Tracked Entries"}),B.jsxs("table",{align:"center",style:{borderSpacing:"15px"},children:[B.jsx("thead",{children:B.jsxs("tr",{children:[B.jsx("th",{children:"User"}),B.jsx("th",{children:"Source"}),B.jsx("th",{children:"Amount"}),B.jsx("th",{children:"Date"}),B.jsx("th",{children:"Actions"})]})}),B.jsx("tbody",{children:r.length!==0?r.map((_,m)=>B.jsxs("tr",{children:[B.jsx("td",{children:_.user_id}),B.jsx("td",{children:_.source}),B.jsx("td",{children:_.amount}),B.jsx("td",{children:new Date(_.date).toLocaleDateString()}),B.jsx("td",{children:B.jsx("button",{onClick:()=>h(_),children:"Edit"})})]},m)):e.map((_,m)=>B.jsxs("tr",{children:[B.jsx("td",{children:_.user_id}),B.jsx("td",{children:_.source}),B.jsx("td",{children:_.amount}),B.jsx("td",{children:new Date(_.date).toLocaleDateString()}),B.jsxs("td",{children:[B.jsx("button",{onClick:()=>h(_),children:"Edit"}),B.jsx("button",{className:"delete-button",onClick:()=>p(_.id),children:"Delete"})]})]},m))})]}),c&&B.jsx(DA,{show:c,setShow:u,entry:a,type:"income"})]})})},aB=t=>{const e=Sg().user_id,n=t.categories,[r,i]=H.useState(!0),[o,s]=H.useState([]),[a,l]=H.useState([]),[c,u]=H.useState({user_id:Number(e),category_id:"",type:"expense",amount:"",description:"",date:"",isSubmitting:!1}),[h,p]=H.useState({}),[d,y]=H.useState(!1);H.useEffect(()=>{(async()=>{try{const P=await Yp.getExpenses(e);s(P.data)}catch(P){console.error("Error fetching data:",P)}finally{i(!1)}})()},[o,e]);const b=A=>{y(!0),p(A)},_=async A=>{try{await Yp.deleteExpenses(A),s(o.filter(P=>P.id!==A))}catch(P){console.error("Error deleting data:",P)}},m=async A=>{try{A.user_id=Number(e),A.date=new Date(A.date).toISOString();const P=await Yp.addExpenses(A);s([...o,P.data])}catch(P){console.error("Error adding data:",P)}},w=async A=>{A.preventDefault(),u({...c,isSubmitting:!0}),await m(c),u({type:"expense",amount:"",description:"",date:"",category_id:""})},S=A=>{let{name:P,value:z}=A.target;u({...c,[P]:z})};return B.jsx(B.Fragment,{children:e==="undefined"?B.jsx("div",{children:B.jsx("h1",{children:"Please Login to access this page"})}):B.jsxs("div",{children:[r&&B.jsx("div",{className:"spinner-border text-light",role:"status",children:B.jsx("span",{className:"visually-hidden",children:"Loading..."})}),B.jsx("h1",{children:"Track Your Expenses"}),B.jsxs("form",{onSubmit:w,children:[B.jsx("div",{children:B.jsx("div",{children:B.jsxs("label",{children:[B.jsx("b",{children:"Search by Category: "}),B.jsxs("select",{name:"searchCategory",onChange:A=>{const P=A.target.value;if(P==="")l([]),s(o);else{const I=[...o].filter(R=>R.category_id===parseInt(P));I.length===0&&alert("No entries found for the selected category"),I.length>0&&l(I)}},disabled:c.isSubmitting,children:[B.jsx("option",{value:"",children:"All"}),n.map(A=>B.jsx("option",{value:A.id,children:A.name},A.id))]})]})})}),B.jsx("div",{children:B.jsxs("label",{children:[B.jsx("b",{children:" Category "}),B.jsx("select",{name:"category_id",value:c.category_id,onChange:A=>S(A),disabled:c.isSubmitting,children:n.map(A=>B.jsx("option",{value:A.id,children:A.name},A.id))})]})}),B.jsx("div",{children:B.jsxs("label",{children:[B.jsx("b",{children:"Amount: "}),B.jsx("input",{type:"number",name:"amount",value:c.amount,onChange:S,required:!0,disabled:c.isSubmitting})]})}),B.jsx("div",{children:B.jsxs("label",{children:[B.jsx("b",{children:"Description:"}),B.jsx("input",{type:"text",name:"description",value:c.description,onChange:S,required:!0,disabled:c.isSubmitting})]})}),B.jsx("div",{children:B.jsxs("label",{children:[B.jsx("b",{children:"Date: "}),B.jsx("input",{type:"date",name:"date",value:c.date,onChange:S,required:!0,disabled:c.isSubmitting})]})}),B.jsx("button",{type:"submit",className:"add-entry",disabled:c.isSubmitting,children:c.isSubmitting?"Submitting...":"Add Entry"})]}),B.jsx("h2",{children:"Tracked Entries"}),B.jsxs("table",{align:"center",style:{borderSpacing:"15px"},children:[B.jsx("thead",{children:B.jsxs("tr",{children:[B.jsx("th",{children:"User"}),B.jsx("th",{children:"Category"}),B.jsx("th",{children:"Amount"}),B.jsx("th",{children:"Description"}),B.jsx("th",{children:"Date"}),B.jsx("th",{children:"Actions"})]})}),B.jsx("tbody",{children:a.length!==0?a.map((A,P)=>{var z;return B.jsxs("tr",{children:[B.jsx("td",{children:A.user_id}),B.jsx("td",{children:(z=n.find(I=>I.id===A.category_id))==null?void 0:z.name}),B.jsx("td",{children:A.amount}),B.jsx("td",{children:A.description}),B.jsx("td",{children:new Date(A.date).toLocaleDateString("en-US",{timeZone:"UTC"})})]},P)}):o.map((A,P)=>{var z;return B.jsxs("tr",{children:[B.jsx("td",{children:A.user_id}),B.jsx("td",{children:(z=n.find(I=>I.id===A.category_id))==null?void 0:z.name}),B.jsx("td",{children:A.amount}),B.jsx("td",{children:A.description}),B.jsx("td",{children:new Date(A.date).toLocaleDateString()}),B.jsxs("td",{children:[B.jsx("button",{onClick:()=>b(A),children:"Edit"}),B.jsx("button",{className:"delete-button",onClick:()=>_(A.id),children:"Delete"})]})]},P)})})]}),d&&B.jsx(DA,{show:d,setShow:y,entry:h,categories:n,type:"expense"})]})})},i1="http://localhost:3000/api/goal",lB=async t=>{try{return(await me.get(`${i1}/${t}`)).data}catch(e){throw console.error("Error fetching goals:",e),e}},cB=async t=>{try{return(await me.post(`${i1}`,t)).data}catch(e){throw console.error("Error adding goal:",e),e}},uB=async(t,e)=>{try{return(await me.put(`${i1}/${t}`,e)).data}catch(n){throw console.error(`Error updating goal with id ${t}:`,n),n}},fB=async t=>{try{return(await me.delete(`${i1}/${t}`)).data}catch(e){throw console.error(`Error deleting goal with id ${t}:`,e),e}},lp={getGoals:lB,addGoal:cB,updateGoal:uB,deleteGoal:fB},hB=()=>{const t=Number(Sg().user_id),[e,n]=H.useState([]),[r,i]=H.useState({user_id:t,goal_name:"",target_amount:"",current_amount:"",deadline:""}),[o,s]=H.useState(0);H.useEffect(()=>{(async()=>{try{const w=await lp.getGoals(t);n(w.data)}catch(w){console.error("Error fetching goals:",w)}})()},[r,t]);const a=m=>{const{name:w,value:S}=m.target;i({...r,[w]:S})},l=async()=>{try{r.user_id=t,r.deadline=new Date(r.deadline).toISOString();const m=await lp.addGoal(r);n([...e,m]),i({user_id:t,goal_name:"",target_amount:"",current_amount:"",deadline:""})}catch(m){console.error("Error adding goal:",m)}},c=()=>{const m=e.reduce((S,A)=>S+parseFloat(A.target_amount),0),w=e.reduce((S,A)=>S+parseFloat(A.current_amount||0),0);s(w/m*100)};H.useEffect(()=>{c()},[e]);const[u,h]=H.useState(!1),[p,d]=H.useState(null),y=m=>{const w=e.find(S=>S.id===m);i({...w,deadline:new Date(w.deadline).toISOString().split("T")[0]}),h(!0),d(m)},b=async()=>{try{const m={...r,deadline:new Date(r.deadline).toISOString()};await lp.updateGoal(p,m),n(e.map(w=>w.id===p?m:w)),i({user_id:t,goal_name:"",target_amount:"",current_amount:"",deadline:""}),h(!1),d(null)}catch(m){console.error("Error updating goal:",m)}},_=async m=>{try{await lp.deleteGoal(m),n(e.filter(w=>w.id!==m))}catch(w){console.error("Error deleting goal:",w)}};return B.jsx(B.Fragment,{children:isNaN(t)?B.jsx("div",{children:B.jsx("h1",{children:"Please Login to access this page"})}):B.jsxs("div",{children:[B.jsx("h1",{children:"Savings Goals"}),B.jsxs("div",{className:"form-container",children:[B.jsx("input",{type:"text",name:"goal_name",placeholder:"Goal Name",value:r.goal_name,onChange:a}),B.jsx("input",{type:"number",name:"target_amount",placeholder:"Target Amount",value:r.target_amount,onChange:a}),B.jsx("input",{type:"number",name:"current_amount",placeholder:"Current Amount",value:r.current_amount,onChange:a}),B.jsx("input",{type:"date",name:"deadline",placeholder:"Deadline",value:r.deadline,onChange:a}),u?B.jsx("button",{className:"update-goal",onClick:b,children:"Update Goal"}):B.jsx("button",{className:"add-goal",onClick:l,children:"Add Goal"})]}),B.jsxs("div",{children:[B.jsx("h2",{children:"Progress"}),B.jsx("div",{className:"progress-bar",children:B.jsx("div",{className:"progress-bar-inner",style:{width:`${o}%`,backgroundColor:"#f2c069",height:"24px"}})})]}),B.jsxs("div",{children:[B.jsx("h2",{children:"Goals"}),B.jsxs("table",{children:[B.jsx("thead",{children:B.jsxs("tr",{children:[B.jsx("th",{children:"Goal Name"}),B.jsx("th",{children:"Target Amount"}),B.jsx("th",{children:"Current Amount"}),B.jsx("th",{children:"Deadline"}),B.jsx("th",{children:"Actions"})]})}),B.jsx("tbody",{children:e==null?void 0:e.map(m=>B.jsxs("tr",{children:[B.jsx("td",{children:m.goal_name}),B.jsxs("td",{children:["$",m.target_amount]}),B.jsxs("td",{children:["$",m.current_amount]}),B.jsx("td",{children:new Date(m.deadline).toLocaleDateString()}),B.jsxs("td",{children:[B.jsx("button",{onClick:()=>y(m.id),children:"Edit"}),B.jsx("button",{className:"delete-button",onClick:()=>_(m.id),children:"Delete"})]})]},m.id))})]})]})]})})};/*!
+ * @kurkle/color v0.3.4
+ * https://github.com/kurkle/color#readme
+ * (c) 2024 Jukka Kurkela
+ * Released under the MIT License
+ */function qh(t){return t+.5|0}const Hs=(t,e,n)=>Math.max(Math.min(t,n),e);function bf(t){return Hs(qh(t*2.55),0,255)}function oa(t){return Hs(qh(t*255),0,255)}function Zo(t){return Hs(qh(t/2.55)/100,0,1)}function m3(t){return Hs(qh(t*100),0,100)}const pi={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},R5=[..."0123456789ABCDEF"],dB=t=>R5[t&15],pB=t=>R5[(t&240)>>4]+R5[t&15],cp=t=>(t&240)>>4===(t&15),gB=t=>cp(t.r)&&cp(t.g)&&cp(t.b)&&cp(t.a);function mB(t){var e=t.length,n;return t[0]==="#"&&(e===4||e===5?n={r:255&pi[t[1]]*17,g:255&pi[t[2]]*17,b:255&pi[t[3]]*17,a:e===5?pi[t[4]]*17:255}:(e===7||e===9)&&(n={r:pi[t[1]]<<4|pi[t[2]],g:pi[t[3]]<<4|pi[t[4]],b:pi[t[5]]<<4|pi[t[6]],a:e===9?pi[t[7]]<<4|pi[t[8]]:255})),n}const yB=(t,e)=>t<255?e(t):"";function vB(t){var e=gB(t)?dB:pB;return t?"#"+e(t.r)+e(t.g)+e(t.b)+yB(t.a,e):void 0}const bB=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function jA(t,e,n){const r=e*Math.min(n,1-n),i=(o,s=(o+t/30)%12)=>n-r*Math.max(Math.min(s-3,9-s,1),-1);return[i(0),i(8),i(4)]}function xB(t,e,n){const r=(i,o=(i+t/60)%6)=>n-n*e*Math.max(Math.min(o,4-o,1),0);return[r(5),r(3),r(1)]}function wB(t,e,n){const r=jA(t,1,.5);let i;for(e+n>1&&(i=1/(e+n),e*=i,n*=i),i=0;i<3;i++)r[i]*=1-e-n,r[i]+=e;return r}function _B(t,e,n,r,i){return t===i?(e-n)/r+(e i)return l}return Math.max(i,1)}function tU(t){const e=[];let n,r;for(n=0,r=t.length;n0)for(var n=new Array(i),r=0,i,o;r=0&&(e=t.slice(0,n))!=="xmlns"&&(t=t.slice(n+1)),Rw.hasOwnProperty(e)?{space:Rw[e],local:t}:t}function HO(t){return function(){var e=this.ownerDocument,n=this.namespaceURI;return n===m5&&e.documentElement.namespaceURI===m5?e.createElement(t):e.createElementNS(n,t)}}function GO(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function bk(t){var e=Gg(t);return(e.local?GO:HO)(e)}function YO(){}function Cv(t){return t==null?YO:function(){return this.querySelector(t)}}function XO(t){typeof t!="function"&&(t=Cv(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;it.ownerDocument.defaultView.getComputedStyle(t,null);function Iz(t,e){return l1(t).getPropertyValue(e)}const Fz=["top","right","bottom","left"];function il(t,e,n){const r={};n=n?"-"+n:"";for(let i=0;i<4;i++){const o=Fz[i];r[o]=parseFloat(t[e+"-"+o+n])||0}return r.width=r.left+r.right,r.height=r.top+r.bottom,r}const Dz=(t,e,n)=>(t>0||e>0)&&(!n||!n.shadowRoot);function jz(t,e){const n=t.touches,r=n&&n.length?n[0]:t,{offsetX:i,offsetY:o}=r;let s=!1,a,l;if(Dz(i,o,t.target))a=i,l=o;else{const c=e.getBoundingClientRect();a=r.clientX-c.left,l=r.clientY-c.top,s=!0}return{x:a,y:l,box:s}}function Ba(t,e){if("native"in t)return t;const{canvas:n,currentDevicePixelRatio:r}=e,i=l1(n),o=i.boxSizing==="border-box",s=il(i,"padding"),a=il(i,"border","width"),{x:l,y:c,box:u}=jz(t,n),h=s.left+(u&&a.left),p=s.top+(u&&a.top);let{width:d,height:y}=e;return o&&(d-=s.width+a.width,y-=s.height+a.height),{x:Math.round((l-h)/d*n.width/r),y:Math.round((c-p)/y*n.height/r)}}function Bz(t,e,n){let r,i;if(e===void 0||n===void 0){const o=t&&ib(t);if(!o)e=t.clientWidth,n=t.clientHeight;else{const s=o.getBoundingClientRect(),a=l1(o),l=il(a,"border","width"),c=il(a,"padding");e=s.width-c.width-l.width,n=s.height-c.height-l.height,r=tg(a.maxWidth,o,"clientWidth"),i=tg(a.maxHeight,o,"clientHeight")}}return{width:e,height:n,maxWidth:r||Q0,maxHeight:i||Q0}}const pp=t=>Math.round(t*10)/10;function zz(t,e,n,r){const i=l1(t),o=il(i,"margin"),s=tg(i.maxWidth,t,"clientWidth")||Q0,a=tg(i.maxHeight,t,"clientHeight")||Q0,l=Bz(t,e,n);let{width:c,height:u}=l;if(i.boxSizing==="content-box"){const p=il(i,"border","width"),d=il(i,"padding");c-=d.width+p.width,u-=d.height+p.height}return c=Math.max(0,c-o.width),u=Math.max(0,r?c/r:u-o.height),c=pp(Math.min(c,s,l.maxWidth)),u=pp(Math.min(u,a,l.maxHeight)),c&&!u&&(u=pp(c/2)),(e!==void 0||n!==void 0)&&r&&l.height&&u>l.height&&(u=l.height,c=pp(Math.floor(u*r))),{width:c,height:u}}function O3(t,e,n){const r=e||1,i=Math.floor(t.height*r),o=Math.floor(t.width*r);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const s=t.canvas;return s.style&&(n||!s.style.height&&!s.style.width)&&(s.style.height=`${t.height}px`,s.style.width=`${t.width}px`),t.currentDevicePixelRatio!==r||s.height!==i||s.width!==o?(t.currentDevicePixelRatio=r,s.height=i,s.width=o,t.ctx.setTransform(r,0,0,r,0,0),!0):!1}const $z=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};rb()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch{}return t}();function R3(t,e){const n=Iz(t,e),r=n&&n.match(/^(\d+)(\.\d+)?px$/);return r?+r[1]:void 0}function za(t,e,n,r){return{x:t.x+n*(e.x-t.x),y:t.y+n*(e.y-t.y)}}function Uz(t,e,n,r){return{x:t.x+n*(e.x-t.x),y:r==="middle"?n<.5?t.y:e.y:r==="after"?n<1?t.y:e.y:n>0?e.y:t.y}}function qz(t,e,n,r){const i={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},s=za(t,i,n),a=za(i,o,n),l=za(o,e,n),c=za(s,a,n),u=za(a,l,n);return za(c,u,n)}const Wz=function(t,e){return{x(n){return t+t+e-n},setWidth(n){e=n},textAlign(n){return n==="center"?n:n==="right"?"left":"right"},xPlus(n,r){return n-r},leftForLtr(n,r){return n-r}}},Vz=function(){return{x(t){return t},setWidth(t){},textAlign(t){return t},xPlus(t,e){return t+e},leftForLtr(t,e){return t}}};function Dc(t,e,n){return t?Wz(e,n):Vz()}function sC(t,e){let n,r;(e==="ltr"||e==="rtl")&&(n=t.canvas.style,r=[n.getPropertyValue("direction"),n.getPropertyPriority("direction")],n.setProperty("direction",e,"important"),t.prevTextDirection=r)}function aC(t,e){e!==void 0&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function lC(t){return t==="angle"?{between:bh,compare:HB,normalize:Yr}:{between:ns,compare:(e,n)=>e-n,normalize:e=>e}}function I3({start:t,end:e,count:n,loop:r,style:i}){return{start:t%n,end:e%n,loop:r&&(e-t+1)%n===0,style:i}}function Hz(t,e,n){const{property:r,start:i,end:o}=n,{between:s,normalize:a}=lC(r),l=e.length;let{start:c,end:u,loop:h}=t,p,d;if(h){for(c+=l,u+=l,p=0,d=l;pa({chart:e,initial:n.initial,numSteps:s,currentStep:Math.min(r-n.start,s)}))}_refresh(){this._request||(this._running=!0,this._request=GA.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(e=Date.now()){let n=0;this._charts.forEach((r,i)=>{if(!r.running||!r.items.length)return;const o=r.items;let s=o.length-1,a=!1,l;for(;s>=0;--s)l=o[s],l._active?(l._total>r.duration&&(r.duration=l._total),l.tick(e),a=!0):(o[s]=o[o.length-1],o.pop());a&&(i.draw(),this._notify(i,r,e,"progress")),o.length||(r.running=!1,this._notify(i,r,e,"complete"),r.initial=!1),n+=o.length}),this._lastDate=e,n===0&&(this._running=!1)}_getAnims(e){const n=this._charts;let r=n.get(e);return r||(r={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},n.set(e,r)),r}listen(e,n,r){this._getAnims(e).listeners[n].push(r)}add(e,n){!n||!n.length||this._getAnims(e).items.push(...n)}has(e){return this._getAnims(e).items.length>0}start(e){const n=this._charts.get(e);n&&(n.running=!0,n.start=Date.now(),n.duration=n.items.reduce((r,i)=>Math.max(r,i._duration),0),this._refresh())}running(e){if(!this._running)return!1;const n=this._charts.get(e);return!(!n||!n.running||!n.items.length)}stop(e){const n=this._charts.get(e);if(!n||!n.items.length)return;const r=n.items;let i=r.length-1;for(;i>=0;--i)r[i].cancel();n.items=[],this._notify(e,n,Date.now(),"complete")}remove(e){return this._charts.delete(e)}}var Yo=new Qz;const j3="transparent",Zz={boolean(t,e,n){return n>.5?e:t},color(t,e,n){const r=N3(t||j3),i=r.valid&&N3(e||j3);return i&&i.valid?i.mix(r,n).hexString():e},number(t,e,n){return t+(e-t)*n}};class t${constructor(e,n,r,i){const o=n[r];i=xf([e.to,i,o,e.from]);const s=xf([e.from,o,i]);this._active=!0,this._fn=e.fn||Zz[e.type||typeof s],this._easing=Df[e.easing]||Df.linear,this._start=Math.floor(Date.now()+(e.delay||0)),this._duration=this._total=Math.floor(e.duration),this._loop=!!e.loop,this._target=n,this._prop=r,this._from=s,this._to=i,this._promises=void 0}active(){return this._active}update(e,n,r){if(this._active){this._notify(!1);const i=this._target[this._prop],o=r-this._start,s=this._duration-o;this._start=r,this._duration=Math.floor(Math.max(s,e.duration)),this._total+=o,this._loop=!!e.loop,this._to=xf([e.to,n,i,e.from]),this._from=xf([e.from,i,n])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(e){const n=e-this._start,r=this._duration,i=this._prop,o=this._from,s=this._loop,a=this._to;let l;if(this._active=o!==a&&(s||ni-o))}return t._cache.$bar}function d$(t){const e=t.iScale,n=h$(e,t.type);let r=e._length,i,o,s,a;const l=()=>{s===32767||s===-32768||(vh(a)&&(r=Math.min(r,Math.abs(s-a)||r)),a=s)};for(i=0,o=n.length;i=0;--r)n=Math.max(n,e[r].size(this.resolveDataElementOptions(r))/2);return n>0&&n}getLabelAndValue(e){const n=this._cachedMeta,r=this.chart.data.labels||[],{xScale:i,yScale:o}=n,s=this.getParsed(e),a=i.getLabelForValue(s.x),l=o.getLabelForValue(s.y),c=s._custom;return{label:r[e]||"",value:"("+a+", "+l+(c?", "+c:"")+")"}}update(e){const n=this._cachedMeta.data;this.updateElements(n,0,n.length,e)}updateElements(e,n,r,i){const o=i==="reset",{iScale:s,vScale:a}=this._cachedMeta,{sharedOptions:l,includeOptions:c}=this._getSharedOptions(n,i),u=s.axis,h=a.axis;for(let p=n;p=w){I.skip=!0;continue}const R=this.getParsed(P),X=be(R[d]),C=I[p]=s.getPixelForValue(R[p],P),L=I[d]=o||X?a.getBasePixel():a.getPixelForValue(l?this.applyStack(a,R,l):R[d],P);I.skip=isNaN(C)||isNaN(L)||X,I.stop=P>0&&Math.abs(R[p]-A[p])>_,b&&(I.parsed=R,I.raw=c.data[P]),h&&(I.options=u||this.resolveDataElementOptions(P,z.active?"active":i)),m||this.updateElement(z,P,I,i),A=R}}getMaxOverflow(){const e=this._cachedMeta,n=e.dataset,r=n.options&&n.options.borderWidth||0,i=e.data||[];if(!i.length)return r;const o=i[0].size(this.resolveDataElementOptions(0)),s=i[i.length-1].size(this.resolveDataElementOptions(i.length-1));return Math.max(r,o,s)/2}draw(){const e=this._cachedMeta;e.dataset.updateControlPoints(this.chart.chartArea,e.iScale.axis),super.draw()}}Mt(zf,"id","line"),Mt(zf,"defaults",{datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1}),Mt(zf,"overrides",{scales:{_index_:{type:"category"},_value_:{type:"linear"}}});class $f extends Hi{constructor(e,n){super(e,n),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(e){const n=this._cachedMeta,r=this.chart,i=r.data.labels||[],o=Wh(n._parsed[e].r,r.options.locale);return{label:i[e]||"",value:o}}parseObjectData(e,n,r,i){return iC.bind(this)(e,n,r,i)}update(e){const n=this._cachedMeta.data;this._updateRadius(),this.updateElements(n,0,n.length,e)}getMinMax(){const e=this._cachedMeta,n={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return e.data.forEach((r,i)=>{const o=this.getParsed(i).r;!isNaN(o)&&this.chart.getDataVisibility(i)&&(o{typeof b.beforeLayout=="function"&&b.beforeLayout()});const u=l.reduce((b,_)=>_.box.options&&_.box.options.display===!1?b:b+1,0)||1,h=Object.freeze({outerWidth:e,outerHeight:n,padding:i,availableWidth:o,availableHeight:s,vBoxMaxWidth:o/2/u,hBoxMaxHeight:s/2}),p=Object.assign({},i);gC(p,pr(r));const d=Object.assign({maxPadding:p,w:o,h:s,x:i.left,y:i.top},i),y=T$(l.concat(c),h);wf(a.fullSize,d,h,y),wf(l,d,h,y),wf(c,d,h,y)&&wf(l,d,h,y),I$(d),K3(a.leftAndTop,d,h,y),d.x+=d.w,d.y+=d.h,K3(a.rightAndBottom,d,h,y),t.chartArea={left:d.left,top:d.top,right:d.left+d.w,bottom:d.top+d.h,height:d.h,width:d.w},Pe(a.chartArea,b=>{const _=b.box;Object.assign(_,t.chartArea),_.update(d.w,d.h,{left:0,top:0,right:0,bottom:0})})}};class mC{acquireContext(e,n){}releaseContext(e){return!1}addEventListener(e,n,r){}removeEventListener(e,n,r){}getDevicePixelRatio(){return 1}getMaximumSize(e,n,r,i){return n=Math.max(0,n||e.width),r=r||e.height,{width:n,height:Math.max(0,i?Math.floor(n/i):r)}}isAttached(e){return!0}updateConfig(e){}}class D$ extends mC{acquireContext(e){return e&&e.getContext&&e.getContext("2d")||null}updateConfig(e){e.options.animation=!1}}const Zp="$chartjs",j$={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},J3=t=>t===null||t==="";function B$(t,e){const n=t.style,r=t.getAttribute("height"),i=t.getAttribute("width");if(t[Zp]={initial:{height:r,width:i,style:{display:n.display,height:n.height,width:n.width}}},n.display=n.display||"block",n.boxSizing=n.boxSizing||"border-box",J3(i)){const o=R3(t,"width");o!==void 0&&(t.width=o)}if(J3(r))if(t.style.height==="")t.height=t.width/(e||2);else{const o=R3(t,"height");o!==void 0&&(t.height=o)}return t}const yC=$z?{passive:!0}:!1;function z$(t,e,n){t&&t.addEventListener(e,n,yC)}function $$(t,e,n){t&&t.canvas&&t.canvas.removeEventListener(e,n,yC)}function U$(t,e){const n=j$[t.type]||t.type,{x:r,y:i}=Ba(t,e);return{type:n,chart:e,native:t,x:r!==void 0?r:null,y:i!==void 0?i:null}}function eg(t,e){for(const n of t)if(n===e||n.contains(e))return!0}function q$(t,e,n){const r=t.canvas,i=new MutationObserver(o=>{let s=!1;for(const a of o)s=s||eg(a.addedNodes,r),s=s&&!eg(a.removedNodes,r);s&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}function W$(t,e,n){const r=t.canvas,i=new MutationObserver(o=>{let s=!1;for(const a of o)s=s||eg(a.removedNodes,r),s=s&&!eg(a.addedNodes,r);s&&n()});return i.observe(document,{childList:!0,subtree:!0}),i}const wh=new Map;let Q3=0;function vC(){const t=window.devicePixelRatio;t!==Q3&&(Q3=t,wh.forEach((e,n)=>{n.currentDevicePixelRatio!==t&&e()}))}function V$(t,e){wh.size||window.addEventListener("resize",vC),wh.set(t,e)}function H$(t){wh.delete(t),wh.size||window.removeEventListener("resize",vC)}function G$(t,e,n){const r=t.canvas,i=r&&ib(r);if(!i)return;const o=YA((a,l)=>{const c=i.clientWidth;n(a,l),c{this.draw(o)}}]:[{z:r,draw:o=>{this.drawBackground(),this.drawGrid(o),this.drawTitle()}},{z:i,draw:()=>{this.drawBorder()}},{z:n,draw:o=>{this.drawLabels(o)}}]}getMatchingVisibleMetas(e){const n=this.chart.getSortedVisibleDatasetMetas(),r=this.axis+"AxisID",i=[];let o,s;for(o=0,s=n.length;o{const r=n.split("."),i=r.pop(),o=[t].concat(r).join("."),s=e[n].split("."),a=s.pop(),l=s.join(".");Ze.route(o,i,l,a)})}function hU(t){return"id"in t&&"defaults"in t}class dU{constructor(){this.controllers=new yp(Hi,"datasets",!0),this.elements=new yp(Xi,"elements"),this.plugins=new yp(Object,"plugins"),this.scales=new yp(bl,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...e){this._each("register",e)}remove(...e){this._each("unregister",e)}addControllers(...e){this._each("register",e,this.controllers)}addElements(...e){this._each("register",e,this.elements)}addPlugins(...e){this._each("register",e,this.plugins)}addScales(...e){this._each("register",e,this.scales)}getController(e){return this._get(e,this.controllers,"controller")}getElement(e){return this._get(e,this.elements,"element")}getPlugin(e){return this._get(e,this.plugins,"plugin")}getScale(e){return this._get(e,this.scales,"scale")}removeControllers(...e){this._each("unregister",e,this.controllers)}removeElements(...e){this._each("unregister",e,this.elements)}removePlugins(...e){this._each("unregister",e,this.plugins)}removeScales(...e){this._each("unregister",e,this.scales)}_each(e,n,r){[...n].forEach(i=>{const o=r||this._getRegistryForType(i);r||o.isForType(i)||o===this.plugins&&i.id?this._exec(e,o,i):Pe(i,s=>{const a=r||this._getRegistryForType(s);this._exec(e,a,s)})})}_exec(e,n,r){const i=Yv(e);$e(r["before"+i],[],r),n[e](r),$e(r["after"+i],[],r)}_getRegistryForType(e){for(let n=0;na&&o>a;return{count:r,start:l,loop:e.loop,ilen:c-1?t.split(`
+`):t}function zq(t,e){const{element:n,datasetIndex:r,index:i}=e,o=t.getDatasetMeta(r).controller,{label:s,value:a}=o.getLabelAndValue(i);return{chart:t,label:s,parsed:o.getParsed(i),raw:t.data.datasets[r].data[i],formattedValue:a,dataset:o.getDataset(),dataIndex:i,datasetIndex:r,element:n}}function S_(t,e){const n=t.chart.ctx,{body:r,footer:i,title:o}=t,{boxWidth:s,boxHeight:a}=e,l=In(e.bodyFont),c=In(e.titleFont),u=In(e.footerFont),h=o.length,p=i.length,d=r.length,y=pr(e.padding);let b=y.height,_=0,m=r.reduce((A,P)=>A+P.before.length+P.lines.length+P.after.length,0);if(m+=t.beforeBody.length+t.afterBody.length,h&&(b+=h*c.lineHeight+(h-1)*e.titleSpacing+e.titleMarginBottom),m){const A=e.displayColors?Math.max(a,l.lineHeight):l.lineHeight;b+=d*A+(m-d)*l.lineHeight+(m-1)*e.bodySpacing}p&&(b+=e.footerMarginTop+p*u.lineHeight+(p-1)*e.footerSpacing);let w=0;const S=function(A){_=Math.max(_,n.measureText(A).width+w)};return n.save(),n.font=c.string,Pe(t.title,S),n.font=l.string,Pe(t.beforeBody.concat(t.afterBody),S),w=e.displayColors?s+2+e.boxPadding:0,Pe(r,A=>{Pe(A.before,S),Pe(A.lines,S),Pe(A.after,S)}),w=0,n.font=u.string,Pe(t.footer,S),n.restore(),_+=y.width,{width:_,height:b}}function $q(t,e){const{y:n,height:r}=e;return na)break;n.push({value:T})}return w&&p&&R!==a?n.length&&Ff(n[n.length-1].value,a,P_(a,A,t))?n[n.length-1].value=a:n.push({value:a}):(!w||R===a)&&n.push({value:R}),n}function P_(t,e,{horizontal:n,minRotation:r}){const i=qi(r),o=(n?Math.sin(i):Math.cos(i))||.001,s=.75*e*(""+t).length;return Math.min(e/o,s)}class rg extends bl{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(e,n){return be(e)||(typeof e=="number"||e instanceof Number)&&!isFinite(+e)?null:+e}handleTickRangeOptions(){const{beginAtZero:e}=this.options,{minDefined:n,maxDefined:r}=this.getUserBounds();let{min:i,max:o}=this;const s=l=>i=n?i:l,a=l=>o=r?o:l;if(e){const l=No(i),c=No(o);l<0&&c<0?a(0):l>0&&c>0&&s(0)}if(i===o){let l=o===0?1:Math.abs(o*.05);a(o+l),e||s(i-l)}this.min=i,this.max=o}getTickLimit(){const e=this.options.ticks;let{maxTicksLimit:n,stepSize:r}=e,i;return r?(i=Math.ceil(this.max/r)-Math.floor(this.min/r)+1,i>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${r} would result generating up to ${i} ticks. Limiting to 1000.`),i=1e3)):(i=this.computeTickLimit(),n=n||11),n&&(i=Math.min(n,i)),i}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const e=this.options,n=e.ticks;let r=this.getTickLimit();r=Math.max(2,r);const i={maxTicks:r,bounds:e.bounds,min:e.min,max:e.max,precision:n.precision,step:n.stepSize,count:n.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:n.minRotation||0,includeBounds:n.includeBounds!==!1},o=this._range||this,s=Qq(i,o);return e.bounds==="ticks"&&qA(s,this,"value"),e.reverse?(s.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),s}configure(){const e=this.ticks;let n=this.min,r=this.max;if(super.configure(),this.options.offset&&e.length){const i=(r-n)/Math.max(e.length-1,1)/2;n-=i,r+=i}this._startValue=n,this._endValue=r,this._valueRange=r-n}getLabelForValue(e){return Wh(e,this.chart.options.locale,this.options.ticks.format)}}class H5 extends rg{determineDataLimits(){const{min:e,max:n}=this.getMinMax(!0);this.min=pn(e)?e:0,this.max=pn(n)?n:1,this.handleTickRangeOptions()}computeTickLimit(){const e=this.isHorizontal(),n=e?this.width:this.height,r=qi(this.options.ticks.minRotation),i=(e?Math.sin(r):Math.cos(r))||.001,o=this._resolveTickFontOptions(0);return Math.ceil(n/Math.min(40,o.lineHeight/i))}getPixelForValue(e){return e===null?NaN:this.getPixelForDecimal((e-this._startValue)/this._valueRange)}getValueForPixel(e){return this._startValue+this.getDecimalForPixel(e)*this._valueRange}}Mt(H5,"id","linear"),Mt(H5,"defaults",{ticks:{callback:o1.formatters.numeric}});const _h=t=>Math.floor(Gs(t)),Ia=(t,e)=>Math.pow(10,_h(t)+e);function E_(t){return t/Math.pow(10,_h(t))===1}function M_(t,e,n){const r=Math.pow(10,n),i=Math.floor(t/r);return Math.ceil(e/r)-i}function Zq(t,e){const n=e-t;let r=_h(n);for(;M_(t,e,r)>10;)r++;for(;M_(t,e,r)<10;)r--;return Math.min(r,_h(t))}function tW(t,{min:e,max:n}){e=Hr(t.min,e);const r=[],i=_h(e);let o=Zq(e,n),s=o<0?Math.pow(10,Math.abs(o)):1;const a=Math.pow(10,o),l=i>o?Math.pow(10,i):0,c=Math.round((e-l)*s)/s,u=Math.floor((e-l)/a/10)*a*10;let h=Math.floor((c-u)/Math.pow(10,o)),p=Hr(t.min,Math.round((l+u+h*Math.pow(10,o))*s)/s);for(;pY){if(Y=J,et=xt,J>M)break;for(var Z=Math.min(xt,J-2),ut=0,lt=0;lt
=s&&(l[u/8|0]=c,Tt=s),u=qC(l,u+1,t.subarray(X,Tt))}o.i=s}return UC(a,0,r+cb(u)+i)},VC=function(){var t=1,e=0;return{p:function(n){for(var r=t,i=e,o=n.length|0,s=0;s!=o;){for(var a=Math.min(s+2655,o);s>16),i=(i&65535)+15*(i>>16)}t=r,e=i},d:function(){return t%=65521,e%=65521,(t&255)<<24|(t&65280)<<8|(e&255)<<8|e>>8}}},FW=function(t,e,n,r,i){if(!i&&(i={l:1},e.dictionary)){var o=e.dictionary.subarray(-32768),s=new fr(o.length+t.length);s.set(o),s.set(t,o.length),t=s,i.w=o.length}return IW(t,e.level==null?6:e.level,e.mem==null?i.l?Math.ceil(Math.max(8,Math.min(13,Math.log(t.length)))*1.5):20:12+e.mem,n,r,i)},HC=function(t,e,n){for(;n;++e)t[e]=n,n>>>=8},DW=function(t,e){var n=e.level,r=n==0?0:n<6?1:n==9?3:2;if(t[0]=120,t[1]=r<<6|(e.dictionary&&32),t[1]|=31-(t[0]<<8|t[1])%31,e.dictionary){var i=VC();i.p(e.dictionary),HC(t,2,i.d())}},jW=function(t,e){return((t[0]&15)!=8||t[0]>>4>7||(t[0]<<8|t[1])%31)&&Bi(6,"invalid zlib data"),(t[1]>>5&1)==+!e&&Bi(6,"invalid zlib data: "+(t[1]&32?"need":"unexpected")+" dictionary"),(t[1]>>3&4)+2};function ey(t,e){e||(e={});var n=VC();n.p(t);var r=FW(t,e,e.dictionary?6:2,4);return DW(r,e),HC(r,r.length-4,n.d()),r}function BW(t,e){return OW(t.subarray(jW(t,e),-4),{i:2},e,e)}var zW=typeof TextDecoder<"u"&&new TextDecoder,$W=0;try{zW.decode(WC,{stream:!0}),$W=1}catch{}var Zt=function(){return typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:this}();function o2(){Zt.console&&typeof Zt.console.log=="function"&&Zt.console.log.apply(Zt.console,arguments)}var je={log:o2,warn:function(t){Zt.console&&(typeof Zt.console.warn=="function"?Zt.console.warn.apply(Zt.console,arguments):o2.call(null,arguments))},error:function(t){Zt.console&&(typeof Zt.console.error=="function"?Zt.console.error.apply(Zt.console,arguments):o2(t))}};function s2(t,e,n){var r=new XMLHttpRequest;r.open("GET",t),r.responseType="blob",r.onload=function(){$a(r.response,e,n)},r.onerror=function(){je.error("could not download file")},r.send()}function q_(t){var e=new XMLHttpRequest;e.open("HEAD",t,!1);try{e.send()}catch{}return e.status>=200&&e.status<=299}function Sp(t){try{t.dispatchEvent(new MouseEvent("click"))}catch{var e=document.createEvent("MouseEvents");e.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),t.dispatchEvent(e)}}var Uf,ny,$a=Zt.saveAs||((typeof window>"u"?"undefined":Ee(window))!=="object"||window!==Zt?function(){}:typeof HTMLAnchorElement<"u"&&"download"in HTMLAnchorElement.prototype?function(t,e,n){var r=Zt.URL||Zt.webkitURL,i=document.createElement("a");e=e||t.name||"download",i.download=e,i.rel="noopener",typeof t=="string"?(i.href=t,i.origin!==location.origin?q_(i.href)?s2(t,e,n):Sp(i,i.target="_blank"):Sp(i)):(i.href=r.createObjectURL(t),setTimeout(function(){r.revokeObjectURL(i.href)},4e4),setTimeout(function(){Sp(i)},0))}:"msSaveOrOpenBlob"in navigator?function(t,e,n){if(e=e||t.name||"download",typeof t=="string")if(q_(t))s2(t,e,n);else{var r=document.createElement("a");r.href=t,r.target="_blank",setTimeout(function(){Sp(r)})}else navigator.msSaveOrOpenBlob(function(i,o){return o===void 0?o={autoBom:!1}:Ee(o)!=="object"&&(je.warn("Deprecated: Expected third argument to be a object"),o={autoBom:!o}),o.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(i.type)?new Blob(["\uFEFF",i],{type:i.type}):i}(t,n),e)}:function(t,e,n,r){if((r=r||open("","_blank"))&&(r.document.title=r.document.body.innerText="downloading..."),typeof t=="string")return s2(t,e,n);var i=t.type==="application/octet-stream",o=/constructor/i.test(Zt.HTMLElement)||Zt.safari,s=/CriOS\/[\d]+/.test(navigator.userAgent);if((s||i&&o)&&(typeof FileReader>"u"?"undefined":Ee(FileReader))==="object"){var a=new FileReader;a.onloadend=function(){var u=a.result;u=s?u:u.replace(/^data:[^;]*;/,"data:attachment/file;"),r?r.location.href=u:location=u,r=null},a.readAsDataURL(t)}else{var l=Zt.URL||Zt.webkitURL,c=l.createObjectURL(t);r?r.location=c:location.href=c,r=null,setTimeout(function(){l.revokeObjectURL(c)},4e4)}});/**
+ * A class to parse color values
+ * @author Stoyan Stefanov >8)>>8)throw new Error("Character at position "+G+" of string '"+v+"' exceeds 16bits. Cannot be encoded into UCS-2 BE");$t.push(ie),$t.push(se-(ie<<8))}return String.fromCharCode.apply(void 0,$t)},Sr=d.__private__.pdfEscape=d.pdfEscape=function(v,E){return Nl(v,E).replace(/\\/g,"\\\\").replace(/\(/g,"\\(").replace(/\)/g,"\\)")},du=d.__private__.beginPage=function(v){Lt[++an]=[],re[an]={objId:0,contentsObjId:0,userUnit:Number(a),artBox:null,bleedBox:null,cropBox:null,trimBox:null,mediaBox:{bottomLeftX:0,bottomLeftY:0,topRightX:Number(v[0]),topRightY:Number(v[1])}},Zh(an),dt(Lt[$])},Qh=function(v,E){var G,Q,ct;switch(n=E||n,typeof v=="string"&&(G=m(v.toLowerCase()),Array.isArray(G)&&(Q=G[0],ct=G[1])),Array.isArray(v)&&(Q=v[0]*zt,ct=v[1]*zt),isNaN(Q)&&(Q=i[0],ct=i[1]),(Q>14400||ct>14400)&&(je.warn("A page in a PDF can not be wider or taller than 14400 userUnit. jsPDF limits the width/height to 14400"),Q=Math.min(14400,Q),ct=Math.min(14400,ct)),i=[Q,ct],n.substr(0,1)){case"l":ct>Q&&(i=[ct,Q]);break;case"p":Q>ct&&(i=[ct,Q])}du(i),od(yu),K(Mi),bu!==0&&K(bu+" J"),xu!==0&&K(xu+" j"),Ce.publish("addPage",{pageNumber:an})},w1=function(v){v>0&&v<=an&&(Lt.splice(v,1),re.splice(v,1),an--,$>an&&($=an),this.setPage($))},Zh=function(v){v>0&&v<=an&&($=v)},_1=d.__private__.getNumberOfPages=d.getNumberOfPages=function(){return Lt.length-1},td=function(v,E,G){var Q,ct=void 0;return G=G||{},v=v!==void 0?v:Re[fe].fontName,E=E!==void 0?E:Re[fe].fontStyle,Q=v.toLowerCase(),Xe[Q]!==void 0&&Xe[Q][E]!==void 0?ct=Xe[Q][E]:Xe[v]!==void 0&&Xe[v][E]!==void 0?ct=Xe[v][E]:G.disableWarning===!1&&je.warn("Unable to look up font label for font '"+v+"', '"+E+"'. Refer to getFontList() for available fonts."),ct||G.noFallback||(ct=Xe.times[E])==null&&(ct=Xe.times.normal),ct},S1=d.__private__.putInfo=function(){var v=En(),E=function(Q){return Q};for(var G in u!==null&&(E=tr.encryptor(v,0)),K("<<"),K("/Producer ("+Sr(E("jsPDF "+Qt.version))+")"),te)te.hasOwnProperty(G)&&te[G]&&K("/"+G.substr(0,1).toUpperCase()+G.substr(1)+" ("+Sr(E(te[G]))+")");K("/CreationDate ("+Sr(E(ot))+")"),K(">>"),K("endobj")},pu=d.__private__.putCatalog=function(v){var E=(v=v||{}).rootDictionaryObjId||Ro;switch(En(),K("<<"),K("/Type /Catalog"),K("/Pages "+E+" 0 R"),Dt||(Dt="fullwidth"),Dt){case"fullwidth":K("/OpenAction [3 0 R /FitH null]");break;case"fullheight":K("/OpenAction [3 0 R /FitV null]");break;case"fullpage":K("/OpenAction [3 0 R /Fit]");break;case"original":K("/OpenAction [3 0 R /XYZ null null 1]");break;default:var G=""+Dt;G.substr(G.length-1)==="%"&&(Dt=parseInt(Dt)/100),typeof Dt=="number"&&K("/OpenAction [3 0 R /XYZ null null "+C(Dt)+"]")}switch(Bt||(Bt="continuous"),Bt){case"continuous":K("/PageLayout /OneColumn");break;case"single":K("/PageLayout /SinglePage");break;case"two":case"twoleft":K("/PageLayout /TwoColumnLeft");break;case"tworight":K("/PageLayout /TwoColumnRight")}Kt&&K("/PageMode /"+Kt),Ce.publish("putCatalog"),K(">>"),K("endobj")},k1=d.__private__.putTrailer=function(){K("trailer"),K("<<"),K("/Size "+(Z+1)),K("/Root "+Z+" 0 R"),K("/Info "+(Z-1)+" 0 R"),u!==null&&K("/Encrypt "+tr.oid+" 0 R"),K("/ID [ <"+rt+"> <"+rt+"> ]"),K(">>")},A1=d.__private__.putHeader=function(){K("%PDF-"+y),K("%ºß¬à")},C1=d.__private__.putXRef=function(){var v="0000000000";K("xref"),K("0 "+(Z+1)),K("0000000000 65535 f ");for(var E=1;E<=Z;E++)typeof ut[E]=="function"?K((v+ut[E]()).slice(-10)+" 00000 n "):ut[E]!==void 0?K((v+ut[E]).slice(-10)+" 00000 n "):K("0000000000 00000 n ")},Io=d.__private__.buildDocument=function(){Ht(),dt(gt),Ce.publish("buildDocument"),A1(),wa(),Kh(),Xh(),u!==null&&y1(),S1(),pu();var v=pt;return C1(),k1(),K("startxref"),K(""+v),K("%%EOF"),dt(Lt[$]),gt.join(`
+`)},Pl=d.__private__.getBlob=function(v){return new Blob([qt(v)],{type:"application/pdf"})},El=d.output=d.__private__.output=Ei(function(v,E){switch(typeof(E=E||{})=="string"?E={filename:E}:E.filename=E.filename||"generated.pdf",v){case void 0:return Io();case"save":d.save(E.filename);break;case"arraybuffer":return qt(Io());case"blob":return Pl(Io());case"bloburi":case"bloburl":if(Zt.URL!==void 0&&typeof Zt.URL.createObjectURL=="function")return Zt.URL&&Zt.URL.createObjectURL(Pl(Io()))||void 0;je.warn("bloburl is not supported by your system, because URL.createObjectURL is not supported by your browser.");break;case"datauristring":case"dataurlstring":var G="",Q=Io();try{G=ny(Q)}catch{G=ny(unescape(encodeURIComponent(Q)))}return"data:application/pdf;filename="+E.filename+";base64,"+G;case"pdfobjectnewwindow":if(Object.prototype.toString.call(Zt)==="[object Window]"){var ct="https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js",vt=' integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==" crossorigin="anonymous"';E.pdfObjectUrl&&(ct=E.pdfObjectUrl,vt="");var Ct='
+
✈️Budget Buddy API
')
+})
+
+app.use('/auth', authRoutes)
+// app.use('/user', userRoutes);
+app.use('/api/category', categoryRoutes);
+app.use('/api/income', incomeRoutes);
+app.use('/api/expense', expenseRoutes);
+app.use('/api/goal', goalRoutes);
+app.use('/api/report', reportRoutes);
+
+const PORT = process.env.PORT || 3000;
+
+if (process.env.NODE_ENV === 'production') {
+ app.get('/*', (_, res) =>
+ res.sendFile(path.resolve('public', 'index.html'))
+ )
+}
+
+app.listen(PORT, () => {
+ console.log(`Server is running on port ${PORT}`);
+})
\ No newline at end of file
diff --git a/server/util/auth.js b/server/util/auth.js
new file mode 100644
index 000000000..8bddc8546
--- /dev/null
+++ b/server/util/auth.js
@@ -0,0 +1,62 @@
+/* eslint-disable no-undef */
+import { scrypt, randomBytes, timingSafeEqual } from "crypto";
+import jwt from 'jsonwebtoken';
+
+// command to create jwt secret key
+// node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
+const KEYLEN = 32;
+
+const JWT_SECRET = process.env.JWT_SECRET
+
+const hash = async (password) => {
+ return new Promise((resolve, reject) => {
+ const salt = randomBytes(16).toString("hex");
+
+ scrypt(password, salt, KEYLEN, (err, derivedKey) => {
+ if (err) {
+ reject(err);
+ }
+
+ //derivedKey is a Buffer
+ const derivedHex = derivedKey.toString("hex");
+ resolve(`${salt}:${derivedHex}`);
+ });
+ });
+};
+
+const compare = async (password, dbSaltHash) => {
+ return new Promise((resolve, reject) => {
+ const [salt, hash] = dbSaltHash.split(":");
+
+ const hashBuffer = Buffer.from(hash, "hex");
+
+ scrypt(password, salt, KEYLEN, (err, derivedKey) => {
+ if (err) {
+ reject(err);
+ }
+
+ const isEqual = timingSafeEqual(hashBuffer, derivedKey);
+ resolve(isEqual);
+ });
+ });
+};
+
+//encrypt
+const signToken = (user) => {
+ try{
+ return jwt.sign(user, JWT_SECRET, {expiresIn: '1h'});
+ } catch (error){
+ return error;
+ }
+};
+
+//decrypt
+const verifyToken = (token) => {
+ try{
+ return jwt.verify(token, JWT_SECRET);
+ } catch(error){
+ return error;
+ }
+}
+
+export { hash, compare, signToken, verifyToken};