Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@ model UserProfile {
UserFollow_UserFollow_followerIdToUserProfile UserFollow[] @relation("UserFollow_followerIdToUserProfile")
UserFollow_UserFollow_followingIdToUserProfile UserFollow[] @relation("UserFollow_followingIdToUserProfile")
event_rsvp event_rsvp[]
bookmarkedToWatch String[] @default([])
bookmarkedWatched String[] @default([])

@@schema("public")
}
Expand Down
13 changes: 13 additions & 0 deletions backend/prisma/seed.sql
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ INSERT INTO "public"."UserProfile" (
"favoriteMovies",
"createdAt",
"updatedAt",
"bookmarkedToWatch",
"bookmarkedWatched"
) VALUES
('11111111-1111-1111-1111-111111111111', 'alice_movie_fan', true, 'English', ARRAY['Spanish'], NULL, 'USA', 'New York', ARRAY['Drama', 'Thriller'], ARRAY['tt0111161', 'tt0068646'], NOW(), NOW(), ARRAY[]::text[], ARRAY['tt0073486', 'tt0099685']),
('22222222-2222-2222-2222-222222222222', 'bob_cineaste', true, 'English', ARRAY['French'], NULL, 'USA', 'Los Angeles', ARRAY['Action', 'Sci-Fi'], ARRAY['tt0468569', 'tt0137523'], NOW(), NOW(), ARRAY[]::text[], ARRAY['tt0468569', 'tt0137523']),
('33333333-3333-3333-3333-333333333333', 'charlie_critic', true, 'English', ARRAY[]::text[], NULL, 'Canada', 'Toronto', ARRAY['Comedy', 'Romance'], ARRAY['tt0109830', 'tt1375666'], NOW(), NOW(), ARRAY[]::text[], ARRAY['tt0109830', 'tt1375666']),
('44444444-4444-4444-4444-444444444444', 'diana_director', true, 'English', ARRAY['Italian'], NULL, 'Italy', 'Rome', ARRAY['Drama', 'Biography'], ARRAY['tt0073486', 'tt0099685'], NOW(), NOW(), ARRAY[]::text[], ARRAY['tt0073486', 'tt0099685']),
('55555555-5555-5555-5555-555555555555', 'evan_enthusiast', true, 'English', ARRAY['Japanese'], NULL, 'USA', 'San Francisco', ARRAY['Animation', 'Fantasy'], ARRAY['tt0245429', 'tt1853728'], NOW(), NOW(), ARRAY[]::text[], ARRAY['tt0245429', 'tt1853728']),
('66666666-6666-6666-6666-666666666666', 'fiona_film_buff', true, 'English', ARRAY['German'], NULL, 'Germany', 'Berlin', ARRAY['Horror', 'Mystery'], ARRAY['tt0816692', 'tt0110912'], NOW(), NOW(), ARRAY[]::text[], ARRAY['tt0816692', 'tt0110912']),
('77777777-7777-7777-7777-777777777777', 'george_genre_fan', true, 'English', ARRAY[]::text[], NULL, 'USA', 'Chicago', ARRAY['Western', 'Crime'], ARRAY['tt0076759', 'tt0050083'], NOW(), NOW(), ARRAY[]::text[], ARRAY['tt0076759', 'tt0050083']),
('88888888-8888-8888-8888-888888888888', 'hannah_hollywood', true, 'English', ARRAY['Korean'], NULL, 'South Korea', 'Seoul', ARRAY['Drama', 'Thriller'], ARRAY['tt6751668', 'tt0167260'], NOW(), NOW(), ARRAY[]::text[], ARRAY['tt6751668', 'tt0167260']),
('99999999-9999-9999-9999-999999999999', 'isaac_indie', true, 'English', ARRAY[]::text[], NULL, 'UK', 'London', ARRAY['Independent', 'Documentary'], ARRAY['tt0114369', 'tt0120737'], NOW(), NOW(), ARRAY[]::text[], ARRAY['tt0114369', 'tt0120737']),
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'julia_junkie', true, 'English', ARRAY['Portuguese'], NULL, 'Brazil', 'São Paulo', ARRAY['Drama', 'Romance'], ARRAY['tt0133093', 'tt0088763'], NOW(), NOW(), ARRAY[]::text[], ARRAY['tt0133093', 'tt0088763'])
"spoiler"
) VALUES
('11111111-1111-1111-1111-111111111111', 'alice_movie_fan', true, 'English', ARRAY['Spanish'], NULL, 'USA', 'New York', ARRAY['Drama', 'Thriller'], ARRAY['tt0111161', 'tt0068646'], NOW(), NOW(), false),
Expand Down
189 changes: 133 additions & 56 deletions backend/src/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,114 @@ import { prisma } from '../services/db.js';
import { Prisma } from "@prisma/client";
import { UserProfile } from "../types/models";

