[Calendar] Introduce permissions for editing events
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import fs from 'fs'
|
||||
|
||||
export async function setEventTime(eventId, startHour, startMin, endHour, endMin, month, day, year = 2026) {
|
||||
const start = new Date(`${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}T${String(startHour).padStart(2,'0')}:${String(startMin).padStart(2,'0')}:00-05:00`)
|
||||
const end = new Date(`${year}-${String(month).padStart(2,'0')}-${String(day).padStart(2,'0')}T${String(endHour).padStart(2,'0')}:${String(endMin).padStart(2,'0')}:00-05:00`)
|
||||
|
||||
console.log(start, end)
|
||||
await this.sql`
|
||||
await context.sql`
|
||||
UPDATE events.events
|
||||
SET time_start = ${start}, time_end = ${end}
|
||||
WHERE id = ${eventId}
|
||||
@@ -22,7 +23,7 @@ export async function getEventsByNetwork(networkId) {
|
||||
}
|
||||
|
||||
// Returns each event with its linked calendar IDs, file attachments, and recurrence rule
|
||||
return await this.sql`
|
||||
return await context.sql`
|
||||
SELECT e.*,
|
||||
COALESCE(jsonb_agg(DISTINCT ec.calendar_id) FILTER (WHERE ec.calendar_id IS NOT NULL), '[]') AS calendars,
|
||||
COALESCE(jsonb_agg(DISTINCT jsonb_build_object('id', f.id, 'name', f.name, 'type', f.type, 'original_name', f.original_name, 'size_bytes', f.size_bytes)) FILTER (WHERE f.id IS NOT NULL), '[]') AS attachments,
|
||||
@@ -42,22 +43,23 @@ export async function getCalendarsByNetwork(networkId) {
|
||||
throw new global.ServerError(400, "Invalid network ID!");
|
||||
}
|
||||
|
||||
return await this.sql`
|
||||
return await context.sql`
|
||||
SELECT c.* FROM events.calendars c
|
||||
WHERE c.network_id = ${networkId}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function addCalendar(userId, newCalendar, networkId) {
|
||||
export async function addCalendar(newCalendar, networkId) {
|
||||
//@ should have scoping here
|
||||
const { name, description, color } = newCalendar;
|
||||
try {
|
||||
let insertedCalendar;
|
||||
|
||||
await this.sql.begin(async tx => {
|
||||
await context.sql.begin(async tx => {
|
||||
const [calendar] = await tx`
|
||||
INSERT INTO events.calendars ${tx({
|
||||
network_id: networkId,
|
||||
owner_id: userId,
|
||||
owner_id: context.userId,
|
||||
name: name,
|
||||
description: description ?? null,
|
||||
color: color
|
||||
@@ -74,10 +76,10 @@ export async function addCalendar(userId, newCalendar, networkId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function editCalendar(userId, id, updatedCalendar, networkId) {
|
||||
export async function editCalendar(id, updatedCalendar, networkId) {
|
||||
const { name, description, color } = updatedCalendar;
|
||||
try {
|
||||
const [calendar] = await this.sql`
|
||||
const [calendar] = await context.sql`
|
||||
UPDATE events.calendars
|
||||
SET
|
||||
name = ${name},
|
||||
@@ -87,7 +89,8 @@ export async function editCalendar(userId, id, updatedCalendar, networkId) {
|
||||
WHERE
|
||||
id = ${id}
|
||||
AND network_id = ${networkId}
|
||||
AND owner_id = ${userId}
|
||||
--@ Calendar editing is owner-only on the backend.
|
||||
AND owner_id = ${context.userId}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
@@ -99,11 +102,12 @@ export async function editCalendar(userId, id, updatedCalendar, networkId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCalendar(userId, id, networkId) {
|
||||
export async function deleteCalendar(id, networkId) {
|
||||
try {
|
||||
const [calendar] = await this.sql`
|
||||
//@ Calendar deletion is owner-only on the backend.
|
||||
const [calendar] = await context.sql`
|
||||
DELETE FROM events.calendars
|
||||
WHERE id = ${id} AND network_id = ${networkId} AND owner_id = ${userId}
|
||||
WHERE id = ${id} AND network_id = ${networkId} AND owner_id = ${context.userId}
|
||||
RETURNING *
|
||||
`;
|
||||
if (!calendar) return { status: 403, error: "Calendar not found or not authorized" };
|
||||
@@ -114,12 +118,12 @@ export async function deleteCalendar(userId, id, networkId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function addEvent(userId, newEvent, networkId) {
|
||||
export async function addEvent(newEvent, networkId) {
|
||||
const { title, description, location, time_start, time_end, all_day, calendars, recurrence } = newEvent;
|
||||
try {
|
||||
let insertedEvent;
|
||||
|
||||
await this.sql.begin(async tx => {
|
||||
await context.sql.begin(async tx => {
|
||||
let recurrenceId = null;
|
||||
if (recurrence) {
|
||||
const [rule] = await tx`
|
||||
@@ -137,7 +141,7 @@ export async function addEvent(userId, newEvent, networkId) {
|
||||
const [event] = await tx`
|
||||
INSERT INTO events.events ${tx({
|
||||
network_id: networkId,
|
||||
creator_id: userId,
|
||||
creator_id: context.userId,
|
||||
title,
|
||||
description: description ?? null,
|
||||
location: location ?? null,
|
||||
@@ -162,18 +166,30 @@ export async function addEvent(userId, newEvent, networkId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function networkFromEvent(eventId) {
|
||||
return await context.sql`
|
||||
SELECT c.network_id
|
||||
FROM events.event_calendars ec
|
||||
JOIN events.calendars c ON c.id = ec.calendar_id
|
||||
WHERE ec.event_id = ${eventId};
|
||||
`
|
||||
}
|
||||
|
||||
export async function addAttachments(userId, eventId, files) {
|
||||
const [event] = await this.sql`
|
||||
let results = await networkFromEvent(eventId)
|
||||
if(!(await global.permissions.canDo("events.edit", userId, results[0].network_id))) return { status: 403, error: "Forbidden" };
|
||||
const [event] = await context.sql`
|
||||
SELECT id FROM events.events
|
||||
WHERE id = ${eventId} AND creator_id = ${userId}
|
||||
`;
|
||||
|
||||
if (!event) throw new global.ServerError(403, "Not authorized to add attachments to this event");
|
||||
|
||||
try {
|
||||
let insertedFiles = [];
|
||||
|
||||
// Will rollback changes if it encounters an error
|
||||
await this.sql.begin(async tx => {
|
||||
await context.sql.begin(async tx => {
|
||||
insertedFiles = await Promise.all(
|
||||
files.map(file =>
|
||||
tx`
|
||||
@@ -207,13 +223,16 @@ export async function addAttachments(userId, eventId, files) {
|
||||
}
|
||||
|
||||
export async function deleteAttachment(userId, fileId, eventId) {
|
||||
const [event] = await this.sql`
|
||||
let results = await networkFromEvent(eventId)
|
||||
if(!(await global.permissions.canDo("events.edit", userId, results[0].network_id))) return { status: 403, error: "Forbidden" };
|
||||
|
||||
const [event] = await context.sql`
|
||||
SELECT id FROM events.events
|
||||
WHERE id = ${eventId} AND creator_id = ${userId}
|
||||
`;
|
||||
if (!event) throw new global.ServerError(403, "Not authorized to modify this event");
|
||||
|
||||
const [file] = await this.sql`
|
||||
const [file] = await context.sql`
|
||||
SELECT f.* FROM public.files f
|
||||
JOIN public.file_to_event fte ON fte.file_id = f.id
|
||||
WHERE f.id = ${fileId} AND fte.event_id = ${eventId}
|
||||
@@ -222,7 +241,7 @@ export async function deleteAttachment(userId, fileId, eventId) {
|
||||
|
||||
try {
|
||||
let deletedFile = null;
|
||||
await this.sql.begin(async tx => {
|
||||
await context.sql.begin(async tx => {
|
||||
await tx`DELETE FROM public.file_to_event WHERE file_id = ${fileId} AND event_id = ${eventId}`;
|
||||
// Only remove the file record (and trigger disk unlink) if no other event still references it
|
||||
const [stillReferenced] = await tx`SELECT 1 FROM public.file_to_event WHERE file_id = ${fileId} LIMIT 1`;
|
||||
@@ -238,16 +257,17 @@ export async function deleteAttachment(userId, fileId, eventId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function editEvent(userId, id, updatedEvent, networkId) {
|
||||
export async function editEvent(id, updatedEvent, networkId) {
|
||||
if(!(await global.permissions.canDo("events.edit", context.userId, networkId))) return { status: 403, error: "Forbidden" };
|
||||
const { title, description, location, time_start, time_end, all_day, calendars, recurrence, scope, exception_date } = updatedEvent;
|
||||
|
||||
try {
|
||||
let editedEvent;
|
||||
|
||||
await this.sql.begin(async tx => {
|
||||
await context.sql.begin(async tx => {
|
||||
const [current] = await tx`
|
||||
SELECT * FROM events.events
|
||||
WHERE id = ${id} AND network_id = ${networkId} AND creator_id = ${userId}
|
||||
WHERE id = ${id} AND network_id = ${networkId}
|
||||
`;
|
||||
if (!current) return;
|
||||
|
||||
@@ -299,7 +319,7 @@ export async function editEvent(userId, id, updatedEvent, networkId) {
|
||||
all_day = ${all_day},
|
||||
recurrence_id = ${newRecurrenceId},
|
||||
updated_at = NOW()
|
||||
WHERE id = ${id} AND network_id = ${networkId} AND creator_id = ${userId}
|
||||
WHERE id = ${id} AND network_id = ${networkId} AND creator_id = ${context.userId}
|
||||
RETURNING *
|
||||
`;
|
||||
if (!event) return;
|
||||
@@ -321,7 +341,7 @@ export async function editEvent(userId, id, updatedEvent, networkId) {
|
||||
const [override] = await tx`
|
||||
INSERT INTO events.events ${tx({
|
||||
network_id: networkId,
|
||||
creator_id: userId,
|
||||
creator_id: context.userId,
|
||||
title,
|
||||
description: description ?? null,
|
||||
location: location ?? null,
|
||||
@@ -410,7 +430,7 @@ export async function editEvent(userId, id, updatedEvent, networkId) {
|
||||
const [newEvent] = await tx`
|
||||
INSERT INTO events.events ${tx({
|
||||
network_id: networkId,
|
||||
creator_id: userId,
|
||||
creator_id: context.userId,
|
||||
title,
|
||||
description: description ?? null,
|
||||
location: location ?? null,
|
||||
@@ -453,11 +473,12 @@ export async function editEvent(userId, id, updatedEvent, networkId) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteEvent(userId, eventId, networkId, scope, exception_date) {
|
||||
export async function deleteEvent(eventId, networkId, scope, exception_date) {
|
||||
if(!global.permissions.canDo("events.delete", context.userId, networkId)) return { status: 403, error: "Forbidden" };
|
||||
try {
|
||||
const [event] = await this.sql`
|
||||
const [event] = await context.sql`
|
||||
SELECT * FROM events.events
|
||||
WHERE id = ${eventId} AND network_id = ${networkId} AND creator_id = ${userId}
|
||||
WHERE id = ${eventId} AND network_id = ${networkId}
|
||||
`;
|
||||
if (!event) return { status: 403, error: "Event not found or not authorized" };
|
||||
|
||||
@@ -466,16 +487,16 @@ export async function deleteEvent(userId, eventId, networkId, scope, exception_d
|
||||
|
||||
if (isOverride) {
|
||||
// Already a concrete row — just mark it cancelled
|
||||
await this.sql`UPDATE events.events SET is_cancelled = true WHERE id = ${eventId}`;
|
||||
await context.sql`UPDATE events.events SET is_cancelled = true WHERE id = ${eventId}`;
|
||||
return { status: 200 };
|
||||
}
|
||||
|
||||
if (isRecurring && scope === 'single') {
|
||||
// Virtual occurrence — insert a cancelled placeholder so it gets skipped
|
||||
await this.sql`
|
||||
INSERT INTO events.events ${this.sql({
|
||||
await context.sql`
|
||||
INSERT INTO events.events ${context.sql({
|
||||
network_id: networkId,
|
||||
creator_id: userId,
|
||||
creator_id: context.userId,
|
||||
title: event.title,
|
||||
time_start: exception_date,
|
||||
time_end: exception_date,
|
||||
@@ -490,16 +511,16 @@ export async function deleteEvent(userId, eventId, networkId, scope, exception_d
|
||||
|
||||
if (isRecurring && scope === 'future' && new Date(exception_date) > new Date(event.time_start)) {
|
||||
// Capture current end_date before modifying — marks the boundary of independent later splits
|
||||
const [existingRule] = await this.sql`SELECT end_date FROM events.event_recurrence WHERE id = ${event.recurrence_id}`;
|
||||
const [existingRule] = await context.sql`SELECT end_date FROM events.event_recurrence WHERE id = ${event.recurrence_id}`;
|
||||
const oldEndDate = existingRule?.end_date ?? null;
|
||||
|
||||
// Cap the series at this date
|
||||
await this.sql`
|
||||
await context.sql`
|
||||
UPDATE events.event_recurrence SET end_date = ${exception_date}, updated_at = NOW()
|
||||
WHERE id = ${event.recurrence_id}
|
||||
`;
|
||||
// Promote future non-cancelled overrides (single-event edits) to standalone events
|
||||
await this.sql`
|
||||
await context.sql`
|
||||
UPDATE events.events
|
||||
SET recurrence_parent_id = NULL, recurrence_exception_date = NULL, updated_at = NOW()
|
||||
WHERE recurrence_parent_id = ${eventId}
|
||||
@@ -507,24 +528,24 @@ export async function deleteEvent(userId, eventId, networkId, scope, exception_d
|
||||
AND (is_cancelled IS NULL OR is_cancelled = false)
|
||||
`;
|
||||
// Drop future cancelled placeholders — they are meaningless without the series
|
||||
await this.sql`
|
||||
await context.sql`
|
||||
DELETE FROM events.events
|
||||
WHERE recurrence_parent_id = ${eventId} AND recurrence_exception_date >= ${exception_date}
|
||||
AND is_cancelled = true
|
||||
`;
|
||||
// Remove intermediate descendant templates in [exception_date, oldEndDate); independent splits at/beyond oldEndDate are preserved
|
||||
const descendants = await this.sql`
|
||||
const descendants = await context.sql`
|
||||
SELECT id, recurrence_id FROM events.events
|
||||
WHERE parent_template_id = ${eventId}
|
||||
AND time_start >= ${exception_date}
|
||||
${oldEndDate ? this.sql`AND time_start < ${oldEndDate}` : this.sql``}
|
||||
${oldEndDate ? context.sql`AND time_start < ${oldEndDate}` : context.sql``}
|
||||
`;
|
||||
if (descendants.length > 0) {
|
||||
const descIds = descendants.map(d => d.id);
|
||||
const descRuleIds = descendants.map(d => d.recurrence_id).filter(Boolean);
|
||||
await this.sql`DELETE FROM events.events WHERE id = ANY(${descIds})`;
|
||||
await context.sql`DELETE FROM events.events WHERE id = ANY(${descIds})`;
|
||||
if (descRuleIds.length > 0) {
|
||||
await this.sql`DELETE FROM events.event_recurrence WHERE id = ANY(${descRuleIds})`;
|
||||
await context.sql`DELETE FROM events.event_recurrence WHERE id = ANY(${descRuleIds})`;
|
||||
}
|
||||
}
|
||||
return { status: 200 };
|
||||
@@ -533,7 +554,7 @@ export async function deleteEvent(userId, eventId, networkId, scope, exception_d
|
||||
// Delete the whole event/series — collect files from the template AND all overrides,
|
||||
// but only those not shared with surviving events (e.g. a 'this and future' split
|
||||
// inherits the same file_ids; deleting the physical file would orphan that series)
|
||||
const files = await this.sql`
|
||||
const files = await context.sql`
|
||||
SELECT DISTINCT f.* FROM public.files f
|
||||
JOIN public.file_to_event fte ON fte.file_id = f.id
|
||||
JOIN events.events e ON e.id = fte.event_id
|
||||
@@ -548,7 +569,7 @@ export async function deleteEvent(userId, eventId, networkId, scope, exception_d
|
||||
`;
|
||||
const recurrenceId = event.recurrence_id;
|
||||
|
||||
await this.sql.begin(async tx => {
|
||||
await context.sql.begin(async tx => {
|
||||
// Promote non-cancelled overrides (single-event edits) to standalone events before deleting the template
|
||||
await tx`
|
||||
UPDATE events.events
|
||||
|
||||
Reference in New Issue
Block a user