feat: enhance session management by implementing edit permissions for team admins and updating session components to reflect new access controls
This commit is contained in:
@@ -17,6 +17,9 @@ export async function createSwotItem(
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Non autorisé' };
|
return { success: false, error: 'Non autorisé' };
|
||||||
}
|
}
|
||||||
|
if (!(await sessionsService.canEditSession(sessionId, session.user.id))) {
|
||||||
|
return { success: false, error: 'Non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const item = await sessionsService.createSwotItem(sessionId, data);
|
const item = await sessionsService.createSwotItem(sessionId, data);
|
||||||
@@ -45,6 +48,9 @@ export async function updateSwotItem(
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Non autorisé' };
|
return { success: false, error: 'Non autorisé' };
|
||||||
}
|
}
|
||||||
|
if (!(await sessionsService.canEditSession(sessionId, session.user.id))) {
|
||||||
|
return { success: false, error: 'Non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const item = await sessionsService.updateSwotItem(itemId, data);
|
const item = await sessionsService.updateSwotItem(itemId, data);
|
||||||
@@ -68,6 +74,9 @@ export async function deleteSwotItem(itemId: string, sessionId: string) {
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Non autorisé' };
|
return { success: false, error: 'Non autorisé' };
|
||||||
}
|
}
|
||||||
|
if (!(await sessionsService.canEditSession(sessionId, session.user.id))) {
|
||||||
|
return { success: false, error: 'Non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sessionsService.deleteSwotItem(itemId);
|
await sessionsService.deleteSwotItem(itemId);
|
||||||
@@ -90,6 +99,9 @@ export async function duplicateSwotItem(itemId: string, sessionId: string) {
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Non autorisé' };
|
return { success: false, error: 'Non autorisé' };
|
||||||
}
|
}
|
||||||
|
if (!(await sessionsService.canEditSession(sessionId, session.user.id))) {
|
||||||
|
return { success: false, error: 'Non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const item = await sessionsService.duplicateSwotItem(itemId);
|
const item = await sessionsService.duplicateSwotItem(itemId);
|
||||||
@@ -120,6 +132,9 @@ export async function moveSwotItem(
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Non autorisé' };
|
return { success: false, error: 'Non autorisé' };
|
||||||
}
|
}
|
||||||
|
if (!(await sessionsService.canEditSession(sessionId, session.user.id))) {
|
||||||
|
return { success: false, error: 'Non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const item = await sessionsService.moveSwotItem(itemId, newCategory, newOrder);
|
const item = await sessionsService.moveSwotItem(itemId, newCategory, newOrder);
|
||||||
@@ -156,6 +171,9 @@ export async function createAction(
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Non autorisé' };
|
return { success: false, error: 'Non autorisé' };
|
||||||
}
|
}
|
||||||
|
if (!(await sessionsService.canEditSession(sessionId, session.user.id))) {
|
||||||
|
return { success: false, error: 'Non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const action = await sessionsService.createAction(sessionId, data);
|
const action = await sessionsService.createAction(sessionId, data);
|
||||||
@@ -190,6 +208,9 @@ export async function updateAction(
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Non autorisé' };
|
return { success: false, error: 'Non autorisé' };
|
||||||
}
|
}
|
||||||
|
if (!(await sessionsService.canEditSession(sessionId, session.user.id))) {
|
||||||
|
return { success: false, error: 'Non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const action = await sessionsService.updateAction(actionId, data);
|
const action = await sessionsService.updateAction(actionId, data);
|
||||||
@@ -213,6 +234,9 @@ export async function deleteAction(actionId: string, sessionId: string) {
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Non autorisé' };
|
return { success: false, error: 'Non autorisé' };
|
||||||
}
|
}
|
||||||
|
if (!(await sessionsService.canEditSession(sessionId, session.user.id))) {
|
||||||
|
return { success: false, error: 'Non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sessionsService.deleteAction(actionId);
|
await sessionsService.deleteAction(actionId);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP
|
|||||||
<EditableMotivatorTitle
|
<EditableMotivatorTitle
|
||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
initialTitle={session.title}
|
initialTitle={session.title}
|
||||||
isOwner={session.isOwner}
|
canEdit={session.canEdit}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<CollaboratorDisplay collaborator={session.resolvedParticipant} size="lg" showEmail />
|
<CollaboratorDisplay collaborator={session.resolvedParticipant} size="lg" showEmail />
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ interface SwotSession {
|
|||||||
shares: Share[];
|
shares: Share[];
|
||||||
_count: { items: number; actions: number };
|
_count: { items: number; actions: number };
|
||||||
workshopType: 'swot';
|
workshopType: 'swot';
|
||||||
|
isTeamCollab?: true;
|
||||||
|
canEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MotivatorSession {
|
interface MotivatorSession {
|
||||||
@@ -79,6 +81,8 @@ interface MotivatorSession {
|
|||||||
shares: Share[];
|
shares: Share[];
|
||||||
_count: { cards: number };
|
_count: { cards: number };
|
||||||
workshopType: 'motivators';
|
workshopType: 'motivators';
|
||||||
|
isTeamCollab?: true;
|
||||||
|
canEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface YearReviewSession {
|
interface YearReviewSession {
|
||||||
@@ -94,6 +98,8 @@ interface YearReviewSession {
|
|||||||
shares: Share[];
|
shares: Share[];
|
||||||
_count: { items: number };
|
_count: { items: number };
|
||||||
workshopType: 'year-review';
|
workshopType: 'year-review';
|
||||||
|
isTeamCollab?: true;
|
||||||
|
canEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WeeklyCheckInSession {
|
interface WeeklyCheckInSession {
|
||||||
@@ -109,6 +115,8 @@ interface WeeklyCheckInSession {
|
|||||||
shares: Share[];
|
shares: Share[];
|
||||||
_count: { items: number };
|
_count: { items: number };
|
||||||
workshopType: 'weekly-checkin';
|
workshopType: 'weekly-checkin';
|
||||||
|
isTeamCollab?: true;
|
||||||
|
canEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WeatherSession {
|
interface WeatherSession {
|
||||||
@@ -122,6 +130,8 @@ interface WeatherSession {
|
|||||||
shares: Share[];
|
shares: Share[];
|
||||||
_count: { entries: number };
|
_count: { entries: number };
|
||||||
workshopType: 'weather';
|
workshopType: 'weather';
|
||||||
|
isTeamCollab?: true;
|
||||||
|
canEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnySession = SwotSession | MotivatorSession | YearReviewSession | WeeklyCheckInSession | WeatherSession;
|
type AnySession = SwotSession | MotivatorSession | YearReviewSession | WeeklyCheckInSession | WeatherSession;
|
||||||
@@ -732,19 +742,16 @@ function SessionCard({ session, isTeamCollab = false }: { session: AnySession; i
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
{isTeamCollab ? (
|
<Link
|
||||||
<div
|
href={href}
|
||||||
className="cursor-default"
|
className={isTeamCollab ? 'cursor-pointer' : ''}
|
||||||
title="Atelier non partagé avec vous – visible en tant qu'admin d'équipe"
|
title={isTeamCollab ? "Atelier de l'équipe – éditable en tant qu'admin" : undefined}
|
||||||
>
|
>
|
||||||
{cardContent}
|
{cardContent}
|
||||||
</div>
|
</Link>
|
||||||
) : (
|
|
||||||
<Link href={href}>{cardContent}</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action buttons - only for owner */}
|
{/* Edit: owner, EDITOR, or team admin | Delete: owner or team admin only (not EDITOR) */}
|
||||||
{session.isOwner && (
|
{(session.isOwner || session.role === 'EDITOR' || session.isTeamCollab) && (
|
||||||
<div className="absolute top-3 right-3 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="absolute top-3 right-3 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -764,6 +771,7 @@ function SessionCard({ session, isTeamCollab = false }: { session: AnySession; i
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
{(session.isOwner || session.isTeamCollab) && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -782,6 +790,7 @@ function SessionCard({ session, isTeamCollab = false }: { session: AnySession; i
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default async function SessionPage({ params }: SessionPageProps) {
|
|||||||
<EditableSessionTitle
|
<EditableSessionTitle
|
||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
initialTitle={session.title}
|
initialTitle={session.title}
|
||||||
isOwner={session.isOwner}
|
canEdit={session.canEdit}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<CollaboratorDisplay
|
<CollaboratorDisplay
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
|||||||
<EditableWeatherTitle
|
<EditableWeatherTitle
|
||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
initialTitle={session.title}
|
initialTitle={session.title}
|
||||||
isOwner={session.isOwner}
|
canEdit={session.canEdit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn
|
|||||||
<EditableWeeklyCheckInTitle
|
<EditableWeeklyCheckInTitle
|
||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
initialTitle={session.title}
|
initialTitle={session.title}
|
||||||
isOwner={session.isOwner}
|
canEdit={session.canEdit}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<CollaboratorDisplay collaborator={session.resolvedParticipant} size="lg" showEmail />
|
<CollaboratorDisplay collaborator={session.resolvedParticipant} size="lg" showEmail />
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio
|
|||||||
<EditableYearReviewTitle
|
<EditableYearReviewTitle
|
||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
initialTitle={session.title}
|
initialTitle={session.title}
|
||||||
isOwner={session.isOwner}
|
canEdit={session.canEdit}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<CollaboratorDisplay collaborator={session.resolvedParticipant} size="lg" showEmail />
|
<CollaboratorDisplay collaborator={session.resolvedParticipant} size="lg" showEmail />
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import { updateMotivatorSession } from '@/actions/moving-motivators';
|
|||||||
interface EditableMotivatorTitleProps {
|
interface EditableMotivatorTitleProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
initialTitle: string;
|
initialTitle: string;
|
||||||
isOwner: boolean;
|
canEdit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditableMotivatorTitle({
|
export function EditableMotivatorTitle({
|
||||||
sessionId,
|
sessionId,
|
||||||
initialTitle,
|
initialTitle,
|
||||||
isOwner,
|
canEdit,
|
||||||
}: EditableMotivatorTitleProps) {
|
}: EditableMotivatorTitleProps) {
|
||||||
return (
|
return (
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
initialTitle={initialTitle}
|
initialTitle={initialTitle}
|
||||||
isOwner={isOwner}
|
canEdit={canEdit}
|
||||||
onUpdate={async (id, title) => {
|
onUpdate={async (id, title) => {
|
||||||
const result = await updateMotivatorSession(id, { title });
|
const result = await updateMotivatorSession(id, { title });
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import { updateSessionTitle } from '@/actions/session';
|
|||||||
interface EditableSessionTitleProps {
|
interface EditableSessionTitleProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
initialTitle: string;
|
initialTitle: string;
|
||||||
isOwner: boolean;
|
canEdit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditableSessionTitle({
|
export function EditableSessionTitle({
|
||||||
sessionId,
|
sessionId,
|
||||||
initialTitle,
|
initialTitle,
|
||||||
isOwner,
|
canEdit,
|
||||||
}: EditableSessionTitleProps) {
|
}: EditableSessionTitleProps) {
|
||||||
return (
|
return (
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
initialTitle={initialTitle}
|
initialTitle={initialTitle}
|
||||||
isOwner={isOwner}
|
canEdit={canEdit}
|
||||||
onUpdate={async (id, title) => {
|
onUpdate={async (id, title) => {
|
||||||
const result = await updateSessionTitle(id, title);
|
const result = await updateSessionTitle(id, title);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { useState, useTransition, useRef, useEffect, useMemo } from 'react';
|
|||||||
interface EditableTitleProps {
|
interface EditableTitleProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
initialTitle: string;
|
initialTitle: string;
|
||||||
isOwner: boolean;
|
canEdit: boolean;
|
||||||
onUpdate: (sessionId: string, title: string) => Promise<{ success: boolean; error?: string }>;
|
onUpdate: (sessionId: string, title: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditableTitle({
|
export function EditableTitle({
|
||||||
sessionId,
|
sessionId,
|
||||||
initialTitle,
|
initialTitle,
|
||||||
isOwner,
|
canEdit,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
}: EditableTitleProps) {
|
}: EditableTitleProps) {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
@@ -65,7 +65,7 @@ export function EditableTitle({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isOwner) {
|
if (!canEdit) {
|
||||||
return <h1 className="text-3xl font-bold text-foreground">{title}</h1>;
|
return <h1 className="text-3xl font-bold text-foreground">{title}</h1>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import { updateWeatherSession } from '@/actions/weather';
|
|||||||
interface EditableWeatherTitleProps {
|
interface EditableWeatherTitleProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
initialTitle: string;
|
initialTitle: string;
|
||||||
isOwner: boolean;
|
canEdit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditableWeatherTitle({
|
export function EditableWeatherTitle({
|
||||||
sessionId,
|
sessionId,
|
||||||
initialTitle,
|
initialTitle,
|
||||||
isOwner,
|
canEdit,
|
||||||
}: EditableWeatherTitleProps) {
|
}: EditableWeatherTitleProps) {
|
||||||
return (
|
return (
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
initialTitle={initialTitle}
|
initialTitle={initialTitle}
|
||||||
isOwner={isOwner}
|
canEdit={canEdit}
|
||||||
onUpdate={async (id, title) => {
|
onUpdate={async (id, title) => {
|
||||||
const result = await updateWeatherSession(id, { title });
|
const result = await updateWeatherSession(id, { title });
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import { updateWeeklyCheckInSession } from '@/actions/weekly-checkin';
|
|||||||
interface EditableWeeklyCheckInTitleProps {
|
interface EditableWeeklyCheckInTitleProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
initialTitle: string;
|
initialTitle: string;
|
||||||
isOwner: boolean;
|
canEdit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditableWeeklyCheckInTitle({
|
export function EditableWeeklyCheckInTitle({
|
||||||
sessionId,
|
sessionId,
|
||||||
initialTitle,
|
initialTitle,
|
||||||
isOwner,
|
canEdit,
|
||||||
}: EditableWeeklyCheckInTitleProps) {
|
}: EditableWeeklyCheckInTitleProps) {
|
||||||
return (
|
return (
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
initialTitle={initialTitle}
|
initialTitle={initialTitle}
|
||||||
isOwner={isOwner}
|
canEdit={canEdit}
|
||||||
onUpdate={async (id, title) => {
|
onUpdate={async (id, title) => {
|
||||||
const result = await updateWeeklyCheckInSession(id, { title });
|
const result = await updateWeeklyCheckInSession(id, { title });
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import { updateYearReviewSession } from '@/actions/year-review';
|
|||||||
interface EditableYearReviewTitleProps {
|
interface EditableYearReviewTitleProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
initialTitle: string;
|
initialTitle: string;
|
||||||
isOwner: boolean;
|
canEdit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditableYearReviewTitle({
|
export function EditableYearReviewTitle({
|
||||||
sessionId,
|
sessionId,
|
||||||
initialTitle,
|
initialTitle,
|
||||||
isOwner,
|
canEdit,
|
||||||
}: EditableYearReviewTitleProps) {
|
}: EditableYearReviewTitleProps) {
|
||||||
return (
|
return (
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
sessionId={sessionId}
|
sessionId={sessionId}
|
||||||
initialTitle={initialTitle}
|
initialTitle={initialTitle}
|
||||||
isOwner={isOwner}
|
canEdit={canEdit}
|
||||||
onUpdate={async (id, title) => {
|
onUpdate={async (id, title) => {
|
||||||
const result = await updateYearReviewSession(id, { title });
|
const result = await updateYearReviewSession(id, { title });
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
import { resolveCollaborator } from '@/services/auth';
|
import { resolveCollaborator } from '@/services/auth';
|
||||||
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
|
||||||
import type { ShareRole, MotivatorType } from '@prisma/client';
|
import type { ShareRole, MotivatorType } from '@prisma/client';
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -104,6 +104,7 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
isOwner: false as const,
|
isOwner: false as const,
|
||||||
role: 'VIEWER' as const,
|
role: 'VIEWER' as const,
|
||||||
isTeamCollab: true as const,
|
isTeamCollab: true as const,
|
||||||
|
canEdit: true as const, // Admin has full rights on team member sessions
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
@@ -115,8 +116,8 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getMotivatorSessionById(sessionId: string, userId: string) {
|
export async function getMotivatorSessionById(sessionId: string, userId: string) {
|
||||||
// Check if user owns the session OR has it shared
|
// Check if user owns the session, has it shared, or is team admin of owner
|
||||||
const session = await prisma.movingMotivatorsSession.findFirst({
|
let session = await prisma.movingMotivatorsSession.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
OR: [
|
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
|
// Determine user's role
|
||||||
const isOwner = session.userId === userId;
|
const isOwner = session.userId === userId;
|
||||||
const share = session.shares.find((s) => s.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 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
|
// Resolve participant to user if it's an email
|
||||||
const resolvedParticipant = await resolveCollaborator(session.participant);
|
const resolvedParticipant = await resolveCollaborator(session.participant);
|
||||||
@@ -151,7 +164,7 @@ export async function getMotivatorSessionById(sessionId: string, userId: string)
|
|||||||
return { ...session, isOwner, role, canEdit, resolvedParticipant };
|
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) {
|
export async function canAccessMotivatorSession(sessionId: string, userId: string) {
|
||||||
const count = await prisma.movingMotivatorsSession.count({
|
const count = await prisma.movingMotivatorsSession.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -159,10 +172,15 @@ export async function canAccessMotivatorSession(sessionId: string, userId: strin
|
|||||||
OR: [{ userId }, { shares: { some: { userId } } }],
|
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) {
|
export async function canEditMotivatorSession(sessionId: string, userId: string) {
|
||||||
const count = await prisma.movingMotivatorsSession.count({
|
const count = await prisma.movingMotivatorsSession.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -170,7 +188,22 @@ export async function canEditMotivatorSession(sessionId: string, userId: string)
|
|||||||
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
|
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[] = [
|
const DEFAULT_MOTIVATOR_TYPES: MotivatorType[] = [
|
||||||
@@ -216,15 +249,21 @@ export async function updateMotivatorSession(
|
|||||||
userId: string,
|
userId: string,
|
||||||
data: { title?: string; participant?: string }
|
data: { title?: string; participant?: string }
|
||||||
) {
|
) {
|
||||||
|
if (!(await canEditMotivatorSession(sessionId, userId))) {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
return prisma.movingMotivatorsSession.updateMany({
|
return prisma.movingMotivatorsSession.updateMany({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId },
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteMotivatorSession(sessionId: string, userId: string) {
|
export async function deleteMotivatorSession(sessionId: string, userId: string) {
|
||||||
|
if (!(await canDeleteMotivatorSession(sessionId, userId))) {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
return prisma.movingMotivatorsSession.deleteMany({
|
return prisma.movingMotivatorsSession.deleteMany({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
import { resolveCollaborator } from '@/services/auth';
|
import { resolveCollaborator } from '@/services/auth';
|
||||||
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
|
||||||
import type { SwotCategory, ShareRole } from '@prisma/client';
|
import type { SwotCategory, ShareRole } from '@prisma/client';
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -111,6 +111,7 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
isOwner: false as const,
|
isOwner: false as const,
|
||||||
role: 'VIEWER' as const,
|
role: 'VIEWER' as const,
|
||||||
isTeamCollab: true as const,
|
isTeamCollab: true as const,
|
||||||
|
canEdit: true as const, // Admin has full rights on team member sessions
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
@@ -122,8 +123,8 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getSessionById(sessionId: string, userId: string) {
|
export async function getSessionById(sessionId: string, userId: string) {
|
||||||
// Check if user owns the session OR has it shared
|
// Check if user owns the session, has it shared, or is team admin of owner
|
||||||
const session = await prisma.session.findFirst({
|
let session = await prisma.session.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
OR: [
|
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
|
// Determine user's role
|
||||||
const isOwner = session.userId === userId;
|
const isOwner = session.userId === userId;
|
||||||
const share = session.shares.find((s) => s.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 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
|
// Resolve collaborator to user if it's an email
|
||||||
const resolvedCollaborator = await resolveCollaborator(session.collaborator);
|
const resolvedCollaborator = await resolveCollaborator(session.collaborator);
|
||||||
@@ -168,7 +186,7 @@ export async function getSessionById(sessionId: string, userId: string) {
|
|||||||
return { ...session, isOwner, role, canEdit, resolvedCollaborator };
|
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) {
|
export async function canAccessSession(sessionId: string, userId: string) {
|
||||||
const count = await prisma.session.count({
|
const count = await prisma.session.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -176,10 +194,15 @@ export async function canAccessSession(sessionId: string, userId: string) {
|
|||||||
OR: [{ userId }, { shares: { some: { userId } } }],
|
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) {
|
export async function canEditSession(sessionId: string, userId: string) {
|
||||||
const count = await prisma.session.count({
|
const count = await prisma.session.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -187,7 +210,22 @@ export async function canEditSession(sessionId: string, userId: string) {
|
|||||||
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
|
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 }) {
|
export async function createSession(userId: string, data: { title: string; collaborator: string }) {
|
||||||
@@ -204,15 +242,21 @@ export async function updateSession(
|
|||||||
userId: string,
|
userId: string,
|
||||||
data: { title?: string; collaborator?: string }
|
data: { title?: string; collaborator?: string }
|
||||||
) {
|
) {
|
||||||
|
if (!(await canEditSession(sessionId, userId))) {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
return prisma.session.updateMany({
|
return prisma.session.updateMany({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId },
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteSession(sessionId: string, userId: string) {
|
export async function deleteSession(sessionId: string, userId: string) {
|
||||||
|
if (!(await canDeleteSession(sessionId, userId))) {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
return prisma.session.deleteMany({
|
return prisma.session.deleteMany({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<boolean> {
|
||||||
|
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). */
|
/** Returns user IDs of all members in teams where the given user is ADMIN (excluding self). */
|
||||||
export async function getTeamMemberIdsForAdminTeams(userId: string): Promise<string[]> {
|
export async function getTeamMemberIdsForAdminTeams(userId: string): Promise<string[]> {
|
||||||
const adminTeams = await prisma.teamMember.findMany({
|
const adminTeams = await prisma.teamMember.findMany({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
|
||||||
import { getWeekBounds } from '@/lib/date-utils';
|
import { getWeekBounds } from '@/lib/date-utils';
|
||||||
import type { ShareRole } from '@prisma/client';
|
import type { ShareRole } from '@prisma/client';
|
||||||
|
|
||||||
@@ -96,12 +96,13 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
isOwner: false as const,
|
isOwner: false as const,
|
||||||
role: 'VIEWER' as const,
|
role: 'VIEWER' as const,
|
||||||
isTeamCollab: true 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) {
|
export async function getWeatherSessionById(sessionId: string, userId: string) {
|
||||||
// Check if user owns the session OR has it shared
|
// Check if user owns the session, has it shared, or is team admin of owner
|
||||||
const session = await prisma.weatherSession.findFirst({
|
let session = await prisma.weatherSession.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
OR: [
|
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
|
// Determine user's role
|
||||||
const isOwner = session.userId === userId;
|
const isOwner = session.userId === userId;
|
||||||
const share = session.shares.find((s) => s.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 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 };
|
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) {
|
export async function canAccessWeatherSession(sessionId: string, userId: string) {
|
||||||
const count = await prisma.weatherSession.count({
|
const count = await prisma.weatherSession.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -144,10 +160,15 @@ export async function canAccessWeatherSession(sessionId: string, userId: string)
|
|||||||
OR: [{ userId }, { shares: { some: { userId } } }],
|
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) {
|
export async function canEditWeatherSession(sessionId: string, userId: string) {
|
||||||
const count = await prisma.weatherSession.count({
|
const count = await prisma.weatherSession.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -155,7 +176,22 @@ export async function canEditWeatherSession(sessionId: string, userId: string) {
|
|||||||
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
|
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 }) {
|
export async function createWeatherSession(userId: string, data: { title: string; date?: Date }) {
|
||||||
@@ -180,15 +216,21 @@ export async function updateWeatherSession(
|
|||||||
userId: string,
|
userId: string,
|
||||||
data: { title?: string; date?: Date }
|
data: { title?: string; date?: Date }
|
||||||
) {
|
) {
|
||||||
|
if (!(await canEditWeatherSession(sessionId, userId))) {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
return prisma.weatherSession.updateMany({
|
return prisma.weatherSession.updateMany({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId },
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteWeatherSession(sessionId: string, userId: string) {
|
export async function deleteWeatherSession(sessionId: string, userId: string) {
|
||||||
|
if (!(await canDeleteWeatherSession(sessionId, userId))) {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
return prisma.weatherSession.deleteMany({
|
return prisma.weatherSession.deleteMany({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
import { resolveCollaborator } from '@/services/auth';
|
import { resolveCollaborator } from '@/services/auth';
|
||||||
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
|
||||||
import type { ShareRole, WeeklyCheckInCategory, Emotion } from '@prisma/client';
|
import type { ShareRole, WeeklyCheckInCategory, Emotion } from '@prisma/client';
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -104,6 +104,7 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
isOwner: false as const,
|
isOwner: false as const,
|
||||||
role: 'VIEWER' as const,
|
role: 'VIEWER' as const,
|
||||||
isTeamCollab: true as const,
|
isTeamCollab: true as const,
|
||||||
|
canEdit: true as const, // Admin has full rights on team member sessions
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
@@ -115,8 +116,8 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getWeeklyCheckInSessionById(sessionId: string, userId: string) {
|
export async function getWeeklyCheckInSessionById(sessionId: string, userId: string) {
|
||||||
// Check if user owns the session OR has it shared
|
// Check if user owns the session, has it shared, or is team admin of owner
|
||||||
const session = await prisma.weeklyCheckInSession.findFirst({
|
let session = await prisma.weeklyCheckInSession.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
OR: [
|
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
|
// Determine user's role
|
||||||
const isOwner = session.userId === userId;
|
const isOwner = session.userId === userId;
|
||||||
const share = session.shares.find((s) => s.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 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
|
// Resolve participant to user if it's an email
|
||||||
const resolvedParticipant = await resolveCollaborator(session.participant);
|
const resolvedParticipant = await resolveCollaborator(session.participant);
|
||||||
@@ -151,7 +164,7 @@ export async function getWeeklyCheckInSessionById(sessionId: string, userId: str
|
|||||||
return { ...session, isOwner, role, canEdit, resolvedParticipant };
|
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) {
|
export async function canAccessWeeklyCheckInSession(sessionId: string, userId: string) {
|
||||||
const count = await prisma.weeklyCheckInSession.count({
|
const count = await prisma.weeklyCheckInSession.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -159,10 +172,15 @@ export async function canAccessWeeklyCheckInSession(sessionId: string, userId: s
|
|||||||
OR: [{ userId }, { shares: { some: { userId } } }],
|
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) {
|
export async function canEditWeeklyCheckInSession(sessionId: string, userId: string) {
|
||||||
const count = await prisma.weeklyCheckInSession.count({
|
const count = await prisma.weeklyCheckInSession.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -170,7 +188,22 @@ export async function canEditWeeklyCheckInSession(sessionId: string, userId: str
|
|||||||
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
|
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(
|
export async function createWeeklyCheckInSession(
|
||||||
@@ -196,15 +229,21 @@ export async function updateWeeklyCheckInSession(
|
|||||||
userId: string,
|
userId: string,
|
||||||
data: { title?: string; participant?: string; date?: Date }
|
data: { title?: string; participant?: string; date?: Date }
|
||||||
) {
|
) {
|
||||||
|
if (!(await canEditWeeklyCheckInSession(sessionId, userId))) {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
return prisma.weeklyCheckInSession.updateMany({
|
return prisma.weeklyCheckInSession.updateMany({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId },
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteWeeklyCheckInSession(sessionId: string, userId: string) {
|
export async function deleteWeeklyCheckInSession(sessionId: string, userId: string) {
|
||||||
|
if (!(await canDeleteWeeklyCheckInSession(sessionId, userId))) {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
return prisma.weeklyCheckInSession.deleteMany({
|
return prisma.weeklyCheckInSession.deleteMany({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { prisma } from '@/services/database';
|
import { prisma } from '@/services/database';
|
||||||
import { resolveCollaborator } from '@/services/auth';
|
import { resolveCollaborator } from '@/services/auth';
|
||||||
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
|
||||||
import type { ShareRole, YearReviewCategory } from '@prisma/client';
|
import type { ShareRole, YearReviewCategory } from '@prisma/client';
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -104,6 +104,7 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
isOwner: false as const,
|
isOwner: false as const,
|
||||||
role: 'VIEWER' as const,
|
role: 'VIEWER' as const,
|
||||||
isTeamCollab: true as const,
|
isTeamCollab: true as const,
|
||||||
|
canEdit: true as const, // Admin has full rights on team member sessions
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
@@ -115,8 +116,8 @@ export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getYearReviewSessionById(sessionId: string, userId: string) {
|
export async function getYearReviewSessionById(sessionId: string, userId: string) {
|
||||||
// Check if user owns the session OR has it shared
|
// Check if user owns the session, has it shared, or is team admin of owner
|
||||||
const session = await prisma.yearReviewSession.findFirst({
|
let session = await prisma.yearReviewSession.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
OR: [
|
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
|
// Determine user's role
|
||||||
const isOwner = session.userId === userId;
|
const isOwner = session.userId === userId;
|
||||||
const share = session.shares.find((s) => s.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 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
|
// Resolve participant to user if it's an email
|
||||||
const resolvedParticipant = await resolveCollaborator(session.participant);
|
const resolvedParticipant = await resolveCollaborator(session.participant);
|
||||||
@@ -151,7 +164,7 @@ export async function getYearReviewSessionById(sessionId: string, userId: string
|
|||||||
return { ...session, isOwner, role, canEdit, resolvedParticipant };
|
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) {
|
export async function canAccessYearReviewSession(sessionId: string, userId: string) {
|
||||||
const count = await prisma.yearReviewSession.count({
|
const count = await prisma.yearReviewSession.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -159,10 +172,15 @@ export async function canAccessYearReviewSession(sessionId: string, userId: stri
|
|||||||
OR: [{ userId }, { shares: { some: { userId } } }],
|
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) {
|
export async function canEditYearReviewSession(sessionId: string, userId: string) {
|
||||||
const count = await prisma.yearReviewSession.count({
|
const count = await prisma.yearReviewSession.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -170,7 +188,22 @@ export async function canEditYearReviewSession(sessionId: string, userId: string
|
|||||||
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
|
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(
|
export async function createYearReviewSession(
|
||||||
@@ -195,15 +228,21 @@ export async function updateYearReviewSession(
|
|||||||
userId: string,
|
userId: string,
|
||||||
data: { title?: string; participant?: string; year?: number }
|
data: { title?: string; participant?: string; year?: number }
|
||||||
) {
|
) {
|
||||||
|
if (!(await canEditYearReviewSession(sessionId, userId))) {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
return prisma.yearReviewSession.updateMany({
|
return prisma.yearReviewSession.updateMany({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId },
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteYearReviewSession(sessionId: string, userId: string) {
|
export async function deleteYearReviewSession(sessionId: string, userId: string) {
|
||||||
|
if (!(await canDeleteYearReviewSession(sessionId, userId))) {
|
||||||
|
return { count: 0 };
|
||||||
|
}
|
||||||
return prisma.yearReviewSession.deleteMany({
|
return prisma.yearReviewSession.deleteMany({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user