export const updateUserProfile = async (req: AuthenticatedRequest, res: Response) => {
const { user } = req;
if (!user) return res.status(401).json({ message: "Unauthorized" });

const {
username,
onboardingCompleted,
primaryLanguage,
secondaryLanguage,
profilePicture,
country,
city,
favoriteGenres,
favoriteMovies,
updatedAt,
privateAccount,
spoiler,
} = (req.body ?? {}) as Partial<UserProfile>;
import { prisma } from '../services/db'; // or wherever yours is
import { Prisma } from '@prisma/client';

// inside your handler:
export const updateUserProfile = async (req, res) => {
try {
const existingProfile = await prisma.userProfile.findUnique({
where: { userId: user.id },
});
// however you're getting this – body already validated / normalized
const body = req.body;

if (!existingProfile) {
return res.status(404).json({ message: "User profile not found" });
}
console.log('🟠 [BE] raw body in updateUserProfile:', body);

const mergedSecondaryLanguages = Array.isArray(secondaryLanguage)
? Array.from(
new Set([
...(Array.isArray(existingProfile.secondaryLanguage)
? (existingProfile.secondaryLanguage as string[])
: []),
...secondaryLanguage,
]),
)
: undefined;

const data = mapUserProfilePatchToUpdateData({
username,
onboardingCompleted,
primaryLanguage,
secondaryLanguage: mergedSecondaryLanguages,
profilePicture,
country,
city,
favoriteGenres,
favoriteMovies,
updatedAt,
privateAccount,
spoiler,
const normalized = {
username: body.username ?? null,
onboardingCompleted: body.onboardingCompleted,
primaryLanguage: body.primaryLanguage,
secondaryLanguage: Array.isArray(body.secondaryLanguage)
? body.secondaryLanguage
: [],
profilePicture: body.profilePicture,
country: body.country,
city: body.city,
favoriteGenres: Array.isArray(body.favoriteGenres)
? body.favoriteGenres
: [],
favoriteMovies: Array.isArray(body.favoriteMovies)
? body.favoriteMovies
: [],
privateAccount:
typeof body.privateAccount === 'boolean'
? body.privateAccount
: undefined,
spoiler:
typeof body.spoiler === 'boolean' ? body.spoiler : undefined,

// 🔥 IMPORTANT PART: keep arrays exactly as passed, just dedupe
bookmarkedToWatch: Array.isArray(body.bookmarkedToWatch)
? Array.from(new Set(body.bookmarkedToWatch))
: undefined,
bookmarkedWatched: Array.isArray(body.bookmarkedWatched)
? Array.from(new Set(body.bookmarkedWatched))
: undefined,
};

console.log('🔍 [BE] normalized body before prisma:', {
bookmarkedToWatch: normalized.bookmarkedToWatch,
bookmarkedWatched: normalized.bookmarkedWatched,
});

const updated = await prisma.userProfile.update({
where: { userId: user.id },
data,
const prismaData: Prisma.UserProfileUpdateInput = {
// only set fields that are explicitly provided (!== undefined)
...(normalized.username !== undefined && {
username: normalized.username,
}),
...(normalized.onboardingCompleted !== undefined && {
onboardingCompleted: normalized.onboardingCompleted,
}),
...(normalized.primaryLanguage !== undefined && {
primaryLanguage: normalized.primaryLanguage,
}),
...(normalized.secondaryLanguage !== undefined && {
secondaryLanguage: normalized.secondaryLanguage,
}),
...(normalized.profilePicture !== undefined && {
profilePicture: normalized.profilePicture,
}),
...(normalized.country !== undefined && { country: normalized.country }),
...(normalized.city !== undefined && { city: normalized.city }),
...(normalized.favoriteGenres !== undefined && {
favoriteGenres: normalized.favoriteGenres,
}),
...(normalized.favoriteMovies !== undefined && {
favoriteMovies: normalized.favoriteMovies,
}),
...(normalized.privateAccount !== undefined && {
privateAccount: normalized.privateAccount,
}),
...(normalized.spoiler !== undefined && { spoiler: normalized.spoiler }),

...(normalized.bookmarkedToWatch !== undefined && {
bookmarkedToWatch: normalized.bookmarkedToWatch,
}),
...(normalized.bookmarkedWatched !== undefined && {
bookmarkedWatched: normalized.bookmarkedWatched,
}),

updatedAt: new Date(),
};

console.log(
'🟡 [BE] updateUserProfile() prisma update data:',
prismaData
);

const userId = req.user.id; // or however you attach auth
const result = await prisma.userProfile.update({
where: { userId },
data: prismaData,
});

res.json({ message: "Profile updated", data: mapUserProfileDbToApi(updated) });
} catch (error) {
console.error("updateUserProfile error:", error);
res.status(500).json({ message: "Failed to update profile" });
console.log('🟢 [BE] updateUserProfile() prisma result:', result);

res.json({ userProfile: result });
} catch (err) {
console.error('🔴 [BE] updateUserProfile() error:', err);
res.status(500).json({ error: 'Failed to update profile' });
}
};


export const deleteUserProfile = async (req: AuthenticatedRequest, res: Response) => {
const { user } = req;
Expand Down Expand Up @@ -113,7 +156,10 @@ export const ensureUserProfile = async (req: AuthenticatedRequest, res: Response
privateAccount: false,
spoiler: false,
updatedAt: new Date(),
bookmarkedToWatch: [],
bookmarkedWatched: [],
},

});
}

Expand Down Expand Up @@ -176,6 +222,12 @@ export const getUserProfile = async (req: AuthenticatedRequest, res: Response) =
spoiler: Boolean(userProfile.spoiler),
createdAt: userProfile.createdAt,
updatedAt: userProfile.updatedAt,
bookmarkedToWatch: Array.isArray(userProfile.bookmarkedToWatch)
? userProfile.bookmarkedToWatch as string[]
: [],
bookmarkedWatched: Array.isArray(userProfile.bookmarkedWatched)
? userProfile.bookmarkedWatched as string[]
: [],
});

const basicUser = req.user
Expand Down Expand Up @@ -229,7 +281,6 @@ export const getUserRatings = async (req: Request, res: Response): Promise<void>
const ratings = await prisma.rating.findMany({
where: { userId: user_id },
orderBy: { date: "desc" },
include: { Comment: true },
});

// Fetch user profile
Expand Down Expand Up @@ -261,6 +312,12 @@ export const getUserRatings = async (req: Request, res: Response): Promise<void>
spoiler: Boolean(userProfile.spoiler),
createdAt: userProfile.createdAt,
updatedAt: userProfile.updatedAt,
bookmarkedToWatch: Array.isArray(userProfile.bookmarkedToWatch)
? userProfile.bookmarkedToWatch as string[]
: [],
bookmarkedWatched: Array.isArray(userProfile.bookmarkedWatched)
? userProfile.bookmarkedWatched as string[]
: [],
});
}

