diff --git a/src/app/weekly-checkin/[id]/page.tsx b/src/app/weekly-checkin/[id]/page.tsx
index 963137e..bcf7ac2 100644
--- a/src/app/weekly-checkin/[id]/page.tsx
+++ b/src/app/weekly-checkin/[id]/page.tsx
@@ -62,7 +62,7 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn
diff --git a/src/app/year-review/[id]/page.tsx b/src/app/year-review/[id]/page.tsx
index 3c55377..66ef024 100644
--- a/src/app/year-review/[id]/page.tsx
+++ b/src/app/year-review/[id]/page.tsx
@@ -47,7 +47,7 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio
diff --git a/src/components/ui/EditableMotivatorTitle.tsx b/src/components/ui/EditableMotivatorTitle.tsx
index 8f765fe..d989f50 100644
--- a/src/components/ui/EditableMotivatorTitle.tsx
+++ b/src/components/ui/EditableMotivatorTitle.tsx
@@ -6,19 +6,19 @@ import { updateMotivatorSession } from '@/actions/moving-motivators';
interface EditableMotivatorTitleProps {
sessionId: string;
initialTitle: string;
- isOwner: boolean;
+ canEdit: boolean;
}
export function EditableMotivatorTitle({
sessionId,
initialTitle,
- isOwner,
+ canEdit,
}: EditableMotivatorTitleProps) {
return (
{
const result = await updateMotivatorSession(id, { title });
return result;
diff --git a/src/components/ui/EditableSessionTitle.tsx b/src/components/ui/EditableSessionTitle.tsx
index c9df8c6..d5612e7 100644
--- a/src/components/ui/EditableSessionTitle.tsx
+++ b/src/components/ui/EditableSessionTitle.tsx
@@ -6,19 +6,19 @@ import { updateSessionTitle } from '@/actions/session';
interface EditableSessionTitleProps {
sessionId: string;
initialTitle: string;
- isOwner: boolean;
+ canEdit: boolean;
}
export function EditableSessionTitle({
sessionId,
initialTitle,
- isOwner,
+ canEdit,
}: EditableSessionTitleProps) {
return (
{
const result = await updateSessionTitle(id, title);
return result;
diff --git a/src/components/ui/EditableTitle.tsx b/src/components/ui/EditableTitle.tsx
index 5799d35..4d05ec3 100644
--- a/src/components/ui/EditableTitle.tsx
+++ b/src/components/ui/EditableTitle.tsx
@@ -5,14 +5,14 @@ import { useState, useTransition, useRef, useEffect, useMemo } from 'react';
interface EditableTitleProps {
sessionId: string;
initialTitle: string;
- isOwner: boolean;
+ canEdit: boolean;
onUpdate: (sessionId: string, title: string) => Promise<{ success: boolean; error?: string }>;
}
export function EditableTitle({
sessionId,
initialTitle,
- isOwner,
+ canEdit,
onUpdate,
}: EditableTitleProps) {
const [isEditing, setIsEditing] = useState(false);
@@ -65,7 +65,7 @@ export function EditableTitle({
}
};
- if (!isOwner) {
+ if (!canEdit) {
return {title}
;
}
diff --git a/src/components/ui/EditableWeatherTitle.tsx b/src/components/ui/EditableWeatherTitle.tsx
index 72c9018..83890db 100644
--- a/src/components/ui/EditableWeatherTitle.tsx
+++ b/src/components/ui/EditableWeatherTitle.tsx
@@ -6,19 +6,19 @@ import { updateWeatherSession } from '@/actions/weather';
interface EditableWeatherTitleProps {
sessionId: string;
initialTitle: string;
- isOwner: boolean;
+ canEdit: boolean;
}
export function EditableWeatherTitle({
sessionId,
initialTitle,
- isOwner,
+ canEdit,
}: EditableWeatherTitleProps) {
return (
{
const result = await updateWeatherSession(id, { title });
return result;
diff --git a/src/components/ui/EditableWeeklyCheckInTitle.tsx b/src/components/ui/EditableWeeklyCheckInTitle.tsx
index 3725393..995cad1 100644
--- a/src/components/ui/EditableWeeklyCheckInTitle.tsx
+++ b/src/components/ui/EditableWeeklyCheckInTitle.tsx
@@ -6,19 +6,19 @@ import { updateWeeklyCheckInSession } from '@/actions/weekly-checkin';
interface EditableWeeklyCheckInTitleProps {
sessionId: string;
initialTitle: string;
- isOwner: boolean;
+ canEdit: boolean;
}
export function EditableWeeklyCheckInTitle({
sessionId,
initialTitle,
- isOwner,
+ canEdit,
}: EditableWeeklyCheckInTitleProps) {
return (
{
const result = await updateWeeklyCheckInSession(id, { title });
return result;
diff --git a/src/components/ui/EditableYearReviewTitle.tsx b/src/components/ui/EditableYearReviewTitle.tsx
index 3dd887b..1d0a7a0 100644
--- a/src/components/ui/EditableYearReviewTitle.tsx
+++ b/src/components/ui/EditableYearReviewTitle.tsx
@@ -6,19 +6,19 @@ import { updateYearReviewSession } from '@/actions/year-review';
interface EditableYearReviewTitleProps {
sessionId: string;
initialTitle: string;
- isOwner: boolean;
+ canEdit: boolean;
}
export function EditableYearReviewTitle({
sessionId,
initialTitle,
- isOwner,
+ canEdit,
}: EditableYearReviewTitleProps) {
return (
{
const result = await updateYearReviewSession(id, { title });
return result;
diff --git a/src/services/moving-motivators.ts b/src/services/moving-motivators.ts
index bafab3f..892e713 100644
--- a/src/services/moving-motivators.ts
+++ b/src/services/moving-motivators.ts
@@ -1,6 +1,6 @@
import { prisma } from '@/services/database';
import { resolveCollaborator } from '@/services/auth';
-import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
+import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
import type { ShareRole, MotivatorType } from '@prisma/client';
// ============================================
@@ -104,6 +104,7 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
isOwner: false as const,
role: 'VIEWER' as const,
isTeamCollab: true as const,
+ canEdit: true as const, // Admin has full rights on team member sessions
}));
return Promise.all(
@@ -115,8 +116,8 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
}
export async function getMotivatorSessionById(sessionId: string, userId: string) {
- // Check if user owns the session OR has it shared
- const session = await prisma.movingMotivatorsSession.findFirst({
+ // Check if user owns the session, has it shared, or is team admin of owner
+ let session = await prisma.movingMotivatorsSession.findFirst({
where: {
id: sessionId,
OR: [
@@ -137,13 +138,25 @@ export async function getMotivatorSessionById(sessionId: string, userId: string)
},
});
- if (!session) return null;
+ if (!session) {
+ const raw = await prisma.movingMotivatorsSession.findUnique({
+ where: { id: sessionId },
+ include: {
+ user: { select: { id: true, name: true, email: true } },
+ cards: { orderBy: { orderIndex: 'asc' } },
+ shares: { include: { user: { select: { id: true, name: true, email: true } } } },
+ },
+ });
+ if (!raw || !(await isAdminOfUser(raw.userId, userId))) return null;
+ session = raw;
+ }
// Determine user's role
const isOwner = session.userId === userId;
const share = session.shares.find((s) => s.userId === userId);
+ const isAdminOfOwner = !isOwner && !share && (await isAdminOfUser(session.userId, userId));
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
- const canEdit = isOwner || role === 'EDITOR';
+ const canEdit = isOwner || role === 'EDITOR' || isAdminOfOwner;
// Resolve participant to user if it's an email
const resolvedParticipant = await resolveCollaborator(session.participant);
@@ -151,7 +164,7 @@ export async function getMotivatorSessionById(sessionId: string, userId: string)
return { ...session, isOwner, role, canEdit, resolvedParticipant };
}
-// Check if user can access session (owner or shared)
+// Check if user can access session (owner, shared, or team admin of owner)
export async function canAccessMotivatorSession(sessionId: string, userId: string) {
const count = await prisma.movingMotivatorsSession.count({
where: {
@@ -159,10 +172,15 @@ export async function canAccessMotivatorSession(sessionId: string, userId: strin
OR: [{ userId }, { shares: { some: { userId } } }],
},
});
- return count > 0;
+ if (count > 0) return true;
+ const session = await prisma.movingMotivatorsSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ return session ? isAdminOfUser(session.userId, userId) : false;
}
-// Check if user can edit session (owner or EDITOR role)
+// Check if user can edit session (owner, EDITOR role, or team admin of owner)
export async function canEditMotivatorSession(sessionId: string, userId: string) {
const count = await prisma.movingMotivatorsSession.count({
where: {
@@ -170,7 +188,22 @@ export async function canEditMotivatorSession(sessionId: string, userId: string)
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
},
});
- return count > 0;
+ if (count > 0) return true;
+ const session = await prisma.movingMotivatorsSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ return session ? isAdminOfUser(session.userId, userId) : false;
+}
+
+// Check if user can delete session (owner or team admin only - NOT EDITOR)
+export async function canDeleteMotivatorSession(sessionId: string, userId: string) {
+ const session = await prisma.movingMotivatorsSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ if (!session) return false;
+ return session.userId === userId || isAdminOfUser(session.userId, userId);
}
const DEFAULT_MOTIVATOR_TYPES: MotivatorType[] = [
@@ -216,15 +249,21 @@ export async function updateMotivatorSession(
userId: string,
data: { title?: string; participant?: string }
) {
+ if (!(await canEditMotivatorSession(sessionId, userId))) {
+ return { count: 0 };
+ }
return prisma.movingMotivatorsSession.updateMany({
- where: { id: sessionId, userId },
+ where: { id: sessionId },
data,
});
}
export async function deleteMotivatorSession(sessionId: string, userId: string) {
+ if (!(await canDeleteMotivatorSession(sessionId, userId))) {
+ return { count: 0 };
+ }
return prisma.movingMotivatorsSession.deleteMany({
- where: { id: sessionId, userId },
+ where: { id: sessionId },
});
}
diff --git a/src/services/sessions.ts b/src/services/sessions.ts
index 566dd9b..a1a6611 100644
--- a/src/services/sessions.ts
+++ b/src/services/sessions.ts
@@ -1,6 +1,6 @@
import { prisma } from '@/services/database';
import { resolveCollaborator } from '@/services/auth';
-import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
+import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
import type { SwotCategory, ShareRole } from '@prisma/client';
// ============================================
@@ -111,6 +111,7 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
isOwner: false as const,
role: 'VIEWER' as const,
isTeamCollab: true as const,
+ canEdit: true as const, // Admin has full rights on team member sessions
}));
return Promise.all(
@@ -122,8 +123,8 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
}
export async function getSessionById(sessionId: string, userId: string) {
- // Check if user owns the session OR has it shared
- const session = await prisma.session.findFirst({
+ // Check if user owns the session, has it shared, or is team admin of owner
+ let session = await prisma.session.findFirst({
where: {
id: sessionId,
OR: [
@@ -154,13 +155,30 @@ export async function getSessionById(sessionId: string, userId: string) {
},
});
- if (!session) return null;
+ if (!session) {
+ // Fallback: team admin viewing team member's session
+ const raw = await prisma.session.findUnique({
+ where: { id: sessionId },
+ include: {
+ user: { select: { id: true, name: true, email: true } },
+ items: { orderBy: { order: 'asc' } },
+ actions: {
+ include: { links: { include: { swotItem: true } } },
+ orderBy: { createdAt: 'asc' },
+ },
+ shares: { include: { user: { select: { id: true, name: true, email: true } } } },
+ },
+ });
+ if (!raw || !(await isAdminOfUser(raw.userId, userId))) return null;
+ session = raw;
+ }
// Determine user's role
const isOwner = session.userId === userId;
const share = session.shares.find((s) => s.userId === userId);
+ const isAdminOfOwner = !isOwner && !share && (await isAdminOfUser(session.userId, userId));
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
- const canEdit = isOwner || role === 'EDITOR';
+ const canEdit = isOwner || role === 'EDITOR' || isAdminOfOwner;
// Resolve collaborator to user if it's an email
const resolvedCollaborator = await resolveCollaborator(session.collaborator);
@@ -168,7 +186,7 @@ export async function getSessionById(sessionId: string, userId: string) {
return { ...session, isOwner, role, canEdit, resolvedCollaborator };
}
-// Check if user can access session (owner or shared)
+// Check if user can access session (owner, shared, or team admin of owner)
export async function canAccessSession(sessionId: string, userId: string) {
const count = await prisma.session.count({
where: {
@@ -176,10 +194,15 @@ export async function canAccessSession(sessionId: string, userId: string) {
OR: [{ userId }, { shares: { some: { userId } } }],
},
});
- return count > 0;
+ if (count > 0) return true;
+ const session = await prisma.session.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ return session ? isAdminOfUser(session.userId, userId) : false;
}
-// Check if user can edit session (owner or EDITOR role)
+// Check if user can edit session (owner, EDITOR role, or team admin of owner)
export async function canEditSession(sessionId: string, userId: string) {
const count = await prisma.session.count({
where: {
@@ -187,7 +210,22 @@ export async function canEditSession(sessionId: string, userId: string) {
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
},
});
- return count > 0;
+ if (count > 0) return true;
+ const session = await prisma.session.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ return session ? isAdminOfUser(session.userId, userId) : false;
+}
+
+// Check if user can delete session (owner or team admin only - NOT EDITOR)
+export async function canDeleteSession(sessionId: string, userId: string) {
+ const session = await prisma.session.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ if (!session) return false;
+ return session.userId === userId || isAdminOfUser(session.userId, userId);
}
export async function createSession(userId: string, data: { title: string; collaborator: string }) {
@@ -204,15 +242,21 @@ export async function updateSession(
userId: string,
data: { title?: string; collaborator?: string }
) {
+ if (!(await canEditSession(sessionId, userId))) {
+ return { count: 0 };
+ }
return prisma.session.updateMany({
- where: { id: sessionId, userId },
+ where: { id: sessionId },
data,
});
}
export async function deleteSession(sessionId: string, userId: string) {
+ if (!(await canDeleteSession(sessionId, userId))) {
+ return { count: 0 };
+ }
return prisma.session.deleteMany({
- where: { id: sessionId, userId },
+ where: { id: sessionId },
});
}
diff --git a/src/services/teams.ts b/src/services/teams.ts
index 5c3f753..5eecdc7 100644
--- a/src/services/teams.ts
+++ b/src/services/teams.ts
@@ -244,6 +244,13 @@ export async function getTeamMember(teamId: string, userId: string) {
});
}
+/** Returns true if adminUserId is ADMIN of any team that contains ownerUserId. */
+export async function isAdminOfUser(ownerUserId: string, adminUserId: string): Promise {
+ if (ownerUserId === adminUserId) return false;
+ const teamMemberIds = await getTeamMemberIdsForAdminTeams(adminUserId);
+ return teamMemberIds.includes(ownerUserId);
+}
+
/** Returns user IDs of all members in teams where the given user is ADMIN (excluding self). */
export async function getTeamMemberIdsForAdminTeams(userId: string): Promise {
const adminTeams = await prisma.teamMember.findMany({
diff --git a/src/services/weather.ts b/src/services/weather.ts
index a95e73a..cc7159e 100644
--- a/src/services/weather.ts
+++ b/src/services/weather.ts
@@ -1,5 +1,5 @@
import { prisma } from '@/services/database';
-import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
+import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
import { getWeekBounds } from '@/lib/date-utils';
import type { ShareRole } from '@prisma/client';
@@ -96,12 +96,13 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
isOwner: false as const,
role: 'VIEWER' as const,
isTeamCollab: true as const,
+ canEdit: true as const, // Admin has full rights on team member sessions
}));
}
export async function getWeatherSessionById(sessionId: string, userId: string) {
- // Check if user owns the session OR has it shared
- const session = await prisma.weatherSession.findFirst({
+ // Check if user owns the session, has it shared, or is team admin of owner
+ let session = await prisma.weatherSession.findFirst({
where: {
id: sessionId,
OR: [
@@ -125,18 +126,33 @@ export async function getWeatherSessionById(sessionId: string, userId: string) {
},
});
- if (!session) return null;
+ if (!session) {
+ const raw = await prisma.weatherSession.findUnique({
+ where: { id: sessionId },
+ include: {
+ user: { select: { id: true, name: true, email: true } },
+ entries: {
+ include: { user: { select: { id: true, name: true, email: true } } },
+ orderBy: { createdAt: 'asc' },
+ },
+ shares: { include: { user: { select: { id: true, name: true, email: true } } } },
+ },
+ });
+ if (!raw || !(await isAdminOfUser(raw.userId, userId))) return null;
+ session = raw;
+ }
// Determine user's role
const isOwner = session.userId === userId;
const share = session.shares.find((s) => s.userId === userId);
+ const isAdminOfOwner = !isOwner && !share && (await isAdminOfUser(session.userId, userId));
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
- const canEdit = isOwner || role === 'EDITOR';
+ const canEdit = isOwner || role === 'EDITOR' || isAdminOfOwner;
return { ...session, isOwner, role, canEdit };
}
-// Check if user can access session (owner or shared)
+// Check if user can access session (owner, shared, or team admin of owner)
export async function canAccessWeatherSession(sessionId: string, userId: string) {
const count = await prisma.weatherSession.count({
where: {
@@ -144,10 +160,15 @@ export async function canAccessWeatherSession(sessionId: string, userId: string)
OR: [{ userId }, { shares: { some: { userId } } }],
},
});
- return count > 0;
+ if (count > 0) return true;
+ const session = await prisma.weatherSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ return session ? isAdminOfUser(session.userId, userId) : false;
}
-// Check if user can edit session (owner or EDITOR role)
+// Check if user can edit session (owner, EDITOR role, or team admin of owner)
export async function canEditWeatherSession(sessionId: string, userId: string) {
const count = await prisma.weatherSession.count({
where: {
@@ -155,7 +176,22 @@ export async function canEditWeatherSession(sessionId: string, userId: string) {
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
},
});
- return count > 0;
+ if (count > 0) return true;
+ const session = await prisma.weatherSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ return session ? isAdminOfUser(session.userId, userId) : false;
+}
+
+// Check if user can delete session (owner or team admin only - NOT EDITOR)
+export async function canDeleteWeatherSession(sessionId: string, userId: string) {
+ const session = await prisma.weatherSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ if (!session) return false;
+ return session.userId === userId || isAdminOfUser(session.userId, userId);
}
export async function createWeatherSession(userId: string, data: { title: string; date?: Date }) {
@@ -180,15 +216,21 @@ export async function updateWeatherSession(
userId: string,
data: { title?: string; date?: Date }
) {
+ if (!(await canEditWeatherSession(sessionId, userId))) {
+ return { count: 0 };
+ }
return prisma.weatherSession.updateMany({
- where: { id: sessionId, userId },
+ where: { id: sessionId },
data,
});
}
export async function deleteWeatherSession(sessionId: string, userId: string) {
+ if (!(await canDeleteWeatherSession(sessionId, userId))) {
+ return { count: 0 };
+ }
return prisma.weatherSession.deleteMany({
- where: { id: sessionId, userId },
+ where: { id: sessionId },
});
}
diff --git a/src/services/weekly-checkin.ts b/src/services/weekly-checkin.ts
index 337c5e3..829b510 100644
--- a/src/services/weekly-checkin.ts
+++ b/src/services/weekly-checkin.ts
@@ -1,6 +1,6 @@
import { prisma } from '@/services/database';
import { resolveCollaborator } from '@/services/auth';
-import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
+import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
import type { ShareRole, WeeklyCheckInCategory, Emotion } from '@prisma/client';
// ============================================
@@ -104,6 +104,7 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
isOwner: false as const,
role: 'VIEWER' as const,
isTeamCollab: true as const,
+ canEdit: true as const, // Admin has full rights on team member sessions
}));
return Promise.all(
@@ -115,8 +116,8 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
}
export async function getWeeklyCheckInSessionById(sessionId: string, userId: string) {
- // Check if user owns the session OR has it shared
- const session = await prisma.weeklyCheckInSession.findFirst({
+ // Check if user owns the session, has it shared, or is team admin of owner
+ let session = await prisma.weeklyCheckInSession.findFirst({
where: {
id: sessionId,
OR: [
@@ -137,13 +138,25 @@ export async function getWeeklyCheckInSessionById(sessionId: string, userId: str
},
});
- if (!session) return null;
+ if (!session) {
+ const raw = await prisma.weeklyCheckInSession.findUnique({
+ where: { id: sessionId },
+ include: {
+ user: { select: { id: true, name: true, email: true } },
+ items: { orderBy: [{ category: 'asc' }, { order: 'asc' }] },
+ shares: { include: { user: { select: { id: true, name: true, email: true } } } },
+ },
+ });
+ if (!raw || !(await isAdminOfUser(raw.userId, userId))) return null;
+ session = raw;
+ }
// Determine user's role
const isOwner = session.userId === userId;
const share = session.shares.find((s) => s.userId === userId);
+ const isAdminOfOwner = !isOwner && !share && (await isAdminOfUser(session.userId, userId));
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
- const canEdit = isOwner || role === 'EDITOR';
+ const canEdit = isOwner || role === 'EDITOR' || isAdminOfOwner;
// Resolve participant to user if it's an email
const resolvedParticipant = await resolveCollaborator(session.participant);
@@ -151,7 +164,7 @@ export async function getWeeklyCheckInSessionById(sessionId: string, userId: str
return { ...session, isOwner, role, canEdit, resolvedParticipant };
}
-// Check if user can access session (owner or shared)
+// Check if user can access session (owner, shared, or team admin of owner)
export async function canAccessWeeklyCheckInSession(sessionId: string, userId: string) {
const count = await prisma.weeklyCheckInSession.count({
where: {
@@ -159,10 +172,15 @@ export async function canAccessWeeklyCheckInSession(sessionId: string, userId: s
OR: [{ userId }, { shares: { some: { userId } } }],
},
});
- return count > 0;
+ if (count > 0) return true;
+ const session = await prisma.weeklyCheckInSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ return session ? isAdminOfUser(session.userId, userId) : false;
}
-// Check if user can edit session (owner or EDITOR role)
+// Check if user can edit session (owner, EDITOR role, or team admin of owner)
export async function canEditWeeklyCheckInSession(sessionId: string, userId: string) {
const count = await prisma.weeklyCheckInSession.count({
where: {
@@ -170,7 +188,22 @@ export async function canEditWeeklyCheckInSession(sessionId: string, userId: str
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
},
});
- return count > 0;
+ if (count > 0) return true;
+ const session = await prisma.weeklyCheckInSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ return session ? isAdminOfUser(session.userId, userId) : false;
+}
+
+// Check if user can delete session (owner or team admin only - NOT EDITOR)
+export async function canDeleteWeeklyCheckInSession(sessionId: string, userId: string) {
+ const session = await prisma.weeklyCheckInSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ if (!session) return false;
+ return session.userId === userId || isAdminOfUser(session.userId, userId);
}
export async function createWeeklyCheckInSession(
@@ -196,15 +229,21 @@ export async function updateWeeklyCheckInSession(
userId: string,
data: { title?: string; participant?: string; date?: Date }
) {
+ if (!(await canEditWeeklyCheckInSession(sessionId, userId))) {
+ return { count: 0 };
+ }
return prisma.weeklyCheckInSession.updateMany({
- where: { id: sessionId, userId },
+ where: { id: sessionId },
data,
});
}
export async function deleteWeeklyCheckInSession(sessionId: string, userId: string) {
+ if (!(await canDeleteWeeklyCheckInSession(sessionId, userId))) {
+ return { count: 0 };
+ }
return prisma.weeklyCheckInSession.deleteMany({
- where: { id: sessionId, userId },
+ where: { id: sessionId },
});
}
diff --git a/src/services/year-review.ts b/src/services/year-review.ts
index 40a187b..b0afa9a 100644
--- a/src/services/year-review.ts
+++ b/src/services/year-review.ts
@@ -1,6 +1,6 @@
import { prisma } from '@/services/database';
import { resolveCollaborator } from '@/services/auth';
-import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
+import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
import type { ShareRole, YearReviewCategory } from '@prisma/client';
// ============================================
@@ -104,6 +104,7 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
isOwner: false as const,
role: 'VIEWER' as const,
isTeamCollab: true as const,
+ canEdit: true as const, // Admin has full rights on team member sessions
}));
return Promise.all(
@@ -115,8 +116,8 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
}
export async function getYearReviewSessionById(sessionId: string, userId: string) {
- // Check if user owns the session OR has it shared
- const session = await prisma.yearReviewSession.findFirst({
+ // Check if user owns the session, has it shared, or is team admin of owner
+ let session = await prisma.yearReviewSession.findFirst({
where: {
id: sessionId,
OR: [
@@ -137,13 +138,25 @@ export async function getYearReviewSessionById(sessionId: string, userId: string
},
});
- if (!session) return null;
+ if (!session) {
+ const raw = await prisma.yearReviewSession.findUnique({
+ where: { id: sessionId },
+ include: {
+ user: { select: { id: true, name: true, email: true } },
+ items: { orderBy: [{ category: 'asc' }, { order: 'asc' }] },
+ shares: { include: { user: { select: { id: true, name: true, email: true } } } },
+ },
+ });
+ if (!raw || !(await isAdminOfUser(raw.userId, userId))) return null;
+ session = raw;
+ }
// Determine user's role
const isOwner = session.userId === userId;
const share = session.shares.find((s) => s.userId === userId);
+ const isAdminOfOwner = !isOwner && !share && (await isAdminOfUser(session.userId, userId));
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
- const canEdit = isOwner || role === 'EDITOR';
+ const canEdit = isOwner || role === 'EDITOR' || isAdminOfOwner;
// Resolve participant to user if it's an email
const resolvedParticipant = await resolveCollaborator(session.participant);
@@ -151,7 +164,7 @@ export async function getYearReviewSessionById(sessionId: string, userId: string
return { ...session, isOwner, role, canEdit, resolvedParticipant };
}
-// Check if user can access session (owner or shared)
+// Check if user can access session (owner, shared, or team admin of owner)
export async function canAccessYearReviewSession(sessionId: string, userId: string) {
const count = await prisma.yearReviewSession.count({
where: {
@@ -159,10 +172,15 @@ export async function canAccessYearReviewSession(sessionId: string, userId: stri
OR: [{ userId }, { shares: { some: { userId } } }],
},
});
- return count > 0;
+ if (count > 0) return true;
+ const session = await prisma.yearReviewSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ return session ? isAdminOfUser(session.userId, userId) : false;
}
-// Check if user can edit session (owner or EDITOR role)
+// Check if user can edit session (owner, EDITOR role, or team admin of owner)
export async function canEditYearReviewSession(sessionId: string, userId: string) {
const count = await prisma.yearReviewSession.count({
where: {
@@ -170,7 +188,22 @@ export async function canEditYearReviewSession(sessionId: string, userId: string
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
},
});
- return count > 0;
+ if (count > 0) return true;
+ const session = await prisma.yearReviewSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ return session ? isAdminOfUser(session.userId, userId) : false;
+}
+
+// Check if user can delete session (owner or team admin only - NOT EDITOR)
+export async function canDeleteYearReviewSession(sessionId: string, userId: string) {
+ const session = await prisma.yearReviewSession.findUnique({
+ where: { id: sessionId },
+ select: { userId: true },
+ });
+ if (!session) return false;
+ return session.userId === userId || isAdminOfUser(session.userId, userId);
}
export async function createYearReviewSession(
@@ -195,15 +228,21 @@ export async function updateYearReviewSession(
userId: string,
data: { title?: string; participant?: string; year?: number }
) {
+ if (!(await canEditYearReviewSession(sessionId, userId))) {
+ return { count: 0 };
+ }
return prisma.yearReviewSession.updateMany({
- where: { id: sessionId, userId },
+ where: { id: sessionId },
data,
});
}
export async function deleteYearReviewSession(sessionId: string, userId: string) {
+ if (!(await canDeleteYearReviewSession(sessionId, userId))) {
+ return { count: 0 };
+ }
return prisma.yearReviewSession.deleteMany({
- where: { id: sessionId, userId },
+ where: { id: sessionId },
});
}