87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/client/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/client/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/client/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
19 |
28 |
29 | ))
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
33 |
--------------------------------------------------------------------------------
/client/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Wifi,
3 | Waves,
4 | Dumbbell,
5 | Car,
6 | PawPrint,
7 | Tv,
8 | Thermometer,
9 | Cigarette,
10 | Cable,
11 | Maximize,
12 | Bath,
13 | Phone,
14 | Sprout,
15 | Hammer,
16 | Bus,
17 | Mountain,
18 | VolumeX,
19 | Home,
20 | Warehouse,
21 | Building,
22 | Castle,
23 | Trees,
24 | LucideIcon,
25 | } from "lucide-react";
26 |
27 | export enum AmenityEnum {
28 | WasherDryer = "WasherDryer",
29 | AirConditioning = "AirConditioning",
30 | Dishwasher = "Dishwasher",
31 | HighSpeedInternet = "HighSpeedInternet",
32 | HardwoodFloors = "HardwoodFloors",
33 | WalkInClosets = "WalkInClosets",
34 | Microwave = "Microwave",
35 | Refrigerator = "Refrigerator",
36 | Pool = "Pool",
37 | Gym = "Gym",
38 | Parking = "Parking",
39 | PetsAllowed = "PetsAllowed",
40 | WiFi = "WiFi",
41 | }
42 |
43 | export const AmenityIcons: Record = {
44 | WasherDryer: Waves,
45 | AirConditioning: Thermometer,
46 | Dishwasher: Waves,
47 | HighSpeedInternet: Wifi,
48 | HardwoodFloors: Home,
49 | WalkInClosets: Maximize,
50 | Microwave: Tv,
51 | Refrigerator: Thermometer,
52 | Pool: Waves,
53 | Gym: Dumbbell,
54 | Parking: Car,
55 | PetsAllowed: PawPrint,
56 | WiFi: Wifi,
57 | };
58 |
59 | export enum HighlightEnum {
60 | HighSpeedInternetAccess = "HighSpeedInternetAccess",
61 | WasherDryer = "WasherDryer",
62 | AirConditioning = "AirConditioning",
63 | Heating = "Heating",
64 | SmokeFree = "SmokeFree",
65 | CableReady = "CableReady",
66 | SatelliteTV = "SatelliteTV",
67 | DoubleVanities = "DoubleVanities",
68 | TubShower = "TubShower",
69 | Intercom = "Intercom",
70 | SprinklerSystem = "SprinklerSystem",
71 | RecentlyRenovated = "RecentlyRenovated",
72 | CloseToTransit = "CloseToTransit",
73 | GreatView = "GreatView",
74 | QuietNeighborhood = "QuietNeighborhood",
75 | }
76 |
77 | export const HighlightIcons: Record = {
78 | HighSpeedInternetAccess: Wifi,
79 | WasherDryer: Waves,
80 | AirConditioning: Thermometer,
81 | Heating: Thermometer,
82 | SmokeFree: Cigarette,
83 | CableReady: Cable,
84 | SatelliteTV: Tv,
85 | DoubleVanities: Maximize,
86 | TubShower: Bath,
87 | Intercom: Phone,
88 | SprinklerSystem: Sprout,
89 | RecentlyRenovated: Hammer,
90 | CloseToTransit: Bus,
91 | GreatView: Mountain,
92 | QuietNeighborhood: VolumeX,
93 | };
94 |
95 | export enum PropertyTypeEnum {
96 | Rooms = "Rooms",
97 | Tinyhouse = "Tinyhouse",
98 | Apartment = "Apartment",
99 | Villa = "Villa",
100 | Townhouse = "Townhouse",
101 | Cottage = "Cottage",
102 | }
103 |
104 | export const PropertyTypeIcons: Record = {
105 | Rooms: Home,
106 | Tinyhouse: Warehouse,
107 | Apartment: Building,
108 | Villa: Castle,
109 | Townhouse: Home,
110 | Cottage: Trees,
111 | };
112 |
113 | // Add this constant at the end of the file
114 | export const NAVBAR_HEIGHT = 52; // in pixels
115 |
116 | // Test users for development
117 | export const testUsers = {
118 | tenant: {
119 | username: "Carol White",
120 | userId: "us-east-2:76543210-90ab-cdef-1234-567890abcdef",
121 | signInDetails: {
122 | loginId: "carol.white@example.com",
123 | authFlowType: "USER_SRP_AUTH",
124 | },
125 | },
126 | tenantRole: "tenant",
127 | manager: {
128 | username: "John Smith",
129 | userId: "us-east-2:12345678-90ab-cdef-1234-567890abcdef",
130 | signInDetails: {
131 | loginId: "john.smith@example.com",
132 | authFlowType: "USER_SRP_AUTH",
133 | },
134 | },
135 | managerRole: "manager",
136 | };
137 |
--------------------------------------------------------------------------------
/client/src/lib/schemas.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 | import { PropertyTypeEnum } from "@/lib/constants";
3 |
4 | export const propertySchema = z.object({
5 | name: z.string().min(1, "Name is required"),
6 | description: z.string().min(1, "Description is required"),
7 | pricePerMonth: z.coerce.number().positive().min(0).int(),
8 | securityDeposit: z.coerce.number().positive().min(0).int(),
9 | applicationFee: z.coerce.number().positive().min(0).int(),
10 | isPetsAllowed: z.boolean(),
11 | isParkingIncluded: z.boolean(),
12 | photoUrls: z
13 | .array(z.instanceof(File))
14 | .min(1, "At least one photo is required"),
15 | amenities: z.string().min(1, "Amenities are required"),
16 | highlights: z.string().min(1, "Highlights are required"),
17 | beds: z.coerce.number().positive().min(0).max(10).int(),
18 | baths: z.coerce.number().positive().min(0).max(10).int(),
19 | squareFeet: z.coerce.number().int().positive(),
20 | propertyType: z.nativeEnum(PropertyTypeEnum),
21 | address: z.string().min(1, "Address is required"),
22 | city: z.string().min(1, "City is required"),
23 | state: z.string().min(1, "State is required"),
24 | country: z.string().min(1, "Country is required"),
25 | postalCode: z.string().min(1, "Postal code is required"),
26 | });
27 |
28 | export type PropertyFormData = z.infer;
29 |
30 | export const applicationSchema = z.object({
31 | name: z.string().min(1, "Name is required"),
32 | email: z.string().email("Invalid email address"),
33 | phoneNumber: z.string().min(10, "Phone number must be at least 10 digits"),
34 | message: z.string().optional(),
35 | });
36 |
37 | export type ApplicationFormData = z.infer;
38 |
39 | export const settingsSchema = z.object({
40 | name: z.string().min(1, "Name is required"),
41 | email: z.string().email("Invalid email address"),
42 | phoneNumber: z.string().min(10, "Phone number must be at least 10 digits"),
43 | });
44 |
45 | export type SettingsFormData = z.infer;
46 |
--------------------------------------------------------------------------------
/client/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 | import { toast } from "sonner";
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs));
7 | }
8 |
9 | export function formatEnumString(str: string) {
10 | return str.replace(/([A-Z])/g, " $1").trim();
11 | }
12 |
13 | export function formatPriceValue(value: number | null, isMin: boolean) {
14 | if (value === null || value === 0)
15 | return isMin ? "Any Min Price" : "Any Max Price";
16 | if (value >= 1000) {
17 | const kValue = value / 1000;
18 | return isMin ? `$${kValue}k+` : `<$${kValue}k`;
19 | }
20 | return isMin ? `$${value}+` : `<$${value}`;
21 | }
22 |
23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
24 | export function cleanParams(params: Record): Record {
25 | return Object.fromEntries(
26 | Object.entries(params).filter(
27 | (
28 | [_, value] // eslint-disable-line @typescript-eslint/no-unused-vars
29 | ) =>
30 | value !== undefined &&
31 | value !== "any" &&
32 | value !== "" &&
33 | (Array.isArray(value) ? value.some((v) => v !== null) : value !== null)
34 | )
35 | );
36 | }
37 |
38 | type MutationMessages = {
39 | success?: string;
40 | error: string;
41 | };
42 |
43 | export const withToast = async (
44 | mutationFn: Promise,
45 | messages: Partial
46 | ) => {
47 | const { success, error } = messages;
48 |
49 | try {
50 | const result = await mutationFn;
51 | if (success) toast.success(success);
52 | return result;
53 | } catch (err) {
54 | if (error) toast.error(error);
55 | throw err;
56 | }
57 | };
58 |
59 | export const createNewUserInDatabase = async (
60 | user: any,
61 | idToken: any,
62 | userRole: string,
63 | fetchWithBQ: any
64 | ) => {
65 | const createEndpoint =
66 | userRole?.toLowerCase() === "manager" ? "/managers" : "/tenants";
67 |
68 | const createUserResponse = await fetchWithBQ({
69 | url: createEndpoint,
70 | method: "POST",
71 | body: {
72 | cognitoId: user.userId,
73 | name: user.username,
74 | email: idToken?.payload?.email || "",
75 | phoneNumber: "",
76 | },
77 | });
78 |
79 | if (createUserResponse.error) {
80 | throw new Error("Failed to create user record");
81 | }
82 |
83 | return createUserResponse;
84 | };
85 |
--------------------------------------------------------------------------------
/client/src/state/index.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | export interface FiltersState {
4 | location: string;
5 | beds: string;
6 | baths: string;
7 | propertyType: string;
8 | amenities: string[];
9 | availableFrom: string;
10 | priceRange: [number, number] | [null, null];
11 | squareFeet: [number, number] | [null, null];
12 | coordinates: [number, number];
13 | }
14 |
15 | interface InitialStateTypes {
16 | filters: FiltersState;
17 | isFiltersFullOpen: boolean;
18 | viewMode: "grid" | "list";
19 | }
20 |
21 | export const initialState: InitialStateTypes = {
22 | filters: {
23 | location: "Los Angeles",
24 | beds: "any",
25 | baths: "any",
26 | propertyType: "any",
27 | amenities: [],
28 | availableFrom: "any",
29 | priceRange: [null, null],
30 | squareFeet: [null, null],
31 | coordinates: [-118.25, 34.05],
32 | },
33 | isFiltersFullOpen: false,
34 | viewMode: "grid",
35 | };
36 |
37 | export const globalSlice = createSlice({
38 | name: "global",
39 | initialState,
40 | reducers: {
41 | setFilters: (state, action: PayloadAction>) => {
42 | state.filters = { ...state.filters, ...action.payload };
43 | },
44 | toggleFiltersFullOpen: (state) => {
45 | state.isFiltersFullOpen = !state.isFiltersFullOpen;
46 | },
47 | setViewMode: (state, action: PayloadAction<"grid" | "list">) => {
48 | state.viewMode = action.payload;
49 | },
50 | },
51 | });
52 |
53 | export const { setFilters, toggleFiltersFullOpen, setViewMode } =
54 | globalSlice.actions;
55 |
56 | export default globalSlice.reducer;
57 |
--------------------------------------------------------------------------------
/client/src/state/redux.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRef } from "react";
4 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
5 | import { combineReducers, configureStore } from "@reduxjs/toolkit";
6 | import { Provider } from "react-redux";
7 | import { setupListeners } from "@reduxjs/toolkit/query";
8 | import globalReducer from "@/state";
9 | import { api } from "@/state/api";
10 |
11 | /* REDUX STORE */
12 | const rootReducer = combineReducers({
13 | global: globalReducer,
14 | [api.reducerPath]: api.reducer,
15 | });
16 |
17 | export const makeStore = () => {
18 | return configureStore({
19 | reducer: rootReducer,
20 | middleware: (getDefaultMiddleware) =>
21 | getDefaultMiddleware().concat(api.middleware),
22 | });
23 | };
24 |
25 | /* REDUX TYPES */
26 | export type AppStore = ReturnType;
27 | export type RootState = ReturnType;
28 | export type AppDispatch = AppStore["dispatch"];
29 | export const useAppDispatch = () => useDispatch();
30 | export const useAppSelector: TypedUseSelectorHook = useSelector;
31 |
32 | /* PROVIDER */
33 | export default function StoreProvider({
34 | children,
35 | }: {
36 | children: React.ReactNode;
37 | }) {
38 | const storeRef = useRef(null);
39 | if (!storeRef.current) {
40 | storeRef.current = makeStore();
41 | setupListeners(storeRef.current.dispatch);
42 | }
43 | return {children};
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import { LucideIcon } from "lucide-react";
2 | import { AuthUser } from "aws-amplify/auth";
3 | import { Manager, Tenant, Property, Application } from "./prismaTypes";
4 | import { MotionProps as OriginalMotionProps } from "framer-motion";
5 |
6 | declare module "framer-motion" {
7 | interface MotionProps extends OriginalMotionProps {
8 | className?: string;
9 | }
10 | }
11 |
12 | declare global {
13 | enum AmenityEnum {
14 | WasherDryer = "WasherDryer",
15 | AirConditioning = "AirConditioning",
16 | Dishwasher = "Dishwasher",
17 | HighSpeedInternet = "HighSpeedInternet",
18 | HardwoodFloors = "HardwoodFloors",
19 | WalkInClosets = "WalkInClosets",
20 | Microwave = "Microwave",
21 | Refrigerator = "Refrigerator",
22 | Pool = "Pool",
23 | Gym = "Gym",
24 | Parking = "Parking",
25 | PetsAllowed = "PetsAllowed",
26 | WiFi = "WiFi",
27 | }
28 |
29 | enum HighlightEnum {
30 | HighSpeedInternetAccess = "HighSpeedInternetAccess",
31 | WasherDryer = "WasherDryer",
32 | AirConditioning = "AirConditioning",
33 | Heating = "Heating",
34 | SmokeFree = "SmokeFree",
35 | CableReady = "CableReady",
36 | SatelliteTV = "SatelliteTV",
37 | DoubleVanities = "DoubleVanities",
38 | TubShower = "TubShower",
39 | Intercom = "Intercom",
40 | SprinklerSystem = "SprinklerSystem",
41 | RecentlyRenovated = "RecentlyRenovated",
42 | CloseToTransit = "CloseToTransit",
43 | GreatView = "GreatView",
44 | QuietNeighborhood = "QuietNeighborhood",
45 | }
46 |
47 | enum PropertyTypeEnum {
48 | Rooms = "Rooms",
49 | Tinyhouse = "Tinyhouse",
50 | Apartment = "Apartment",
51 | Villa = "Villa",
52 | Townhouse = "Townhouse",
53 | Cottage = "Cottage",
54 | }
55 |
56 | interface SidebarLinkProps {
57 | href: string;
58 | icon: LucideIcon;
59 | label: string;
60 | }
61 |
62 | interface PropertyOverviewProps {
63 | propertyId: number;
64 | }
65 |
66 | interface ApplicationModalProps {
67 | isOpen: boolean;
68 | onClose: () => void;
69 | propertyId: number;
70 | }
71 |
72 | interface ContactWidgetProps {
73 | onOpenModal: () => void;
74 | }
75 |
76 | interface ImagePreviewsProps {
77 | images: string[];
78 | }
79 |
80 | interface PropertyDetailsProps {
81 | propertyId: number;
82 | }
83 |
84 | interface PropertyOverviewProps {
85 | propertyId: number;
86 | }
87 |
88 | interface PropertyLocationProps {
89 | propertyId: number;
90 | }
91 |
92 | interface ApplicationCardProps {
93 | application: Application;
94 | userType: "manager" | "renter";
95 | children: React.ReactNode;
96 | }
97 |
98 | interface CardProps {
99 | property: Property;
100 | isFavorite: boolean;
101 | onFavoriteToggle: () => void;
102 | showFavoriteButton?: boolean;
103 | propertyLink?: string;
104 | }
105 |
106 | interface CardCompactProps {
107 | property: Property;
108 | isFavorite: boolean;
109 | onFavoriteToggle: () => void;
110 | showFavoriteButton?: boolean;
111 | propertyLink?: string;
112 | }
113 |
114 | interface HeaderProps {
115 | title: string;
116 | subtitle: string;
117 | }
118 |
119 | interface NavbarProps {
120 | isDashboard: boolean;
121 | }
122 |
123 | interface AppSidebarProps {
124 | userType: "manager" | "tenant";
125 | }
126 |
127 | interface SettingsFormProps {
128 | initialData: SettingsFormData;
129 | onSubmit: (data: SettingsFormData) => Promise;
130 | userType: "manager" | "tenant";
131 | }
132 |
133 | interface User {
134 | cognitoInfo: AuthUser;
135 | userInfo: Tenant | Manager;
136 | userRole: JsonObject | JsonPrimitive | JsonArray;
137 | }
138 | }
139 |
140 | export {};
141 |
--------------------------------------------------------------------------------
/client/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | background: 'hsl(var(--background))',
14 | foreground: 'hsl(var(--foreground))',
15 | card: {
16 | DEFAULT: 'hsl(var(--card))',
17 | foreground: 'hsl(var(--card-foreground))'
18 | },
19 | popover: {
20 | DEFAULT: 'hsl(var(--popover))',
21 | foreground: 'hsl(var(--popover-foreground))'
22 | },
23 | primary: {
24 | '50': '#fcfcfc',
25 | '100': '#f1f1f2',
26 | '200': '#e0e0e2',
27 | '300': '#c7c7cc',
28 | '400': '#a8a8af',
29 | '500': '#82828b',
30 | '600': '#57575f',
31 | '700': '#27272a',
32 | '800': '#111113',
33 | '900': '#040405',
34 | '950': '#000000'
35 | },
36 | secondary: {
37 | '50': '#fefcfc',
38 | '100': '#fdf2f2',
39 | '200': '#fae1e1',
40 | '300': '#f6c9c9',
41 | '400': '#f1abab',
42 | '500': '#eb8686',
43 | '600': '#e45a5a',
44 | '700': '#dc2828',
45 | '800': '#7c1414',
46 | '900': '#400a0a',
47 | '950': '#2c0707'
48 | },
49 | muted: {
50 | DEFAULT: 'hsl(var(--muted))',
51 | foreground: 'hsl(var(--muted-foreground))'
52 | },
53 | accent: {
54 | DEFAULT: 'hsl(var(--accent))',
55 | foreground: 'hsl(var(--accent-foreground))'
56 | },
57 | destructive: {
58 | DEFAULT: 'hsl(var(--destructive))',
59 | foreground: 'hsl(var(--destructive-foreground))'
60 | },
61 | border: 'hsl(var(--border))',
62 | input: 'hsl(var(--input))',
63 | ring: 'hsl(var(--ring))',
64 | chart: {
65 | '1': 'hsl(var(--chart-1))',
66 | '2': 'hsl(var(--chart-2))',
67 | '3': 'hsl(var(--chart-3))',
68 | '4': 'hsl(var(--chart-4))',
69 | '5': 'hsl(var(--chart-5))'
70 | },
71 | sidebar: {
72 | DEFAULT: 'hsl(var(--sidebar-background))',
73 | foreground: 'hsl(var(--sidebar-foreground))',
74 | primary: 'hsl(var(--sidebar-primary))',
75 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
76 | accent: 'hsl(var(--sidebar-accent))',
77 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
78 | border: 'hsl(var(--sidebar-border))',
79 | ring: 'hsl(var(--sidebar-ring))'
80 | }
81 | }
82 | }
83 | },
84 | plugins: [require("tailwindcss-animate")],
85 | };
86 | export default config;
87 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | },
23 | "typeRoots": ["./node_modules/@types", "./src/types"],
24 | "types": [],
25 | "target": "ES2017"
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | # Keep environment variables out of version control
3 | .env
4 |
--------------------------------------------------------------------------------
/server/aws-ec2-instructions.md:
--------------------------------------------------------------------------------
1 | # EC2 Setup Instructions
2 |
3 | ## 1. Connect to EC2 Instance via EC2 Instance Connect
4 |
5 | ## 2. Install Node Version Manager (nvm) and Node.js
6 |
7 | - **Switch to superuser and install nvm:**
8 |
9 | ```
10 | sudo su -
11 | ```
12 |
13 | ```
14 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
15 | ```
16 |
17 | - **Activate nvm:**
18 |
19 | ```
20 | . ~/.nvm/nvm.sh
21 | ```
22 |
23 | - **Install the latest version of Node.js using nvm:**
24 |
25 | ```
26 | nvm install node
27 | ```
28 |
29 | - **Verify that Node.js and npm are installed:**
30 |
31 | ```
32 | node -v
33 | ```
34 |
35 | ```
36 | npm -v
37 | ```
38 |
39 | ## 3. Install Git
40 |
41 | - **Update the system and install Git:**
42 |
43 | ```
44 | sudo yum update -y
45 | ```
46 |
47 | ```
48 | sudo yum install git -y
49 | ```
50 |
51 | - **Check Git version:**
52 |
53 | ```
54 | git --version
55 | ```
56 |
57 | - **Clone your code repository from GitHub:**
58 |
59 | ```
60 | git clone [your-github-link]
61 | ```
62 |
63 | - **Navigate to the directory and install packages:**
64 |
65 | ```
66 | cd real-estate-prod
67 | ```
68 |
69 | ```
70 | cd server
71 | ```
72 |
73 | ```
74 | npm i
75 | ```
76 |
77 | - **Create Env File and Port 80:**
78 |
79 | ```
80 | echo "PORT=80" > .env
81 | ```
82 |
83 | - **Start the application:**
84 | ```
85 | npm run dev
86 | ```
87 |
88 | ## 4. Install pm2 (Production Process Manager for Node.js)
89 |
90 | - **Install pm2 globally:**
91 |
92 | ```
93 | npm i pm2 -g
94 | ```
95 |
96 | - **Create a pm2 ecosystem configuration file (inside server directory):**
97 |
98 | ```
99 | module.exports = { apps : [{ name: 'inventory-management', script: 'npm', args: 'run dev', env: { NODE_ENV: 'development', ENV_VAR1: 'environment-variable', } }], };
100 | ```
101 |
102 | - **Modify the ecosystem file if necessary:**
103 |
104 | ```
105 | nano ecosystem.config.js
106 | ```
107 |
108 | - **Set pm2 to restart automatically on system reboot:**
109 |
110 | ```
111 | sudo env PATH=$PATH:$(which node) $(which pm2) startup systemd -u $USER --hp $(eval echo ~$USER)
112 | ```
113 |
114 | - **Start the application using the pm2 ecosystem configuration:**
115 |
116 | ```
117 | pm2 start ecosystem.config.js
118 | ```
119 |
120 | **Useful pm2 commands:**
121 |
122 | - **Stop all processes:**
123 |
124 | ```
125 | pm2 stop all
126 | ```
127 |
128 | - **Delete all processes:**
129 |
130 | ```
131 | pm2 delete all
132 | ```
133 |
134 | - **Check status of processes:**
135 |
136 | ```
137 | pm2 status
138 | ```
139 |
140 | - **Monitor processes:**
141 | ```
142 | pm2 monit
143 | ```
144 |
--------------------------------------------------------------------------------
/server/ecosystem.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | apps: [
3 | {
4 | name: "real-estate",
5 | script: "npm",
6 | args: "run dev",
7 | env: {
8 | NODE_ENV: "development",
9 | },
10 | },
11 | ],
12 | };
13 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "build": "rimraf dist && npx tsc",
7 | "start": "npm run build && node dist/index.js",
8 | "dev": "npm run build && concurrently \"npx tsc -w\" \"nodemon --exec ts-node src/index.ts\"",
9 | "seed": "ts-node prisma/seed.ts",
10 | "prisma:generate": "prisma generate",
11 | "postprisma:generate": "shx cp -r node_modules/.prisma/client/index.d.ts ../client/src/types/prismaTypes.d.ts"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "description": "",
17 | "dependencies": {
18 | "@aws-sdk/client-s3": "^3.740.0",
19 | "@aws-sdk/lib-storage": "^3.740.0",
20 | "@prisma/client": "^6.3.0",
21 | "@terraformer/wkt": "^2.2.1",
22 | "axios": "^1.7.9",
23 | "body-parser": "^1.20.3",
24 | "cors": "^2.8.5",
25 | "dotenv": "^16.4.7",
26 | "express": "^4.21.2",
27 | "helmet": "^8.0.0",
28 | "jsonwebtoken": "^9.0.2",
29 | "morgan": "^1.10.0",
30 | "multer": "^1.4.5-lts.1",
31 | "prisma": "^6.3.0",
32 | "uuid": "^11.0.5"
33 | },
34 | "devDependencies": {
35 | "@types/cors": "^2.8.17",
36 | "@types/jsonwebtoken": "^9.0.8",
37 | "@types/morgan": "^1.9.9",
38 | "@types/multer": "^1.4.12",
39 | "@types/node": "^22.13.0",
40 | "@types/terraformer__wkt": "^2.0.3",
41 | "@types/uuid": "^10.0.0",
42 | "concurrently": "^9.1.2",
43 | "nodemon": "^3.1.9",
44 | "rimraf": "^6.0.1",
45 | "shx": "^0.3.4",
46 | "ts-node": "^10.9.2",
47 | "typescript": "^5.7.3"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/server/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (e.g., Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/server/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | previewFeatures = ["postgresqlExtensions"]
4 | }
5 |
6 | datasource db {
7 | provider = "postgresql"
8 | url = env("DATABASE_URL")
9 | extensions = [postgis]
10 | }
11 |
12 | enum Highlight {
13 | HighSpeedInternetAccess
14 | WasherDryer
15 | AirConditioning
16 | Heating
17 | SmokeFree
18 | CableReady
19 | SatelliteTV
20 | DoubleVanities
21 | TubShower
22 | Intercom
23 | SprinklerSystem
24 | RecentlyRenovated
25 | CloseToTransit
26 | GreatView
27 | QuietNeighborhood
28 | }
29 |
30 | enum Amenity {
31 | WasherDryer
32 | AirConditioning
33 | Dishwasher
34 | HighSpeedInternet
35 | HardwoodFloors
36 | WalkInClosets
37 | Microwave
38 | Refrigerator
39 | Pool
40 | Gym
41 | Parking
42 | PetsAllowed
43 | WiFi
44 | }
45 |
46 | enum PropertyType {
47 | Rooms
48 | Tinyhouse
49 | Apartment
50 | Villa
51 | Townhouse
52 | Cottage
53 | }
54 |
55 | enum ApplicationStatus {
56 | Pending
57 | Denied
58 | Approved
59 | }
60 |
61 | enum PaymentStatus {
62 | Pending
63 | Paid
64 | PartiallyPaid
65 | Overdue
66 | }
67 |
68 | model Property {
69 | id Int @id @default(autoincrement())
70 | name String
71 | description String
72 | pricePerMonth Float
73 | securityDeposit Float
74 | applicationFee Float
75 | photoUrls String[]
76 | amenities Amenity[]
77 | highlights Highlight[]
78 | isPetsAllowed Boolean @default(false)
79 | isParkingIncluded Boolean @default(false)
80 | beds Int
81 | baths Float
82 | squareFeet Int
83 | propertyType PropertyType
84 | postedDate DateTime @default(now())
85 | averageRating Float? @default(0)
86 | numberOfReviews Int? @default(0)
87 | locationId Int
88 | managerCognitoId String
89 |
90 | location Location @relation(fields: [locationId], references: [id])
91 | manager Manager @relation(fields: [managerCognitoId], references: [cognitoId])
92 | leases Lease[]
93 | applications Application[]
94 | favoritedBy Tenant[] @relation("TenantFavorites")
95 | tenants Tenant[] @relation("TenantProperties")
96 | }
97 |
98 | model Manager {
99 | id Int @id @default(autoincrement())
100 | cognitoId String @unique
101 | name String
102 | email String
103 | phoneNumber String
104 |
105 | managedProperties Property[]
106 | }
107 |
108 | model Tenant {
109 | id Int @id @default(autoincrement())
110 | cognitoId String @unique
111 | name String
112 | email String
113 | phoneNumber String
114 |
115 | properties Property[] @relation("TenantProperties")
116 | favorites Property[] @relation("TenantFavorites")
117 | applications Application[]
118 | leases Lease[]
119 | }
120 |
121 | model Location {
122 | id Int @id @default(autoincrement())
123 | address String
124 | city String
125 | state String
126 | country String
127 | postalCode String
128 | coordinates Unsupported("geography(Point, 4326)")
129 |
130 | properties Property[]
131 | }
132 |
133 | model Application {
134 | id Int @id @default(autoincrement())
135 | applicationDate DateTime
136 | status ApplicationStatus
137 | propertyId Int
138 | tenantCognitoId String
139 | name String
140 | email String
141 | phoneNumber String
142 | message String?
143 | leaseId Int? @unique
144 |
145 | property Property @relation(fields: [propertyId], references: [id])
146 | tenant Tenant @relation(fields: [tenantCognitoId], references: [cognitoId])
147 | lease Lease? @relation(fields: [leaseId], references: [id])
148 | }
149 |
150 | model Lease {
151 | id Int @id @default(autoincrement())
152 | startDate DateTime
153 | endDate DateTime
154 | rent Float
155 | deposit Float
156 | propertyId Int
157 | tenantCognitoId String
158 |
159 | property Property @relation(fields: [propertyId], references: [id])
160 | tenant Tenant @relation(fields: [tenantCognitoId], references: [cognitoId])
161 | application Application?
162 | payments Payment[]
163 | }
164 |
165 | model Payment {
166 | id Int @id @default(autoincrement())
167 | amountDue Float
168 | amountPaid Float
169 | dueDate DateTime
170 | paymentDate DateTime
171 | paymentStatus PaymentStatus
172 | leaseId Int
173 |
174 | lease Lease @relation(fields: [leaseId], references: [id])
175 | }
176 |
--------------------------------------------------------------------------------
/server/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient, Prisma } from "@prisma/client";
2 | import fs from "fs";
3 | import path from "path";
4 |
5 | const prisma = new PrismaClient();
6 |
7 | function sleep(ms: number) {
8 | return new Promise((resolve) => setTimeout(resolve, ms));
9 | }
10 |
11 | function toPascalCase(str: string): string {
12 | return str.charAt(0).toUpperCase() + str.slice(1);
13 | }
14 |
15 | function toCamelCase(str: string): string {
16 | return str.charAt(0).toLowerCase() + str.slice(1);
17 | }
18 |
19 | async function insertLocationData(locations: any[]) {
20 | for (const location of locations) {
21 | const { id, country, city, state, address, postalCode, coordinates } =
22 | location;
23 | try {
24 | await prisma.$executeRaw`
25 | INSERT INTO "Location" ("id", "country", "city", "state", "address", "postalCode", "coordinates")
26 | VALUES (${id}, ${country}, ${city}, ${state}, ${address}, ${postalCode}, ST_GeomFromText(${coordinates}, 4326));
27 | `;
28 | console.log(`Inserted location for ${city}`);
29 | } catch (error) {
30 | console.error(`Error inserting location for ${city}:`, error);
31 | }
32 | }
33 | }
34 |
35 | async function resetSequence(modelName: string) {
36 | const quotedModelName = `"${toPascalCase(modelName)}"`;
37 |
38 | const maxIdResult = await (
39 | prisma[modelName as keyof PrismaClient] as any
40 | ).findMany({
41 | select: { id: true },
42 | orderBy: { id: "desc" },
43 | take: 1,
44 | });
45 |
46 | if (maxIdResult.length === 0) return;
47 |
48 | const nextId = maxIdResult[0].id + 1;
49 | await prisma.$executeRaw(
50 | Prisma.raw(`
51 | SELECT setval(pg_get_serial_sequence('${quotedModelName}', 'id'), coalesce(max(id)+1, ${nextId}), false) FROM ${quotedModelName};
52 | `)
53 | );
54 | console.log(`Reset sequence for ${modelName} to ${nextId}`);
55 | }
56 |
57 | async function deleteAllData(orderedFileNames: string[]) {
58 | const modelNames = orderedFileNames.map((fileName) => {
59 | return toPascalCase(path.basename(fileName, path.extname(fileName)));
60 | });
61 |
62 | for (const modelName of modelNames.reverse()) {
63 | const modelNameCamel = toCamelCase(modelName);
64 | const model = (prisma as any)[modelNameCamel];
65 | if (!model) {
66 | console.error(`Model ${modelName} not found in Prisma client`);
67 | continue;
68 | }
69 | try {
70 | await model.deleteMany({});
71 | console.log(`Cleared data from ${modelName}`);
72 | } catch (error) {
73 | console.error(`Error clearing data from ${modelName}:`, error);
74 | }
75 | }
76 | }
77 |
78 | async function main() {
79 | const dataDirectory = path.join(__dirname, "seedData");
80 |
81 | const orderedFileNames = [
82 | "location.json", // No dependencies
83 | "manager.json", // No dependencies
84 | "property.json", // Depends on location and manager
85 | "tenant.json", // No dependencies
86 | "lease.json", // Depends on property and tenant
87 | "application.json", // Depends on property and tenant
88 | "payment.json", // Depends on lease
89 | ];
90 |
91 | // Delete all existing data
92 | await deleteAllData(orderedFileNames);
93 |
94 | // Seed data
95 | for (const fileName of orderedFileNames) {
96 | const filePath = path.join(dataDirectory, fileName);
97 | const jsonData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
98 | const modelName = toPascalCase(
99 | path.basename(fileName, path.extname(fileName))
100 | );
101 | const modelNameCamel = toCamelCase(modelName);
102 |
103 | if (modelName === "Location") {
104 | await insertLocationData(jsonData);
105 | } else {
106 | const model = (prisma as any)[modelNameCamel];
107 | try {
108 | for (const item of jsonData) {
109 | await model.create({
110 | data: item,
111 | });
112 | }
113 | console.log(`Seeded ${modelName} with data from ${fileName}`);
114 | } catch (error) {
115 | console.error(`Error seeding data for ${modelName}:`, error);
116 | }
117 | }
118 |
119 | // Reset the sequence after seeding each model
120 | await resetSequence(modelName);
121 |
122 | await sleep(1000);
123 | }
124 | }
125 |
126 | main()
127 | .catch((e) => console.error(e))
128 | .finally(async () => await prisma.$disconnect());
129 |
--------------------------------------------------------------------------------
/server/prisma/seedData/lease.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "startDate": "2023-07-01T00:00:00Z",
5 | "endDate": "2024-06-30T00:00:00Z",
6 | "rent": 1500.0,
7 | "deposit": 1500.0,
8 | "propertyId": 1,
9 | "tenantCognitoId": "us-east-2:98765432-90ab-cdef-1234-567890abcdef"
10 | },
11 | {
12 | "id": 2,
13 | "startDate": "2023-08-01T00:00:00Z",
14 | "endDate": "2024-07-31T00:00:00Z",
15 | "rent": 1800.0,
16 | "deposit": 1800.0,
17 | "propertyId": 2,
18 | "tenantCognitoId": "us-east-2:87654321-90ab-cdef-1234-567890abcdef"
19 | },
20 | {
21 | "id": 3,
22 | "startDate": "2023-09-01T00:00:00Z",
23 | "endDate": "2024-08-31T00:00:00Z",
24 | "rent": 2200.0,
25 | "deposit": 2200.0,
26 | "propertyId": 3,
27 | "tenantCognitoId": "817b3540-a061-707b-742a-a28391181149"
28 | },
29 | {
30 | "id": 4,
31 | "startDate": "2023-07-15T00:00:00Z",
32 | "endDate": "2024-07-14T00:00:00Z",
33 | "rent": 1700.0,
34 | "deposit": 1700.0,
35 | "propertyId": 4,
36 | "tenantCognitoId": "us-east-2:65432109-90ab-cdef-1234-567890abcdef"
37 | },
38 | {
39 | "id": 5,
40 | "startDate": "2023-08-15T00:00:00Z",
41 | "endDate": "2024-08-14T00:00:00Z",
42 | "rent": 2000.0,
43 | "deposit": 2000.0,
44 | "propertyId": 5,
45 | "tenantCognitoId": "us-east-2:54321098-90ab-cdef-1234-567890abcdef"
46 | },
47 | {
48 | "id": 6,
49 | "startDate": "2023-09-15T00:00:00Z",
50 | "endDate": "2024-09-14T00:00:00Z",
51 | "rent": 2400.0,
52 | "deposit": 2400.0,
53 | "propertyId": 6,
54 | "tenantCognitoId": "us-east-2:43210987-90ab-cdef-1234-567890abcdef"
55 | },
56 | {
57 | "id": 7,
58 | "startDate": "2023-10-01T00:00:00Z",
59 | "endDate": "2024-09-30T00:00:00Z",
60 | "rent": 2200.0,
61 | "deposit": 2200.0,
62 | "propertyId": 3,
63 | "tenantCognitoId": "us-east-2:32109876-90ab-cdef-1234-567890abcdef"
64 | },
65 | {
66 | "id": 8,
67 | "startDate": "2023-08-01T00:00:00Z",
68 | "endDate": "2024-07-31T00:00:00Z",
69 | "rent": 5000.0,
70 | "deposit": 5000.0,
71 | "propertyId": 5,
72 | "tenantCognitoId": "us-east-2:21098765-90ab-cdef-1234-567890abcdef"
73 | },
74 | {
75 | "id": 9,
76 | "startDate": "2023-11-01T00:00:00Z",
77 | "endDate": "2024-10-31T00:00:00Z",
78 | "rent": 3000.0,
79 | "deposit": 3000.0,
80 | "propertyId": 7,
81 | "tenantCognitoId": "us-east-2:10987654-90ab-cdef-1234-567890abcdef"
82 | },
83 | {
84 | "id": 10,
85 | "startDate": "2023-08-15T00:00:00Z",
86 | "endDate": "2024-02-14T00:00:00Z",
87 | "rent": 2000.0,
88 | "deposit": 2000.0,
89 | "propertyId": 2,
90 | "tenantCognitoId": "us-east-2:09876543-90ab-cdef-1234-567890abcdef"
91 | },
92 | {
93 | "id": 11,
94 | "startDate": "2023-09-01T00:00:00Z",
95 | "endDate": "2024-08-31T00:00:00Z",
96 | "rent": 1000.0,
97 | "deposit": 1000.0,
98 | "propertyId": 8,
99 | "tenantCognitoId": "us-east-2:a9876543-90ab-cdef-1234-567890abcdef"
100 | },
101 | {
102 | "id": 12,
103 | "startDate": "2023-10-01T00:00:00Z",
104 | "endDate": "2023-12-31T00:00:00Z",
105 | "rent": 1800.0,
106 | "deposit": 1800.0,
107 | "propertyId": 9,
108 | "tenantCognitoId": "us-east-2:b9876543-90ab-cdef-1234-567890abcdef"
109 | },
110 | {
111 | "id": 13,
112 | "startDate": "2023-09-15T00:00:00Z",
113 | "endDate": "2024-03-14T00:00:00Z",
114 | "rent": 900.0,
115 | "deposit": 900.0,
116 | "propertyId": 10,
117 | "tenantCognitoId": "us-east-2:c9876543-90ab-cdef-1234-567890abcdef"
118 | },
119 | {
120 | "id": 14,
121 | "startDate": "2023-09-01T00:00:00Z",
122 | "endDate": "2024-08-31T00:00:00Z",
123 | "rent": 1500.0,
124 | "deposit": 1500.0,
125 | "propertyId": 1,
126 | "tenantCognitoId": "us-east-2:d9876543-90ab-cdef-1234-567890abcdef"
127 | },
128 | {
129 | "id": 15,
130 | "startDate": "2023-10-01T00:00:00Z",
131 | "endDate": "2024-09-30T00:00:00Z",
132 | "rent": 2500.0,
133 | "deposit": 2500.0,
134 | "propertyId": 4,
135 | "tenantCognitoId": "us-east-2:e9876543-90ab-cdef-1234-567890abcdef"
136 | }
137 | ]
138 |
--------------------------------------------------------------------------------
/server/prisma/seedData/location.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "country": "United States",
5 | "city": "Pasadena",
6 | "state": "CA",
7 | "address": "123 Colorado Blvd",
8 | "postalCode": "91105",
9 | "coordinates": "POINT(-118.144516 34.147785)"
10 | },
11 | {
12 | "id": 2,
13 | "country": "United States",
14 | "city": "Santa Monica",
15 | "state": "CA",
16 | "address": "456 Ocean Ave",
17 | "postalCode": "90401",
18 | "coordinates": "POINT(-118.496513 34.013654)"
19 | },
20 | {
21 | "id": 3,
22 | "country": "United States",
23 | "city": "Burbank",
24 | "state": "CA",
25 | "address": "789 Hollywood Way",
26 | "postalCode": "91505",
27 | "coordinates": "POINT(-118.328661 34.180839)"
28 | },
29 | {
30 | "id": 4,
31 | "country": "United States",
32 | "city": "Long Beach",
33 | "state": "CA",
34 | "address": "101 Pine Ave",
35 | "postalCode": "90802",
36 | "coordinates": "POINT(-118.192604 33.766720)"
37 | },
38 | {
39 | "id": 5,
40 | "country": "United States",
41 | "city": "New York",
42 | "state": "NY",
43 | "address": "555 Manhattan Ave",
44 | "postalCode": "10001",
45 | "coordinates": "POINT(-74.005941 40.712784)"
46 | },
47 | {
48 | "id": 6,
49 | "country": "United States",
50 | "city": "Malibu",
51 | "state": "CA",
52 | "address": "888 Malibu Road",
53 | "postalCode": "90265",
54 | "coordinates": "POINT(-118.774518 34.025922)"
55 | },
56 | {
57 | "id": 7,
58 | "country": "United States",
59 | "city": "Glendale",
60 | "state": "CA",
61 | "address": "777 Brand Blvd",
62 | "postalCode": "91203",
63 | "coordinates": "POINT(-118.254708 34.142508)"
64 | },
65 | {
66 | "id": 8,
67 | "country": "United States",
68 | "city": "Torrance",
69 | "state": "CA",
70 | "address": "555 Torrance Blvd",
71 | "postalCode": "90503",
72 | "coordinates": "POINT(-118.352575 33.835849)"
73 | },
74 | {
75 | "id": 9,
76 | "country": "United States",
77 | "city": "Miami",
78 | "state": "FL",
79 | "address": "1234 Ocean Dr",
80 | "postalCode": "33139",
81 | "coordinates": "POINT(-80.130045 25.782551)"
82 | },
83 | {
84 | "id": 10,
85 | "country": "United States",
86 | "city": "Beverly Hills",
87 | "state": "CA",
88 | "address": "789 Rodeo Dr",
89 | "postalCode": "90210",
90 | "coordinates": "POINT(-118.400356 34.073620)"
91 | }
92 | ]
93 |
--------------------------------------------------------------------------------
/server/prisma/seedData/manager.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "cognitoId": "010be580-60a1-70ae-780e-18a6fd94ad32",
5 | "name": "John Smith",
6 | "email": "john.smith@example.com",
7 | "phoneNumber": "+1 (555) 123-4567"
8 | },
9 | {
10 | "id": 2,
11 | "cognitoId": "us-east-2:23456789-90ab-cdef-1234-567890abcdef",
12 | "name": "Sarah Johnson",
13 | "email": "sarah.johnson@example.com",
14 | "phoneNumber": "+1 (555) 987-6543"
15 | },
16 | {
17 | "id": 3,
18 | "cognitoId": "us-east-2:34567890-90ab-cdef-1234-567890abcdef",
19 | "name": "Michael Brown",
20 | "email": "michael.brown@example.com",
21 | "phoneNumber": "+1 (555) 456-7890"
22 | },
23 | {
24 | "id": 4,
25 | "cognitoId": "us-east-2:45678901-90ab-cdef-1234-567890abcdef",
26 | "name": "Emily Davis",
27 | "email": "emily.davis@example.com",
28 | "phoneNumber": "+1 (555) 234-5678"
29 | },
30 | {
31 | "id": 5,
32 | "cognitoId": "us-east-2:56789012-90ab-cdef-1234-567890abcdef",
33 | "name": "Robert Wilson",
34 | "email": "robert.wilson@example.com",
35 | "phoneNumber": "+1 (555) 876-5432"
36 | },
37 | {
38 | "id": 6,
39 | "cognitoId": "us-east-2:67890123-90ab-cdef-1234-567890abcdef",
40 | "name": "Lisa Thompson",
41 | "email": "lisa.thompson@example.com",
42 | "phoneNumber": "+1 (555) 345-6789"
43 | },
44 | {
45 | "id": 7,
46 | "cognitoId": "us-east-2:78901234-90ab-cdef-1234-567890abcdef",
47 | "name": "Daniel Martinez",
48 | "email": "daniel.martinez@example.com",
49 | "phoneNumber": "+1 (555) 789-0123"
50 | },
51 | {
52 | "id": 8,
53 | "cognitoId": "us-east-2:89012345-90ab-cdef-1234-567890abcdef",
54 | "name": "Olivia Garcia",
55 | "email": "olivia.garcia@example.com",
56 | "phoneNumber": "+1 (555) 678-9012"
57 | },
58 | {
59 | "id": 9,
60 | "cognitoId": "us-east-2:90123456-90ab-cdef-1234-567890abcdef",
61 | "name": "Ethan Rodriguez",
62 | "email": "ethan.rodriguez@example.com",
63 | "phoneNumber": "+1 (555) 567-8901"
64 | },
65 | {
66 | "id": 10,
67 | "cognitoId": "us-east-2:01234567-90ab-cdef-1234-567890abcdef",
68 | "name": "Sophia Kim",
69 | "email": "sophia.kim@example.com",
70 | "phoneNumber": "+1 (555) 456-7890"
71 | }
72 | ]
73 |
--------------------------------------------------------------------------------
/server/prisma/seedData/payment.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "amountDue": 1500.0,
4 | "amountPaid": 1500.0,
5 | "dueDate": "2023-07-01T00:00:00Z",
6 | "paymentDate": "2023-06-28T00:00:00Z",
7 | "paymentStatus": "Paid",
8 | "lease": {
9 | "connect": { "id": 1 }
10 | }
11 | },
12 | {
13 | "amountDue": 1800.0,
14 | "amountPaid": 0.0,
15 | "dueDate": "2023-08-01T00:00:00Z",
16 | "paymentDate": "2023-07-28T00:00:00Z",
17 | "paymentStatus": "Pending",
18 | "lease": {
19 | "connect": { "id": 2 }
20 | }
21 | },
22 | {
23 | "amountDue": 2200.0,
24 | "amountPaid": 2200.0,
25 | "dueDate": "2023-09-01T00:00:00Z",
26 | "paymentDate": "2023-08-30T00:00:00Z",
27 | "paymentStatus": "Paid",
28 | "lease": {
29 | "connect": { "id": 3 }
30 | }
31 | },
32 | {
33 | "amountDue": 1700.0,
34 | "amountPaid": 1700.0,
35 | "dueDate": "2023-07-15T00:00:00Z",
36 | "paymentDate": "2023-07-14T00:00:00Z",
37 | "paymentStatus": "Paid",
38 | "lease": {
39 | "connect": { "id": 4 }
40 | }
41 | },
42 | {
43 | "amountDue": 2000.0,
44 | "amountPaid": 0.0,
45 | "dueDate": "2023-08-15T00:00:00Z",
46 | "paymentDate": "2023-08-14T00:00:00Z",
47 | "paymentStatus": "Pending",
48 | "lease": {
49 | "connect": { "id": 5 }
50 | }
51 | },
52 | {
53 | "amountDue": 2400.0,
54 | "amountPaid": 2400.0,
55 | "dueDate": "2023-09-15T00:00:00Z",
56 | "paymentDate": "2023-09-14T00:00:00Z",
57 | "paymentStatus": "Paid",
58 | "lease": {
59 | "connect": { "id": 6 }
60 | }
61 | },
62 | {
63 | "amountDue": 2200.0,
64 | "amountPaid": 1100.0,
65 | "dueDate": "2023-10-01T00:00:00Z",
66 | "paymentDate": "2023-09-25T00:00:00Z",
67 | "paymentStatus": "PartiallyPaid",
68 | "lease": {
69 | "connect": { "id": 7 }
70 | }
71 | },
72 | {
73 | "amountDue": 5000.0,
74 | "amountPaid": 5000.0,
75 | "dueDate": "2023-08-01T00:00:00Z",
76 | "paymentDate": "2023-07-30T00:00:00Z",
77 | "paymentStatus": "Paid",
78 | "lease": {
79 | "connect": { "id": 8 }
80 | }
81 | },
82 | {
83 | "amountDue": 3000.0,
84 | "amountPaid": 1500.0,
85 | "dueDate": "2023-11-01T00:00:00Z",
86 | "paymentDate": "2023-10-25T00:00:00Z",
87 | "paymentStatus": "PartiallyPaid",
88 | "lease": {
89 | "connect": { "id": 9 }
90 | }
91 | },
92 | {
93 | "amountDue": 2000.0,
94 | "amountPaid": 2000.0,
95 | "dueDate": "2023-08-15T00:00:00Z",
96 | "paymentDate": "2023-08-10T00:00:00Z",
97 | "paymentStatus": "Paid",
98 | "lease": {
99 | "connect": { "id": 10 }
100 | }
101 | },
102 | {
103 | "amountDue": 1000.0,
104 | "amountPaid": 0.0,
105 | "dueDate": "2023-09-01T00:00:00Z",
106 | "paymentDate": "2023-08-30T00:00:00Z",
107 | "paymentStatus": "Pending",
108 | "lease": {
109 | "connect": { "id": 11 }
110 | }
111 | },
112 | {
113 | "amountDue": 1800.0,
114 | "amountPaid": 1800.0,
115 | "dueDate": "2023-10-01T00:00:00Z",
116 | "paymentDate": "2023-09-28T00:00:00Z",
117 | "paymentStatus": "Paid",
118 | "lease": {
119 | "connect": { "id": 12 }
120 | }
121 | },
122 | {
123 | "amountDue": 900.0,
124 | "amountPaid": 450.0,
125 | "dueDate": "2023-09-15T00:00:00Z",
126 | "paymentDate": "2023-09-10T00:00:00Z",
127 | "paymentStatus": "PartiallyPaid",
128 | "lease": {
129 | "connect": { "id": 13 }
130 | }
131 | },
132 | {
133 | "amountDue": 1500.0,
134 | "amountPaid": 1500.0,
135 | "dueDate": "2023-09-01T00:00:00Z",
136 | "paymentDate": "2023-08-30T00:00:00Z",
137 | "paymentStatus": "Paid",
138 | "lease": {
139 | "connect": { "id": 14 }
140 | }
141 | },
142 | {
143 | "amountDue": 2500.0,
144 | "amountPaid": 0.0,
145 | "dueDate": "2023-10-01T00:00:00Z",
146 | "paymentDate": "2023-09-28T00:00:00Z",
147 | "paymentStatus": "Paid",
148 | "lease": {
149 | "connect": { "id": 15 }
150 | }
151 | }
152 | ]
153 |
--------------------------------------------------------------------------------
/server/prisma/seedData/tenant.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "cognitoId": "817b3540-a061-707b-742a-a28391181149",
5 | "name": "Carol White",
6 | "email": "carol.white@example.com",
7 | "phoneNumber": "+1 (555) 555-6666",
8 | "properties": {
9 | "connect": [{ "id": 5 }, { "id": 6 }]
10 | }
11 | },
12 | {
13 | "id": 2,
14 | "cognitoId": "us-east-2:87654321-90ab-cdef-1234-567890abcdef",
15 | "name": "Bob Green",
16 | "email": "bob.green@example.com",
17 | "phoneNumber": "+1 (555) 333-4444",
18 | "favorites": {
19 | "connect": [{ "id": 2 }]
20 | }
21 | },
22 | {
23 | "id": 3,
24 | "cognitoId": "us-east-2:98765432-90ab-cdef-1234-567890abcdef",
25 | "name": "Alice Brown",
26 | "email": "alice.brown@example.com",
27 | "phoneNumber": "+1 (555) 111-2222",
28 | "properties": {
29 | "connect": []
30 | },
31 | "favorites": {
32 | "connect": []
33 | }
34 | },
35 | {
36 | "id": 4,
37 | "cognitoId": "us-east-2:65432109-90ab-cdef-1234-567890abcdef",
38 | "name": "David Lee",
39 | "email": "david.lee@example.com",
40 | "phoneNumber": "+1 (555) 777-8888",
41 | "properties": {
42 | "connect": [{ "id": 4 }]
43 | },
44 | "favorites": {
45 | "connect": [{ "id": 4 }, { "id": 5 }]
46 | }
47 | },
48 | {
49 | "id": 5,
50 | "cognitoId": "us-east-2:54321098-90ab-cdef-1234-567890abcdef",
51 | "name": "Emma Taylor",
52 | "email": "emma.taylor@example.com",
53 | "phoneNumber": "+1 (555) 999-0000",
54 | "properties": {
55 | "connect": [{ "id": 2 }, { "id": 9 }]
56 | },
57 | "favorites": {
58 | "connect": [{ "id": 1 }, { "id": 5 }, { "id": 6 }]
59 | }
60 | },
61 | {
62 | "id": 6,
63 | "cognitoId": "us-east-2:43210987-90ab-cdef-1234-567890abcdef",
64 | "name": "Frank Wilson",
65 | "email": "frank.wilson@example.com",
66 | "phoneNumber": "+1 (555) 222-3333",
67 | "properties": {
68 | "connect": [{ "id": 10 }]
69 | }
70 | },
71 | {
72 | "id": 7,
73 | "cognitoId": "us-east-2:32109876-90ab-cdef-1234-567890abcdef",
74 | "name": "Grace Miller",
75 | "email": "grace.miller@example.com",
76 | "phoneNumber": "+1 (555) 444-5555",
77 | "properties": {
78 | "connect": [{ "id": 6 }]
79 | },
80 | "favorites": {
81 | "connect": [{ "id": 3 }, { "id": 7 }]
82 | }
83 | },
84 | {
85 | "id": 8,
86 | "cognitoId": "us-east-2:21098765-90ab-cdef-1234-567890abcdef",
87 | "name": "Henry Wilson",
88 | "email": "henry.wilson@example.com",
89 | "phoneNumber": "+1 (555) 444-5555",
90 | "properties": {
91 | "connect": [{ "id": 3 }]
92 | }
93 | },
94 | {
95 | "id": 9,
96 | "cognitoId": "us-east-2:10987654-90ab-cdef-1234-567890abcdef",
97 | "name": "Isabella Garcia",
98 | "email": "isabella.garcia@example.com",
99 | "phoneNumber": "+1 (555) 666-7777",
100 | "properties": {
101 | "connect": [{ "id": 7 }, { "id": 8 }]
102 | },
103 | "favorites": {
104 | "connect": [{ "id": 7 }, { "id": 9 }]
105 | }
106 | },
107 | {
108 | "id": 10,
109 | "cognitoId": "us-east-2:09876543-90ab-cdef-1234-567890abcdef",
110 | "name": "Jack Thompson",
111 | "email": "jack.thompson@example.com",
112 | "phoneNumber": "+1 (555) 888-9999",
113 | "properties": {
114 | "connect": [{ "id": 10 }]
115 | },
116 | "favorites": {
117 | "connect": [{ "id": 2 }, { "id": 10 }]
118 | }
119 | },
120 | {
121 | "id": 11,
122 | "cognitoId": "us-east-2:a9876543-90ab-cdef-1234-567890abcdef",
123 | "name": "Karen Martinez",
124 | "email": "karen.martinez@example.com",
125 | "phoneNumber": "+1 (555) 123-4567",
126 | "properties": {
127 | "connect": [{ "id": 2 }]
128 | },
129 | "favorites": {
130 | "connect": [{ "id": 1 }, { "id": 4 }, { "id": 7 }]
131 | }
132 | },
133 | {
134 | "id": 12,
135 | "cognitoId": "us-east-2:b9876543-90ab-cdef-1234-567890abcdef",
136 | "name": "Liam Johnson",
137 | "email": "liam.johnson@example.com",
138 | "phoneNumber": "+1 (555) 987-6543",
139 | "favorites": {
140 | "connect": [{ "id": 3 }, { "id": 6 }, { "id": 9 }]
141 | }
142 | },
143 | {
144 | "id": 13,
145 | "cognitoId": "us-east-2:c9876543-90ab-cdef-1234-567890abcdef",
146 | "name": "Mia Rodriguez",
147 | "email": "mia.rodriguez@example.com",
148 | "phoneNumber": "+1 (555) 246-8135",
149 | "properties": {
150 | "connect": [{ "id": 4 }]
151 | }
152 | },
153 | {
154 | "id": 14,
155 | "cognitoId": "us-east-2:d9876543-90ab-cdef-1234-567890abcdef",
156 | "name": "Noah Kim",
157 | "email": "noah.kim@example.com",
158 | "phoneNumber": "+1 (555) 369-2580",
159 | "favorites": {
160 | "connect": [{ "id": 1 }, { "id": 10 }]
161 | }
162 | },
163 | {
164 | "id": 15,
165 | "cognitoId": "us-east-2:e9876543-90ab-cdef-1234-567890abcdef",
166 | "name": "Olivia Chen",
167 | "email": "olivia.chen@example.com",
168 | "phoneNumber": "+1 (555) 159-7531",
169 | "properties": {
170 | "connect": [{ "id": 5 }]
171 | },
172 | "favorites": {
173 | "connect": [{ "id": 4 }, { "id": 7 }, { "id": 9 }]
174 | }
175 | }
176 | ]
177 |
--------------------------------------------------------------------------------
/server/src/controllers/leaseControllers.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import { PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | export const getLeases = async (req: Request, res: Response): Promise => {
7 | try {
8 | const leases = await prisma.lease.findMany({
9 | include: {
10 | tenant: true,
11 | property: true,
12 | },
13 | });
14 | res.json(leases);
15 | } catch (error: any) {
16 | res
17 | .status(500)
18 | .json({ message: `Error retrieving leases: ${error.message}` });
19 | }
20 | };
21 |
22 | export const getLeasePayments = async (
23 | req: Request,
24 | res: Response
25 | ): Promise => {
26 | try {
27 | const { id } = req.params;
28 | const payments = await prisma.payment.findMany({
29 | where: { leaseId: Number(id) },
30 | });
31 | res.json(payments);
32 | } catch (error: any) {
33 | res
34 | .status(500)
35 | .json({ message: `Error retrieving lease payments: ${error.message}` });
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/server/src/controllers/managerControllers.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import { PrismaClient } from "@prisma/client";
3 | import { wktToGeoJSON } from "@terraformer/wkt";
4 |
5 | const prisma = new PrismaClient();
6 |
7 | export const getManager = async (
8 | req: Request,
9 | res: Response
10 | ): Promise => {
11 | try {
12 | const { cognitoId } = req.params;
13 | const manager = await prisma.manager.findUnique({
14 | where: { cognitoId },
15 | });
16 |
17 | if (manager) {
18 | res.json(manager);
19 | } else {
20 | res.status(404).json({ message: "Manager not found" });
21 | }
22 | } catch (error: any) {
23 | res
24 | .status(500)
25 | .json({ message: `Error retrieving manager: ${error.message}` });
26 | }
27 | };
28 |
29 | export const createManager = async (
30 | req: Request,
31 | res: Response
32 | ): Promise => {
33 | try {
34 | const { cognitoId, name, email, phoneNumber } = req.body;
35 |
36 | const manager = await prisma.manager.create({
37 | data: {
38 | cognitoId,
39 | name,
40 | email,
41 | phoneNumber,
42 | },
43 | });
44 |
45 | res.status(201).json(manager);
46 | } catch (error: any) {
47 | res
48 | .status(500)
49 | .json({ message: `Error creating manager: ${error.message}` });
50 | }
51 | };
52 |
53 | export const updateManager = async (
54 | req: Request,
55 | res: Response
56 | ): Promise => {
57 | try {
58 | const { cognitoId } = req.params;
59 | const { name, email, phoneNumber } = req.body;
60 |
61 | const updateManager = await prisma.manager.update({
62 | where: { cognitoId },
63 | data: {
64 | name,
65 | email,
66 | phoneNumber,
67 | },
68 | });
69 |
70 | res.json(updateManager);
71 | } catch (error: any) {
72 | res
73 | .status(500)
74 | .json({ message: `Error updating manager: ${error.message}` });
75 | }
76 | };
77 |
78 | export const getManagerProperties = async (
79 | req: Request,
80 | res: Response
81 | ): Promise => {
82 | try {
83 | const { cognitoId } = req.params;
84 | const properties = await prisma.property.findMany({
85 | where: { managerCognitoId: cognitoId },
86 | include: {
87 | location: true,
88 | },
89 | });
90 |
91 | const propertiesWithFormattedLocation = await Promise.all(
92 | properties.map(async (property) => {
93 | const coordinates: { coordinates: string }[] =
94 | await prisma.$queryRaw`SELECT ST_asText(coordinates) as coordinates from "Location" where id = ${property.location.id}`;
95 |
96 | const geoJSON: any = wktToGeoJSON(coordinates[0]?.coordinates || "");
97 | const longitude = geoJSON.coordinates[0];
98 | const latitude = geoJSON.coordinates[1];
99 |
100 | return {
101 | ...property,
102 | location: {
103 | ...property.location,
104 | coordinates: {
105 | longitude,
106 | latitude,
107 | },
108 | },
109 | };
110 | })
111 | );
112 |
113 | res.json(propertiesWithFormattedLocation);
114 | } catch (err: any) {
115 | res
116 | .status(500)
117 | .json({ message: `Error retrieving manager properties: ${err.message}` });
118 | }
119 | };
120 |
--------------------------------------------------------------------------------
/server/src/controllers/tenantControllers.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from "express";
2 | import { PrismaClient } from "@prisma/client";
3 | import { wktToGeoJSON } from "@terraformer/wkt";
4 |
5 | const prisma = new PrismaClient();
6 |
7 | export const getTenant = async (req: Request, res: Response): Promise => {
8 | try {
9 | const { cognitoId } = req.params;
10 | const tenant = await prisma.tenant.findUnique({
11 | where: { cognitoId },
12 | include: {
13 | favorites: true,
14 | },
15 | });
16 |
17 | if (tenant) {
18 | res.json(tenant);
19 | } else {
20 | res.status(404).json({ message: "Tenant not found" });
21 | }
22 | } catch (error: any) {
23 | res
24 | .status(500)
25 | .json({ message: `Error retrieving tenant: ${error.message}` });
26 | }
27 | };
28 |
29 | export const createTenant = async (
30 | req: Request,
31 | res: Response
32 | ): Promise => {
33 | try {
34 | const { cognitoId, name, email, phoneNumber } = req.body;
35 |
36 | const tenant = await prisma.tenant.create({
37 | data: {
38 | cognitoId,
39 | name,
40 | email,
41 | phoneNumber,
42 | },
43 | });
44 |
45 | res.status(201).json(tenant);
46 | } catch (error: any) {
47 | res
48 | .status(500)
49 | .json({ message: `Error creating tenant: ${error.message}` });
50 | }
51 | };
52 |
53 | export const updateTenant = async (
54 | req: Request,
55 | res: Response
56 | ): Promise => {
57 | try {
58 | const { cognitoId } = req.params;
59 | const { name, email, phoneNumber } = req.body;
60 |
61 | const updateTenant = await prisma.tenant.update({
62 | where: { cognitoId },
63 | data: {
64 | name,
65 | email,
66 | phoneNumber,
67 | },
68 | });
69 |
70 | res.json(updateTenant);
71 | } catch (error: any) {
72 | res
73 | .status(500)
74 | .json({ message: `Error updating tenant: ${error.message}` });
75 | }
76 | };
77 |
78 | export const getCurrentResidences = async (
79 | req: Request,
80 | res: Response
81 | ): Promise => {
82 | try {
83 | const { cognitoId } = req.params;
84 | const properties = await prisma.property.findMany({
85 | where: { tenants: { some: { cognitoId } } },
86 | include: {
87 | location: true,
88 | },
89 | });
90 |
91 | const residencesWithFormattedLocation = await Promise.all(
92 | properties.map(async (property) => {
93 | const coordinates: { coordinates: string }[] =
94 | await prisma.$queryRaw`SELECT ST_asText(coordinates) as coordinates from "Location" where id = ${property.location.id}`;
95 |
96 | const geoJSON: any = wktToGeoJSON(coordinates[0]?.coordinates || "");
97 | const longitude = geoJSON.coordinates[0];
98 | const latitude = geoJSON.coordinates[1];
99 |
100 | return {
101 | ...property,
102 | location: {
103 | ...property.location,
104 | coordinates: {
105 | longitude,
106 | latitude,
107 | },
108 | },
109 | };
110 | })
111 | );
112 |
113 | res.json(residencesWithFormattedLocation);
114 | } catch (err: any) {
115 | res
116 | .status(500)
117 | .json({ message: `Error retrieving manager properties: ${err.message}` });
118 | }
119 | };
120 |
121 | export const addFavoriteProperty = async (
122 | req: Request,
123 | res: Response
124 | ): Promise => {
125 | try {
126 | const { cognitoId, propertyId } = req.params;
127 | const tenant = await prisma.tenant.findUnique({
128 | where: { cognitoId },
129 | include: { favorites: true },
130 | });
131 |
132 | if (!tenant) {
133 | res.status(404).json({ message: "Tenant not found" });
134 | return;
135 | }
136 |
137 | const propertyIdNumber = Number(propertyId);
138 | const existingFavorites = tenant.favorites || [];
139 |
140 | if (!existingFavorites.some((fav) => fav.id === propertyIdNumber)) {
141 | const updatedTenant = await prisma.tenant.update({
142 | where: { cognitoId },
143 | data: {
144 | favorites: {
145 | connect: { id: propertyIdNumber },
146 | },
147 | },
148 | include: { favorites: true },
149 | });
150 | res.json(updatedTenant);
151 | } else {
152 | res.status(409).json({ message: "Property already added as favorite" });
153 | }
154 | } catch (error: any) {
155 | res
156 | .status(500)
157 | .json({ message: `Error adding favorite property: ${error.message}` });
158 | }
159 | };
160 |
161 | export const removeFavoriteProperty = async (
162 | req: Request,
163 | res: Response
164 | ): Promise => {
165 | try {
166 | const { cognitoId, propertyId } = req.params;
167 | const propertyIdNumber = Number(propertyId);
168 |
169 | const updatedTenant = await prisma.tenant.update({
170 | where: { cognitoId },
171 | data: {
172 | favorites: {
173 | disconnect: { id: propertyIdNumber },
174 | },
175 | },
176 | include: { favorites: true },
177 | });
178 |
179 | res.json(updatedTenant);
180 | } catch (err: any) {
181 | res
182 | .status(500)
183 | .json({ message: `Error removing favorite property: ${err.message}` });
184 | }
185 | };
186 |
--------------------------------------------------------------------------------
/server/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import dotenv from "dotenv";
3 | import bodyParser from "body-parser";
4 | import cors from "cors";
5 | import helmet from "helmet";
6 | import morgan from "morgan";
7 | import { authMiddleware } from "./middleware/authMiddleware";
8 | /* ROUTE IMPORT */
9 | import tenantRoutes from "./routes/tenantRoutes";
10 | import managerRoutes from "./routes/managerRoutes";
11 | import propertyRoutes from "./routes/propertyRoutes";
12 | import leaseRoutes from "./routes/leaseRoutes";
13 | import applicationRoutes from "./routes/applicationRoutes";
14 |
15 | /* CONFIGURATIONS */
16 | dotenv.config();
17 | const app = express();
18 | app.use(express.json());
19 | app.use(helmet());
20 | app.use(helmet.crossOriginResourcePolicy({ policy: "cross-origin" }));
21 | app.use(morgan("common"));
22 | app.use(bodyParser.json());
23 | app.use(bodyParser.urlencoded({ extended: false }));
24 | app.use(cors());
25 |
26 | /* ROUTES */
27 | app.get("/", (req, res) => {
28 | res.send("This is home route");
29 | });
30 |
31 | app.use("/applications", applicationRoutes);
32 | app.use("/properties", propertyRoutes);
33 | app.use("/leases", leaseRoutes);
34 | app.use("/tenants", authMiddleware(["tenant"]), tenantRoutes);
35 | app.use("/managers", authMiddleware(["manager"]), managerRoutes);
36 |
37 | /* SERVER */
38 | const port = Number(process.env.PORT) || 3002;
39 | app.listen(port, "0.0.0.0", () => {
40 | console.log(`Server running on port ${port}`);
41 | });
42 |
--------------------------------------------------------------------------------
/server/src/middleware/authMiddleware.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from "express";
2 | import jwt, { JwtPayload } from "jsonwebtoken";
3 |
4 | interface DecodedToken extends JwtPayload {
5 | sub: string;
6 | "custom:role"?: string;
7 | }
8 |
9 | declare global {
10 | namespace Express {
11 | interface Request {
12 | user?: {
13 | id: string;
14 | role: string;
15 | };
16 | }
17 | }
18 | }
19 |
20 | export const authMiddleware = (allowedRoles: string[]) => {
21 | return (req: Request, res: Response, next: NextFunction): void => {
22 | const token = req.headers.authorization?.split(" ")[1];
23 |
24 | if (!token) {
25 | res.status(401).json({ message: "Unauthorized" });
26 | return;
27 | }
28 |
29 | try {
30 | const decoded = jwt.decode(token) as DecodedToken;
31 | const userRole = decoded["custom:role"] || "";
32 | req.user = {
33 | id: decoded.sub,
34 | role: userRole,
35 | };
36 |
37 | const hasAccess = allowedRoles.includes(userRole.toLowerCase());
38 | if (!hasAccess) {
39 | res.status(403).json({ message: "Access Denied" });
40 | return;
41 | }
42 | } catch (err) {
43 | console.error("Failed to decode token:", err);
44 | res.status(400).json({ message: "Invalid token" });
45 | return;
46 | }
47 |
48 | next();
49 | };
50 | };
51 |
--------------------------------------------------------------------------------
/server/src/routes/applicationRoutes.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { authMiddleware } from "../middleware/authMiddleware";
3 | import {
4 | createApplication,
5 | listApplications,
6 | updateApplicationStatus,
7 | } from "../controllers/applicationControllers";
8 |
9 | const router = express.Router();
10 |
11 | router.post("/", authMiddleware(["tenant"]), createApplication);
12 | router.put("/:id/status", authMiddleware(["manager"]), updateApplicationStatus);
13 | router.get("/", authMiddleware(["manager", "tenant"]), listApplications);
14 |
15 | export default router;
16 |
--------------------------------------------------------------------------------
/server/src/routes/leaseRoutes.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { authMiddleware } from "../middleware/authMiddleware";
3 | import { getLeasePayments, getLeases } from "../controllers/leaseControllers";
4 |
5 | const router = express.Router();
6 |
7 | router.get("/", authMiddleware(["manager", "tenant"]), getLeases);
8 | router.get(
9 | "/:id/payments",
10 | authMiddleware(["manager", "tenant"]),
11 | getLeasePayments
12 | );
13 |
14 | export default router;
15 |
--------------------------------------------------------------------------------
/server/src/routes/managerRoutes.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getManager,
4 | createManager,
5 | updateManager,
6 | getManagerProperties,
7 | } from "../controllers/managerControllers";
8 |
9 | const router = express.Router();
10 |
11 | router.get("/:cognitoId", getManager);
12 | router.put("/:cognitoId", updateManager);
13 | router.get("/:cognitoId/properties", getManagerProperties);
14 | router.post("/", createManager);
15 |
16 | export default router;
17 |
--------------------------------------------------------------------------------
/server/src/routes/propertyRoutes.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getProperties,
4 | getProperty,
5 | createProperty,
6 | } from "../controllers/propertyControllers";
7 | import multer from "multer";
8 | import { authMiddleware } from "../middleware/authMiddleware";
9 |
10 | const storage = multer.memoryStorage();
11 | const upload = multer({ storage: storage });
12 |
13 | const router = express.Router();
14 |
15 | router.get("/", getProperties);
16 | router.get("/:id", getProperty);
17 | router.post(
18 | "/",
19 | authMiddleware(["manager"]),
20 | upload.array("photos"),
21 | createProperty
22 | );
23 |
24 | export default router;
25 |
--------------------------------------------------------------------------------
/server/src/routes/tenantRoutes.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import {
3 | getTenant,
4 | createTenant,
5 | updateTenant,
6 | getCurrentResidences,
7 | addFavoriteProperty,
8 | removeFavoriteProperty,
9 | } from "../controllers/tenantControllers";
10 |
11 | const router = express.Router();
12 |
13 | router.get("/:cognitoId", getTenant);
14 | router.put("/:cognitoId", updateTenant);
15 | router.post("/", createTenant);
16 | router.get("/:cognitoId/current-residences", getCurrentResidences);
17 | router.post("/:cognitoId/favorites/:propertyId", addFavoriteProperty);
18 | router.delete("/:cognitoId/favorites/:propertyId", removeFavoriteProperty);
19 |
20 | export default router;
21 |
--------------------------------------------------------------------------------
|