Expand Down Expand Up @@ -319,6 +376,12 @@ export const getUserComments = async (req: Request, res: Response): Promise<void
spoiler: Boolean(userProfile.spoiler),
createdAt: userProfile.createdAt,
updatedAt: userProfile.updatedAt,
bookmarkedToWatch: Array.isArray(userProfile.bookmarkedToWatch)
? userProfile.bookmarkedToWatch as string[]
: [],
bookmarkedWatched: Array.isArray(userProfile.bookmarkedWatched)
? userProfile.bookmarkedWatched as string[]
: [],
});
}

Expand Down Expand Up @@ -352,6 +415,8 @@ export function mapUserProfileDbToApi(row: {
spoiler?: boolean | null;
createdAt: Date;
updatedAt: Date;
bookmarkedToWatch: string[];
bookmarkedWatched: string[];
}): UserProfile {
return {
userId: row.userId,
Expand All @@ -368,6 +433,12 @@ export function mapUserProfileDbToApi(row: {
spoiler: Boolean(row.spoiler),
createdAt: row.createdAt,
updatedAt: row.updatedAt,
bookmarkedToWatch: Array.isArray(row.bookmarkedToWatch)
? row.bookmarkedToWatch as string[]
: [],
bookmarkedWatched: Array.isArray(row.bookmarkedWatched)
? row.bookmarkedWatched as string[]
: [],
};
}

Expand Down Expand Up @@ -412,6 +483,12 @@ export function mapUserProfilePatchToUpdateData(
if (Object.prototype.hasOwnProperty.call(patch, "spoiler")) {
data.spoiler = patch.spoiler ?? false;
}
if (Object.prototype.hasOwnProperty.call(patch, "bookmarkedToWatch")) {
data.bookmarkedToWatch = patch.bookmarkedToWatch ?? [];
}
if (Object.prototype.hasOwnProperty.call(patch, "bookmarkedWatched")) {
data.bookmarkedWatched = patch.bookmarkedWatched ?? [];
}

// Always refresh updatedAt to now unless caller explicitly provided one
data.updatedAt = patch.updatedAt ?? new Date();
Expand Down
12 changes: 8 additions & 4 deletions backend/src/tests/api/users.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ describe("User Profile API Tests", () => {
.expect(HTTP_STATUS.OK);

// Verify profile was created and updated
expect(res.body.data).toMatchObject({
expect(res.body.userProfile).toMatchObject({
username: "newuser",
country: "USA",
onboardingCompleted: true,
Expand Down Expand Up @@ -250,9 +250,13 @@ describe("User Profile API Tests", () => {
.set(authHeader())
.expect(HTTP_STATUS.OK);

expect(res.body).toHaveProperty("message", "Profile updated");
expect(res.body).toHaveProperty("data");
expect(res.body.data).toMatchObject(payload);
expect(res.body).toHaveProperty("userProfile");
expect(res.body.userProfile).toMatchObject({
username: payload.username,
secondaryLanguage: payload.secondaryLanguage,
favoriteGenres: payload.favoriteGenres,
favoriteMovies: payload.favoriteMovies
});
});
});

Expand Down
2 changes: 2 additions & 0 deletions backend/src/types/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export type UpdateUserProfileInput = {
favoriteMovies?: string[];
privateAccount?: boolean;
spoiler?: boolean;
bookmarkedToWatch?: string[];
bookmarkedWatched?: string[];
};

export type UpdateUserProfileResponse = { message: string; data: UserProfile };
Expand Down
2 changes: 2 additions & 0 deletions backend/src/types/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export type UserProfile = {
spoiler: boolean;
createdAt: Date;
updatedAt: Date;
bookmarkedToWatch: string[];
bookmarkedWatched: string[];
};

export type Rating = {
Expand Down
Loading